feat: 离线反思闭环 + 人设漂移守护(issue #41) #50

Merged
Yushu merged 1 commit from feature/41-offline-reflection-persona-drift into main 2026-07-24 01:33:59 +00:00
Member

这个 PR 做了什么

实现 issue #41「离线反思闭环 + 人设漂移守护」(ADR-0006/0021)。这是目前
Phase 4 里范围最大的一片,落地前先跟用户核实了三个 ADR 没钉死的关键实现
决策(见下)。

核实过的三个关键决策

  1. 证据指针怎么落地:ADR 只说"决策快照"要写 tracing,但目前代码里
    连一张持久化的决策快照表都没有(只有 logger.debug)。排除了"零成本
    复用 Sent Log"(它现在没有 chat_id/时间戳/触发类型字段,字面意义上做
    不到),也排除了"顺带把 ADR-0010 完整决策快照建起来"(工作量超出本
    ticket 范围)。选择新建一张精简的"主动尝试记录"表(只服务反思:
    chat_id + 发送时间 + Sent Log 句柄引用 + 发送时的 valence 快照),判定
    完即删除,不是永久审计日志。
  2. hit-rate 追踪哪些发送:钉死为 Drive Tick/Recall 主动重连发送
    ——判定"是否已回应"复用的 ReconnectCooldownState 两段式状态目前只在
    这条路径上维护,tier-2 环境信号(issue #39)没有对应的"等回应"窗口
    概念,暂不纳入。
  3. 技能建多大只建写入侧(存储 + 向量查重合并),不建
    inject_skills 检索注入工具、不做三级渐进披露——检索侧留给真正需要
    消费它的后续 ticket(目前 issue 列表里没有)。

反坍缩机制(proactivity_offset.py

不维护任何"命中率"比值/窗口计数器——每次结局都是独立、有界、不对称步长
的即时调整:landed 小步松弛(step_down),fell-flat/annoyed 大步
抬高安静倾向(step_up,噪声坍缩是主风险,ceiling 承重)。无信号时只对
"比基值话痨"的负值衰减回 0,不会主动把安静状态推向话痨,也不会因为"从不
尝试"本身被解读成"应该更安静"——用一组序列模拟直接验证两种退化解都被
目标函数本身挡住(test_low_hit_rate_high_volume_ends_up_quieter_than_high_hit_rate_low_volume
等测试),不是靠额外检测逻辑。

结局判定不读原始回复文本

Delta 缓冲会被 Delta 压缩周期自己的调度 drain 走,到反思批处理跑的时候
时序不可靠。改比较"发送时"与"判定时"两个时间点的快情感态 valence:明显
下降视为 annoyed,否则视为 landed;距发送已久仍无回应直接判
fell-flat(零 LLM,沉默本身自证结局)。

共享结局分类器首次真正接线

Outcomelanded/fell-flat/annoyed)和 update_fast_state/
update_slow_user_stateoutcome 参数在 issue #34 就建好了,但从来
没有真实生产调用方传过非 None 的值。本 PR 是第一个真正驱动它的入口:
dominance 快路(当轮)+ per-user 慢情感态.dominance(更慢路)都在反思
批处理判定结局的同一时刻更新。

人设漂移守护(persona_drift_guard.py

独立 LLM-as-judge 调用——全新的 messages 列表,不承接产出候选那条推理链
(防"自我橡皮图章")。喂 persona port 完整结构化人设做二元判断,通过才
真正落库;判否/调用失败一律 fail-closed 丢弃候选,不影响其它候选的判断
结果(画像候选和技能候选各自独立过一次)。管辖范围按 AC 明确要求扩到
Delta 既有的 Knowledge 写入路径——run_delta_compression_cycle 新增可选
persona 参数,缺省 None 时行为与之前完全一致(不接入否决闸,已有测试
套件零改动),真实生产(__init__.py)恒传入。

技能(skill.py,写入侧)

Qdrant 直接当唯一落盘位置,不建 Postgres 表(技能"本来就活在 Qdrant
里")。查重合并复用 Knowledge 的确定性合并策略(新内容不是已有内容的
子串才拼接),只是匹配信号从关键词交集换成向量相似度。

新增/改动文件

  • 新增:proactive_attempt.py(待判定队列)、proactivity_offset.py
    (反坍缩机制)、skill.py(写入侧存储)、persona_drift_guard.py
    (否决闸)、reflection.py(LLM 提取 + 批处理编排)
  • gating.pyevaluate_gate 新增 proactivity_offset 参数,叠加在
    情感态基值之上
  • delta.pyrun_delta_compression_cycle 新增可选 persona 参数
  • storage.py/sent_log.py:三张新表/结构(arise_proactive_attempt/
    arise_proactivity_offset + Qdrant arise_skill collection)的内存版
    • 持久化版实现
  • __init__.py:Drive Tick 发送成功后记录主动尝试;新增
    arise_reflection 调度任务;_evaluate_unified_gate 现读现衰减
    offset 传给 evaluate_gate
  • config.py:新增反思/offset/技能相关的静态阈值配置

测试

新增 9 个测试文件,覆盖纯函数(含反坍缩性质测试)、存储契约(内存版 +
持久化版共用测试)、独立判断调用(含调用失败 fail-closed)、完整批处理
编排(12 个场景:awaiting 跳过、cold/replied 两条结局分支、offset 方向、
dominance 更新、候选落库/丢弃、LLM 失败仍不阻塞核心机制、多尝试并发处理)

  • Drive Tick e2e 记录验证 + Delta 漂移守护retrofit 的 5 个新场景。

全部 751 个测试通过,ty check/ruff check 干净。

## 这个 PR 做了什么 实现 issue #41「离线反思闭环 + 人设漂移守护」(ADR-0006/0021)。这是目前 Phase 4 里范围最大的一片,落地前先跟用户核实了三个 ADR 没钉死的关键实现 决策(见下)。 ### 核实过的三个关键决策 1. **证据指针怎么落地**:ADR 只说"决策快照"要写 tracing,但目前代码里 连一张持久化的决策快照表都没有(只有 `logger.debug`)。排除了"零成本 复用 Sent Log"(它现在没有 chat_id/时间戳/触发类型字段,字面意义上做 不到),也排除了"顺带把 ADR-0010 完整决策快照建起来"(工作量超出本 ticket 范围)。选择**新建一张精简的"主动尝试记录"表**(只服务反思: chat_id + 发送时间 + Sent Log 句柄引用 + 发送时的 valence 快照),判定 完即删除,不是永久审计日志。 2. **hit-rate 追踪哪些发送**:钉死为 **Drive Tick/Recall 主动重连发送** ——判定"是否已回应"复用的 `ReconnectCooldownState` 两段式状态目前只在 这条路径上维护,tier-2 环境信号(issue #39)没有对应的"等回应"窗口 概念,暂不纳入。 3. **技能建多大**:**只建写入侧**(存储 + 向量查重合并),不建 `inject_skills` 检索注入工具、不做三级渐进披露——检索侧留给真正需要 消费它的后续 ticket(目前 issue 列表里没有)。 ### 反坍缩机制(`proactivity_offset.py`) 不维护任何"命中率"比值/窗口计数器——每次结局都是独立、有界、不对称步长 的即时调整:`landed` 小步松弛(`step_down`),`fell-flat`/`annoyed` 大步 抬高安静倾向(`step_up`,噪声坍缩是主风险,ceiling 承重)。无信号时只对 "比基值话痨"的负值衰减回 0,不会主动把安静状态推向话痨,也不会因为"从不 尝试"本身被解读成"应该更安静"——用一组序列模拟直接验证两种退化解都被 目标函数本身挡住(`test_low_hit_rate_high_volume_ends_up_quieter_than_high_hit_rate_low_volume` 等测试),不是靠额外检测逻辑。 ### 结局判定不读原始回复文本 Delta 缓冲会被 Delta 压缩周期自己的调度 drain 走,到反思批处理跑的时候 时序不可靠。改比较"发送时"与"判定时"两个时间点的快情感态 valence:明显 下降视为 `annoyed`,否则视为 `landed`;距发送已久仍无回应直接判 `fell-flat`(零 LLM,沉默本身自证结局)。 ### 共享结局分类器首次真正接线 `Outcome`(`landed`/`fell-flat`/`annoyed`)和 `update_fast_state`/ `update_slow_user_state` 的 `outcome` 参数在 issue #34 就建好了,但从来 没有真实生产调用方传过非 `None` 的值。本 PR 是第一个真正驱动它的入口: dominance 快路(当轮)+ per-user 慢情感态.dominance(更慢路)都在反思 批处理判定结局的同一时刻更新。 ### 人设漂移守护(`persona_drift_guard.py`) 独立 LLM-as-judge 调用——全新的 messages 列表,不承接产出候选那条推理链 (防"自我橡皮图章")。喂 persona port 完整结构化人设做二元判断,通过才 真正落库;判否/调用失败一律 fail-closed 丢弃候选,不影响其它候选的判断 结果(画像候选和技能候选各自独立过一次)。管辖范围按 AC 明确要求扩到 Delta 既有的 Knowledge 写入路径——`run_delta_compression_cycle` 新增可选 `persona` 参数,缺省 `None` 时行为与之前完全一致(不接入否决闸,已有测试 套件零改动),真实生产(`__init__.py`)恒传入。 ### 技能(`skill.py`,写入侧) Qdrant 直接当唯一落盘位置,不建 Postgres 表(技能"本来就活在 Qdrant 里")。查重合并复用 Knowledge 的确定性合并策略(新内容不是已有内容的 子串才拼接),只是匹配信号从关键词交集换成向量相似度。 ### 新增/改动文件 - 新增:`proactive_attempt.py`(待判定队列)、`proactivity_offset.py` (反坍缩机制)、`skill.py`(写入侧存储)、`persona_drift_guard.py` (否决闸)、`reflection.py`(LLM 提取 + 批处理编排) - `gating.py`:`evaluate_gate` 新增 `proactivity_offset` 参数,叠加在 情感态基值之上 - `delta.py`:`run_delta_compression_cycle` 新增可选 `persona` 参数 - `storage.py`/`sent_log.py`:三张新表/结构(`arise_proactive_attempt`/ `arise_proactivity_offset` + Qdrant `arise_skill` collection)的内存版 + 持久化版实现 - `__init__.py`:Drive Tick 发送成功后记录主动尝试;新增 `arise_reflection` 调度任务;`_evaluate_unified_gate` 现读现衰减 offset 传给 `evaluate_gate` - `config.py`:新增反思/offset/技能相关的静态阈值配置 ### 测试 新增 9 个测试文件,覆盖纯函数(含反坍缩性质测试)、存储契约(内存版 + 持久化版共用测试)、独立判断调用(含调用失败 fail-closed)、完整批处理 编排(12 个场景:awaiting 跳过、cold/replied 两条结局分支、offset 方向、 dominance 更新、候选落库/丢弃、LLM 失败仍不阻塞核心机制、多尝试并发处理) + Drive Tick e2e 记录验证 + Delta 漂移守护retrofit 的 5 个新场景。 全部 751 个测试通过,`ty check`/`ruff check` 干净。
给主动尝试建一条零 LLM 的反坍缩护栏:Drive Tick 每次真正发送都记一条
待判定的主动尝试(chat_id + 发送时间 + 发送时的快情感态 valence 快照),
离线反思批处理定时扫描——距发送已久仍无回应直接判 fell-flat,已获回应
则比较发送时与判定时的 valence 变化判 landed/annoyed(不读原始回复文本,
避免依赖 Delta 缓冲的排空时序)。结局驱动三件事:门控阈值 offset(有界
+ 不对称步长 + 衰减锚回基值,逐次归一,天然挡住沉默/噪声两种坍缩退化解,
用序列模拟直接验证)、dominance 快路/per-user 慢路(issue #34/#37 建好的
共享结局分类器首次真正接线)、以及一次 LLM 调用写自然语言教训 + 按需的
画像/技能候选。

候选落库前都要过人设漂移守护——独立调用(不承接产出候选那条推理链),
喂 persona port 完整人设做二元否决判断,通过才真正写入,判否/调用失败
一律 fail-closed 丢弃。管辖范围按 AC 明确要求扩到 Delta 既有的 Knowledge
写入路径(新增可选 persona 参数,缺省 None 时行为不变,不破坏已有测试)。

技能是全新的写入侧存储(Qdrant 直接当唯一落盘位置,不建 Postgres 表,
不建检索/注入工具——检索侧留给未来有真实读取方的 ticket)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Yushu merged commit 7e30c31e75 into main 2026-07-24 01:33:59 +00:00
Yushu deleted branch feature/41-offline-reflection-persona-drift 2026-07-24 01:34:00 +00:00
Sign in to join this conversation.
No description provided.