Phase 1: Advanced Prompt Engineering for Agents · 55 min · Python · Anthropic SDK
The Problem
You ask an agent to analyze a complex contract and recommend the risk level. It outputs "High risk" — two words. You have no idea why. When a lawyer asks you to explain the recommendation, you can't. When it produces a wrong answer next week, you can't debug it. You consider logging it, but there's nothing to log.
The core problem isn't accuracy — it's opacity. A "High risk" answer with no reasoning is not just hard to trust; it's impossible to improve. You can't tell if the model is reasoning correctly and landed on the wrong conclusion, or if it's reasoning incorrectly but reaching a plausible answer by accident.
Chain-of-thought prompting changes "High risk" into a 10-step analysis: "The contract contains a unilateral termination clause in section 4.2 (risk: high), an uncapped liability provision in section 7 (risk: high), and no dispute resolution mechanism (risk: medium)..." Now you can audit it, debug it, and explain it.
Accuracy and auditability are the same investment
Wei et al. (2022) showed that chain-of-thought prompting improves accuracy on multi-step reasoning tasks by 10–30 percentage points — not because it adds external information, but because it forces the model to process sub-problems serially rather than collapsing them into a single token prediction. Auditability and accuracy are the same mechanism: explicit intermediate steps.
Why chain-of-thought works
CoT prompting forces serial decomposition. When a model answers directly, it tries to compress the entire reasoning path into a single next-token prediction. When it's required to write out intermediate steps, each step becomes a conditioning context for the next — effectively giving the model a working memory in the context window.
The mechanism: language models predict the next token based on all preceding tokens. When intermediate reasoning steps exist in the context, they constrain and guide subsequent steps. A model predicting step 5 of an analysis has 4 prior steps anchoring it to the right conceptual track.
Zero-shot CoT: Adding "Let's think step by step." to a prompt triggers this behavior without examples. It works because the instruction shifts the model's generation mode.
Few-shot CoT: Including 2–3 worked examples in the format Question → Reasoning steps → Answer is more reliable for specific domains. The model matches the style and depth of reasoning from the examples.
For production agents, few-shot CoT is preferred because you can control the reasoning format: how many steps, what level of detail, whether to cite sources, and how to structure the conclusion.
The ReAct pattern
ReAct (Yao et al., 2022) is the standard pattern for tool-using agents that need auditable traces. Every step follows a fixed structure:
Thought: [What do I need to do next and why?]
Action: [Tool name and arguments]
Observation: [Tool result]
Thought: [What did I learn? What's next?]
Action: ...
The loop terminates when the Thought step concludes there's enough information to produce a Final Answer.
A complete ReAct trace for a 2-step research task:
Question: Who founded the company that makes PyTorch, and what year?
Thought: PyTorch is an ML framework. I need to know who makes it. I'll search for "PyTorch creator company."
Action: web_search(query="PyTorch creator company")
Observation: PyTorch was created at Facebook AI Research (FAIR), now Meta AI.
Thought: Now I know the company is Meta. I need the founding year of Meta (originally Facebook).
Action: web_search(query="Facebook Inc founding year")
Observation: Facebook was founded in 2004 by Mark Zuckerberg.
Thought: I have both pieces of information. PyTorch comes from Meta (Facebook), founded in 2004 by Mark Zuckerberg.
Final Answer: Meta (formerly Facebook) makes PyTorch. It was founded in 2004 by Mark Zuckerberg.
Each Thought step is inspectable. If the agent produces a wrong final answer, you can find exactly which Observation was incorrect or which Thought step made a wrong inference.
You're building a financial analysis agent. A junior engineer suggests: "Let's just ask the model for the final answer — chain-of-thought costs extra tokens." What's wrong with this reasoning?
For a financial analysis agent making decisions that affect real money, auditability is not optional — it's a compliance requirement in most regulated industries. CoT provides both: better accuracy on multi-step reasoning (Wei et al. 2022) AND a reasoning trace you can inspect, debug, and show to auditors. The token cost is typically 20–40% overhead — a small price for a system where wrong answers have real financial consequences.
Scratchpad and internal monologue
Sometimes you want the model to reason internally but show only a clean answer to the user. Three implementation patterns:
1. XML scratchpad tags (strip before display):
system = """
Before answering, reason through the problem in <thinking> tags.
The user will not see the <thinking> block — only your final answer after it.
"""
# After generation:
answer = re.sub(r'<thinking>.*?</thinking>', '', response, flags=re.DOTALL).strip()
2. Claude extended thinking parameter: Claude models support a thinking parameter that enables a built-in internal reasoning mode. The thinking tokens are billed but not shown in the API response by default.
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=16000,
thinking={"type": "enabled", "budget_tokens": 10000},
messages=[{"role": "user", "content": prompt}]
)
# response.content[0] is type="thinking" (internal)
# response.content[1] is type="text" (visible answer)
3. Two-call pattern (reasoning call → answer call):
# Call 1: get detailed reasoning
reasoning = call_model(f"Analyze this step by step: {problem}")
# Call 2: get clean answer using the reasoning as context
answer = call_model(f"Given this analysis:\n{reasoning}\n\nProvide a 1-sentence conclusion.")
When to use each:
| Pattern | Cost | Latency | Transparency | Best for |
|---|
| XML scratchpad | Low | Low | Full reasoning logged | Production agents, debugging |
| Extended thinking | Medium | Medium | Hidden from user | Consumer-facing features |
| Two-call | 2× | 2× | Fully inspectable | Complex analysis, compliance |
Your ReAct agent is researching a multi-hop question: "Who is the CEO of the company that makes the programming language used to write TensorFlow?" After the first tool call returns "TensorFlow is written in Python," the agent immediately outputs "Final Answer: The CEO of Python is Guido van Rossum." What went wrong in the agent's reasoning loop?
The agent collapsed the ReAct loop by jumping from Observation directly to Final Answer, skipping the Thought step where it should have reasoned: "Python is a programming language, not a company. I need to find out which company or organization maintains Python, then find that organization's CEO." This Thought step is exactly where multi-hop reasoning happens — without it, the agent can't decompose a multi-step question into its sub-problems. The fix is to make the ReAct format explicit in the system prompt and ensure the model always produces a Thought before any Action or Final Answer.
Build
Build it
Here's a tiny, self-contained ReAct-style loop over a mock knowledge base — no API calls, just the Thought/Action/Observation structure applied to a 2-hop lookup.
KNOWLEDGE = {
"pytorch_maker": "Meta AI (formerly Facebook AI Research)",
"meta_founded": "2004 by Mark Zuckerberg",
}
def web_search(query):
if "pytorch" in query.lower():
return KNOWLEDGE["pytorch_maker"]
if "meta" in query.lower() or "facebook" in query.lower():
return KNOWLEDGE["meta_founded"]
return "No result found"
def react_trace(question):
print(f"Question: {question}\n")
print("Thought: I need to find who makes PyTorch.")
print("Action: web_search('who makes pytorch')")
obs1 = web_search("who makes pytorch")
print(f"Observation: {obs1}\n")
print("Thought: Now I need the founding year of that company.")
print("Action: web_search('meta founded')")
obs2 = web_search("meta founded")
print(f"Observation: {obs2}\n")
print(f"Final Answer: PyTorch comes from {obs1}, founded {obs2}.")
react_trace("Who founded the company that makes PyTorch, and what year?")
Build lesson_01_03_cot_react.py yourself — this is the lab. A complete implementation contains:
ReActAgent class with the Thought/Action/Observation loop and structured logging- Scratchpad extractor that strips
<thinking> tags before user-visible output - Side-by-side accuracy comparison: direct answer vs. CoT on 10 multi-step reasoning questions
- Step budget enforcer that terminates runaway loops after
max_steps - Trace visualizer that prints formatted ReAct traces
"""
lesson_01_03_cot_react.py
ReAct agent with Thought/Action/Observation loop.
Includes scratchpad stripping, accuracy comparison, and step budget enforcement.
Usage:
export ANTHROPIC_API_KEY=sk-ant-...
python lesson_01_03_cot_react.py
Requirements:
pip install anthropic
"""
import os
import re
import json
from dataclasses import dataclass, field
import anthropic
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY", ""))
MOCK_TOOLS = {
"web_search": lambda query: f"[Search result for '{query}': relevant factual information found]",
"calculate": lambda expression: str(eval(expression, {"__builtins__": {}}, {})),
}
@dataclass
class ReActStep:
thought: str
action: str
action_input: dict
observation: str
@dataclass
class ReActTrace:
question: str
steps: list[ReActStep] = field(default_factory=list)
final_answer: str = ""
terminated_early: bool = False
class ReActAgent:
"""
Implements the ReAct (Reason + Act) pattern.
Each iteration: Thought → Action → Observation.
Terminates on "Final Answer:" or max_steps.
"""
SYSTEM = (
"You are a research agent. For each question, reason step by step using this format:\n\n"
"Thought: [your reasoning about what to do next]\n"
"Action: web_search\n"
"Action Input: {\"query\": \"...\"}\n"
"Observation: [tool result will be inserted here]\n\n"
"Repeat until you have enough information, then write:\n"
"Final Answer: [your answer]\n\n"
"Available tools: web_search(query), calculate(expression)"
)
def __init__(self, max_steps: int = 6):
self.max_steps = max_steps
def _call_tool(self, action: str, action_input: dict) -> str:
tool_fn = MOCK_TOOLS.get(action)
if not tool_fn:
return f"[Error: unknown tool '{action}']"
try:
return tool_fn(**action_input)
except Exception as e:
return f"[Tool error: {e}]"
def run(self, question: str) -> ReActTrace:
trace = ReActTrace(question=question)
context = f"Question: {question}\n\n"
for step_num in range(self.max_steps):
response = client.messages.create(
model="claude-haiku-4-5",
max_tokens=500,
system=self.SYSTEM,
messages=[{"role": "user", "content": context}],
stop_sequences=["Observation:"],
)
text = response.content[0].text.strip()
if "Final Answer:" in text:
trace.final_answer = text.split("Final Answer:")[-1].strip()
return trace
# Parse Thought and Action from the response
thought = ""
action = ""
action_input = {}
for line in text.split("\n"):
if line.startswith("Thought:"):
thought = line[len("Thought:"):].strip()
elif line.startswith("Action:"):
action = line[len("Action:"):].strip()
elif line.startswith("Action Input:"):
try:
action_input = json.loads(line[len("Action Input:"):].strip())
except json.JSONDecodeError:
action_input = {"query": line[len("Action Input:"):].strip()}
observation = self._call_tool(action, action_input)
trace.steps.append(ReActStep(thought, action, action_input, observation))
context += f"{text}\nObservation: {observation}\n\n"
trace.terminated_early = True
trace.final_answer = "[Max steps reached — partial reasoning above]"
return trace
def strip_thinking_tags(text: str) -> str:
"""Remove <thinking>...</thinking> blocks before displaying to user."""
return re.sub(r"<thinking>.*?</thinking>", "", text, flags=re.DOTALL).strip()
def direct_answer(question: str) -> str:
"""Answer without CoT for comparison."""
r = client.messages.create(
model="claude-haiku-4-5",
max_tokens=100,
messages=[{"role": "user", "content": question}],
)
return r.content[0].text.strip()
def cot_answer(question: str) -> str:
"""Answer with CoT scratchpad, then strip thinking before returning."""
r = client.messages.create(
model="claude-haiku-4-5",
max_tokens=400,
system="Think through the problem in <thinking> tags, then give your final answer.",
messages=[{"role": "user", "content": question}],
)
return strip_thinking_tags(r.content[0].text)
REASONING_QUESTIONS = [
("If a train leaves at 9am going 60mph and another leaves at 10am going 90mph "
"in the same direction, how many hours after 10am does the second train catch up?",
"2 hours"),
("A bat and ball cost $1.10 total. The bat costs $1.00 more than the ball. "
"How much does the ball cost?",
"$0.05"),
("There are 3 boxes: one has apples, one has oranges, one has both. "
"All labels are wrong. You pick from the 'both' box and get an apple. "
"Which box has both?",
"oranges"),
]
def print_trace(trace: ReActTrace) -> None:
print(f"\nQuestion: {trace.question}")
for i, step in enumerate(trace.steps, 1):
print(f"\n Step {i}:")
print(f" Thought: {step.thought[:80]}")
print(f" Action: {step.action}({json.dumps(step.action_input)[:60]})")
print(f" Observation: {step.observation[:80]}")
print(f"\n Final Answer: {trace.final_answer}")
if trace.terminated_early:
print(" [WARNING: terminated early — max steps reached]")
if __name__ == "__main__":
print("=" * 60)
print("DEMO 1: ReAct trace for a 2-step research question")
print("=" * 60)
agent = ReActAgent(max_steps=4)
trace = agent.run("Who founded the company that created PyTorch, and in what year?")
print_trace(trace)
print("\n\n" + "=" * 60)
print("DEMO 2: Direct answer vs. CoT on reasoning questions")
print("=" * 60)
for question, expected in REASONING_QUESTIONS:
direct = direct_answer(question)
cot = cot_answer(question)
print(f"\nQ: {question[:80]}...")
print(f" Expected: {expected}")
print(f" Direct answer: {direct[:60]}")
print(f" CoT answer: {cot[:60]}")