From 4365224c9b4d846d0133e510f5a166304935a851 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Tue, 13 Oct 2026 20:57:31 +0800 Subject: [PATCH] =?UTF-8?q?docs(memory):=20N2=20=E5=AE=89=E5=85=A8?= =?UTF-8?q?=E8=AE=BA=E8=AF=81=E6=94=B9=E6=88=90=E7=9C=9F=E5=AE=9E=E6=9C=BA?= =?UTF-8?q?=E5=88=B6=EF=BC=88=E4=BD=8D=E7=BD=AE=E9=94=81=20+=20=E5=9B=9B?= =?UTF-8?q?=E7=B1=BB=E4=BF=AE=E9=A5=B0=EF=BC=89+=20=E4=B8=A4=E6=9D=A1=20tr?= =?UTF-8?q?ipwire?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 应答 review round3 的 p2#1(要修的只有这一条)。 ## 注释里的论证被证伪,改成真实机制 原句「封闭表里**没有承载内容的词**:没有动词、名词、量词」**字面不成立** —— reviewer 逐表审过:情绪词表(既是核心也是后缀 token)含 `心情`(名词)、 `生气`/`害怕`/`紧张`(动词)、`压力大`(动宾);否定表里 `没有` 是动词;程度表里 `比较` 可作动词/名词;主语表里 `主人`/`我`/`他` 本身就是**指人实体**。 真正保安全的是**位置**: 1. **位置锁定** —— 主语 / 时间词只允许出现在**句首**、各至多一次 ⇒ `主人`/`我`/`你`/ `他`/`今天` 这些指人/指时实体**在文法里永远不可能落到宾语位置**; (上一版黑名单正是死在这里:`会想我` 的 `我` 被当成主语剥掉) 2. **核心之后只允许「程度 / 否定 / 情绪 / 语气」四类修饰**,且它们只允许**紧跟** 情绪核心之后 —— 核心必须先被消费,那一段在语义上只能是**情绪的修饰** (「心情不错」「不开心」),**装不下一个宾语短语**。 「上一版为什么错、黑名单为什么修不完」那一段**保留**(reviewer 确认该部分写得对)。 ## 两条 tripwire(回应「后来人照字面加词、没有任何测试会红」) - `TestExtractionRetention_PersistentFactsAreNeverDeferred`:**25 条**持久事实/习惯 语料(塔给的 6 条 + reviewer 扩展的 19 条,来自 review round3 的对抗构造)逐条断言 **绝不 defer**,钉住**语义后果**;语料是**可增长的**(新事故 → 一条回归断言)。 - `TestExtractionRetention_SuffixTableIsPinned`:把后缀表的**内容**逐 token 钉死 (60 个 token 的 golden),任何增删立刻红。 **为什么它比语料更接近「表的内容性质」**:语料只能覆盖**已经想到的**输入,而这张表 是判据的全部自由度 —— 往后缀里加一个名词,表变了而语料可能一条都不碰;golden 钉的 是「表变了」这个**事实本身**,改表必须显式改 golden ⇒ 变成一次可 review 的 意图声明,而不是一次静默放宽。 两条一起才够:golden 钉「表变了」,语料钉「变了以后语义还对不对」。 ## 变异自证(两条 tripwire 分别会红) 变异 A:往后缀语气词表加一个名词「**妈妈**」(语料里没有任何一条会因此变 defer) ``` --- FAIL: TestExtractionRetention_SuffixTableIsPinned (0.00s) 后缀表 token 数 = 61, want 60 —— 表被改过。… FAIL ``` ⇒ golden 抓住了**语料抓不到**的改表。 变异 B:往后缀表加 `会`/`想`/`时`(看起来像无害虚词) ``` --- FAIL: TestExtractionRetention_PersistentFactsAreNeverDeferred (0.00s) --- FAIL: …/主人心情不好时会不好 extract_retention_test.go:284: 持久事实 "主人心情不好时会不好" 被判成 "n2_emotion" —— 它会进临时层、7 天后被物理删除(不可逆) --- FAIL: …/主人不开心时会好好想 extract_retention_test.go:284: 持久事实 "主人不开心时会好好想" 被判成 "n2_emotion" —— … --- FAIL: TestExtractionRetention_SuffixTableIsPinned (…) 后缀表 token 数 = 63, want 60 FAIL ``` ⇒ 语料把「会上线的那个后果」直接指了出来。两处变异均已还原。 验收:go build ./... + go vet ./... + go test -count=1 ./... 全绿; TestExtractionRetention 六个用例共 69 个子例全 PASS。 --- internal/runtime/memory/extract_retention.go | 32 +++++- .../runtime/memory/extract_retention_test.go | 97 +++++++++++++++++++ 2 files changed, 125 insertions(+), 4 deletions(-) diff --git a/internal/runtime/memory/extract_retention.go b/internal/runtime/memory/extract_retention.go index 8381f8c..ac4aaa5 100644 --- a/internal/runtime/memory/extract_retention.go +++ b/internal/runtime/memory/extract_retention.go @@ -151,10 +151,22 @@ func judgeRetentionChatter(text string) retentionRule { // // 1. 匹配要求**整句被消费干净**(`retentionEmotionFormRest` 返回空余部)。任何 // 一个字符只要不在封闭表里,就会留在余部 ⇒ 判不出空 ⇒ **放行**。 -// 2. 封闭表里**没有承载内容的词**:没有动词、名词、量词。主语/时间词**只允许在句首** -// 出现一次,情绪核心之后**只**允许「程度 / 否定 / 情绪 / 语气」四类修饰。 -// 因此核心之后的部分若含 `想`、`会`、`时`、`和`、`我`、`听音乐`、`妈妈` 这类 -// 承载内容的字,**必然消费不掉** ⇒ 余部非空 ⇒ 放行。 +// +// 2. **位置锁定 + 核心之后只允许四类修饰** —— 这才是真正保安全的一条。 +// (早先这里写的是「封闭表里没有承载内容的词」,**那句被 reviewer 证伪** +// —— 表里确实有:`心情` 是名词,`生气`/`害怕`/`紧张`/`没有`/`比较` 可作动词, +// `压力大` 是动宾,而主语表里的 `主人`/`我`/`他` 本身就是指人实体。) +// +// 真正的机制是**位置**而不是"表里没有实词": +// - 主语 / 时间词**只允许出现在句首**、各至多一次 ⇒ `主人`/`我`/`你`/`他`/`今天` +// 这些**指人 / 指时的实体在文法里永远不可能落到宾语位置**(上一版正是死在 +// 这里:`会想我` 的 `我` 被当成主语剥掉了); +// - 核心之后只允许「程度 / 否定 / 情绪 / 语气」四类 token,而它们**只允许紧跟 +// 在情绪核心之后** —— 核心本身必须先被消费掉,因此那一段在语义上只能是 +// **情绪的修饰**(「心情**不错**」「**不**开心」「生气**得很**」), +// 装不下一个宾语短语。核心之后一旦出现四类之外的东西(`想`/`会`/`时`/`和`/ +// `听音乐`/`妈妈`),**必然消费不掉** ⇒ 余部非空 ⇒ 放行。 +// // 3. 失败方向是**安全**的:认不出的形态一律**放行**(留进长期层,有 T1 与整理兜底), // 而**绝不**误弃。上一版(黑名单剥离)恰好相反 —— 它把「能当实体的词」也列进 // 剥离表,于是 `主人不开心时会想我` 被剥成空、判成 defer,而那条是**持久习惯**, @@ -162,6 +174,18 @@ func judgeRetentionChatter(text string) retentionRule { // 黑名单那种形态**修不完**(review 举证 `会想和好` 被 `和`+`好` 吃掉), // 白名单把失败模式从「未知实体被吞」翻成「未知情绪句没认出」,方向才正确。 // +// 🔴 **后缀表不得加入承载内容的词**(名词 / 动词 / 量词 / 新的指人词)。上面第 2 条 +// 的整条安全性都建立在「四类之外一律消费不掉」上:往后缀表里塞一个名词,就会**静默** +// 把「情绪 + 该名词作宾语」的持久事实判成 defer,而且**不会有任何行为测试变红** +// (除非那条输入恰好在语料里)—— 这正是 review 点出的风险。两条 tripwire 钉住它: +// +// - `TestExtractionRetention_SuffixTableIsPinned`:把后缀表的**内容**逐 token 钉死, +// **任何增删都立刻红**(改表必须显式改这份 golden ⇒ 变成一次可 review 的意图声明); +// - `TestExtractionRetention_PersistentFactsAreNeverDeferred`:25 条持久事实/习惯 +// 语料逐条断言「绝不 defer」,钉住**语义后果**。 +// +// 两条一起才够:golden 钉「表变了」,语料钉「变了以后语义还对不对」。 +// // 长度不单独设限:全局 40 rune 的上限(judgeExtractionRetention)已经兜住。 func judgeRetentionEmotion(text string) retentionRule { rest, sawEmotion := retentionEmotionFormRest(text) diff --git a/internal/runtime/memory/extract_retention_test.go b/internal/runtime/memory/extract_retention_test.go index 0699b49..7a3444e 100644 --- a/internal/runtime/memory/extract_retention_test.go +++ b/internal/runtime/memory/extract_retention_test.go @@ -226,3 +226,100 @@ func TestJudgeIngestKeepsValuableItemAsCreate(t *testing.T) { t.Fatalf("正常条目 decision = %q, want %q(闸不许误伤)", v.Decision, IngestCreate) } } + +// persistentFactCorpus 是**持久事实 / 习惯**语料:它们**逐条都不许**被判成 N2 的 +// defer。这份语料来自 review round3 的对抗构造(塔给的 6 条 + reviewer 扩展的 19 条, +// 共 25 条),每一条都是「带情绪色彩、但真正要记的是情绪之后的那个持续行为/对象」。 +// +// 判 defer 的后果是**不可逆**的:7 天后从所有读路径消失(读侧时效闸),随后被后台 +// 清扫物理删除(design/23 §19)。因此这份语料是**不可回退的 tripwire**。 +// +// 它是**可增长的**:以后遇到新的持久事实被判 defer,往这里加一条即可 —— 加进去就 +// 从「一次线上事故」变成「一条回归断言」。 +func persistentFactCorpus() []string { + return []string{ + "主人不开心时会好起来", + "主人难过时会想不开", + "主人不开心时会好好想", + "主人心情不好时会不好", + "主人难过时会想我妈妈", + "主人开心时话就多", + "主人难过会想我", + "主人心情不好就没胃口", + "主人开心就爱说话", + "主人难过的时候想一个人待着", + "主人焦虑时睡不着", + "主人紧张时会结巴", + "主人孤单时会找人聊天", + "主人兴奋时会话多", + "主人害怕时会躲起来", + "主人心情差时会听歌", + "主人不开心了会想和好", + "主人压力大的时候会失眠", + "主人最近很容易焦虑", + "主人一个人时容易难过", + "主人心情不好就会想家", + "主人难过会想妈妈", + "主人不开心就沉默", + "主人心情不好时会找我", + "主人焦虑时会咬指甲", + } +} + +// TestExtractionRetention_PersistentFactsAreNeverDeferred 逐条断言上面那份语料 +// **一条都不判 defer**(规则闸返回 retentionNone ⇒ 走既有的 create 路径)。 +// +// 它钉的是**语义后果**:只要有哪条持久事实被判进临时层,本用例立刻红。 +func TestExtractionRetention_PersistentFactsAreNeverDeferred(t *testing.T) { + t.Parallel() + corpus := persistentFactCorpus() + if len(corpus) < 25 { + t.Fatalf("语料只有 %d 条,不应少于 review 举证的 25 条(别把语料删小)", len(corpus)) + } + for _, content := range corpus { + content := content + t.Run(content, func(t *testing.T) { + t.Parallel() + if rule := judgeExtractionRetention(content); rule != retentionNone { + t.Fatalf("持久事实 %q 被判成 %q —— 它会进临时层、7 天后被物理删除(不可逆)", + content, rule) + } + }) + } +} + +// TestExtractionRetention_SuffixTableIsPinned 是**表的内容性质** tripwire:把「情绪 +// 核心之后允许出现的修饰 token」这张表的**内容**逐 token 钉死,任何增删都立刻红。 +// +// 为什么它比「语料回归」更可靠:语料只能覆盖**已经想到的**输入,而这张表是判据的 +// 全部自由度 —— 往后缀表里加一个名词(比如把 `想` / `妈妈` 也当成"语气词"),表变了 +// 而语料可能一条都不碰。**golden 钉住「表变了」这个事实本身**,于是改表必须显式改这 +// 份 golden ⇒ 变成一次可 review 的意图声明,而不是一次静默放宽。 +// +// review round3 的 p2#1 正是这个风险:注释里一句「表里没有承载内容的词」若被当真, +// 后来人就会照着它往表里加词。现在那句话已被改成真实论证(位置锁定 + 四类修饰), +// 这张 golden 则是它的**机器化形态**。 +func TestExtractionRetention_SuffixTableIsPinned(t *testing.T) { + t.Parallel() + golden := []string{ + "不开心", "压力大", "非常", "特别", "稍微", "真的", "真是", "十分", "比较", "有点", + "有些", "没有", "好累", "累了", "开心", "高兴", "难过", "焦虑", "心情", "疲惫", + "难受", "无聊", "郁闷", "沮丧", "生气", "委屈", "烦死", "好烦", "烦躁", "兴奋", + "紧张", "害怕", "孤单", "寂寞", "不错", "很", "挺", "太", "好", "超", + "蛮", "更", "最", "不", "没", "累", "啊", "呀", "啦", "哦", + "喔", "嘛", "呢", "吧", "了", "的", "哎", "唉", "嗯", "哈", + } + got := retentionEmotionSuffixTokens() + if len(got) != len(golden) { + t.Fatalf("后缀表 token 数 = %d, want %d —— 表被改过。**改这张表会放宽/收紧 N2 的判据**:"+ + "往后缀里加名词/动词会让「情绪 + 该名词作宾语」的持久事实被静默判成 defer。"+ + "确认是**有意**改动后,同步更新这份 golden 与 persistentFactCorpus()。\ngot = %#v", + len(got), len(golden), got) + } + for i := range golden { + if got[i] != golden[i] { + t.Fatalf("后缀表第 %d 个 token = %q, want %q(表的内容变了,见上一条的理由)", + i, got[i], golden[i]) + } + } +}