公司动态
让不同大模型共享一个 Agent:Pi 如何统一 Provider 与 Context Handoff
让不同大模型共享一个 AgentPi 如何统一 Provider 与 Context Handoff设想一个正在写代码的 Agent前半段由模型 A 分析日志并发起 Tool Call中途因为成本或能力切到模型 B。B 不仅要读懂文字还要知道哪个 Tool Result 对应哪个 Call、此前的 Thinking 能保留到什么程度以及哪些 Provider 私有状态已经无法迁移。只替换 Base URL 和 model最容易在这里把“请求成功”误当成“任务可以继续”。本文只沿一条主线展开多 Provider Agent 的核心不是接入更多 API而是把历史转换成目标模型仍能继续执行的可移植记录。Provider、Model、API Implementation、认证、流式事件和验收矩阵都服务于这条 Handoff 主线。事实基线固定在 Piv0.82.1b4f2936。本文依据该 Tag 的官方 README 与源码做静态分析没有登录各 Provider也没有完成真实跨 Provider Runtime 实测后文测试矩阵是接入验收设计不是本文已经取得的运行结果。一、多模型支持不是维护一张模型名称表一个看起来支持多模型的简单实现可能是constclientcreateClient(provider);returnclient.chat({model,messages});这只能解决最表面的请求路由。真实 Agent 还要面对Provider 使用不同认证方式一个 Provider 可能提供多个 Wire Protocol同一家厂商同时支持 Responses API 和 Completions APITool Call 字段不同Reasoning 内容结构不同图片和多模态支持不同Stop Reason 命名不同Token Usage 与缓存统计不同Context Window 和最大输出不同模型目录可能动态变化本地 OpenAI-compatible 服务存在兼容差异同一 Session 切换模型时历史消息不能全部原样发送。因此Pi AI 需要同时抽象三种对象Provider Model API Implementation这三个概念不能混在一起。二、先把三层职责拆开谁拥有模型谁描述能力谁处理协议Provider运行时归属Provider 是运行时拥有者。它负责Provider ID 与名称Base URL认证解析模型目录动态刷新Model 过滤Stream 路由与该 Provider 绑定的请求归属。例如openai anthropic google minimax moonshot local-vllmModel可选择的能力描述Model 是可选择的能力描述。它通常包含Model ID所属 Provider使用哪种 APIContext Window最大输出输入模态Reasoning 支持成本信息兼容选项自定义 Header 或 Base URL。API Implementation实际协议与序列化API Implementation 表示实际 Wire Protocol 和序列化逻辑例如Anthropic MessagesOpenAI ResponsesOpenAI CompletionsGoogle Generative AI。多个 Provider 可以复用同一种 API Implementation。例如某云厂商 OpenAI-compatible Provider → 复用 openai-completions API Implementation同一个 Provider 也可能混合多种 APIProvider A ├── Model X → openai-responses └── Model Y → openai-completions固定版本createProvider()支持为 Provider 指定单一 API 实现或提供以model.api为键的 API 实现映射。这说明 Pi 没有错误地把“Provider 名称”等同于“协议类型”。Provider ├── Model Catalog │ ├── Model A · api: openai-responses │ │ └── OpenAI Responses Implementation │ └── Model B · api: openai-completions │ └── OpenAI Completions Implementation ├── Auth └── Runtime RoutingModels CollectionAgent 看到的统一入口Pi AI 提供一个 Models Collection用来注册 Provider根据 Provider ID 查找 Provider根据 Model ID 查找 Model解析认证刷新动态模型目录调用 Stream 或 Complete计算和记录成本。从上层 Agent Runtime 的角度调用链可以简化为选择 Model → Models 查找所属 Provider → Provider 应用认证与 Header → 根据 model.api 选择 API Implementation → 发起流式请求 → 返回统一事件和 AssistantMessageAgent Runtime │ stream(model, context, options) ▼ Models Collection · locate owning provider ▼ Provider · resolve auth and request headers ▼ API Implementation · select by model.api and serialize ▼ LLM Endpoint │ provider stream events ▼ API Implementation · normalize events ▼ Models Collection → Agent Runtime · AssistantMessage stream这一层避免 Agent Loop 出现大量 Provider 条件分支。Agent Core 只关心Text DeltaThinking DeltaTool CallUsageStop ReasonErrorAbort。Provider Factory 为什么使用惰性加载Pi AI 的 Provider Factory 不要求应用启动时加载全部厂商实现。惰性加载可以减少初始 Bundle不必要的 Provider 依赖浏览器或特定运行时中的兼容问题启动成本未使用代码的导入副作用。这对支持大量 Provider 的库尤为重要。如果一个本地应用只使用 OpenAI-compatible vLLM它不应该因为 Provider Registry 中存在所有云厂商就初始化每一套认证与 API 代码。但惰性加载也提高了错误发现的延迟Provider 配置可能在首次调用时才失败动态导入路径问题不一定在启动时暴露测试需要覆盖每个 Provider 的加载分支。静态目录与动态目录怎样互补不是所有 Provider 都适合把模型列表写死在代码中。静态目录适合模型集合稳定需要手工维护准确能力元数据Provider 不提供可靠的模型发现 API。动态目录适合本地 vLLM、Ollama 或 LM Studio企业网关模型列表频繁变化用户拥有不同访问权限。固定版本的 Models 设计支持 Provider 刷新模型并允许模型目录持久化。这一能力需要谨慎处理。Provider 的/models端点通常只能告诉系统“存在这个 ID”不一定能准确提供Context WindowTool CallingReasoning图片输入价格最大输出兼容怪癖。因此动态发现和能力元数据不是同一个问题。合理实现通常需要动态发现模型 ID 静态或用户提供的能力覆盖 运行时探测或兼容配置三、再建立公共调用合同认证、Context 与流式事件Provider 可能使用API KeyOAuth Access TokenSubscription Login自定义 Header企业网关 Token本地无需认证。固定版本的 Models 实现会组合多层 HeaderProvider Auth Model Headers 调用时显式 Headers Models 调用层的 transformHeaders可以概括为headersmerge(providerAuthHeaders,modelHeaders,requestHeaders);headersoptions.transformHeaders?.(headers)??headers;这是说明性伪代码。为什么需要多层Provider Auth 是通用凭证Model Header 可以解决特定模型或网关要求Request Header 支持一次调用覆盖transformHeaders可以执行本次请求需要的最终调整但它属于 Models 调用层消费后不会继续下传给 Provider。风险是 Header 优先级可能造成凭证覆盖。自定义 Provider 与调用方文档必须明确哪一层优先哪些 Header 不应被用户覆盖Secret 是否会被记录Redirect 时是否保留认证Base URL 是否可信。统一 Context 的核心结构Pi AI 的统一 Context 可以序列化成普通 JSON主要包含systemPrompt messages 当前 Tool Definitions消息内容需要表达User TextAssistant TextThinkingTool CallTool ResultImageError/Aborted 部分内容。统一结构的价值是Session 可以持久化Agent Core 不依赖厂商 Wire FormatProvider Adapter 可以在请求边界转换同一 Context 可以交给另一个 Provider测试可以围绕统一语义构造输入。但统一 Context 不是无损格式。例如Provider 原生响应可能包含签名字段加密或不透明 Reasoning Token缓存控制服务端引用 IDProvider 特有状态Hosted Tool 元数据。如果这些内容无法迁移就必须保留为 Provider 扩展字段转换成普通文本或明确丢弃。流式事件如何被统一不同 Provider 的流式协议差异很大。统一后的 Runtime 需要获得稳定事件例如text_start text_delta text_end thinking_start thinking_delta thinking_end tool_call_start tool_call_delta tool_call_end usage finish / error / aborted这样 Agent Core 可以维护 Partial AssistantMessage而不用理解 SSE 或厂商事件名称。固定版本文档还明确指出不同内容块的事件并不保证连续Text、Thinking 与 Tool Call 的 Delta 可能交错出现消费者不能把“同类事件一定连在一起”当作协议保证。OpenAI Events ───────┐ Anthropic Events ────┤ Google Events ───────┼──→ Normalized Stream Compatible API Events┘ ├── Text ├── Reasoning ├── Tool Calls ├── Usage └── Stop Reason这层统一有两个主要风险。最小公分母如果只保留所有 Provider 都支持的字段会丢失强能力。抽象仍会泄漏如果统一对象塞入大量 Provider 特有字段上层仍然需要判断 Provider。Pi 的方向是提供通用能力同时允许 Provider-specific Options 作为逃生口。这比强行彻底统一更现实。Reasoning Level 只能统一相对意图不同模型使用不同术语Thinking BudgetReasoning EffortThinking Mode是否输出 Reasoning Content。Pi 提供统一的 Reasoning/Thinking Level方便 Agent 在常见档位间切换。但它不能保证Provider A 的 high Provider B 的 high这些档位只表达用户意图希望模型投入相对更多或更少的推理资源。真实 Token、延迟、可见 Reasoning 和质量变化仍由 Provider 决定。因此Pi 同时允许传入 Provider-specific Options。正确理解是统一 Level 用于常规路由 Provider Options 用于精细控制不能用统一枚举掩盖底层能力差异。Stop Reason 决定 Agent Loop 怎样收尾Agent Loop 需要知道模型为什么停止。固定版本统一的 Stop Reason 包括stop length toolUse error abortedpending若用于描述流式消息尚未完成只是生命周期状态不属于该固定版本的StopReason类型。这些语义直接影响 Runtime 行为。stop通常表示自然结束。toolUse表示响应包含需要执行的 Tool Call。length意味着输出被截断。Agent Core 不应执行可能不完整的 Tool Call。errorProvider 请求失败但统一 AssistantMessage 可以保留已产生的部分内容和 Usage。aborted用户或 Runtime 主动取消。错误被表达为最终消息而不是只从 Stream API 抛出这使 Agent 可以保存部分文本部分 Thinking已发生 Usage终止原因。这对成本、审计和恢复很重要。四、Handoff 的核心保留执行关系承认语义降级假设 Session 先使用 Anthropic 模型User → Assistant Thinking → Assistant Tool Call → Tool Result → Assistant Text用户随后切换到 OpenAI 模型。不能简单把 Anthropic 的原始 Messages JSON 原样提交给 OpenAI因为Role 与 Content Block 结构不同Thinking Block 可能有 Provider 签名Tool Call ID 规则不同Tool Result 表达方式不同某些字段只对原 Provider 有意义。Pi 固定版本文档给出的 Handoff 策略可概括为User Message 与 Tool Result 在统一 Context 中保持统一消息会继续交给目标 Provider 的实现做序列化这里的“保持”指 Pi 的统一消息语义而不是把源 Provider 的 Wire JSON 原封不动转发。Tool Call 与 Tool Result 的关系必须继续成立Tool Call 与对应结果需要继续存在否则新模型无法理解历史动作。同 Provider、同 API 的 AssistantMessage 尽量保留当消息仍由相同 Provider/API 处理时可以保留更多原生内容。跨 Provider 的 Thinking 转成普通文本Thinking Block 会转换为带thinking标记的文本表示。普通文本和 Tool Call 语义继续保留新 Provider 能够看到之前模型说了什么、调用了什么工具、得到什么结果。Source Provider AssistantMessage ▼ Same provider/API? ├── Yes → preserve native representation where supported └── No → convert to portable semantic form ├── Thinking → tagged text ├── Text → text ├── Tool Call → normalized tool call └── Tool Result → normalized result ▼ Target Provider SerializerThinking 为什么只能降级成文本某些 Provider 的 Thinking Block 不是普通文本。它可能包含签名不透明数据服务器验证信息只允许回传给原 Provider 的结构。把它原样发送给另一 Provider 既不兼容也可能违反协议假设。转换成thinking之前模型可迁移的推理内容/thinking可以保留语义线索但会发生降级目标模型将其视为普通历史文本不再拥有原生 Reasoning 权重或协议语义Provider 签名丢失Prompt Cache 行为可能变化目标模型可能过度依赖或忽视这段内容。因此Context Handoff 是“尽量保留可迁移语义”不是“无损迁移模型内部状态”。Tool Call Handoff 必须保持调用关系考虑历史Assistant: 调用 read(pathsrc/auth.ts), call_idabc Tool Result: call_idabc, 返回文件内容切换 Provider 后新模型仍需理解Assistant 发起过一次read参数是什么后面的 Tool Result 对应哪次调用。因此统一 Context 需要保留内部 Tool Call ID 关系再由目标 Provider Adapter 生成其接受的格式。风险包括目标 Provider 对 ID 字符限制不同同一消息中多个调用顺序不同某些 Provider 要求 Tool Result 紧跟调用被 Abort 的调用没有完整结果并行 Tool Call 的结果顺序不同。Context Handoff 不能只转换文本必须转换执行图。哪些状态不会迁移即使统一 Context 完整也有一些状态无法迁移。Provider 服务端缓存Prompt Cache 或 Conversation ID 可能只对原 Provider 有效。隐藏模型状态模型没有公开的内部激活状态不会被转移。原生 Reasoning 语义转换成文本后不再是目标 Provider 的原生 Reasoning。Hosted Tool 状态如果原 Provider 使用服务端内置工具新 Provider 未必能重放。特有安全与策略字段Provider 对消息来源或签名的判断不能通用迁移。相同输出行为目标模型即使看到同样语义也可能采用完全不同策略。因此Session 可迁移不等于模型行为连续。把开头案例放回完整执行链假设用户先用低成本模型探索再切换强模型修复阶段 A本地 vLLM - 搜索项目 - 建立文件地图 - 运行只读分析 阶段 B云端强模型 - 读取同一 Session 历史 - 检查分析结果 - 修改代码 - 运行测试 阶段 C低成本模型 - 总结 Diff - 生成文档Harness 需要在每次切换时选择新 Model由 Models Collection 找到 Provider对历史执行目标 Provider 序列化转换跨 Provider AssistantMessage重新计算 Context 是否超限处理目标模型不支持的模态或工具能力使用新 Provider 继续 Agent Loop。Session Context → Local vLLM · analysis turn Local vLLM → Session Context · text tool calls results Session Context → Pi AI Handoff · switch model/provider Pi AI Handoff · normalize and convert history Pi AI Handoff → Cloud Model · target-provider messages Cloud Model → Session Context · continue task真正困难的不是model newModel而是历史的可移植性与目标能力检查。五、统一层的边界兼容接口不等于 Agent 兼容Ollama、vLLM、LM Studio 和企业网关常提供 OpenAI-compatible API。“Compatible”通常表示主要请求与响应结构相似不保证每个细节一致。差异可能包括Tool ChoiceParallel Tool CallsReasoning 字段UsageStreaming ChunkJSON SchemaStop Reason图片输入/models返回Base URL 路径空 ContentAssistant Tool Call 格式。因此自定义 Provider 需要 Compat Flags 或配置而不是只写baseURLhttp://localhost:8000/v1一个概念性配置如下constlocalProvidercreateProvider({id:local-vllm,baseUrl:http://localhost:8000/v1,api:openAICompletionsImplementation,models:[{id:my-tool-model,contextWindow:32768,maxTokens:8192,supportsTools:true}]});这不是固定版本可直接复制的完整代码只用于说明所需信息。关键是不要把服务端宣称的模型能力当作已验证事实为 Tool Calling、Streaming 和 Usage 建立契约测试明确模型上下文和最大输出记录服务端版本避免把兼容问题误判成模型能力问题。成本统计为什么属于模型层Pi AI 会统一 Token 与成本统计。成本通常需要结合Input TokenOutput TokenCache ReadCache WriteReasoning TokenProvider 定价Model 定价版本。成本计算放在模型层更合理因为Agent Core 不应知道每家 Provider 定价字段Model Metadata 已经位于模型注册表同一个 Agent Loop 可以跨 Provider 汇总上层应用可以根据统一 Usage 做路由或预算控制。但定价是动态数据。仓库中的价格表可能滞后用户自定义 Provider 也可能没有价格信息。文章或产品不能把估算写成账单真值。正确表述应是基于当前 Model Metadata 的估算成本。实际费用以 Provider 账单为准。抽象会从哪些位置泄漏任何多 Provider 抽象都存在泄漏。模型能力差异有的模型不支持 Tool CallingPi AI 当前主要面向 Tool-calling Models。Reasoning 差异统一 Level 不能保证等价预算或质量。Tool Schema 差异复杂 JSON Schema 在不同模型上的支持程度不同。Message OrderingProvider 对 System、Tool Result 和空消息的约束不同。Context 计算不同 Tokenizer 对同一文本长度计算不同。Prompt Cache切换 Provider 后缓存通常失效。Multimodal目标模型可能不支持历史中的图片。Hosted ToolsProvider 原生搜索或代码执行能力无法普遍迁移。OAuth 与服务条款技术上能够登录不等于登录方式永久可用或适用于所有场景。订阅登录必须根据当前官方文档与条款核对。好的抽象不会隐藏这些差异而会提供统一主路径暴露能力元数据允许 Provider-specific Options在不兼容时明确失败记录转换与降级。六、用一张验收矩阵判断“任务能否继续”接入 MiniMax、Kimi、DeepSeek、本地 vLLM 或企业网关时至少测试测试需要确认普通文本流式文本是否完整Unicode中文、Emoji、特殊字符是否正常Tool Call参数是否完整、可验证多个 Tool顺序与并行语义Tool Result模型能否继续推理Length Stop截断是否正确标记Abort请求是否真正取消UsageInput、Output、Cache 是否可信Reasoning是否支持如何返回图片支持的格式与大小Context Limit错误是否明确Model Discovery目录和能力是否准确Cross-provider历史 Tool Call 是否可继续AuthenticationToken 刷新与错误处理Headers自定义 Header 是否泄露只有/chat/completions返回 200并不能证明 Provider 可用于 Agent。Pi README 也把cross-provider-handoff.test.ts单列为 Provider 接入测试但本文没有在本轮环境登录各 Provider因此下表是验收设计不是本文已经取得的运行结果。七、结论迁移的是可移植执行记录Pi 的多模型架构不是“给 Agent 接很多 API”而是建立三层稳定边界Models Collection 负责统一查找、认证和路由 Provider 负责目录、认证、Header 与运行时归属 API Implementation 负责具体 Wire Protocol 与事件转换Agent Core 最终只面对统一的ContextStream EventsAssistantMessageTool CallsUsageStop Reason。当 Session 切换 Provider 时Pi 会尽量保留User Message普通文本Tool CallTool Result可迁移的 Thinking 语义。但它不能迁移模型内部状态Provider 缓存原生 Reasoning 语义所有 Hosted Tool 状态完全一致的后续行为。所以 Context Handoff 的准确含义是把历史转换成目标模型能够理解的可移植执行记录而不是把一个模型的思维状态复制给另一个模型。如果只记住一个判断可以记住这一句接口响应成功只能证明请求走通只有历史关系、降级边界和接入矩阵同时成立才能证明切换后 Agent 仍可继续执行。参考资料Pi AI READMEhttps://github.com/earendil-works/pi/blob/v0.82.1/packages/ai/README.mdModels 实现https://github.com/earendil-works/pi/blob/v0.82.1/packages/ai/src/models.tsProvider 源码目录https://github.com/earendil-works/pi/tree/v0.82.1/packages/ai/src/providersAPI 实现目录https://github.com/earendil-works/pi/tree/v0.82.1/packages/ai/src/apisAgent Core READMEhttps://github.com/earendil-works/pi/blob/v0.82.1/packages/agent/README.mdPi Coding Agent READMEhttps://github.com/earendil-works/pi/blob/v0.82.1/packages/coding-agent/README.md