Skip to content

第 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 也会犯这种错—— 反复调同一个工具,或者陷入死循环。

→ 下一步:加一个安全阀。

MIT 许可证开源