Advanced Prompting and Reasoning

Tree-of-Thoughts & Search-Based Reasoning


Give a strong model this puzzle: using 4, 7, 8 and 8 exactly once each, with only + − × ÷ and brackets, make 24.

Give it to a model without built-in thinking, with ordinary step-by-step prompting, and it nearly always opens the same way:

Text
Let me start with the largest numbers. 8 × 4 = 32. Now I need toget from 32 to 24 using 7 and 8. 32 - 8 = 24 — that works!So the answer is 8 × 4 - 8 = 24.

The 7 is never used. The model noticed nothing: by the time it wrote "that works!" the tokens "8 × 4" were fixed in the context, and every later token was predicted in a world where that opening was correct. It found a valid arithmetic identity and quietly dropped an input.

Sometimes it catches itself: "wait, I haven't used the 7". Then it tries again — but the failed attempt is still in the context, and the next attempt is conditioned on it. Attempts three, four and five drift around the same neighbourhood. Occasionally you get twelve paragraphs of increasingly frantic arithmetic ending in a confident wrong answer.

The answer is (7−8÷8)×4=6×4=24(7 - 8 \div 8) \times 4 = 6 \times 4 = 24. Finding it requires trying 8÷88 \div 8 first — a move that looks pointless in isolation, since it turns two numbers into a 1. No greedy heuristic proposes it. You get there by trying it, seeing where it leads, and being willing to abandon it.

Propose, score, expand the best — 24 from 4, 7, 8, 8Make 24 from4, 7, 8, 88 plus 8 gives 167 plus 8 gives 154 plus 7 gives 1116 with 4, 7left — keep11 with 8, 8left — prune
Decoding cannot take a token back, so the backtracking has to live outside the model, in a search you write.

The real problem: decoding cannot backtrack

Two mechanical facts explain the failure above, and they explain the fix too.

Every emitted token is permanent. Autoregressive generation appends to a sequence; there is no delete. When the model wrote "8 × 4 = 32" it committed — not by choice, but structurally. It can write "that was wrong", but that correction is additional context, not a rollback. The failed branch keeps influencing every subsequent probability distribution, which is why in-context retries cluster rather than exploring freely.

Each choice gets one token's worth of compute. Selecting the first operation took exactly one forward pass. No lookahead, no evaluation of consequences three moves later. A decision that determines the whole solution gets the same fixed budget as the word "the".

Put those together and you have a system doing greedy search with no evaluation function and no undo. It is remarkable it works as often as it does.

The model is not bad at search. It is not doing search — it is doing one greedy left-to-right pass and hoping. Tree-of-Thoughts does not make the model better at anything; it takes the search away from the model and gives it to a program.

Splitting the job in two

Tree-of-Thoughts (ToT) decomposes the work into three parts, and puts each where it belongs.

PartWho does itWhy there
Propose — given a partial state, suggest bb plausible next stepsThe modelRequires judgement about what is worth trying. No program can enumerate "sensible next moves" for an open problem.
Evaluate — given a partial state, judge how promising it isThe model (or code, when the criterion is checkable)Requires judgement about whether a half-finished attempt is on track.
Search — expand, compare, prune, backtrack, terminateYour codeBacktracking is stack.pop(). Keeping the best kk is sorted(...)[:k]. These are trivial in Python and structurally impossible inside a token stream.

The model is used only for short, local, single-judgement calls. Nothing asks it to hold a search frontier in its head, because it cannot: the context is its only memory, and a frontier written into the context is just more text competing for attention.

Text
                        [ start ]                        /    |    \                   8÷8=1  8×4=32  7×4=28      <- model proposes                   score7   score4   score5      <- model evaluates                     |        X        |         <- code prunes                7-1=6      (dropped)  28-8=20                score9                score3                   |                     X             6×4=24  ✓

Search strategies

Once search lives in your code, you choose the algorithm. Three are worth knowing, differing in what they assume about your evaluator.

Breadth-first / beam search

Expand every node at depth dd, score them all, keep the best kk, move to depth d+1d+1. The retained set is the beam and kk is the beam width.

