feat(memory): D3 第五态 defer(入库审查)+ 提取留存规则闸 + 临时 TTL 配置面

主人原话:「收窄提取器,确实要给他一个主动式的记忆留存规则,不能想到什么都
往记忆里写」「.env 只做兜底用 …这些键优先从面板配置上取」「临时记忆到期删除」。
按 design/23 §18(D3 第五态 defer)、§19(到期处置)、design/03 §9.6
(tempTtlSec 默认 7 天 + 坏值口径)、§10(提取留存规则)落地。

- IngestDecision 闭集加第五态 defer:照常 active 落库 + 写短 expires_at 落
  临时层(不删行、不改正文,回滚 = 清空该列);IngestRuleVersion d3.2 → d3.3。
- 落点:ingestGatePlan 增 ExpiresAt + expiresAt()(走 expiryText/formatTime 的
  定宽 30 字节 UTC,与读侧时效闸同一套文本口径),由 finishIngest 在 defer 一态
  补写 expires_at(INSERT 与补写之间的短暂窗口如实登记,方向 = 宁可多留)。
  非 defer 三态一笔都不多写。
- T0 提取留存规则闸(extract_retention.go):design/03 §10.5 的 N1~N5 纯规则,
  接进 judgeIngest 的候选比对之前,命中即 defer + reason=t0_low_value,不进 T1;
  两道护栏(显式「记住」/ 持久信号)一票放行,fail-safe 方向 = 宁可多留。
- T1 闭集扩为 merge/modify/keep/defer(同一个 prompt、同一次调用,绝不新增
  第二次模型调用):只有关系判为真新时才问价值;defer 要求 target=0,否则判
  非法回落 create(reason=t1_reply_invalid)。
- 提取器提示词加固(design/03 §10.3 B):正向清单 + 反例清单 + 「允许返回 []」。
- 临时记忆 TTL:代码默认 tempTtlSec = 604800(7 天);面板
  $DATA_DIR/config/memory-tidy.json 的 tempTtlSec 优先(ApplyReviewTuning 逐键
  解析,坏值回落默认 + WARN,绝不写进 cfg)。该键在 .env 里没有出处且不新增
  (.env 已冻结)⇒ 实际生效链是「面板 > 代码默认」。启动播种同步该键(*int +
  omitempty,nil 省略、绝不写 null),保证播种文件与生效值逐字一致。
- preinsert_store.go 的 listReviewCandidatesSQL 原位注释:这条内部召回 SQL
  刻意不过时效闸的裁定与理由(design/03 §11.2 第 3 条)。
- 测试:defer 落库带短 expires_at(闸 + T1 两条路径)、写入口径与时效闸同源、
  TTL 坏值与回落、规则版本 d3.3、整理候选幂等键不含 IngestRuleVersion、
  提取规则正例 11 条 / 反例 11 条。

验收:go build ./... + go vet ./... + go test ./... 全绿;windows/darwin 交叉
编译通过(GOOS=darwin GOARCH=arm64 / GOOS=windows GOARCH=amd64)。
This commit is contained in:
AskaEth committed 2026-10-13 20:22:48 +08:00
1 parent 68ba85389b
commit 21b906ffb0
18 files changed
+1352 -35

No files matched your search

+34 -1
View File
@@ -15,7 +15,40 @@ import (
// 以便 parseCandidates 确定性地反序列化。溯源(source_conversation)
// 刻意不在其中:它由持久化层盖真实 session/message id,绝不由模型生成
// (裁决 04.4.3,见 source.go)。
const extractSystemPrompt = `你是记忆提取器。从对话中提取值得长期记住的记忆项(用户偏好、习惯、约定、重要事件)。
//
// 2026-10-13(design/03 §10.3 的 **B 级:提示词加固**):prompt 里补上
// **正向清单一句话 + 反例清单一句话 + 明确允许返回 `[]`**。回应主人原话
// 「**收窄提取器,确实要给他一个主动式的记忆留存规则,不能想到什么都往记忆里
// 写**」。
//
// 🔴 为什么提示词**只是辅助**(design/03 §10.2):提示词产出是自由文本,**不可
// 验收**(测试只能断言「模型有没有听话」),且判据落在提示词上违反 AGENTS §9
// 「判据靠元数据,不靠提示词」。**真正的防线是 A 级规则闸**(extract_retention.go
// → judgeIngest,纯函数、可单测、在唯一写路径上);这里的两份清单是**同源口径的
// 第二道**,让模型与规则闸朝同一个方向使劲,而不是各写各的。
//
// 「允许返回 `[]`」是 Mem0 官方口径(design/23 §2:官方原话「Include examples
// that should produce []」)—— 不写这一句,模型会为了「有产出」而硬凑。
const extractSystemPrompt = `你是记忆提取器。从对话中提取**值得长期记住**的记忆项。
只保留这些(正向清单):
- 长期事实:身份、住址、职业、家庭成员、健康约束(过敏、忌口、长期用药)。
- 稳定偏好:口味、作息、喜欢什么、讨厌什么。
- 关系里程碑:约定、承诺、共同经历、纪念日、称呼习惯。
- 项目约定:编号规则、暗号、路径、命名约定。
- 能力边界:主人明确说过的「不要做 X」。
下面这些**不要提取**(反例清单):
- 寒暄与致谢:「谢谢」「早上好」「辛苦啦」「晚安」。
- 情绪波动:「今天好累」「心情不错」。
- 一次性琐事与任务指令:「帮我订个 7 点的闹钟」「把这个文件转成 pdf」。
- 时效性数据:当前时间、天气、「现在几点了」。
- 助手自己说过的话 —— 只提取主人这边的长期事实与偏好。
- 不确定的推断(「他可能喜欢 X」):拿不准就不要提取。
**允许返回空数组 []**:一轮对话里没有值得长期记住的内容时,正确做法就是回
[],不要为了凑数而提取。
只输出一个 JSON 数组,不要任何其他文字。每个元素形如:
{"content":"记忆内容","scope":"user|persona|project","kind":"relationship|world_event","tags":["标签"],"confidence":0.9,"project_id":"可选项目id"}
去噪去重:只保留值得长期记住的,忽略寒暄与临时信息。`