Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/DEVLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 的决策表定。
150 changes: 150 additions & 0 deletions docs/roadmap/memory-batch3.md
Original file line number Diff line number Diff line change
@@ -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)。
2 changes: 2 additions & 0 deletions docs/roadmap/memory-system-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 一个字不动且仍拒答;新会话能用旧会话的口径。

---
Expand Down
Loading