第一章:Agent 运行时概述 -- 从"单次调用"到"自动循环"

对应源文件:packages/agent/src/

回顾上一阶段

在第一阶段,我们学了 packages/ai。它解决的问题是:如何向各种 LLM 发请求、收响应

ai 包只管"一来一回"。你调用 stream(model, context) 得到一个 AssistantMessage,如果 AI 想调用工具,ai 包不会帮你执行工具,也不会自动把工具结果喂回给 AI。你得自己写这个循环。

Agent 包解决的问题

agent 包在 ai 之上构建了完整的自动循环

用户说一句话 → AI 回复(可能包含工具调用)→ 自动执行工具 → 把结果喂回 AI → AI 继续回复 → 直到 AI 说"我说完了" → 结束

这个循环叫做 tool-call loop(工具调用循环),是所有 AI Agent 的核心机制。

没有 agent 包时:

你的代码手动循环:
  1. stream(model, context)
  2. 如果 stopReason === "toolUse"
     → 手动找到工具
     → 手动验证参数
     → 手动执行工具
     → 手动把结果放回 messages
     → 回到 1
  3. 如果 stopReason === "stop"
     → 结束

有了 agent 包后:

  agent.prompt("帮我读取 main.ts")
  → 自动完成上面所有步骤
  → 通过事件通知你每一步发生了什么

这个包有多小

agent 包只有 5 个文件

packages/agent/src/
├── types.ts          # 类型定义(366 行)
├── agent-loop.ts     # 核心循环逻辑(684 行)
├── agent.ts          # Agent 类封装(544 行)
├── proxy.ts          # 浏览器代理流式传输(368 行)
└── index.ts          # 导出(3 行)

总共不到 2000 行代码。但它的设计非常精巧,值得仔细理解。

两层架构

agent 包有两层 API:

层次代码位置说明
底层agent-loop.ts纯函数,无状态。接收上下文和配置,运行循环,通过事件回调通知调用者
高层agent.ts (Agent 类)有状态的封装。管理消息历史、事件订阅、abort 控制、消息队列

底层像一个"引擎",高层像一辆"汽车"。你可以直接用引擎(底层 API),但大多数时候用汽车(Agent 类)更方便。

核心设计理念

1. 消息双层转换

agent 包引入了 AgentMessage 类型,它是 LLM Message超集

Message(LLM 能理解的消息)
  ├── UserMessage        // 用户消息
  ├── AssistantMessage   // AI 回复
  └── ToolResultMessage  // 工具结果

AgentMessage(应用层消息 = Message + 自定义类型)
  ├── UserMessage
  ├── AssistantMessage
  ├── ToolResultMessage
  └── 你的自定义消息类型    ← 新增!
      ├── 通知消息
      ├── UI 状态消息
      ├── 上下文压缩摘要
      └── ...

为什么需要这个?因为你的应用可能有 LLM 不认识的消息类型。比如 pi 有"bash 执行记录"、"分支摘要"等自定义消息。这些消息在 UI 中显示,但发给 LLM 前需要转换成它能理解的格式。

2. 不拥有 stream 函数

agent 包不直接 import 某个特定的 LLM 调用函数。你通过配置传入一个 streamFn,告诉它"用这个函数调用 LLM"。默认使用 ai 包的 streamSimple,但你可以替换为代理函数、mock 函数等。

3. 事件驱动

循环过程中的每一步都会发出事件。你的 UI 代码通过监听这些事件来更新界面,而不是轮询状态。

下一步

下一章我们会拆解核心类型系统 -- AgentMessageAgentToolAgentEvent 等。