Personalize this lesson
Adapt explanations and teaching visuals to your background and preferred voice.
Compressed embeddings can retrieve the right policy evidence and still leave the assistant with a decision to make. Retrieval isn't the end of the task. An assistant may have the right facts and still choose the wrong action because it skips a condition, mishandles arithmetic, or commits too early to one plan.
Treat extra inference work as an engineering decision. You'll build a small release-resolution controller for a risky code change:
- Chain-of-Thought (CoT): request one decomposed candidate decision.
- Self-Consistency: sample several candidates, normalize final actions, and vote.
- Tree-of-Thoughts (ToT): expand and prune branches when a decision needs backtracking.
Long rationales aren't the target. The target is better measurable decision accuracy under a token, latency, and safety budget.
Start with a failure you can audit
Suppose a repository has a candidate release. Unit tests pass, the security scan is clear, and a reviewer has approved the diff. Policy permits auto-merge only when all three facts are true. A direct response may still overlook one condition and suggest a force-merge or escalation unnecessarily.
A useful outward artifact is a short decision record: facts used, checks applied, and final action. It's smaller than an open-ended rationale, easy to score, and safe to compare against deterministic policy logic.
1from dataclasses import dataclass
2
3@dataclass(frozen=True)
4class ReleaseCase:
5 tests_passed: bool
6 security_scan_clear: bool
7 reviewer_approved: bool
8
9def decision_record(case: ReleaseCase) -> dict[str, object]:
10 checks = {
11 "tests_passed": case.tests_passed,
12 "security_scan_clear": case.security_scan_clear,
13 "reviewer_approved": case.reviewer_approved,
14 }
15 action = "merge_release" if all(checks.values()) else "manual_review"
16 return {"checks": checks, "final_action": action}
17
18record = decision_record(
19 ReleaseCase(tests_passed=True, security_scan_clear=True, reviewer_approved=True)
20)
21for name, passed in record["checks"].items():
22 print(f"{name}: {passed}")
23print(f"final_action: {record['final_action']}")1tests_passed: True
2security_scan_clear: True
3reviewer_approved: True
4final_action: merge_releaseThis code isn't an LLM. It's the oracle that your prompt variants must match. Before increasing model compute, define an output contract and a scorer.
When to skip CoT entirely: if the inputs are already structured machine state (booleans, enum fields, policy predicates the runtime owns), evaluate the policy in code. Sampling five CoT traces over three known flags adds cost without evidence. Use LLM reasoning only over unstructured evidence (diff text, free-form review comments, incident narratives) where there is no deterministic scorer yet. The fixtures below teach scoring and controllers; they aren't a product recommendation to put
all(checks)behind a model.

