issue #133: 反思闭环样本补全——周期触发级一打分+群聊追问结局接入 #139

Merged
Yushu merged 3 commits from feat/133-reflection-cycle-sample-completion into main 2026-08-14 07:57:32 +00:00
Member

Closes #133

实现内容

反思闭环反坍缩护栏(hit-rate + offset)在两处收不到该收的样本,本 PR 补齐。

一、周期触发级一打分(让 offset 真正生效)

_evaluate_drive_tick_chat 此前调用统一门控从不传 level1_score,恒吃
gating.EVENT_TRIGGER_LEVEL1_SCORE+inf)——反思闭环抬高的 offset 拦不住任何周期候选,
护栏本身开路。

  • 新增 proactive_reconnect.periodic_significance_score(trigger_path, *, subject_familiarity, base_weight, timing_override_bonus_weight, familiarity_weight)——不是字面仿
    tier2_significance_score 的"二元代理+熟悉度"两项
    :tier2 的 outcome 二元分量默认权重
    0.6 已经让大多数候选独立跨过默认阈值;周期候选没有天然二元信号(drive_tick/
    immediate_followup 到达该函数前都已过零 LLM 粗筛,粗筛结果对两条路径恒真,不能当区分量用)。
    改为三项:base_weight(两条路径共享基线,默认 0.6,保证 familiarity=0 时默认仍跨过
    默认阈值 0.5——不让新分数默默让 drive-tick 变得默认永不生效,e2e 测试实测钉住)+
    timing_override_bonus_weight(只在 immediate_followup 时加成,默认 0.1——措辞明确不代表
    "模型显式意图",因为 RecallTimingOverride 到期可能来自 expects_reply 也可能来自对方
    上线状态变化)+ familiarity_weight(默认 0.3)
  • _evaluate_drive_tick_chat 在熔断闸检查之后、门控调用之前接入打分并传 level1_score=
  • 三个新 config 字段(静态 config,ADR-0011 阈值三层)
  • gating.py 模块文档/EVENT_TRIGGER_LEVEL1_SCORE/progressive_unlock_scale/
    evaluate_gate docstring、agent_loop_breaker.pyconfig.pygate_pipeline.py 均已
    同步措辞
  • 端到端守卫测试:同一个 drive-tick 候选,offset 默认 0.0 时 proceed,推到 ceiling 后变
    silent_level1——不止测纯函数,钉住链路真的通了
  • 未把 offset 改为作用于沉默预算(AC 明确禁止)
  • 发现的前提缺口:progressive_unlock_scale"消费者集合为空"论证对 drive-tick/私聊即刻追问
    不再完全成立(这两条路径现在也是 level1_score 真实消费者,但没有类似 tier-2 的解锁门槛)
    ——本票不解决,只标注,ADR-0011 补了对应更新节

二、群聊追问结局接入反思闭环

_run_group_followup_check 此前全程不调 record_proactive_attempt。接入前先修两个隐藏坑:

  • 坑1reflection.py::_apply_outcomechat_id.removesuffix(".p") 对群聊 chat_id 是
    静默 no-op,会把整个群 id 当 user_id 写出假的 per-user 慢情感态。改为按
    is_private_chat_id(chat_id) 分流:私聊读写 ("user", user_id) 不变;群聊快层锚点改读
    ("group", chat_id)(只读不写,同 delta.py::_apply_interaction_polarity 既有先例逐字
    一致),完全跳过 per-user 慢层写(AC 给定的二选一框架下唯一没有引入错误归因的答案)
  • 坑2run_reflection_cycle 判"是否已回应"恒用 get_reconnect_cooldown(attempt.chat_id)
    ——私聊有效,但群聊的冷却记录按裸 sender_id 存(服务的是"距上次交互"文本这件完全不同的事),
    查询恒落空、结局恒判 fell-flat。新增 chat_activity.pyChatActivityStoragePort
    per-chat_id 一行,结构同 TypingStatusState 先例):_debounced_flush 在去抖批次 flush
    之后(早于门控判定)双写,不取代 _last_group_chat_activity 那份进程内 dict(继续服务
    _run_group_followup_check/_human_is_active 两个高频消费者);私聊场景不写
  • _run_group_followup_check 接入 record_proactive_attempt,群聊即刻追问结局第一次真正
    计入 hit-rate
  • proactive_attempt.py 模块文档作用域声明已更新

