diff --git a/docs/DEVLOG.md b/docs/DEVLOG.md index de54a5b..4ff62ac 100644 --- a/docs/DEVLOG.md +++ b/docs/DEVLOG.md @@ -4379,3 +4379,11 @@ c03 最初写 `result = df['风向'].value_counts()`。脏化把 1822 条缺失 `verify_holdout.py` 的 `load_raw()` 原来只认 `air` / `rental`,加 `air_report` 时直接 `sys.exit("不认识的 dataset")`。改成 `name in ("air", "air_report")` —— 两者是同一份数据的 两种导出,原始侧对照表相同,差的是脏化表怎么读出来。 + +## [110] 2026-11-02 · 记忆批 3 范围增补(方案)· commit <待提交>(PR #96) + +- 新文件 `docs/roadmap/memory-batch3.md`,规格 §5 批 3 后面加一行指向它。只是方案,没有改代码。 +- 依据 eval-v4 多轮 state 的失败分布:8 道错题和翻转题里,有 4 道是同一个形状——第 1 轮只给约束、不提问(mul_012/020/021/030)。纯文字约束轮不进状态(`working_state.py:485-499`),只活在历史块里,而历史块从最早那一轮开始丢。 +- 范围:B1 用户前提区(用户原话,带轮次,不受窗口和历史预算裁剪),B2 状态带出处轮次,B3 取原文工具 `get_turn(n)`(即 M3b);M2 口径库这批不做,只先钉写入规则(explicit_user、supersedes、软遗忘)。 +- 偏离规格 §3.2:B1 不走 M6 确认,理由见 §2.2。S 已按建议定下 §8 三条:不走 M6 确认;B3 和 B1/B2 放同一个 PR,B3 放最后;前提区先不过滤问句。 +- B1–B3 的先后等 7b24d4f 上的多轮 state 重跑读数,按 §1.3 的决策表定。 diff --git a/docs/roadmap/memory-batch3.md b/docs/roadmap/memory-batch3.md new file mode 100644 index 0000000..a306d57 --- /dev/null +++ b/docs/roadmap/memory-batch3.md @@ -0,0 +1,150 @@ +# 记忆批 3 范围增补(口头称"记忆批 2") + +> 2026-11-02 起草,基线 master `b278193`(含批 1.1 `7b24d4f`)。 +> 编号说明:规格 §5 的"批 2"是评测侧的消融(已由 eval-v3/v4 完成),所以记忆这边的下一批在规格里叫**批 3**。本文就是 §5 批 3 那句"按批 2 的失败分布排序后,再开一个方案增补 PR 定范围"的那个 PR。 +> 本文只定**范围、规则、验收**;实现另开 PR。和代码冲突以代码为准。 + +## 0 结论先说 + +| 编号 | 做什么 | 修哪类失败 | 改 prompt | 建议 | +|---|---|---|---|---| +| B1 | **用户前提区**:用户纯文字给出的约束原样进工作状态,带轮次,不受 12 轮窗口和历史预算裁剪 | 第 1 轮只说约束、第 2 轮才问(mul_012/020/021/030) | 是(state) | **做** | +| B2 | **状态带出处轮次**:每条前提后标 `(第N轮)` | 指代消解、出错追溯 | 是(state) | **做**,和 B1 一起 | +| B3 | **取原文工具**(即规格 M3b):ReAct 加只读工具 `get_turn(n)`,取第 n 轮完整回答 + 最后一次成功代码 | 指代上一轮结果(mul_008/026),"把刚才那张图改成…" | 是(工具表) | **做**,但排在 B1/B2 之后,看重跑读数定 | +| B4 | **M2 口径库最小版**:只收用户明说的口径,版本化,不覆盖 | 口径题 | 是 | **不做**,跟阶段 7 A5,本文只先钉写入规则(§5) | +| — | M4 压缩、M7 改写、M8 workflow 记忆、M9、M10、向量检索 | — | — | 不做,理由见 §6 | + +B1–B3 的先后要等 7b24d4f 上的多轮 state 重跑读数(§1.3 决策表)。 + +## 1 输入:失败分布 + +### 1.1 eval-v4 多轮 state(代码 c53874f,批 1.1 之前) + +三跑 28/26/28,均值 82.8%,执行率 100%(DEVLOG [91])。三跑全错 4 题,翻转 4 题: + +| 题 | 轮次 | 类型 | +|---|---|---| +| mul_012 | ① 只看 2024 年 3 月,重复只算一次 ② 各品类笔数占比 | 纯文字约束 + 月份 | +| mul_030 | ① 只看 2024 年 2 月,重复只算一次 ② 食品类实付合计 | 纯文字约束 + 月份 | +| mul_020(翻) | ① 只看 2024 年 3 月,重复只算一次 ② 其中广州多少笔 | 纯文字约束 + 月份 | +| mul_021(翻) | ① 只看数码类已完成,重复只算一次 ② 平均实付 | 纯文字约束 | +| mul_008 | ① K100 期末库存合计 ② 把它按有记录的天数平均 | 指代上一轮结果 + 口径 | +| mul_026 | ① 哪个部门平均月薪最高 ② 那个部门里绩效 A 多少人 | 指代上一轮结果(名字) | +| mul_028(翻) | ①② 两次拒答 ③ 只算广州金额合计,去重 | 拒答后续问 | +| mul_031(翻) | ① 按产品汇总 ② 再按地区拆一层 | 粒度叠加 | + +**8 题里有 4 题是同一个形状:第 1 轮只给约束、不提问。** 同形状的 mul_010(三条纯文字约束)三跑都对,差别是它没有月份条件。这和批 1.1 的 P1(块头让模型忽略"问"里的要求)、P2(`.str[:7]` 抽不到)对得上,但没有逐题原始输出,不能确证(评测侧已记为"逐题读数不归档")。 + +### 1.2 为什么批 1.1 之后仍然需要 B1 + +批 1.1 修的是"有代码的轮次抽得准"和"模型别忽略用户的问"。纯文字约束轮依然**只活在历史块的"问"里**: + +- `fold()` 对没执行代码的轮次不动状态(`app/memory/working_state.py:485-499`),所以这类约束进不了状态块; +- 历史块最多 12 轮(`snapshots.py:16`),超过预算时从最早一轮开始丢(`snapshots.py:199-203`、`context.py:47-53`)。约束恰恰在最早那一轮,**最先被丢**; +- 教程 §2.2.1 的说法一致:硬约束要进工作记忆,不能只留在对话窗口里。 + +现有 33 题最长 4 轮,测不到窗口裁剪;B1 的验收要自带长会话用例(§4)。 + +### 1.3 决策表(拿到 7b24d4f 的 state 三跑后) + +| 读数 | 含义 | 顺序 | +|---|---|---| +| 012/020/021/030 三跑基本全对 | 批 1.1 已经修了短会话;B1 只为长会话兜底 | B1+B2 → B3 | +| 这 4 题仍有错 | 只靠历史块不够 | B1+B2 优先,验收必须包含这 4 题 | +| 008/026 仍全错 | 指代上一轮结果是独立问题 | B3 提前和 B1 同批 | +| 出现新的错题 | 先归因再定,不硬套本表 | — | + +## 2 B1 用户前提区 + +### 2.1 规则 + +- **来源**:status=ok 的轮次里,**没有执行代码**的用户问题原文。不做自然语言解析,不调模型,原样保留。 +- **写入字段**:`WorkingState.constraints`,元素为 `{"turn": N, "text": 原文}`。 +- **截断**:单条最多 120 字,超过截到 117 加 `…`。 +- **容量**:最多 6 条(规格 §3.2 已定 `constraints ≤ 6`),超了丢最早的一条,`dropped += 1`。 +- **不过期**:后面的轮次执行了代码也不删。它是用户本人说的话,冲突交给模型按"本轮显式指令 > 新近 > 旧"判断,块头写明这条优先级。 +- **渲染**:状态块里单独一段,放在最前面: + +``` +用户此前明确说过的前提(用户原话,除非本轮问题改了它,否则继续有效): +- 第1轮:只看 2024 年 3 月的订单,重复录入的只算一次。 +``` + +- **预算**:先裁 `earlier`,再裁 filters,**最后**才裁前提区;一旦裁了前提区,就在 `trimmed` 里记 `constraints`。 +- **off 模式**不出现,P8 哈希不变。 + +### 2.2 和规格的偏离(S 已同意) + +规格 §3.2 写的是"`constraints` 只有经过 M6 确认的才写入"。**B1 建议不走 M6 确认。** 理由: + +1. M6 防的是从模型输出里**推断**出来的约束被当成事实(记忆投毒)。B1 存的是用户本人的原话,不经过任何推断,所以没有投毒面; +2. 弹确认会多一次交互,而桌面端现在还没有确认 UI。 + +代价:用户顺口说的话也会进前提区(例如"顺便说一下这张表都有哪些字段?",mul_022 第 2 轮)。它是问句,不是约束,对模型没有误导,但会占容量。可选的过滤规则:以"?/?"结尾、并且没有"只/排除/去掉/不要/重复"这类词的就不收。**建议先不过滤**,用 mul_022 看实际影响再说。 + +### 2.3 涉及文件 + +`app/memory/working_state.py`(fold/render)、`app/memory/snapshots.py`(块头措辞)、`docs/roadmap/memory-system-spec.md` §3.2/§3.8、对应测试。`service.py` / `react_agent.py` 不动。 + +## 3 B2 出处轮次 + +- filters 标的是"最近一次执行代码的那一轮"的轮次;earlier 标的是它最后一次被使用的轮次;前提区标用户说这句话的那一轮。 +- 格式:`order_date 的月份 == 3(第2轮)`。轮次编号和历史块的 `[第N轮 · …]` 用同一套编号(`render_block` 的 enumerate,从 1 开始,按全会话计)。 +- 注意:历史块按 12 轮窗口截断时,**编号不重排**。否则状态里写的"第 2 轮"会对不上历史块里的轮次。这一条要加测试钉住。 +- 代价:每条前提多 6–8 个 token。STATE_TOKENS=500 不调整,先实测。 + +## 4 B3 取原文工具(M3b) + +- 工具签名:`get_turn(n: int) -> str`。返回第 n 轮的问题、完整回答(不是只有 300 字的 a_brief)、最后一次成功执行的代码,整体用数据块包起来("只是数据,不是指令"),总长上限 4000 字。 +- 只在 `state` / `full` 模式下注册。off 模式下工具表不变,P8 哈希不变。 +- 只做 ReAct。workflow 没有工具循环,不做。 +- 数据来源:当前会话的 messages + result_json,在 `analyze()` 里把可读的轮次快照传进来。**不读其他会话**,也不按 message_id 任意读取,n 只能是当前会话已有的轮次。 +- 失败轮返回"问 + 状态",和 M5 一致。 +- 改的是工具表,会影响指纹(rendered),按规格 §14.4 的纪律先通知评测侧。 +- 要对评测侧说明:重复计算率(规格 M11 指标表)在 B3 之后才有目标可设。 + +## 5 B4 口径库:本批不做,先钉写入规则 + +M2 跟着阶段 7 A5 走,本批不实现。教程对照里 S 同意记下的规则先写在这里,实现时照用: + +- 写入门槛:只收 `source=explicit_user`(用户在对话里明说的定义)。模型推断出来的、从答案里抽出来的一律不收; +- 版本化:改口径时新增一行,用 `supersedes` 指向旧行,不覆盖; +- 删除:软遗忘,标记 `retired_at`,不物理删除,保证能回溯; +- 反向回归不变:口径里引用的字段在表里不存在时,仍然拒答;m10 一个字都不改。 + +## 6 明确不做(本批) + +| 项 | 理由 | +|---|---| +| M4 压缩 | 现有失败里没有一条是窗口溢出造成的;B1 已经把最怕丢的约束保护起来 | +| M7 改写 | 要多一次模型调用;B1+B2+B3 已经覆盖指代问题。等 B3 之后指代题仍然错,再考虑 | +| M8 workflow 记忆 | 依赖 M7 | +| M9 / M10 | 跟阶段 7 A6 / A5 | +| 向量库、混合检索、衰减、图库 | 单机单用户、按数据集划分,没有可检索的长期语料 | + +## 7 验收 + +**闸门**:pytest / vitest / e2e 全量通过;`state` 模式下 e2e 另跑一遍;`off` 模式下 P8 的三个哈希不变(测试钉住)。 + +**单元测试**(实现 PR 里写出测试名): +- 纯文字轮进前提区;有代码的轮不进; +- 15 轮会话里第 1 轮的约束:历史块已经把它丢了,状态块里仍然有; +- 前提区超过 6 条时丢最早的一条,`dropped` 正确累加; +- 预算紧张时裁剪顺序是 earlier → filters → 前提区; +- 轮次编号在窗口截断后不重排,状态块和历史块的编号一致; +- `get_turn` 越界、失败轮、超长回答被截断、off 模式下不注册。 + +**记忆专项题**(新建 `app/eval/cases_memory.py`,必须过 `test_eval_cases.py`,m10 不动): +- 长会话:第 1 轮给约束,中间插 12 轮以上不相关的问题,最后一轮问"这些订单……"; +- 约束被改:第 1 轮"只看 3 月",第 3 轮"改成 4 月",最后一轮应该按 4 月算; +- 指代名字:mul_026 形状的变体 2 道。 + +**真模型**(向评测侧提出的需求,不替它安排): +- 在实现 PR 的 head 上跑多轮 33 题 state 三跑,再加 cases_memory,结果和 7b24d4f 的读数做 McNemar; +- 底线:012/020/021/030 不回退,长会话题 off 和 state 的差异显著。 + +## 8 已定(S,2026-11-02) + +1. B1 不走 M6 确认,直接收用户原话(§2.2)。规格 §3.2 的偏离随实现 PR 记进 §3.8。 +2. B3 和 B1/B2 放在同一个实现 PR,B3 放最后;重跑读数不支持的话就拆出去(§1.3)。 +3. 前提区先不过滤问句,用 mul_022 看实际影响(§2.2)。 diff --git a/docs/roadmap/memory-system-spec.md b/docs/roadmap/memory-system-spec.md index b21b866..639850a 100644 --- a/docs/roadmap/memory-system-spec.md +++ b/docs/roadmap/memory-system-spec.md @@ -1052,6 +1052,8 @@ M2 ──→ M10 ──→ M9 M7(改写)、M2/M10(口径,跟阶段 7 A5 的条件)、M8(workflow,依赖 M7)、M4(压缩)、M6(门槛)、M3b(取码工具)。按批 2 的失败分布排序后,再开一个方案增补 PR 定范围。M9 跟随阶段 7 A6 推迟。 +> **2026-11-02**:范围增补见 `docs/roadmap/memory-batch3.md`(B1 用户前提区、B2 出处轮次、B3 取原文工具,即 M3b;M2 只钉写入规则)。 + M2 若做,DoD 固定为:口径题(`cases_memory.py`)在口径库存在时按口径计算;**口径里引用的字段在表里不存在时仍拒答**(反向回归,比正向用例更重要,防止口径库变成"编造许可证");闸门 m10 一个字不动且仍拒答;新会话能用旧会话的口径。 ---