01 · 从一次回答到循环执行
第 1 章的 agent 只会"问一次答一次"。这一节把它升级成持续推进的执行循环,理解为什么 Coding Agent 必须是一个循环而不是单次调用。
第 1 章留下的问题
第 1 章做出的 agent 能用了:给它一个问题,它搜索文件、读取内容、给出回答。但仔细看 agent.ts,它其实是一个固定上限的循环——最多跑 5 轮工具调用就强制停止。
这带来几个问题:
上限太死板。 5 轮够回答"入口文件在哪"这种简单问题,但如果任务需要搜索 3 个关键词、读 5 个文件、再交叉对比呢?5 轮根本不够用,但又不能设太大——每多一轮就多消耗一次模型调用,token 费用和等待时间都跟着涨。
没有统计。 调了几次模型?执行了几次工具?是因为完成了才停下,还是因为到上限被强制停下的?这些信息对用户不可见,也不利于调试。
没有任务管理。 面对"帮我找到这个 bug 并给出修复方案"这样的多步骤任务,模型不知道该分几步、每步做什么。它只是反复调用工具,直到碰巧完成或被迫停止。
这一章解决这些问题。这一节先理解"为什么必须是循环",然后升级循环本身。
为什么不能只调一次模型
假设用户说:"帮我找到 runAgent 函数的所有调用点,并说明每个调用点的用途。"
如果只调一次模型,模型能做的只是猜。它没看过你的代码,不知道 runAgent 在哪个文件里、被谁调用。它要么瞎编答案,要么承认自己不知道。
正确的做法是让模型多步执行:
- 调用
search搜索runAgent→ 发现它在agent.ts和main.ts中出现 - 调用
read_file读取main.ts中相关行 → 看到main.ts里 import 并调用了它 - 调用
read_file读取agent.ts中的函数定义 → 理解它的职责 - 综合这些信息给出回答
每一步都是"拿到新信息 → 决定下一步做什么"。这就是一个循环,不是一个单次调用。
循环的本质:Reason-Act-Observe
把上面的过程抽象一下,每一步都是三个阶段:
Reason(推理):我看到搜索结果里有 agent.ts 和 main.ts,需要读取它们的内容
Act(行动):调用 read_file 读取 main.ts
Observe(观察):文件内容是 ...然后拿着观察结果回到 Reason,决定下一步。这个 Reason → Act → Observe → Reason 的循环,就是 Agent Loop 的核心模式,学术界叫它 ReAct。
在我们的代码里,这三步分别对应:
| ReAct 阶段 | 代码对应 | 做了什么 |
|---|---|---|
| Reason | model.chat(allMessages, allTools) | 模型看对话历史,决定下一步 |
| Act | tool.execute(toolCall.arguments, state) | 执行模型选择的工具 |
| Observe | 把工具结果追加到 allMessages | 新信息成为下一轮 Reason 的输入 |
第 1 章的代码已经在做这件事了,只是没有给它一个明确的名字。这一章要做的升级是:让这个循环更智能、更可控、更可观测。
升级循环:从 MAX_TOOL_ROUNDS 到结构化结果
第 1 章的 runAgent 返回一个 string。这一章改成返回一个结构化的 AgentResult:
// src/types.ts
/** 执行统计 */
interface AgentStats {
modelCalls: number; // 调用了几次模型
toolCallCount: number; // 执行了几次工具
hitLimit: boolean; // 是否因为达到上限而停止
}
/** Agent 运行结果 */
interface AgentResult {
answer: string; // 最终回复
todos: TodoItem[]; // 执行计划(下一节展开)
stats: AgentStats; // 执行统计
}为什么是结构化结果而不是字符串?两个原因:
调用方需要更多信息。 main.ts 需要知道执行了多久、用了多少工具,好展示给用户。如果只返回一个字符串,这些信息就丢了。
方便测试。 测试可以断言 stats.modelCalls、stats.hitLimit 等具体字段,而不是只能检查输出文本。
升级后的 agent.ts 骨架
升级后的 runAgent 大致长这样(省略计划工具部分,下一节展开):
export async function runAgent(
state: AgentState,
model: Model,
tools: Tool[],
options?: { maxIterations?: number },
): Promise<AgentResult> {
const maxIterations = options?.maxIterations ?? 15;
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++) {
if (!response.toolCalls || response.toolCalls.length === 0) {
return { answer: response.content, todos: [], stats };
}
// 执行工具...
// 把结果追加到对话历史...
// 再次调用模型
response = await model.chat(allMessages, allTools);
stats.modelCalls++;
}
stats.hitLimit = true;
return { answer: "未能完成任务", todos: [], stats };
}几个关键改动:
默认上限从 5 提升到 15。 复杂任务需要更多步骤。15 次足够大多数场景,又不会让一次任务消耗过多资源。
可通过 options.maxIterations 覆盖。 测试时可以传一个很小的值来验证"达到上限"的行为,不需要真的跑 15 轮。
返回 AgentResult。 调用方拿到的不只是回答,还有执行计划和统计数据。
下一节
循环的骨架升级完了。但光有循环还不够——模型在循环里不知道自己应该分几步、每步做什么。下一节引入 ReAct 模式的完整实现,让模型能先规划、再执行、最后验证。
登录以继续阅读
解锁完整文档、代码示例及更多高级功能。