Skip to content

第 8 章 · AgentMessage 与消息转换

本章目标:理解 AgentMessage 灵活类型与 LLM 原生消息的差异,掌握 convertToLlm 桥接与自定义消息类型的声明合并。

8.1 两种消息体系

Agent 内部流转的消息 ≠ 发给 LLM 的消息:

text
AgentMessage(应用层,灵活)
   │  可含 user / assistant / toolResult
   │  还可含自定义类型:notification / progress / card …

transformContext()      (第 9 章)

convertToLlm()         ← 本主角:过滤+转换

Message[](LLM 层,严格)
   仅认 user / assistant / toolResult 三种角色

为什么这样设计?UI 提示、审批卡片、系统通知这类信息人要看、机器不用看——让它们留在 Agent 历史,又在每次调 LLM 前被过滤掉。

8.2 convertToLlm:桥接函数

convertToLlm 在每次 LLM 调用前执行。最简单的实现是只保留三种标准角色:

typescript
const agent = new Agent({
  streamFn: models.streamSimple.bind(models),
  // 过滤掉非 LLM 消息;无自定义类型时也推荐显式声明以防泄漏
  convertToLlm: (messages) =>
    messages.filter((m) =>
      ["user", "assistant", "toolResult"].includes(m.role)
    ),
  initialState: { systemPrompt: "...", model },
});

8.3 自定义消息类型:声明合并

通过 TypeScript 的 module augmentation 给 AgentMessage 联合类型添加新成员:

typescript
// 扩展 pi-agent-core 的 CustomAgentMessages 接口
declare module "@earendil-works/pi-agent-core" {
  interface CustomAgentMessages {
    notification: {
      role: "notification";
      text: string;
      timestamp: number;
    };
  }
}

// 之后即可合法构造这种消息并放入历史
const msg: AgentMessage = {
  role: "notification",
  text: "用户已上传附件 report.pdf",
  timestamp: Date.now(),
};
agent.state.messages.push(msg);

然后在 convertToLlm 里决定它的命运——过滤或降级:

typescript
convertToLlm: (messages) =>
  messages.flatMap((m) => {
    if (m.role === "notification") return [];          // 选择 A:对模型隐藏
    // return [{ role: "user", content: `[系统通知] ${m.text}`,
    //            timestamp: m.timestamp }];           // 选择 B:转成 user 消息
    return [m];
  }),

8.4 实战模式:审批卡片

一个典型的生产场景:工具调用前需要人工审批。

typescript
declare module "@earendil-works/pi-agent-core" {
  interface CustomAgentMessages {
    approval_request: {
      role: "approval_request";
      action: string;
      status: "pending" | "approved" | "rejected";
      timestamp: number;
    };
  }
}

const agent = new Agent({
  streamFn: models.streamSimple.bind(models),
  initialState: { systemPrompt: "...", model },
  // 审批记录对模型不可见,但完整保留在会话历史供审计
  convertToLlm: (msgs) => msgs.filter((m) => m.role !== "approval_request"),
});

8.5 与低层 API 的关系

使用低层 agentLoop() 时没有默认转换,必须显式提供:

typescript
import { agentLoop } from "@earendil-works/pi-agent-core";

const config = {
  model,
  // 低层必须自己给 convertToLlm
  convertToLlm: (msgs) => msgs.filter((m) =>
    ["user", "assistant", "toolResult"].includes(m.role)),
};

for await (const event of agentLoop([userMsg], context, config)) {
  console.log(event.type);
}

8.6 本章小结

  • AgentMessage 面向应用可扩展,LLM 只认 user/assistant/toolResult;
  • convertToLlm 是两者之间的桥:在每次 LLM 调用前过滤与转换;
  • declare module + CustomAgentMessages 声明合并添加自定义消息类型;
  • 典型模式:通知/审批类消息留档不进模型上下文;
  • 低层 agentLoop() 不带默认转换,需自行提供。

🧪 随堂测验

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

1. LLM 能理解哪些角色的消息?

2. convertToLlm 的执行时机是?

3. 如何给 AgentMessage 添加自定义的 notification 类型?

4. 使用低层 agentLoop() 时若不提供 convertToLlm 会怎样?

🛠️ 动手实践

  1. 实现「进度日志」自定义消息类型 progress,在 UI 显示但对模型完全透明。
  2. 把 notification 消息改为「转成 user 文本」策略,观察模型行为差异并记录适用场景。
  3. 写一个单元测试:历史里混入 3 种自定义消息后,convertToLlm 输出只含标准三角色。

下一章第 9 章:在 convertToLlm 之前还有一层 transformContext——长对话的救命稻草。