r1 复审 p1-1(会毁数据):tag_set 簇的安全门只有数值与否定,**专有名词/引号取值不比**, 生产语料实测把 `用户的昵称是「叶酱」。` 并进 `主人的昵称是「喵喵七号」…` 的存活行, 而同库另有 4 行写着「喵喵七号已作废、应称叶酱」⇒ 一次 apply 丢掉当前事实。 修法(结构性,不是补丁): - 新增取值门 tidy_dedupe_values.go:被吸收行的**引号取值**(「」『』“”‘’")与 **拉丁 token**(≥2 字母,含 h3R3 这类混排)必须在存活行正文中一个不少; 方向单向(被吸收 ⊆ 存活),与软合并语义同向(存活行多出的取值不会丢)。 补齐 design/23 §5.1-3 要求的「专有名词 / 数值 / 否定词」三件套里缺的第一件。 - tag_set 簇也过相似度下限(dedupeGroupMinSimilarity 两档统一)。取值 0.10 而非更高: 语料实测 tag_set 合法改写(`用户可能需要提醒吃早饭…` 0.047、`叶酱有工作…` 0.109) 的余弦本来就在 0.05~0.4 —— 抬高它只会删掉合法去重,不会多挡住危险对。 实测(503 对):0.10→保留 98%、0.20→86%、0.30→63%、0.40→40%、0.50→20%。 - 回归用例:TestDedupeGroupRejectsSameTagDifferentValue(脱敏真实语料 + 同值对照组)、 TestDedupeGroupLatinTokenIsAlsoAValue、TestDedupeGroupNeverLosesAValue(簇级不变式)、 TestDedupeGroupTagSetAlsoNeedsSimilarityFloor。去掉取值门 ⇒ 前三条红;把 tag 相似度 归零 ⇒ 第四条红。 r1 复审 p2-1(数据安全):元信息查询失败时**该分区本轮不分簇**(旧实现降级成 nil map, `temporary` 恒 false ⇒ 绕开「临时行绝不进簇」红线);apply 侧补 `expires_at` 复核 (候选久置/生成后变临时 ⇒ 整簇 skip,tidySkipTemporaryRow)。用例: TestDedupeInPartitionSkipsWhenMetaUnavailable、TestTidyApplyDedupeGroupSkipsTemporaryRows。 r1 复审 p2-2(闸误杀被低报):补三类安全阀 ——「以后/从此/往后/今后」= 自此刻起永久成立、 「换成/变成/改成/搬进」= 状态已改变且新状态持续、N7 的进行中标记同样必须句首 12 字内; 「宿舍」进长期事实词表;顺手修 `<=` 的 off-by-one(改为 `<`)。 实测(生产库只读 mode=ro,active 3667): - dedupe:281 簇/594 行 → **255 簇/505 行(13.8%)**;tag_set 240 / prefix12 15; 取值门挡 76 对、相似度门挡 12 对;簇大小 2:155 3:48 4:26 5:8 6:7 7:4 8:2 9:2 18:1 20:2 (两个 20 行簇与 18 行簇的取值集合均为单一「喵喵七号」⇒ 大簇=同一事实的重写)。 - 闸:改前 2 (0.05%) → 改后 **75 (2.05%)**(n6 53 / n7 20 / n1 2);含锚点词的 27 条 逐条人读均为「约定明天中午见 / 下周留个午后」类到期即失效的安排。 - 达标代价:+目前/现在 当锚点 → 2.13%;再叠一次性事件动词表(曾/发来/分享…)→ 6.74% 且新增 172 条里 56 条是长期身份/里程碑(「i love you 文件」17 条、「海报就是助手本人」 10 条…)⇒ 仍到不了 8%,不许为凑区间放宽。 验收:go build ./... + go vet ./... + go test ./... 全绿;-race ./internal/runtime/memory ok。
243 lines
12 KiB
Go
243 lines
12 KiB
Go
package memory
|
|
|
|
import "sort"
|
|
|
|
// 本文件是提取留存规则闸(design/03 §10)的**词表**:每一项都是**函数**而不是
|
|
// 包级变量(AGENTS §10 禁包级全局变量;同 tidySystemSessions / tidyHabitMarkers 的手法)。
|
|
//
|
|
// 它从 extract_retention.go 拆出来只为守 AGENTS §10 的文件行数上限(≤500 行)——
|
|
// 词表内容与逐条理由**逐字未动**;判定逻辑在 extract_retention.go。
|
|
|
|
// 下列词表一律是**函数**而不是包级变量(AGENTS §10 禁包级全局变量;
|
|
// 同 tidySystemSessions / tidyHabitMarkers 的手法)。
|
|
|
|
// retentionRememberDirectives 是「主人明确要求记住」的信号(护栏①)。
|
|
func retentionRememberDirectives() []string {
|
|
return []string{"记住", "记下", "别忘了", "不要忘", "别忘记"}
|
|
}
|
|
|
|
// retentionDurableMarkers 是正向清单(design/03 §10.4)的词族里、**不被
|
|
// tidyHabitMarkers 覆盖**的那些:长期事实 / 关系里程碑 / 项目约定 / 能力边界。
|
|
func retentionDurableMarkers() []string {
|
|
return []string{
|
|
// 长期事实
|
|
//
|
|
// 「宿舍」(2026-10-25 r1 复审 P2-2 补):语料里「近期在宿舍过夜 / 睡在宿舍」
|
|
// 是**居住事实 + 稳定照料约定**,不含「住」字却被 N6 判成临时;收它之后
|
|
// 这类句子与「住在宿舍」同档(都要过护栏②才放行)。
|
|
"生日", "纪念日", "住", "宿舍", "职业", "工作", "家庭", "家人", "孩子", "父母",
|
|
"过敏", "忌口", "健康状况", "疾病", "吃药", "身高", "体重", "年龄",
|
|
// 关系里程碑 / 约定
|
|
"约定", "承诺", "答应", "说好", "暗号", "口令", "称呼",
|
|
// 项目约定
|
|
"项目", "迁移号", "端口", "路径", "命名", "编号", "约定俗成",
|
|
// 能力边界(主人明确的「不要做 X」)
|
|
"不要做", "别做", "禁止", "不许",
|
|
// 稳定偏好(补 tidyHabitMarkers 之外的)
|
|
"口味", "爱好", "讨厌", "不喜欢", "偏爱", "嗜好",
|
|
}
|
|
}
|
|
|
|
// retentionChatterWords 是 N1 的寒暄 / 致谢词表。
|
|
func retentionChatterWords() []string {
|
|
return []string{
|
|
"谢谢", "多谢", "感谢", "辛苦", "麻烦你了", "早上好", "早安", "中午好",
|
|
"下午好", "晚上好", "晚安", "你好", "您好", "在吗", "收到", "好的",
|
|
"好呀", "好嘞", "嗯嗯", "哈哈", "嘿嘿", "呵呵", "么么", "拜拜", "再见",
|
|
"加油", "不错", "真棒", "太棒了", "hi", "hello", "hey", "thx", "thanks",
|
|
}
|
|
}
|
|
|
|
// retentionEmotionWords 是 N2 的情绪词表。
|
|
func retentionEmotionWords() []string {
|
|
return []string{
|
|
"好累", "累了", "开心", "高兴", "难过", "不开心", "焦虑", "压力大",
|
|
"心情", "疲惫", "难受", "无聊", "郁闷", "沮丧", "生气", "委屈",
|
|
"烦死", "好烦", "烦躁", "兴奋", "紧张", "害怕", "孤单", "寂寞",
|
|
// 「不错」是**情绪评价**(「心情不错」),因此它必须在**核心词表**里按整词
|
|
// 出现:只靠否定词表里的「不」,`不错` 会被拆成 `不` + 光杆「错」,于是
|
|
// 「心情不错」既认不出核心、也消费不干净 —— 一条纯情绪句反而漏判。
|
|
"不错",
|
|
// 光杆「累」同理:「主人今天真的特别累」只有一个 `累` 而**不含** `好累` /
|
|
// `累了`,漏掉它就会留下一个消费不掉的「累」、把一条纯情绪句放行。
|
|
// 收它**不会**带来误弃 —— 判 defer 的门槛恒是「整句被形态消费干净」,
|
|
// 多收一个情绪词只会让**本该 defer 的**句子认得出来。
|
|
"累",
|
|
}
|
|
}
|
|
|
|
// retentionEmotionSuffixTokens 是「情绪核心之后**允许**出现的修饰」的封闭集合:
|
|
// 程度 / 否定 / 情绪 / 语气。**它不含任何承载内容的词** —— 这正是白名单判据在
|
|
// 结构上不会吞掉实体的原因(核心之后一旦出现 `想`/`会`/`时`/`和`/`我` 这类字,
|
|
// 就必然消费不掉、留下非空余部 ⇒ 放行)。
|
|
//
|
|
// 它是函数而不是包级变量(AGENTS §10 禁包级全局变量;同 tidyHabitMarkers 的手法);
|
|
// 长度先按**最长词优先**排序,`不` 不会抢在 `不错` 前面被消费。
|
|
func retentionEmotionSuffixTokens() []string {
|
|
tokens := make([]string, 0, 64)
|
|
tokens = append(tokens, retentionEmotionDegreeWords()...)
|
|
tokens = append(tokens, retentionEmotionNegations()...)
|
|
tokens = append(tokens, retentionEmotionWords()...)
|
|
tokens = append(tokens, retentionEmotionToneWords()...)
|
|
sort.SliceStable(tokens, func(i, j int) bool {
|
|
return len(tokens[i]) > len(tokens[j])
|
|
})
|
|
return tokens
|
|
}
|
|
|
|
// retentionEmotionTimeWords 是**只允许在「主语之后」出现一次**的时间词:无主语时
|
|
// 它就是首词(「今天好累」),有主语时紧跟主语(「主人今天好累」)—— 之外的位置一律
|
|
// 不匹配(`今天主人好累` 实测 `rest="今天主人好累"`, `saw=false` ⇒ 放行)。
|
|
// 它们让一句情绪陈述显形为「今天怎么样」,本身不含实体。
|
|
func retentionEmotionTimeWords() []string {
|
|
return []string{
|
|
"今天", "明天", "昨天", "今晚", "昨晚", "今早", "此刻", "现在", "刚才", "刚刚",
|
|
"最近", "这两天", "这几天", "这阵子", "这段时间", "目前", "当下", "一整天",
|
|
}
|
|
}
|
|
|
|
// retentionEmotionSubjects 是**只允许出现在句首**的主语 / 称呼。把它限死在句首是
|
|
// 白名单判据的要害之一:`我`/`他`/`我们` 这些词**本身可以当实体**(「会想我」的
|
|
// 宾语就是它),若允许它们在任意位置被消费,`主人不开心时会想我` 就会被吞成空。
|
|
func retentionEmotionSubjects() []string {
|
|
return []string{
|
|
"主人", "咱们", "我们", "你们", "他们", "她们", "自己",
|
|
"我", "你", "他", "她", "它", "咱",
|
|
}
|
|
}
|
|
|
|
// retentionEmotionDegreeWords 是程度词(只修饰强度,不引入实体)。
|
|
func retentionEmotionDegreeWords() []string {
|
|
return []string{
|
|
"非常", "特别", "稍微", "真的", "真是", "十分", "比较", "有点", "有些",
|
|
"很", "挺", "太", "好", "超", "蛮", "更", "最",
|
|
}
|
|
}
|
|
|
|
// retentionEmotionNegations 是情绪核心之后的否定词(「心情**不**好」)。它只能出现在
|
|
// 后缀位;`不` 与 `没` 都不承载实体。
|
|
func retentionEmotionNegations() []string {
|
|
return []string{"没有", "不", "没"}
|
|
}
|
|
|
|
// retentionEmotionToneWords 是句尾的语气词(「好累**啊**」「不错**了**」)。它们只出现
|
|
// 在句尾,不引入实体。
|
|
func retentionEmotionToneWords() []string {
|
|
return []string{"啊", "呀", "啦", "哦", "喔", "嘛", "呢", "吧", "了", "的", "哎", "唉", "嗯", "哈"}
|
|
}
|
|
|
|
// retentionPunctuationRunes 是「消费干净」判定允许剩下的标点与空白。
|
|
const retentionPunctuationRunes = ",。!?、;:…~~·--,.!?;:()() \t\n"
|
|
|
|
// retentionTimelyQueries 是 N4 的时效性**查询整句**词表(不是单词,理由见
|
|
// judgeRetentionTimelyData)。
|
|
func retentionTimelyQueries() []string {
|
|
return []string{
|
|
"现在几点", "几点了", "现在时间", "当前时间",
|
|
"今天天气", "明天天气", "天气怎么样", "天气如何", "天气怎样",
|
|
"温度多少", "多少度", "气温多少",
|
|
}
|
|
}
|
|
|
|
// retentionAskPrefixes 是 N3 的祈使开头词表。
|
|
func retentionAskPrefixes() []string {
|
|
return []string{
|
|
"帮我", "请帮", "麻烦帮", "帮个忙", "提醒我", "叫我", "替我",
|
|
"给我订", "给我买", "帮我查", "帮我搜", "帮我订", "帮我买",
|
|
}
|
|
}
|
|
|
|
// retentionOneOffActions 是 N5 的一次性动作词表(位置不限 —— 它们描述的是
|
|
// 「转一次文件」这类有始有终的动作,而不是可以长期成立的偏好)。
|
|
func retentionOneOffActions() []string {
|
|
return []string{
|
|
"转成pdf", "转成 pdf", "转换成", "截图", "打开这个", "关掉这个",
|
|
"搜一下", "查一下", "翻译一下", "订个闹钟", "设个闹钟",
|
|
}
|
|
}
|
|
|
|
// retentionConstraintMarkers 是「**具体约束**」的词项(护栏②与 N6/N7 共用):
|
|
// 禁忌 / 硬性要求 / 频次与时间规律。它是「这条记忆将来会被用上」的可执行证据。
|
|
//
|
|
// ⚠️ 只收**能落到行动**的词:把「喜欢/讨厌」这类偏好词收进来就等于取消护栏②
|
|
// 的意义(它们已经在 tidyHabitMarkers 里当锚点,锚点 + 锚点不构成约束)。
|
|
func retentionConstraintMarkers() []string {
|
|
return []string{
|
|
// 禁忌 / 硬性要求(主人明确的「不要做 X」)
|
|
"不要", "不准", "不许", "不能", "禁止", "必须", "一定要", "记得", "别忘",
|
|
// 频次 / 时间规律
|
|
"每天", "每日", "每晚", "每早", "每周", "每月", "每年", "每个",
|
|
"按时", "定期", "定量", "固定",
|
|
// 量化边界
|
|
"不超过", "至少", "最多", "以内", "不少于",
|
|
}
|
|
}
|
|
|
|
// retentionTimepointMarkers 是 N6 的**指示性时点词**(一次性事件的信号)。
|
|
//
|
|
// 🔴 两张表都**不收**「周期性时段词」:晚上/早上/中午/周末/工作日 —— 它们说的是
|
|
// 规律而不是某个时刻,收进来会让人把作息习惯判成一次性事件(M27 §4 R4 实测:
|
|
// 把这类词当锚点,误杀从 ~8% 升到 ~35%)。也**不收**「目前/现在/当下/这段时间」
|
|
// 这类**当前状态**词:实测它们命中的是「用户目前生活在花溪」「用户现在有工作」
|
|
// 这类**地点 / 职业事实**,正是最不该过期的一类。
|
|
func retentionTimepointMarkers() []string {
|
|
return []string{
|
|
"今天", "明天", "后天", "昨天", "前天",
|
|
"今晚", "明晚", "昨晚", "今早", "明早",
|
|
"刚才", "刚刚",
|
|
"这两天", "这几天", "这阵子",
|
|
"本周", "这周", "下周", "上周", "下个月", "本月",
|
|
"最近", "近期",
|
|
}
|
|
}
|
|
|
|
// retentionInProgressMarkers 是 N7 的「进行中状态」词(有始有终的状态,
|
|
// 不是长期事实)。它们比时点词更弱,因此 N7 与 N6 共用同一组安全阀。
|
|
func retentionInProgressMarkers() []string {
|
|
return []string{"正在", "暂时", "眼下", "这会儿"}
|
|
}
|
|
|
|
// retentionHardDurableMarkers 是 N6/N7 的**硬性长期事实词**:命中即放行。
|
|
//
|
|
// 它是 retentionDurableMarkers **减去「约定/承诺/答应/说好」** 的差集,理由:
|
|
// 那四个词是「锚定到某个时刻的安排」,配上时点恰恰是临时记忆(「约定下周留个
|
|
// 午后」→ 下周过去就没意义了);而「住/职业/工作/生日/过敏/称呼/项目/端口…」
|
|
// 说的是**不会因为一个时刻而过期**的事实,标题里带「今天」也不许判临时。
|
|
func retentionHardDurableMarkers() []string {
|
|
appointments := map[string]struct{}{"约定": {}, "承诺": {}, "答应": {}, "说好": {}}
|
|
markers := make([]string, 0, len(retentionDurableMarkers()))
|
|
for _, m := range retentionDurableMarkers() {
|
|
if _, ok := appointments[m]; ok {
|
|
continue
|
|
}
|
|
markers = append(markers, m)
|
|
}
|
|
return markers
|
|
}
|
|
|
|
// retentionPermanenceMarkers 是「**自此刻起永久成立**」的显式标记:出现它就绝不是
|
|
// 一次性事件(2026-10-25 r1 复审 P2-2 补)。
|
|
//
|
|
// 反例(复审构造即可复现,旧实现会判成临时 ⇒ 7 天后物理删除):
|
|
//
|
|
// "昨天主人说以后都叫我小叶"
|
|
// "上周主人说以后周末不加班了"
|
|
//
|
|
// 句首的「昨天/上周」是**说话的时刻**,而「以后…」说的是从那一刻起一直成立的东西 ——
|
|
// N6 只看时点词就把它们判成临时,方向正好反了。
|
|
func retentionPermanenceMarkers() []string {
|
|
return []string{"以后", "往后", "今后", "从此", "自此", "从今以后"}
|
|
}
|
|
|
|
// retentionStateChangeMarkers 是「**状态已经改变**」的标记:新状态会持续下去,
|
|
// 因此不是一次性事件(2026-10-25 r1 复审 P2-2 补)。
|
|
//
|
|
// 反例(语料实测被误判临时):`茂密最近换了装:蝴蝶结从棕色变成深色…` ——
|
|
// 宠物/物品的稳定外观与配饰是长期事实,只是「最近」换过一次。
|
|
//
|
|
// 与 design/23 §5.2-3 的关系:那里说「改成了Y」是**覆盖**不是重复(合并语义),
|
|
// 与这里「覆盖后的新状态是长期的」并不矛盾 —— 两条都在说「变化之后有稳定的新事实」。
|
|
func retentionStateChangeMarkers() []string {
|
|
return []string{"换成", "变成", "改成", "改为", "换装", "搬进", "搬到"}
|
|
}
|