维护 Hermes Agent CN 过程中的碎碎念,以及从bug上得到的一点点启发

引子

上次写完那篇"省 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

花了半天时间做了一件事:把项目文档系统化

具体做了:

  1. README 重构:817 行 → 230 行,删掉重复内容,精简设计哲学,只留"快速开始 + 安装 + 命令速查 + 文档导航"
  2. ARCHITECTURE.md:系统架构图、核心数据流、7 大模块详解、上游对比
  3. API.md:CLI 命令参考(50+ 命令)、配置项参考(30+ 配置段)、Provider 注册 SDK
  4. FAQ.md:17 个常见问题,从安装到运行时全覆盖
  5. 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 交流。


参考资源


求索实验室 · 探索 AI 技术的工程实践

相关推荐
java修仙传4 小时前
Java 实习日记:一次 Excel 导入校验 Bug 的定位与数据更新逻辑优化
java·数据库·bug·excel·后端开发
当战神遇到编程4 小时前
软件测试基础入门:从 BUG 到测试用例设计完整指南
测试用例·bug
Bear on Toilet3 天前
3. BUG篇
bug
编程探索者小陈3 天前
【测试】之BUG篇
bug
棋宣3 天前
uni-app编译到微信小程序中,父传子props首次传递数据不接收的bug
微信小程序·uni-app·bug
wqdian_com3 天前
华为手机浏览器的一个bug
服务器·华为·bug
清水白石0084 天前
把事故变成护城河:如何设计回归测试,防止“订单重复创建”这类历史 Bug 卷土重来?
python·bug
njsgcs4 天前
c# solidworks createline 拉伸发现有微小两点间隙 导致拉伸变成薄壁特征 改bug画了6个小时 解决结果
c#·bug·solidworks
前端达人4 天前
第18课:实战案例二,线上紧急 Bug 修复全过程
bug