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 全部显示成普通回答。
运行时必须保证结果引用的是 call-01;这个 ID 不是可忽略的 UI 装饰。跨 provider 转换需要正规适配,不能直接把某家原始 payload 当另一家的历史发送。
4.4 System message 是可重放状态¶
初始 system message 定义 baseline。后来的 system message 可以追加 content、替换命名 sections、添加或撤回工具。有效 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,比如应用通知。模型不能自动理解这些角色。
处理顺序:
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" 拒绝执行该消息里的所有工具调用,即使参数的最佳努力解析恰好得到可校验对象,仍可能丢了末尾信息。
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、模型配置。