引子
上次写完那篇"省 token 比汉化更有意思"的文章后,我收到了不少反馈。
有人问:"你这工具到底能不能用?"
这是个好问题。一个项目的设计理念写得再漂亮,如果在实际使用中三天两头报错,那也只是空中楼阁。
所以这篇文章不讲理念,讲点更实在的东西------v0.12.0-cn.1 发布后,用户在实际使用中发现了什么问题,我们又怎么修的。
项目地址:github.com/xyshanren/hermes-agent-cn (分支 cn)
第一个 Bug:配置了 API Key,但它不认识
现象
用户反映:按文档跑完 hermes setup,输入了 DeepSeek 的 API Key,然后运行 hermes chat,报错:
⚠️ Provider resolver returned an empty API key. Set OPENROUTER_API_KEY or run: hermes setup
Key 明明配置了,系统不认识。
排查
第一反应是:配置没保存成功?检查 ~/.hermes/.env,文件确实存在,Key 也在里面。
那问题出在哪?代码走了一遍,找到 resolve_provider() 函数。
这是一个负责自动检测有哪些 Provider 可用的函数。它需要知道用户配置了哪些 API Key。
它的检测方式是:os.getenv("DEEPSEEK_API_KEY")。
问题就在这里。
os.getenv() 只读取当前 Shell 进程的环境变量 。而用户通过 hermes setup 保存的 Key 是写入了 ~/.hermes/.env 文件------这个文件只在 Hermes 启动时被 dotenv 库加载到 os.environ 里。但 resolve_provider() 是在某些特定路径下被调用的,那条路径上没有执行过 load_dotenv()。
所以 Key 虽然在文件里,但 resolve_provider() 读不到。
修复
修改了 resolve_provider(),把所有 os.getenv() 替换成了 get_env_value()------这个函数来自 Hermes 自己的配置模块,它会先检查环境变量,再检查 ~/.hermes/.env 文件。
python
# 修复前
if os.getenv("DEEPSEEK_API_KEY"):
return "deepseek"
# 修复后
from hermes_cli.config import get_env_value
if get_env_value("DEEPSEEK_API_KEY"):
return "deepseek"
改动不多,但暴露了一个问题:一个功能(配置 API Key)和另一个功能(检测 API Key)使用了不同的数据源,却没有同步。
这种"不同模块各读各的数据"的问题,在单体应用中不容易发现,但在 Agent 这种多层架构中很常见------config.py 管配置文件、auth.py 管认证、doctor.py 管诊断......它们各自的"从哪里读取配置"的逻辑如果不统一,就会出这种怪问题。
第二个 Bug:Provider 列表,外国供应商比中国的还多
现象
用户运行 hermes setup 选择 Provider 时,发现列表里还有一堆国外的:OpenRouter、Anthropic、Google Gemini......
而硅基流动(SiliconFlow,一个重要的国产代理平台)却不在列表里。
问题出在哪
这里有两个问题:
问题 A :Provider 列表的过滤不完整。之前做 Provider 裁剪时,只过滤了主列表(显示给用户的列表),但 Provider 还有另一个注册途径------plugins/model-providers/ 目录下的动态注册。这些动态注册的 Provider 会绕过过滤,直接出现在选择菜单中。
问题 B:硅基流动是新加的国产 Provider,没有包含在初始的 Provider 列表中。
修复
修复 A :加了一个 _cn_skip_providers 集合,在动态注册的 Provider 进入列表之前做一层过滤。
修复 B :在 CANONICAL_PROVIDERS 中添加了硅基流动,配置了它的 API 端点和模型列表(Qwen 系列、GLM 系列、DeepSeek 系列等 25 个模型)。
背后的工程教训
这件事告诉我:"隐藏"和"彻底移除"是两回事。配置隐藏的优点是上游合并零冲突,但缺点是隐藏不彻底------只要有一个地方忘了处理,被隐藏的内容就会露出来。
第三个 Bug:重构之后,Doctor 开始"说谎"
现象
修复完上面两个 Bug 后,我想确认一下效果,于是运行了 hermes doctor。
doctor 说:
✓ Ollama 配置正确
✓ 本地模型已安装
✗ .env 文件不存在
但我的 Ollama 就是本地跑的,根本不需要 .env。
这就像你去体检,医生说"你血压正常、心率正常......等等,你没带身份证,不合格"。
根因
doctor 对 .env 的检查是硬性的 。不管你是否需要 API Key(比如只用本地 Ollama),它都要求 .env 文件存在。
这不是 Bug,是设计缺陷 ------诊断规则没有考虑使用场景的差异性。
改进
趁这个机会,我对 doctor 做了一轮系统的改进,覆盖了 D1 到 D5 五个方向:
D1 --- .env 内容智能检测 :不再硬性要求 .env 存在(只用本地模型时不需要),改为按场景分级提示。
D2 --- Conda/Pyenv 环境识别 :之前只检测 sys.prefix != sys.base_prefix,现在能区分 venv、conda、pyenv 三种环境类型,并显示当前 Python 解释器路径。
D3 --- 外部模型服务检查:新增检查段,检测 Ollama 运行状态和已部署模型列表。
D4 --- Fallback 链一致性:检测主力模型是否配了回退链、回退链条目是否缺少字段、主力模型是否出现在回退链中造成重复。
D5 --- 路由可视化 :新增 hermes route-status 命令,一键查看当前路由模式、Ollama 在线状态、云端 API 配置、嵌入式模型就绪状态。doctor 中也集成了路由配置概览。
bash
◆ 外部模型服务
✓ Ollama 服务运行中(3 个模型: qwen3-vl:8b, qwen3:32b, nomic-embed-text)
✓ 主力模型: deepseek-v4-flash(Provider: deepseek)
✓ Fallback 链已配置(2 个条目)
✓ [1] ollama/qwen3:32b
✓ [2] siliconflow/Qwen/Qwen2.5-72B-Instruct
✓ Auxiliary 视觉模型已配置(ollama/qwen3-vl:8b)
按任务路由:从"一个模型通用"到"什么任务用什么模型"
第一个版本的多模型路由(Phase 1)解决了一个基础问题:检测到多个模型时,自动分配文本和视觉模型到不同角色。
但实践下来发现不够。因为用户的对话不是单一类型的------同一轮对话里,可能先发一张截图问"这张图里有什么",然后接着问"这段代码怎么写"。如果系统只能用一个模型,要么视觉强但推理慢,要么推理强但不支持图片。
所以做了 Phase 2:消息级的按任务路由。
核心逻辑
用户消息进入 Agent
│
├── 有图片附件?→ 路由到 vision 模型
├── 包含"分析""推理""证明"等关键词?→ 路由到 reasoning 模型
└── 都不是 → 路由到 default 模型
每一轮对话都重新评估:当前这条消息需要什么能力------视觉理解?深度推理?还是常规对话?然后自动切换到最合适的模型。
配置方式
hermes quickstart 自动生成,无需手动编辑:
yaml
model_routing:
default:
model: "qwen3:32b" # 日常对话、代码
vision:
model: "qwen3-vl:8b" # 图片理解
reasoning:
model: "qwen3:32b" # 复杂推理(和 default 同一模型不浪费)
为何不继续做 Phase 3
Phase 3 的方案是运行时动态模型切换 ------允许用户在对话中用 /model 手动切换模型,并自动处理跨模型的上下文兼容性。
评估后决定关闭:
| 子项 | 评估结果 |
|---|---|
| 手动切换 | Phase 9 已绑定模型启动,用户可退出对话后切换;与自动路由重复 |
| 上下文管理 | 所有路由模型在同一 Ollama provider 下,无需重建上下文 |
结论:Phase 2 的自动路由已覆盖核心需求,手动切换和跨 provider 上下文管理在当前架构下不必要。
几个值得讨论的设计问题
1. 模型命名不统一
Ollama 的模型名长这样:qwen3-vl:8b,LM Studio 的是本地文件路径:C:\models\qwen3-vl-8b\qwen3-vl-8b.gguf。
当前的做法是用模型名启发式匹配(vl → vision,r1 → reasoning),但对 LM Studio 用户不够友好。有没有更好的统一方案?目前的想法是可以加一个模型能力标签 (tags: [vision, reasoning]),让用户在配置中手动标记。
2. 任务类型粒度
目前只分了 three 类:vision / reasoning / default。但实际使用中可能还需要:
- coding:专门的代码模型(如 deepseek-coder)
- tool_call:工具调用用快速模型,减少等待
- short_chat:简短对话走本地模型
但分类太细也有问题------误判的代价比"该用视觉模型但用了通用模型"高得多。
3. 用户如何干预?
当前的自动路由是"幕后"的------用户发消息,系统自动选模型。但用户有时就是想手动指定:"这条用 visual 模型看看"。
可能的方案:
/model qwen3-vl:8b临时覆盖当前轮次的路由- 下一次消息自动恢复到自动路由状态
目前还没实现,因为要解决一个微妙的问题:用户手动指定模型后,自动路由是否应该覆盖它? 覆盖了用户会困惑,不覆盖又失去了自动路由的意义。
4. 多 Key 轮换与多模型切换的关系
credential_pool(多 API Key 轮换)和 model_routing(多模型切换)是两个独立维度。当同一个 provider 下有多个模型、多个 Key 时,切换逻辑应该是:先选模型,再选 Key。因为模型决定能力,Key 只决定配额和速率。
5. 辅助视觉 vs 主对话视觉
现在有两个视觉模型配置:
auxiliary.vision:用于vision_analyze工具(处理 Agent 内部视觉任务,如浏览器截图分析)model_routing.vision:用于主对话中的图片理解(用户上传图片让 Agent 看)
两者可以是同一个模型,也可以不同。用户能同时配两个不同的视觉模型吗?技术上可以,但 UX 上可能让人困惑。当前的做法是:quickstart 自动配置时,两者指向同一个视觉模型,用户有特殊需求再手动分开。
以上,如图你有不同意见可以在评论区留言。
文档体系:从"作者懂就行"到"别人也能懂"
Bug 修完了,功能加上了,但还有一个问题:别人怎么知道这些东西改了?
之前的文档状况:
- README 817 行,堆了大量重复信息和设计哲学
- 没有文档索引,新用户打开 GitHub 不知道从哪里看起
- 没有 FAQ,用户遇到问题只能提 Issue
花了半天时间做了一件事:把项目文档系统化。
具体做了:
- README 重构:817 行 → 230 行,删掉重复内容,精简设计哲学,只留"快速开始 + 安装 + 命令速查 + 文档导航"
- ARCHITECTURE.md:系统架构图、核心数据流、7 大模块详解、上游对比
- API.md:CLI 命令参考(50+ 命令)、配置项参考(30+ 配置段)、Provider 注册 SDK
- FAQ.md:17 个常见问题,从安装到运行时全覆盖
- DOCS-MAP.md:文档体系总览,每份文档的角色和读者定位
这样,新用户打开 GitHub 就知道:README 是入口,有问题去 FAQ,想深入看 ARCHITECTURE,写扩展看 API。
总结:从"我能用"到"别人也能用"
这轮改造,从 v0.12.0-cn.1 到 v0.12.0-cn.5,20+ 个 commit,大约 3 天时间。
主要变化:
| 类别 | 之前 | 之后 |
|---|---|---|
| API Key 检测 | os.getenv() 只读环境变量 |
get_env_value() 读 .env 文件 |
| Provider 列表 | 28 个,含大量不可用国际版 | 11 个,国产+本地 |
| 硅基流动 | 不支持 | 支持(25 个模型) |
| 多模型路由 | 只支持一个模型 | 自动分类 + 消息级按任务路由 |
| Doctor 诊断 | 10 项基础检查 | 15+ 项,含 Ollama/Fallback/路由可视化 |
| 文档 | 817 行 README,无子文档 | 6 份文档,体系化 |
| README | 817 行,重复臃肿 | 230 行,精简聚焦 |
但这些数字背后,我觉得更有价值的是这轮改造让我想清楚了一件事:
开源项目的 Bug 修复,不只是改代码,更是在补"认知差"。
用户不知道你的代码是怎么写的,不知道哪个模块从哪读配置,不知道什么场景下会触发什么检查。所以当 Bug 出现时,用户看到的只是"配置了 Key 但系统不认识",而开发者需要回溯整个调用链才能找到根因。
每修一个 Bug,都是在把系统的隐式假设变成显式逻辑。
如果你也在折腾 AI Agent 的本地化,欢迎来 GitHub 交流。
参考资源
- 项目主页 :github.com/xyshanren/hermes-agent-cn(分支
cn) - 架构文档 :ARCHITECTURE.md
- 设计提案 :PROPOSAL-multi-model-routing.md
- API 参考 :API.md
- 更新日志 :CHANGELOG_CN.md
- 前一篇文章 :我把这个 AI Agent 汉化了,然后发现省 token 比汉化更有意思
求索实验室 · 探索 AI 技术的工程实践