背景(M37 复审的原话):「真正卡住上游的是提取器的改写:一次性事件被写成 30~80 字的第三人称长期陈述……这不是阈值问题,是判定链缺信息位。」本片补的 就是那一位,不动 N1~N7 的任何阈值。 产出侧 - temporal.go:TemporalKind 闭集(one_off / recurring / durable / unknown, 空串 = 未分型),normalizeTemporal 只认四个字面值,其余一律归 unknown(绝不猜 同义词);提取提示词追加该枚举(模型只负责标注,判定在 Go 闭集 switch 里)。 - llmextractor:候选解析后归一并随 MemoryItem 带出;非法/缺失值不报错、不拖累 同批其他候选。 落库 - 迁移 0027_memory_temporal(up: ADD COLUMN temporal TEXT NOT NULL DEFAULT ''; down: DROP COLUMN,只撤结构、不改写任何行数据)+ NewStore 的自举前滚 (duplicate column 是预期路径);Store.Add 的同一笔 INSERT 写该列,判定与 留痕不分叉。 判定(仍是 resolveRetention 一处收口,judgeIngest 一字未改) - one_off ⇒ temp(+ expires_at),但先过 temporalSanityVeto 六条语义安全阀: 显式「记住」/ 在陈述约定 / 习惯词 / 硬性长期事实词 / 永久化 / 状态改变 —— 命中即不给结论、落回 N1~N7。依据是离线重放的实测:去掉「记住+约定」两条时 21 条长期约定会被判 temp(4.80%),全开后在最近 500 条窗口内翻转条数为 0。 - durable / recurring ⇒ long 且不再跑规则;unknown / 未分型 ⇒ 落回 N1~N7 (不许因为「正文看着像今天的事」就判 temp)。 - IngestRuleVersion d3.4 → d3.5(判定结论会变,留痕必须可分)。 验证 - go build ./... + go vet ./... 全绿;go test -count=1 ./internal/runtime/memory/... ./internal/db/... 全绿;memory -race 全绿。 - 新增 temporal_test.go(验收点 ①~⑤ + 两条交叉护栏 + 闭集/归一化)与 temporal_migration_test.go(0027 up/down/同形/真实启动顺序)。变异实测: 摘掉 one_off 支路 ⇒ ① 全红;关掉 temporalSanityVeto ⇒ 三条护栏用例全红。 - 迁移测试的「回滚 N 步」改为从 schema_migrations 现算(migrateDownToBefore), 新加一条迁移不再打红 0025/0026 的用例。
44 lines
3.5 KiB
SQL
44 lines
3.5 KiB
SQL
-- 0027_memory_temporal:提取器产出侧的**时间性元数据位**(切片 M45)。
|
||
--
|
||
-- 为什么需要这一列(而不是再调一次词项阈值):M37 复审的量化结论是「defer 命中
|
||
-- 2/500 = 0.4%」不是阈值太严,而是**判定链缺信息位** —— 真正卡住上游的是提取器的
|
||
-- 改写:一次性事件被写成 30~80 字的第三人称长期陈述,规则闸只看落库正文,因此
|
||
-- 看不见「这是今天的一次性事件」这一位。实测反证:把 N6 的时点词窗口从 12 放宽到
|
||
-- 24 字,命中 2→14,多出的 11 条逐条都是**同一长期约定的重复陈述**(「一起晒太阳」
|
||
-- 暂时搁置 ×5、「傍晚出门走十分钟…今天没走成明天补上」×6)—— 放宽阈值等于 7 天后
|
||
-- 把约定本身删掉,违背 fail-safe。所以补的是信息位,不是阈值。
|
||
--
|
||
-- 为什么是**列**而不是新增一种 kind:`kind`(关于什么)、`scope`(谁的)、
|
||
-- `retention_tier`(留多久)、本条 `temporal`(**说的是一次发生的事,还是反复/长期
|
||
-- 成立的事**)是四根正交的轴。时间性是**提取器对原始对话的判断结果**,它必须能被
|
||
-- 单独存下来(事后巡检「模型当时标了什么」、以及将来回填),塞进 kind 就丢了这条
|
||
-- 观测线;靠提示词里的自然语言描述则违反 AGENTS §9「判据靠元数据,不靠提示词」。
|
||
--
|
||
-- 列值闭集(代码侧定义:internal/runtime/memory/temporal.go 的 TemporalKind):
|
||
-- one_off 一次性事件(今天 / 这次 / 某一次)⇒ 判定为 temp + 非空 expires_at
|
||
-- recurring 反复发生的规律(每天 / 每周 / 每次都…)⇒ long
|
||
-- durable 长期成立的事实 / 偏好 / 约定 / 称呼 / 身份 ⇒ long
|
||
-- unknown 模型拿不准或没给出合法值 ⇒ **没有结论**,交给既有 N1~N7 规则判
|
||
-- '' 尚未分型:本迁移之前的历史行,以及非提取器的写入路径(默认值)
|
||
--
|
||
-- 🔴 默认值 `''` 而**不是** `unknown`:两者在判定上走同一条路(都落到旧规则),但
|
||
-- 含义不同 —— `''` 是「没问过」,`unknown` 是「问过,模型拿不准」。把历史行写成
|
||
-- `unknown` 会让「模型答了什么」这个观测面从第一行起就掺假。本迁移**刻意不回填**:
|
||
-- 历史行没有对应的提取器输出,拿正文反推「它当时是不是一次性事件」正是本片要消灭
|
||
-- 的那种猜测。
|
||
--
|
||
-- 幂等性:`ALTER TABLE ADD COLUMN` 在 SQLite 里**没有** IF NOT EXISTS 形式,因此本
|
||
-- 迁移与 0019 / 0026 同一手法 —— 依赖启动顺序「先 MigrateUp、后 NewStore」
|
||
-- (cmd/cyrene/wire.go):生产库里这一步先跑,随后 NewStore 的自举前滚
|
||
-- (internal/runtime/memory/temporal_schema.go 的 ensureTemporalSchema)走到
|
||
-- "duplicate column name" 是预期路径。列的形状必须与自举 DDL
|
||
-- (internal/runtime/memory/store_schema.go)**逐字同形**,由
|
||
-- internal/runtime/memory/temporal_migration_test.go 钉住。
|
||
--
|
||
-- 本列**不建索引**:目前没有任何按时间性位的读路径(它是写入时的判定输入 +
|
||
-- 事后巡检的留痕列)。将来整理侧真要按它筛,再补索引不迟(两条路径一起加)。
|
||
--
|
||
-- 命名口径:表名复数(AGENTS §10 已登记例外)、列名与代码的 JSON 键同名
|
||
-- (`temporal`),与 0026 的 `retention_tier` 同族。
|
||
ALTER TABLE memory_items ADD COLUMN temporal TEXT NOT NULL DEFAULT '';
|