这是ReAct Agent系列的完整记录。
我的PDF问答RAG项目是用LangChain搭的。跑通之后,面试官问“Agent底层怎么工作的”,我答不上来——因为LangChain把一切都封装好了。所以我决定不用任何框架,纯Python手写一遍ReAct循环,搞清楚Agent到底怎么“想”、怎么“决定调工具”。
ReAct循环是什么
ReAct = Reasoning + Acting。核心是一个循环:
用户输入
↓
拼 prompt → 调 LLM → 解析 JSON → 取 action
↓
action 是什么?
├─ finish → 返回答案
├─ search → 调工具 → 结果存 history → 回循环
└─ calculator → 调工具 → 结果存 history → 回循环
每一轮,模型返回一个JSON,里面包含四个字段:
thought:模型在想什么action:决定做什么(search/calculator/finish)action_input:做什么的参数
执行完工具后,把工具返回的结果(Observation)追加到 history,再进入下一轮。模型看到 history 里已经有了 Observation,就知道“上一轮已经做过了”,从而决定下一步。
踩过的坑
坑一:循环停不下来。
mock版循环一直重复调用搜索。原因:mock_llm 只根据输入里有没有“北京”来判断,第二轮history里还有“北京”,所以它一直调搜索。加了一个判断——如果history里已经有“Observation”,就finish。解决。
坑二:GLM-4返回的JSON带Markdown包裹。
真实API返回的是:
text
```json
{"thought": "...", "action": "search", ...}
```
json.loads 直接报错。加了三层容错:第一层直接解析,第二层清洗Markdown后解析,第三层正则提取 {...} 后解析。三层都失败就返回None,让模型重试。
坑三:finish时action_input为null。
GLM-4在第2轮觉得“已经拿到结果了”,把结论放在thought里,action_input填了null。导致最终回答打印出None。改了system_prompt,明确要求“finish时必须填最终答案,不要填null”。解决。
坑四:接和风天气API失败。
报错 403 Invalid Host——我的账号是共享API Host模式,域名不能用 api.qweather.com。
换成专属域名 pk3wt4re.re.qweather.com 后,报错 getaddrinfo failed——电脑DNS解析不了这个域名。
最后决定:回退到mock,保证Agent主流程可演示。工具层设计成可替换的,接真API只改工具函数内部。
最终效果
Agent现在有:
- 手写ReAct循环(不依赖LangChain)
- 两个工具:搜索(mock)、计算器
- 三层JSON容错
- 多轮工具调用(search → calculator → finish)
- Streamlit界面,可视化每一步的Thought/Action/Observation
- 公网链接:my-react-agent-nnqugx3fujxhet6ep4rv5t.streamlit.app/
测试用例: “北京今天多少度?如果超过25度,帮我算一下华氏度”
- 第1轮:search → 北京今天晴,30度
- 第2轮:calculator(30*1.8+32) → 86.0
- 第3轮:finish → 北京今天温度30度,超过25度,华氏度86.0度
总结
手写ReAct Agent最大的收获,不是“会写代码”,而是理解了Tool Calling的本质:Agent的智能不在于模型多强,而在于“让模型自己决定下一步做什么”这个循环。
LangChain帮你封装了这个循环,但面试官问你“底层怎么工作的”,你得能画出来。