资讯动态

cn-llm-router:打通harness与国产模型的三层兼容断层

发布时间:2026/10/26 12:41:48 来源:尧图企业网站定制
最近我把常用的那个国外Agent harness接到了国内高性价比模型上折腾了两天最后写了个 cn-llm-router 才彻底消停。先说结论网上常见的“改一行 BASE_URL 就能用”只适用于恰好协议一致的情况真正把 harness 这类 Agent 框架和 DeepSeek 这类国产模型接起来中间隔着三个兼容断层API 协议格式、模型命名规则、以及工具调用Tool Calling的行为预期。这篇文章就把我的踩坑过程和 router 的完整设计一次讲清楚。如果你也在给 harness 找便宜好用的国产模型看完可以直接抄作业。先交代一下背景免得你拿到的配置对不上号。我用的“harness”指的不是某个单一产品而是现在很流行的一类 Agent 框架外壳它负责把大模型、工具调用、上下文管理、执行循环这些组件组装在一起对外提供一套“命令式”的编程入口。这类框架默认绑定国外某家头部的模型服务API 风格是 Messages 格式和国内绝大多数模型服务商采用的 OpenAI Chat Completions 格式并不一样。所以当你想换成 DeepSeek 这类模型时真正的难点不是模型能力够不够而是怎么让一个只说外语的外卖系统听懂一家本地餐厅的菜单。这篇文章适合两类人一类是已经在用 harness想把模型切换到国产 API、省点预算的开发者另一类是刚开始接触 LLM Agent 工程想知道框架和模型之间到底隔着哪些隐形的墙的人。我会把 cn-llm-router 的架构、关键转换细节、配置方式、以及我实际跑到过的故障现场都写出来代码和配置会给到可直接复制改用的程度。1. 为什么 harness 接国产模型不是改个 URL 那么简单1.1 三个真实存在的兼容断层第一个断层是协议格式。主流 harness 默认输出的是 Anthropic 风格的 Messages 请求里面用 system、user、assistant 三段消息结构工具定义里写的是 input_schema。而国内绝大多数模型服务商对外提供的是 OpenAI 兼容接口工具定义里写的是 parameters消息角色也不完全一样。你直接把地址一换请求发过去轻则报 404重则静默返回一段文不对题的文本。第二个断层是模型名。harness 在初始化时会按照它内置的模型表去解析模型参数比如上下文窗口、最大输出、是否支持某些扩展能力。你把请求里的模型名改成国产模型的编号之后harness 内部可能还在按国外模型的元数据去裁剪上下文长度结果就是长对话莫名被截断或者模型明明支持 64K框架偏偏只给你传 8K。第三个断层最坑是工具调用的行为预期。Agent 框架和模型之间的工具调用依赖严格的 JSON 结构。国外消息协议里工具参数是嵌套对象OpenAI 风格里参数却是 JSON 字符串。很多模型服务商为了兼容 OpenAI 做了简化实现但对一些比较少见的字段比如并行工具调用、多轮工具结果回传支持得不彻底。于是你看到的现象就是模型说“我要调用搜索工具”但 harness 拿到的响应格式不合法直接判定为执行失败又让模型重来。一来一回浪费大量 token效果还差。1.2 与其到处打补丁不如加一个路由层我在最开始也尝试过给 harness 打补丁比如魔改它的客户端适配器、给请求体做字符串替换。但问题在于散两个模型之间有差异要改换一个模型还要再改维护成本越来越高。后来我换了个思路不去动 harness也不去动模型服务端而是在两者中间插一层专门做“翻译、路由、护栏”的组件这就是 cn-llm-router。这个思路其实很朴素。你把 harness 想成一个只讲外语的客户把国产模型想成一家只做本地菜的餐厅中间那个会双语的服务员就是 router。harness 永远感觉自己在和原来的国外服务说话router 负责把请求翻译成餐厅能听懂的方言再把餐厅的回应翻译回客户熟悉的表达。这样框架不用改模型不用改所有差异都被收拢到路由层你可以只维护 router 的配置。cn-llm-router 支持三种使用模式单模型直连只做协议转换多模型路由按别名或任务类型分发故障转移模式主模型挂掉自动切备用模型。我实际跑下来日常开发用得最多的是第二种和第三种组合既能控制成本又能保证服务不中断。2. cn-llm-router 是怎么设计的2.1 整体架构与核心模块整个组件只有一个进程监听本地端口对外暴露和国外模型服务一致的 HTTP 接口。内部拆成四个模块入站服务、转换引擎、路由决策器、出站客户端。入站服务负责接收 harness 发来的消息并校验访问令牌。转换引擎是核心它负责把 Anthropic 风格请求转成 OpenAI 风格再把 OpenAI 风格响应转回来。路由决策器根据配置决定当前请求发给哪个模型供应商并且处理重试和故障转移。出站客户端管理对各家模型服务的连接池、超时和限流。关键设计原则是“上层无感知”。harness 不知道自己在跟 router 说话它依然认为自己连的是官方服务所以它的超时策略、重试逻辑、模型元数据解析全部继续生效。这意味着我只要把 router 部署在稳定的本地地址harness 侧的改动就只有两个环境变量。2.2 请求转换要处理的细节请求转换这件事看起来只是改字段名实际做起来有不少细节。首先消息角色要重排。国外的 system 消息是独立字段OpenAI 风格里 system 得塞进 messages 数组的第一条。其次工具定义里的 input_schema 要改成 parameters同时把 type 字段从 object 改成 JSON Schema 对应的写法。另外一个容易被忽略的点是预填充prefill。很多 Agent 框架为了不让模型开场白废话会在请求里塞一段 assistant 消息当作预填内容。OpenAI 风格接口对连续多条 assistant 消息的容忍度不一样如果原样透传有些模型会报错。router 的处理规则是预填充内容如果是纯文本直接合并到上一条 assistant 消息里如果里面带了工具调用块就要拆开重排保证消息序列里 assistant 消息和 tool 消息严格交错。流式开关也要特殊处理。harness 默认开启流式所以 router 永远要在流式模式下工作。转换时要把 stream 参数置为 true并且把 stop 参数从国外协议的结束标记列表映射成 OpenAI 风格里常见的停止词格式。这些细节不处理平时看起来没事一到长文本生成就容易出诡异问题。2.3 响应改写与流式事件重映射响应侧是整个 router 技术含量最高的部分。非流式场景相对简单把 OpenAI 返回的 choices[0].message.tool_calls 转成嵌套对象填进消息内容里就行。但流式场景要在事件流上做实时改写这会涉及一次只能处理一个事件的限制。OpenAI 风格的流式事件是 chat.completion.chunk里面每个 delta 只有一小段增量。Anthropic 风格的事件则是按 content_block 组织的有开始事件、增量事件、结束事件。router 要做的是把 chunk 序列重新组装成 content block 序列。文本增量还好办直接映射成 content_block_delta。工具调用增量就麻烦一点OpenAI 把工具调用按 index 分段流式返回每一段的函数名和参数片段是分多次到达的router 必须做“攒批再转换”也就是等到某个工具的多个参数片段到齐再合成一个完整的 content_block。这里我踩过一个深坑。如果 router 只做简单的“事件名替换”工具调用参数一旦被拆成多个 chunk就会产生半截 JSONharness 解析后直接报错。正确做法是引入一个小的状态机对工具调用片段做缓冲等 JSON 完整后再生成 content_block 事件。行尾的 stop_reason 也要做映射表比如 OpenAI 的 tool_calls 映射到工具的 tool_uselength 映射到 max_tokens。最后还要做 token 用量合并很多国产模型流式接口不返回 usagerouter 要在最后自己补一个估算值插进事件流否则 harness 那边的成本统计会显示 0。3. 路由策略与选模型的实战思路3.1 用别名代替真实模型名强烈建议在 router 里定义一个“别名层”。也就是说harness 侧永远只认你起的名字比如 workhorse、fast_thinker、long_contextrouter 再把这些别名映射到具体的模型编号。这样做的好处是切换模型服务商时你只需要改 router 配置不用动 harness 里任何参数。我之前就是吃了这个亏直接在 harness 配置里写死了某个模型编号后来要换另一家供应商结果所有用到旧模型名的地方都要改。配置弄成别名后这种痛苦彻底消失。一个实际的配置片段是这样models: - alias: workhorse provider: deepseek actual_model: deepseek-chat base_url: https://你的模型服务域名/v1 api_key_env: DEEPSEEK_API_KEY context_window: 65536 max_output_tokens: 8192注意 context_window 和 max_output_tokens 必须写对。router 虽然不帮模型做推理但它要根据这两个值去拦截超限请求避免明明模型接不住那么多 tokenharness 还拼命往里塞。3.2 故障转移与任务类型路由静态映射适合单模型场景但多模型场景下建议再加上路由决策。我的 router 支持两种策略优先级故障转移和标签匹配。优先级故障转移就是给同一个别名配置多个模型第一个是主后几个是备。请求先发到主模型如果连续失败超过阈值或者返回特定的限流错误码router 会自动把请求转给备用模型。这个机制对生产环境很重要。国产模型偶尔会有限流或者服务波动有了自动切换harness 那边完全感知不到用户不会看到报错。标签匹配则适合“按任务分流”。比如代码生成类任务用工作模型长文总结类任务用长上下文模型。harness 在请求里会带上一些元信息router 可以根据上下文里的指令特征做关键词匹配命中规则的走专用模型没命中的走默认模型。注意关键词匹配不能太刚性否则会把正常对话误判过去建议只在非常明确的指令模式上启用。3.3 并发控制与限流国产模型 API 通常按密钥限流而 harness 经常是多任务并行跑的。如果不做并发控制几秒钟就能把限额打爆然后整批请求返回 429。router 内置一个简单的令牌桶按别名维度限制并发数超过的请求进入等待队列而不是直接拒绝。这个设计在单用户使用时几乎无感但工程团队多人共用同一套模型服务时区别非常大。我见过有人不做并发控制结果 API 自动熔断整个团队一上午都没法用。加了令牌桶之后即使单个消费者触发限流也只是等待几十毫秒不会影响全局。另外连接池的大小要根据模型服务的并发上限来调开太大容易被打回开太小首字延迟会偏高。4. 部署与接入可以直接抄作业的实操步骤4.1 快速部署与配置文件cn-llm-router 我用纯 Python 写的依赖没几个留着基于常见实践的默认配置、再配合一套 sklearn 风格的健康检查路径其实没必要整个部署就是“跑一个服务”。先准备一个 config.yaml把所有模型供应商信息放进去。server: bind: 127.0.0.1 port: 8080 tokens: - sk-router-local providers: - name: deepseek base_url: https://你的模型服务域名/v1 api_key_env: DEEPSEEK_API_KEY type: openai_compatible routes: - alias: workhorse provider: deepseek actual_model: deepseek-chat fallback: - provider: openai_compatible actual_model: fallback-model context_window: 65536 max_output_tokens: 8192启动方式很简单export DEEPSEEK_API_KEY你的密钥 python main.py --config config.yaml如果不想直接跑进程也可以用 Docker 包一层。我这里默认你是本地单机部署如果你有多台机器要给多个服务共用可以考虑把 bind 改成 0.0.0.0 并挂一层访问控制但注意不要暴露到公网。4.2 接入 harness 侧配置最关键的接入步骤是设置 harness 的 base URL。如果你用的框架是走 Anthropic 协议那只需要两个环境变量一个指着 router 地址另一个换成 router 的访问令牌。export ANTHROPIC_BASE_URLhttp://127.0.0.1:8080 export ANTHROPIC_AUTH_TOKENsk-router-local这里容易被忽略的是token 不是你的模型 API 密钥而是 router 自己认证用的令牌。路由层和上游模型服务的密钥分离之后你的密钥就不会暴露给每一个使用框架的人权限边界更清楚。如果框架支持自定义模型名称把模型参数设成你在 router 里定义的别名比如 workhorse这样每次换模型都只动 yaml 文件。4.3 验证连通性的几个关键信号接入完成后不要马上下复杂任务先跑一个最简单的问题确认链路是通的。然后跑一个需要工具调用的任务观察两个关键信号模型是否正确输出工具调用以及 harness 能否正常接收结果并执行下一步。我建议用 curl 直接发一个测试请求给 router验证响应格式是否符合预期。如果返回的字段结构不对先别怀疑模型多半是 router 的事件转换没接对。也可以通过给 router 加 debug 日志来看每一次转发的请求和响应确认模型名、参数和事件序列都没有异常。跑通这两项再上多轮复杂任务基本上就稳了。5. 常见问题与排查记录5.1 工具调用一直“卡住”或超时症状是模型已经说了要调用工具但 harness 迟迟不执行要么一直转圈要么直接报工具执行失败。这个问题的根因绝大多数是响应里的工具调用格式不合法。我加了个专门的状态机处理工具调用片段后这类故障明显减少。排查时先抓 router 最后的原始响应看 tool_calls 里的参数是不是完整 JSON。如果参数被截断检查是不是流式缓冲区太小如果字段结构不对检查是不是 input 对象和 arguments 字符串的转换漏了。另一个容易忽略的坑是有些国产模型在工具调用时还会同时输出文本OpenAI 事件流里这两个内容是分开发送的router 必须把它们合并到同一个 content_block 才能被 harness 正确识别。5.2 流式输出断在第一个块症状是模型开始回复了但只看到一两个字就停住然后整个连接被断开。这个大概率是流式事件序列不对。Anthropic 协议严格要求先发 message_start再发 content_block_start如果 router 直接把 OpenAI 的第一个 chunk 原样转发harness 会因为缺少开始标记而拒绝解析。解决办法是在转发前构造一个符合协议结构的开始事件序列然后再把后续增量填进去。再看一下超时配置有些国产模型首 token 较慢harness 侧有个空闲超时如果超时太短即使是正常的模型思考过程也会被掐断。建议把空闲超时调大到 120 秒以上尤其是启用深度思考或长文生成功能的时候。5.3 上下文超长或配额超限症状是一开始正常跑几个复杂任务之后突然报上下文超长或者模型服务商返回余额不足、配额超限之类的问题。上下文超长通常是 router 没按 context_window 做截断导致请求体积超过模型上限。我后来给 router 加了一道上游请求体检查根据配置的窗口大小在发给模型前就拒绝明显超限的请求而不是等到模型服务商来报错。配额超限则多半是并发控制没生效。建议把所有模型别名都配上独立的令牌桶并设置合理的每分钟阈值。另外如果用了 fallback 配置要确认 fallback 模型的配额足够否则主模型限流时会雪崩到备用模型上把备用也打挂。5.4 回复变慢首字延迟很高如果同一个模型直接用官方 API 很快经过 router 之后明显变慢问题多半出在“串行化”上。一种可能是请求里包含了重型工具定义每次都要组装和解析另一种可能是连接池被占满请求在排队。router 日志里可以看到每个请求的处理耗时如果耗集中在“等待上游连接”上就该调大连接池或者缩短空闲连接回收时间。还有一种容易被忽视的情况是路由决策器里设置了过重的关键词匹配导致每个请求都要做大量的文本扫描。生产环境里建议不要在主路径上做复杂策略把路由决策做成“先查表再默认”的快速通道只有查不到的时候才进入慢路径。我自己实际跑下来的体会是cn-llm-router 这东西维护成本不高但收益很实在。以前每换一个模型服务商就要翻 harness 源码找适配点现在所有入口都收敛到一个配置文件里加模型、换模型、调限流全部在 yaml 里完成。如果你也在用 harness 想控制模型成本或者想试试 DeepSeek 这类国内模型我建议动手之前先想清楚上面这五个问题能少走很多弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