Skip to content

第 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. 构建"故障降级链"时,判断某次生成失败的可靠依据是?

🛠️ 动手实践

  1. 实现三段式对话:Claude 出题 → GPT 解答 → Gemini 点评,全程共享同一 context。
  2. 编写测试验证 handoff 后新模型确实能看到 <thinking> 标签文本(结合 Faux)。
  3. 给第 14 章的 DeepSeek Provider 加入降级链:DeepSeek 故障时自动切到 OpenAI。

接下来解决长会话的记忆问题——第 17 章 · 会话持久化