10 min read教程

PiAgent 04|模型适配器与流式响应

把不同 Provider 的消息、thinking、工具调用和增量事件翻译成 Agent 能消费的统一协议

PiAgent 04|模型适配器与流式响应#

把不同 Provider 的消息、thinking、工具调用和增量事件翻译成 Agent 能消费的统一协议

Agent Loop 表面上只需要一个 stream(),现实却远不止这一行。不同模型供应商在消息角色、工具格式、SSE 事件、thinking、缓存字段、上下文窗口和错误码上都有自己的方言。如果这些差异散落在 Loop、工具和 UI 代码中,任何换模型的需求都会变成全链路改造。

Pi 的模型层采用适配器思路:上层依赖统一的模型对象、上下文和事件流,Provider 差异集中在模型层消化。这样做的目标不是假装所有模型一样,而是把“共性控制流”和“供应商特性”分开。

1. 从三种对象理解模型边界#

Model:选择谁来回答#

模型对象至少需要 Provider、模型 ID、显示名称和能力信息。provider + id 通常构成稳定的模型身份;显示名称只用于 UI,不适合作为路由键。

Context:本次请求带什么#

上下文包含系统提示词、消息历史、工具定义以及可能的 thinking 或缓存选项。它是 Agent 内部状态到 Provider 请求的中间表示,不应直接等同于某一个供应商的 JSON。

Stream:回答如何回来#

流式结果不是字符串,而是一串有顺序的生命周期事件:消息开始、文本增量、thinking 增量、工具调用增量、usage、完成或错误。Agent 和 UI 依赖事件顺序,而不是只依赖最后那段文本。

2. 配置层:模型声明不等于模型可用#

Pi 的本地配置通常由 models.jsonauth.json 和内置 Provider 定义共同构成:

json
{
  "providers": {
    "deepseek": {
      "baseUrl": "https://api.deepseek.com/v1",
      "api": "openai-completions",
      "apiKey": "$DEEPSEEK_API_KEY",
      "models": [
        { "id": "deepseek-chat", "name": "DeepSeek Chat" }
      ]
    }
  }
}

几个字段的职责不同:

字段作用
baseUrlProvider 的接口根地址
api采用哪种协议适配器,例如 OpenAI 兼容协议
apiKey认证来源,生产环境应使用变量或认证存储
models该 Provider 暴露的模型 ID 和显示名称

ModelRuntime.create() 负责加载配置,getAvailable() 负责根据认证状态过滤模型。企业内网接入时,最先验证的不是 Agent 代码,而是三件事:网络是否可达、认证是否被读取、模型返回格式是否符合选定的 API 协议。

3. 适配器的两次翻译#

text
Pi Context / AgentMessage


Provider Request Adapter
        │ 角色、工具、thinking、缓存、参数

具体模型 API


Provider Stream Adapter
        │ 文本增量、tool call、usage、错误

Pi 的统一 AssistantMessageEvent

第一步是请求翻译,第二步是响应翻译。只做第一步而不统一事件,上层仍然要为每个 Provider 编写一套流式处理器;只做第二步而不处理请求差异,则无法保证工具和历史消息能被正确发送。

4. 为什么流式响应要有事件协议#

ts
for await (const event of stream(model, context)) {
  switch (event.type) {
    case "text_delta":
      process.stdout.write(event.delta);
      break;
    case "thinking_delta":
      // 交给可折叠的思考区域,而不是混进最终答案
      break;
    case "tool_call":
      // 交给 Agent Loop 的工具执行阶段
      break;
    case "done":
      // 记录 usage、stop reason 等收尾信息
      break;
  }
}

如果 stream() 只返回最终字符串,会产生三个问题:

  1. UI 只能等待完整响应,无法做增量渲染。
  2. Agent 不能在模型完成整段文字后及时发现 tool call。
  3. 观测层无法区分等待模型、执行工具和重试的时间。

