Personalize this lesson
Adapt explanations and teaching visuals to your background and preferred voice.
Picture a release candidate with passing tests, a clear security scan, and an approved review. Policy allows an automatic merge only when all three facts hold. A direct answer can still miss one of them.
The last chapter used a compressed embedding index to put the right policy clause in context, including an auto-merge rule. Retrieval isn't the end: an assistant can still skip a condition, mishandle a check, or commit too early to one plan. The question isn't whether a model can produce a longer explanation; it's how much extra test-time compute buys a better decision under token, latency, and safety budgets. Start with one prompt-elicited chain of thought (CoT), compare several candidates with self-consistency, and move to an external Tree of Thoughts (ToT) search only when early choices can dead-end.
Start with a failure you can audit
Put those facts on one row: unit tests pass, the security scan is clear, and a reviewer has approved the diff. The policy permits auto-merge only when every field is true. A model can still skip a field or suggest a force-merge.
A useful outward artifact is a short decision record containing those facts, the checks applied, and the final action. It's smaller than an open-ended rationale, easy to score, and safe to compare against deterministic policy logic. It isn't raw chain of thought. It's a task-specific output contract that the application can validate.
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. Define the output contract and scorer before increasing model compute.
When to skip CoT entirely: structured machine state (booleans, enum fields, and runtime-owned policy predicates) belongs in code. Sampling five CoT traces over three known flags adds cost without evidence. Use model reasoning for unstructured evidence (diff text, free-form review comments, or incident narratives) when no deterministic scorer exists yet. These fixtures teach scoring and controllers; they aren't a recommendation to put
all(checks)behind a model.
If a required fact isn't in the prompt at all, a longer rationale can't invent it. The missing value has to come from the source system. That boundary leads to the typed tool request in the next lesson.

