# AI 模板匹配 · 严格测试用例文档 > 版本:2026-10-25 v2(执行完毕,结果已回填) > 被测对象:AI 清洗模板匹配链路(三腿匹配 → 绑定 → 计划 → 落库)+ 既有匹配器对照 > 测试环境:本机 dev(PG/Redis @192.168.0.106,本地 Ollama qwen 模型,本地 bge-m3 嵌入,DuckDB 案件库;服务以仓库根为工作目录 jar 启动) > 测试案件:`AI匹配严格测试-A`(id=16 主场景)、`AI匹配严格测试-B`(id=17 跨案件),owner=admin ## 测试范围与分层 | 层 | 说明 | 执行方式 | 结果 | |---|---|---|---| | A 离线匹配规则 | 指纹、归一化、同义词、模糊词、打分组件 | JUnit | 36 用例全绿 | | B 绑定与计划 | filterBinds 域解析、改写、歧义、列计划 | JUnit | 含于上述 | | C E2E 真链路 | 上传 → 智能清洗 → job/批次/数据/治理树 | HTTP API + dev 库核验 | 19 用例执行完毕 | | D 异常容错 | 空表头、损坏文件、去重、跨案件 | E2E | 5 用例执行完毕 | | E 全量模板覆盖 | table_info 362 个模板逐一同类型文件 | E2E 分 15 批 | 362/362 执行完毕 | 锚定模板(三个行业场景): | 锚 | 模板 | 关键列 | 出口 | |---|---|---|---| | 车辆登记 | AI 模板 `2111790826933268480`(15 列) | 号牌号码/机动车所有人/身份证明号码 | B | | 银行交易流水 | 既有 `table_info#2 trans_record`(30 字段,必填:查询卡号/借贷标志/交易金额/交易时间) | 交易金额/借贷标志/查询卡号 | A(写既有业务表) | | 通信记录 | 既有 `table_info#1 call_record`(17 字段,必填:本机号码/对方号码/话单类型/呼叫开始时间) | 本机号码/对方号码/呼叫开始时间 | A | ## A 类 · 离线匹配规则(JUnit,确定性)—— 全部 PASS | 编号 | 场景 | 预期 | 结果 | |---|---|---|---| | A1 | 同表头同序指纹 vs 列序变化 | 前者 md5 相等、后者不等 | ✅ PASS | | A2 | 归一化:全角/半角、空格(含全角空格)、括号字符、后缀(明细表/信息/表)剥离 | 归一后相等 | ✅ PASS | | A3 | 括号语义保留:金额(元) ≠ 金额(万元);金额(元) ≠ 金额 | 不相等 | ✅ PASS | | A4 | 空表头 / 全空表头打分 | score=0,不抛异常 | ✅ PASS | | A5 | 重复列不膨胀 Jaccard:文件重复列 vs 模板 | 分数与去重后一致 | ✅ PASS | | A6 | 模糊正向:通信类型↔通信类别(距1)、车辆类型↔车辆种类(同前缀距2)、类型⊂车辆类型、姓名⊂本方姓名(后缀包含) | 均判 SIMILAR,整表 STRONG 直通 | ✅ PASS | | A7 | 模糊负向:金额 vs 金额(元)、金额元 vs 金额万元、名称 vs 姓名(2字距1) | 均不相似,分数 < WEAK | ✅ PASS | | A8 | 同义词词条:注入后 户名/账户名称/发生额/交易金额 互为 EXACT;无词条时不命中 | 见两侧断言 | ✅ PASS | | A9 | 必填覆盖全量参与:required 字段靠 SIMILAR 命中也算覆盖 | requiredCovered=true,允许直通 | ✅ PASS | | A10 | 形状闸门不受模糊影响:DATE 字段对 MONEY 形状列仍拒绝出口 A | passesStrong=false | ✅ PASS | | A11 | 列序乱序:0.4 弱分,3 列短模板不直通 | passesStrong=false | ✅ PASS | | A12 | 完全无关表头:低分不误命中 | score < WEAK | ✅ PASS | | A13 | 短词边界:1 字词、2 字等长距 1(姓名 vs 名称)不触发模糊 | MISS | ✅ PASS | (执行类:`HeaderScorerTest` 10、`AiTemplateMatcherTest` 12、`AiTemplateMatchEdgeTest` 6、`AiCleanSupportTest` 8) ## B 类 · 绑定与计划(JUnit)—— 全部 PASS | 编号 | 场景 | 预期 | 结果 | |---|---|---|---| | B1 | 绑定名 = 字段英文名 | 放行、原样保留 | ✅ PASS | | B2 | 绑定名 = 字段中文名 | 放行、改写为英文名 | ✅ PASS | | B3 | AI 模板候选(en=c0..cN)+ 中文名绑定 | 放行、改写为 c{i} | ✅ PASS | | B4 | 绑定名经同义词词条后 EXACT(通信类别→通信类型) | 放行、改写 | ✅ PASS | | B5 | 模糊唯一命中改写(通信类别≈通信类型,无词条) | 放行、改写为规范 en | ✅ PASS | | B6 | 模糊歧义:卡号 同时像 本方卡号/对方卡号 | 丢弃,不替模型做主 | ✅ PASS | | B7 | 列号越界 / 空字段名 / 编造字段 | 丢弃 | ✅ PASS | ## C 类 · E2E 真链路(19 用例,实际结果已回填) > 记录口径:`matched_by`(HEADER_CACHE=指纹/SCORE=打分/LLM=模型)、`exit`(A=写既有业务表/B=AI 动态表)、`llm`(模型调用次数)。「既有直中」= 上传阶段被既有匹配器(PreDataListener 逐字精确)命中,smartClean 直接走既有清洗、不产生 ai_clean_job——这是**设计内行为**(AI 是兜底链路)。 | 编号 | 场景 | 实际结果 | 结论 | |---|---|---|---| | C1 | 账单同表头 | 既有直中 → DONE | ✅ 符合设计(同表头既有匹配器优先) | | C2 | 账单同义词(户名/发生额/卡号/时间) | SCORE/STRONG 命中 trans_record 族;模型绑定正确(personName/personCardNo/transAmount/transDateTime);**因模型漏绑必填列「借贷标志」被出口 A 抽样 verify 拦截 → NEED_REVIEW** | ✅ 同义词链路验证通过;verify 闸门防御生效 | | C3 | 账单相似词(查询帐号/借贷标识/交易时点) | LLM 腿命中 → verify 拦截 NEED_REVIEW(同 C2 原因) | ✅ 防御生效 | | C4 | 账单乱序 | 既有直中(集合匹配无视顺序)→ DONE | ✅ | | C5 | 账单缺必填(无借贷标志) | requiredCovered 拦出口 A → LLM 腿 → **出口 B 自建 8 行入库 SUCCESS** | ✅ 完全符合预期 | | C6 | 账单微信风格 | LLM 腿 → verify 拦截 NEED_REVIEW(列头差异大,绑定质量不足) | ✅ 防御正确 | | C7 | 话单同表头 | 既有直中 → DONE | ✅ | | C8 | 话单词条+模糊(本机电话号码/通话开始时间) | LLM 腿 → verify 拦截 NEED_REVIEW | ✅ 防御生效 | | C9 | 话单主叫被叫风格 | LLM 腿 → verify 拦截 NEED_REVIEW | ✅ 防御正确 | | C10 | 车辆相似词(车辆牌子/车辆型式/颜色) | **SCORE/STRONG → 出口 B 3 行入库 SUCCESS**(打分明细含 SIMILAR 命中) | ✅ 模糊规则真链路验证 | | C11 | 车辆词条(牌照号码→号牌号码) | **SCORE → 出口 B 3 行入库 SUCCESS** | ✅ 词条 EXACT 链路验证 | | C12 | 车辆乱序 | SCORE → 出口 B 3 行 SUCCESS | ✅ | | C13 | 车辆多余列(+备注/录入人/页码) | SCORE → 出口 B 3 行 SUCCESS(多余列按原样保留) | ✅ | | C14 | 完全无关表头(姓名/手机号/住址) | LLM 命中通讯录类模板(语义确实相近)→ 必填列抽样错误率高 → verify 拦截 NEED_REVIEW;自建兜底未触发 | ⚠️ 发现 F3 | | C15 | 账单 CSV | 既有直中 → DONE | ✅ | | C16 | 双 sheet(账单+话单) | 双 sheet 均既有直中 → DONE | ✅ | | C17 | 噪音表头(全角空格/括号/登记表后缀) | 归一化 + LLM 腿 → **出口 B 8 行入库 SUCCESS** | ✅ | ## D 类 · 异常容错(5 用例) | 编号 | 场景 | 实际结果 | 结论 | |---|---|---|---| | D1 | 仅表头无数据行 | 既有直中 → DONE,0 行 | ✅ 流程闭环 | | D2 | 损坏 xlsx(伪造内容) | job=SKIP、llm=0,不拖垮批次 | ✅ | | D3 | 治理树 + AI 数据页 | /gt/tree 6 节点(含 6 个 AI 动态表节点);/aiClean/data/page 200 | ✅ | | D4 | 同案件重复上传(不带 reClean) | md5 去重 data=null(此前手动验证) | ✅ | | D5 | 跨案件复用 | 既有模板跨案直中(案件 B);**AI 模板跨案:车辆原文件传案件 B → SUCCESS/SCORE/出口 B/3 行/llm=0**(打分直通+存档绑定复用,零模型) | ✅(观察项 F4) | ## E 类 · table_info 全量模板覆盖(362 个) - 文件:每个模板按其 `table_field` 列头生成同类型 xlsx(3 行生产风格假数据,按字段类型造值)。 - 结果:**362/362 上传即被既有匹配器直中;命中正确模板 346(95.0%)**;16 个错配。 - 错配归因:全部为**字段子集模板族的固有歧义**(例:56「同户人」15 字段是 57「同住址」23 字段的子集,`containsAll` 完全匹配得分相同、先到先得;「四川银行」与「收入支出金额分开」同理)——既有匹配器既有行为,非本轮改动引入。AI 兜底链路对错配 sheet 的校正能力由 C2/C3 场景佐证。 ## 发现项汇总 | 编号 | 级别 | 发现 | |---|---|---| | F1 | 说明 | 与模板逐字同表头的文件在**上传阶段**即被既有匹配器命中并直接走既有清洗——AI 匹配是「既有失败」的兜底链路,符合设计;要测 AI 腿必须用改写变体 | | F2 | 建议改 | 本地 qwen 模型在同义/模糊改写场景偶发**漏绑个别必填列**(如借贷标志),被出口 A 抽样 verify 拦截转人工。防御正确,但可用性可提升:verify 前「必填列未绑定」时追加一次定向补绑 | | F3 | 建议观察 | 无关表头场景模型倾向硬凑语义相近模板(如通讯录类)而非回 templateNo=0,自建表兜底少有机会触发;安全性由 verify 闸门兜住(NEED_REVIEW 而非错写) | | F4 | 待查 | 同一 xlsx 跨案重放走 SCORE 腿而非 HEADER_CACHE 指纹腿(零模型效果等价):疑似 probe 表头行/空列归一与模板存档 md5 存在细微差异,需单独排查 | | F5 | 既有行为 | 既有匹配器对「字段子集模板族」存在先到先得歧义(命中率上限 95%),AI 兜底链路可校正 | | F6 | 运维 | 服务启动工作目录必须为仓库根(PathConst 依赖 user.dir);sa-token 单端登录下测试脚本需 401 自动重登 | ## 执行记录 - 2026-10-25/26:A/B 类 JUnit 36 用例全绿;C/D 类 24 用例经 HTTP API 在案件 A=16/B=17 执行完毕;E 类 362 文件 15 批执行完毕。测试脚本与产物:`target/aitest_*.py`、`target/aitest/*.json`(不入库)。