Skip to content

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 的心脏。它的工作流程用一张图概括:

逐步解读

  1. 用户输入:用户说了一句话,先作为 user 角色的消息写入 Session
  2. 组装消息:把 system prompt + Session 里的所有历史消息打包成一个列表,发给 LLM
  3. LLM 思考:LLM 看完所有消息后,做出判断——
    • 如果需要工具:返回 tool_calls("我需要用计算器")
    • 如果不需要:直接返回文本回复
  4. 执行工具:如果有 tool_calls,逐个执行工具,把结果作为 tool 角色的消息加入 Session
  5. 再次调用 LLM:把包含工具结果的消息列表再发给 LLM,让它继续思考
  6. 循环往复:直到 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 装上"双手"。