测试与验证

  • 变异测试验证 6 处新增/改动守卫全部可被单独抓住;其中一处首次尝试未被既有测试抓住(两个
    scope 都未 seed 时读哪个都退回同一中性零点,无法区分"读对了"还是"巧合看起来正确")——已补
    一条显式 seed 两个 scope 为不同值的回归测试,重新变异验证确认现在能抓住
  • 全量 1794 passing,ruff 干净
  • ty 检查(本地临时装包核验,非项目依赖)47 条诊断全部落在本票未触及的文件,storage.py
    那两条经行号偏移核对(+30 行,与本票新增代码量吻合)是 issue #127 就已存在的旧噪音

两轴 review

Standards 维度发现并经对抗校验确认两处真实的文档不一致(均非功能性 bug):
proactive_attempt.py::attempt_status() 自己的 docstring 没有随模块级文档一起更新群聊来源;
gate_pipeline.py::_evaluate_unified_gate()level1_score 编排层入口,本票 diff 完全没碰
这个文件)的 docstring 仍只提 tier-2 消费者。已在本 PR 内以单独 fix commit 修正。

文档

docs/adr/0006-agency-and-drive.md/docs/adr/0011-technology-selection.md 已补更新节
(docs 分支本地提交,按约定不推送)。CONTEXT.md/design.md 核实后确认无需改动——既有描述在
实现前就准确描述了目标状态。

🤖 Generated with Claude Code

