Agent loop:模型调用之后,程序还要做什么
用一个资料整理助手拆解模型、工具与观察结果之间的循环,重点讨论谁负责执行、循环何时结束,以及为什么回复完成不等于任务验收通过。
延伸分析 · 从项目场景展开的原理与方案讨论。
- AI 学习
- Agent loop
- 工具调用
- TypeScript
用户说“查一下 agent 的执行方式,整理成一份笔记”,模型第一次回答可能是一段说明,也可能是一次搜索请求。如果它选择搜索,接下来由谁发请求?搜索结果怎样交还?它一直想搜下去怎么办?
Loop 就是“做一步、看结果、再决定下一步”的循环。 比如你找资料时,不会一开始就知道所有要打开的网页,而是读完一个页面,再决定是否继续查。Agent loop 把这个过程交给模型和程序配合完成。
本文用一个虚构的资料整理助手说明这个过程,例子用于学习,不对应某个已上线系统。
先把一次调用与一次任务分开
可以把最小循环写成:
准备本轮输入 → 调用模型 → 检查输出
├─ 请求工具 → 执行 → 记录结果 ─┐
└─ 最终回复 → 结束 │
↑─────────────────────────────────────┘
模型产生工具调用请求,应用负责真正调用工具,再把结果送入后续输入。LangChain 的 agents 文档展示了模型与工具节点之间的这种反复执行。这里的循环指应用控制流,不能据此判断模型内部如何推理。
普通问答可能一轮就结束;整理资料可能需要搜索、阅读,再形成回答。循环次数增加本身并不证明质量提高,每一轮都应该带来新的证据或缩小问题范围。
用一个可测试的小循环理解职责
先走一遍具体任务:“找一份解释工具重试的资料,写三句话概括。”
- 模型提出“搜索工具重试”,程序执行搜索。
- 搜索工具返回资料片段,程序把它交给模型。
- 模型根据片段生成三句话,程序识别为最终回复并结束。
如果第二步没找到有效资料,第三步也可能是换个关键词再搜。这时才进入下一轮。无论再搜多少次,真正发送搜索请求的都是程序。
下面是自定义的教学接口,不是任何 SDK 的请求格式。为突出控制流,只支持一个只读搜索工具,一轮最多一个工具请求;真实模型响应需要先由适配层校验并转换为这里的 Decision。传入的 model 函数预先绑定了用户任务;history 只记录执行过程,所以第一轮可以是空列表。
type Decision =
| { kind: 'answer'; text: string }
| { kind: 'search'; callId: string; query: string }
type Turn = { decision: Decision; observation?: string }
type Model = (history: readonly Turn[]) => Promise<Decision>
export async function runLoop(
model: Model,
search: (query: string) => Promise<string>,
maxCalls = 4,
) {
if (!Number.isInteger(maxCalls) || maxCalls < 1) {
throw new RangeError('maxCalls must be a positive integer')
}
const history: Turn[] = []
for (let calls = 0; calls < maxCalls; calls++) {
const decision = await model(history)
if (decision.kind === 'answer') {
return { status: 'answered' as const, text: decision.text, history }
}
if (!decision.query.trim() || !decision.callId.trim()) {
throw new Error('Search requires a query and call ID')
}
const observation = await search(decision.query)
history.push({ decision, observation })
}
return { status: 'limit_reached' as const, text: '', history }
}
model 和 search 是注入的依赖,因此可以先用固定返回值验证循环,而不用支付真实模型调用费用。例如让模型依次返回搜索请求、最终答案,应当只执行一次搜索;若模型始终要求搜索,最多调用模型四次,然后返回明确的限额状态。
这里记录的是工具请求和观察结果,适配到实际 API 时,要保留原请求与结果的关联标识。不能把不同调用的返回值混在一起,也不能把模型生成的“搜索成功”当成真正的工具返回。
限制轮数,只解决一种失控
maxCalls 限制的是模型调用次数,不是运行时长。一次搜索挂起,循环仍然会停在 await;一次请求产生大量内容,也可能迅速占满上下文。
这个例子故意不实现网络层、持久化与自动重试:工具异常向调用方抛出,不会悄悄吞掉。用于真实任务时还要分别处理请求超时、取消传播、结果长度、费用预算和错误记录。仅用 Promise.race 返回超时,不代表底层网络请求已取消。
如果已经达到上限,界面应该显示“未完成,达到执行上限”,并保留已获得的资料。示例返回的 history 让调用方拿到已有观察结果;要跨进程恢复,还需要调用方把它保存下来。把空结果包装成“完成”,会让控制流正确退出,却让用户误判任务状态。
最终回复与任务成功是两个判断
假设助手回复“笔记已保存”。可以依次追问:
| 观察到的事实 | 可以说明什么 |
|---|---|
| 模型生成了这句话 | 模型表达了完成意图 |
| 保存工具返回成功 | 工具报告操作成功 |
| 目标文件存在且内容正确 | 在检查范围内验证了产物 |
| 引用确实支持结论 | 进一步验证了研究质量 |
所以 answered 不是 verified_success。是否要求文件、引用或人工审核,应由任务的验收约定决定,详见如何评测 agent 的实际结果。
什么时候需要这样的循环
如果下一步固定为“读文档、抽取字段、输出表格”,普通顺序流程可能已经足够。如果下一步取决于刚读到的信息,模型参与选择工具与动作才更有价值。Anthropic 的 Building effective agents用预定义工作流与模型动态控制来区分这两类设计。
学习时可以先做三个实验:立即回答;搜索后回答;永远要求搜索。再增加一个搜索失败的情况,观察错误是否真正到达调用方。先搞清楚循环能怎样退出,再考虑增加工具和并行分支。
下一篇:graph 如何表达状态和流转。执行环境、预算与恢复则放在 harness 中讨论。