11 KiB
ASR 管线 × Agent 耦合点 Review
日期:2026-09-18
背景:Hermes 网关从 .145 迁移到 .48 后,meeting-status-notify.py 因硬编码 127.0.0.1:8644 而 Telegram 通知失效(核心转录/整理流程本身不受影响)。这次故障暴露的不是孤立 bug,而是整条管线里 Claude/Hermes 两个 agent 以"硬编码地址/私有配置文件/直接 shell out"方式接入,缺乏稳定接口层。本文档目标:列出所有耦合点,诊断问题,提出"预留接口 + 通用 skill"的解耦方案,供下一轮重构使用。不在本文档内直接修改代码。
1. 涉及文件清单
1.1 生产代码(按耦合程度分类)
| 文件 | 耦合对象 | 耦合方式 | 严重度 |
|---|---|---|---|
OBSIDIAN/bin/meeting-status-notify.py |
Hermes | 硬编码 http://127.0.0.1:8644/webhooks/...;直接读 ~/.hermes/webhook_subscriptions.json 取 HMAC secret |
🔴 高(本次故障根源) |
OBSIDIAN/recordings/bin/meeting-watch.py |
Hermes | post_webhook()/post_status() 重新实现了一遍同样的 HMAC 签名 + 硬编码 URL 逻辑(与上面文件重复代码,两处维护) |
🔴 高(重复实现同一故障点) |
OBSIDIAN/asr-pipeline/asrpipe/vault.py (notify()) |
Hermes(间接) | 不直接硬编码地址,但通过 CFG.notifier 固定 subprocess.run(["python3", meeting-status-notify.py, ...]),位置参数契约无 schema |
🟡 中(依赖上面两个文件之一才能工作) |
OBSIDIAN/asr-pipeline/asrpipe/config.py (review_public_host) |
部署环境 | 默认值硬编码 192.168.50.145(本机 IP),可用 env 覆盖 |
🟢 低(有 env 覆盖,且 review-web 确实还在 .145,不是错误,只是"写死默认值"的一贯风格问题) |
OBSIDIAN/bin/obsidian-asr-refine.sh(已停用,仅供 rollback) |
Claude Code | 直接 shell out $HOME/.local/bin/claude -p,依赖交互登录态 |
🟡 中(非活跃路径,但 rollback 会重新引入该耦合) |
OBSIDIAN/bin/obsidian-asr-hotwords.py、OBSIDIAN/bin/obsidian-asr-intake.sh |
无(命名遗留) | 调用 ~/bin/hermes-qwen3-asr-smart——这是个纯 STT wrapper shell 脚本,跟 Hermes agent 运行时完全无关,只是历史命名带 "hermes" 前缀,容易被误读为依赖 agent |
🟢 低(认知负担,不是真耦合,建议重命名去混淆) |
~/.config/systemd/user/obsidian-recording-watch.service |
Hermes | Environment=MEETING_WEBHOOK_URL=http://127.0.0.1:8644/... 在 systemd 层再硬编码一次 |
🔴 高(第三处需要同步修改的地方) |
asr-pipeline/asrpipe/llm.py |
Claude(仅模型选择) | 字符串形式引用 anthropic/claude-sonnet-5 等模型名做定价/no-temperature 判断 |
🟢 低(纯 API 层配置,非 agent 运行时耦合,正常) |
asr-pipeline/asrpipe/cli.py、config.py 文件头注释 |
无 | 明确声明"no Claude Code, no opencode, no agent runtime"——这是架构上已经做对的部分,核心管线是纯 HTTPS+文件驱动,值得保留和强化 | 🟢 无耦合(正面案例) |
1.2 文档/Skill 层(描述与现状不一致,需要一并清理)
| 文件 | 问题 |
|---|---|
~/.claude/skills/memo-inbox/references/webhook-toolset-diagnostics.md |
描述的是已被取代的旧架构:假设有一个"webhook agent"(Hermes)直接处理 meeting-recording 路由、需要 hermes-webhook/file/skills toolset、把转录内容发给 Hermes agent 做 memo-inbox 整理。但 CLAUDE.md 明确写着"已取代旧的 Hermes→Telegram agent 精修",且 meeting-watch.py 里 post_webhook()(对应 meeting-recording 路由,携带正文)从未被实际调用,只有 post_status()(状态 ping)在用。这份 skill 文档描述的故障排查对象已经不存在,但仍在 memo-inbox skill 里被索引,可能误导下一次排障。 |
~/.claude/skills/memo-inbox/references/asr-refine-pipeline.md |
与 CLAUDE.md、HANDOFF.md 三份文档描述同一条管线,各有侧重但存在信息重复(例如五阶段说明在两份文档里几乎逐字重复)。不是错误,但增加了"改一处忘改另一处"的维护成本——这次 8644 端口就是同一类问题在配置层面的重演。 |
OBSIDIAN/asr-pipeline/HANDOFF.md |
明确写了"Telegram 通知(hermes-gateway :8644 签名 webhook)",是当前唯一还把 Hermes 网关端口写进设计文档的地方,本身没错但需要随实现一起更新。 |
目前不存在 agent-protocol.md(据 hindsight 记忆,2026-07-17 曾规划把 obsidian skill 拆分出一份 Hermes/Codex/Claude Code 共用的 agent-protocol.md,统一七步协议) |
说明"通用协议文档"这个方向此前已经被认可过,但从未真正落地到 ASR 通知这一层,这次故障正好是没有该抽象层的直接后果。 |
2. 问题诊断
2.1 核心问题:通知通道是唯一的"横切关注点",却用点对点硬编码实现
真正跨 Claude/Hermes 边界的交互只有一件事:管线要把"状态变化"告诉人(Kai),而人在 Telegram 上。当前实现把这件事等同于"调用 Hermes 网关在某个 IP:port 上开的私有 webhook",导致:
- 通知目标地址在三个物理位置各写一份(
meeting-status-notify.py、meeting-watch.py、systemd unit 的Environment=),Hermes 一换主机全部失效,还都要单独改。 - 认证方式依赖读取 Hermes 自己的私有配置文件
~/.hermes/webhook_subscriptions.json,管线对 Hermes 的内部文件结构做了假设——Hermes 的实现细节变了,管线也会跟着坏,而管线本身的 git 仓库里完全看不出这个依赖关系。 meeting-watch.py里的post_webhook/post_status和meeting-status-notify.py是同一逻辑的两份实现(都做 HMAC 签名 + POST),维护者容易改一处忘改另一处。
2.2 次要问题:文档层的"多真相源"
CLAUDE.md(基础设施说明)、HANDOFF.md(设计交接)、asr-refine-pipeline.md(skill 引用)三份文档对同一条管线各写一遍架构图和五阶段表格,webhook-toolset-diagnostics.md 甚至在描述一个已经退役的架构分支。这不是本次故障的直接原因,但预示着如果解耦时只改代码不改文档索引,同样的"多处不同步"问题会在文档层重演。
2.3 正面案例,值得保留的设计
asrpipe/核心包(S0-S4 五阶段 + fidelity gate + termbank)本身已经是纯 HTTPS + 文件驱动,不依赖任何 agent CLI 或登录态,cli.py/config.py头部注释明确声明了这个边界——这是解耦该学的样子。asr-review-web/control_web.py是一个独立 HTTP 服务,路由/状态机/上传都通过标准 HTTP 暴露,没有假设调用方是谁——这个"HTTP 接口面"是天然可以承载 Claude/Hermes 通用交互的地方,不需要另起炉灶。
3. 解耦方案建议(预留接口 + 通用 skill)
3.1 接口层:把"通知"做成可插拔的抽象,而不是"调用 Hermes"
建议参考 config.py 里其余配置项的风格(use_termbank/use_hindsight 等 boolean 开关 + env 覆盖),把通知也做成同一等级的可插拔能力:
- 在
asrpipe/config.py增加notify_backend(如webhook/none/ 未来的stdout/file等),而不是隐式认定"notifier = 某个写死路径的 python 脚本"。 meeting-status-notify.py改造成唯一的通知发送实现(meeting-watch.py不再自己重新实现 HMAC 签名,改为 shell out 到同一份脚本,或提取成共享库函数),并且:- 目标地址、密钥来源全部走 env / 独立配置文件(例如
OBSIDIAN/bin/notify-target.json),不直接读~/.hermes/*——管线自己的配置与 Hermes 的私有配置解耦,Hermes 换主机/换协议时只改这一份 target 配置,不改代码。 - 把"目标是 Hermes 网关的 webhook"这件事,退化为该抽象接口的一种实现,而不是唯一实现——未来即使 Hermes 换成完全不同的通知方式(比如直接调 Telegram Bot API),也只是换一个 backend,管线代码不用动。
- 目标地址、密钥来源全部走 env / 独立配置文件(例如
3.2 协议层:让 Claude 和 Hermes 通过同一份"通用 skill/协议文档"交互,而不是各自看各的私有细节
现状是 Hermes 一侧靠私有 webhook 配置、Claude 一侧靠读 CLAUDE.md/HANDOFF.md,两边对同一条管线的认知来自不同文档,容易出现你这次遇到的情况——迁移动作只在 Hermes 侧发生,ASR 侧的假设没人同步更新。
建议:
- 落地此前已经规划过的
agent-protocol.md(hindsight 记忆里 2026-07-17 就提出过这个方向,但目前尚未创建)。把"任何 agent(Hermes/Claude Code/Codex)如何得知 ASR 管线的状态变化、如何触发人工确认、通知目标怎么配置"写成一份三方共用的协议文档,而不是分散在CLAUDE.md+HANDOFF.md+ memo-inbox skill 引用里各写一份。 - 把
control_web.py已有的 HTTP 接口面正式定义为唯一对外交互入口:任何 agent 需要查询任务状态/触发处理,都走这个 HTTP API,而不是直接读 sqlite(intake.db)或直接改文件。目前meeting-watch.py直接读写CONTROL_DB(sqlite),这本身也是一种"绕过接口层直连内部状态"的耦合,建议后续统一收敛到control_web.py的 API 之后。 - 清理
webhook-toolset-diagnostics.md:要么标注为"历史架构,已废弃于 2026-07-25",要么直接删除/归档,避免下次排障时被过时描述误导。 CLAUDE.md/HANDOFF.md/asr-refine-pipeline.md三份重叠内容建议收敛为"一份权威来源 + 其余引用链接"的结构(类似 vault 侧AGENTS.md作为 SOP 索引、具体细则各自一份的模式),而不是三份都写一遍五阶段表格。
3.3 命名去混淆
~/bin/hermes-qwen3-asr-smart 建议评估是否重命名(例如 qwen3-asr-transcribe-wrapper)——它不依赖 Hermes agent 运行时,纯粹是历史命名,留着容易让人(或下一轮重构的 LLM)误判为真实耦合点而做无用功。
4. 优先级建议
| 优先级 | 事项 | 原因 |
|---|---|---|
| P0 | 通知 backend 抽象化 + 三处硬编码收敛为一处配置 | 直接对应本次故障,且是唯一真正跨 agent 边界的运行时依赖 |
| P1 | 落地 agent-protocol.md,清理 webhook-toolset-diagnostics.md 等过时文档 |
防止"文档多真相源"重演同类故障 |
| P2 | meeting-watch.py 收敛直连 sqlite 为走 control_web.py API |
减少内部状态的多点直连,但当前未造成故障,可延后 |
| P3 | hermes-qwen3-asr-smart 重命名 |
纯认知成本,无功能风险,可随手做 |
5. 明确不建议做的事
- 不建议把核心
asrpipe/五阶段管线本身引入任何 agent 依赖——它当前"纯 HTTPS + 文件驱动"的设计是对的,解耦应该聚焦在通知/协议这一层,不要往核心管线里加 Claude/Hermes 相关代码。 - 不建议为了解耦而过度设计一套通用消息总线/插件系统——当前只有"一个通知需求 + 一个 HTTP 状态查询需求",用简单的 backend 开关 + 现有 HTTP 接口面就够,避免过度工程化。