智能体死循环熔断 #122

Merged
Yushu merged 3 commits from feat/117-agent-loop-breaker into main 2026-08-13 01:28:23 +00:00
Member

Closes #117

实现

per-chat 连续智能体源(is_bot)输入计数 + 阈值拦截,防两个机器人账号互相触发无限对话循环——落点在两级门控之前(级一门控对事件触发恒 +inf 直通,拦不住)。

  • agent_loop_breaker.pynext_consecutive_bot_count/should_break 纯函数。
  • 状态 _consecutive_bot_message_counts 落在 runtime_state.py(跨簇,反应式消息与环境信号编排共用)。
  • 消息侧(entry_reactive.py::_handle_reactive_message)与环境信号侧(environment_pipeline.py::_process_environment_signal)都接了线;EnvironmentSignal 新增 is_bot: bool = False 字段,为真时跳过 tier-1/tier-2、只计入熔断计数。
  • 阈值 arise_agent_loop_breaker_threshold(默认 5)归静态 config(ADR-0011 阈值三层分类表已列出这一项)。
  • design.md 真人感层补条目,ADR-0011/ADR-0014 各补一个更新节。

两轴 review 发现与修正

两轴 review(sonnet)跑出 3 条发现,adversarial verify 后 2 条确认、1 条证伪:

  1. [HIGH,已修] _handle_interaction_event(戳一戳/表情回应,target_is_self=True 分支)当初完全没有接线到熔断——它走的是 to_interaction 返回的 ConversationInput.is_bot,与反应式消息共用同一份状态,但只有消息那一条入口真的读了它。两个 bot 账号互相戳一戳、其中一个绑了"戳回去"的工具调用,就是这条要防的无限循环,之前完全没有防护。修法:抽出 _loop_breaker_tripped 供两条反应式入口共用,避免再出现"只写了一处"的半截落地;同时改掉已被证伪的"这里是唯一能在两级门控前拦截的位置"注释。补了三条回归测试(单独触发拦截、真人戳一戳不受影响、消息侧与交互侧共享同一份计数)。
  2. [LOW,已修] environmental_awareness.pyEnvironmentSignal.is_bot 文档一处拼写缺分隔符("messageis_bot")。
  3. [LOW,证伪] environment_pipeline.pyshould_break() 只影响日志、不产生行为分支——核实后是有意为之(bot 源信号无条件跳过 tier-1/tier-2,不看阈值),函数自己的 docstring 已经写明,也有测试钉住,未改动。

已知缺口(记录在案,非本票范围)

第三方交互原语(target_is_self=False)转成的 EnvironmentSignal 目前恒 is_bot=False——InteractionEvent 没有携带"发起方是不是机器人账号"这个信息。要补上需要先扩 InteractionEvent 的契约,超出本片范围,留给后续按真实需求评估(EnvironmentSignal.is_bot 文档与 ADR-0014 更新节均已写明)。

测试

  • 全量测试通过(1707 passed;test_debounce_e2e.py 一个测试仅在满负载下偶发 flaky,隔离运行必过,与本票改动无关,是已知的 pre-existing flake)。
  • ruff check 通过。
  • 变异测试验证了消息侧、环境信号侧、交互原语侧三处新守卫(分别 mutate 后确认对应测试变红,还原后 md5 校验一致)。
