用户换了模型,Agent 的行为应该无缝延续。

为什么需要路由层

一个 Agent 产品如果只支持一个 LLM 服务商,工程上会简单很多。但现实迫使你不可能只绑定一家。

首先是可用性。任何一家 LLM 服务商都会遭遇宕机、限流、模型下线。如果你的 Agent 只绑定一家,那在服务中断的几个小时里,你的产品就是一个空壳。

其次是成本。不同场景对模型能力的要求差异极大。主对话需要最聪明的模型,但记忆提取和权限分类只需要一个便宜够用的小模型。如果所有场景都用同一个模型,成本会高出数倍。

最后是地域。国内用户用 Qwen 或 DeepSeek 延迟最低,海外用户用 Claude 或 GPT 体验最好。一个面向全球用户的产品不可能只接一家。

这些需求叠加在一起,意味着你需要一个路由层,把 Agent 循环和具体的 LLM 服务商解耦。

多模型策略的方案对比

在设计路由层之前,需要先想清楚一个更上层的问题:Agent 产品应该用什么样的多模型策略?

方案 A:单一最强模型

只接最好的那一个模型。工程最简单,提示词只需要针对一个模型优化,行为一致性最好。代价是绑定了单一供应商,模型降级时没有退路,而且不同场景无法用不同档次的模型来优化成本。Claude Code 最初就是这个策略,只支持 Anthropic 自家模型,好处是能把提示词和模型特性深度绑定,坏处是用户完全没有选择。

方案 B:任务路由(自动分发)

系统根据任务类型自动选择最合适的模型。比如代码生成用 Codex,翻译用 GPT,数学推理用 Qwen。这在理论上是最优的,但工程代价非常高。需要一个元分类器来判断当前任务的类型,这个分类器本身也是一个 LLM 调用,增加了延迟和成本。更大的问题是分类器不可能做到完全准确,错误的路由反而比固定模型更糟。

方案 C:用户自选 + 场景默认

让用户自己选主对话模型,同时系统为不同场景(记忆提取、权限分类、压缩)预设合理的默认模型。这是 Alice 采用的策略。用户对主对话模型有明确的偏好和感知,应该尊重这个偏好;内部场景用户不感知,系统替用户做最优选择。

方案 D:自动降级

平时用最好的模型,检测到错误或超时时自动切换到备选模型。这需要维护一个模型优先级列表和健康检查机制。实现上不复杂,但对用户体验有隐患:用户正在进行一个复杂任务,中途模型降级到一个小模型,行为可能出现断裂。

Alice 最终选择了 C 方案作为主策略,辅以 D 方案的超时保护。用户选择决定主体验,系统在极端情况下兜底。

统一接口设计

无论底层是哪个 Provider,路由层对上层暴露完全统一的接口。Agent 循环调用统一的流式接口,拿到一个异步事件流。Agent 循环只消费这些事件,不知道也不关心底层是哪个 Provider。

这个抽象的价值在实际开发中被反复验证:Alice 从最初只支持 OpenAI 兼容 API 到支持 Anthropic 原生 API,再到支持 Gemini,Agent 循环的代码一行没改。

适配多家服务商的工程代价

统一接口听起来很美,但实际适配二十多个服务商的过程充满了琐碎的差异处理。

三种 API 类型

所有 LLM 服务商归结为三种 API 格式:OpenAI 兼容(覆盖 OpenAI、阿里云 DashScope、DeepSeek、Moonshot、LM Studio 等大多数服务商)、Anthropic 原生(Claude 系列独有的格式)、Gemini(Google 自己的格式)。

三个适配器听起来不多,但每个适配器内部都有大量的边界处理。OpenAI 兼容这个名字本身就是个陷阱。虽然大家都号称兼容 OpenAI API,但实际行为差异很大。阿里云 DashScope 的流式返回格式和 OpenAI 有微妙区别,DeepSeek 在推理内容上有自己的扩展字段,Moonshot 的工具调用返回有时候会多嵌套一层。每接入一个新的服务商,都需要至少半天时间来处理这些边界情况。

维护成本的增长曲线

服务商数量和维护成本不是线性关系,增长曲线接近 O(N×M),N 是服务商数量,M 是功能特性数量。每次新增一个功能特性(比如支持图片输入、支持 JSON Mode),你需要在每个适配器里确认这个特性是否被支持,如果不支持要怎么降级。

