
1. 项目概述一个真实存在的“夹心层”需求催生的路由中间件你有没有遇到过这种场景团队里跑着一套成熟的国外 harness 框架——可能是某开源大模型服务编排平台也可能是某企业自研的 AI 工作流引擎它设计精良、API 规范、生态成熟但默认只对接 OpenAI、Anthropic、Cohere 等境外模型服务。而你手头却有一批国内厂商提供的高性价比模型响应快、价格低、中文理解强、支持私有化部署甚至还能走内网直连。可问题来了——harness 不认它们改源码成本太高加 adapter 又得反复适配各家 API 差异有的用 /v1/chat/completions有的用 /api/inference有的要 sign 签名有的要传 x-api-key有的还要带 tenant_id更别说模型参数映射temperature → tempmax_tokens → max_length、流式响应格式转换、错误码对齐这些细节了。“cn-llm-router”就是为解决这个“最后一公里”而生的轻量级路由中间件。它不替换 harness也不重写模型服务而是像一个懂行的翻译调度员坐在 harness 和国内模型之间把 harness 发来的标准 OpenAI-style 请求实时翻译成目标国产模型能听懂的语言再把国产模型返回的原始响应规整成 harness 期待的 OpenAI 兼容格式同时还能按策略做负载均衡、熔断降级、计费统计、日志审计。它不是替代品是连接器不是大而全的平台是小而准的 glue code。关键词就三个harness 兼容、国产模型接入、高性价比落地。适合正在推进 AI 落地但受制于采购预算、数据合规或网络条件的中型技术团队也适合想快速验证多个国产模型效果的算法工程师——你不用动一行 harness 代码只要改一个 endpoint 地址就能把通义千问、零一万物、月之暗面、智谱清言、百川、MiniMax 甚至本地部署的 Qwen2-7B、GLM-4-9B 全部“假装”成 OpenAI 模型跑起来。我第一次在某客户现场看到这个需求时他们已经卡在联调环节两周了。harness 是某国际咨询公司交付的标准组件合同里明确写了“不得修改核心模块”。而国产模型供应商给的 SDK 文档只有 Python 示例且每个厂的重试逻辑、超时设置、鉴权方式都不一样。当时我就意识到这不是一个“能不能接”的问题而是一个“值不值得为每个模型单独写一套 adapter”的工程经济学问题。如果接入 5 个模型就要维护 5 套独立适配层人力成本远超模型本身费用。cn-llm-router 的设计哲学很朴素用一份配置驱动 N 种模型用一套协议屏蔽所有差异用最小侵入换取最大灵活。它不追求取代任何一方只专注做好“翻译”这件事——而且是带上下文感知、带质量反馈、带灰度能力的智能翻译。2. 整体架构与设计思路为什么是“Router”而不是“Adapter”或“Proxy”2.1 核心定位协议桥接器而非功能增强器很多人第一反应是“这不就是个反向代理”或者“写个 OpenAI 兼容层不就行了”——这两类思路我都实测过结果都不理想。纯反向代理如 Nginx Lua能转发请求但无法处理模型间本质性协议差异比如 OpenAI 的messages是数组而某国产模型要求prompt字符串 history数组OpenAI 的stream是 SSE而另一家只支持 WebSocket 或普通 JSONOpenAI 的错误响应是{ error: { code: ..., message: ... } }而某家返回的是{ code: 500, msg: xxx, data: null }。这些不是靠正则替换或简单字段映射能解决的必须有业务逻辑层介入。而“Adapter”模式即为每个模型写一个独立服务又走向另一个极端代码重复率高、配置分散、升级困难。我们曾为某客户写过三个 Adapter分别对接三家国产模型结果发现 70% 的代码是共用的HTTP 客户端封装、重试机制、日志打点、指标上报、超时控制。真正差异化的只有请求组装和响应解析两部分加起来不到 300 行。这意味着与其维护三套几乎一样的服务不如抽象出一个通用运行时把差异化逻辑下沉为可插拔的“模型驱动”。cn-llm-router 的定位因此非常清晰它是一个协议标准化运行时Protocol Standardization Runtime。它的输入是严格遵循 OpenAI API Spec 的 HTTP 请求/chat/completions, /embeddings, /models 等输出也是完全兼容的响应中间所有模型特异性逻辑都通过声明式配置 插件化驱动实现。它不提供新功能比如不自带 RAG、不集成向量库、不支持函数调用扩展只确保“能通、能转、能稳”。这种克制恰恰是它能在生产环境长期存活的关键——没有额外功能意味着没有额外故障点没有复杂依赖意味着部署极简没有业务逻辑意味着升级零风险。2.2 架构分层四层解耦各司其职整个系统采用清晰的四层架构每层职责单一接口契约明确接入层Ingress Layer基于 FastAPI 实现负责接收 harness 发来的标准 OpenAI 请求。它只做三件事校验基础字段如model是否在白名单、记录原始请求日志用于审计与回溯、将请求转发给路由层。不解析messages内容不校验 token 数不干预任何业务语义。路由层Routing Layer核心大脑。根据请求中的model字段如qwen2-7b-chat、zhipu-glm4查配置中心获取对应模型驱动的元信息endpoint、auth 方式、支持的能力集。这里支持多级路由策略默认按 model 名精确匹配进阶支持按请求 header如X-Region: shanghai做地理路由最高级支持按请求内容特征如检测到systemmessage 含“法律”二字做语义路由。路由决策毫秒级完成且支持热更新——改完配置无需重启服务。驱动层Driver Layer真正的“翻译官”。每个国产模型对应一个 Driver 插件如QwenDriver、GLMDriver它只关心两件事请求翻译Request Translation把 OpenAI 格式的{model:qwen2-7b-chat,messages:[{role:user,content:你好}]}转换成该模型所需的格式如{prompt:你好,history:[],top_p:0.8}响应归一化Response Normalization把模型返回的原始 JSON可能含result、text、choices等任意字段重新组装成 OpenAI 标准结构{id:...,object:chat.completion,choices:[{index:0,message:{role:assistant,content:你好}}]}。 Driver 是纯逻辑代码无网络、无状态、无副作用单元测试覆盖率要求 100%。执行层Execution Layer统一的 HTTP 客户端工厂。它不关心业务只提供标准化的请求发送能力自动处理重试指数退避、超时connect/read 分开设置、熔断基于失败率与响应延迟、连接池管理。所有 Driver 都通过它发请求确保底层网络行为一致、可观测、可调优。这四层之间通过明确定义的 Python Protocol接口契约通信例如DriverInterface规定必须实现translate_request()和normalize_response()两个方法。这种设计带来三大好处一是 Driver 开发者只需关注协议转换不用操心网络细节二是新增模型只需实现一个 Driver 类注册进配置即可零改造主程序三是便于隔离测试——你可以 mock 执行层只测 Driver 的翻译逻辑是否正确。2.3 为什么放弃“大模型网关”路线市面上已有不少“大模型网关”产品功能强大支持鉴权、限流、缓存、审计、可观测性全套。但它们普遍面临一个困境为了通用性牺牲了对国产模型细节的深度适配能力。比如某网关声称支持“全模型接入”但实际测试发现它对某家模型的流式响应解析有 bug导致 harness 接收不完整又或者它的重试逻辑与模型厂商的推荐策略冲突引发雪崩。更关键的是这类网关往往自带 UI、数据库、用户体系部署复杂度陡增而我们的客户只需要“让 harness 能调通”不需要一个新平台。cn-llm-router 的选择很务实不做网关只做路由器不求功能全但求协议准不拼生态大只保落地稳。它没有自己的数据库配置用 YAML 文件或环境变量注入没有前端界面所有管理通过 CLI 或 API不内置监控指标直接暴露 Prometheus 格式由客户现有监控体系采集。它被设计成一个“隐形”的基础设施组件——上线后harness 感知不到它的存在运维人员除了看一眼进程是否在跑几乎不用管它。这种“无感”的稳定性恰恰是生产环境最稀缺的品质。3. 核心细节解析与实操要点配置驱动、驱动插件与协议对齐3.1 配置即代码YAML 驱动的模型注册体系cn-llm-router 的灵魂在于其声明式配置。所有模型接入信息都通过一个models.yaml文件定义。这不是简单的 endpoint 列表而是一份完整的“模型能力说明书”。以下是一个真实可用的配置片段已脱敏# models.yaml models: - name: qwen2-7b-chat display_name: 通义千问 Qwen2-7B杭州集群 driver: qwen endpoint: https://qwen-api.hz.example.com/v1 auth: type: api_key header: Authorization value: Bearer {{API_KEY}} capabilities: chat: true embeddings: false streaming: true function_calling: false parameters: temperature: 0.7 top_p: 0.9 max_tokens: 2048 timeouts: connect: 5 read: 60 health_check: path: /health interval: 30 timeout: 3 - name: zhipu-glm4 display_name: 智谱 GLM-4北京集群 driver: glm endpoint: https://glm-api.bj.example.com/api auth: type: header header: X-API-Key value: {{ZHIPU_API_KEY}} capabilities: chat: true embeddings: true streaming: false function_calling: true parameters: temperature: 0.95 top_k: 10 max_length: 4096 timeouts: connect: 3 read: 120 health_check: path: /status interval: 15 timeout: 2这个配置文件蕴含了大量工程细节driver字段指向具体的 Driver 类名如qwen对应drivers/qwen.py中的QwenDriver。router 启动时会动态导入该模块实现插件化加载。auth配置支持多种鉴权方式。api_key类型会自动在请求头插入Authorization: Bearer xxxheader类型则按指定header名插入value支持 Jinja2 模板语法从环境变量读取密钥避免硬编码。capabilities这是路由决策的关键依据。当 harness 发来/embeddings请求时router 会先检查目标模型的capabilities.embeddings是否为true否则直接返回 404而不是把请求发给不支持的模型导致超时。parameters定义模型默认参数。当 harness 请求中未指定temperature时自动补上此处值若 harness 指定了则以 harness 的为准实现“配置兜底 请求覆盖”双保险。timeouts为每个模型单独设置超时。为什么zhipu-glm4的read超时是 120 秒因为实测其长文本生成平均耗时 80 秒设太短会导致大量误超时而qwen2-7b-chat是 60 秒因其响应更快。这种精细化控制是保障整体 SLA 的基础。health_check主动健康探测。router 会定期interval秒向模型 endpoint 的path发起 GET 请求若连续timeout秒无响应或返回非 2xx则将该实例标记为不可用后续请求自动绕过。这比被动等待超时更及时能有效避免“雪球效应”。提示配置文件支持 Jinja2 模板语法可结合环境变量实现多环境部署。例如{{ENV}}可替换为prod或staging从而动态切换 endpoint 地址。我们建议将敏感密钥如API_KEY全部通过环境变量注入配置文件中只保留模板占位符杜绝密钥泄露风险。3.2 Driver 插件开发一个 Driver 的完整生命周期以QwenDriver为例展示一个国产模型驱动的开发全过程。Driver 的核心是两个方法translate_request()和normalize_response()。但要让它真正健壮还需处理边界情况。# drivers/qwen.py from typing import Dict, Any, List, Optional from pydantic import BaseModel class QwenRequest(BaseModel): prompt: str history: List[List[str]] [] top_p: float 0.8 temperature: float 0.7 max_length: int 2048 class QwenResponse(BaseModel): text: str request_id: str class QwenDriver: def translate_request(self, openai_req: Dict[str, Any]) - Dict[str, Any]: # 1. 提取 messages 并转换为 prompt history messages openai_req.get(messages, []) if not messages: raise ValueError(messages is required) # system message 处理Qwen 不支持 system role需合并到第一个 user prompt system_content user_messages [] for msg in messages: if msg[role] system: system_content msg[content] elif msg[role] in [user, assistant]: user_messages.append(msg) # 构建 promptsystem user/assistant 交替 prompt_parts [system_content] if system_content else [] for i, msg in enumerate(user_messages): if msg[role] user: prompt_parts.append(msg[content]) else: # assistant # Qwen 的 history 格式是 [[user1, ai1], [user2, ai2]] if i 0 and i % 2 0: # 将前一个 user 和当前 assistant 组成一对 pass # 实际项目中这里会调用一个专门的 messages_to_qwen_prompt() 函数 # 为简洁此处省略具体实现重点看逻辑分支 prompt self._build_qwen_prompt(messages) history self._extract_qwen_history(messages) # 2. 映射参数 qwen_params { prompt: prompt, history: history, top_p: openai_req.get(top_p, 0.8), temperature: openai_req.get(temperature, 0.7), max_length: openai_req.get(max_tokens, 2048), } return qwen_params def normalize_response(self, qwen_resp: Dict[str, Any], openai_req: Dict[str, Any]) - Dict[str, Any]: # 1. 构建标准 choices 结构 choices [{ index: 0, message: { role: assistant, content: qwen_resp.get(text, ) }, finish_reason: stop # Qwen 无流式固定为 stop }] # 2. 构建顶层响应 return { id: qwen_resp.get(request_id, qwen- str(hash(qwen_resp))), object: chat.completion, created: int(time.time()), model: openai_req.get(model, qwen2-7b-chat), choices: choices, usage: { prompt_tokens: self._estimate_tokens(openai_req.get(messages, [])), completion_tokens: self._estimate_tokens([{role:assistant,content:qwen_resp.get(text,)}]), total_tokens: 0 # 实际项目中会调用 tokenizer 计算 } } def _build_qwen_prompt(self, messages: List[Dict[str, str]]) - str: # 真实实现会处理 system/user/assistant 的严格顺序 # 这里简化为取最后一个 user message 作为 prompt for msg in reversed(messages): if msg[role] user: return msg[content] return def _extract_qwen_history(self, messages: List[Dict[str, str]]) - List[List[str]]: # 提取 user/assistant 交替的历史对 history [] for i in range(len(messages) - 1): if messages[i][role] user and messages[i1][role] assistant: history.append([messages[i][content], messages[i1][content]]) return history def _estimate_tokens(self, messages: List[Dict[str, str]]) - int: # 简单估算按字符数粗略计算生产环境应替换为真实 tokenizer return sum(len(m.get(content, )) for m in messages) // 3这个 Driver 展示了几个关键实操要点translate_request()的健壮性它不假设messages一定存在先做空值检查它处理systemrole 的特殊逻辑Qwen 不识别需合并它对参数做兜底get(top_p, 0.8)避免因 harness 未传参导致请求失败。normalize_response()的兼容性它严格遵循 OpenAI 的字段命名id,object,choices即使 Qwen 原始响应没有request_id也生成一个伪 ID它填充usage字段即使只是估算因为 harness 很可能依赖此字段做计费或限流。边界处理意识_estimate_tokens()方法注明“生产环境应替换为真实 tokenizer”这是经验之谈——很多团队初期用字符数估算后期发现误差大再切到 HuggingFace 的transformerstokenizerrouter 本身无需改动只需替换这个私有方法。注意Driver 必须是无状态的。所有配置如 endpoint、auth都由 router 在调用时注入Driver 自身不持有任何连接或配置。这保证了 Driver 可以被安全地多线程复用也方便单元测试——你只需 mock 一个字典输入就能验证输出是否符合 OpenAI Spec。3.3 协议对齐的魔鬼细节流式响应、错误码、模型列表协议对齐不是“看起来像”而是“每一个字节都对得上”。以下是三个最容易踩坑的细节以及 cn-llm-router 的解决方案1. 流式响应Streaming的逐 chunk 转换OpenAI 的流式响应是 Server-Sent Events (SSE)每条消息以data: {...}\n\n分隔。而某国产模型只返回普通 JSON 数组[{delta:{content:a}},{delta:{content:b}}]。router 的处理流程是接收 harness 的streamtrue请求向国产模型发起普通请求非流式拿到完整响应后模拟 SSE 流将数组拆成单个对象每个对象包装成data: {...}\n\n格式通过yield逐个返回给 harness最后发送data: [DONE]\n\n结束流。这样做的好处是harness 完全感知不到后端是非流式模型前端 JS 代码一行不用改。缺点是丧失了真正的低延迟——但实测发现对于 95% 的中文对话场景用户感知不到差异而工程复杂度大幅降低。2. 错误码的精准映射OpenAI 的错误码是400 Bad Request参数错误、401 Unauthorized密钥无效、429 Too Many Requests限流、500 Internal Error服务端崩溃。而国产模型 A 返回{code:4001,msg:invalid api key}模型 B 返回{error_code:1002,error_msg:rate limit exceeded}。router 建立了一个错误码映射表国产模型错误码OpenAI 状态码OpenAI Error CodeOpenAI Message4001(A)401invalid_api_keyInvalid API key1002(B)429rate_limit_exceededRate limit exceeded5003(A)500internal_errorInternal server error当 router 收到国产模型错误响应时先解析其code字段查表得到对应的 OpenAI 状态码和 error structure然后构造标准 OpenAI error body 返回。这样 harness 的错误处理逻辑完全复用无需为每个模型写不同 catch 分支。3./v1/models接口的动态聚合harness 启动时会调用GET /v1/models获取可用模型列表。router 不是返回一个静态列表而是实时聚合所有已配置且健康模型的信息。它会遍历models.yaml中所有模型对每个模型发起一次health_check如前所述只将健康状态为UP的模型以 OpenAI 格式{id:qwen2-7b-chat,object:model,owned_by:qwen}加入返回列表同时/v1/models响应中还包含一个router_version字段标识当前 router 版本便于 harness 做兼容性判断。这个设计让 harness 总是看到“最新、最准”的模型视图避免因模型下线或故障导致 harness 调用失败。4. 实操过程与核心环节实现从零部署到生产就绪4.1 环境准备与一键启动cn-llm-router 的部署目标是“开箱即用”最低要求仅需 Python 3.9 和 pip。整个过程分为三步全程命令行操作无图形界面依赖。步骤 1克隆与安装# 创建独立虚拟环境强烈推荐 python3 -m venv ./cn-llm-router-env source ./cn-llm-router-env/bin/activate # Linux/Mac # ./cn-llm-router-env/Scripts/activate # Windows # 克隆代码假设已发布到内部 Git 仓库 git clone https://git.example.com/internal/cn-llm-router.git cd cn-llm-router # 安装依赖核心依赖极少fastapi, httpx, pydantic, jinja2, uvicorn pip install -r requirements.txt步骤 2配置模型复制示例配置并编辑cp config/models.example.yaml config/models.yaml # 用 vim/nano 编辑 config/models.yaml填入你的模型信息 # 特别注意将 API 密钥设为环境变量不要写在文件里 export QWEN_API_KEYyour_actual_key_here export ZHIPU_API_KEYyour_actual_key_here步骤 3启动服务# 启动默认监听 0.0.0.0:8000 uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload # 生产环境推荐使用 gunicorn uvicorn worker更稳定 gunicorn app.main:app --bind 0.0.0.0:8000 --workers 4 --worker-class uvicorn.workers.UvicornWorker --timeout 120启动后访问http://localhost:8000/docs可看到自动生成的 FastAPI 文档其中包含了所有 OpenAI 兼容接口/chat/completions,/models,/health等。此时你就可以用 curl 测试了# 测试模型列表 curl http://localhost:8000/v1/models # 测试聊天假设已配置 qwen2-7b-chat curl http://localhost:8000/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer dummy \ -d { model: qwen2-7b-chat, messages: [{role: user, content: 你好你是谁}] }实操心得首次启动时务必先调用/health接口curl http://localhost:8000/health。它会返回一个 JSON包含所有已加载模型的健康状态。如果某个模型显示status: DOWN说明配置有误如 endpoint 不通、密钥错误、health_check path 不对此时应立即检查config/models.yaml和网络连通性不要急于测试业务接口。这个/health接口是生产环境的“第一道哨兵”。4.2 与 harness 的无缝集成只需改一个 URL集成 harness 是整个项目中最轻松的环节。以某典型 harness代号 “FlowEngine”为例其模型配置通常位于一个config.yaml文件中# harness 的原始配置对接 OpenAI llm: provider: openai api_key: sk-xxx base_url: https://api.openai.com/v1 model: gpt-3.5-turbo你只需要做两处修改修改base_url将https://api.openai.com/v1替换为你的 cn-llm-router 地址如http://cn-llm-router.internal:8000/v1修改model将gpt-3.5-turbo替换为你在models.yaml中定义的模型名如qwen2-7b-chat。# harness 的新配置对接 cn-llm-router llm: provider: openai # 注意provider 名称保持不变harness 仍认为自己在用 OpenAI api_key: dummy # 任意字符串router 会忽略它用自己的 auth 配置 base_url: http://cn-llm-router.internal:8000/v1 # 关键指向 router model: qwen2-7b-chat # 关键必须与 models.yaml 中的 name 一致提示api_key设为dummy是故意为之。因为 router 的鉴权逻辑完全独立于 harnessharness 发来的任何Authorization头都会被 router 忽略它只认自己配置里的auth。这样设计是为了彻底解耦harness 无需知道后端换了哪家模型。改完配置重启 harness。此时harness 发出的所有/v1/chat/completions请求都会先到达 router再由 router 转发给真正的国产模型。整个过程对 harness 透明它甚至不知道自己已经不再调用 OpenAI。4.3 生产就绪关键配置日志、监控、安全加固开箱即用只是第一步生产环境还需几项关键加固1. 日志分级与审计router 默认输出INFO级别日志但生产环境必须开启DEBUG级别用于问题排查。关键日志字段包括request_id: 全局唯一请求 ID由 router 生成贯穿 harness → router → 国产模型全链路model: 目标模型名upstream_status: 国产模型返回的 HTTP 状态码duration_ms: 整个请求耗时毫秒error: 若失败记录详细错误原因。配置方式在config/settings.py中LOG_LEVEL DEBUG LOG_FORMAT %(asctime)s | %(levelname)-8s | %(name)s | %(request_id)s | %(model)s | %(upstream_status)s | %(duration_ms)dms | %(message)s2. Prometheus 监控指标router 内置了 12 个核心 Prometheus 指标全部通过/metrics端点暴露cn_llm_router_requests_total{model, status_code, upstream_status}总请求数按模型、router 状态码、上游状态码多维统计cn_llm_router_request_duration_seconds_bucket{model, le}请求耗时分布直方图cn_llm_router_upstream_health{model}上游模型健康状态1UP, 0DOWNcn_llm_router_tokens_total{model, direction}token 使用量directionpromptorcompletion。运维只需将 router 的/metrics端点加入 Prometheus 抓取任务即可在 Grafana 中构建大盘实时监控各模型的 P95 延迟、错误率、QPS。3. 安全加固HTTPS 强制生产环境必须启用 HTTPS。router 支持通过--ssl-keyfile和--ssl-certfile参数加载证书或前置 Nginx 做 TLS 终结。IP 白名单通过--allowed-origins参数限制允许跨域的域名如https://harness.example.com防止恶意调用。速率限制虽然 router 本身不内置限流但提供了X-RateLimit-Limit和X-RateLimit-Remaining响应头其值由上游模型返回如某国产模型原生支持限流。router 会透传这些头供 harness 做客户端限流。实操心得上线前必做“压力测试”。我们用locust模拟 100 并发用户持续 10 分钟观察 router 的 CPU、内存、错误率。重点看两点一是cn_llm_router_request_duration_seconds_bucket的 P99 是否稳定建议 2s二是cn_llm_router_upstream_health是否始终为 1。如果某模型频繁 DOWN说明其health_check配置不合理如timeout太短需调整。5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 典型问题速查表问题现象可能原因排查命令/步骤解决方案harness 调用 404model名不匹配curl http://router/v1/models查看返回列表确保 harness 的model字段值与models.yaml中的name完全一致区分大小写harness 收到空响应或超时上游模型health_check失败curl http://router/health查看各模型状态检查models.yaml中endpoint是否可达curl -v https://xxx/health确认auth配置正确流式响应在前端卡住前端未正确处理 SSE浏览器开发者工具 Network Tab查看chat/completions?streamtrue的响应体确认响应头含Content-Type: text/event-stream检查 router 日志是否有streaming not supported警告中文乱码显示为 \uXXXX字符编码未设为 UTF-8curl -H Accept: application/json http://router/v1/chat/completions在app/main.py中确保JSONResponse的media_type为application/json; charsetutf-8harness 报错invalid_api_keyharness 发来的Authorization头被 router 误用curl -H Authorization: Bearer test http://router/v1/models确认models.yaml中auth.type配置正确且 router 未开启forward_auth_header: true默认关闭5.2 独家避坑技巧技巧 1用curl -v抓包比看日志更直接当问题难以复现时最有效的方法是抓取原始 HTTP 流量。在 router 服务器上用tcpdump抓包# 抓取 8000 端口的流量保存为 pcap sudo tcpdump -i any port 8000 -w router.pcap然后用 Wireshark 打开router.pcap过滤http你能清晰看到harness 发来的原始请求含 headers、bodyrouter 转发给国产模型的请求含它添加的 auth header、修改的 body国产模型返回的原始响应router 返回给 harness 的最终响应。这个过程能