第 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;其余运行期选项(如 toolExecution、beforeToolCall)作为构造参数传入后也可随时修改。
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); // 04.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. 想让一个报错的回合从当前上下文重试(不新增用户消息),应该调用?
🛠️ 动手实践
- 创建一个「苏格拉底式导师」Agent:只提问不给答案,连续对话 5 轮并打印每轮后
state.messages.length。 - 对话中途调用
agent.state.model切换到另一个厂商的模型,验证上下文无缝延续。 - 在长回复过程中调用
agent.abort(),观察waitForIdle()后最后一条 assistant 消息的 stopReason。
状态机已就绪。下一章第 5 章将深入事件流——构建实时 UI 的关键。