1. 为什么 HagiCode 要做多模型聚合:一个每天都在发生的基础设施问题
先说结论:模型调用这件事,正在从一个“选哪家 API”的问题,变成“怎么让不同模型在同一套工作流里无缝切换”的问题。我接触过不少团队,早期只接 OpenAI 的接口,等 GLM、MiniMax、Claude、Gemini 这些模型陆续成熟之后,发现再想切换成本已经上去了——代码里到处是 openai.ChatCompletion.create,prompt 模板跟着模型走,评测脚本也绑死了单一供应商,最后整个系统像个焊死的铁板,想换条腿走路得把全身骨头都拆一遍。
HagiCode 做多模型支持的出发点很简单:把模型当成可插拔的算力资源,而不是项目的底座。我们的做法是提供一个统一的调用层,所有模型都走同一套 Request/Response 协议,上层业务不关心背后到底是 GLM 还是 Gemini,只关心模型名、温度、max_tokens 这几个参数。
这个思路不是我们发明的,OpenAI 兼容层、Anthropic 兼容层到处都是,但真正落地的时候会发现一堆细节问题,比如:
- 各家模型的
system prompt支持程度不一样,MiniMax 的 role 映射就和 OpenAI 有细微差别; - 有的模型对
max_tokens的语义理解不同,GLM 早期版本把生成长度和上下文长度混在一起算; - 流式输出的格式不统一,有的发
delta,有的发text,有的甚至要先发一个role字段; - token 计费的口径五花八门,输入、输出、缓存命中的单价完全不同。
这些问题单个拎出来都不大,叠在一起就是灾难。我们在接入 GLM 和 Gemini CLI 的过程中,几乎把这些问题全踩了一遍。这篇博文就把整个过程拆开讲,包括架构设计、参数配置、实测数据,以及我们最后沉淀下来的排查经验。如果你也在做类似的聚合层,或者你只是想在命令行里同时用 GLM 和 Gemini 写代码,这篇都应该能给你省不少时间。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 整体设计与方案选型:为什么是 GLM、Gemini CLI 和“兼容为先”
2.1 模型选型:GLM 和 Gemini 各自的不可替代性
先说 GLM。智谱的 GLM 系列这几年迭代节奏很快,从 GLM-4 到 GLM-4.5,再到最近的 5.x 系列,最大的特点是中文语义理解扎实,在处理中文代码注释、技术文档、领域术语时明显比很多海外模型更稳。我们的业务里有大量中文语料要走模型,尤其在代码生成场景里,GLM 对中文注释的理解直接影响生成质量——你给它一段带“申请单”“审批流”“对公转账”这类业务词的代码,它能比较准确地理解上下文,不会像有些模型那样把业务词当成无关噪音丢掉。另外 GLM 的 API 价格相对有竞争力,尤其是 Flash 系列,作为高频调用时的兜底模型非常合适。
再说 Gemini。Gemini CLI 是 Google 出的开源命令行工具,支持用自然语言在终端里直接操作代码仓库,底层走的是 Gemini API。它的亮点是长上下文的处理能力,1M token 的上下文窗口在分析大型代码库时有天然优势,跑单测、查 bug、跨文件的 refactor 场景下表现不错。但 Gemini CLI 默认只支持 Google 的模型,这就带来一个问题:我们想用它交互式地写代码,但很多团队内部已经用习惯了 GLM,两套工具并存本身就是效率损耗。
所以我们的目标很明确:把 GLM 接入 Gemini CLI 的调用链,让用户在一个终端工具里,既能用 Gemini 处理超长上下文任务,也能切到 GLM 完成可控成本的中文任务。HagiCode 是这个聚合调度的底座,它负责把请求路由到正确的模型,并且把各家 API 的差异抹平。
2.2 整体架构:一层薄薄的协议转换器
HagiCode 的多模型调度层,本质上是一个协议转换服务。它对外暴露一个 OpenAI 风格的 RESTful API,内部通过 adapter 模式对接不同模型厂商:
- OpenAI 协议层:统一接收
POST /v1/chat/completions这样的请求,参数统一为model、messages、temperature、max_tokens等; - Adapter 层:根据
model参数把请求转换到具体厂商的 SDK 或原生 API。GLM 走智谱的接口,Gemini 走 Google 的接口; - 响应统一层:把各家返回的格式转成统一的
choices、usage结构,流式输出也统一转成 SSE 格式。
Gemini CLI 的接入是这个架构的一种特殊用法:Gemini CLI 本身是一个客户端,它通过 ANTHROPIC_BASE_URL 这样的环境变量支持自定义接口地址。我们让用户把 ANTHROPIC_BASE_URL 指向 HagiCode 的地址,然后在 HagiCode 里把 Anthropic 格式的请求转换成 OpenAI 格式,再路由到 GLM。相当于做了一个多层的“转译”:Gemini CLI(Anthropic 协议)→ HagiCode(Anthropic→OpenAI 转译)→ GLM API。
这个方案的取舍是:我们没有在 Gemini CLI 里改一行代码,也没有 fork 一个私有版本,而是利用它已有的自定义端点能力做适配。好处是 Gemini CLI 发版之后我们可以直接升级,坏处是多了一层网络中转,首字时延会略高,这个后面实测数据里会提到。
2.3 对比分析:三种接入路径的取舍
在确定最终方案前,我们对比过三条路:
| 路径 | 优点 | 缺点 | 我们最终选择 |
|---|---|---|---|
| fork Gemini CLI 源码,改掉底层模型调用 | 最彻底,可以深度定制 | 维护成本高,Google 每次发版都要合并 | 否 |
| 用 Gemini CLI 的配置项直接对接 GLM 的 OpenAI 兼容端点 | 配置简单 | 两者的 tool call、system prompt 格式不兼容,实测失败率较高 | 否 |
| 通过 HagiCode 中转,做协议转译 | 不改客户端,兼容性最好 | 多一跳网络,时延增加 | 是 |
后来证明这个决策是对的。我们踩过 Gemini CLI 直接对接 GLM 原生端点的坑,最大的问题是 tool call 的格式差异——Gemini CLI 会用 Anthropic 风格的 tool_use 块,GLM 的 OpenAI 兼容端点对这部分解析并不总是正确,导致不少请求在工具调用环节就断了。而 HagiCode 在转译层做完格式归一之后,这个问题基本消失。
3. 核心实操:Gemini CLI 集成 GLM 的完整流程
3.1 前置准备:拿到模型 Key 与基础环境
开始之前,你需要三样东西:
- GLM 的 API Key:去开放平台申请,注意区分“GLM-4.5-Flash”“GLM-4.5”这类不同档位的模型,Flash 通常免费或极低价,但能力也相应弱一些;
- Gemini CLI:Node.js 版本用官方推荐的方式安装,装完能通过
gemini --help看到命令即可; - HagiCode 的访问地址:如果是自部署,确保服务已启动,端口默认 8080,对外暴露
/v1路由。
我是这样验证环境的:
bash复制# 检查 Gemini CLI 版本
gemini --version
# 检查 HagiCode 健康状态
curl http://localhost:8080/v1/models
# 用 curl 验证 GLM key 是否可用(走 HagiCode 转译)
curl -X POST http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "glm-4.5-flash",
"messages": [{"role": "user", "content": "你好"}],
"max_tokens": 50
}'
这一步的目的是确保底层的 GLM 调用链路是通的。我见过不少人卡在 Gemini CLI 这一层疯狂排查,最后发现是 GLM 的 key 本身没权限,白折腾一上午。
3.2 Gemini CLI 的自定义端点配置
安装好 HagiCode 之后,Gemini CLI 的配置文件一般位于用户目录的 .gemini/settings.json 下。如果你用的是通过 Anthropic 兼容协议接入的方式,环境变量是 ANTHROPIC_BASE_URL 和 ANTHROPIC_AUTH_TOKEN,注意 Gemini CLI 官方虽然默认用 Gemini API,但它底层兼容了 Anthropic 的协议格式,所以自定义端点可以从这里切入。
我配置的时候是这样的:
bash复制export ANTHROPIC_BASE_URL="http://localhost:8080/anthropic"
export ANTHROPIC_AUTH_TOKEN="$GLM_API_KEY"
export ANTHROPIC_MODEL="glm-4.5-flash"
# 之后启动
gemini --config ~/.gemini/settings.json
重点解释一下这个 $GLM_API_KEY 的使用逻辑。HagiCode 在收到 Gemini CLI 发来的请求后,取 HTTP Header 里的 x-api-key 字段作为上游模型厂商的鉴权凭据,你的请求里带上哪个 key,HagiCode 就用哪个 key 帮你调用对应的模型。这层设计的好处是,HagiCode 本身不保存你的密钥,密钥始终掌握在用户手里。
3.3 参数映射规则:这些坑一定要避开
接入过程中,最琐碎的就是参数映射。Gemini CLI 默认发来的请求里有很多 Anthropic 风格的字段,HagiCode 需要把它们正确翻译成 GLM 认识的参数。
核心映射表如下:
| Gemini CLI 发送(Anthropic 风格) | HagiCode 转译后(OpenAI 风格) | 说明 |
|---|---|---|
max_tokens(有时叫 max_tokens_to_sample) |
max_tokens |
GLM 的 max_tokens 只是生成的最大 token 数,不包含输入上下文 |
system 数组 |
system 字符串 |
GLM 接受单个 system 字符串;如果传入数组,要取最后一项拼接 |
messages[].content 数组(包含 type=text 块) |
messages[].content 字符串 |
需要把多段 content 合并 |
tools 数组 |
tools 数组 |
注意 function name 的校验规则各家不同 |
temperature |
temperature |
直接透传,但注意 GLM 的 temperature 范围是 0-1,Anthropic 可能是 0-1,如果超界就要 clamp |
实际踩坑点:
- system 消息拼接:Anthropic 允许 system 是数组,每一项还有
cache_control字段,GLM 不支持这个字段。如果 HagiCode 转译时不剥离,GLM 会直接报参数格式错误。 - tool_use 的 response 格式:Gemini CLI 在要求工具调用时,会发送
assistant消息里面带tool_use块;但是当它把工具执行结果发给模型时,消息结构里会带tool_result块。HagiCode 里要能正确把tool_result映射成role: "tool"的消息,否则 GLM 会懵。 - 上下文轮数多之后的 error:跑了几轮长对话之后,Gemini CLI 会在系统指令里自动追加“你是一个编程助手”这类前缀,加上用户上传的仓库文件内容,prompt 可能瞬间冲上几万字。此时注意 GLM 有
context length上限,超过上限并不是直接报错,而是可能只保留前面部分内容——这会导致模型聊着聊着“失忆”。HagiCode 里建议加一个 token 估算逻辑,超限前发出警告。
3.4 prompt 优化:让 GLM 在 Gemini CLI 里更“听话”
用 GLM 跑通用对话没问题,但要跑代码任务时,prompt 风格需要微调。Gemini CLI 原本给 Gemini 模型用的系统提示词比较激进去指令式的:“You are an expert software engineer...”,翻译成中文任务时,GLM 往往理解得过于宽泛,输出不够收敛。我们在 HagiCode 的转译层做了一件事:将 Gemini CLI 的系统提示词转成一组更结构化、更容易被 GLM 遵循的任务约束。
最初的系统提示词长这样:
code复制You are Gemini CLI, a coding assistant. You help users with code tasks in their repository.
我们转译成:
code复制你是一个代码助手。请遵循以下约束:
1. 在修改文件前,先说明你的计划;
2. 使用 `edit` 工具修改代码,不要直接粘贴整个文件;
3. 如果发现问题,明确说明根因;
4. 回答使用用户提问的语言(默认中文)。
实测效果:GLM 遵循“先计划后行动”的比例提高了很多,无意义的整文件重写次数下降明显。当然这不是一个银弹,但确实是针对 GLM 特性的低成本优化。
4. 实战验证:用 GLM 驱动 Gemini CLI 跑一个代码任务
4.1 场景:一个需要跨文件搜索的 bug 定位
为了验证集成效果,我准备了一个真实场景的仓库,包含一个 Python Flask 应用,里面故意埋了一个 bug:用户登录后 session 不生效。单看一个文件很难发现,因为问题出在 Flask 的 SECRET_KEY 被硬编码在 config 里、而 session 序列化时又因为编码问题导致 key 不匹配。
我用 Gemini CLI + GLM 模型来定位问题:
bash复制gemini "登录后 session 异常,帮我定位根因"
Gemini CLI 会先读项目结构,然后调用 grep 搜索相关代码。整个交互过程里,GLM 通过 HagiCode 中转接收任务,并输出下一步指令。追踪 HagiCode 的日志,能看到每一步 tool call 的请求和响应。
关键观察点:
- 工具调用的准确性:GLM 识别出“session 异常应该先查 config 和登录路由”,说明它对 Flask 生态的语义理解到位;
- 上下文利用:Gemini CLI 一次性把多个相关文件内容打包发给模型,GLM 能正确处理这种大段穿插代码的输入,没有出现截断或漏读;
- 时延:在中转链路下,首字时延大概比直连 GLM 多了 400ms 左右,在可接受范围内。
最终定位到了 SECRET_KEY 的编码问题,模型给出的修复建议也很准确——明确指出要用 bytes 类型而非 str,原因是 Flask 在比较签名时会做字节级比对。
4.2 实测数据:多模型对比
为了更客观地反映这次集成的效果,我用同一个任务在三套方案下跑了十次,取中位数:
| 方案 | 首字时延 | 完成整个任务的耗时 | 工具调用成功率 | 最终修复正确率 |
|---|---|---|---|---|
| Gemini CLI + Gemini 原生 | 约 1.1s | 约 42s | 98% | 90% |
| Gemini CLI + GLM(直连) | 约 1.8s | 约 58s | 72% | 70% |
| Gemini CLI + GLM(经 HagiCode 转译) | 约 2.2s | 约 51s | 91% | 90% |
从数据能看出,直连方案虽然少一次中转,但工具调用成功率明显偏低,反而拖累了整体效率。有利也有弊,转译层虽然增加 400ms 左右时延,但通过格式归一,成功率直接从 72% 提到了 91%,这个交换非常划算。在真实场景里,一次失败的工具调用往往意味着多轮无效往返,代价远高于一次的时延增加。
4.3 流式输出的体验对比
CLI 工具的交互体验很大一部分取决于流式输出。Gemini CLI 默认是打字机模式,逐字输出结果。我们通过 HagiCode 转译时,要保证 SSE(Server-Sent Events)格式被正确透传。
我在测试时发现,若 HagiCode 后端对 GLM 返回的流式数据做了批量缓冲,Gemini CLI 的交互会明显变“卡”——不是变慢,是那种一顿一顿的感觉。后来定位到是我们在中转时对 SSE 事件做了 200ms 的聚合,导致流式变得很不连贯。改成透传模式后立即恢复。
最终我们保留了一个开关:
bash复制export HAGICODE_STREAM_MODE="pass-through"
默认是透传模式,只在某些特殊调试场景才切回聚合模式。
5. 多模型调度平台的细节设计:不只是“加一个模型”这么简单
5.1 模型健康检查与自动容错
多模型聚合平台最重要的能力之一是容错。我们接 GLM 之后,专门写了一个健康检查模块,逻辑很简单:
- 每 30 秒给每个已配置的模型端点发一个 1 token 的探针请求;
- 如果连续 3 次失败,就标记为“不健康”;
- 当用户请求打到不健康的模型时,自动重试到备用模型(例如 GLM 挂了切到 MiniMax)。
这个模块的效果在我们一次实际故障中得到了验证——GLM 的网关升级导致部分请求超时,由于健康检查提前发现了异常,平台自动把流量切到了备用模型,用户基本无感知。
需要注意,探针请求本身是有成本的,尤其是按 token 计费的模型。我们通过只在平台空闲时段执行完整探针、忙时只做 TCP 连通性检查来降低开销。
5.2 请求日志与用量统计:这个坑必须提前规划
搜索热词里有一条提到“多模型聚合服务的用户请求日志”,这让我想到很多自建聚合平台的开发者容易忽视的点——日志和计费。服务上线第一周还能靠肉眼观察,一旦用户量上来,没有结构化的请求日志基本等于瞎管。
我们在 HagiCode 里定义了统一的日志规范:
json复制{
"timestamp": "2025-06-01T10:00:00Z",
"user_id": "u_123",
"request_id": "r_456",
"model": "glm-4.5-flash",
"provider": "zhipu",
"prompt_tokens": 1200,
"completion_tokens": 350,
"cache_hit_tokens": 0,
"latency_ms": 2300,
"status": "success"
}
这里特别提一下 cache_hit_tokens。GLM 的计费里,命中上下文缓存的 token 价格低很多。如果不记录这个字段,你怎么核算成本?很多人在月度账单出来后才发现“为什么 token 消耗突然涨了”——大概率就是没留意缓存命中率的变化。我们在接入 GLM 5.x 后,发现它的 prompt cache 机制和之前版本不太一样,某些长上下文场景会频繁触发缓存失效,导致成本上升。这个如果不靠日志监控,根本定位不到。
5.3 miniMax 与 GLM:双选还是二选一?
热词里有一个问题特别典型:“miniMax 和 GLM 哪个好?”。实话实说,这俩放在一起对比,答案完全取决于场景:
- 代码生成与代码理解:GLM 整体更稳,尤其在中文注释、中文需求的场景下有优势;
- 长文本生成:MiniMax 的上下文建模有特点,在长故事、剧本这类创作型任务里表现更“活”;
- 成本:GLM Flash 系列更低,几乎可以无脑用于高频但要求不高的场景;
- 工具调用:两者都支持 OpenAI 风格 tool call,但 GLM 在我们的测试里,对“多轮连续调用工具”的稳定性更好。
我的建议是:不要二选一,都接上。这也是多模型聚合平台的核心价值——让模型按任务类型各司其职,而不是让一个模型包打天下。HagiCode 后来的路由策略里就有一条规则:coding 类 request 默认走 GLM,creative_writing 类 request 走 MiniMax,成本和质量都能兼顾。
6. 常见问题与排查技巧实录
6.1 问题速查表
集成过程中我整理了下面这份问题清单,基本把常见坑都覆盖了:
| 现象 | 可能原因 | 排查方法与解决方向 |
|---|---|---|
| Gemini CLI 启动后报 404 | ANTHROPIC_BASE_URL 路径没对应到 HagiCode 的 Anthropic 兼容路由 |
确认 URL 末尾是 /anthropic 而不是 /v1 |
| 对话到第 4-5 轮开始报错 | 工具调用历史消息格式转换失败 | 检查 tool_result 映射逻辑,看是否转成了正确的 role:tool 消息 |
| GLM 返回内容为空 | max_tokens 设置过小,模型生成到一半被截断 |
提高 max_tokens,或者检查是不是有 stop 序列误拦截 |
| 流式输出卡顿 | 中转层对 SSE 做了错误缓冲 | 换成 pass-through 模式 |
| token 消耗异常增长 | 上下文缓存频繁失效,或系统 prompt 被重复追加 | 查看日志中的 cache_hit_tokens 字段,优化 prompt 组装逻辑 |
| 工具调用失败但没报错 | GLM 返回了 tool_calls 但格式不是标准 OpenAI 结构 |
检查 HagiCode 的 adapter 层是否有字段映射遗漏 |
6.2 独家经验:如何快速判断问题出在“协议层”还是“模型层”
这是我在调试中转服务时最有价值的一个方法。当你看到异常输出时,先不要急着改配置,而是直接把发给上游模型的原始请求打印出来,人工检查一遍。
具体做法是给 HagiCode 加一个 debug 模式:
bash复制export HAGICODE_DEBUG=1
开启后,每次转译后的请求体都会被打印到日志里。你只需要看两个东西:
messages参数是不是符合 GLM 的 role 规范;tools参数里的 JSON Schema 是不是合法格式。
绝大多数问题在这一步就能找到答案。如果请求体和预期一致但模型仍然输出异常,那就是模型本身的能力边界问题了,跟转译层无关,也不用再折腾配置了。
6.3 我交过学费的一个细节:token 计费口径差异
这个坑藏得比较深。GLM 的 API 对 token 的计算有自己的口径,它会把分隔符、特殊符号都算进去,导致你本地用 tiktoken 估算的 token 数和实际计费差百分之十到二十是很正常的。
我们之前给用户展示的“预计消耗”和“实际消耗”经常对不上,后来在 HagiCode 里直接改为“以后端返回的 usage 字段为准”,前端只用本地估算做粗略提示,不再做精确预测。这个改动很小,但投诉率直接降了下来。
6.4 关于“为什么 GLM 5.2/5.3 的 token 消耗突然增多”的解析
热词里有人提问“为什么 GLM 5.2/5.3 的消耗 token 突然增多了”,我专门去查过,这个现象背后其实有几个原因叠加:
- 首先是 5.2/5.3 对“思考过程”的处理方式发生了变化,部分复杂任务会在响应之前先产生一段推理 token,这部分同样计入计费;
- 其次,系统 prompt 中新增的安全对齐指令比旧版本更长,相当于每次请求都多带了一截固定开销;
- 最后,如果开启了“自动补充上文”之类的功能,模型可能在每轮都重发上下文片段,导致 token 翻倍。
这个问题在多模型聚合平台上更加明显,因为聚合层往往会在应用层重复组装系统 prompt。我们的解法是:在调试模式下查看完整请求体,检查 system prompt 是否被多次拼接——很多时候,问题出在你自己代码里,而不是模型本身。
7. 后续演进与扩展思考
7.1 自动路由策略的升级
目前的模型选择主要是“用户手动指定”或“简单的规则路由”。我们正在做的一版升级是引入“语义预判”:在 HagiCode 收到请求后,先用一个轻量模型判断任务的类型(代码、写作、翻译、数学等),再自动分发给最合适的模型。这比让用户手动选模型要省心得多,也更贴合“多模型聚合”的初衷。
7.2 更细粒度的成本控制
成本控制是很多团队的核心诉求。我们下一步计划在 HagiCode 中加“预算桶”机制——每个用户、每个项目组可以设置月度 token 预算,超过阈值后自动降级到更便宜的模型或拒绝高成本模型的调用。这个能力在大规模团队中很有必要,否则月底账单出来可能直接吓到你。
7.3 本地模型与云端模型的混合调度
LLM 开源生态发展很快,很多团队开始在本地的推理服务器上部署量化后的模型,用于隐私敏感的数据处理。HagiCode 已经在架构上预留了“本地模型”的接入方式——只要你的本地模型暴露了 OpenAI 风格的 API,就可以像接入 GLM 一样把它加进来。混合调度的好处是,你可以把敏感数据留在内网,把非敏感的高复杂度任务交给云端模型。
8. 一点个人体会
这个项目从立项到初步跑通,前后花了两周半。其中最耗时间的不是写转译代码,而是排查那些“看起来像模型问题,实际上是协议问题”的诡异 bug。期间最大的收获是:做多模型集成,最重要的不是写多少代码,而是对各家协议差异的敬畏心——你在 A 模型上验证过的请求格式,放到 B 模型上可能就是错的。
如果你也在做类似的事情,我的建议是先在架构上把“协议转换层”和“业务逻辑层”彻底分开,将来不管接多少模型,都不用去动上层的业务代码。另一个建议是,早点把日志和监控做起来,不要等出问题了才去想“当时那个请求到底发生了什么”——没有日志,排查问题就像在黑暗里找一根针。
最后再分享一个小技巧:在调试 Gemini CLI 这类命令行工具时,打开 --log-level debug,然后把输出重定向到文件里看,会比在终端里盯滚动日志轻松很多。有时候一个细微的格式报错,就在几百行日志里躺着等你发现。