Closes #133 ## 实现内容 反思闭环反坍缩护栏(hit-rate + offset)在两处收不到该收的样本,本 PR 补齐。 ### 一、周期触发级一打分(让 offset 真正生效) `_evaluate_drive_tick_chat` 此前调用统一门控从不传 `level1_score`,恒吃 `gating.EVENT_TRIGGER_LEVEL1_SCORE`(`+inf`)——反思闭环抬高的 offset 拦不住任何周期候选, 护栏本身开路。 - 新增 `proactive_reconnect.periodic_significance_score(trigger_path, *, subject_familiarity, base_weight, timing_override_bonus_weight, familiarity_weight)`——**不是字面仿 `tier2_significance_score` 的"二元代理+熟悉度"两项**:tier2 的 `outcome` 二元分量默认权重 0.6 已经让大多数候选独立跨过默认阈值;周期候选没有天然二元信号(drive_tick/ immediate_followup 到达该函数前都已过零 LLM 粗筛,粗筛结果对两条路径恒真,不能当区分量用)。 改为三项:`base_weight`(两条路径共享基线,默认 0.6,保证 `familiarity=0` 时默认仍跨过 默认阈值 0.5——不让新分数默默让 drive-tick 变得默认永不生效,e2e 测试实测钉住)+ `timing_override_bonus_weight`(只在 immediate_followup 时加成,默认 0.1——措辞明确不代表 "模型显式意图",因为 `RecallTimingOverride` 到期可能来自 `expects_reply` 也可能来自对方 上线状态变化)+ `familiarity_weight`(默认 0.3) - `_evaluate_drive_tick_chat` 在熔断闸检查之后、门控调用之前接入打分并传 `level1_score=` - 三个新 config 字段(静态 config,ADR-0011 阈值三层) - `gating.py` 模块文档/`EVENT_TRIGGER_LEVEL1_SCORE`/`progressive_unlock_scale`/ `evaluate_gate` docstring、`agent_loop_breaker.py`、`config.py`、`gate_pipeline.py` 均已 同步措辞 - 端到端守卫测试:同一个 drive-tick 候选,offset 默认 0.0 时 `proceed`,推到 ceiling 后变 `silent_level1`——不止测纯函数,钉住链路真的通了 - 未把 offset 改为作用于沉默预算(AC 明确禁止) - 发现的前提缺口:`progressive_unlock_scale`"消费者集合为空"论证对 drive-tick/私聊即刻追问 不再完全成立(这两条路径现在也是 `level1_score` 真实消费者,但没有类似 tier-2 的解锁门槛) ——本票不解决,只标注,ADR-0011 补了对应更新节 ### 二、群聊追问结局接入反思闭环 `_run_group_followup_check` 此前全程不调 `record_proactive_attempt`。接入前先修两个隐藏坑: - **坑1**:`reflection.py::_apply_outcome` 的 `chat_id.removesuffix(".p")` 对群聊 chat_id 是 静默 no-op,会把整个群 id 当 `user_id` 写出假的 per-user 慢情感态。改为按 `is_private_chat_id(chat_id)` 分流:私聊读写 `("user", user_id)` 不变;群聊快层锚点改读 `("group", chat_id)`(只读不写,同 `delta.py::_apply_interaction_polarity` 既有先例逐字 一致),完全跳过 per-user 慢层写(AC 给定的二选一框架下唯一没有引入错误归因的答案) - **坑2**:`run_reflection_cycle` 判"是否已回应"恒用 `get_reconnect_cooldown(attempt.chat_id)` ——私聊有效,但群聊的冷却记录按裸 `sender_id` 存(服务的是"距上次交互"文本这件完全不同的事), 查询恒落空、结局恒判 `fell-flat`。新增 `chat_activity.py`(`ChatActivityStoragePort`, per-chat_id 一行,结构同 `TypingStatusState` 先例):`_debounced_flush` 在去抖批次 flush 之后(早于门控判定)双写,**不取代** `_last_group_chat_activity` 那份进程内 dict(继续服务 `_run_group_followup_check`/`_human_is_active` 两个高频消费者);私聊场景不写 - `_run_group_followup_check` 接入 `record_proactive_attempt`,群聊即刻追问结局第一次真正 计入 hit-rate - `proactive_attempt.py` 模块文档作用域声明已更新 ## 测试与验证 - 变异测试验证 6 处新增/改动守卫全部可被单独抓住;其中一处首次尝试未被既有测试抓住(两个 scope 都未 seed 时读哪个都退回同一中性零点,无法区分"读对了"还是"巧合看起来正确")——已补 一条显式 seed 两个 scope 为不同值的回归测试,重新变异验证确认现在能抓住 - 全量 1794 passing,ruff 干净 - ty 检查(本地临时装包核验,非项目依赖)47 条诊断全部落在本票未触及的文件,`storage.py` 那两条经行号偏移核对(+30 行,与本票新增代码量吻合)是 issue #127 就已存在的旧噪音 ## 两轴 review Standards 维度发现并经对抗校验确认两处真实的文档不一致(均非功能性 bug): `proactive_attempt.py::attempt_status()` 自己的 docstring 没有随模块级文档一起更新群聊来源; `gate_pipeline.py::_evaluate_unified_gate()`(`level1_score` 编排层入口,本票 diff 完全没碰 这个文件)的 docstring 仍只提 tier-2 消费者。已在本 PR 内以单独 fix commit 修正。 ## 文档 `docs/adr/0006-agency-and-drive.md`/`docs/adr/0011-technology-selection.md` 已补更新节 (docs 分支本地提交,按约定不推送)。CONTEXT.md/design.md 核实后确认无需改动——既有描述在 实现前就准确描述了目标状态。 🤖 Generated with [Claude Code](https://claude.com/claude-code)
一、周期触发级一打分(让offset真正生效):
- 新增 proactive_reconnect.periodic_significance_score(三项加权:
  base_weight共享基线+timing_override_bonus_weight短窗口追问加成+
  familiarity_weight连续熟悉度),配套三个config权重项
- _evaluate_drive_tick_chat 接线 level1_score=,此前恒吃 +inf 默认值,
  反思闭环抬高的offset拦不住任何周期候选——现在真正生效
- 公式不是字面仿tier2的"二元代理+熟悉度"两项:tier2的outcome二元分量
  是"是否非噪声"的粗筛代理,默认权重0.6已让大多数候选独立跨过默认阈值;
  周期候选没有这样一个天然二元信号(drive_tick/immediate_followup到达
  本函数前都已过零LLM粗筛,粗筛结果对两条路径恒真,不能当区分量用),
  改为base_weight是两条路径共享基线(默认0.6,保证familiarity=0时默认
  仍跨过默认阈值0.5,不让新分数默默让drive_tick变得默认永不生效——
  e2e测试实测钉住这条:offset默认0.0时候选照常proceed,推到ceiling后
  才变silent_level1)