The Policy route handles known flags, while the Tool route obtains a live observation. This chapter takes the middle route: extra inference over facts already present, scored against a contract.

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. On GSM8K (a grade-school math benchmark), PaLM 540B rose from 17.9% with standard prompting to 56.9% with eight CoT exemplars, with further gains on commonsense and symbolic tasks once models were large enough for the scratchpad to matter.[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. Meincke et al. tested GPQA Diamond and found that an explicit "think step by step" request often added little accuracy for reasoning-tuned models while increasing time and tokens; for ordinary chat models the average gain was small and sometimes came with more errors on questions the model already answered reliably.[3]
🔬 Research insight: More reasoning text isn't always better. Zheng et al. tested CoT and its variants (including ToT and ReAct, or Reasoning and Acting) 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.[4] When a task is pattern matching from examples, a visible scratchpad can add noise instead of signal. Measure that tradeoff rather than assuming a longer response will help.
When one prompt decomposition isn't enough, least-to-most prompting handles a different shape of problem. It breaks the task into ordered sub-questions, answers the simplest one first, and passes each intermediate answer into the next sub-prompt.[5] That sequential handoff can improve compositional generalization without the latency and memory overhead of branching search.
Keep four reasoning artifacts separate
The word reasoning now covers several different artifacts. Mixing them creates false audit claims and brittle integrations, so name the artifact before deciding what it proves.
| Artifact | What it contains | What you can safely conclude |
|---|---|---|
| Prompt-elicited CoT | Intermediate text generated because a prompt asks for steps | It may help the final answer; it may still be unfaithful |
| Native reasoning tokens | Internal inference tokens used by a reasoning model | Token usage measures compute, not whether the reasoning was correct or exposed |
| Reasoning summary | Provider-generated description of internal reasoning | It can orient an operator, but it isn't raw CoT or proof of faithful cognition |
| Decision record | Task-specific facts, checks, and final action emitted for your application | Its fields can be schema-checked and scored against an oracle |
Provider disclosure rules and API surfaces differ. OpenAI's API documentation says raw reasoning tokens aren't exposed, although supported models can return an opt-in reasoning summary. Those hidden tokens still count toward context and billing.[6]
Anthropic still returns thinking blocks, including summarized thinking. Its control surface is model-specific: older Claude 4.x models use a fixed budget_tokens extended-thinking mode, while newer models reject that setting and expect adaptive thinking with an effort control.[7]
Recheck provider documentation when you integrate. Don't write controller logic that assumes every model returns a visible scratchpad or that one thinking API works across Claude generations.
This distinction changes the build-versus-buy decision. An external CoT, voting, or tree-search controller gives you explicit sample and search state, but generated rationales still aren't guaranteed to be faithful.
A native reasoning model owns more of the internal inference process, while its controls, visibility, and accounting follow the provider contract. Put both behind the same task-level evaluator and compare outcomes, latency, token use, and parse failures.
Ask for reviewable task artifacts at the workflow boundary rather than text that claims to reveal private internal reasoning:
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 required checks make a missed predicate observable. They still 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.[8] Log inputs, actions, validations, and outcomes; don't treat eloquent reasoning text or a provider summary 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 still help 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.
Stable formatting solves parsing, not sampling variance. A well-shaped candidate can still choose the wrong action.
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.[9] The vote operates on extracted answers, not on whose rationale sounds best.
Let be the final text from sample , and let map that text to an allowed action or reject it as invalid. With total samples, the winning action and its share are:
The denominator remains , even when some outputs fail to parse. Emit only when the winner is unique and meets a configured threshold ; otherwise abstain.
To obtain different candidates, use stochastic decoding, usually with a nonzero temperature, rather than rerunning one deterministic decode. The earlier Decoding Algorithms lesson explains that control. Keep the setting fixed in your experiment and log it with the sample count.
In the paper's PaLM-540B setting, 40 independently sampled outputs per run raised GSM8K accuracy from 56.5% with greedy CoT to 74.4% with self-consistency, a 17.9 percentage-point gain.[9] That result belongs to its model, prompts, sampling setup, and benchmark. Five samples here aren't expected to reproduce it.
Wang et al. also observed that the share of samples agreeing with the aggregated answer correlated with GSM8K accuracy. That's a research observation, not a calibrated confidence score. Treat agreement as a vote statistic you threshold, then still run ordinary policy checks.

Normalize before counting
Model outputs rarely use exactly one spelling. 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. A passing vote chooses a candidate; ordinary runtime policy still decides whether that action is allowed.
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. Each fixture represents five already-canonicalized final actions for one case. Unknown values are rejected, but they remain in the denominator. The controller compares first-sample accuracy with five-sample voting accuracy.
1from collections import Counter
2
3fixtures = {
4 "approved_clean_release": {
5 "expected": "merge_release",
6 "samples": ["manual_review", "merge_release", "merge_release", "merge_release", "force"],
7 },
8 "scan_failed": {
9 "expected": "manual_review",
10 "samples": ["manual_review", "manual_review", "force", "manual_review", "hold"],
11 },
12 "review_missing": {
13 "expected": "manual_review",
14 "samples": ["merge_release", "manual_review", "manual_review", "manual_review", "merge_release"],
15 },
16}
17
18ALLOWED = {"merge_release", "manual_review"}
19
20def vote(samples: list[str], minimum_share: float = 0.6) -> str:
21 counts = Counter(action for action in samples if action in ALLOWED)
22 if not counts:
23 return "abstain"
24 ranked = counts.most_common(2)
25 winner, count = ranked[0]
26 tied = len(ranked) > 1 and ranked[0][1] == ranked[1][1]
27 if tied or count / len(samples) < minimum_share:
28 return "abstain"
29 return winner
30
31single_correct = 0
32vote_correct = 0
33for item in fixtures.values():
34 winner = vote(item["samples"])
35 single_correct += item["samples"][0] == item["expected"]
36 vote_correct += winner == item["expected"]
37
38total = len(fixtures)
39print(f"single_trace_accuracy: {single_correct / total:.0%}")
40print(f"vote_5_accuracy: {vote_correct / total:.0%}")
41print(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, so it tests controller logic rather than model quality. 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 don't 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 an earlier choice. Tree-of-Thoughts (ToT) represents partial solutions as search states, then generates alternatives, evaluates those states, and preserves only branches worth extending.[10]
Yao et al. evaluated ToT on tasks built for planning and search. A beam width is the number of partial states retained after each scoring step. On Game of 24 with GPT-4, ToT with beam width 5 solved 74% of tasks, while CoT solved 4.0% and CoT self-consistency with 100 samples solved 9.0%.[10] Independent voting didn't rescue a search problem.
The narrower conclusion isn't "use trees everywhere." When a problem has verifiable partial states and meaningful backtracking, search can recover from a bad early move.
Beyond tree branching, Graph-of-Thoughts (GoT) stores reasoning states on a directed acyclic graph (DAG) instead of a tree.[11] ToT grows each branch from a single parent. GoT can merge independent paths into one state (thought aggregation), refine a thought after feedback, and rewrite the graph as search proceeds.
That helps when separate sub-tasks, such as reading two logs or drafting two candidate clauses, have to join before a final decision.
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.
In the original ToT framework, a language model can propose and evaluate thoughts while an external search algorithm owns state, branching, and backtracking. The deterministic code below isolates that controller skeleton; it doesn't simulate an LLM evaluator.
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[State]:
10 next_states: list[State] = []
11 if state.security_clear is None:
12 next_states.extend([
13 State(True, state.reviewer_approved),
14 State(False, state.reviewer_approved),
15 ])
16 if state.reviewer_approved is None:
17 next_states.extend([
18 State(state.security_clear, True),
19 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(State(state.security_clear, state.reviewer_approved, action))
28 return next_states
29
30def label(state: State) -> str:
31 scan = {True: "clear", False: "failed"}.get(state.security_clear, "?")
32 review = {True: "approved", False: "rejected"}.get(state.reviewer_approved, "?")
33 action = state.final_action or "pending"
34 return f"scan={scan}, review={review}, {action}"
35
36frontier = [State()]
37for depth in range(3):
38 frontier = list(dict.fromkeys(child for state in frontier for child in expand(state)))
39 print(f"depth_{depth + 1}: {[label(state) for state in frontier]}")1depth_1: ['scan=clear, review=?, pending', 'scan=failed, review=?, pending', 'scan=?, review=approved, pending', 'scan=?, review=rejected, pending']
2depth_2: ['scan=clear, review=approved, pending', 'scan=clear, review=rejected, pending', 'scan=failed, review=approved, pending', 'scan=failed, review=rejected, pending']
3depth_3: ['scan=clear, review=approved, merge_release', 'scan=clear, review=rejected, block_release', 'scan=failed, review=approved, block_release', 'scan=failed, review=rejected, block_release']The two evidence-gathering orders converge on the same four complete evidence states. Only a clear scan plus an approved review can merge; any failed scan or rejected review blocks the release. The next lesson will populate those outcomes from real APIs through tool calls.
A fully runnable search example
Game of 24 gives us an exact evaluator: 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 branchexact_score is a hand-labeled fixture oracle, not a capability you get for free. Real search tasks often need executable validators, environment feedback, or a separately evaluated scorer.
The 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, then 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 can't recover it.
Search can recover from bad early choices, but each expansion multiplies calls and latency. Choose a strategy through one quality-and-cost experiment rather than assuming a bigger tree is better.
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. Track p95 latency, the time under which 95% of requests finish, alongside quality. Snell et al. found that the best test-time allocation can depend on problem difficulty, so a fixed extra-sample budget isn't automatically optimal.[12]

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_5Treat these as example evaluation results, not a benchmark claim. For a real 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 |
| Multi-step task with sequential sub-dependencies | Least-to-most prompting before expanding search trees |
| 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, one trace, or linear decomposition, not ToT/GoT |
| 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 |
Adding a visible CoT scaffold to a reasoning-tuned model can add tokens and latency with little accuracy gain. On ordinary chat models it can also inject errors on questions the model already answered reliably.[3] Treat that prompt as another experimental arm, not as a required wrapper around every reasoning endpoint.
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.[13] This loop doesn't require raw private reasoning. It needs 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.
- Keep artifacts distinct. Prompt-elicited CoT, hidden reasoning tokens, provider summaries, and application decision records aren't interchangeable.
- One trace is one candidate. CoT can expose missed steps, but a plausible rationale isn't a faithful audit log.[8]
- Vote on normalized outcomes. Self-consistency is useful only when its measured gain beats its sample cost.[9]
- Search only with branch structure. ToT and GoT need meaningful states, evaluators, pruning limits, and failure measurements.[10] [11]
- 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.