This suits problems with a known, uniform depth — a three-step plan, a four-move puzzle — and is safest when your evaluator is noisy, because keeping k>1k > 1 means one bad score does not eliminate the right branch. It parallelises well: every node at a level can be scored concurrently.

Depth-first search

Follow one branch to a terminal state. If it fails a validity check, pop back to the last node with untried children and continue. Memory is O(depth)O(\text{depth}) rather than O(k×depth)O(k \times \text{depth}).

DFS wins when solutions are deep, failure is cheaply and definitively detectable, and any valid solution will do. The 24 puzzle fits perfectly: a branch is dead the moment you cannot reach 24 from the remaining numbers, and that is a code check, not a model call. DFS is a bad fit without a crisp failure test, because you will descend a doomed branch to full depth.

Best-first search

Keep a priority queue of all frontier nodes across all depths, ordered by score. Always expand the highest-scoring node, wherever it sits in the tree.

The most flexible, and the most dependent on evaluator quality — it chases whatever the evaluator rates highly, so a miscalibrated evaluator sends it somewhere useless. It has a subtle bias too: without depth normalisation, shallow nodes score higher simply because fewer commitments have been made and less can be criticised, so the search spreads sideways instead of going deep. Normalise for depth, or add a small depth bonus.

Beam / BFSDFSBest-first
MemoryO(k⋅b)O(k \cdot b) per levelO(d)O(d)O(frontier)O(\text{frontier}), can grow large
Needs a good evaluator?Moderately — ranks within a level onlyBarely — needs a validity testHeavily — ranks across levels
ParallelismExcellent, whole level at oncePoor, inherently serialModerate, expand top-mm together
Finds the best solution?Best within the beamFirst valid one foundBest under the evaluator's ordering
Use forFixed-depth planning, option comparisonPuzzles, constraint satisfaction, code that must compileUneven trees, anytime search with a cost cap

Writing the two prompts

ToT stands or falls on the proposer and the evaluator. Both are short prompts, and both are usually written badly.

The proposer

Text
BADHere's the current state of the problem. What are some optionsfor what to do next? Give me a few ideas.

"A few ideas" gives no count, so you cannot build a fixed-width beam. Nothing forbids near-duplicates, and the highest-probability continuation after one proposal is a rephrasing of it — three wordings of one idea, a branching factor of 1 dressed as 3. And nothing requires proposals to be legal moves from the current state, so you get suggestions that ignore constraints already committed to.

Text
GOODCURRENT STATE  Numbers remaining: [7, 4, 1]  Operations used so far: 8 ÷ 8 = 1  Target: 24Propose exactly 3 candidate next operations.Each candidate must:  - combine exactly two of the remaining numbers with one of + - × ÷  - be genuinely different from the others, not a rewording  - be a legal move: only numbers currently remaining may be usedOutput exactly 3 lines, nothing else, in this form:  <expression> = <result> | remaining: [<numbers after this move>]

The state is rendered explicitly rather than left implicit in a long transcript. "Exactly 3" fixes the branching factor. "Genuinely different, not a rewording" attacks the duplication failure. The rigid output line makes your parser four lines of code instead of a fragile heuristic. And writing the resulting state alongside the move means the next expansion need not re-derive it — the state is carried in the text, the only place it can be carried.

The evaluator

Text
BADRate this partial solution out of 10.

Two mechanical problems. First, an unanchored 1–10 scale collapses: without definitions, rating tokens are dominated by 7 and 8 for anything coherent, so scores carry almost no ordering information and the beam prunes at random. Second, a score generated first comes from a single forward pass with no analysis behind it, and everything after it is rationalisation of a number already committed.

Text
GOODSTATE  Numbers remaining: [7, 4, 1]  Target: 24Assess whether 24 is still reachable from this state.Write your assessment in this exact order:REACHABLE: one of {yes, maybe, no}EVIDENCE: if yes, give one concrete expression that reaches 24.          If no, say which constraint makes it impossible.          If maybe, name the most promising direction.SCORE: an integer 0-10, using this scale:   0-2  = a constraint is already violated, or the target is          provably out of reach   3-4  = no promising direction identified   5-6  = plausible but no concrete route yet   7-8  = a concrete route exists but needs one more non-obvious step   9-10 = a complete valid solution has been found