Why define an oracle and decision record before testing reasoning prompts?
Answer
Without a scored outcome and required checks, a longer response can look thoughtful without being more correct. The oracle lets you compare strategies on the same task.
Chain-of-Thought: one decomposed candidate
Wei et al. introduced few-shot CoT by placing worked intermediate steps in prompt examples. Their experiments found gains on arithmetic, commonsense, and symbolic reasoning benchmarks for sufficiently large models, including a strong GSM8K result with PaLM 540B.[1] Kojima et al. later showed that the zero-shot trigger "Let's think step by step" could improve several benchmark reasoning tasks without worked examples.[2]
Those papers establish techniques to evaluate, not a production law. On your endpoint and task, a visible scratchpad may help, do nothing, or add cost. If an API offers a native reasoning control, evaluate that option as another strategy rather than assuming that extra visible text helps.
🔬 Research insight: More reasoning text isn't always better. Zheng et al. tested CoT and its variants (including ToT and ReAct) on nine pattern-based in-context-learning benchmarks across 16 models and found they consistently underperformed plain direct answering, with the gap widening as more demonstrations were added. Even long-CoT reasoning models that spent far more tokens didn't overcome the effect.[3] When a task is really pattern matching from examples, a visible scratchpad can add noise instead of signal. That's the point of the eval gate: measure the win, don't assume it.
This tradeoff also shapes a build-versus-buy decision you'll face in production. You can orchestrate reasoning yourself with prompt scaffolding (the CoT, self-consistency, and tree-search controllers in this chapter), or call a native reasoning model that performs its own internal reasoning before answering. Scaffolding keeps each step visible, auditable, and cheap to swap, but you own the token and latency budget. A native reasoning model can lift accuracy on genuinely multi-step problems, yet it hides its reasoning, bills you for it, and gives you fewer control points to gate. Treat the two as competing candidates behind the same eval gate rather than assuming the newer option wins.
For a production workflow, don't ask the model to reveal unrestricted inner reasoning. Ask it to produce reviewable artifacts:
1Use only the supplied release facts and policy rules.
2Return:
31. required_checks: each policy predicate with pass/fail
42. final_action: one allowed action enum
53. operator_message: one sentence
6
7Facts:
8- tests_passed: true
9- security_scan_clear: true
10- reviewer_approved: true
11
12Policy:
13- merge_release is allowed only when tests pass,
14 the security scan is clear, and review approval exists.The checks are useful because a missed predicate becomes observable. They aren't proof of faithful hidden cognition. Turpin et al. showed that CoT explanations can rationalize outputs influenced by hidden biasing features without mentioning those features.[4] Log inputs, actions, validations, and outcomes; don't treat eloquent reasoning text as an audit guarantee.
1from dataclasses import dataclass
2
3@dataclass(frozen=True)
4class Candidate:
5 name: str
6 checks: set[str]
7 final_action: str
8
9required_checks = {
10 "tests_passed",
11 "security_scan_clear",
12 "reviewer_approved",
13}
14expected_action = "merge_release"
15candidates = [
16 Candidate("direct", {"tests_passed"}, "manual_review"),
17 Candidate("structured_trace", required_checks, "merge_release"),
18]
19
20for candidate in candidates:
21 coverage = len(candidate.checks & required_checks) / len(required_checks)
22 action_ok = candidate.final_action == expected_action
23 print(f"{candidate.name}: coverage={coverage:.0%}, action_ok={action_ok}")1direct: coverage=33%, action_ok=False
2structured_trace: coverage=100%, action_ok=TrueHere, the trace is useful because it meets the scored contract. It doesn't win merely because it contains more words.
Zero-shot or few-shot?
Zero-shot CoT supplies an instruction and lets the model choose its intermediate format. Few-shot CoT gives one or more solved examples, so it can teach both decomposition and the final answer shape. Native structured output enforcement is better when available; examples are still useful when the prompt must communicate task-specific checks.

1example = """Example:
2facts: tests_passed=true, security_scan_clear=false, reviewer_approved=true
3required_checks: tests_passed=true, security_scan_clear=false, reviewer_approved=true
4final_action: manual_review"""
5
6case = "facts: tests_passed=true, security_scan_clear=true, reviewer_approved=true"
7zero_shot = f"Evaluate release policy step by step.\n{case}\nfinal_action:"
8few_shot = f"{example}\n\nNow evaluate:\n{case}\nrequired_checks:\nfinal_action:"
9
10print(f"zero_shot_has_example: {'Example:' in zero_shot}")
11print(f"few_shot_has_example: {'Example:' in few_shot}")
12print(f"few_shot_requests_checks: {'required_checks:' in few_shot}")1zero_shot_has_example: False
2few_shot_has_example: True
3few_shot_requests_checks: TrueWhen is few-shot CoT worth more prompt tokens than zero-shot CoT?
Answer
When examples teach task-specific checks or output fields that your evaluator needs, and held-out results justify the added tokens.
Self-Consistency: sample answers, then vote
One structured trace can fail because generation takes an unlucky path. Self-Consistency replaces reliance on one path with several sampled paths and chooses the most consistent final answer.[5] The vote operates on extracted answers, not on whose rationale sounds best.
To obtain different candidates, use stochastic decoding, usually with a nonzero temperature, rather than rerunning one deterministic decode. The earlier Decoding Strategies lesson explains that control. Keep it fixed in your experiment and log it with the sample count.
On the original benchmark setting, Wang et al. reported a 17.9 percentage-point GSM8K improvement over CoT prompting for PaLM 540B.[5] That number is evidence for the method on those benchmarks. It's not your expected support-resolution gain. Measure your own cases and sample cost.

