Chain-of-Thought and ReAct
Tokens are where the thinking happens. Not a metaphor — a budget.
Download chain_of_thought_from_scratch.py — everything below runs from it. NumPy and plain Python. There is no language model anywhere in it: what the script runs is the mechanism — the per-token compute budget, the loop shape, and the voting.
Blunt about that last bit first, the same way the RAG page is about its retriever. There is no LLM in this script. Nothing it prints is evidence about what GPT-4 does when you say "think step by step"; where something stands in for a model, it's a hand-written state machine and the code says so on the line above.
What it does show is the part I had backwards for two years. I thought chain-of-thought was a psychological trick — flatter the model, make it try harder. It isn't. It's an allocation of compute. Once I saw that, the rest of the agent stack stopped being prompt folklore and became something I could reason about like any other system.
A pure function versus one with a scratch buffer
You've written this function. It has no local variables:
def answer(question):
return LOOKUP_TABLE[question] # one step. That's all you get.
A transformer emitting one token is that function. One forward pass, a fixed number of layers, the same arithmetic whether the question is "what colour is the sky" or a 12-step word problem. It cannot loop internally. There is no while in there.
Chain-of-thought gives it somewhere to put the intermediate values:
def answer(question):
scratch = [] # each entry is another forward pass
for step in range(n):
scratch.append(one_step(question, scratch)) # reads its own output
return scratch[-1]
Every token the model emits gets appended to its own input, and the next forward pass reads it. So the number of tokens you let it emit is the number of serial steps it gets. Nothing else about the model changed.
| Term | Means | Programmer's analogy |
|---|---|---|
| Forward pass | Fixed work per token: N layers, no loops | A function with a fixed call depth |
| Chain-of-thought | Emit intermediate tokens before the answer | A scratch buffer the function can write to and re-read |
| Self-consistency | Sample several paths, take the modal answer | Retry the flaky job 20 times, ship the most common result |
| ReAct | Let the scratch buffer hold a real tool's output | The loop body makes an API call instead of guessing the response |
Why this beat plain few-shot prompting
Standard few-shot prompting, the thing GPT-3 introduced, shows the model input/output pairs and asks for one more output. It works beautifully for classification and translation, and it falls over on arithmetic word problems in a way that looked, for a while, like the models simply couldn't do maths.
They could. They were being asked to do six sequential operations inside one forward pass. The fix wasn't a bigger model or a new loss — it was changing the exemplars so they showed their working, and therefore so did the continuation. No fine-tuning, no gradient updates, no architecture change.
The one formula
Chain-of-thought doesn't really have a formula; it's a prompt format. But self-consistency does, and it's the one worth knowing, because it says what a "reasoning path" is in probability terms:
Read it as pipeline stages:
| Step | What it is | What it does |
|---|---|---|
r | A reasoning path | The intermediate tokens. A latent variable — you don't care which one you got |
a | The final answer | The bit you actually parse out and return |
P(a, r | ·) | Joint probability | One sample from the model is one draw from this |
Σr | Marginalise | Sum over paths. In practice: sample k of them and count |
argmaxa | Take the mode | Majority vote over final answers. That's the whole algorithm |
Plain chain-of-thought is the k=1 case with greedy decoding: one path, take its answer, hope it was a good one. Self-consistency is the same thing with the sum actually done.
The budget is a hard ceiling
The script implements an evaluator that may do exactly one operation per token it emits, and hands it a chain of six operations where each needs the previous one's result. This is an analogy for the depth limit of a fixed-size network, not a measurement of one. I'm labelling that hard because it's the one place on this page where you could mistake a toy for evidence.
Don't take my word for it. Here's the actual output:
token budget 1: steps needed 6, steps available 1 -> answer 7 (WRONG)
token budget 3: steps needed 6, steps available 3 -> answer 32 (WRONG)
token budget 6: steps needed 6, steps available 6 -> answer 63 (correct) ✓
the working, when it is allowed to exist:
token 1: start 7 -> 7
token 2: mul 3 -> 21
token 3: add 11 -> 32
token 4: sub 5 -> 27
token 5: mul 2 -> 54
token 6: add 9 -> 63
There is real theory for this now: Li et al. (2024) prove that constant-depth transformers with a chain of thought can solve serial problems that constant-depth transformers without one provably cannot. My evaluator is a cartoon of their result. Link at the bottom.
The prompts, side by side
Here are both prompts as the script builds them. Look for the difference. It is entirely in the exemplars' answers.
--- STANDARD few-shot ---
Q: A shelf holds 4 boxes. Each box holds 6 mugs. 5 mugs break. How many
are left?
A: The answer is 19.
--- CHAIN-OF-THOUGHT few-shot ---
Q: A shelf holds 4 boxes. Each box holds 6 mugs. 5 mugs break. How many
are left?
A: 4 boxes * 6 mugs = 24 mugs. 24 - 5 broken = 19. The answer is 19.
prompt lengths: standard 301 chars, CoT 400 chars
Ninety-nine characters. That's the paper. (The script prints both full two-exemplar prompts; I've shown one exemplar each to fit the column.)
Related, and frequently misattributed: the zero-shot "Let's think step by step" result is Kojima et al., 2022, not Wei — Wei's paper uses hand-written exemplars throughout. Worth having straight; it's an easy interview trip-up.
The bug that will actually bite you
Turning on chain-of-thought changes the shape of the model's output, and the answer parser you wrote for the old shape does not know that. This is the most common real failure in a CoT pipeline, and it does not throw:
model output:
9 pots * 8 cups = 72 cups. 72 - 14 spilled = 58. The answer is 58.
parse_loose -> 9 <-- first number in the working
parse_strict -> 58 ✓ <-- anchored on 'the answer is'
Under the standard prompt the answer held exactly one number, so re.search(r"\d+") was correct and nobody reviewed it. Under CoT the same regex silently returns an intermediate value, accuracy drops, and the prompt gets blamed for a parser bug.
Self-consistency: why voting works
Wang et al., 2022 — arXiv 2203.11171.
This is the one piece of genuine statistics on the page, and the numbers mean what they say. The script's solver is stochastic: each of the six steps is carried out correctly 80% of the time, and a slip lands on a scattered wrong value. Sample k independent paths, take the modal answer.
The mechanism is one sentence: correct paths all agree on one number; wrong paths disagree with each other. So the mode beats any single draw, even when a single draw is right well under half the time.
per-step accuracy 0.8, 6 steps, 4000 trials, true answer 63
paths (k) accuracy of the vote
1 0.270
3 0.332
5 0.458
11 0.745
21 0.927
41 0.994 ✓
one sample of 11 paths, so you can see the shape of it:
answers: [77, 45, 81, 63, 51, 67, 69, 71, 66, 63, 87]
vote : 63 truth: 63
Note the gains flattening between 21 and 41. You're converging on the distribution's mode, not adding information, so the curve is bounded by "is the mode the right answer at all". Past that, more samples buy nothing but tokens.
And the failure mode people actually ship. Vote over paths that aren't independent and the vote does nothing:
k=21, independent paths : 0.928 ✓
k=21, 21 copies of ONE path : 0.272 ← same as k=1
Same vote, same k, no benefit whatsoever. Sampling at temperature 0, or reusing one cached trace across your "samples", buys the bottom row and a 21× bill. Self-consistency needs independent errors — check that first when voting doesn't help.
ReAct: put a real tool in the scratch buffer
Chain-of-thought gives the model somewhere to write. ReAct's move is to let one of those writes be something the world said, not something the model produced. Thought, action, observation, repeat, until it answers or you cut it off.
Here's a full transcript from the script: two lookups against a real table and one call to a real calculator, both actually executed. The policy choosing the next action is a hand-written state machine — that's the part standing in for a model. The loop is the real thing.
Question: How many more vans does Northwind have than Contoso?
Thought 1: I need Northwind's van count. I should look it up, not recall it.
Action 1: lookup('northwind', 'vans')
Obs 1: 34
Thought 2: Got Northwind. Now Contoso.
Action 2: lookup('contoso', 'vans')
Obs 2: 21
Thought 3: Now subtract: 34 minus 21.
Action 3: calc('34 - 21')
Obs 3: 13
Thought 4: The difference is 13.
Answer: 13
finished in 4 loop iterations, answer = 13 ✓
ground truth = 13
Now remove one line — the one appending the observation to the scratchpad. The tool still gets called and the observation still gets printed. It just never reaches the thing deciding the next step:
with observations fed back : 13 ✓ ← checked
with observations discarded : 15 ← guessed
ground truth : 13
Action 1: lookup('northwind', 'vans')
Obs 1: 34 ← fetched, then thrown away
Action 2: lookup('contoso', 'vans')
Obs 2: 21 ← fetched, then thrown away
Thought 3: Now subtract: 40 minus 25. ← plausible. memorised. wrong.
Answer: 15
That fallback is a hardcoded number standing in for a memorised guess — I'm not claiming a model would produce 40. What the transcript shows is the shape of the failure: the fact was available, the call was made, and the answer still came from memory, because nothing wired the result back into the next step. Yao et al. report reduced hallucination for ReAct relative to chain-of-thought alone on the tasks they tested, and this is the plumbing reason why.
Put a limit on the loop
A ReAct loop with no step count is a while True with a credit card attached. Point it at a tool that keeps returning nothing and it never terminates on its own — the policy has no reason to stop asking:
max_steps=None: still looping after 200 iterations, no answer, no end
tool calls made: 200, distinct observations: 1
max_steps=5 : guard fired at step 5, returned None ✓
Two hundred identical tool calls, one distinct observation. The guard is three lines. Return None and let the caller decide what a non-answer means — don't let the loop decide for itself, and don't let it invent an answer to escape.
முதலில் அந்தக் கடைசி வரியைப் பற்றி நேரடியாகச் சொல்லிவிடுகிறேன், RAG பக்கம் தன் retriever-ஐப் பற்றிச் சொல்வது போலவே. இந்த script-ல் எந்த LLM-ம் இல்லை. "step by step யோசி" என்று நீங்கள் சொல்லும்போது GPT-4 என்ன செய்கிறது என்பதற்கு இது அச்சிடும் எதுவும் ஆதாரம் இல்லை; எங்கே ஒன்று model-க்குப் பதிலாக நிற்கிறதோ, அது கையால் எழுதப்பட்ட ஒரு state machine, அதை மேல் வரியிலேயே code சொல்லிவிடுகிறது.
இது காட்டுவது என்னவென்றால் — இரண்டு வருடங்களாக நான் தலைகீழாகப் புரிந்து வைத்திருந்த பகுதி. chain-of-thought என்பது ஒரு உளவியல் தந்திரம், model-ஐப் புகழ்ந்து அதிகம் முயற்சி செய்ய வைப்பது என்று நினைத்தேன். இல்லை. அது ஒரு compute ஒதுக்கீடு. இதைப் பார்த்த பிறகுதான், மீதி agent stack prompt நாட்டுப்புறக் கதையாக இல்லாமல், வேறு எந்த system-ஐயும் போல நான் யோசிக்கக்கூடிய ஒன்றாக மாறியது.
Pure function vs. scratch buffer உள்ள function
இந்த function-ஐ நீங்கள் எழுதியிருக்கிறீர்கள். இதில் local variables இல்லை:
def answer(question):
return LOOKUP_TABLE[question] # ஒரே ஒரு step. அவ்வளவுதான்.
ஒரு token-ஐ வெளியிடும் transformer இந்த function தான். ஒரு forward pass,
நிர்ணயிக்கப்பட்ட layer எண்ணிக்கை, "வானம் என்ன நிறம்" என்றாலும் 12-step
கணக்கு என்றாலும் அதே arithmetic. உள்ளே loop போட முடியாது. அதற்குள் ஒரு
while கிடையாது.
Chain-of-thought இடைநிலை மதிப்புகளை வைக்க ஓர் இடம் தருகிறது:
def answer(question):
scratch = [] # ஒவ்வொரு entry-ம் இன்னொரு forward pass
for step in range(n):
scratch.append(one_step(question, scratch)) # தன் output-ஐயே படிக்கிறது
return scratch[-1]
Model வெளியிடும் ஒவ்வொரு token-ம் அதன் சொந்த input-உடன் சேர்க்கப்படுகிறது, அடுத்த forward pass அதைப் படிக்கிறது. எனவே எத்தனை tokens வெளியிட அனுமதிக்கிறீர்களோ, அத்தனைதான் அதற்குக் கிடைக்கும் serial steps. Model-ல் வேறு எதுவும் மாறவில்லை.
Oru token = fixed compute. Model loop panna mudiyaadhu. Chain-of-thought kudukkardhu extra time illa — extra steps. Tokens dhaan scratch buffer.
| சொல் | அர்த்தம் | Programmer உதாரணம் |
|---|---|---|
| Forward pass | Token ஒன்றுக்கு நிர்ணயிக்கப்பட்ட வேலை: N layers, loop இல்லை | நிர்ணயிக்கப்பட்ட call depth உள்ள function |
| Chain-of-thought | விடைக்கு முன் இடைநிலை tokens வெளியிடுவது | Function எழுதி மீண்டும் படிக்கக்கூடிய scratch buffer |
| Self-consistency | பல பாதைகளை sample செய்து, அதிகம் வரும் விடையை எடு | Flaky job-ஐ 20 முறை retry செய்து, அதிகம் வந்த முடிவை ship செய் |
| ReAct | Scratch buffer-ல் நிஜ tool-ன் output இருக்கட்டும் | Loop body யூகிக்காமல் ஓர் API call செய்கிறது |
சாதாரண few-shot prompting-ஐ இது ஏன் வென்றது
GPT-3 அறிமுகப்படுத்திய standard few-shot prompting, model-க்கு input/output ஜோடிகளைக் காட்டி இன்னொரு output கேட்கிறது. Classification, translation-க்கு இது அற்புதமாக வேலை செய்கிறது; ஆனால் arithmetic word problems-ல் விழுந்துவிடுகிறது — சிறிது காலம் அது models-க்குக் கணக்கே தெரியாது என்பது போலத் தோன்றியது.
தெரியும். ஒரே forward pass-க்குள் ஆறு தொடர்ச்சியான operations செய்யச் சொல்லப்பட்டிருந்தது, அவ்வளவுதான். தீர்வு பெரிய model-ம் இல்லை, புது loss-ம் இல்லை — exemplars-ஐ மாற்றி அவை வேலையைக் காட்டச் செய்தது, அதனால் continuation-ம் காட்டியது. Fine-tuning இல்லை, gradient updates இல்லை, architecture மாற்றம் இல்லை.
அந்த ஒரு சூத்திரம்
Chain-of-thought-க்கு உண்மையில் ஒரு சூத்திரம் கிடையாது; அது ஒரு prompt format. ஆனால் self-consistency-க்கு உண்டு, அதைத் தெரிந்துகொள்வது மதிப்புள்ளது — ஏனெனில் "reasoning path" என்றால் என்ன என்பதை probability மொழியில் சொல்கிறது:
இதை pipeline stages-ஆகப் படியுங்கள்:
| Step | என்ன | என்ன செய்கிறது |
|---|---|---|
r | ஒரு reasoning path | இடைநிலை tokens. ஒரு latent variable — எது கிடைத்தது என்பது முக்கியமில்லை |
a | இறுதி விடை | நீங்கள் உண்மையில் parse செய்து திருப்பித் தரும் பகுதி |
P(a, r | ·) | Joint probability | Model-லிருந்து ஒரு sample என்பது இதிலிருந்து ஒரு draw |
Σr | Marginalise | பாதைகள் மேல் கூட்டு. நடைமுறையில்: k பாதைகளை sample செய்து எண்ணு |
argmaxa | Mode-ஐ எடு | இறுதி விடைகள் மேல் majority vote. அவ்வளவுதான் algorithm |
சாதாரண chain-of-thought என்பது greedy decoding-உடன் k=1 நிலை: ஒரு பாதை, அதன் விடையை எடு, அது நல்லதாக இருந்திருக்கும் என்று நம்பு. Self-consistency என்பது அதே விஷயம், கூட்டலை உண்மையிலேயே செய்தது.
Budget என்பது கடினமான உச்சவரம்பு
வெளியிடும் ஒவ்வொரு token-க்கும் சரியாக ஒரு operation மட்டுமே செய்யக்கூடிய ஒரு evaluator-ஐ script உருவாக்குகிறது, பிறகு ஆறு operations கொண்ட ஒரு சங்கிலியைக் கொடுக்கிறது — ஒவ்வொன்றுக்கும் முந்தையதன் முடிவு தேவை. இது நிர்ணய அளவு network-ன் depth limit-க்கான ஓர் உவமை, அதன் அளவீடு அல்ல. இதைக் கடினமாகவே குறிக்கிறேன், ஏனெனில் இந்தப் பக்கத்தில் ஒரு விளையாட்டுக் கருவியை ஆதாரம் என்று தவறாக நினைக்கக்கூடிய ஒரே இடம் இதுதான்.
என் சொல்லை நம்ப வேண்டாம். இதோ உண்மையான output:
token budget 1: steps needed 6, steps available 1 -> answer 7 (WRONG)
token budget 3: steps needed 6, steps available 3 -> answer 32 (WRONG)
token budget 6: steps needed 6, steps available 6 -> answer 63 (correct) ✓
the working, when it is allowed to exist:
token 1: start 7 -> 7
token 2: mul 3 -> 21
token 3: add 11 -> 32
token 4: sub 5 -> 27
token 5: mul 2 -> 54
token 6: add 9 -> 63
இதற்கு இப்போது உண்மையான theory இருக்கிறது: constant-depth transformers, chain of thought-உடன், அது இல்லாத constant-depth transformers நிரூபிக்கப்பட்ட விதத்தில் தீர்க்க முடியாத serial problems-ஐத் தீர்க்கும் என்று Li et al. (2024) நிரூபிக்கிறார்கள். என் evaluator அவர்கள் முடிவின் ஒரு cartoon. கீழே link.
இரண்டு prompts, அருகருகே
Script உருவாக்கும் இரண்டு prompts-ம் இதோ. வித்தியாசத்தைத் தேடுங்கள். அது முழுக்க முழுக்க exemplars-ன் விடைகளில் மட்டுமே இருக்கிறது.
--- STANDARD few-shot ---
Q: A shelf holds 4 boxes. Each box holds 6 mugs. 5 mugs break. How many
are left?
A: The answer is 19.
--- CHAIN-OF-THOUGHT few-shot ---
Q: A shelf holds 4 boxes. Each box holds 6 mugs. 5 mugs break. How many
are left?
A: 4 boxes * 6 mugs = 24 mugs. 24 - 5 broken = 19. The answer is 19.
prompt lengths: standard 301 chars, CoT 400 chars
தொண்ணூற்றொன்பது எழுத்துகள். அதுதான் paper. (Script இரண்டு exemplar கொண்ட முழு prompts-ஐயும் அச்சிடுகிறது; column-ல் அடங்க ஒவ்வொன்றிலும் ஒரு exemplar மட்டும் காட்டியிருக்கிறேன்.)
இதனுடன் தொடர்புடையது, அடிக்கடி தவறாக யாருக்கோ சேர்க்கப்படுவது: zero-shot "Let's think step by step" முடிவு Kojima et al., 2022, Wei அல்ல — Wei-ன் paper முழுவதும் கையால் எழுதிய exemplars-ஐப் பயன்படுத்துகிறது. இதைத் தெளிவாக வைத்திருங்கள்; interview-ல் எளிதில் தடுக்கி விழ வைக்கும்.
உண்மையில் உங்களைக் கடிக்கப் போகும் bug
Chain-of-thought-ஐ ஆன் செய்தால் model-ன் output-ன் வடிவம் மாறுகிறது, பழைய வடிவத்துக்கு நீங்கள் எழுதிய answer parser-க்கு அது தெரியாது. CoT pipeline-ல் இதுதான் மிகச் சாதாரணமான நிஜத் தோல்வி, இது exception-ம் எறியாது:
model output:
9 pots * 8 cups = 72 cups. 72 - 14 spilled = 58. The answer is 58.
parse_loose -> 9 <-- first number in the working
parse_strict -> 58 ✓ <-- anchored on 'the answer is'
Standard prompt-ல் விடையில் சரியாக ஒரே ஒரு எண் இருந்தது, எனவே
re.search(r"\d+") சரியாக இருந்தது, யாரும் அதை மறுபரிசீலனை
செய்யவில்லை. CoT-ல் அதே regex அமைதியாக ஓர் இடைநிலை மதிப்பைத் திருப்பித்
தருகிறது, accuracy குறைகிறது, குற்றம் prompt மேல் விழுகிறது.
Self-consistency: voting ஏன் வேலை செய்கிறது
Wang et al., 2022 — arXiv 2203.11171.
இந்தப் பக்கத்தில் உண்மையான புள்ளியியல் இருக்கும் ஒரே இடம் இதுதான், இந்த எண்கள் சொல்வது சரியானதே. Script-ன் solver stochastic: ஆறு steps-ல் ஒவ்வொன்றும் 80% நேரம் சரியாகச் செய்யப்படுகிறது, தவறினால் சிதறிய ஒரு தவறான மதிப்பில் விழுகிறது. k தனித்தனிப் பாதைகளை sample செய்து, அதிகம் வரும் விடையை எடுங்கள்.
Mechanism ஒரே வாக்கியம்: சரியான பாதைகள் அனைத்தும் ஒரே எண்ணில் ஒத்துப்போகின்றன; தவறான பாதைகள் ஒன்றோடொன்று மாறுபடுகின்றன. எனவே ஒரு தனித்த draw சரியாக இருப்பது பாதிக்கும் குறைவான நேரமாக இருந்தாலும், mode எந்த ஒரு draw-ஐயும் விட சிறந்தது.
per-step accuracy 0.8, 6 steps, 4000 trials, true answer 63
paths (k) accuracy of the vote
1 0.270
3 0.332
5 0.458
11 0.745
21 0.927
41 0.994 ✓
one sample of 11 paths, so you can see the shape of it:
answers: [77, 45, 81, 63, 51, 67, 69, 71, 66, 63, 87]
vote : 63 truth: 63
21-க்கும் 41-க்கும் இடையே ஆதாயம் தட்டையாவதைக் கவனியுங்கள். நீங்கள் distribution-ன் mode-ஐ நோக்கிச் சேர்கிறீர்கள், தகவலைச் சேர்க்கவில்லை — எனவே வளைவு "mode உண்மையில் சரியான விடையா" என்பதால் வரையறுக்கப்படுகிறது. அதற்குப் பிறகு அதிக samples tokens-ஐத் தவிர வேறெதையும் வாங்கித் தராது.
இப்போது மக்கள் உண்மையில் ship செய்யும் தோல்வி. சுதந்திரமாக இல்லாத பாதைகள் மேல் vote செய்தால், vote எதுவும் செய்யாது:
k=21, independent paths : 0.928 ✓
k=21, 21 copies of ONE path : 0.272 ← same as k=1
அதே vote, அதே k, எந்தப் பயனும் இல்லை. Temperature 0-ல் sample செய்வது, அல்லது உங்கள் "samples" முழுவதும் ஒரே cached trace-ஐ மறுபயன்படுத்துவது — கடைசி வரிசையையும் 21× bill-ஐயும் வாங்கித் தரும். Self-consistency-க்கு சுதந்திரமான தவறுகள் தேவை — voting உதவாதபோது முதலில் இதைச் சரிபாருங்கள்.
Correct paths ellaam ore number-la agree aagum; wrong paths thani thaniya poagum. Adhaan majority vote velai seiyaradhu — aana paths independent-a irundhaa mattum dhaan.
ReAct: scratch buffer-ல் ஒரு நிஜ tool-ஐ வையுங்கள்
Chain-of-thought model-க்கு எழுத ஓர் இடம் தருகிறது. ReAct-ன் நகர்வு என்னவென்றால், அந்த எழுத்துகளில் ஒன்று model உருவாக்கியது அல்லாமல் உலகம் சொன்னது ஆக இருக்கட்டும் என்பது. Thought, action, observation, மீண்டும் — அது பதில் சொல்லும் வரை, அல்லது நீங்கள் நிறுத்தும் வரை.
Script-லிருந்து ஒரு முழு transcript இதோ: ஒரு நிஜ table மேல் இரண்டு lookups, ஒரு நிஜ calculator-க்கு ஒரு call — இரண்டும் உண்மையிலேயே நடக்கின்றன. அடுத்த action-ஐத் தேர்வு செய்யும் policy கையால் எழுதப்பட்ட ஒரு state machine — அதுதான் model-க்குப் பதிலாக நிற்கும் பகுதி. Loop மட்டும் நிஜம்.
Question: How many more vans does Northwind have than Contoso?
Thought 1: I need Northwind's van count. I should look it up, not recall it.
Action 1: lookup('northwind', 'vans')
Obs 1: 34
Thought 2: Got Northwind. Now Contoso.
Action 2: lookup('contoso', 'vans')
Obs 2: 21
Thought 3: Now subtract: 34 minus 21.
Action 3: calc('34 - 21')
Obs 3: 13
Thought 4: The difference is 13.
Answer: 13
finished in 4 loop iterations, answer = 13 ✓
ground truth = 13
இப்போது ஒரே ஒரு வரியை நீக்குங்கள் — observation-ஐ scratchpad-ல் சேர்க்கும் வரி. Tool இன்னும் அழைக்கப்படுகிறது, observation இன்னும் அச்சிடப்படுகிறது. அது அடுத்த step-ஐத் தீர்மானிக்கும் பொருளை மட்டும் சென்றடைவதில்லை:
with observations fed back : 13 ✓ ← checked
with observations discarded : 15 ← guessed
ground truth : 13
Action 1: lookup('northwind', 'vans')
Obs 1: 34 ← fetched, then thrown away
Action 2: lookup('contoso', 'vans')
Obs 2: 21 ← fetched, then thrown away
Thought 3: Now subtract: 40 minus 25. ← plausible. memorised. wrong.
Answer: 15
அந்த fallback ஒரு hardcoded எண், மனப்பாடமான ஒரு யூகத்துக்குப் பதிலாக நிற்கிறது — ஒரு model 40 என்று தரும் என்று நான் சொல்லவில்லை. Transcript காட்டுவது தோல்வியின் வடிவம்: உண்மை கிடைக்கும் நிலையில் இருந்தது, call செய்யப்பட்டது, இருந்தும் விடை நினைவிலிருந்தே வந்தது — ஏனெனில் முடிவை அடுத்த step-உடன் எதுவும் இணைக்கவில்லை. அவர்கள் சோதித்த tasks-ல் chain-of-thought மட்டும் இருப்பதோடு ஒப்பிடும்போது ReAct-க்கு hallucination குறைவாக இருந்ததாக Yao et al. அறிக்கை செய்கிறார்கள்; அதற்கான குழாய்ப் பணிக் காரணம் இதுதான்.
Loop-க்கு ஒரு எல்லை வையுங்கள்
Step எண்ணிக்கை இல்லாத ஒரு ReAct loop என்பது credit card இணைக்கப்பட்ட ஒரு
while True. எதையும் திருப்பித் தராத ஒரு tool-ஐ நோக்கிக்
காட்டுங்கள், அது தானாக ஒருபோதும் நிற்காது — policy-க்குக் கேட்பதை நிறுத்த
எந்தக் காரணமும் இல்லை:
max_steps=None: still looping after 200 iterations, no answer, no end
tool calls made: 200, distinct observations: 1
max_steps=5 : guard fired at step 5, returned None ✓
இருநூறு ஒரே மாதிரியான tool calls, ஒரே ஒரு தனித்த observation. Guard மூன்று
வரிகள். None-ஐத் திருப்பித் தந்து, பதில் இல்லாதது என்றால் என்ன
என்பதைக் காலர் முடிவு செய்யட்டும் — loop தானே முடிவு செய்ய விடாதீர்கள்,
தப்பிக்க ஒரு விடையைக் கண்டுபிடிக்கவும் விடாதீர்கள்.
पहले उस आख़िरी बात को साफ़ कह देता हूँ, ठीक वैसे ही जैसे RAG वाला पन्ना अपने retriever के बारे में कहता है। इस script में कोई LLM नहीं है। जब आप "step by step सोचो" कहते हैं तो GPT-4 क्या करता है — इसका कोई सबूत यह script जो छापती है उसमें नहीं है; जहाँ कोई चीज़ model की जगह खड़ी है, वह हाथ से लिखी हुई एक state machine है, और code ऊपर वाली लाइन में यह बात लिख देता है।
यह जो दिखाती है, वह हिस्सा मैं दो साल तक उल्टा समझता रहा। मुझे लगता था chain-of-thought एक मनोवैज्ञानिक चाल है — model की तारीफ़ करो, वह ज़्यादा कोशिश करेगा। ऐसा नहीं है। यह compute का आवंटन है। जिस दिन यह दिखा, उस दिन से बाक़ी agent stack prompt की लोककथा नहीं रहा — किसी भी दूसरे system की तरह सोचने लायक़ चीज़ बन गया।
Pure function बनाम scratch buffer वाला function
यह function आपने लिखा है। इसमें कोई local variable नहीं है:
def answer(question):
return LOOKUP_TABLE[question] # एक ही step. बस इतना ही.
एक token निकालता transformer यही function है। एक forward pass, तय संख्या
में layers, वही अंकगणित — चाहे सवाल "आसमान किस रंग का है" हो या 12 step
वाला word problem। वह अंदर loop नहीं चला सकता। उसके भीतर कोई
while है ही नहीं।
Chain-of-thought उसे बीच के मान रखने की जगह देता है:
def answer(question):
scratch = [] # हर entry एक और forward pass है
for step in range(n):
scratch.append(one_step(question, scratch)) # अपना ही output पढ़ता है
return scratch[-1]
Model जो भी token निकालता है वह उसके अपने input में जुड़ जाता है, और अगला forward pass उसे पढ़ता है। यानी आप जितने tokens निकालने देते हैं, उतने ही serial steps उसे मिलते हैं। Model में और कुछ नहीं बदला।
| शब्द | मतलब | Programmer की उपमा |
|---|---|---|
| Forward pass | हर token पर तय काम: N layers, कोई loop नहीं | तय call depth वाला function |
| Chain-of-thought | जवाब से पहले बीच के tokens निकालना | Scratch buffer जिसमें function लिख और दोबारा पढ़ सके |
| Self-consistency | कई paths sample करो, सबसे आम जवाब लो | Flaky job 20 बार retry करो, सबसे आम नतीजा ship करो |
| ReAct | Scratch buffer में असली tool का output रखने दो | Loop body अनुमान लगाने के बजाय API call करती है |
यह सादे few-shot prompting से बेहतर क्यों निकला
Standard few-shot prompting, जिसे GPT-3 लाया था, model को input/output जोड़े दिखाकर एक और output माँगता है। Classification और translation पर यह शानदार चलता है, और arithmetic word problems पर ऐसे गिरता है कि कुछ समय तक लगा models को गणित आता ही नहीं।
आता था। उनसे एक ही forward pass के भीतर छह क्रमिक operations करवाए जा रहे थे। इलाज न बड़ा model था, न नया loss — exemplars बदलकर उनमें काम दिखाया गया, इसलिए continuation ने भी दिखाया। न fine-tuning, न gradient updates, न architecture में कोई बदलाव।
वह एक सूत्र
Chain-of-thought का असल में कोई सूत्र नहीं है; वह एक prompt format है। लेकिन self-consistency का है, और वही जानने लायक़ है — क्योंकि वह बताता है कि probability की भाषा में "reasoning path" होता क्या है:
इसे pipeline के चरणों की तरह पढ़िए:
| Step | क्या है | क्या करता है |
|---|---|---|
r | एक reasoning path | बीच के tokens। एक latent variable — कौन सा मिला, इससे फ़र्क़ नहीं |
a | आख़िरी जवाब | वही हिस्सा जिसे आप parse करके लौटाते हैं |
P(a, r | ·) | Joint probability | Model से एक sample यानी इसी से एक draw |
Σr | Marginalise | Paths पर जोड़ो। व्यवहार में: k paths sample करो और गिनो |
argmaxa | Mode लो | आख़िरी जवाबों पर majority vote। पूरा algorithm यही है |
सादा chain-of-thought greedy decoding के साथ k=1 वाली स्थिति है: एक path, उसका जवाब लो, और उम्मीद करो कि वह अच्छा path था। Self-consistency वही चीज़ है, बस जोड़ सचमुच किया गया है।
Budget एक सख़्त छत है
Script एक ऐसा evaluator बनाती है जो हर निकाले गए token पर ठीक एक operation कर सकता है, और उसे छह operations की एक शृंखला देती है जिसमें हर operation को पिछले का नतीजा चाहिए। यह किसी तय आकार के network की depth limit की उपमा है, उसका माप नहीं। इसे मैं जान-बूझकर मोटे अक्षरों में लिख रहा हूँ, क्योंकि इस पन्ने पर यही एक जगह है जहाँ आप एक खिलौने को सबूत समझ सकते हैं।
मेरी बात पर भरोसा मत कीजिए। यह रहा असली output:
token budget 1: steps needed 6, steps available 1 -> answer 7 (WRONG)
token budget 3: steps needed 6, steps available 3 -> answer 32 (WRONG)
token budget 6: steps needed 6, steps available 6 -> answer 63 (correct) ✓
the working, when it is allowed to exist:
token 1: start 7 -> 7
token 2: mul 3 -> 21
token 3: add 11 -> 32
token 4: sub 5 -> 27
token 5: mul 2 -> 54
token 6: add 9 -> 63
इसका अब असली theory भी है: Li et al. (2024) सिद्ध करते हैं कि chain of thought वाले constant-depth transformers ऐसे serial problems हल कर सकते हैं जो उसके बिना वाले constant-depth transformers प्रमाणित रूप से नहीं कर सकते। मेरा evaluator उनके नतीजे का cartoon भर है। Link नीचे है।
दोनों prompts, आमने-सामने
यह रहे दोनों prompts, जैसे script उन्हें बनाती है। फ़र्क़ ढूँढिए। वह पूरी तरह exemplars के जवाबों में है।
--- STANDARD few-shot ---
Q: A shelf holds 4 boxes. Each box holds 6 mugs. 5 mugs break. How many
are left?
A: The answer is 19.
--- CHAIN-OF-THOUGHT few-shot ---
Q: A shelf holds 4 boxes. Each box holds 6 mugs. 5 mugs break. How many
are left?
A: 4 boxes * 6 mugs = 24 mugs. 24 - 5 broken = 19. The answer is 19.
prompt lengths: standard 301 chars, CoT 400 chars
निन्यानबे characters। यही पूरा paper है। (Script दोनों पूरे दो-exemplar वाले prompts छापती है; column में समाने के लिए मैंने हर एक का एक ही exemplar दिखाया है।)
इससे जुड़ी और अक्सर ग़लत नाम से जोड़ी जाने वाली बात: zero-shot "Let's think step by step" वाला नतीजा Kojima et al., 2022 का है, Wei का नहीं — Wei का paper पूरे समय हाथ से लिखे exemplars इस्तेमाल करता है। इसे साफ़ रखिए; interview में आसानी से फिसलाने वाली बात है।
वह bug जो सचमुच आपको काटेगा
Chain-of-thought चालू करने से model के output का आकार बदल जाता है, और पुराने आकार के लिए लिखा आपका answer parser यह नहीं जानता। CoT pipeline में यही सबसे आम असली failure है, और यह exception भी नहीं फेंकती:
model output:
9 pots * 8 cups = 72 cups. 72 - 14 spilled = 58. The answer is 58.
parse_loose -> 9 <-- first number in the working
parse_strict -> 58 ✓ <-- anchored on 'the answer is'
Standard prompt में जवाब में ठीक एक संख्या होती थी, इसलिए
re.search(r"\d+") सही था और किसी ने उसकी समीक्षा नहीं की। CoT
में वही regex चुपचाप बीच का कोई मान लौटा देता है, accuracy गिरती है, और
इल्ज़ाम prompt पर आ जाता है।
Self-consistency: voting काम क्यों करती है
Wang et al., 2022 — arXiv 2203.11171.
इस पन्ने पर असली आँकड़ों वाला यही एक हिस्सा है, और ये संख्याएँ जो कहती हैं वही मतलब रखती हैं। Script का solver stochastic है: छह steps में हर step 80% बार सही होता है, और चूक होने पर जवाब किसी बिखरे हुए ग़लत मान पर गिरता है। k स्वतंत्र paths sample कीजिए और सबसे आम जवाब लीजिए।
Mechanism एक वाक्य का है: सही paths सब एक ही संख्या पर सहमत होते हैं; ग़लत paths आपस में असहमत होते हैं। इसलिए mode किसी भी एक draw से बेहतर निकलता है — तब भी, जब एक draw आधे से काफ़ी कम बार सही होता हो।
per-step accuracy 0.8, 6 steps, 4000 trials, true answer 63
paths (k) accuracy of the vote
1 0.270
3 0.332
5 0.458
11 0.745
21 0.927
41 0.994 ✓
one sample of 11 paths, so you can see the shape of it:
answers: [77, 45, 81, 63, 51, 67, 69, 71, 66, 63, 87]
vote : 63 truth: 63
ध्यान दीजिए कि 21 और 41 के बीच फ़ायदा सपाट हो जाता है। आप distribution के mode की ओर पहुँच रहे हैं, नई जानकारी नहीं जोड़ रहे — इसलिए curve की सीमा यही है कि "mode सही जवाब है भी या नहीं"। उसके आगे ज़्यादा samples सिर्फ़ tokens ख़रीदते हैं।
और वह failure जो लोग सचमुच ship करते हैं। ऐसे paths पर vote कीजिए जो स्वतंत्र नहीं हैं, तो vote कुछ नहीं करती:
k=21, independent paths : 0.928 ✓
k=21, 21 copies of ONE path : 0.272 ← same as k=1
वही vote, वही k, फ़ायदा शून्य। Temperature 0 पर sample करना, या अपने "samples" में एक ही cached trace दोबारा इस्तेमाल करना — यह आपको नीचे वाली पंक्ति और 21× का bill देता है। Self-consistency को स्वतंत्र ग़लतियाँ चाहिए — जब voting मदद न करे, सबसे पहले यही जाँचिए।
ReAct: scratch buffer में एक असली tool रखिए
Chain-of-thought model को लिखने की जगह देता है। ReAct की चाल यह है कि उन लिखावटों में से एक दुनिया ने जो कहा वह हो, model ने जो बनाया वह नहीं। Thought, action, observation, फिर दोहराओ — जब तक वह जवाब न दे दे, या आप उसे रोक न दें।
Script से एक पूरा transcript: एक असली table पर दो lookups और एक असली calculator को एक call, दोनों सचमुच चले। अगला action चुनने वाली policy हाथ से लिखी state machine है — वही हिस्सा model की जगह खड़ा है। Loop असली है।
Question: How many more vans does Northwind have than Contoso?
Thought 1: I need Northwind's van count. I should look it up, not recall it.
Action 1: lookup('northwind', 'vans')
Obs 1: 34
Thought 2: Got Northwind. Now Contoso.
Action 2: lookup('contoso', 'vans')
Obs 2: 21
Thought 3: Now subtract: 34 minus 21.
Action 3: calc('34 - 21')
Obs 3: 13
Thought 4: The difference is 13.
Answer: 13
finished in 4 loop iterations, answer = 13 ✓
ground truth = 13
अब एक लाइन हटा दीजिए — वह जो observation को scratchpad में जोड़ती है। Tool फिर भी call होता है और observation फिर भी छपता है। बस वह उस चीज़ तक कभी नहीं पहुँचता जो अगला क़दम तय करती है:
with observations fed back : 13 ✓ ← checked
with observations discarded : 15 ← guessed
ground truth : 13
Action 1: lookup('northwind', 'vans')
Obs 1: 34 ← fetched, then thrown away
Action 2: lookup('contoso', 'vans')
Obs 2: 21 ← fetched, then thrown away
Thought 3: Now subtract: 40 minus 25. ← plausible. memorised. wrong.
Answer: 15
वह fallback एक hardcoded संख्या है जो रटे हुए अनुमान की जगह खड़ी है — मैं यह दावा नहीं कर रहा कि कोई model 40 निकालेगा। Transcript जो दिखाता है वह failure का आकार है: तथ्य उपलब्ध था, call हुई भी, और फिर भी जवाब याददाश्त से आया, क्योंकि नतीजे को अगले क़दम से किसी ने जोड़ा ही नहीं। Yao et al. बताते हैं कि जिन tasks पर उन्होंने परखा, वहाँ अकेले chain-of-thought की तुलना में ReAct में hallucination कम हुई — और plumbing वाली वजह यही है।
Loop पर एक सीमा लगाइए
बिना step count वाला ReAct loop एक while True है जिससे credit
card जुड़ा है। उसे ऐसे tool की तरफ़ मोड़ दीजिए जो कुछ नहीं लौटाता, और वह अपने
आप कभी नहीं रुकेगा — policy के पास पूछना बंद करने की कोई वजह ही नहीं:
max_steps=None: still looping after 200 iterations, no answer, no end
tool calls made: 200, distinct observations: 1
max_steps=5 : guard fired at step 5, returned None ✓
दो सौ एक जैसी tool calls, एक ही अलग observation। Guard तीन लाइनों का है।
None लौटाइए और caller को तय करने दीजिए कि जवाब न मिलना क्या
मतलब रखता है — loop को ख़ुद तय मत करने दीजिए, और बचने के लिए जवाब गढ़ने
भी मत दीजिए।
The code
NumPy and plain Python. No torch, no autograd, no model of any kind — the demos are the mechanism, printed. Every function short enough to read in one sitting.
Download the full script — 535 lines including comments, with eight runnable demos. Then:
python3 chain_of_thought_from_scratch.py
def solve_with_token_budget(problem, budget):
"""Emit at most `budget` tokens; one op each; answer with what you have.
Returns (answer, tokens_emitted, steps_needed, scratchpad).
The scratchpad IS the emitted tokens -- it is the only memory there is.
"""
steps_needed = len(problem)
scratch = []
state = None
for op in problem[:budget]:
state = apply_op(state, op)
scratch.append(f"{op[0]} {op[1]} -> {state}")
return state, min(budget, steps_needed), steps_needed, scratch
def react_loop(policy, question, max_steps=8, feed_observations=True,
verbose=True):
"""Run thought/action/observation until the policy answers or we cut off.
max_steps=None means no guard. Nothing stops it.
"""
scratch = [] # the ONLY thing the policy gets to condition on
steps = 0
while max_steps is None or steps < max_steps:
steps += 1
thought, action = policy(question, scratch)
if action[0] == "finish":
return action[1], steps, scratch
obs = run_action(action)
# THE line that matters. With feed_observations=False the fact the
# tool just fetched never reaches the scratchpad, so the next
# thought is conditioned on a tool result the policy never saw.
if feed_observations or action[0] == "calc":
scratch.append((action, obs))
else:
scratch.append((action, None))
return None, steps, scratch
Now break it
This is the part that actually makes it stick. Reading won't do it. You debug for a living — reverse-engineering from broken behaviour is your native mode, and every row below is something you can do to this file in under a minute.
| Break this | Watch for |
|---|---|
In demo_1, ask for the answer immediately: budget 1 instead of 6 | Answer 7 instead of 63. Six serial steps needed, one available. The budget ran out. |
Swap parse_strict for parse_loose in your answer extraction | Returns 9, not 58 — a number from inside the working. Silent. No exception. Your eval drops and the prompt gets blamed. |
In demo_5, reuse one seed for all 21 paths | Vote accuracy falls from 0.928 to 0.272 — exactly the k=1 number. Voting over correlated paths adds nothing. |
Call react_loop(..., feed_observations=False) | The tool is still called and still prints Obs 1: 34, but the loop answers 15 instead of 13. Fetched, then ignored. |
Call react_loop(..., max_steps=None) with missing_key_policy | 200 identical tool calls, one distinct observation, no termination. With max_steps=5 the guard fires and returns None. |
Set STEP_CORRECT_P = 0.35 and rerun demo_4 | The accuracy column stays near zero at every k. The mode of the samples is no longer the right answer, so more paths just converge on a wrong one, confidently. |
On the maths anxiety
Good news: these two papers need almost none. No new loss, no new layer, no derivation. Chain-of-thought is a prompt format; ReAct is a while loop. If you can read the code above, you have read the methods.
The one bit that earns its keep is self-consistency, and it's first-year probability: a joint distribution over (answer, reasoning path), and marginalising — summing over the paths because you only care about the answer. "Marginalise" is a scary word for "group by the column you care about and sum the rest away". You've written that query.
What's genuinely hard here isn't maths at all, it's systems: token budgets, latency (k=41 paths is 41× the cost), tool timeouts, loop guards, answer parsing, and deciding when a non-answer beats a guess. That's engineering, and it's the half the papers don't cover.
Where to go next
This page gave you one angle: chain-of-thought as compute allocation, ReAct as the loop that makes the scratchpad factual. It skipped the experimental evidence entirely, because I can't produce that with NumPy. These four can.
-
Chain-of-Thought Prompting Elicits Reasoning in Large Language Models
Read the method section, then go straight to the scale ablation. That plot — benefit appearing only past a size threshold, small models doing worse with CoT than without — is the finding, and it's the one thing here my script cannot show you.
-
ReAct: Synergizing Reasoning and Acting in Language Models
The method section is short and you've already run its state machine. Go for the failure analysis instead: reasoning-only versus acting-only versus interleaved, and where each goes wrong. That comparison is the argument.
-
Chain of Thought Empowers Transformers to Solve Inherently Serial Problems
The proof of the thing my demo 1 only mimes: constant-depth transformers with a chain of thought can compute functions that ones without provably cannot. Circuit complexity, so a real step up — but the intro alone is worth it for the framing.
-
LLM Powered Autonomous Agents
Where ReAct sits in the wider agent picture: planning, reflection, memory, tool APIs. My page stops at one loop with one calculator; this is the survey of what got built on top of it, with the failure modes named honestly.