ARTICLE / 开源项目
补充对话式追问路由特性的研究
PR 地址:NousResearch/hermes-agent#47043 · 基于上下文回复检测的对话式追问路由
研究摘要
在 IM 平台上,用户针对 agent 某条消息做"回复"(追问、修正、补充)是高频交互:agent 列了一个方案清单,用户回复其中一条"第三项换个做法"。此前 Hermes 网关把这类回复合并进单槽 _pending_messages 文本,与普通消息混在一起——会话正忙或消息排队时,追问与主文本混淆,agent 无法识别"这是针对哪条消息的回应",上下文就此错位。本研究向 Hermes 上游补充了对话式追问路由:检测到用户回复的是 agent 自己的消息时,把追问标记为上下文相关的后续消息(_is_contextual_followup),路由到 _queued_events FIFO 溢出队列独立处理,不再与单槽文本合并。改动规模 +56/-1,仅 2 个文件,无需任何配置变更。
一、问题背景:回复语义在合并中丢失
1.1 单槽合并模型的信息损失
网关对活跃会话的待处理消息采用单槽 _pending_messages 文本承载:新消息到达时合并进同一块文本。这个模型对"顺序到达的独立消息"够用——消息之间没有引用关系,按序合并即可——但对"针对某条消息的回复"会丢失关键信息:回复的指向性(回复的是 agent 的第几条消息、哪个方案、哪段论述)被抹平成一串文本,agent 只能靠猜测还原语义。
指向性恰恰是追问交互的核心:用户回复"第三项换个做法",信息的价值不在"换个做法"这个动作,而在"第三项"这个指代。合并模型把指代关系丢掉了,agent 拿到的是一句没有锚点的指令。
指向性有两种形态:显式引用(平台转发的回复目标 ID)与隐式引用(用户口头的“第三项”)。本特性解决显式引用——平台已经记录了回复关系,网关需要把它利用起来;隐式引用仍依赖模型的语义推理。这条分界划定了特性的边界,也划定了它的可靠性:显式引用的识别是确定性的,不存在“猜错”的问题。
1.2 会话繁忙时的问题放大
会话正忙(agent 正在生成回复)时,追问到达被合并进待处理文本,与生成期间到达的其他消息混杂。等 agent 处理时,追问与其目标消息的关联已不可恢复:用户问"第三项呢",agent 看到的是混入多轮内容的文本块,无法定位"第三项"指哪条。
更隐蔽的是顺序错位:合并模型下,追问与其他消息的相对顺序也可能失真——迟到的追问被并进先到的文本里,agent 甚至无法判断追问发生在哪条消息之后。追问越是精准、越依赖上下文锚点,合并模型的损失越大。这类问题不报错、不留痕,用户只会觉得 agent"听不懂话"。
从用户体感看,问题表现为“我回复了它的消息,它却把回复当成新话题”:追问被当作独立消息处理后,agent 丢失锚点,给出与上下文无关的回复,用户需要再次澄清,对话轮次成倍增加。修复的价值不只是路由正确,更是对话效率。
二、特性设计:识别指向性,路由到独立队列
2.1 消息事件的上下文标志
gateway/platforms/base.py 为 MessageEvent 数据类新增 _is_contextual_followup: bool = False 字段。这个标志是后续路由决策的依据:普通消息保持 False,走既有路径;被判定为追问的消息置 True,走独立路径。新增字段默认 False,保证既有消息构造路径零改动——这是对既有代码最小的侵入方式,任何未感知该字段的旧路径都不会受影响。
字段级别的扩展还意味着序列化与持久化链路无需变动:MessageEvent 若被缓存、落盘或跨进程传递,新增的布尔字段不改变既有格式的兼容性。
2.2 三段式识别与解析
gateway/run.py 新增三组状态与三个辅助方法,构成完整的识别链:
- 状态:
_sent_message_ids集合(记录 agent 已发送的消息 ID)与_message_context_map字典(消息 ID → 上下文键),为判定提供事实依据; _is_our_bot_message:判定用户回复的目标是否属于 agent 自己发送的消息——回复第三方消息、系统消息不算追问;_resolve_reply_context:解析回复指向的具体消息及其上下文,把"回复的是哪条"变成可用的上下文锚点;_build_context_key:为消息构建可比较的上下文键,用于关联匹配,让不同形态的消息引用能落到同一把键上。
识别逻辑接入 _handle_active_session_busy_message:会话忙碌路径上,先判断到达消息是否为对 agent 消息的回复,命中则走追问路由,而不是直接合并进单槽文本。识别发生在消息进入处理管线的最前端,追问的指向性在第一时间被捕获,之后无论排队多久都不丢失。
识别逻辑只运行在活跃会话的忙路径上:空闲会话不涉及合并,不需要路由决策,识别零开销;识别链的三步(判定归属 → 解析上下文 → 构建键)是顺序执行的确定性逻辑,不依赖模型判断,可测试、可回归。
2.3 路由去向:FIFO 溢出队列
命中判定后的追问被路由到 _queued_events(FIFO 溢出队列),与 _pending_messages 单槽文本分离。FIFO 语义保证追问按到达顺序处理、不与其他文本合并,指向性信息得以保留到处理时刻。选择 FIFO 队列而非独立消息通道,是因为追问本质仍是消息流的一部分:它需要排队,但不能与普通消息互相污染。顺带修复了合并参数:merge_text=True 改为 merge_text=False,从根源上停止追问与主文本的合并行为——修复的不是单个分支,而是合并模型的默认行为。
追问走后,单槽文本的职责收敛为“普通消息合并”,两类消息的语义彻底分离:普通消息按序处理,追问按上下文关联处理。后续处理环节可以依据 _is_contextual_followup 标志分别对待,而无需再从合并文本里反向拆解。
2.4 零配置变更
PR 明确声明无需配置变更:识别与路由是行为层改进,不引入新开关。用户升级后直接获得更准确的追问处理,这是这类修复最友好的形态——正确性提升不以配置负担为代价。也意味着该行为对所有网关用户统一生效,没有灰度开关可以按用户关闭。
零配置的代价是行为变更对所有用户同时生效:依赖旧合并行为的下游逻辑如果没有被 402 项测试覆盖到,会在真实环境中暴露——这是零配置设计的固有风险,需要在发布后观察。
三、实测结果
PR body 记录的回归测试数据:
| 测试套件 | 结果 |
|---|---|
| tests/test_hermes_state.py | 267 passed |
| tests/test_lazy_session_regressions.py + tests/honcho_plugin/test_session.py | 135 passed |
| 合计 | 402 passed |
回归测试全部通过,确认路由改动未破坏既有会话状态机与懒加载会话的行为。402 项测试的覆盖面说明改动虽然只有 56 行增量,但触及的消息处理路径经过了既有测试体系的完整回归。追问路由在真实平台回复链路上的端到端表现未在 PR body 中记录独立验证数据。
402 项测试的构成值得注意:覆盖会话状态机与懒加载会话两大块,与改动所在的活跃会话路径(状态机核心)直接相关,且包含 honcho 插件会话的回归——多会话形态都经过了验证,回归覆盖的针对性较好。
四、能力边界
- 识别依赖平台转发"回复"关系(引用/回复目标消息 ID):平台不转发回复语义时,追问无法被识别,退回合并路径——该特性在回复关系完整平台上价值最大;
- 路由到 FIFO 队列后仍存在排队延迟,繁忙会话中追问不会插队,时效性受队列长度影响;
- 识别只覆盖"回复 agent 自己的消息"这一形态:回复第三方消息、回复系统消息、仅引用不回复等场景不在本次范围内;
- 合并行为修复(merge_text 参数)改变了既有的消息合并语义,依赖旧合并行为的下游逻辑需要回归验证(测试已覆盖主路径);
- 改动集中在网关活跃会话路径,非活跃会话的追问处理不在本次范围;多平台之间回复语义的差异处理未被 PR body 记录。
- 识别依赖 _sent_message_ids 记录网关视角的已发送消息,平台同步延迟可能导致判定窗口偏差(消息已发但集合未更新);
- 追问携带的被回复消息内容未注入 agent 提示词,路由只保留“指向性”语义,模型仍看不到被回复消息的原文。
五、PR 信息
- PR 地址:https://github.com/NousResearch/hermes-agent/pull/47043
- 改动规模:+56 / -1,2 个文件(gateway/platforms/base.py +7,gateway/run.py +49/-1)
- 状态:closed
- 提交时间:2026-06-16
本文记录 x7peeps 向 Hermes Agent 上游贡献的特性研究,所有数据来自 PR 实测记录。