Agent 的核心组件与规划机制
Agent 不是「会调工具的聊天机器人」。它和 workflow 的本质区别在于:下一步做什么由模型在运行时决定,而不是由人提前画好流程图。
一、谁决定下一步
判断标准只有一条:步骤能不能提前枚举。
能枚举的——填表 → 校验 → 提交 → 发通知——就该用 workflow。它路径固定、可预测、可测试、成本可控;出了问题能定位到具体哪一步。
不能枚举的——用户的需求决定了下一步该查什么、调哪个工具、要不要再向用户确认一次——才用 Agent。它的路径由模型在运行时生成,灵活,但不可穷举,也就不可完整测试。
为了「看起来智能」把确定性流程包装成 Agent,是常见的一类过度设计:付出的是不可预测性与调试成本,收益基本为零。反过来也成立:真正的开放任务硬要用 workflow 枚举,会写成一个越补越大的 if-else 迷宫。
一句话判断:如果你能在需求评审时把流程图画完整,那就画出来,别交给模型猜。
二、模型、工具、记忆与控制循环
模型(大脑) —— 负责推理与决策:读上下文、判断当前进度、决定下一步是调工具还是直接回答。它的输入是上下文,输出是一个决策。
工具(手脚) —— 外部能力的集合。模型看不到工具的实现,只看得到它的描述。所以描述的质量直接决定调用质量:含糊的描述会导致不调用或乱调用,而越权的描述本身就是风险(详见《MCP 协议及其三种能力》)。
记忆(上下文) —— 分两层。短期记忆就是上下文窗口本身,放了什么模型才看得到什么;长期记忆要靠外部存储(向量库、数据库、文件)按需检索后回填进上下文。很多人以为「记忆」是某个组件提供的能力,实际上它是检索 + 拼接这两个朴素动作。
规划(控制器) —— 把目标拆成步骤,并在每一步之后重新评估。它不一定是独立模块,很多时候就是主循环本身:每轮把「到目前为止做了什么」交给模型,让它决定下一步。
这四个组件里,工程上最容易被低估的是规划——因为它常常「只是个 while 循环」,直到它开始原地打转才被发现。
三、先规划后执行与边走边看
Plan-and-Execute(先计划后执行) —— 先让模型产出一份完整计划,再按计划逐步执行,必要时在执行后重规划。
- 优点:步骤清晰、便于展示进度、长链路下方向不易跑偏;
- 代价:计划会过时。第三步的结果可能让第四步的计划失去意义,如果不做重规划,后面几步就是在执行一份已经作废的方案。
ReAct(思考—行动—观察交替) —— 每一步都是「先想一下 → 调工具 → 看结果」,循环直到得出答案。
- 优点:容错好,每一步都基于最新观察决策,不会出现「按过时计划执行」;
- 代价:步数与 token 消耗更高,且容易在原地绕圈。
选择上没有通用答案:链路长、步骤之间依赖强的任务更适合先出计划(配合中途重规划);短链路、需要频繁看结果调整的任务用 ReAct 更省事。实际系统里也常见混合用法——先出一个粗计划定方向,每一步内部用 ReAct 执行。
四、没有出口会怎样
Agent 的循环由模型决定,理论上它可以一直转下去。实际会遇到的几种「转不停」:
- 反复调用同一个工具,每次参数略有不同,结果始终不满意;
- 在两个动作之间来回切换,A 的结果让它想做 B,B 的结果又让它想做 A;
- 工具一直报错,于是不断重试,每轮都消耗一次调用。
所以必须设三重上限:
- 最大步数 —— 硬上限,达到就停止并返回已有结论;
- 总超时 —— 防止单步卡住导致整体挂起;
- token 预算 —— 控制成本,也间接限制了上下文膨胀。
再加一条经验规则:连续若干步工具返回无变化时判定为卡死,强制退出。它比单纯的步数上限更早触发,也更贴近真实的失败模式。
while (steps < maxSteps && !isTimeout() && tokensUsed < budget) {
const decision = await model.decide(history)
if (decision.type === 'tool') {
const result = await callTool(decision.name, decision.args)
history.push({ role: 'tool', name: decision.name, content: summarize(result) })
if (noProgress(history, N)) break // 连续 N 步无进展
} else {
return decision.answer
}
steps++
}
return fallbackAnswer(history) // 用已有信息给出结论,而不是空手而归
注意最后一行:超限之后应该带着已经拿到的信息给用户一个说法,而不是抛一个「执行失败」。用户能接受「我只查到了前两项,第三步的接口超时了」,不能接受转了半天什么都没有。
顺带一提:凡是可能被重试的动作都应当幂等,或者接受一个幂等键——超时重试是常态,重复执行的代价要有地方兜住。
指标来自 trace,字段设计时就该想清楚要统计什么:
trace.push({
step,
tool: name,
args, // 入参全存,排查时是关键线索
resultSummary, // 结果只存摘要,完整响应会让存储迅速失控
tokens,
ms,
ok: !error,
})
// 由此可以算:平均步数、工具失败率、单会话 token、超时率
五、上线后要看哪些指标
Agent 出问题时,最难的是「不知道它到底做了什么」。所以每一步都要留下可回放的痕迹:
- 工具名、入参、耗时、返回摘要 —— 四个字段缺一不可。返回要做摘要,完整响应体直接进日志会迅速失控;
- 总步数、重试次数、失败率 —— 这三个指标比「准确率」更早暴露问题。准确率要等用户反馈才看得出,而步数突然变多、重试率突然升高,往往是提示词或工具描述刚改坏了;
- 最终是否命中上限 —— 命中 stop 条件的比例如果持续偏高,说明要么是步数上限设低了,要么是任务本身不适合用 Agent。
一条轨迹记录大概是这个形态:
trace.push({
step: 3,
tool: 'searchOrder',
args: { orderId: '202601010001' },
durationMs: 412,
resultDigest: '命中 1 条,状态=已发货', // 摘要,不存完整响应
})
几个取舍要说明:入参可以全存(它是调试的关键线索),返回结果只存摘要(完整响应体进日志会迅速失控);耗时是定位「慢在哪一步」的唯一依据。
观测数据还有一个用处:它是回放与评测的基础。把一次真实会话的工具轨迹存下来,就能在改动提示词或工具描述之后重跑一遍,对比差异——这比凭感觉调提示词可靠得多。
// Plan-and-Execute:先出计划,逐步执行,必要时中途重规划
const plan = await model.plan(goal)
for (const step of plan) {
const result = await execute(step)
// 第三步的结果可能让后续步骤失去意义,这时重新规划而不是硬执行
if (result.invalidatesLaterSteps) plan = await model.replan(goal, plan, result)
}
缺了「信息够了就停」这个判据,循环就只是重复调用工具:
while (steps < maxSteps && !isTimeout()) {
const next = await llm.decide(context)
if (next.type === 'finish') break // 必须有显式出口
context.push(await runTool(next))
steps++
}
// 少了 finish 这一支,模型会一直「再查一次」,最后撞上步数上限直接失败
六、会调工具不等于 Agent
「会调工具的聊天机器人」和 Agent 的区别在于:下一步由模型在运行时决定,而不是人事先把流程画好。
能力越强也不代表越该全用 Agent——能用 workflow 枚举的场景用 Agent 是净亏损,付出不可预测性却换不到收益。
准确率也不是衡量 Agent 的最佳指标。步数、重试次数、失败率这三个更早暴露问题,也更可操作。
设了最大步数同样不等于安全:步数、超时、token 预算要同时设,还要想清楚「超限之后怎么给用户一个交代」。
上 Agent 之前先问一句「这些步骤能不能提前枚举」——能枚举的场景,用 workflow 更便宜也更可靠。
推理与行动交替这个思路的原始论文是 ReAct,看完再设计 Agent 循环会少走弯路。