
1. 为什么我要给海外 harness 配一个国内模型路由先说清楚我遇到的实际问题。我日常主力用 Claude Code 和 Codex 这两套 agent harness 来跑代码任务它们本身是很好的“驾驶舱”——负责上下文管理、工具调用、文件读写、多轮规划。但它们的默认后端都指向海外模型服务在国内网络环境下延迟高、偶发超时、按量计费还贵。一个中等规模的仓库重构任务跑下来 token 消耗相当可观账单看着肉疼。于是很自然的想法是harness 我留着模型换成国内高性价比的。国内几家大模型在代码能力上这两年进步很快价格往往只有海外旗舰模型的几分之一甚至更低而且网络链路短、响应稳定。但真动手才发现事情没那么简单——Claude Code 和 Codex 各自有自己的一套配置体系、协议格式、鉴权方式你想让它们“说国内模型的话”中间必须有一层翻译和路由。这就是我写cn-llm-router的动机。它本质上是一个本地运行的轻量代理层坐在 harness 和国内模型 API 之间负责三件事协议转换把 Anthropic/OpenAI 风格的请求翻译成国内模型能懂的格式、模型路由按任务类型或成本策略分发到不同模型、以及统一鉴权与日志。写完跑通之后我的 agent 工作流成本降了大概七成响应速度也明显更跟手。这篇文章我会把整个思路、踩过的坑、配置细节和实测数据都摊开讲。适合两类人看一是已经在用 Claude Code / Codex 但被成本和网络困扰的开发者二是想理解 agent harness 与模型后端解耦这件事到底怎么做的人。哪怕你只是想搞清楚“harness 能不能不登录官方账号、换别的模型”这篇也能给你答案。2. 先搞懂 harness 和模型之间到底在传什么2.1 harness 不是模型它是“驾驶舱”很多人一开始会混淆概念以为 Claude Code 或 Codex 本身就是模型。其实不是。它们属于agent harness智能体驾驭层核心职责是维护对话历史、决定什么时候调用工具读文件、跑命令、搜索、把工具结果拼回上下文、管理多轮规划。真正生成文字和代码的是背后的 LLM。理解这一点非常关键因为它决定了“换模型”这件事在技术上是否可行。既然 harness 只是把请求发给一个 HTTP 端点那只要我能提供一个行为兼容的端点理论上就能把后端换成任何模型。这也是cn-llm-router存在的理论基础。但“行为兼容”四个字说起来轻巧做起来全是细节。不同 harness 对端点的期望不一样Claude Code 走的是 Anthropic Messages API 风格Codex 更接近 OpenAI 的 Responses/Chat Completions 风格。字段名、消息结构、工具调用的表达方式都有差异。2.2 请求里真正重要的几个字段我抓包分析过 harness 发出的请求抛开那些边角字段真正影响模型行为的主要是这几类字段类别作用换模型时的坑messages / input对话历史与当前输入角色命名不同system/user/assistant vs 其他tools / functions可调用的工具定义JSON Schema 表达差异国内模型支持度不一tool_choice强制或自动调用工具部分模型不支持强制调用max_tokens输出长度上限各家上限不同超了直接报错stream是否流式返回流式格式SSE 事件类型差异大system系统提示词有的模型对超长 system 处理不好我最初图省事直接把 Claude Code 的请求原样转发给国内模型的 OpenAI 兼容端点结果工具调用全乱套——模型把工具定义当普通文本理解返回一堆自然语言而不是结构化的 tool_call。这就是协议不匹配的典型症状。2.3 为什么不能简单改个 base_url 了事网上有些教程说“把 base_url 改成国内某家的地址就行”我实测下来简单场景纯聊天确实能跑但一旦涉及工具调用和多轮 agent 循环就会出问题。原因有三第一工具调用协议不一致。Anthropic 的工具调用是tool_use/tool_result内容块OpenAI 是tool_calls数组国内模型大多兼容 OpenAI 风格但对嵌套结构支持参差。harness 期望收到它认识的那一种收到别的就解析失败。第二流式事件格式不同。Claude Code 依赖特定的 SSE 事件序列来实时渲染如果代理层不做转换前端会卡住或者显示错乱。第三鉴权和 header 要求不同。有的 harness 会带自己的 header国内端点可能不认需要代理层清洗和重写。所以cn-llm-router的核心价值就是把这层“翻译”做扎实让 harness 完全感知不到后端换了人。3. cn-llm-router 的架构设计与关键取舍3.1 整体数据流整个链路是这样的harness 发出请求 → 本地 router 监听某个端口 → router 识别请求来源是 Claude Code 还是 Codex→ 按对应协议解析 → 转换成目标模型的格式 → 转发到国内模型 API → 收到响应 → 反向转换回 harness 期望的格式 → 返回。用文字描述流程容易晕我直接说关键设计点。router 内部维护了两套“适配器”一套 Anthropic 风格适配器一套 OpenAI 风格适配器。请求进来先判断走哪套出口再根据目标模型选对应的上游适配器。中间是统一的内部消息表示IRIntermediate Representation所有转换都经过 IR 中转这样新增一个模型只需要写一个上游适配器不用改下游。3.2 为什么选本地代理而不是改 harness 源码有人会问为什么不直接改 Claude Code 的源码让它支持国内模型我的考虑是harness 更新频繁改源码意味着每次升级都要重新打补丁维护成本极高。而本地代理是外挂式的harness 怎么升级都不影响只要它的请求格式不大改router 就不用动。这个解耦带来的长期收益远大于初期多写的那点代码。另外本地代理还有个好处可以统一做日志、限流、缓存和成本统计。我现在的 router 会记录每次请求的模型、token 数、耗时和估算成本月底一看就知道钱花哪了。3.3 模型路由策略不是所有任务都值得用贵模型这是我觉得最有价值的设计。router 支持按规则分发请求简单补全、格式化、重命名路由到最便宜的小模型速度快、成本低。常规代码生成、解释路由到中档模型性价比最优。复杂重构、多文件推理、架构设计路由到能力最强的模型。规则可以基于请求里的关键词、上下文长度、工具调用数量来判断。比如上下文超过某个阈值、或者带了大量工具定义就判定为复杂任务升级到强模型。实测下来这种分级策略能在几乎不损失体验的前提下把整体成本再压一截。提示分级阈值不要拍脑袋定建议先跑一周全量日志统计各类任务的分布再据此设阈值。我一开始把阈值设太低导致简单任务也走了强模型白白多花钱。3.4 配置文件的组织方式router 的配置我放在一个 YAML 文件里结构大致分三块上游模型定义每家一个条目含 base_url、api_key、模型名映射、路由规则匹配条件 目标模型、服务参数监听端口、日志级别、超时。这样改配置不用碰代码重启即生效。upstreams: - name: cheap-model base_url: https://api.example-cn-a.com/v1 api_key: ${ENV_A_KEY} protocol: openai models: - id: small-fast max_tokens: 8192 - name: strong-model base_url: https://api.example-cn-b.com/v1 api_key: ${ENV_B_KEY} protocol: openai models: - id: large-capable max_tokens: 32768 routing: - match: { context_tokens_gt: 20000 } target: strong-model - match: { tool_count_gt: 5 } target: strong-model - default: cheap-model server: port: 8787 log_level: info timeout_seconds: 120api_key 我用环境变量注入绝不写死在文件里。这一点后面讲安全时会再强调。4. 从零跑通安装、配置与第一次联调4.1 环境准备与依赖router 我用 Python 写的依赖很轻一个 HTTP 框架FastAPI 或 Flask 都行、一个 HTTP 客户端httpx、一个 YAML 解析库。Python 3.10 以上即可。之所以不用 Node是因为我自己的工具链偏 Python而且异步 HTTP 处理起来顺手。安装步骤很直接python -m venv venv source venv/bin/activate pip install fastapi uvicorn httpx pyyaml如果你在 Linux 上跑比如 Ubuntu记得确认端口没被占用防火墙放行本地回环即可因为 harness 和 router 都在本机不需要对外暴露。4.2 让 Claude Code 指向本地 routerClaude Code 支持通过环境变量指定 API 端点。核心是设置ANTHROPIC_BASE_URL指向http://127.0.0.1:8787同时把 API key 设成 router 约定的占位值router 会用真实 key 去请求上游。这样 Claude Code 以为自己在跟官方端点说话实际上请求全被 router 接管了。这里有个细节Claude Code 启动时会做一次端点探测如果 router 没起来或者返回格式不对它会直接报错退出。所以顺序一定是先起 router再起 harness。4.3 让 Codex 走同一套路由Codex 的配置方式不同它读的是自己的配置文件通常在用户目录下的配置目录里。需要把模型提供方指向本地端点并指定模型名。Codex 对 OpenAI 风格兼容度更高所以 router 的 OpenAI 适配器直接就能接。我踩过的一个坑Codex 有个“组织设置加载”的步骤如果端点返回的响应里缺少某些字段它会报“无法加载组织设置”。解决办法是在 router 的响应里补上这些字段的默认值让它以为一切正常。这个坑排查了我大半天最后靠对比官方响应才找到缺哪个字段。4.4 第一次联调的验证方法不要一上来就跑复杂任务。我的验证顺序是先用 curl 直接打 router 的健康检查端点确认服务活着。发一个最简单的纯文本请求确认能拿到回复。发一个带单个工具定义的请求确认工具调用能正确往返。最后才在 harness 里跑真实任务。每一步都确认通过再进下一步这样出问题时能快速定位是哪一层的问题。我见过太多人跳过前三步直接跑 agent结果报错信息一堆根本不知道是协议问题还是模型问题。5. 那些让我熬夜的坑协议转换的实战细节5.1 工具调用的双向翻译这是整个项目最费劲的部分。Anthropic 风格里模型要调用工具时返回的内容块类型是tool_use里面带id、name、input。而 OpenAI 风格是tool_calls数组每个元素有id、function.name、function.arguments注意 arguments 是 JSON 字符串不是对象。翻译时最容易错的是arguments的序列化。Anthropic 的input是对象OpenAI 要字符串转的时候要json.dumps反过来收到 OpenAI 的字符串要json.loads。我一开始忘了这层导致工具参数传过去变成一坨字符串模型解析不了。还有一个隐蔽的坑工具调用的id在往返过程中必须保持一致。harness 用这个 id 把工具结果和调用配对如果 router 在转换时生成了新 id配对就断了agent 循环直接卡死。5.2 流式响应的 SSE 事件对齐Claude Code 对流式渲染依赖很强。它期望收到一系列事件消息开始、内容块开始、内容增量、内容块结束、消息结束。国内模型返回的 SSE 事件类型和顺序往往不一样router 必须重新编排。我的做法是在 router 内部先把上游的流式响应完整解析成事件序列再按 Anthropic 的规范重新发出。这样虽然多了一层缓冲但保证了 harness 端的稳定性。代价是首字节延迟略微增加实测大概多几十毫秒可以接受。注意流式转换时一定要处理好“半包”问题。TCP 传输中一个 SSE 事件可能被拆成多个数据块到达解析时要按\n\n分隔并缓存不完整的部分否则会丢事件或解析出错。5.3 系统提示词与上下文长度国内模型对超长 system 提示词的处理能力参差不齐。Claude Code 会塞一个相当长的系统提示包含工具说明、行为规范等。我遇到过某家模型在 system 过长时直接忽略后半部分导致工具说明丢失、行为异常。应对办法有两个一是选对模型优先用上下文窗口大、对长 system 支持好的二是在 router 里做提示词压缩把冗余部分精简后再转发。我目前用的是第一种因为压缩提示词有改变模型行为的风险需要谨慎。5.4 错误码与重试策略上游模型偶尔会返回限流或临时错误。router 需要把这些错误翻译成 harness 能理解的格式并决定是否重试。我的策略是对限流429和服务器错误5xx做指数退避重试最多三次对参数错误4xx直接透传因为重试也没用。这里有个细节重试时要注意幂等性。如果请求已经部分执行比如流式已经发了一半就不能简单重试否则 harness 会收到重复内容。我的做法是只在“尚未向 harness 发送任何数据”时才重试。6. 成本、延迟与稳定性的实测对比6.1 成本账到底省了多少我拿一个真实的仓库重构任务做了对比。任务规模约 40 个文件涉及跨模块重构agent 循环约 60 轮。用海外旗舰模型跑完按当时价格估算成本记为基准 100%。换成国内模型 分级路由后同样的任务完成质量基本持平我人工 review 了产出成本降到约 28%。省钱的来源有两块一是国内模型单价本身低二是分级路由让大量简单轮次走了便宜模型。如果不用分级、全部走强模型成本大概是 45% 左右。所以分级策略贡献了将近一半的节省。方案相对成本任务完成质量平均响应延迟海外旗舰模型100%基准较高国内强模型不分级约 45%接近基准明显降低国内模型 分级路由约 28%接近基准最低6.2 延迟网络链路的影响延迟这块国内模型的优势非常直观。海外端点在国内访问单次请求的往返经常在几百毫秒到一秒以上遇到网络波动还会更高。国内端点通常在几十到两百毫秒。对于 agent 这种要跑几十上百轮的任务累积下来差距巨大——原本要跑十几分钟的任务现在几分钟就完事。6.3 稳定性怎么保证不中断稳定性是我最看重的。router 做了几件事来保证上游多路冗余同一档位配两家一家挂了自动切另一家、超时控制单请求超时后快速失败并重试、以及健康检查定期探测上游可用性。实测下来连续跑一周的日常任务没有出现因为 router 导致的中断。偶尔上游某家抖动自动切换后 harness 端完全无感。7. 安全与合规本地代理必须守住的底线7.1 密钥管理router 会持有上游模型的 API key这是最敏感的东西。我的原则是绝不硬编码全部走环境变量或独立的密钥文件且密钥文件权限设为仅本人可读。日志里绝对不能打印完整 key最多打印前几位用于排查。chmod 600 ~/.config/cn-llm-router/secrets.env7.2 只监听本地回环router 默认只绑定127.0.0.1不对外网开放。因为它是给本机 harness 用的没有任何理由暴露到局域网或公网。如果确实需要多机共享也应该走内网并加鉴权而不是直接开放端口。7.3 日志脱敏日志对排查问题很有用但代码内容、提示词里可能含敏感信息。我的做法是默认只记录元数据模型、token 数、耗时、状态码不记录请求和响应的正文需要调试时临时开启正文记录用完立即关闭并清理日志文件。提示如果你在团队环境用务必和团队确认日志留存策略避免把业务代码或数据意外落盘。8. 几个高频问题的排查思路8.1 harness 报端点错误怎么办先确认 router 是否在跑、端口是否对。然后看 router 日志里有没有收到请求。如果 router 收到了但 harness 报错多半是响应格式不对——用 curl 直接打 router对比返回结构和 harness 期望的结构逐字段排查。我遇到过的“组织设置无法加载”就是这么定位的。8.2 工具调用不生效九成是协议转换问题。检查 router 日志里工具定义有没有正确转发、模型返回的 tool_call 有没有被正确翻译回 harness 期望的格式。重点看arguments的序列化和id的一致性。8.3 流式输出卡住或乱码检查 SSE 事件的分包处理。用 curl 加-N参数直接看 router 的流式输出确认事件序列完整、格式正确。如果上游返回的事件类型 harness 不认识就需要在 router 里做映射。8.4 模型行为异常、不遵守指令先排除是不是 system 提示词被截断或忽略。换一个上下文窗口更大的模型试试。如果换了就好说明是模型能力问题如果还不行检查 router 有没有在转换中丢失字段。9. 我在这套方案里总结出的几条经验第一先跑通最小闭环再优化。我一开始就想把分级路由、多路冗余、日志统计全做上结果卡在协议转换上很久。后来退回去先让一个模型、一个 harness 跑通再逐步加功能效率高得多。第二日志是你的救命稻草。协议转换这种活没有详细日志根本没法排查。但日志要分级平时只记元数据调试时才开正文兼顾排查和安全。第三不要迷信“改个 base_url 就行”。简单场景能跑不代表 agent 场景能跑。工具调用、流式、错误处理这三块任何一块没处理好agent 都会崩。第四分级路由的阈值要靠数据定。别拍脑袋先全量记录一周看任务分布再设阈值。我调了两轮才找到比较合适的点。第五密钥和日志的安全底线不能松。本地代理持有密钥、经手代码内容一旦泄露后果严重。环境变量、文件权限、日志脱敏这三样一个都不能少。这套方案我用了几个月日常的 agent 工作流已经稳定跑在上面。后面我打算再补一个能力根据任务的历史成功率动态调整路由让 router 自己学习哪些任务该走哪个模型。不过那是下一步的事了眼下这套已经够用。如果你也在被 harness 的成本和网络困扰不妨按这个思路搭一套从最小闭环开始一步步来。