返回路线图
简体中文约 7 分钟阅读

练习 3:从零实现 ReAct(不用 framework)

examples/stage-3/03-react-from-scratch/README.zh-Hans.md

繁體中文 | 简体中文 | English

练习 3:从零实现 ReAct(不用 framework)

对应 Stage 3 — Tool Use & Agent 入门 练习 3。

🎓 学习模式:这份 starter.py完整解答、不是 TODO skeleton。建议用主动模式——mv starter.py starter_reference.py、看 signature 不看 body、自己重写一份 starter.py、跑 python test.py 验证;卡 20 分钟再回去对照 reference。完整方法论看 docs/HOW_TO_USE.md

📚 想要 chapter-length 深入版? 本 folder 的 starter 是 70-150 行 illustrative 版、聚焦 核心 pattern + 两条 SDK path,不是进阶深度教材。深度教材推荐:

#为什么从零写

ReAct(Reasoning + Acting)是现代 agent 的基础 pattern:

while not done:
    thought    = LLM 看完目前 context、讲出下一步要做什么
    action     = LLM 调用一个 tool
    observation = tool 执行结果、喂回去给 LLM

LangGraph / CrewAI 把这个 loop 藏起来了。你自己写过一次才知道:

  • 为什么 messages array 一直长
  • tool_use_id 跟 tool_result 怎么配对
  • stop_reason 为什么是 tool_useend_turn
  • max_iter 为什么是 safety net

70 行 Python 全交代清楚。

#怎么跑 — 两条路径

#Path A(默认、本机免费)

pip install -r requirements.txt
ollama pull qwen2.5:3b
ollama serve
python starter.py

预算:$0。本机 qwen2.5:3b 跑 ReAct loop 4-6 轮 ≈ 30-120 秒(CPU 慢、GPU 快)。

#Path B(Anthropic、想看 cloud 高质量)

pip install -r requirements.txt
export ANTHROPIC_API_KEY=sk-ant-...
python starter_anthropic.py

预算:每次 ≈ $0.001(claude-haiku-4-5)。比本机快 5-15 倍、答案质量更稳。

预期看到(Path A、本机):

❓ 问题:'台北人口' 除以 '纽约人口'、答案保留 4 位小数。
------------------------------------------------------------
[step 0] thought: 我先查台北人口...
           tool: lookup_fact({'query': '台北人口'}) → 2602000
[step 1] thought: 接着查纽约人口...
           tool: lookup_fact({'query': '纽约人口'}) → 8336000
[step 2] thought: 计算比例...
           tool: calculator({'expression': '2602000 / 8336000'}) → 0.3121...
[step 3] thought: 答案是 0.3122
------------------------------------------------------------
✅ 最终答案:台北人口除以纽约人口约 0.3122
   共 4 轮
✅ 练习 3 通过 — ReAct loop 自己连用了 lookup_fact 跟 calculator

#不花钱验证程序逻辑

python test.py            # 验 Path A (Ollama) starter.py 逻辑
python test_anthropic.py  # 验 Path B (Anthropic) starter_anthropic.py 逻辑

两条 test 都用 unittest.mock、不打真 API、$0/run。Path A 用 OpenAI-compat response shape、Path B 用 Anthropic content blocks。

test.pyunittest.mock.MagicMock 取代 Anthropic client、塞固定 response、验证你的 loop 逻辑。预期:

✅ test_calculator_basic
✅ test_calculator_rejects_eval_injection
✅ test_lookup_fact
✅ test_react_loop_single_tool_call
✅ test_react_loop_multi_step
✅ test_react_loop_respects_max_iter

🎉 全部通过 — 你的 ReAct loop 逻辑正确

#程序结构走查

在做什么
tool_calculator~30-40安全的计算器(whitelist 过滤、避免 eval 漏洞)
tool_lookup_fact~42-50假事实库(教学用、避免依赖外部 API)
TOOLS_SPEC~52-75tool schema 给 LLM 看
TOOL_IMPL~77-80name → callable 对应表(dispatch)
react_loop~85-130主循环、含 max_iter safety、messages 累积、tool result 接回去

#常见坑

  1. 忘记把 assistant response 加进 messages:下一轮 LLM 就看不到自己上一轮讲过什么、会 loop forever
  2. tool_result 没带 tool_use_id:LLM 无法配对哪个 result 对应哪个 call
  3. while True 没 max_iter:tool 结果写得不好、LLM 会无限调用;safety net 一定要设
  4. eval 没过滤:calculator 直接 eval(user_input) = RCE 漏洞;用 whitelist 或 ast.literal_eval

#想看更聪明的答案?

预设用 claude-haiku-4-5(最便宜)。改成 sonnet:

MODEL=claude-sonnet-5 python starter.py

或在 starter.pyMODEL = ... 那行。

#延伸

  • 加更多 tool:在 TOOLS_SPEC + TOOL_IMPL 补一个 entry 即可
  • 加 streaming:把 client.messages.create(...) 换成 with client.messages.stream(...) as s:、边跑边印
  • 加 prompt cache:在 system=tools=cache_control={"type":"ephemeral"} 重复 call 省 90% token
  • LangGraphPydantic AI 看 framework 怎么帮你藏掉这 70 行