直觉:从"会说话"到"会做事"

一个纯粹的大语言模型(LLM)本质上是一个 f(prompt) -> text 的函数:给一段上下文,预测下一个 token。它没有状态、不能联网、算不准两个 20 位数相乘、也无法在你授权下改一行代码。Agent 做的事,就是在这个无状态函数外面套一个循环,让模型可以"观察环境 → 思考 → 行动 → 再观察",把单次推理变成多步决策。

所以 Agent 不是某个新模型,而是一种控制流模式。理解它的关键有三块:推理-行动循环(ReAct)、工具调用(tool calling)、以及记忆(memory)。

机制一:ReAct 循环

ReAct 的核心思想是把"推理(Reasoning)"和"行动(Acting)"交错进行。模型不是一口气吐出答案,而是产出一段思考,决定调用某个工具,拿到结果(Observation)后再继续思考,直到给出最终答案。

最小化的伪代码:

1
2
3
4
5
6
7
8
9
10
11
def react_loop(task, tools, llm, max_steps=10):
scratchpad = [] # 累积的 thought/action/observation
for step in range(max_steps):
prompt = build_prompt(task, tools, scratchpad)
out = llm(prompt) # 模型输出一步
if out.is_final():
return out.answer
tool = tools[out.action] # 选定工具
obs = tool(out.action_input) # 执行,拿到真实结果
scratchpad.append((out.thought, out.action, obs))
return "达到步数上限"

经典的 ReAct 文本格式长这样:

1
2
3
4
5
6
7
8
Thought: 我需要先查一下当前汇率
Action: search[USD to CNY rate]
Observation: 1 USD ≈ 7.2 CNY
Thought: 现在可以换算 100 美元
Action: calculator[100 * 7.2]
Observation: 720
Thought: 我已经有答案了
Final Answer: 约 720 元

这里有个容易踩的坑:scratchpad 是不断追加的,每一步都会把之前所有的 thought/action/observation 重新塞回 prompt。这意味着上下文长度随步数线性增长,token 成本是 O(steps2)O(\text{steps}^2) 量级——第 k 步要重读前 k-1 步的全部内容。长任务必须做上下文压缩(摘要旧步骤、丢弃冗长 observation),否则既烧钱又会撞上上下文窗口上限。

机制二:工具调用的真实数据流

早期的 ReAct 靠纯文本解析 Action: xxx[yyy],正则容易碎。现代做法是结构化 tool calling:你用 JSON Schema 声明每个工具的名字、描述和参数类型,模型被约束输出一个结构化的函数调用(通常通过约束解码 / grammar 保证 JSON 合法)。

1
2
3
4
5
6
7
8
9
tools = [{
"name": "get_weather",
"description": "查询某城市当前天气",
"input_schema": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
}]

完整数据流是这样的:

  1. 你把 tools 描述 + 用户问题发给模型;
  2. 模型返回一个特殊的"工具调用"消息:{name: "get_weather", input: {city: "Beijing"}}
  3. 执行权在你的代码手里——模型只是"申请"调用,真正去跑函数、访问数据库、调 API 的是宿主程序;
  4. 你把函数返回值作为一条 tool_result 消息追加回对话,再次请求模型;
  5. 模型基于结果生成自然语言回答,或继续申请下一个工具。

这一步的安全边界非常重要:模型的输出永远是"建议",不要让模型的文本直接拼进 shell 命令或 SQL。一个把 action_input 直接 eval() 的 Agent,等于把命令执行权交给了任何能操纵输入的人——这是 Agent 系统里最常见的注入攻击面。

工程上还有几个权衡:

  • 工具数量:工具描述全部占 prompt。几十个工具时,光 schema 就能吃掉几千 token,而且模型选错工具的概率上升。常见解法是先做一次"工具检索"(用 embedding 召回 top-k 相关工具)再交给模型。
  • 并行 vs 串行:若多个工具调用彼此无依赖(查天气 + 查汇率),可以让模型一次返回多个调用并行执行,省一轮往返。但有依赖关系时(先查到用户 ID 才能查订单)只能串行,模型需要分多轮逐步推进。
  • 结果体积:工具返回的内容会原样进上下文。一个返回几千行日志或整张网页的工具能瞬间撑爆窗口。生产里常对 observation 做截断或摘要,只回喂模型真正需要的字段。

