第 2 步:消息历史与循环
本节是核心。加两个东西:messages 数组和循环。加完就得到了 Agent Loop。
为什么需要消息数组
第 1 步用一个 toolResult 变量传结果。问题是它只装得下一次调用。
如果模型要先查时间、再查天气,然后综合两者回答,一个变量就不够了。 换成数组,把每一步都记下来:
ts
type Message =
| { role: "user"; content: string } // 用户的任务
| { role: "tool"; name: string; result: string }; // 工具的执行结果这就是真实 LLM 的工作方式
调用 OpenAI、Claude 这类 API 时,你传的就是一个 messages 数组。 模型每次都拿到完整历史,据此决定下一步。我们这里做的是同一件事,只是简化了字段。
模型:读历史 → 决定下一步
ts
function model(messages: Message[]): ModelOutput {
// 从历史里找出:已经调过哪些工具、拿到了什么结果
const used = new Map<string, string>();
for (const m of messages) {
if (m.role === "tool") used.set(m.name, m.result);
}
if (!used.has("getTime")) return { type: "tool", name: "getTime" };
if (!used.has("getWeather")) return { type: "tool", name: "getWeather" };
// 都查到了 → 用工具结果拼出答案
return {
type: "answer",
content: `现在是 ${used.get("getTime")},天气 ${used.get("getWeather")}`,
};
}注意最后那个 answer——它真的用到了工具返回的值。 这说明回路是通的:工具结果进了 messages,模型读到了,用它生成了答案。
Agent Loop
ts
function run(task: string): string {
const messages: Message[] = [{ role: "user", content: task }];
for (let step = 1; ; step++) {
// 1. 把完整历史交给模型,问它下一步干什么
const output = model(messages);
// 2. 模型说给答案了 → 跳出循环
if (output.type === "answer") return output.content;
// 3. 模型要调工具 → 执行它
const result = tools[output.name]?.() ?? "unknown tool";
// 4. 关键:结果写回 messages,下一轮模型就能看到
messages.push({ role: "tool", name: output.name, result });
}
}十几行,这就是 Agent 框架的核心。
循环图示
text
┌──────────────────────────────┐
│ model(messages) │ ◄────────┐
│ "你下一步要干什么?" │ │
└──────────────┬───────────────┘ │
│ │
┌──────────┴──────────┐ │
│ │ │
type: "tool" type: "answer" │
│ │ │
▼ ▼ │
执行工具 跳出循环 │
│ │
▼ │
messages.push(结果) ──────────────────────┘
模型下一轮就能看到运行
bash
npx tsx learn/2-loop.ts输出:
text
用户: 现在几点?天气怎么样?
[step 1] getTime → 2026/8/15 17:31:43
[step 2] getWeather → 晴天 28°C
[step 3] 给出答案
答案: 现在是 2026/8/15 17:31:43,天气 晴天 28°C模型调了两次工具,然后基于两个结果给出答案。步数不是写死的——是模型自己决定的。
对照你的直觉
大部分人对 Agent 的第一印象大致是这样,其实已经很接近了:
| 直觉 | 代码 |
|---|---|
| LLM 接收用户信息 | run(task),task 进 messages |
| 助手生成回复 | model(messages) 返回 ModelOutput |
| 中途可能要调工具 | output.type === "tool" |
| 这是个循环 | for 循环 |
| 需要条件跳出 | output.type === "answer" 时 return |
遗留问题
for (let step = 1; ; step++) 是无限循环。
如果模型有 bug,永远返回 type: "tool",程序就卡死了。真实 LLM 也会犯这种错—— 反复调同一个工具,或者陷入死循环。
→ 下一步:加一个安全阀。