月初收到云账单的时候,我盯着那串数字看了很久,以为统计口径出了问题。这只是一个 3000 并发以内的企业知识库 Agent,上个月模型推理费用直接干到 2.3 万美元。我们没有选错模型,旗舰模型的效果确实稳,但问题是——你让所有请求都走最贵的那条链路,再充足的预算也撑不过一个季度。
痛定思痛,我开始动手调整:参考 LangChain 社区里“模型路由器”的经典思路,把不同复杂度的请求分流到不同档位的模型,让容易的问容易的模型答,难啃的骨头才交给大模型。而且我没有停留在 Python 生态里,而是把这套路由机制完整地用 Java 重写了一遍,直接接进了现有 Spring Boot 服务。跑了一个多月后,推理账单从 2.3 万美元降到 8000 美元出头,降幅稳定在 64% 左右。今天就把这份 Java 版“抄作业”方案完整拆开,讲清楚三个问题:路由器到底在解决什么、它的决策逻辑怎么建模、Java 落地时哪些坑一定不能踩。
1. 先算一笔账:Agent 的钱到底烧在哪
1.1 推理账单的构成拆解
很多第一次做 Agent 的团队,对成本的认识只有一个模糊概念——“模型调用要花钱”。实际拆开账单后会发现,钱主要烧在四个地方。
第一是每轮对话的模型调用费。一个带工具调用的 Agent 任务,通常不是一次 LLM 调用就结束的,它需要多轮推理:理解用户意图、拆解子任务、调用工具拿到结果、再把结果加工成自然语言回复。我统计过我们的知识库 Agent,一个稍复杂的查询平均要走 5 到 8 次 LLM 调用,每次调用即使 prompt 只有几百 token,累积起来也相当吓人。
第二是上下文膨胀。Agent 每轮调用都要把“历史消息 + 工具返回结果 + system prompt”拼在一起发给模型。工具返回的 JSON 往往很长,一次数据库查询就可能产生 2000 多个 token,而这些 token 每一轮都要重复上传。Google 在 2024 年公开过一份研究报告,提到 Agent 类应用中非必要 token 占整体输入的 34% 左右——也就是说三分之一的开销其实是重复的“搬运”。
第三是输出 token 比输入贵很多。在大多数模型的计价体系里,输出 token 的单价是输入的 3 到 5 倍。Agent 需要生成结构化 JSON 给下游解析,一旦模型返回了冗余字段或者格式不稳定,重试一两次,成本就翻倍。
第四是失败重试。Agent 链路上任何一个环节出问题——工具调用超时、JSON 解析失败、模型返回幻觉结果——都可能导致整条链路重新跑一遍。我见过最夸张的一次,一个订单查询任务因为参数抽取错误重试了四轮,最后花费是正常情况的 6 倍。
1.2 全量使用旗舰模型为什么不可持续
假设你选的是各家厂商最强的推理模型,按输入 5 美元/百万 token、输出 15 美元/百万 token 的中高价位估算。一个真实用户完成一次复杂咨询,平均消耗输入 token 约 4000,输出 token 约 800,单次成本差不多 0.03 美元。这数字看着不大,但企业知识库 Agent 是高频业务:一天 1 万次咨询,日成本就 300 美元,一个月 9000 美元。如果你还叠加了上下文缓存、夜间重试、日志级联,账单破万很容易。
更要命的是,这些请求里真正需要“最强推理”的,比例比你想象的低得多。我们在路由改造前对线上日志做了一个 7 天的采样统计:用户请求里大约 41% 是“查订单状态”“查物流轨迹”“查公司政策条款”这类确定性检索任务,本质是意图识别 + 参数抽取 + 数据查询;32% 是“这个表格帮我汇总一下”“把会议记录按主题归档”这类轻量任务,小模型完全能胜任;真正需要复杂推理、多步骤规划、长文本综合判断的,只有 27% 左右。也就是说,我们把 70% 的流量都送进了最贵的“推理赛道”,这账当然算不过来。
所以问题的根本解法,不是去压模型单价,而是改变流量分发策略——这正是模型路由器要干的事。
1.3 模型路由器的核心逻辑:让流量分级流动
LangChain 社区里提到的模型路由器,本质上不是一个神秘框架,而是一个决策单元:在每次调用 LLM 前,根据当前请求的特征,决定这次调用应该发往哪个模型。
典型的路由决策维度有三个。一是任务类型:这是一个分类任务、抽取任务、检索任务,还是开放式的复杂推理任务?分类和抽取用中档模型绰绰有余,复杂推理才需要旗舰模型。二是上下文预算:当前 prompt 已经有多长?如果已经超过 8000 token,很多轻量模型的上下文窗口根本接不住,只能走大窗口模型。三是质量要求:下游是直接给用户展示结果,还是作为中间结果被另一个模型继续加工?中间环节允许一定的糙度,可以选更快的模型。
把这套逻辑用在我们的知识库 Agent 上,效果立竿见影。我把 41% 的确定性检索请求全部路由到轻量模型,单次调用成本从 0.03 美元降到 0.004 美元;32% 的汇总整理请求路由到中档模型,成本降一半以上;只有 27% 的复杂推理请求继续走旗舰模型。综合算下来,单次成本从 0.03 美元降到 0.0108 美元,正好砍掉 64%。
64% 这个数字不是我拍脑袋定的,它就是流量分布和模型价差算出来的数学结论。下面详细拆解 LangChain 路由器里的三种判断策略,以及它们在 Java 里怎么建模落地。
2. 路由决策里三件脏活:从 LangChain 抄来的核心机制
2.1 基于规则的意图路由:最稳,也最先做
路由器的第一版,优先级最高的就是规则路由。它不调用任何模型做判断,纯粹靠业务规则在入口处分流。
具体做法是把用户请求先做一轮轻量预处理:用正则、关键词表、业务白名单去匹配用户输入,判断当前请求属于哪个意图域。比如我们的知识库 Agent,会先匹配“订单”“物流”“退款”这类业务词,一旦命中,就直接打到订单查询这条链路上。这条链路的模型只需要做一件事:从用户的话里抽取订单编号和查询参数,然后用便宜模型生成最终回复。
Java 实现规则路由很简单,但有一个关键点要注意:规则的命中判断不要写在业务代码里散落一地,而是抽象成一个可配置的规则表,方便后续运维人员调整。我用一个简单的 Map 结构维护关键词组到路由标签的映射,规则可以随时热加载修改。
public class RuleBasedRouter implements RouterStrategy { private final Map<String, TaskType> keywordRules = new ConcurrentHashMap<>(); public RuleBasedRouter() { // 示例规则:命中订单业务词,走简单检索通道 keywordRules.put("订单|物流|退款|售后", TaskType.SIMPLE_RETRIEVAL); keywordRules.put("统计|汇总|对比|分析表", TaskType.LIGHT_REASONING); } @Override public TaskType route(RoutingContext ctx) { String userInput = ctx.userInput(); for (Map.Entry<String, TaskType> entry : keywordRules.entrySet()) { if (Pattern.compile(entry.getKey()).matcher(userInput).find()) { return entry.getValue(); } } return TaskType.COMPLEX_REASONING; } }这条规则看起来土,但它有一个巨大优势:零成本。每个请求只做一次正则匹配,微秒级完成,不需要消耗任何模型 token。而且它对“砍账单”的贡献最大——我们 41% 的检索类流量,几乎没有经过任何模型判断就被分流到了轻量模型上。
2.2 基于 Token 预算的动态路由:处理中间地带的请求
规则路由覆盖了高频业务请求,但还有大量请求无法靠关键词确定意图。比如用户问“我们公司上个月的退款率为什么升高了”,这种表述既包含业务词“退款”,但实际意图不是查单个订单,而是做数据分析。如果按照规则路由的匹配,它会被错误地分到 SIMPLE_RETRIEVAL,然后便宜模型会给出一个缺少推理的错误答案。
处理这类请求,第二种策略是看预算——预算指的不是钱,而是上下文长度和预期输出长度。中档模型在处理 8000 token 以内的上下文时,输出质量与旗舰模型差距很小;一旦超过这个阈值,理解能力和格式遵循能力就会明显下降。所以 Token 预算路由的做法是:先估算当前请求的 prompt 长度,如果超过设定阈值,直接升级到旗舰模型;如果长度可控,再用中档模型。
估算 prompt 长度时可以做一个简单的缓存映射:每个用户的会话历史 token 数按消息条数估算并缓存起来,避免每条消息都调一次 tokenizer。这里我直接给 Java 实现思路,用 TokenEstimator 工具类缓存最近的会话 token 消耗,超阈值时强制升级模型。
public class TokenBudgetRouter implements RouterStrategy { private static final int LIGHT_MODEL_MAX_CONTEXT = 8192; private static final int MID_MODEL_MAX_CONTEXT = 32768; @Override public TaskType route(RoutingContext ctx) { int historyTokens = ctx.estimatedHistoryTokens(); String taskTag = ctx.taskTag(); // 轻量模型只处理短上下文 + 简单任务 if (taskTag.equals("SIMPLE_RETRIEVAL") && historyTokens < LIGHT_MODEL_MAX_CONTEXT) { return TaskType.SIMPLE_RETRIEVAL; } // 中档模型处理中等长度上下文 if (historyTokens < MID_MODEL_MAX_CONTEXT && taskTag.equals("LIGHT_REASONING")) { return TaskType.LIGHT_REASONING; } // 兜底:全走复杂推理通道 return TaskType.COMPLEX_REASONING; } }这套逻辑里的核心是阈值设置。我在实测中发现一个现象:上下文的“压力点”不是线性的,而是存在明显的陡坡。当一个会话的累计 token 从 6000 涨到 8000,中档模型的错误率几乎翻倍。所以阈值不要拍脑袋定,跑一批离线数据对比不同长度下的准确率,选准确率下跌最明显的位置作为升级点。
2.3 基于 LLM 打分器的动态路由:进阶玩法要克制
前两种策略覆盖了大约 80% 的请求,剩下的 20% 确实要靠模型自己判断。LangChain 社区的动态路由思路是:用一个小型模型或分类器对请求打分,判断“这个请求到底有多难”,然后根据分数选择合适的下游模型。
我在 Java 版里也实现了打分路由,但这里必须提醒一句:LLM 打分器本身也是一次模型调用,它也会产生 token 费用。如果你的打分模型和最终回复模型都是收费的,那相当于把一次调用变成了两次,成本反而有可能上升。我见过一些团队在这个环节翻车,本来想省钱的,结果每次请求先打一次分再调用一次模型,总费用比原来直连旗舰模型还贵 18%。
所以我的建议是:打分器只承担两种职责。第一,规则命中了但置信度不足的场景——比如正则同时命中两个业务域,无法确定用户真实意图;第二,前两种策略都无法分类的“模糊请求”。而且打分器用最小的模型即可,它只输出一个 JSON,包含意图标签和置信度分数,不需要生成大段文本。
public class LLMScoringRouter implements RouterStrategy { private final LLMClient scoringClient; public LLMScoringRouter(LLMClient scoringClient) { this.scoringClient = scoringClient; } @Override public TaskType route(RoutingContext ctx) { String scorePrompt = """ 请判断以下用户请求的复杂度,只返回 JSON: {"task_type": "SIMPLE_RETRIEVAL 或 LIGHT_REASONING 或 COMPLEX_REASONING", "confidence": 0.0-1.0} 用户请求:%s """.formatted(ctx.userInput()); ScoringResult result = scoringClient.call(scorePrompt, ScoringResult.class); if (result.confidence() < 0.6) { return TaskType.COMPLEX_REASONING; } return result.taskType(); } }打分器路由我建议放在路由链的最末端,而且要做熔断:一旦打分器的错误率连续超过 5%,说明它已经不稳定了,直接走兜底策略全部路由到旗舰模型,别让一个不稳定的判断环节拖垮整个 Agent 链路的响应时间。
3. Java 版抄作业:一个可直接落地的轻量路由器
3.1 项目结构设计与依赖选型
我在做这个改造时,没有引入任何 Agent 编排框架,因为团队主力技术栈是 Java,不想为这一个功能引入一套重的 Python 基础设施。最终只选了四个常规依赖:
- Spring Boot 3.2 + Spring WebMVC,提供 HTTP 服务与配置管理
- OkHttp,作为调用模型 API 的 HTTP 客户端,连接池管理方便
- Jackson,处理模型返回的 JSON 数据
- Caffeine,做路由结果和 token 估算的本地缓存
如果你们团队已经在用 Spring AI,可以直接用它封装好的 OpenAI 兼容 API 客户端,省掉 OkHttp 和 Jackson 的接入成本。我这里给出的是不依赖 Spring AI 的通用方案,方便大家自由切换模型服务商。
项目核心包结构我按职责拆分成了四个目录:
com.company.agent.router ├── model // ModelProfile、RoutingContext、TaskType 等数据类 ├── strategy // 路由策略接口与三种实现 ├── dispatch // 路由分发器,调度各策略,维护链路 └── monitor // 调用记录、费用统计、指标上报3.2 动态模型配置:每个模型一份档案
路由器的核心设计理念是“模型配置与业务代码分离”。我用一个 model-profile.json 文件维护所有可用的模型信息,包括模型标识、接入端点、计费价格、上下文上限、路由标签。这样调整模型时不需要重新发布代码,只要在配置中心更新这份文件。
模型配置字段设计如下,其中 pricePer1kInputToken 和 pricePer1kOutputToken 是关键,它们决定了费用统计模块的计算基准。
{ "models": [ { "name": "light-1", "endpoint": "https://api.your-model-service.com/v1/chat/completions", "modelId": "your-light-model", "pricePer1kInputToken": 0.001, "pricePer1kOutputToken": 0.002, "maxContextTokens": 16000, "routerTags": ["SIMPLE_RETRIEVAL", "LIGHT_REASONING"] }, { "name": "mid-1", "endpoint": "https://api.your-model-service.com/v1/chat/completions", "modelId": "your-mid-model", "pricePer1kInputToken": 0.003, "pricePer1kOutputToken": 0.008, "maxContextTokens": 32768, "routerTags": ["LIGHT_REASONING", "MODERATE_SUMMARY"] }, { "name": "flagship-1", "endpoint": "https://api.your-model-service.com/v1/chat/completions", "modelId": "your-flagship-model", "pricePer1kInputToken": 0.005, "pricePer1kOutputToken": 0.015, "maxContextTokens": 131072, "routerTags": ["COMPLEX_REASONING"] } ] }配置文件加载后,通过 Spring 的 @ConfigurationProperties 绑定到一个 ModelRegistry 对象。每个 ModelProfile 里的 routerTags 数组很关键,它决定了某个 TaskType 可以路由到哪些模型。路由分发时,我会在候选模型列表里做选择,而不是每次只认一个固定模型。
3.3 路由分发器:策略链怎么排优先级
三种策略不能是平级关系,必须排列执行顺序,否则会出现同一个请求被多种策略误解的情况。我最终采用的分发模式是责任链:
第一步,规则路由先行,它能直接判定意图就立即返回,不进入后续判断。 第二步,Token 预算路由检查上下文长度,如果当前会话太长,无论规则怎么匹配,都强制升级到能处理长上下文的模型。 第三步,如果前两者都无法确定,才调用打分模型做意图分类。 最后兜底,仍然不确定的任务一律走旗舰模型,保证用户体验优先于成本优化。
这个顺序是有讲究的。规则路由成本最低,最先执行;Token 预算路由保护了长会话场景下的质量;打分模型只在模糊请求上使用。兜底策略保证了任何漏网之鱼都不会拿到一个明显跑不动任务的弱模型。下面是一个精简版的分发器实现:
@Component public class RouterDispatcher { private final List<RouterStrategy> strategies; private final ModelRegistry modelRegistry; public RouterDispatcher(List<RouterStrategy> strategies, ModelRegistry modelRegistry) { // 按 @Order 注解排序后的策略列表 this.strategies = strategies; this.modelRegistry = modelRegistry; } public ModelProfile decide(RoutingContext ctx) { for (RouterStrategy strategy : strategies) { TaskType taskType = strategy.route(ctx); if (taskType != TaskType.UNKNOWN) { Optional<ModelProfile> selected = modelRegistry.select(ctx.taskTag(), taskType); if (selected.isPresent()) { monitor.capture(ctx, selected.get()); return selected.get(); } } } // 兜底 return modelRegistry.getFlagshipModel(); } }实际项目中,我在 RoutingContext 里放了一个 taskTag 字段,它由上游业务模块赋值,用于标记当前这次调用所属的业务域。这个字段配合 ModelProfile.routerTags,让模型选择带上了业务域约束,比如财务域的请求即使判定为 SIMPLE_RETRIEVAL,也只从支持财务域的轻量模型中选,避免跨域串味。
3.4 费用统计必须做成基础设施
路由器如果没有费用统计,就是黑盒优化,根本没法量化到底省了多少。我在改造当初就把费用统计做成了一等公民,所有模型调用统一经过一个 CostRecorder 过滤器。
CostRecorder 的职责有两个。第一,从模型 API 的响应里提取 usage 字段中的 prompt_tokens、completion_tokens、total_tokens,以及命中的模型名。第二,根据 ModelProfile 里的单价,实时累加本次调用的费用,并按业务线、模型名、任务类型三个维度打点。
@Component public class OpenAiCompatibleClient { private final CostRecorder costRecorder; private final OkHttpClient httpClient; private final ObjectMapper objectMapper; public ChatResponse call(ModelProfile profile, ChatRequest request) { String body = buildRequestBody(profile, request); try { String responseBody = executeHttp(profile, body); ChatResponse response = objectMapper.readValue(responseBody, ChatResponse.class); // 这里统一上报 token 消耗与费用,后续可以推到 Prometheus / 日志系统 costRecorder.record( profile.name(), response.usage().promptTokens(), response.usage().completionTokens(), profile.pricePer1kInputToken(), profile.pricePer1kOutputToken() ); return response; } catch (IOException e) { throw new ModelCallException("model call failed", e); } } }我见过不少团队用日志收集 token 再用脚本做后置统计,这个方案延迟太严重,优化迭代时根本等不起。最好的办法是在调用链路上实时同步统计,每个请求完成后 1 秒内就能在 Grafana 看到费用曲线。
4. Java 落地中的五个关键坑与排查实录
4.1 坑一:把便宜模型当万能药,简单任务准确率一样崩
路由改造第一周,我踩了一个典型的坑:把所有 SIMPLE_RETRIEVAL 请求全部丢给最便宜的 light-1 模型后,任务整体准确率下降了 12%。一开始以为是规则路由把一些复杂请求误判成了简单请求,后来定位发现是另一回事——部分“简单任务”其实对输出格式有严格要求。
比如用户问“订单 SH2024001 现在什么状态”,如果便宜模型返回的 JSON 字段名不是我们约定的 status_code,而是 statusCode,下游字段映射就断了。解决这个问题的办法不是把所有流量升级到旗舰模型,而是给便宜模型配一个结构化的少量样本提示词,强制约束输出格式。我给 light-1 模型加了固定输出模板后,准确率回升了 8 个百分点。
这个坑的教训是:路由分流后,必须针对每个档位的模型重新调优提示词,不能从旗舰模型原样复制。每个模型的指令遵循能力和输出偏好不同,需要单独打磨。
4.2 坑二:并发场景下路由状态污染
路由器的 RoutingContext 里我一开始用 ThreadLocal 传递会话信息,结果在 Spring Boot 的线程池场景下出现了灾难性的串数据。一个用户的查询参数跑到了另一个用户的请求里,排查了整整半天才定位到问题。
原因很典型:Spring 的 @Async 或者异步线程池会复用线程,ThreadLocal 里的上下文没有清理,导致下一个任务读到上一个任务残留的数据。解决方式是异步调用时显式传递上下文,而不是依赖线程局部变量。我在 Java 实现里最终改用方法参数传递 RoutingContext,并强制规定所有调用链路上的方法都显式接收该参数。
public class RoutingContext { private String requestId; private String userInput; private String taskTag; private int estimatedHistoryTokens; // 构造方法、getter/setter 此处省略 }如果你在代码规范里允许 ThreadLocal 使用,至少要保证外层包裹 try-finally 并在 finally 中调用 remove(),这个底线不能丢。
4.3 坑三:模型 API 限流与超时导致整条链路雪崩
路由器上线后,下游模型服务商发现我们的调用模式变了:以前是所有请求均匀地打向一个模型,现在是大量请求突然涌向某个轻量模型。这导致轻量模型的 API 网关触发限流,部分调用直接失败返回。我还在日志里看到了类似 agent rpc error (-1) 的空 sid 和服务名错误,本质就是连接被服务端掐断后,客户端用了过期的连接再次发起请求。
排查经验有三点。第一,路由后的流量形态已经变了,必须为轻量模型单独预留更高的调用配额,不能沿用旧配置。第二,HTTP 客户端要配置合理的连接池参数和 keep-alive 时长,OkHttp 这种客户端默认行为未必符合模型服务商网关的会话过期时间。第三,调用失败必须有自动降级,假设 light-1 限流,下一个候选应该是 mid-1 而不是直接抛异常。
private String executeHttp(ModelProfile profile, String body) throws IOException { Request request = new Request.Builder() .url(profile.endpoint()) .post(RequestBody.create(body, MediaType.parse("application/json"))) .addHeader("Authorization", "Bearer " + getApiKey(profile.apiKeyRef())) .addHeader("Content-Type", "application/json") .build(); int retries = 0; while (retries < 2) { try (Response response = httpClient.newCall(request).execute()) { if (response.isSuccessful()) { return response.body().string(); } if (response.code() == 429 || response.code() >= 500) { Thread.sleep(200L * (retries + 1)); retries++; continue; } throw new ModelCallException("HTTP error: " + response.code()); } } throw new ModelCallException("retry exhausted"); }上面这段代码里的指数退避策略比较简单,生产环境建议结合响应头里的 Retry-After 做精确等待。
4.4 坑四:计费统计口径不一致,64% 是虚的还是实的
做费用统计的时候,有一个容易被忽视的差异:不同模型服务商对 token 的计价方式不同,有的按“输入 token + 输出 token”统一计费,有的按“命中缓存的输入 token、未命中缓存的输入 token、输出 token”三项分别计费。如果你的统计模块只按两项计价,算出来的节省比例和实际账单会有偏差。
我在第一版费用统计里就没考虑缓存 token 的差异,导致看板显示的费用比真实账单低了很多。后来调整 CostRecorder,让它直接读取响应里的 usage 明细,按输入、输出、缓存命中三种类型分别记录,并根据服务商定价规则计算。建议各位在做统计模块时,先查阅一下所用模型服务商的报价文档,把 pricing 写进 ModelProfile,而不是把价格写死在代码逻辑里。
4.5 坑五:路由规则硬化,改一条规则要动一次发版
第一版路由器里,我把规则表直接写在 Java 枚举里,导致每次调整阈值都要走完整的代码评审和发布流程。业务方想改一个关键词或者路由目标模型,等发版等了两天。
合理做法是把路由规则和阈值全部外置到配置中心。我用的是 Nacos,把 model-profile.json 和 route-rules.json 推送到配置中心,应用启动时读取,运行时监听配置变更事件,自动刷新内存中的规则缓存。关键路径上采用双缓冲结构——新配置在后台构建完成后,原子替换掉旧配置的引用,避免热加载过程中出现半更新状态。
public class DynamicConfigHolder<T> { private volatile T config; private final Supplier<T> loader; public DynamicConfigHolder(Supplier<T> loader) { this.loader = loader; this.config = loader.get(); } public void refresh() { T newConfig = loader.get(); this.config = newConfig; // 原子替换 } public T get() { return config; } }这套方案上线后,规则调整从两天变为分钟级,省下来的时间成本比模型省下的钱还值。
5. 两种升级路径:从单点路由到企业级 Gateway
5.1 路由器不只是 Agent 的专属工具
模型路由器这层抽象,完全可以脱离 Agent,变成一个独立的模型网关服务。我后来把路由逻辑包了一层 REST API 暴露给团队内部其他系统,一些业务方做的智能客服、报表解读工具也开始复用这套分流逻辑。效果是各个系统都不再直接依赖具体模型厂商的 API,而是统一走网关,这层的价值不局限于省成本,还顺带解决了模型供应商切换的难题——一个接口切换到底,下游模型配置文件一换,所有系统都跟着切过去了。
如果你有兴趣继续往这个方向做,可以看看 LangChain4j 的工具调用设计,以及 Spring AI 的动态模型切换能力。不过对我来说,轻量自研这套网关,最大的价值是四个字:心里有数。每一笔调用走的是哪个模型、花了多少钱、延迟多少,全都一目了然。
5.2 可观测性驱动路由策略迭代
路由器上线后,不要把费用下降 64% 当作终点,持续调优的空间还很大。我给自己的迭代节奏设成每周复盘一次路由决策质量。
复盘的核心数据有三个:各档位模型请求后的用户显式反馈(比如“这个回答不对”)、任务整体完成率、以及分模型的 token 单价与耗时中位数。如果某周 light-1 模型的用户显式投诉率超过 2%,我会调高它向 mid-1 升级的阈值。如果 mid-1 的平均响应时间低于上游制定的 SLO,就说明它还有余力承载更多流量,可以把部分 COMPLEX_REASONING 的任务试探性地降级到 mid-1 处理。
这种调优十分依赖统计粒度的清晰。我建议费用统计至少按照 业务线、任务类型、模型名称、日期 四个维度做 Cube,而不是只算一个月度总成本。只有拆到足够细,才知道钱省在哪、省得值不值。
5.3 不要省略的东西
最后分享一个小技巧。我在开发 ModelProfile 的时候,故意给每个模型加了一个 isFallbackAllowed 字段,并且规定只有旗舰模型允许被设置为其他模型的降级目标。这样即使路由配置被误操作,也不会出现“轻量模型被选为复杂任务的兜底”这种事故。这不是一个复杂的设计,但它天然防止了配置层的稳定性风险。
另外,如果你也要在 Java 技术栈里“抄作业”这套模型路由器,不需要把 LangChain 整个框架引入进来,它的价值核心在于路由决策的思路,而不是 flink 式的技术栈依赖。真正难的不是写一个 Router 接口,而是业务规则的抽象、成本账的透明化、以及模型调优的持续投入。
对我个人来说,这个改造最有价值的收获,是把“智能”两个字从玄学变成了工程度量。64% 不是终点,只是一个开始。