一个常被忽视的细节:ReAct 与 function calling 的关系

很多人把 ReAct(一种"思考-行动交错"的提示范式)和 tool calling(一种结构化输出能力)混为一谈,其实它们是正交的。ReAct 描述的是控制流——要不要先想再做、想几步;function calling 描述的是接口形式——模型怎么表达"我要调这个工具"。你完全可以用结构化 function calling 来实现 ReAct 的每一步(让模型先在一个 thought 字段里推理,再给出工具调用),这也是目前最稳的工程组合:既有 ReAct 的多步推理,又有结构化调用的可靠解析。

机制三:记忆——短期、长期与状态

LLM 本身是无状态的,"记忆"全靠你在 prompt 里喂。通常分两层:

短期记忆就是当前对话/scratchpad,受上下文窗口限制。窗口满了要做滑动窗口或摘要。

长期记忆跨会话保存,主流实现是向量检索(RAG 式记忆):把历史事实、用户偏好、过往结论切块、embedding,存进向量库;新一轮对话时按相似度召回相关片段拼进 prompt。检索通常用余弦相似度:

sim(q,d)=qdqd\text{sim}(q, d) = \frac{\mathbf{q} \cdot \mathbf{d}}{\|\mathbf{q}\|\,\|\mathbf{d}\|}

其中 q\mathbf{q} 是当前 query 的向量,d\mathbf{d} 是某条记忆的向量。召回 top-k 后塞进上下文。

这里的常见误区:把向量检索当成精确数据库。向量召回是"语义近似",对"上次我说的预算上限是多少"这类需要精确数值的查询不可靠——近似匹配可能召回相关但不准确的片段。结构化事实(用户 ID、配置项、计数)应该走键值/关系存储,让模型通过工具去查,而不是塞进向量库。

边界:Agent 不是越自主越好

把上面三块拼起来,一个生产级 Agent 大致是:

1
2
3
4
5
6
环境/工具 ── tool_result ──▶ ┌─────────────┐
│ ReAct 循环 │◀── 短期记忆(scratchpad)
用户输入 ─────────────────▶ │ (LLM 决策) │◀── 长期记忆(向量召回)
└─────┬───────┘
│ 最终答案

几条来自实践的边界经验:

  • 步数上限是必须的。模型可能陷入"调用-失败-重试"的死循环,没有硬上限会无限烧 token。
  • 可观测性 > 自主性。多步 Agent 的失败往往发生在中间某一步选错工具或误读 observation。把每一步的 thought/action/observation 全量日志化,是调试的唯一抓手。
  • 错误要回喂模型而非直接崩溃。工具抛异常时,把错误信息作为 observation 返回,模型常常能自我纠正(换参数、换工具)。
  • 自主层级要匹配风险。读类操作可以放手让 Agent 自己跑;写类、删类、花钱类操作应加人工确认(human-in-the-loop)。
  • 多 Agent 不是银弹。把任务拆给多个专职 Agent(规划者、执行者、审查者)在复杂任务上确实有效,但每多一个 Agent 就多一份上下文、一份延迟、一处出错点。多数场景下,一个带好工具和好记忆的单 Agent 已经够用,先把单体做扎实再谈编排。

反思与自我纠错

在基础 ReAct 之上,常见的增强是加一层反思(reflection):让 Agent 在若干步后停下来,回看自己的轨迹,判断是否跑偏、要不要换策略。实现上可以是一个独立的"评审"提示,把当前 scratchpad 喂回模型问"这条路走得对吗?",再据此调整后续行动。它的收益在于能跳出局部死循环,但代价是额外的 token 和延迟,且反思本身也可能出错。是否值得,取决于任务的容错预算——一次性问答不必反思,长程多步任务(如自动修 bug)则收益明显。

小结

AI Agent 的本质是给无状态的 LLM 套上"循环 + 工具 + 记忆":ReAct 提供交错推理与行动的控制流,结构化 tool calling 把"想"变成可执行的"做"且把执行权牢牢留在宿主代码里,记忆分短期上下文与长期向量召回两层。难点不在让它动起来,而在控制成本(上下文随步数膨胀)、守住安全边界(永不直接执行模型文本)、以及保证可观测性。把这三件事做扎实,比追求"全自动"重要得多。