Closes #117 ## 实现 per-chat 连续智能体源(`is_bot`)输入计数 + 阈值拦截,防两个机器人账号互相触发无限对话循环——落点在两级门控之前(级一门控对事件触发恒 `+inf` 直通,拦不住)。 - `agent_loop_breaker.py`:`next_consecutive_bot_count`/`should_break` 纯函数。 - 状态 `_consecutive_bot_message_counts` 落在 `runtime_state.py`(跨簇,反应式消息与环境信号编排共用)。 - 消息侧(`entry_reactive.py::_handle_reactive_message`)与环境信号侧(`environment_pipeline.py::_process_environment_signal`)都接了线;`EnvironmentSignal` 新增 `is_bot: bool = False` 字段,为真时跳过 tier-1/tier-2、只计入熔断计数。 - 阈值 `arise_agent_loop_breaker_threshold`(默认 5)归静态 config(ADR-0011 阈值三层分类表已列出这一项)。 - `design.md` 真人感层补条目,ADR-0011/ADR-0014 各补一个更新节。 ## 两轴 review 发现与修正 两轴 review(sonnet)跑出 3 条发现,adversarial verify 后 2 条确认、1 条证伪: 1. **[HIGH,已修]** `_handle_interaction_event`(戳一戳/表情回应,`target_is_self=True` 分支)当初完全没有接线到熔断——它走的是 `to_interaction` 返回的 `ConversationInput.is_bot`,与反应式消息共用同一份状态,但只有消息那一条入口真的读了它。两个 bot 账号互相戳一戳、其中一个绑了"戳回去"的工具调用,就是这条要防的无限循环,之前完全没有防护。修法:抽出 `_loop_breaker_tripped` 供两条反应式入口共用,避免再出现"只写了一处"的半截落地;同时改掉已被证伪的"这里是唯一能在两级门控前拦截的位置"注释。补了三条回归测试(单独触发拦截、真人戳一戳不受影响、消息侧与交互侧共享同一份计数)。 2. **[LOW,已修]** `environmental_awareness.py` 里 `EnvironmentSignal.is_bot` 文档一处拼写缺分隔符("messageis_bot")。 3. **[LOW,证伪]** `environment_pipeline.py` 里 `should_break()` 只影响日志、不产生行为分支——核实后是有意为之(bot 源信号无条件跳过 tier-1/tier-2,不看阈值),函数自己的 docstring 已经写明,也有测试钉住,未改动。 ## 已知缺口(记录在案,非本票范围) 第三方交互原语(`target_is_self=False`)转成的 `EnvironmentSignal` 目前恒 `is_bot=False`——`InteractionEvent` 没有携带"发起方是不是机器人账号"这个信息。要补上需要先扩 `InteractionEvent` 的契约,超出本片范围,留给后续按真实需求评估(`EnvironmentSignal.is_bot` 文档与 ADR-0014 更新节均已写明)。 ## 测试 - 全量测试通过(1707 passed;`test_debounce_e2e.py` 一个测试仅在满负载下偶发 flaky,隔离运行必过,与本票改动无关,是已知的 pre-existing flake)。 - `ruff check` 通过。 - 变异测试验证了消息侧、环境信号侧、交互原语侧三处新守卫(分别 mutate 后确认对应测试变红,还原后 md5 校验一致)。
design.md 唯一被标"高·生产事故级"的缺口,实测全仓零实现:级一门控对
事件触发恒+inf直通,拦不住两个bot账号互相@触发的无限对话循环;后果
不是"真人被降级"(reply池protected),是记忆注入静默消失(embedding
横切池被刷穿)。

新增 agent_loop_breaker.py 纯函数(连续is_bot计数+阈值判定)+
runtime_state.py 跨簇进程级状态(仿_last_group_chat_activity先例)+
静态config阈值(ADR-0011阈值三层分类明文归属)。落点在两级门控之前:
- 消息侧:entry_reactive.py 入口最早处拦截
- 环境信号侧:EnvironmentSignal 新增 is_bot 字段,bot源信号跳过
  tier-1/tier-2、只计入熔断计数(design.md"智能体源只计死循环熔断"
  此前是没有实现支撑的悬空声明)
target_is_self=True 的戳一戳/表情回应分支(_handle_interaction_event)
此前完全没有接线到死循环熔断,走的是 ConversationInput.is_bot(与反应
式消息共用同一份状态),不受 InteractionEvent 缺字段那条已知限制约束。
两个 bot 账号互戳、其中一个绑了工具调用会形成本 issue 要防的无限循环,
之前毫无防护。抽出 _loop_breaker_tripped 供两条反应式入口共用,避免再
出现"只有一处真的接线"的半截落地;同时改掉已被证伪的"唯一拦截点"注释,
补齐修复前完全没有覆盖的回归测试,并修正 environmental_awareness.py
文档里的一处拼写(messageis_bot 缺分隔符)。
用户指出 is_bot 在 OneBot v11/QQ 场景(常见的是用户账号跑机器人程序的
userbot 桥接)里基本判不出来——协议本身不携带这个信号,本仓唯一的参照
适配器只能恒 False。这不是 issue #117 引入的缺陷(is_bot 是 issue #70 就
有的既有字段),而是这类协议本身的能力边界,此前散落在各消费方文档里
没有讲透。把权威解释收敛到 ConversationInput.is_bot 字段本身(issue #117
新增消费方 agent_loop_breaker.py 指回来),并点出对 Telegram Bot API 这类
原生带 User.is_bot 的平台,机制本身立刻可用,不算白写。
Yushu merged commit 9011dfae23 into main 2026-08-13 01:28:23 +00:00
Yushu deleted branch feat/117-agent-loop-breaker 2026-08-13 01:28:24 +00:00
Sign in to join this conversation.
No description provided.