实际踩坑:Alice 在添加 CoT(Chain of Thought)模型的思考内容展示功能时,发现不同服务商在流式模式下对推理内容的分块方式各有差异,有的会在一个 chunk 里混合推理内容和正式回复,有的严格分开。这个差异导致了前端渲染的闪烁问题,花了两天才定位和修复。

模型白名单管理

为什么不让用户随便填一个模型 ID 就开始用?

没有白名单的后果

早期 Alice 确实允许用户输入任意模型 ID。结果出现了几个问题。用户输入了一个不存在的模型 ID,LLM 请求返回 404 错误,Agent 循环进入错误状态,用户以为 Alice 坏了。用户选了一个不支持工具调用的模型,模型无法执行任何工具,Agent 变成了一个普通聊天机器人,用户不理解为什么 Alice 突然不能搜索、不能读文件了。用户选了一个上下文窗口很小的模型,对话稍微长一点就开始截断,体验急剧恶化。

白名单的设计

每个 Provider 下维护一个经过验证的模型列表,每个模型声明几个关键属性:是否支持原生工具调用(不支持则使用结构化格式模拟)、最大上下文长度(决定压缩策略的触发阈值)、是否有推理过程字段(决定前端是否展示思考过程)、推荐使用场景(主对话、记忆提取、分类器等)。

白名单也不是完全封闭的。Alice 保留了一个自定义模型入口,允许高级用户添加不在白名单里的模型,但会显示警告提示,告知可能遇到的兼容性问题。这是一个在用户自由度和产品稳定性之间的平衡。

结构化格式工具调用模拟

不支持原生工具调用的模型(本地模型、部分小参数量模型),用结构化文本格式来模拟。在系统提示里注入格式规范,模型按格式输出工具调用,适配器解析结构化文本并转换为标准的工具调用事件。

方案对比

XML 格式 vs JSON 格式 vs 自定义分隔符,三种方案都有人做过。XML 的优势是结构清晰、嵌套友好、大多数模型在预训练数据里见过大量 XML 所以输出质量较好。JSON 模拟的问题是模型经常输出不合法的 JSON(缺少引号、多余逗号),解析失败率明显高于 XML。自定义分隔符方案太脆弱,模型在正常文本输出中可能意外产生分隔符导致误判。

结构化格式模拟的代价是质量下降。模型需要在生成回复的同时还要记住格式约束,这占用了一部分注意力,尤其是小参数量模型,格式遵循率明显低于大模型。但这是一个可以接受的权衡:功能质量下降,但不崩溃,这是可降级原则的体现。

多场景模型配置

Agent 的不同功能对模型的需求完全不同,用同一个模型处理所有场景是巨大的浪费。

场景 核心需求 推荐策略
主对话 最高质量、工具调用能力 最好的可用模型
记忆提取 够用就行、大量调用 低价模型
权限分类 低延迟、简单判断 小快模型
上下文压缩 长文本理解能力 长上下文模型
子 Agent 和主对话接近 中等模型
图像生成 专门能力 多模态模型

这个设计让 Alice 在主体验不妥协的前提下,整体成本降低了数倍。

踩坑经历

最初 Alice 只有一个全局模型设置,所有场景共用。这意味着每次对话结束后的记忆提取也在用最贵的模型。一个用户反馈说他每天只聊了几轮,但 token 消耗异常高。排查后发现记忆提取功能在后台默默用主模型跑了大量 LLM 调用,成本占总消耗相当大比例。引入多场景模型配置后,记忆提取切换到更经济的小模型,成本立刻降了一个数量级。

代理配置

国内用户访问 OpenAI、Anthropic、Google 等海外服务需要代理。但配置所有流量走代理会让国内服务(阿里云、DeepSeek 等)也通过代理绕一圈,增加明显的延迟。

三种代理模式:全部走代理(所有 LLM 请求走代理)、仅海外服务走代理(国内服务直连)、不走代理。

仅海外服务走代理是最常用的配置。每个 Provider 声明一个是否为海外服务的标识,路由层据此决定是否使用代理。这个设计解决了一个国内 Agent 产品的普遍痛点:用户既想用海外大模型也想用国内大模型,但两个服务的网络环境完全不同。

流式超时保护