The scale is anchored: each band has a stated meaning, so the score token is predicted from a description rather than a vague sense of goodness. The evidence line comes before the score, so the score is conditioned on an actual attempt at a solution — not stylistic, it changes what the score is computed from. And the three-way REACHABLE label gives a cheap categorical prune that does not depend on the numeric scale being calibrated.

Put the reasoning before the score, always. A score emitted first is a guess that the following paragraph then defends.

A worked example: a real planning decision

A seed-stage company has six months of runway and four engineers. What to build next? Here is a depth-3 tree with beam width 2, and the scores the evaluator returned.

Level 1 — three strategies:

NodeProposalScoreEvaluator's stated reason
ABuild enterprise SSO6.0Unblocks large deals but sales cycle exceeds the runway
BRebuild onboarding8.0Directly addresses the measured 40% day-one drop-off
CLaunch a self-serve pricing tier7.5Creates revenue without sales headcount; scope unclear

Beam width 2 keeps B and C, dropping A. A greedy search — beam width 1, which is what plain chain-of-thought effectively does — would keep only B. Hold that thought.

Level 2 — expand both survivors:

NodeProposalScoreEvaluator's stated reason
B1Rebuild onboarding for the current customer profile6.0Full rebuild is 10–14 weeks for 4 engineers; leaves no slack
B2Add an in-app checklist only5.5Cheap, but historically moves activation by low single digits
B3Onboarding rebuild plus documentation rewrite4.5Exceeds the runway outright
C1Ship self-serve at 29 per month, card only, no sales touch8.5Roughly 3 weeks of work; revenue starts inside the runway
C2Self-serve with usage-based billing6.5Metering infrastructure adds 4–6 weeks
C3Self-serve behind a waitlist5.0Delays revenue, which is the entire point

The beam now keeps C1 (8.5) and B1 (6.0). The whole B subtree has collapsed. B scored highest at level 1 and every one of its children scored worse than C's best child.

This is the central phenomenon, not an artefact of the example. At level 1 the evaluator was scoring a slogan — "rebuild onboarding" — against a real, measured problem, and it scored well because the diagnosis is sound. Only at level 2, when the proposal carried a duration, did the binding constraint (four engineers, six months) bite. Shallow evaluation rewards whichever option is easiest to describe attractively; depth converts a slogan into something falsifiable.

Level 3 — expand C1 and B1:

NodeProposalScore
C1aShip at 29 with a 14-day trial; instrument activation from day one9.0
C1bShip at 29 with no trial7.0
C1cShip at 49 to protect average revenue per user6.5
B1aRebuild onboarding, cut scope to the first-run flow only6.0
B1bRebuild onboarding, hire a contractor5.5
B1cRebuild onboarding, delay other work5.0

The search returns C1a at 9.0. The greedy path — commit to the best level-1 option and never reconsider — terminates at B1a with 6.0. One extra beam slot, costing three extra evaluations at level 2, changed the recommendation and improved the terminal score by 3.0 points. That is the value proposition of ToT in one number.

Implementing best-first search