Normalize before counting
Model outputs rarely use exactly one spelling. Your controller should map harmless variants to one allowed action and reject unknown outputs before voting.
1from collections import Counter
2
3ALIASES = {
4 "merge release": "merge_release",
5 "merge_release": "merge_release",
6 "auto merge approved change": "merge_release",
7 "manual review": "manual_review",
8}
9
10def canonicalize(text: str) -> str | None:
11 normalized = text.strip().lower().replace("-", " ")
12 return ALIASES.get(normalized)
13
14samples = [
15 "Merge release",
16 "merge_release",
17 "Auto merge approved change",
18 "manual review",
19 "force merge immediately",
20]
21votes = Counter(action for text in samples if (action := canonicalize(text)))
22winner = votes.most_common(1)[0][0] if votes else "manual_review"
23
24print(f"accepted_samples: {sum(votes.values())}/{len(samples)}")
25print(f"votes: {dict(votes)}")
26print(f"winner: {winner}")1accepted_samples: 4/5
2votes: {'merge_release': 3, 'manual_review': 1}
3winner: merge_releaseA winner isn't always confident enough
A 2 to 2 tie, zero accepted outputs, or a narrow plurality with many rejected outputs shouldn't silently become an automated action. Add an abstention rule. The winning share must use all sampled outputs as its denominator, including strings that failed to parse.
1from collections import Counter
2
3def decide(votes: list[str], total_samples: int, minimum_share: float = 0.6) -> str:
4 if not votes:
5 return "manual_review"
6 counts = Counter(votes)
7 winner, count = counts.most_common(1)[0]
8 share = count / total_samples
9 tied = len(counts) > 1 and counts.most_common(2)[0][1] == counts.most_common(2)[1][1]
10 if tied or share < minimum_share:
11 return "manual_review"
12 return winner
13
14strong = ["merge_release", "merge_release", "merge_release", "manual_review"]
15split = ["merge_release", "merge_release", "manual_review", "manual_review"]
16mostly_rejected = ["merge_release", "merge_release"]
17
18print(f"strong_vote: {decide(strong, total_samples=4)}")
19print(f"split_vote: {decide(split, total_samples=4)}")
20print(f"mostly_rejected_vote: {decide(mostly_rejected, total_samples=5)}")
21print(f"no_valid_votes: {decide([], total_samples=5)}")1strong_vote: merge_release
2split_vote: manual_review
3mostly_rejected_vote: manual_review
4no_valid_votes: manual_reviewMeasure gains against call cost
Use a held-out fixture set before calling the strategy ready. The fixture represents five sampled final actions returned for each case; the controller compares first-sample accuracy with five-sample voting accuracy.
1from collections import Counter
2
3fixtures = {
4 "approved_clean_release": {
5 "expected": "merge",
6 "samples": ["review", "merge", "merge", "merge", "force"],
7 },
8 "scan_failed": {
9 "expected": "review",
10 "samples": ["review", "review", "force", "review", "hold"],
11 },
12 "review_missing": {
13 "expected": "review",
14 "samples": ["merge", "review", "review", "review", "merge"],
15 },
16}
17
18single_correct = 0
19vote_correct = 0
20for item in fixtures.values():
21 winner = Counter(item["samples"]).most_common(1)[0][0]
22 single_correct += item["samples"][0] == item["expected"]
23 vote_correct += winner == item["expected"]
24
25total = len(fixtures)
26print(f"single_trace_accuracy: {single_correct / total:.0%}")
27print(f"vote_5_accuracy: {vote_correct / total:.0%}")
28print(f"model_calls: single={total}, vote_5={total * 5}")1single_trace_accuracy: 33%
2vote_5_accuracy: 100%
3model_calls: single=3, vote_5=15This fixture is intentionally small and deterministic: it tests controller logic. A real release decision needs representative labeled cases, real model samples, token counts, latency, refusal rates, and cost.
What should a self-consistency controller do when votes do not form a strong majority?
Answer
Abstain or route to review. More samples are useful only when a measured gain justifies their cost and the action remains safe.
Tree-of-Thoughts: search when branches can dead-end
Voting helps when independent paths tend to converge on the same short answer. It doesn't deliberately revisit earlier choices. Tree-of-Thoughts (ToT) represents partial solutions as search states, generates alternatives, evaluates the states, and preserves only branches worth extending.[6]
Yao et al. evaluated ToT on tasks built for planning and search. On Game of 24, their GPT-4 ToT setup solved 74% of tasks while their CoT baseline solved 4%.[6] The narrower conclusion is not "use trees everywhere." If a problem has verifiable partial states and meaningful backtracking, search can rescue a bad early move.
Search states in a release plan
For a release gate, consider a workflow whose final recommendation must be supported by two observations: a current security scan and a reviewer approval record. A controller can expand legal steps instead of letting a model invent a final action before evidence is present.
1from dataclasses import dataclass
2
3@dataclass(frozen=True)
4class State:
5 security_clear: bool | None = None
6 reviewer_approved: bool | None = None
7 final_action: str | None = None
8
9def expand(state: State) -> list[tuple[str, State]]:
10 next_states: list[tuple[str, State]] = []
11 if state.security_clear is None:
12 next_states.extend([
13 ("security_scan:clear", State(True, state.reviewer_approved)),
14 ("security_scan:failed", State(False, state.reviewer_approved)),
15 ])
16 if state.reviewer_approved is None:
17 next_states.extend([
18 ("review:approved", State(state.security_clear, True)),
19 ("review:rejected", State(state.security_clear, False)),
20 ])
21 if (
22 state.security_clear is not None
23 and state.reviewer_approved is not None
24 and state.final_action is None
25 ):
26 action = "merge_release" if state.security_clear and state.reviewer_approved else "block_release"
27 next_states.append((action, State(state.security_clear, state.reviewer_approved, action)))
28 return next_states
29
30frontier = [State()]
31for depth in range(3):
32 generated = [item for state in frontier for item in expand(state)]
33 print(f"depth_{depth + 1}: {[action for action, _ in generated]}")
34 frontier = list(dict.fromkeys(state for _, state in generated))1depth_1: ['security_scan:clear', 'security_scan:failed', 'review:approved', 'review:rejected']
2depth_2: ['review:approved', 'review:rejected', 'review:approved', 'review:rejected', 'security_scan:clear', 'security_scan:failed', 'security_scan:clear', 'security_scan:failed']
3depth_3: ['merge_release', 'block_release', 'block_release', 'block_release']The two evidence-gathering orders converge on outcome-bearing states. Only the state with a clear scan and an approved review can merge; a failed scan or rejected review blocks the release. In the next lesson, tool calls will populate those outcomes from real APIs.
A fully runnable search example
Game of 24 is useful because the evaluator is exact: arithmetic either reaches 24 using each input once or it doesn't. The solver below explores partial equations with breadth-first search and returns a verified solution.
1from fractions import Fraction
2from itertools import combinations
3
4def combine(left: tuple[Fraction, str], right: tuple[Fraction, str]) -> list[tuple[Fraction, str]]:
5 a, a_expr = left
6 b, b_expr = right
7 outcomes = [
8 (a + b, f"({a_expr} + {b_expr})"),
9 (a - b, f"({a_expr} - {b_expr})"),
10 (b - a, f"({b_expr} - {a_expr})"),
11 (a * b, f"({a_expr} * {b_expr})"),
12 ]
13 if b:
14 outcomes.append((a / b, f"({a_expr} / {b_expr})"))
15 if a:
16 outcomes.append((b / a, f"({b_expr} / {a_expr})"))
17 return outcomes
18
19def solve_24(numbers: list[int]) -> str | None:
20 frontier = [[(Fraction(number), str(number)) for number in numbers]]
21 while frontier:
22 state = frontier.pop(0)
23 if len(state) == 1 and state[0][0] == 24:
24 return state[0][1]
25 for i, j in combinations(range(len(state)), 2):
26 remainder = [item for k, item in enumerate(state) if k not in (i, j)]
27 frontier.extend([remainder + [result] for result in combine(state[i], state[j])])
28 return None
29
30solution = solve_24([4, 5, 6, 7])
31print(f"solution_found: {solution is not None}")
32print(f"expression: {solution}")1solution_found: True
2expression: ((6 - 4) * (5 + 7))
Pruning is a source of failure
An LLM evaluator isn't an arithmetic oracle. If it scores an apparently simple but dead branch above a non-obvious solvable branch, an aggressive beam can remove the answer before expansion.
1branches = [
2 {"move": "6 * 4 = 24 first", "solvable": False, "weak_score": 0.95, "exact_score": 0.0},
3 {"move": "5 + 7 = 12 first", "solvable": True, "weak_score": 0.40, "exact_score": 1.0},
4 {"move": "7 - 5 = 2 first", "solvable": False, "weak_score": 0.35, "exact_score": 0.0},
5]
6
7def keep_one(score_name: str) -> dict[str, object]:
8 return max(branches, key=lambda branch: branch[score_name])
9
10weak_choice = keep_one("weak_score")
11exact_choice = keep_one("exact_score")
12print(f"weak_evaluator_keeps_solution: {weak_choice['solvable']}")
13print(f"exact_evaluator_keeps_solution: {exact_choice['solvable']}")
14print(f"risk: beam_width_1 can prune the valid branch")1weak_evaluator_keeps_solution: False
2exact_evaluator_keeps_solution: True
3risk: beam_width_1 can prune the valid branchThe production implications are concrete:
- Keep ToT for tasks with real branch structure, not ordinary classification.
- Prefer deterministic validators when a partial state can be checked in code.
- Measure solver recall at each beam width alongside final successes.
- Cap expansions and latency before an open-ended search reaches users.
Why can Tree-of-Thoughts perform worse when the evaluator is weak and the beam is narrow?
Answer
Search spends compute only on retained states. If an incorrect evaluator prunes the valid branch early, later expansions cannot recover it.
Choose compute with an eval gate
Direct prompting, one trace, voting, and tree search aren't maturity levels. They are candidates with different accuracy and serving cost. Start with the cheapest candidate, then promote a more expensive strategy through an eval gate only when held-out results require it.