流式响应有一个特殊的超时场景:连接已经建立,前几个 token 也收到了,但中途突然停止了。HTTP 层面连接没断,只是不再收到数据。

超时阈值的由来

这个值不是拍脑袋定的。实际测量了几个场景:正常对话中大多数模型的 token 间隔很短;长文本生成时模型偶尔会思考一段时间再继续输出;工具调用的 JSON 参数生成时部分模型会在关键位置停顿较长;CoT 模型的思考阶段可能持续输出推理内容然后切换到正式回复。

综合这些数据,最终选取的阈值是在兼顾容错和响应性之间取得的合理平衡。太短会误杀正常的长思考;太长会让用户等太久,以为 Alice 卡死了。

超时后的行为

超时触发后直接断开当前流并发起重试。重试使用同一个模型和同一组消息,但会获得一个新的连接。如果连续超时,才会向用户显示错误并建议切换模型。

重试策略的退避设计

LLM API 调用失败的原因多种多样:网络抖动、服务商限流、内部错误、模型过载。简单的立即重试不仅无效,还会加剧服务商的压力。

最大重试次数的选择

这个数字来自对失败模式的统计分析。短暂的网络抖动通常一两次重试就能恢复。服务商的临时限流一般持续半分钟到两分钟,指数退避到第四五次重试时通常已经过了限流窗口。如果经过足够多的重试仍然失败,说明是持续性故障(服务商宕机),继续重试没有意义,应该通知用户。

为什么是指数退避

三种退避策略的对比:固定间隔重试在限流场景下效果差,因为多个客户端会以相同节奏同时重试,形成惊群效应;线性递增稍好,但增长太慢,在长时间故障时重试太频繁;指数退避初始重试快(能快速恢复短暂故障),后期间隔拉大(不会在持续故障时浪费资源)。加上随机抖动(jitter),可以有效避免多客户端的重试同步。

和行业实践的对比

AWS SDK 的默认最大重试次数较少,因为 AWS 服务的 SLA 通常很高,长时间故障极少。LLM API 的可用性远没有这么高,所以需要更多的重试次数来覆盖更频繁的短暂故障。

CoT 模型的特殊处理

部分模型(支持链式思考的模型)在流式响应中包含推理过程内容,这对适配器提出了额外要求。

需要处理三件事:第一,把推理过程保留在 assistant 消息里,某些模型 API 要求在后续请求中包含之前的思考内容,否则质量会下降;第二,可选地把思考内容展示给用户,Alice 里这个功能让用户可以看到模型的思考过程;第三,不同模型的推理字段名和格式不同,需要在适配器层统一处理。

LLM 调用日志

每次 LLM 调用都记录:调用来源(发起目的,比如主对话、记忆提取、压缩、分类器、子 Agent)、使用的模型、输入和输出 token 用量、耗时、会话标识。

调用来源字段是成本分析的关键维度。没有这个字段,你只能看到总的 token 消耗,无法知道成本的结构分布。有了它,你能回答「为什么这次对话这么贵」的问题,精确到是主对话用了太多 token,还是压缩触发得太频繁,还是记忆提取的提示词太长。

这个设计后来被证明在性能优化中价值巨大。Alice 有一次版本更新后用户反馈延迟增加了,通过调用来源字段分析发现是新增的 Skill 分析功能在每次对话开始时多做了一次 LLM 调用,在日志里一目了然。

和行业方案的对比

维度 Alice Claude Code Cursor LangChain
模型绑定 用户自选 + 场景分离 只支持 Anthropic 模型 多模型但路由逻辑不透明 框架层抽象,不做路由决策
适配器数量 3 类(OpenAI/Anthropic/Gemini) 1 类 内部实现未公开 插件式,数十个
工具调用兼容 原生 + XML 模拟双轨 仅原生 仅原生 取决于具体适配器
成本优化 多场景模型分离 单一模型 有但不透明 用户自行处理
代理支持 分级代理 无需考虑(海外服务) 无需考虑 用户自行处理

Alice 在多模型灵活性上的投入,本质上是因为它面向的是全球范围内、使用不同服务商的个人用户。这和 Claude Code 只服务 Anthropic 用户、Cursor 有自己的代理层是完全不同的产品定位。

上一章:自进化架构 · 下一章:安全架构

本文由 洛小山 发布。引用或转载时,请保留原文链接。