Python
import heapq, itertools, json, anthropicclient = anthropic.Anthropic()_tie = itertools.count()          # keeps heapq from comparing dictsdef call(prompt, max_tokens=2000):    resp = client.messages.create(        model="claude-opus-5", max_tokens=max_tokens,        output_config={"effort": "low"},   # short, local judgements need little thinking        messages=[{"role": "user", "content": prompt}],    )    # thinking is on by default; skip thinking blocks and keep the text    return "".join(b.text for b in resp.content if b.type == "text")def propose(state, b=3):    text = call(PROPOSER_TEMPLATE.format(state=state, b=b))    return [ln.strip() for ln in text.splitlines() if ln.strip()][:b]def evaluate(state):    text = call(EVALUATOR_TEMPLATE.format(state=state), max_tokens=1000)    reachable = "no"    score = 0.0    for line in text.splitlines():        if line.startswith("REACHABLE:"):            reachable = line.split(":", 1)[1].strip().lower()        if line.startswith("SCORE:"):            try:                score = float(line.split(":", 1)[1].strip())            except ValueError:                score = 0.0    return score, reachabledef best_first(root, max_depth=3, beam=2, node_budget=25,               prune_below=4.0, is_goal=lambda s: False, is_valid=lambda s: True):    frontier = [(-10.0, next(_tie), root, [root], 0)]   # negated score = max-heap    seen, expanded, best = {canonical(root)}, 0, (-1.0, None, None)    while frontier and expanded < node_budget:        neg, _, state, path, depth = heapq.heappop(frontier)        if is_goal(state):            return {"solution": state, "path": path, "score": -neg,                    "nodes_expanded": expanded}        if depth >= max_depth:            continue        children = propose(state, b=beam + 1)        expanded += 1        scored = []        for child in children:            key = canonical(child)            if key in seen or not is_valid(child):   # free prunes, done in code                continue            seen.add(key)            s, reachable = evaluate(child)            if reachable == "no" or s < prune_below:                continue            scored.append((s, child))        scored.sort(reverse=True, key=lambda t: t[0])        for s, child in scored[:beam]:               # only the best survive            heapq.heappush(frontier, (-s, next(_tie), child, path + [child], depth + 1))            if s > best[0]:                best = (s, child, path + [child])    return {"solution": best[1], "path": best[2], "score": best[0],            "nodes_expanded": expanded, "note": "budget exhausted, best-so-far returned"}

Four details in that code are load-bearing.

  • is_valid is a Python function, not a model call. Check any code-checkable constraint in code: a number reused, a budget exceeded, code that fails to parse. It is free, deterministic and correct, and every node it kills is a model call you did not make.
  • seen deduplicates canonicalised states. Different proposal wordings routinely produce the same state, and paying to evaluate the same state twice is pure waste.
  • node_budget is a hard cap. Without it, a well-scoring but unsolvable region will absorb an unbounded number of calls.
  • The function returns best-so-far when the budget runs out, not an exception. Search under a budget must be anytime: you want the best partial answer, not nothing.

What it costs

Assume an expansion call is roughly 600 input and 350 output tokens, and an evaluation call is 500 input and 150 output, billed at 5 dollars per million input and 25 per million output. Any thinking the model does bills as output on top, which is one reason the code above keeps effort low for these small calls.

  • One expansion: (600×5+350×25)/106=0.01175(600 \times 5 + 350 \times 25) / 10^6 = 0.01175 dollars.
  • One evaluation: (500×5+150×25)/106=0.00625(500 \times 5 + 150 \times 25) / 10^6 = 0.00625 dollars.

Now compare three approaches on a branching factor of 3 and depth of 3:

ApproachExpansionsEvaluationsModel callsCostVersus one chain
Single chain of thought——10.0261×
ToT, beam width 2515200.1525.9×
ToT, exhaustive1339520.39715.3×

Read the exhaustive row carefully: at depth 3 with branching 3 there are 3+9+27=393 + 9 + 27 = 39 nodes, 13 of them internal and needing expansion. Beam width 2 expands the root plus two nodes at each of the next two levels — 5 expansions — and evaluates 3+6+6=153 + 6 + 6 = 15 nodes. Pruning cut cost by 62% and, in the worked example, cost nothing in quality.

Push depth to 4 and the exhaustive tree has 120 nodes; depth 5 gives 363. Cost is O(bd)O(b^d) and there is no prompt that changes that. Pruning is not an optimisation you add later; without it, ToT is unaffordable past depth 3.

Pruning rules, cheapest first

RuleCost to applyRisk
Constraint violation, checked in codeFreeNone, if the check is correct
Duplicate state, by canonical hashFreeNone, if canonicalisation is sound
Evaluator says REACHABLE: noOne evaluationModerate — a false "no" kills the answer
Absolute score thresholdOne evaluationHigh — depends on the scale being calibrated
Relative: drop anything more than 2 points below the best siblingOne evaluationLow — ranking is more reliable than absolute scores
Beam width kkFree once scoredTunable — k=1k=1 is greedy, k=bk=b is exhaustive

