cc 3 dni temu
rodzic
commit
dece5fc241

+ 74 - 0
.zcode/plans/plan-session_5ce997073506749453fef99d8560efa7.md

@@ -0,0 +1,74 @@
+# 同义词/相似词匹配设计方案(提升模板匹配成功率)
+
+## 目标与边界
+
+- **目标**:让「通信类型≈通信类别」「本方姓名≈姓名」「车辆种类≈车辆类型」这类同义/近义表头在模板匹配打分与列绑定中被识别,提高 SCORE 腿直通率与绑定成功率。
+- **范围**:只改 AI 清洗链路(HeaderScorer 打分 + filterBinds 绑定域)。**不动** PreDataListener 既有匹配器、**不动** HeaderFingerprint/HeaderNormalizer/SheetProbe 指纹链路(同义词一旦进指纹,存量模板 headerMd5 全部失效——硬边界)。
+- **策略**(按你的选择):词对存 PG + 管理接口;模糊匹配全量参与计分与直通闸门;出口 A 后既有的运行时安全垫(形状闸门 + 抽样 verify + 置信度放行线)全部保留。
+
+## 一、同义词库表 + 管理(module/aiclean)
+
+**建表**(追加到 `sql/ai_clean.sql`,另出存量库迁移脚本 `sql/ai_clean_synonym_init.sql`):
+
+```sql
+create table if not exists ai_clean_synonym (
+    id          bigint primary key,
+    std_word    varchar(100) not null,   -- 标准词(组内规范形,如 交易金额)
+    alias_word  varchar(100) not null,   -- 别名(如 发生额)
+    remark      varchar(200),
+    status      smallint default 1,      -- 1 启用 / 0 停用
+    create_by   bigint,
+    create_time timestamp default current_timestamp,
+    update_time timestamp
+);
+create unique index if not exists uk_ai_clean_synonym on ai_clean_synonym (std_word, alias_word);
+```
+
+**词组模型(已确认)**:星型「别名 → 标准词」,一个标准词挂任意多别名;**同一归一化词全表只允许出现一次**(Service 新增/修改时校验,撞组直接报错,杜绝一个别名归两组的归一歧义);不做链式传递(新词必须直接指到标准词);「一个词对应多个模板字段」的泛化场景不进表,交给模糊层(包含关系天然双向)。
+
+初始化数据 = HeaderScorer 现有 24 条硬编码词条迁进表(on conflict do nothing,可重复执行),代码删除 SYNONYMS 常量。
+
+**代码**(照 AiCleanBatch / TableInfoService 现成模式):
+- `entity/AiCleanSynonym` + `mapper/AiCleanSynonymMapper`。
+- `service/AiCleanSynonymService`:list / create / update / delete / toggleStatus + 全局唯一校验;写操作事务 **afterCommit** 调 `GlobalCache.rebuildGlobalCacheQuietly()`(照 TableInfoService.refreshGlobalCacheAfterCommit 范本)。
+- `api/AiCleanSynonymController`:`/aiClean/synonym`(GET 列表、POST 新增、POST 修改、POST 删除、POST 启停),Result<T> 包装。
+
+## 二、缓存派生(GlobalCache)
+
+- `GlobalCache.initCache()` 全量读 `ai_clean_synonym`(status=1),构建 `volatile Map<String,String> synonymMap`(key = normalize(alias_word),value = normalize(std_word),整体替换、空表保留旧快照,照 directionConfMap 写法)。
+- 新增测试注入口(`loadSnapshotForTesting` 扩展或 `setSynonymsForTesting`),单测不起 Spring 也能喂词对。
+- `HeaderScorer.canonicalForScore` = `synonymMap.getOrDefault(normalize(word), normalize(word))`;缓存未初始化时退化为现状。
+
+## 三、打分接入:精确同义 + 模糊相似(全量参与)
+
+**HeaderScorer 新增统一匹配函数** `matchWord(fileWord, tplWord)` 返回 EXACT / SIMILAR / MISS:
+1. 双方过 canonicalForScore 后相等 → EXACT;
+2. 归一化后**包含关系**(一方包含另一方且短方 ≥2 字,如 本方姓名⊃姓名、类型⊂车辆类型)→ SIMILAR;
+3. **编辑距离 ≤1 且长度 ≥3**(通信类型↔通信类别、车辆类型↔车辆种类 均距离 1)→ SIMILAR;
+4. 其余 → MISS。
+
+**全量参与**:coverage / jaccard / fieldCoverage / requiredCoverage / passesStrong 里的 `set.contains` 全部换成 matchWord——SIMILAR 与 EXACT 同权(含必填覆盖与出口 A 直通)。实现上把「集合 contains」改为「逐模板列在文件列集合找 EXACT/SIMILAR 命中」,加 per-call memo(15×15 短串比较 × 360 模板,微秒~毫秒级)。
+
+**歧义护栏**:文件列模糊命中**多个**模板字段时(如 本方卡号/对方卡号 同在),歧义只允许存在于打分,绝不带进绑定(见四);EXACT 永远优先于 SIMILAR。
+
+**可观测**:打分入口 log.debug 一行逐列命中明细(`列#5 号牌号码=EXACT / 通信类别→通信类型=SYNONYM / 备注=MISS`)。
+
+## 四、绑定域接入(AiTemplateMatcher.filterBinds)
+
+现有 en∪cn 双域查表保持,查不到时追加模糊解析:对候选模板字段做 matchWord,**唯一命中**才把 bind 的 fieldNameEn 改写为该字段规范 en;命中 0 个或 ≥2 个一律丢弃该绑定(该列在 forAi 按原样 TEXT 保留,语义绑定交给模型腿)。EXACT 优先于 SIMILAR。
+
+## 五、测试
+
+- `HeaderScorerTest`:配置词条后 通信类型/通信类别 整表分数提升并直通;无词条时不命中(回归锚);编辑距离对、包含对;歧义绑定丢弃。
+- `AiTemplateMatcherTest`:filterBinds 模糊唯一命中改写、歧义丢弃。
+- 全量 `mvn -pl ai-server test -Dtest="com.zsjz.ai.module.aiclean.**"` 回归(152 用例基线)。
+- E2E:插 `通信类型=通信类别` 词对 → 上传含该列差异的表验证直通与绑定;现有两个车辆 xls 回归不劣化。
+
+## 六、本期不做(二期记录)
+
+- 词级向量相似(bge-m3 embedBatch + 词向量缓存 + cosine 阈值):模糊层已有编辑距离/包含,边际收益小,且本地冷启动 20-60s、在线模型无批量接口;确需时照 AiCleanTemplateVectorIndex 的 pgvector 模式加。
+- 既有匹配器 PreDataListener 接入同义词(影响全部上传路径,另行评估)。
+
+## 风险声明(全量参与)
+
+模糊命中进入出口 A 直通闸门意味着「像」可能被判成「就是」。既有安全垫不变:WEAK 不走 A、形状安全、出口 A 前抽样 verify(必填列错误率过高转人工)、置信度放行线、既有业务表可按文件清理。词条维护建议全词映射、避免过宽词对(如「类别→类型」单字级映射会把不相关模板拉高分);「一个词对应多个模板字段」的泛化匹配交给模糊层而非词条。