主题
Day 4 — Agent 运行时 (ReAct)
读完这章你能获得什么:理解 AI Agent 的核心推理循环——ReAct 模式,掌握 Agent 如何与 LLM 交互、如何调用工具、如何与 Session 协作。
一、前情提要
经过 Day 1-3,我们有了:
- Gateway:统一消息格式 + 路由
- Channel:多种接入方式
- Session:多用户多轮对话管理
但整个系统还缺一个最关键的东西——"大脑"。消息进来了,保存了,然后呢?谁来理解用户说了什么、决定怎么回答?
本章就要造这个"大脑":AgentRuntime(Agent 运行时)。
二、生活类比:新员工的第一天上班
想象公司新来了一个员工(Agent),他的工作流程是这样的:
老板(用户): "帮我算一下 15+27 再乘以 3 等于多少"
员工(Agent)的脑回路:
1. 【收到任务】把老板说的话记到工作日志里
2. 【思考】这是个数学题,我需要用计算器
3. 【汇报打算】"我打算用计算器算一下"
4. 【动手】拿出计算器,按 (15+27)*3 = 126
5. 【记录结果】把计算结果记到工作日志里
6. 【再想想】好的,算出来了,可以回答了
7. 【回答】"结果是 126"
8. 【记录回答】把自己的回答也记到日志里这就是 ReAct 模式——Reasoning(想)+ Acting(做),交替进行直到有最终答案。
关键:员工不是一口气回答的,而是可能来回好几轮——想一下,做一下,再想一下,再做一下……
三、核心概念详解
3.1 AgentConfig —— 员工的"岗位说明书"
每个员工上岗前都有一份岗位说明书,规定了基本行为:
| 配置项 | 类比 | 说明 |
|---|---|---|
system_prompt | 岗位职责描述 | "你是 miniOpenClaw,一个友好的 AI 助手" |
model | 用的什么"脑子" | gpt-4o-mini 等模型名 |
temperature | 多"有创意" | 0=严谨,2=天马行空 |
max_iterations | 最多来回几趟 | 防止员工陷入"查资料→查资料→查资料…"的死循环 |
timeout_seconds | 加班上限 | 超过这个时间就强制下班 |
3.2 LLMProvider —— "脑子"的接口
Agent 需要一个"脑子"来思考。我们把"脑子"抽象成一个接口 LLMProvider,这样将来可以换不同的 LLM(OpenAI、国产模型等):
python
class LLMProvider(ABC):
async def chat(messages, tools=None) -> LLMResponse
async def chat_stream(messages, tools=None) -> AsyncIterator[str]OpenAIProvider 是目前的实现,内部用 AsyncOpenAI 客户端调用 OpenAI API。
为什么抽象出接口? 类比:电视遥控器的按钮布局是一样的(音量、频道),但底层可以遥控海尔、小米、索尼——接口一样,实现可以换。
3.3 AgentRuntime —— ReAct 推理循环
这是整个 Agent 的心脏。它的工作流程用一张图概括:
逐步解读:
- 用户输入:用户说了一句话,先作为
user角色的消息写入 Session - 组装消息:把 system prompt + Session 里的所有历史消息打包成一个列表,发给 LLM
- LLM 思考:LLM 看完所有消息后,做出判断——
- 如果需要工具:返回
tool_calls("我需要用计算器") - 如果不需要:直接返回文本回复
- 如果需要工具:返回
- 执行工具:如果有 tool_calls,逐个执行工具,把结果作为
tool角色的消息加入 Session - 再次调用 LLM:把包含工具结果的消息列表再发给 LLM,让它继续思考
- 循环往复:直到 LLM 给出最终文本回复,或达到最大迭代次数
3.4 错误处理 —— 出错了怎么办
好员工不会因为一个小问题就崩溃。Agent 的错误处理策略:
| 出错场景 | 处理方式 | 类比 |
|---|---|---|
| LLM 调用失败 | 抛出 AgentError | 脑子罢工了,只能报告无法处理 |
| 工具不存在 | 返回错误文本给 LLM | 工具柜里没这个工具,告诉员工换个办法 |
| 工具执行异常 | 把错误信息当结果告诉 LLM | 计算器坏了,员工可以选择不用计算器 |
| 超过迭代次数 | 返回降级回复 | 员工反复跑腿太多次了,强制给个回答 |
| 超过时间限制 | 返回超时提示 | 加班太久了,先给个临时答复 |
核心思想:工具层面的错误不抛异常,而是把错误信息以文本形式告诉 LLM——让 LLM 自己决定是换个方法还是直接回答。这就像员工发现计算器坏了,会自己判断"用手算也行",而不是直接下班走人。
四、消息组装详解
这是 Agent 最核心的操作之一——把所有信息组装成 LLM 能理解的消息列表:
发给 LLM 的消息列表 = [
{"role": "system", "content": "你是 miniOpenClaw,一个友好的 AI 助手"},
{"role": "user", "content": "你好,今天天气怎么样?"}, ← 历史消息
{"role": "assistant", "content": "让我帮你查一下天气。"}, ← 历史消息
{"role": "user", "content": "帮我算一下 (15+27)*3"}, ← 本轮新消息
]如果 LLM 返回了 tool_calls,消息列表会变长:
[
...前面的消息...,
{"role": "assistant", "tool_calls": [...]}, ← LLM 说"我要用工具"
{"role": "tool", "content": "126", "name": "calculator"}, ← 工具执行结果
]然后整个列表再发给 LLM,它就能基于工具结果给出最终回答。
五、动手实验指南
5.1 运行示例
bash
python day4-agent/example/main.py- 如果设置了
OPENAI_API_KEY环境变量,会用真实的 OpenAI 模型 - 如果没有,会使用内置的 MockLLMProvider(不发网络请求,直接返回模拟回复)
5.2 改一改,看看会怎样
实验 1:修改 system_prompt,把 Agent 的人设改成"海盗说话风格",观察回复的变化。
实验 2:把 max_iterations 设成 1,然后让 Agent 处理一个需要工具的问题——观察它如何因为迭代次数不够而返回降级回复。
实验 3:把 timeout_seconds 设成 0.001(几乎是瞬间超时),观察超时处理逻辑。
5.3 运行测试
bash
pytest day4-agent/agent/ -v六、常见问题 FAQ
Q1:ReAct 和普通的"问答"有什么区别?
A:普通问答是一步到位——用户问,AI 答。ReAct 是"想一步做一步"——AI 可能先查资料、做计算,来回好几轮后才给出最终答案。就像你问一个学生"北京到上海多远",普通问答是靠记忆猜,ReAct 是先打开地图查,查完再回答。
Q2:为什么要把工具结果"告诉" LLM,而不是直接拼接到回复里?
A:因为工具结果可能需要 LLM 来"翻译"成人话。比如工具返回 {"temp_celsius": 25, "humidity": 60},直接给用户看太生硬了。告诉 LLM 这个结果,LLM 会说"现在气温 25 度,湿度 60%,适合出门"。
Q3:如果 LLM 一直要调用工具、永远不给最终回复怎么办?
A:max_iterations 就是为了防这种情况。超过次数后强制返回一条降级回复。在真实场景中,这通常意味着 system_prompt 或工具描述需要优化。
下一章
Agent 已经能思考了,但它目前还没有"工具"可用——tool_registry=None,所以每次 LLM 都只能纯文本回答。
下一章 Day 5: 工具系统 将给 Agent 装上"双手"。