Prefer relative rules to absolute ones. "Is this better than that?" is far more reliable than "is this a 7 or a 6?", because ranking needs only the ordering to be right, while thresholding needs the scale itself to mean something.

How it compares to the alternatives

Single chainSample many chains and voteTree-of-Thoughts
ShapeOne pathkk independent pathsBranching tree with shared prefixes
When judgedNeverOnly at the very end, by countingAt every intermediate step
BacktrackingNoneNone — each path is standaloneYes, in the search code
Work shared between attemptsN/ANone — every path redoes everythingYes — a good prefix is expanded, not repeated
Typical cost1×3–10×5–20×
NeedsNothingA single extractable answerA scorable partial state
Best forMost tasksOne right answer, several routes to itSequential decisions where early moves constrain later ones

The row that decides applicability is the last-but-one. Voting needs an answer you can compare; ToT needs something stronger: a half-finished attempt you can meaningfully score. If a partial state tells you nothing about whether the finished one will be good, there is nothing to prune on, and the tree degenerates into an expensive way to sample.

Where people get this wrong

BeliefReality
"ToT makes the model reason better."The model does exactly the same local work. The improvement comes from your code exploring alternatives and discarding bad ones — capabilities the token stream does not have.
"I'll just ask the model to explore a tree of thoughts."One generation narrating a tree is still one linear sequence, with the same commitment and no-undo problems. There is no branching without an external loop.
"Deeper and wider is better."Cost is bdb^d. Beyond the depth at which your evaluator can actually discriminate, extra levels buy noise at exponential prices.
"The evaluator gives objective scores."Scores are generated tokens, subject to the same biases as any other output: longer and more confident proposals score higher, unanchored scales collapse to 7–8, and shallow nodes are systematically over-rated.
"It works on any hard problem."It needs decomposability into discrete steps and a scorable partial state. For "write a compelling opening paragraph" there is no partial state to score, so ToT reduces to expensive sampling.
"More beam width is always safer."Width is linear in cost and, past a point, keeps branches the evaluator has already correctly rejected. Width 2–3 captures most of the benefit in practice.

What this means when you build something

Start with what has changed since Tree-of-Thoughts was published (Yao et al., 2023, which used this same 24 game). Reasoning models now do a version of propose, evaluate and backtrack inside their own thinking: they try an opening, notice it fails, and try another before writing the answer. A current model at a high effort setting often solves puzzles of this size in one call, so an explicit tree is no longer the default fix for "the model commits too early". It still earns its cost when your code can check partial states (a failing test, a broken constraint), when each step is an expensive or real action rather than a thought, when the search is too large for one context, or when you need the whole tree logged for audit.

Reaching for ToT should follow a specific diagnosis, not a wish for better answers. Ask three questions in order:

  1. Does the task decompose into steps where early choices constrain later ones? If the steps are independent there is no tree — just do them.
  2. Can I score a partial state? Not the finished answer — a half-built plan, a half-written function, a half-solved puzzle. If not, ToT has nothing to prune with.
  3. Is any part of the validity check expressible in code? Every constraint checkable in Python is a free prune, and free prunes are what make the exponential affordable.

If all three are yes, ToT usually pays for itself. If the second is no, sample several complete attempts and pick between them — cheaper and better suited.

Then design for the budget from the start. Fix a node cap before the depth, because the cap bounds your bill. Make the search anytime, so a cap that is hit returns the best partial result rather than failing. Log the whole tree, not just the winning path — when a recommendation looks wrong, the question is almost always what got pruned and on what score, and the winning branch cannot answer it. And build the evaluator first: a mediocre proposer with a good evaluator finds decent answers slowly, while a brilliant proposer with a bad evaluator confidently prunes the right branch and never tells you.

The 24 puzzle is the miniature of all this. The winning move — 8÷8=18 \div 8 = 1 — looks like a step backwards and no greedy heuristic proposes it. You find it by being able to try things and take them back. That capability lives in your search loop, not in the model.