事件协议把响应拆成时间线,既服务于实时体验,也服务于控制流和诊断。

5. message_update 与内部事件的区别#

AgentSession 层,常见的文本事件形态是:

ts
session.subscribe((event) => {
  if (event.type !== "message_update") return;

  const update = event.assistantMessageEvent;
  if (update.type === "text_delta") {
    renderText(update.delta);
  } else if (update.type === "thinking_delta") {
    renderThinking(update.delta);
  }
});

这里有两层 type:外层 message_update 表示 AgentSession 收到一条 assistant 消息更新;内层 text_deltathinking_delta 表示更新的具体类型。只判断外层而直接读取 .delta 是不安全的,因为不同内层事件的字段和用途不同。

6. thinking、工具和能力差异不能靠字符串猜#

不同模型对 thinking 的支持方式可能不同:有的模型提供独立思考片段,有的只返回最终文本,有的使用特定预算参数。适配层应该通过模型能力和配置表达差异,而不是让上层通过模型名称做大量 if/else

同样,工具调用也不是所有模型都以相同格式返回。统一后的 tool call 至少要保留名称、参数、调用 ID 和必要的原始信息,让 Loop 能验证、执行并配对结果。

模型层应保证:

  • 同一个工具调用在增量阶段和最终阶段可以被正确合并。
  • JSON 参数解析失败时产生结构化错误,而不是静默丢弃。
  • Provider 的错误被归类为可重试、不可重试或用户输入错误。
  • usage 和 stop reason 在流结束时仍然可供上层记录。

7. OpenAI 兼容接口的边界#

配置 api: "openai-completions" 只说明请求可以使用某种兼容协议,不代表后端完全等价于 OpenAI。仍然要验证:

  • 是否支持 tools 和正确的 tool call 结构。
  • 是否支持流式响应,并按协议发送结束标记。
  • thinking 字段是被接受、忽略还是报错。
  • 上下文窗口和最大输出限制是否一致。
  • 错误状态码和错误 JSON 是否足够让适配器分类。

企业内网模型能否接入 Pi,应该用一个最小诊断请求验证,而不是直接把完整 Coding Agent 接上去。先确认文本流,再确认工具调用,最后确认长上下文、取消和错误重试。

8. 流式调试的证据链#

出现“模型不输出”时,建议按顺序记录:

text
配置目录 → 选中的 provider/id → 请求是否发出
  → 首个 stream event → 最后一个 event
  → stop reason / usage → AgentSession 是否收到 message_update

这样可以区分四类问题:

  • getAvailable() 为空:配置或认证问题。
  • 请求已发出但没有事件:网络、协议或 Provider 响应问题。
  • Provider 有事件但 Session 没有更新:适配器转换问题。
  • Session 有更新但界面不动:订阅或事件投影问题。

常见误区

  • baseUrl 能访问误认为工具调用和流式协议都兼容。
  • 把 thinking 增量混入最终文本,导致用户看到内部过程。
  • 只记录最终消息,不记录 stop reason 和 usage,无法解释重试与成本。
  • 用模型显示名称作为判断条件,换模型后路由悄悄失效。
  • 在 Agent Loop 里处理 Provider 特殊字段,破坏分层。

小结

模型适配层的真正价值是让上层依赖协议,而不是依赖某个供应商的 JSON 细节。请求侧把统一 Context 翻译成 Provider 方言,响应侧把流式结果翻译成统一事件;Agent Loop 消费这些事件,Session 和 UI 再把它们投影给外部。模型能力不同仍然存在,但差异被关在正确的边界里。

源码定位

  • 模型与事件类型:packages/ai/src/types.ts
  • Provider 与模型目录:packages/ai/src/models.ts
  • 模型运行时:packages/coding-agent/src/core/model-runtime.ts
  • 配置解析:packages/coding-agent/src/core/config.ts
  • AgentSession 的流式事件转发:packages/coding-agent/src/agent-session.ts

相关文章