04 · 退出条件与失败恢复
Agent Loop 不能无限跑下去。这一节实现三种退出条件、失败后的自动重试策略,以及结构化的执行统计。
循环不能永远跑
第 1 章用了一个硬编码的 MAX_TOOL_ROUNDS = 5 来防止无限循环。问题很明显:5 次对简单任务可能太多,对复杂任务又不够。
这一节做三件事:
- 定义更合理的退出条件
- 让循环知道"什么时候该停"
- 把执行过程的信息记录下来
三种退出条件
Agent Loop 在以下三种情况下停止:
1. 模型给出最终回答
这是最正常的退出方式。模型经过若干轮 Reason-Act-Observe 后,认为已经可以回答用户的问题了,于是不再请求工具调用,直接给出文本回复。
if (!response.toolCalls || response.toolCalls.length === 0) {
return { answer: response.content, todos: [...todoManager.getAll()], stats };
}模型的 toolCalls 为空 = "我不想再调用工具了" = "我准备好回答了"。
2. 所有计划步骤完成
如果模型创建了计划,而且每个步骤都标记为 completed,说明任务已经做完。这时如果模型没有给出最终回答(可能还想继续调工具),可以提前退出,避免多余调用。
if (todoManager.allCompleted()) {
if (!response.toolCalls || response.toolCalls.length === 0) {
return { answer: response.content, todos: [...todoManager.getAll()], stats };
}
}注意这里的逻辑:即使所有步骤都完成了,也不强制退出。如果模型还想调工具(比如想再确认一下某个细节),让它继续。只有在模型同时给出文本回答时才退出。这避免了过早截断模型的自我验证行为。
3. 达到迭代上限
不管任务多复杂,都不能无限制地跑下去。每多一次迭代就多一次模型调用,意味着更多的 token 消耗和等待时间。
const DEFAULT_MAX_ITERATIONS = 15;
for (let i = 0; i < maxIterations; i++) {
// ...
}
stats.hitLimit = true;
return {
answer: response.content || "未能完成任务:达到最大迭代次数。",
todos: [...todoManager.getAll()],
stats,
};默认上限设为 15。这个值的取舍:
- 太小(如 5):复杂任务做不完
- 太大(如 50):失败的任务浪费大量资源
- 15 是一个平衡点,足够大多数多步骤任务使用
同时通过 options.maxIterations 参数允许覆盖,测试时可以用很小的值验证边界行为。
执行统计:让过程可观测
退出条件决定了"什么时候停",但用户还需要知道"停的时候发生了什么"。这就是 AgentStats 的作用:
interface AgentStats {
modelCalls: number; // 调了几次模型
toolCallCount: number; // 执行了几次工具
hitLimit: boolean; // 是不是因为到上限停的
}在循环中跟踪这些数据:
const stats = { modelCalls: 0, toolCallCount: 0, hitLimit: false };
// 每次调用模型后
stats.modelCalls++;
// 每次执行工具后
stats.toolCallCount++;最终通过 AgentResult 返回给调用方:
interface AgentResult {
answer: string; // 最终回复
todos: TodoItem[]; // 执行计划
stats: AgentStats; // 执行统计
}在 main.ts 中展示给用户:
[模型调用: 4次 | 工具调用: 3次]如果 stats.hitLimit 为 true,额外提示:
[模型调用: 16次 | 工具调用: 15次 | ⚠ 达到迭代上限]失败恢复:Repair Loop
退出条件解决的是"正常完成"和"超时停止"的情况。还有一种情况需要处理:工具执行失败。
比如模型搜索了一个不存在的文件名,search 工具返回"未找到"。或者读文件时路径拼错了,read_file 工具返回"文件不存在"。
在代码里,工具失败不需要特殊处理——错误信息作为工具结果原样返回给模型:
const result = await tool.execute(toolCall.arguments, state);
stats.toolCallCount++;
/** 不管成功还是失败,都把结果追加到对话历史 */
allMessages.push({
role: "tool_result",
toolCallId: toolCall.id,
result,
});模型在下一轮 Reason 时看到这个错误信息,可以自行决定下一步。它会怎么选?取决于具体的失败原因:
| 失败原因 | 模型可能的反应 |
|---|---|
| 搜索无结果 | 换一个关键词再搜 |
| 文件不存在 | 换一个可能的路径再试 |
| 搜索结果太多 | 缩小搜索范围 |
| 工具调用参数格式错误 | 修正参数后重新调用 |
随着后续章节加入更多工具(第三章的 glob、第四章的权限系统),模型能选择的修复策略也会更丰富。当前这一课的重点是理解 Repair Loop 的机制本身。
这就是 Repair Loop 的核心:不做预设的错误处理逻辑,而是把错误信息交给模型,让模型自己判断如何修复。
这种设计的优势是灵活性——你不需要提前枚举所有可能的失败原因和对应的修复策略。模型作为一个通用推理引擎,能处理预料之外的情况。
升级后的完整循环
把所有内容组合起来,完整的 agent 循环:
export async function runAgent(
state: AgentState,
model: Model,
tools: Tool[],
options?: { maxIterations?: number },
): Promise<AgentResult> {
const maxIterations = options?.maxIterations ?? 15;
const todoManager = new TodoManager();
const allTools = [
...tools,
createTodosTool(todoManager),
updateTodoTool(todoManager),
];
const stats = { modelCalls: 0, toolCallCount: 0, hitLimit: false };
const systemMessage = { /* 升级后的系统提示词 */ };
const allMessages = [systemMessage, ...state.messages];
let response = await model.chat(allMessages, allTools);
stats.modelCalls++;
for (let i = 0; i < maxIterations; i++) {
// 退出条件 1:模型给出最终回答
if (!response.toolCalls || response.toolCalls.length === 0) {
return { answer: response.content, todos: [...todoManager.getAll()], stats };
}
// 执行工具,收集结果(失败也是结果)
for (const toolCall of response.toolCalls) {
const tool = allTools.find((t) => t.name === toolCall.name);
const result = tool
? await tool.execute(toolCall.arguments, state)
: `错误:未知工具 ${toolCall.name}`;
stats.toolCallCount++;
allMessages.push({ role: "tool_result", toolCallId: toolCall.id, result });
}
// 再调用模型
response = await model.chat(allMessages, allTools);
stats.modelCalls++;
// 退出条件 2:所有计划步骤完成
if (todoManager.allCompleted() && !response.toolCalls?.length) {
return { answer: response.content, todos: [...todoManager.getAll()], stats };
}
}
// 退出条件 3:达到迭代上限
stats.hitLimit = true;
return {
answer: response.content || "未能完成任务:达到最大迭代次数。",
todos: [...todoManager.getAll()],
stats,
};
}这一节做了什么
- 定义了三种退出条件:正常完成、计划完成、达到上限
- 实现了
AgentStats和AgentResult来跟踪执行过程 - 说明了 Repair Loop 的原理:错误信息交给模型,让模型自行调整策略
- 组合出了完整的升级版 agent 循环
下一节用一个完整的多步骤任务实战,看看升级后的 agent 表现如何。
登录以继续阅读
解锁完整文档、代码示例及更多高级功能。