Skip to content

第 4 章 · 第一个 Agent

本章目标:用 Agent 类创建有状态的智能体,理解 initialState 与状态机概念,跑通 prompt() 的完整回合。

4.1 Agent 是什么

第 2 章的 models.streamSimple无状态的——每次调用都要手动拼 context、手动处理工具循环。Agent 类把这些全部封装成一个有状态的运行时

text
Agent 内部自动完成:
┌────────────────────────────────────────┐
│ prompt("...")                          │
│  → 把用户消息追加到 state.messages       │
│  → 调 streamFn 请求 LLM                │
│  → 若模型请求调用工具 → 执行工具         │
│  → 把工具结果回填 → 再次请求 LLM         │
│  → 循环直到模型给出最终回答              │
│  → 全程发出事件供 UI 订阅               │
└────────────────────────────────────────┘

4.2 创建与配置

typescript
import { Agent } from "@earendil-works/pi-agent-core";
import { createModels } from "@earendil-works/pi-ai";
import { anthropicProvider } from "@earendil-works/pi-ai/providers/anthropic";

const models = createModels();
models.setProvider(anthropicProvider());
const model = models.getModel("anthropic", "claude-sonnet-4-6")!;

const agent = new Agent({
  // 初始状态:定义这个 Agent「是谁」
  initialState: {
    systemPrompt: "你是一个严谨的 TypeScript 导师。",
    model,                       // 必填:使用哪个模型
    // thinkingLevel: "medium",  // 可选:推理强度
    // tools: [],                // 可选:初始工具列表(第 6 章)
    // messages: [],             // 可选:预置历史消息
  },
  // 必填:流式函数。bind 绑定 this,因为内部会访问集合的路由逻辑
  streamFn: models.streamSimple.bind(models),
});

initialState 完整字段包括 systemPrompt / model / thinkingLevel / tools / messages;其余运行期选项(如 toolExecutionbeforeToolCall)作为构造参数传入后也可随时修改。

4.3 状态管理

所有状态集中在 agent.state 上,可读可写:

typescript
// 运行时随时修改身份与能力
agent.state.systemPrompt = "现在你是代码审查专家。";
agent.state.model = models.getModel("openai", "gpt-5-mini")!;
agent.state.thinkingLevel = "high";

// 只读的运行时状态
console.log(agent.state.isStreaming);        // 是否正在执行中
console.log(agent.state.streamingMessage);   // 流式过程中的部分消息

数组赋值的语义

state.tools / state.messages 整体赋值时会先复制顶层数组再存储;但对取出来的数组调用 push() 则直接修改当前状态。理解这一点能避免「为什么我 push 没生效/生效了却没预期」的困惑。

4.4 prompt():完整的一回合

typescript
// 最简单的文本提示
await agent.prompt("解释一下 Promise.all 和 Promise.allSettled 的区别");

// 带图片的多模态提示(模型需支持视觉)
await agent.prompt("这张图里有什么?", [
  { type: "image", data: base64Data, mimeType: "image/jpeg" },
]);

// 直接传 AgentMessage(可携带自定义字段)
await agent.prompt({
  role: "user",
  content: "继续上面的例子",
  timestamp: Date.now(),
});

一次 prompt() 内部可能发生多轮 LLM 调用(有工具时),但它返回即代表本回合完全结束——包括所有订阅者的事件处理都已落定。

4.5 多轮对话:状态自动累积

typescript
// 无需手动维护 messages —— Agent 自动累积上下文
await agent.prompt("我叫小明,最喜欢的数字是 7");
await agent.prompt("我最喜欢什么数字?");   // 模型能答出 7

// 查看累积的历史
console.log(agent.state.messages.length);  // 4 条:user+assistant 各两轮

// 重置一切,开始全新会话
agent.reset();
console.log(agent.state.messages.length);  // 0

4.6 控制方法速览

typescript
// 中途取消当前操作(stopReason 将为 aborted)
agent.abort();

// 等待当前回合彻底结束(包括事件处理)
await agent.waitForIdle();

// 出错后从现有上下文重试(最后一条必须是 user 或 toolResult)
await agent.continue();

4.7 本章小结

  • Agent = 有状态封装:自动管理消息历史、执行工具循环、发出事件;
  • initialState 定义身份(systemPrompt/model/thinkingLevel/tools/messages);
  • 状态通过 agent.state 随时读写;整体赋值会复制数组,push 则原地修改;
  • prompt() 返回即整回合落定;多轮对话上下文自动累积,reset() 清空重来。

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. 构造 Agent 时哪两个配置是必需的?

2. `agent.state.messages = newArr` 与 `agent.state.messages.push(msg)` 的区别是?

3. 一次 agent.prompt() 调用中如果模型请求了 3 个工具,会发生多少次 LLM 调用?

4. 想让一个报错的回合从当前上下文重试(不新增用户消息),应该调用?

🛠️ 动手实践

  1. 创建一个「苏格拉底式导师」Agent:只提问不给答案,连续对话 5 轮并打印每轮后 state.messages.length
  2. 对话中途调用 agent.state.model 切换到另一个厂商的模型,验证上下文无缝延续。
  3. 在长回复过程中调用 agent.abort(),观察 waitForIdle() 后最后一条 assistant 消息的 stopReason。

状态机已就绪。下一章第 5 章将深入事件流——构建实时 UI 的关键。