1results = [
2 {"strategy": "direct", "accuracy": 0.76, "p95_ms": 190, "calls": 1},
3 {"strategy": "single_trace", "accuracy": 0.84, "p95_ms": 360, "calls": 1},
4 {"strategy": "vote_5", "accuracy": 0.94, "p95_ms": 740, "calls": 5},
5 {"strategy": "tree_search", "accuracy": 0.96, "p95_ms": 1840, "calls": 14},
6]
7
8minimum_accuracy = 0.90
9latency_budget_ms = 900
10eligible = [
11 row for row in results
12 if row["accuracy"] >= minimum_accuracy and row["p95_ms"] <= latency_budget_ms
13]
14selected = min(eligible, key=lambda row: (row["calls"], row["p95_ms"]))
15
16for row in results:
17 print(f"{row['strategy']}: accuracy={row['accuracy']:.0%}, p95_ms={row['p95_ms']}, calls={row['calls']}")
18print(f"selected: {selected['strategy']}")1direct: accuracy=76%, p95_ms=190, calls=1
2single_trace: accuracy=84%, p95_ms=360, calls=1
3vote_5: accuracy=94%, p95_ms=740, calls=5
4tree_search: accuracy=96%, p95_ms=1840, calls=14
5selected: vote_5These numbers are example evaluation results, not a benchmark claim. In your system, keep a table with:
| Metric | Why it matters |
|---|---|
| Action accuracy or task success | Extra reasoning must change correct outcomes |
| Unsafe-action and abstention rates | Reliability includes knowing when not to act |
| Input, output, and reasoning tokens | Sampling and search multiply spend |
| p50 and p95 latency | Long tails can make support interactions unusable |
| Parse and schema failures | A correct thought is useless if the runtime can't consume its action |
When not to use CoT (or heavier search)
Use this checklist before you promote scaffolding or multi-sample voting:
| Situation | Prefer instead |
|---|---|
| Inputs are already structured policy predicates | Deterministic policy engine in the runtime |
| Pattern-matching few-shot ICL (curse-of-CoT regime) | Direct answer; measure before adding a scratchpad |
| Single-label classification, field extraction, or formatting with clear labels | Structured output, no visible reasoning channel |
| No verifiable partial state and no backtracking value | Direct or one trace, not ToT |
| Accuracy already meets the gate at one call | Keep one call; skip vote_5 and tree search |
| Hard latency budgets where extra tokens blow p95 | Cheapest strategy that still clears quality |
| User-visible channels where intermediate tokens can leak secrets | Keep traces server-side or omit them |
| Need a compliance audit of why | Logged inputs, tool observations, and policy checks, not free-form CoT text |
| Missing live facts | Tool call (next lesson), not a longer rationale |
| A native reasoning model already wins the eval | Skip your own scaffolding; compare behind the same gate |
Forcing a visible CoT scaffold on a model that already does hidden long-CoT reasoning can add cost without accuracy gain. Treat that as another arm in the same release table.
Where reasoning ends and tools begin
All runnable experiments above operate on provided facts or deterministic state. A real release decision requires current CI status, security scan output, and review state from source systems. Reasoning alone can't obtain those observations.
ReAct interleaves reasoning traces and task-specific actions so new observations can update the next decision.[7] The production handoff isn't a saved inner monologue. It's a validated action request, a controlled execution result, and a bounded next decision:
1Need: security scan status is not present in supplied facts.
2Next action request: get_security_scan(run_id="ci-1482")
3Runtime responsibility: validate authorization, execute call, log result.
4Next decision: apply policy only after observation is returned.The next lesson implements that action boundary with typed function calls, schemas, errors, and safe execution.
What to remember
- Define the scorer first. A decision record lets you test whether extra inference work improves outcomes.
- One trace is one candidate. CoT can reveal missed steps, but a plausible rationale isn't a faithful audit log.[4]
- Vote on normalized outcomes. Self-consistency is useful only when its measured gain beats its sample cost.[5]
- Search only with branch structure. ToT needs meaningful states, evaluators, pruning limits, and failure measurements.[6]
- Promote strategies through evals. Direct, trace, vote, and search should compete under quality and latency gates.
- Skip CoT when policy is already machine state. Deterministic predicates, pattern-ICL, and tasks already above the accuracy SLO need no scratchpad.