fix(memory): dedupe_group 补中文数字门(量纲/单位归一),修 r2 复审 p2
r2 复审 p2:`sameNumbers` 只抽 ASCII 数字 token,而本语料的数字主要写成中文 (一小时 / 十五分钟 / 第二杯 / 三千次轮回)⇒「午睡一小时 vs 十五分钟」 「咖啡不续第二杯 vs 不超过一杯」这类**数值冲突**被当成同一事实合并。 修法(新文件 internal/runtime/memory/tidy_dedupe_numbers.go): - 抽出「**中文数字 + 单位**」对(一~九/两/半/十/百/千/万,含十五、二十四、一万两千、 半个小时),按**量纲归一**后比较:时间=秒(分钟 60 / 小时 3600 / 天 86400 / 周 604800 / 个月 2592000 / 年 31536000)、长度=米、文字量=个、计数=个; - 只认**后面紧跟已知单位**的数字:`一起 / 一直 / 一些 / 一样 / 第二天(序数位置)` 这类含数字的习语一律不产生数量 —— 不做成「有中文数字就不合并」; - 判据是**冲突**而非差异:存活行在同量纲里没有对应数量 ⇒ 不算冲突(被吸收行多出 的细节);等值(含单位换算后等值,如「半小时」=1800 秒)⇒ 照常合并; 值不等且单位换算不到一起 ⇒ 按冲突处理(复审口径:宁可不合并); - 只**增加否决**,不放宽任何既有门;ASCII 路径仍由 sameNumbers 先判,一字未动; `sameNumbers` 本身也未改(judgePair/judgeIngest 共用它,属于另一处 blast radius, 已在回报里登记为后续建议)。 语料实测(生产库只读 mode=ro,active 3667):999 对里中文数字门挡掉 **17 对**, 其中真冲突:午睡十五分钟 vs 一小时 ×3、咖啡一杯 vs 第二杯 ×4、 「三千次轮回」vs「三千万次轮回」×1(ASCII 路径完全看不见这条); 其余 9 对是「单位不可换算」的保守拒绝(半碗 vs 一个包/一杯、一次 vs 两圈)。 dedupe 产出 255 簇/505 行 → **250 簇/495 行(13.5%)**。 回归用例(去掉这道门即红,已实测): - TestDedupeGroupRejectsConflictingChineseNumbers(复审给的两对真实语料,脱敏保留原意) - TestDedupeGroupRejectsNonConvertibleUnits(两杯 vs 三瓶 ⇒ 保守不合并) - TestDedupeGroupSameChineseNumberStillMerges(同值/换算后同值 ⇒ 照常合并,门没退化成「有数字就不合」) - TestTidyQuantitiesChineseNumerals(解析口径:一小时=3600s、十五分钟=900s、半小时=1800s、 一万两千行=12000、第二天/一起/一直 不产生数量) 验收:go build ./... + go vet ./... + go test ./... 全绿;-race ./internal/runtime/memory ok。
This commit is contained in:
1 parent
54e32304f2
commit
7700a7eda0
5 files changed
+601
-86
No files matched your search
@@ -0,0 +1,128 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 本文件是 `dedupe_group` **中文数字门**(r2 复审 P2)的验收用例集:
|
||||
// 中位数写法下的数值冲突(一小时 vs 十五分钟、第二杯 vs 一杯)、单位不可换算的
|
||||
// 保守拒绝、同值照常合并,以及中文数字解析口径本身。
|
||||
|
||||
// TestDedupeGroupRejectsConflictingChineseNumbers 是 r2 复审 **P2 的回归用例**:
|
||||
// 语料里的数字大多写成**中文**,而 `sameNumbers` 只抽 ASCII 数字 token ⇒
|
||||
// 「午睡一小时 vs 午睡十五分钟」「咖啡不续第二杯 vs 不超过一杯」这类**数值冲突**
|
||||
// 曾被当成同一事实合并掉。
|
||||
//
|
||||
// 旧逻辑(只有 sameNumbers)下这些对会成簇 ⇒ 用例红;补中文数字门后不成簇 ⇒ 绿。
|
||||
func TestDedupeGroupRejectsConflictingChineseNumbers(t *testing.T) {
|
||||
t.Parallel()
|
||||
base := tidyTestTime(t)
|
||||
cases := []struct {
|
||||
name string
|
||||
survivor string
|
||||
member string
|
||||
tagsA []string
|
||||
tagsB []string
|
||||
whyItReds string
|
||||
}{
|
||||
{
|
||||
name: "午睡时长(一小时 vs 十五分钟)",
|
||||
survivor: "用户有午睡习惯,与助手约定午睡一小时、到点叫醒。",
|
||||
member: "主人与人家约定:午睡十五分钟,由人家守时看着时间把她叫醒。",
|
||||
tagsA: []string{"作息", "午睡"},
|
||||
tagsB: []string{"作息", "午睡"},
|
||||
},
|
||||
{
|
||||
name: "咖啡杯数(第二杯 vs 一杯)",
|
||||
survivor: "用户被约定:一天咖啡不许续第二杯,下午犯困时起身走动、喝温水。",
|
||||
member: "用户有喝咖啡的习惯,助手提醒她每天不超过一杯(第二杯不许续)。",
|
||||
tagsA: []string{"习惯", "咖啡"},
|
||||
tagsB: []string{"习惯", "咖啡"},
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
a := dedupeTestItem(tc.survivor, tc.tagsA, base)
|
||||
b := dedupeTestItem(tc.member, tc.tagsB, base.Add(time.Minute))
|
||||
// 先钉住「这不是因为别的门挡住的」:数值门确实报出冲突。
|
||||
if gaps := dedupeNumberConflicts(b.Content, a.Content); len(gaps) == 0 {
|
||||
t.Fatalf("中文数字门没报出冲突:%q vs %q", b.Content, a.Content)
|
||||
}
|
||||
if cands, _ := dedupeCandsOf(t, newTestStore(t), []MemoryItem{a, b}); len(cands) != 0 {
|
||||
t.Fatalf("簇数 = %d, want 0(数值冲突=不同事实,§5.2-2)", len(cands))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestDedupeGroupRejectsNonConvertibleUnits 钉住「单位换算不到一起 ⇒ 宁可不合并」
|
||||
// (r2 复审的明确口径):两杯 vs 三瓶是同一个计数量的不同取值,但杯/瓶不能互换。
|
||||
func TestDedupeGroupRejectsNonConvertibleUnits(t *testing.T) {
|
||||
t.Parallel()
|
||||
base := tidyTestTime(t)
|
||||
tags := []string{"习惯", "饮水"}
|
||||
a := dedupeTestItem("主人每天要喝两杯咖啡,下午不续。", tags, base)
|
||||
b := dedupeTestItem("主人每天要喝三瓶水,下午也要喝。", tags, base.Add(time.Minute))
|
||||
if gaps := dedupeNumberConflicts(b.Content, a.Content); len(gaps) == 0 {
|
||||
t.Fatalf("单位不可换算没有报冲突:%q vs %q", b.Content, a.Content)
|
||||
}
|
||||
if cands, _ := dedupeCandsOf(t, newTestStore(t), []MemoryItem{a, b}); len(cands) != 0 {
|
||||
t.Fatalf("簇数 = %d, want 0(单位不可换算 ⇒ 保守不合并)", len(cands))
|
||||
}
|
||||
}
|
||||
|
||||
// TestDedupeGroupSameChineseNumberStillMerges 是中文数字门的**反向护栏**:
|
||||
// 值相等(含不同单位换算后相等)照常成簇 —— 门收的是「冲突」,不是「出现数字」。
|
||||
func TestDedupeGroupSameChineseNumberStillMerges(t *testing.T) {
|
||||
t.Parallel()
|
||||
base := tidyTestTime(t)
|
||||
tags := []string{"作息", "午睡"}
|
||||
a := dedupeTestItem("主人午睡一小时,午后精神好。", tags, base)
|
||||
b := dedupeTestItem("主人午睡一小时,下午很清醒。", tags, base.Add(time.Minute))
|
||||
// 一条写「一个小时」、另一条写「一小时」:换算后同为 3600 秒,属于同一事实。
|
||||
c := dedupeTestItem("主人午睡一个小时,午后精神不错。", tags, base.Add(2*time.Minute))
|
||||
cands, _ := dedupeCandsOf(t, newTestStore(t), []MemoryItem{a, b, c})
|
||||
if len(cands) != 1 {
|
||||
t.Fatalf("簇数 = %d, want 1(同值照常合并 —— 门不许退化成「有中文数字就不合」)", len(cands))
|
||||
}
|
||||
}
|
||||
|
||||
// TestTidyQuantitiesChineseNumerals 钉住中文数字的解析口径(含单位换算与习语排除)。
|
||||
func TestTidyQuantitiesChineseNumerals(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
text string
|
||||
value float64
|
||||
unit string
|
||||
dim string
|
||||
ok bool
|
||||
}{
|
||||
{"主人午睡一小时", 3600, "小时", tidyDimTime, true},
|
||||
{"主人午睡十五分钟", 900, "分钟", tidyDimTime, true},
|
||||
{"主人午睡半个小时", 1800, "小时", tidyDimTime, true},
|
||||
{"一万两千行的验收纸条", 12000, "行", tidyDimText, true},
|
||||
{"每天要喝两杯咖啡", 2, "杯", tidyDimCount, true},
|
||||
// 序数与习语:不是数量。
|
||||
{"第二天再说", 0, "", "", false},
|
||||
{"一起散步", 0, "", "", false},
|
||||
{"一直加班", 0, "", "", false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
got := tidyQuantities(tc.text)
|
||||
if !tc.ok {
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("%q 应不产生数量,得到 %+v", tc.text, got)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if len(got) == 0 {
|
||||
t.Fatalf("%q 没解析出数量", tc.text)
|
||||
}
|
||||
if got[0].value != tc.value || got[0].unit != tc.unit || got[0].dim != tc.dim {
|
||||
t.Fatalf("%q 解析为 %+v, want value=%v unit=%s dim=%s",
|
||||
tc.text, got[0], tc.value, tc.unit, tc.dim)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user