跳轉到

練習 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,不是進階深度教材。深度教材推薦: - datawhalechina/hello-agents ⭐ 中文圈最完整、章節式 + 16 種 production 能力。本練習對應 hello-agents 的 ReAct 章節(搭配 learn_version 分支 - ReAct 原論文(Yao et al. 2022 第 3 節) + pguso/ai-agents-from-scratch(本機 LLM 從零實作) - 完整 references 見 Stage 3 精選 Projects

為什麼從零寫

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

不花錢驗證程式邏輯(mock-based)

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-75 tool schema 給 LLM 看
TOOL_IMPL ~77-80 name → 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-4-6 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 行