第 16 章 · 跨提供商切换 Handoffs
本章目标:掌握在同一会话中途切换不同 LLM 提供商的机制,理解上下文迁移时思考块、工具调用的自动转换规则。
16.1 为什么需要 Handoff
真实产品中的常见诉求:用便宜的模型(Gemini Flash)处理闲聊,用户问出难题时无缝切到强模型(Claude Sonnet);或者主模型限流/宕机时降级到备用。难点在于——每个 Provider 对历史消息的格式要求不同,直接把 Claude 的消息塞给 GPT 会出错。
pi-ai 内置了跨提供商的消息转换层,让 handoff 像换一个 model 对象一样简单。
16.2 注册多个 Provider
先注册所有可能用到的 Provider——setProvider() 可以连续调用,互不覆盖:
typescript
import { createModels, type Context } from '@earendil-works/pi-ai';
import { anthropicProvider } from '@earendil-works/pi-ai/providers/anthropic';
import { openaiProvider } from '@earendil-works/pi-ai/providers/openai';
import { googleProvider } from '@earendil-works/pi-ai/providers/google';
const models = createModels();
models.setProvider(anthropicProvider());
models.setProvider(openaiProvider());
models.setProvider(googleProvider());
const context: Context = { messages: [] };16.3 中途切换的完整示例
同一个 context 贯穿始终,唯一的变量是每次 complete 传入的 model:
typescript
// 第一轮:用 Claude 回答
const claude = models.getModel('anthropic', 'claude-sonnet-4-5')!;
context.messages.push({ role: 'user', content: 'What is 25 * 18?', timestamp: Date.now() });
context.messages.push(
await models.completeSimple(claude, context, { reasoning: 'medium' })
);
// 第二轮:切换到 GPT——它会看到 Claude 的思考被转为 <thinking> 标签文本
const gpt5 = models.getModel('openai', 'gpt-5-mini')!;
context.messages.push({ role: 'user', content: 'Is that calculation correct?', timestamp: Date.now() });
context.messages.push(await models.complete(gpt5, context));
// 第三轮:再切到 Gemini
const gemini = models.getModel('google', 'gemini-2.5-flash')!;
context.messages.push({ role: 'user', content: 'What was the original question?', timestamp: Date.now() });
const geminiResponse = await models.completeSimple(gemini, context);三次调用共享完整上下文——GPT 知道 Claude 算了什么,Gemini 知道前两轮的全部内容。
16.4 消息转换规则
当消息从 Provider A 迁移到 Provider B 时,库自动执行如下转换:
| 消息类型 | 跨 Provider 处理方式 |
|---|---|
| 用户消息 / 工具结果 | 原样透传 |
| 同 Provider 的 assistant 消息 | 原样保留 |
| 不同 Provider 的 assistant 消息 | 思考块转换为 <thinking> 标签包裹的文本 |
| 工具调用与普通文本 | 原样保留 |
typescript
// 转换示意:Claude 的 thinking block 到 GPT 眼里变成:
// <thinking>Need to inspect package metadata first.</thinking>思考块会降级为文本
跨提供商时思考内容不会丢失,但从结构化 block 退化为带标签的纯文本。新模型看到的是文本形式的"前任推理",而非它自己原生的 reasoning 格式。
16.5 实战:智能降级路由
把 handoff 用于生产——按任务复杂度动态选模型,失败自动降级:
typescript
// 智能路由:复杂问题用强模型,其余用便宜模型;失败降级重试
async function smartComplete(models: Models, context: Context, complex: boolean) {
const chain = complex
? ['anthropic:claude-sonnet-4-5', 'openai:gpt-5-mini'] // 强模型优先
: ['google:gemini-2.5-flash', 'anthropic:claude-haiku']; // 便宜优先
for (const spec of chain) {
const [provider, id] = spec.split(':');
const model = models.getModel(provider, id);
if (!model) continue;
const res = await models.completeSimple(model, context);
if (res.stopReason !== 'error') {
context.messages.push(res); // 成功:写入上下文供后续 handoff 使用
return res;
}
console.warn(`${spec} 失败,降级到下一个模型`);
}
throw new Error('所有模型均不可用');
}本章小结
- Handoff 让同一会话在中途切换 Provider,上下文完整保留;
- 注册多个 Provider 后,切换只是换一个 model 参数;
- 转换规则:user/toolResult 透传、同 Provider assistant 保留、跨 Provider 思考块转
<thinking>文本、工具调用保留; - 思考块跨商会降级为文本形式,不丢失但格式变化;
- 典型应用:难度分级路由 + 故障自动降级链。
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. Claude 生成的思考块在会话切换到 GPT 后会变成什么?
2. 工具调用和工具结果跨提供商传递时会怎样?
3. 注册多个 Provider 的正确方式是?
4. 构建"故障降级链"时,判断某次生成失败的可靠依据是?
🛠️ 动手实践
- 实现三段式对话:Claude 出题 → GPT 解答 → Gemini 点评,全程共享同一 context。
- 编写测试验证 handoff 后新模型确实能看到
<thinking>标签文本(结合 Faux)。 - 给第 14 章的 DeepSeek Provider 加入降级链:DeepSeek 故障时自动切到 OpenAI。
接下来解决长会话的记忆问题——第 17 章 · 会话持久化。