- gating.py/config.py/agent_loop_breaker.py 补文档措辞("周期触发默认
  满分直通"已成过去式);发现的premise缺口(progressive_unlock_scale的
  "消费者集合为空"论证对drive_tick/私聊即刻追问不再完全成立)已标注
  但不在本票展开,留后续ticket评估

二、群聊追问结局接入反思闭环(先修两个隐藏坑):
- bug1: reflection.py::_apply_outcome 的 chat_id.removesuffix(".p") 对
  群聊静默no-op会把整个群id当user_id写出假的per-user慢情感态——改为
  按is_private_chat_id(chat_id)分流:私聊读写("user",user_id)不变;
  群聊快层锚点改读("group",chat_id)(同delta.py::
  _apply_interaction_polarity既有先例,只读不写),完全跳过per-user
  慢层写(AC给定的二选一框架下唯一没有引入错误归因的答案)
- bug2: run_reflection_cycle 判"是否已回应"对群聊恒用
  get_reconnect_cooldown(attempt.chat_id)——群聊冷却记录按裸sender_id
  存,key对不上,永远查不到、恒判cold。新增 chat_activity.py
  持久化port(双写,不取代_last_group_chat_activity那份进程内dict——
  _run_group_followup_check/_human_is_active两个高频消费者继续用它),
  _debounced_flush在去抖批次flush后写入,run_reflection_cycle群聊分支
  改读这份持久化信号
- entry_reactive.py::_run_group_followup_check 接入
  record_proactive_attempt,群聊即刻追问结局第一次真正计入hit-rate
- proactive_attempt.py 模块文档范围声明同步更新

三、变异测试+全量测试+ruff均已在实现过程中反复验证,全量1793 passing。
ADR-0006/0011/0014更新节待补(docs分支,另行提交)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
变异测试把 _apply_outcome 的私聊/群聊 scope 分流改回恒用 "user" scope
后,既有测试全绿——"不写 user scope"这条断言在两个 scope 都未被 seed
过时无法区分读的是哪一个(都退回同一个中性零点)。新增测试显式 seed
group/user 两个 scope 为截然不同的 valence 值,钉住群聊分支真的从
"group" scope 读快层锚点,不是巧合地看起来正确。

变异测试全部 6 处新增/改动守卫(打分公式 base_weight、level1_score
接线、_apply_outcome scope 分流、run_reflection_cycle 信号源分流、
群聊 record_proactive_attempt 接入、chat_activity 私聊写入守卫)均
可被单独抓住,全部文件已用哈希核验确认变异已完整还原。全量 1794
passing,ruff/ty(本地装包核验,非项目依赖)均干净——ty 报出的 47
条诊断全部落在本票未触及的文件,storage.py 那两条经行号偏移核对是
issue #127 就已存在的旧噪音。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
两轴review的Standards维度独立发现并经对抗校验确认两处真实的文档
不一致(均非功能性bug,纯docstring落后于实现):

- proactive_attempt.py::attempt_status() 自己的docstring仍把
  last_interaction_at的来源钉死在ReconnectCooldownState——同文件的
  模块级docstring已经讲清楚私聊/群聊分流来源,但函数自己的docstring
  没跟着改,与模块级说明自相矛盾
- gate_pipeline.py::_evaluate_unified_gate() 的docstring仍把
  level1_score覆盖参数描述成只有tier-2在用——这个函数是drive-tick/
  私聊即刻追问传level1_score的编排层入口,但本票diff完全没碰这个
  文件,gating.py里三处已经同步更新的措辞在这里断了档

全量1794 passing,ruff干净。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Yushu merged commit 8b77ac7d46 into main 2026-08-14 07:57:32 +00:00
Yushu deleted branch feat/133-reflection-cycle-sample-completion 2026-08-14 07:57:33 +00:00
Sign in to join this conversation.
No description provided.