Skip to content

Agent 系统:ReAct、工具与记忆

本文是 AI 应用系统学习系列的 L2 核心篇。前置:33. LLM API 开发详解。 学完可以配合面试题食用:05-agent-architecture-design29-multi-agent-collaboration

Agent 是什么,和 Chatbot 有什么不同

你让一个 Chatbot "帮我查一下明天的天气,顺便提醒我"——它只会回复一句"好的,我记住了",然后无事发生。因为 Chatbot 是单轮问答:输入 prompt → 输出文本,结束。

Agent 是自主决策循环:LLM 输出动作 → 工具执行返回结果 → 结果喂回 LLM 决定下一步 → 直到任务完成。Agent 的核心能力不是"说得对",而是"能做对"。一次对话可能走 3 步、5 步、甚至 20 步,每一步 LLM 都要决定"该调用什么工具、拿什么结果、是否该停了"。

mermaid
flowchart LR
    A[用户输入] --> B[LLM 思考]
    B --> C{需要工具吗?}
    C -->|是| D[调用工具<br>返回结果]
    D --> B
    C -->|否| E[输出最终回答]
    E --> F[结束]

这个循环就是 ReAct 模式的核心骨架。

ReAct 循环:思考→行动→观察

ReAct 这个名字来自"Reasoning + Acting",Google 和 Princeton 在 2022 年提出的范式。一句话:每一步都让 LLM 输出"这次我该做什么",执行后把结果放回去,再让它决定下一步

具体到代码实现,就是一个 while 循环:

  1. 把当前对话历史(包括之前工具调用的结果)构造为 messages 发送给 LLM
  2. LLM 要么返回一个 tool_call(决定调用工具),要么返回纯文本(决定结束)
  3. 如果是 tool_call → 执行工具 → 把结果追加到 messages → 回到步骤 1
  4. 如果是纯文本 → 输出给用户 → 循环结束

终止条件有三个硬性限制:

  • 最大步数:防止无限循环(建议 5-10 步上限)
  • 完成信号:LLM 输出表明"任务完成"(通常是自然语言回答)
  • 异常退出:工具连续报错 / 超时 / 输入超出上下文窗口

关键设计点:ReAct 的思考质量直接取决于你对 LLM 的 prompt 约束。如果给它"你是一个智能助手"这种泛泛的 system prompt,它可能会在不需要工具时也假装调用工具。需要明确告诉它:只有无法凭自身知识回答时调用工具,能直接回答的直接回答

工具设计规范:工具描述就是 prompt 的一部分

Function Calling 的机制在 LLM API 开发篇(33)已经讲过。Agent 场景下,工具设计有几个额外约束:

工具粒度:一个工具只做一件事。"查询天气"和"设置提醒"拆成两个工具,不要合并成"工具处理用户请求"。拆得太粗,LLM 会迷惑到底该不该调用;拆得太细(比如"获取城市经纬度"、"获取天气数据"、"解析天气格式"拆成三个),两步能完成的事变成五步,额外消耗 token 且增加失败概率。

工具描述怎么写:工具描述是 prompt 的一部分,LLM 靠它决定调用时机。好的描述给边界条件,不只给功能:

python
# 好的描述
"""
查询指定城市的天气。 
参数 city 必须是中文城市名,如"北京"、"上海"。
只支持中国城市,海外城市返回空结果。
只返回当日天气,不支持历史或预报。
"""

错误返回:工具执行失败时,不要抛出异常中断循环,而是返回结构化的错误信息给 LLM,让它自行决定是否重试或换方式。比如:

python
# 工具返回值
{"status": "error", "message": "城市 'tokyo' 不在支持列表中", "suggestion": "尝试中文城市名"}

LLM 看到这个错误信息,可能自己就修正了入参再试一次,比代码侧写死重试逻辑更灵活。

记忆体系:Agent 的"短期 + 长期"分工

Agent 面临的记忆困境:上下文窗口是有限的,但对话可能很长。

短期记忆 = 当前上下文窗口内的全部内容。LLM 的 KV Cache 在这里发挥了作用,但窗口塞满后无法"翻页"。短期记忆不需要额外设计,开箱即用。

长期记忆 = Agent 会话结束后需要持久化的信息。实现方式通常是:

  1. 每轮对话结束后,用 LLM 对关键信息做摘要
  2. 摘要存入向量数据库(如 Chroma 或 PGVector)
  3. 下次对话开始时,根据用户输入做语义检索,找回相关摘要注入上下文

工作记忆(scratchpad):ReAct 循环中用于记录中间推理过程的空间。在 prompt 中体现为"当前状态"字段。典型 scratchpad 设计:

Current state:
- 已查询天气:北京,晴,25°C
- 已设置提醒:明天上午 9 点
- 待办:是否需要查询交通信息未确认

