跳转至

04 · 模型、消息与流式协议

4.1 Provider、API、Model 是三件事

Provider 是服务的身份和运行单元,包含认证、模型目录和操作实现;API 是请求/响应协议;Model 是具体模型元数据。同一个 provider 可能支持多种 API,同一模型名称在不同 provider 下也可能有不同 ID。

例如一个本地服务支持 OpenAI chat completions,可以注册为 ollama,API 为 openai-completions,模型 ID 是服务器实际安装的名称。不能因为它叫「OpenAI compatible」就把 API 写成 openai-responses。

Pi 1.0 推荐显式 Models 实例:

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");
if (!model) throw new Error("Model not found");

这段选目录中的模型,不代表你的账户已经有访问权限。真实请求仍可能被服务端拒绝。模型 ID、价格、窗口是可变化信息,产品上线时用实际 provider 元数据核对,不要把课程里的一个 ID 当作永久默认。

4.2 为什么根入口不自动加载所有服务

packages/ai/src/index.ts 的注释明确说明根入口不加载生成目录、provider 工厂、OAuth 实现和旧 compat 全局状态。这样普通 Core 消费者不必为所有服务付出启动和打包成本。

旧 API 如 getModel()、streamSimple() 仍可从 @earendil-works/pi-ai/compat 获取。SDK 为兼容旧消费者设置了默认 stream 函数;但你的新 Agent 应显式注入 streamFn,避免依赖某个模块恰好先导入的副作用。

4.3 四种模型消息

role 典型字段 作用
system content、sections、toolsAdded、toolsRemoved 当前系统指令与工具声明更新
user content、timestamp 用户任务,文字或图片
assistant content、provider、model、usage、stopReason 模型回答与调用请求
toolResult toolCallId、toolName、content、isError 与调用 ID 对应的实际结果

assistant content 可以混合 text、thinking、toolCall。不要把它强行当字符串,也不要把 thinking 全部显示成普通回答。

{
  type: "toolCall",
  id: "call-01",
  name: "read_note",
  arguments: { id: "pi-session" }
}

运行时必须保证结果引用的是 call-01;这个 ID 不是可忽略的 UI 装饰。跨 provider 转换需要正规适配,不能直接把某家原始 payload 当另一家的历史发送。

4.4 System message 是可重放状态

初始 system message 定义 baseline。后来的 system message 可以追加 content、替换命名 sections、添加或撤回工具。有效 prompt 要重放历史,不能只读取数组第一个元素。

flowchart LR S0[初始 sections + tools] --> S1[更新 skills section] S1 --> S2[toolsAdded: search] S2 --> S3[toolsRemoved: write] S3 --> E[当前有效 prompt 与声明]

getCurrentSystemMessage()、getCurrentSystemPrompt() 和 getCurrentTools() 负责重放。normalizeContext() 仅把兼容的外置 systemPrompt / tools 转成开头的 system message,再拼接原 messages,返回 branded TranscriptContext。它不验证 role、schema、调用与结果配对,也不生成服务商 payload。

进入具体 provider adapter 后,resolveTranscript() 对不支持会话中 system 更新的模型执行 collapseSystemMessages():把有效 system 状态折到开头,删除其余 system messages。支持更新的模型保留相应位置;具体工具增量如何编码仍取决于该 API adapter 和 compat。不能假设所有服务都原样接收 Pi 的 sections / toolsAdded。

工具实现函数不会写进 JSONL。历史里的工具声明说明模型看到什么,运行时 AgentTool.execute 才决定怎么执行。恢复时仍需要重新注册真实实现。

4.5 AgentMessage 与 Message

Core 允许通过 TypeScript declaration merging 添加自定义 role,比如应用通知。模型不能自动理解这些角色。

处理顺序:

flowchart LR A[AgentMessage 历史] --> P[prepareRequest] P --> T[transformContext] T --> C[convertToLlm] C --> N[normalizeContext 折入兼容外置字段] N --> D[provider adapter: transcript 与工具映射] D --> M[服务商 payload / request]

transformContext 做应用级裁剪、注入;convertToLlm 做角色转换;normalizeContext 把兼容外置字段折入 transcript;服务商协议适配在 provider adapter。这三个位置不能随意混成一个函数。

例如 UI 通知可以过滤掉;业务状态可以转成 user 或 system 内容,但必须明确信任级别。如果把检索到的网页写成 system 指令,就相当于把不可信数据提升为控制规则。

4.6 流不是最终结果

Provider 事件包括 start、text_delta、thinking_delta、toolcall_delta、done、error。参数增量还没结束时可能不是完整 JSON。

原则:只在 assistant message 完整结束之后执行工具。Pi 还针对 stopReason: "length" 拒绝执行该消息里的所有工具调用,即使参数的最佳努力解析恰好得到可校验对象,仍可能丢了末尾信息。

显示进展:text_delta → 逐段追加
记录事实:message_end → 完整消息
运行操作:完整 toolCall → 参数准备 → schema 校验 → hook → execute

usage 包含输入、输出、缓存读写与估算费用;价格由目录元数据提供。不要把 token 成本当作账单最终金额。

4.7 可兼容服务配置

在你的 agent directory 创建 models.json:

{
  "providers": {
    "local": {
      "baseUrl": "http://127.0.0.1:11434/v1",
      "api": "openai-completions",
      "apiKey": "ollama",
      "models": [{ "id": "YOUR_INSTALLED_MODEL_ID" }]
    }
  }
}

将占位 ID 换成你实际安装的模型。这不是下载模型命令;ollama 在此是本地服务通常忽略的 dummy key。远程认证服务可以使用 ${NAME} 环境插值,避免明文共享。

检查接口差异时先看请求和错误响应,确认是否支持工具、图片、推理参数。只有验证了差异才开兼容选项,不能随意把所有开关打开。

4.8 模型操作类型

Pi 1.0 不只有 chat:还包含 classifier 和 image。它们有不同输入输出,不能把图片模型塞给普通 Agent 循环当聊天模型。Code Mode 的 models.classify() 和 models.generateImages() 可以调用非聊天模型;普通 chat models 在脚本里可列出,但不能在那里作为另一轮聊天调用。

这对你自己的产品很有价值:路由、分类、图片生成可以是工具动作,但要分别统计耗时、费用和权限。

源码:消息类型、transcript、Models、模型配置。