遗忘与压缩:当上下文窗口接近上限时,可以:

  • 丢弃早期工具调用细节,只保留结果摘要
  • 丢弃 LLM 的中间思考步骤,只保留最终结论
  • 完全清空历史,只保留系统 prompt 和最新用户输入

每种策略都有代价:丢细节可能丢失上下文,丢思考步骤可能让 LLM 无法理解当前状态。建议在 Agent 步数超过 10 步时触发一次压缩,而不是等到窗口满了再被动截断。

动手实操:纯 Python 手写 ReAct 循环

下面是一个 60 行的 ReAct 实现,不依赖 LangChain 或任何 Agent 框架,只靠 OpenAI SDK 的 function calling:

python
import json
from openai import OpenAI

client = OpenAI(api_key="sk-xxx", base_url="https://api.example.com/v1")

# 定义两个工具
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "查询中国城市的当日天气",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "中文城市名,如北京"}
                },
                "required": ["city"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "set_reminder",
            "description": "设置提醒",
            "parameters": {
                "type": "object",
                "properties": {
                    "time": {"type": "string", "description": "HH:MM 格式"},
                    "text": {"type": "string", "description": "提醒内容"}
                },
                "required": ["time", "text"]
            }
        }
    }
]

# 工具执行器(模拟)
def execute_tool(name, args):
    if name == "get_weather":
        return json.dumps({"city": args["city"], "weather": "晴", "temperature": 25})
    elif name == "set_reminder":
        return json.dumps({"status": "ok", "reminder": f"{args['time']} {args['text']}"})
    return json.dumps({"error": "unknown tool"})

# ReAct 循环
def react_agent(user_input, max_steps=5):
    messages = [
        {"role": "system", "content": "仅当需要实时数据或外部操作时调用工具。能直接回答的就直接回答。"},
        {"role": "user", "content": user_input}
    ]
    step = 0
    while step < max_steps:
        step += 1
        response = client.chat.completions.create(
            model="gpt-4o",
            messages=messages,
            tools=tools,
            tool_choice="auto"
        )
        msg = response.choices[0].message
        if not msg.tool_calls:
            # LLM 决定直接回答,循环结束
            return msg.content
        # 把 LLM 的 tool_call 请求先加入历史
        messages.append(msg)
        # 再执行每个工具调用,结果紧跟其后
        for tc in msg.tool_calls:
            args = json.loads(tc.function.arguments)
            result = execute_tool(tc.function.name, args)
            messages.append({
                "role": "tool",
                "tool_call_id": tc.id,
                "content": result
            })
    return "超出最大步数限制,任务未完成。"

# 测试
print(react_agent("查一下北京天气,然后设一个明天早上 8 点的起床提醒"))

执行流程拆解:

  • 第 1 步:LLM 看到 "查天气" → 调用 get_weather → 返回结果
  • 第 2 步:LLM 看到天气结果 + "设提醒" → 调用 set_reminder → 返回结果
  • 第 3 步:LLM 看到两个任务都完成 → 输出总结文本 → 循环结束

注意 tool_choice="auto" 让 LLM 自行决定是否调用工具。如果设成 "required",LLM 每一步都会被强制调用至少一个工具,很多时候会浪费 token。

常见误区与小结

常见误区:

  1. 把工具塞进 system prompt 而不是用 function calling 声明:system prompt 里写"你可以用以下工具…"很不稳定,LLM 可能自己编参数格式。用官方的 function calling schema 声明,参数校验和格式由 SDK 保证。
  2. 工具描述太简略:描述写"查询天气",LLM 不知道参数该填什么格式、支持哪些城市。这是 Agent 不可靠的头号原因。
  3. 不设最大步数:没有步数上限的 Agent 可能陷入死循环(比如 LLM 一直认为自己还需要确认一遍结果)。实测中 5 步以内能解决 90% 的任务,超过 10 步说明分解有问题。
  4. 工具错误直接抛异常:Agent 循环中断,用户看到一段 traceback。应该返回结构化的错误信息,让 LLM 决定下一步。
  5. 遗忘压缩策略不做:上下文窗口填满后,LLM 性能急剧下降(lost in the middle 现象)。需要主动压缩,而不是等窗口满了才被动截断。

小结:Agent 的核心是用循环把 LLM 的"表达能力"转化为"执行能力"。ReAct 是最基础的循环模式,工具设计决定了 Agent 的上限,记忆体系决定了 Agent 能处理多复杂的任务。下一篇进入 L3 实战,讲生产级 Agent 架构:LangGraph、MCP 与多 Agent 协作。

参考

参考:ReAct 论文(Yao et al., 2022)、OpenAI Function Calling 文档、LangGraph 核心概念

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。
粤ICP备2026104257号-1