对应用例文档:ai-llm-template-match-strict-20261026.md 结论先行:本轮在「AI 大模型模板匹配」链路发现 2 个 P0(AI 错配模板导致 0 行;既有模糊匹配把话单误判成通信登记信息并写错表)、3 个 P1(统计口径/金额清洗/本地模型并发)、3 个 P2(可观测性与模型输出校验)。 2026-10-26 已按确认范围修复 F-01、F-01B、F-03B、F-05、F-06、F-07(见第 6 节「修复记录」);F-01C、F-02、F-03、F-04、F-04B 按要求暂不处理。 复现文件全部保留在
docs/testing/fixtures/ai-llm-match-20261026/;执行结果 JSON 在results/。
| 项 | 值 |
|---|---|
| 后端 | ai-server @ http://127.0.0.1:8980/js/a,从仓库根启动 |
| 前端 | ai-electron/frontend @ http://localhost:3100 |
| 平台库 | PostgreSQL 192.168.0.106:5432/zsjz-ai |
| 模型 | Ollama qwen(Qwen3.8-27B)@ http://10.66.66.66:8080/v1;本地模型只支持 1 个并发会话(用户确认) |
| AI 判定并发 | 测试后期统一 -Dzsjz.ai-clean.max-concurrent=1;默认值 3 的结果见 F-04 |
| 嵌入 | 本地 bge-m3;AI 模板向量索引已就绪 |
| 工作目录 | E:\workspace\zsjz-ai(PathConst 依赖) |
| 账号 | admin / admin123 |
| 编号 | 级别 | 问题 | 复现文件 / 任务 | 状态 |
|---|---|---|---|---|
| F-01 | P0 | 交通银行同义表被错误判成「农商行交易流水」模板 48,出口 A 写业务表;因模板缺少必填「本方卡号」,8 行数据全部被拒,业务表 0 行 | bills/BILL-09_交通银行_同义.xlsx;案件 24,job 2114558438624731136 |
已修复(复测:上传闸门/兜底通过;模型裁决分支受端点阻塞) |
| F-01B | P0 | 导出话单/通信记录变体在上传阶段被既有模糊匹配器误判为「通信登记信息」模板 3;话单数据被写进 call_opening_info,只保留姓名/号码,通话方向/时间/时长等全部丢失;AI 链路根本没被触发 |
calls/CALL-09_导出话单_同义.xlsx;案件 25,批次 2114559120123633664 |
已修复(复测:上传阶段通过;模型裁决分支受端点阻塞) |
| F-01C | P2 | table_info 全量同表头覆盖:364 个模板中 345 正确、16 错配(子集模板先到先得)、3 失败(headers 与 table_field 不一致);12 个模板存在表头字段在 table_field 中不存在 | full-template/;results/full-exact_20261026_113820.json |
不修复(数据质量) |
| F-02 | P1 | 清洗完成统计把非数据行(标题/表头)算成“导入成功”:文件行显示成功 2、失败 8,但业务表 0 行;另一批次 UI 显示 32 条、实际 30 行 | 案件 24 同上;案件 19 批次 1792983591254364400 |
不修复 |
| F-03B | P1 | AI 动态表的 CALL_DIRECTION 列把方向字典的整串别名写进单元格(如“主叫,短信发送话单,呼出,01,主叫话单”),不是“主叫/被叫”;账单 AI 表的借贷方向也保留 借/贷/D/C/收/付 原值未归一 |
calls/CALL-02/03/10/18;bills/BILL-02 AI 表 |
已修复(复测通过:零模型 HEADER_CACHE 验证) |
| F-03 | P1 | 既有清洗对带千分位/货币符号/负号的金额未识别,直接写 0.00(1,234.50、-100.00、¥2,300.50、¥88.88、5,000.00 全部变 0.00) |
bills/BILL-01_通用流水_标准.xlsx;案件 19 trans_record_all |
不修复 |
| F-04 | P1 | 默认 max-concurrent=3 时,本地单会话模型 2/3 调用超过 180s 硬超时并重试,批次卡在 JUDGING;单并发后恢复正常 |
案件 20/21;日志 logs/backend-test2.log、backend-test3.log |
不修复(环境/配置) |
| F-04B | P1 | 文件 AI 识别调度器仍是 file-ai.max-concurrent=3;含 txt 的批次会与 AI 清洗争用同一单会话模型,导致 HTTP header parser received no bytes、AI 清洗 FAIL/重试 |
案件 26,EDGE-04_说明.txt + EDGE-06;日志 logs/backend-test5.log |
不修复 |
| F-05 | P2 | 出口 B 新建 AI 表的中文名与既有模板同名(“银行交易流水”/“通信记录”),处理日志只写“已写入 N 行(表 xxx)”,无法区分是既有业务表还是 AI 动态表 | 案件 19 批次 1792983591254364400 |
已修复(复测通过:新表名 AI_ 前缀 + 导入备注) |
| F-06 | P2 | 出口 A 的二次 askLlm 只送 1 个候选,但模型返回 templateNo=69 越界序号;该路径不校验序号,仍使用模型 binds |
案件 24 draft 见 results/bill_20261026_112312.json |
已修复(单测通过;E2E 受端点阻塞) |
| F-07 | P2 | 同义/相似词改写后模型能语义选对模板,但出口 A 的确定性闸门(必填覆盖)不通过,最终建 AI 表而非写既有业务表;用户看到“命中模板”却得到新表 | 案件 19 BILL-02/CALL-02 |
已修复(单测通过;E2E 受端点阻塞) |
复现文件
docs/testing/fixtures/ai-llm-match-20261026/bills/BILL-09_交通银行_同义.xlsx
表头(11 列):
银行卡号,借贷方向,发生额,交易日期,交易时间,姓名,对方户名,对方帐号,余额,对方金融机构网点名称,备注
相对模板 34「交通银行」的改写:
交易卡号→银行卡号;借贷方标志→借贷方向;金额→发生额
复现步骤
AI大模型匹配话单行业-1026)。BILL-09_交通银行_同义.xlsx(工作表「交行流水」,表头在第 2 行)。ai_clean_job / 导入文件页 / trans_record_all。实际结果
{
"job_id": 2114558438624731136,
"status": "DISPATCHED_A",
"exit_code": "A",
"matched_by": "SCORE",
"match_tier": "STRONG",
"ref_template_id": 48,
"llm_calls": 1,
"confidence": 0.9185,
"file_status": "FILE_CLEAN_COMPLETE",
"data_num": 10,
"success_num": 2,
"fail_num": 8
}
ai_clean_job.ref_template_id = 48,模板 48 是「农商行交易流水」,不是期望的 34「交通银行」。打分明细(logs/backend-test5.log,job 2114558438624731136):
AI 模板打分明细: #0 交易方向=SIMILAR(≈交易金额) | #1 交易金额=EXACT | #2 日期=EXACT |
#3 交易网点=SIMILAR(≈交易金额) | #4 对方户名=EXACT | #5 对方账号=SIMILAR(≈对方户名) |
#6 交易名称=SIMILAR(≈交易金额) | #7 余额=EXACT ← 农商行交易流水
8 条 clean_error_log 全部是:
第2行 ~ 第9行,[本方卡号] 数据为空
trans_record_all 实际 total = 0;文件行却显示 successNum=2, failNum=8。
导入文件页备注:AI 匹配模板:农商行交易流水。
界面复核
导入成功 2 / 导入失败 8,清洗方式/备注在 sheet 行(API 返回 cleanNote=AI 匹配模板:农商行交易流水)。期望结果
NEED_REVIEW,不写任何业务表。根因线索(代码走读)
HeaderScorer.score 的覆盖率为「模板列被文件命中多少」:coverage = hit / tplSet.size();文件侧多出来的列完全不扣分(HeaderScorer.java 的 score)。模板 48 只有 8 列,交行文件 11 列,7 个模板列命中即可拿高覆盖。交易方向 与 交易金额 判为 SIMILAR(等长、共同前缀“交易”、编辑距离 ≤2),掩盖了“借贷方向/发生额”并不是同一个字段。passesStrong 只校验候选模板的 required 是否覆盖 + 形状安全;候选模板 48 的必填(交易方向/交易金额/交易日期)都“覆盖”了,于是直接出口 A,没有进入模型候选裁决。main_id=2,既有清洗主字段包含必填「本方卡号」;该字段不在文件表头中,导致 8 行全部拒绝,业务表 0 行。修复方式(2026-10-26 已实施)
HeaderScorer 新增文件侧覆盖率(文件列有多少能在模板上找到归宿),并作为 score 的 header 组件之一(0.60×模板列命中率 + 0.20×Jaccard + 0.20×文件侧覆盖率);出口 A 另设硬门槛 FILE_COVERAGE_MIN = 0.6,passesStrong 与 AiTemplateMatcher.exitOf 都校验。similarWords 的「等长 + 共同前缀 2 + 编辑距离 2」规则:差异尾部必须至少共享 1 个字,交易方向/交易金额、交易网点/交易金额、交易日期/交易时间 不再判 SIMILAR;车辆类型/车辆种类(共享「类」)等既有业务词对保持不变。HeaderScorerTest#narrowTemplateCannotHijackWideFile、#wideTemplateCoversWholeFile、#fuzzyNeverCollapsesDifferentBusinessTokens。复现文件
docs/testing/fixtures/ai-llm-match-20261026/calls/CALL-09_导出话单_同义.xlsx
表头(11 列,按导出话单 42 的同义改写):
本机号码,对方号码,通话类型,通话时间,使用人姓名,通话时长,
本方基站大区号,本方基站小区号,本方证件号码,本方号码归属地,对方号码归属地
改写:本方号码→本机号码、主被叫标识→通话类型、本方姓名→使用人姓名;期望模板 42「导出话单」。
复现步骤
CALL-09_导出话单_同义.xlsx。SHEET_TEMPLATE_MATCHED_SUCCESS、templateId=3(「通信登记信息」),没有进入 AI 判定。call_opening_info。实际结果
{
"file": "CALL-09_导出话单_同义.xlsx",
"sheet": "导出话单",
"file_status": "FILE_CLEAN_COMPLETE",
"template_id": 3,
"data_num": 11,
"success_num": 11,
"fail_num": 0,
"expected_template_id": 42
}
call_opening_info 实际写入 10 行,只有 personName / personPhone 有值;operatorId / activationDateTime / phoneType / phoneStatus 全为空。CALL-08(导出话单标准表头)被正确直中模板 42,说明模板本身可用;问题出在变体表头的匹配决策。根因线索
PreDataListener.matchTemplate 的“部分匹配”分支:
if (CollUtil.containsAll(tempFields, headVals)) { ... } // 完全匹配
else if (CollUtil.containsAll(headVals, tempMatchedFields)) {
// 只要文件表头包含模板所有 matched=1 字段,就进入部分匹配
matchTemplateMap.put(templateId, intersection.size() + mustSize);
}
模板 3 的 matched=1 字段只有两个:
使用人姓名, 本机号码
任何同时包含这两个表头的话单/导出话单,即使还有 9 个完全不同的列,也会成为模板 3 的候选;如果没有更强的完全匹配,就直接返回模板 3 并标记 SHEET_TEMPLATE_MATCHED_SUCCESS。AI 清洗只接管 SHEET_TEMPLATE_MATCHED_FAIL,所以这类文件永远不会进入 AI 匹配。
同一问题也影响:
CALL-17_通信日期时间_同义.xlsx:期望模板 64,实际上传直中模板 3;CALL-19_时长格式.xlsx:期望模板 1,实际上传直中模板 3;IND-03_银行开户信息.xlsx(跨行业组):期望模板 4「银行开户信息」(20 列),实际上传直中模板 2084276161「建设银行开户记录」(6 列),14 列数据不会被映射;IND-13_出入境记录.xlsx(跨行业组):期望模板 52「出入境记录信息」(16 列),实际上传直中模板 596849484「出入境信息查询」(5 列),其余 11 列不会进入清洗。跨行业例子的模板情况:
模板 4「银行开户信息」matched=1 字段:客户名称, 卡号
模板 2084276161「建设银行开户记录」headers:户名,账号,证件号码,开户日期,余额,销户日期
文件 IND-03 表头:户名,证件号码,卡号,账号,开户日期,余额,可用余额,币种,...(20 列)
文件把 客户名称 改成了 户名,模板 4 的必填 客户名称 不再命中;子模板 2084276161 的 6 个列全部能在文件里找到,于是部分匹配直接返回子模板,20 列里其余 14 列不会进入清洗。
期望
SHEET_TEMPLATE_MATCHED_FAIL,交由 AI 模板匹配裁决。修复方式(2026-10-26 已实施)
PreDataListener.matchTemplate 的「部分匹配」分支增加覆盖率阈值:重叠列数(交集 + 必填命中)必须 ≥ 0.6 × max(文件列数, 模板列数),且不少于 3 列(小模板按 min(3, 文件列, 模板列) 兜底)。SHEET_TEMPLATE_MATCHED_FAIL,由智能清洗的 AI 模板匹配裁决;完全匹配(表头逐字相同 / 模板包含全部表头)不受影响。PreDataListenerMatchTest#lowCoverageVariantFallsThroughToAi(真 xlsx + 真 Fesod + 真 DuckDB)、#exactHeaderStillMatchesAtUpload、#partialMatchNeedsBothSidesCoverage。执行方式
table_info 每个模板的 headers 生成 3 行生产风格数据,表头放在模板声明的 line_no,共 364 个 xlsx。run_tests.py full-exact --upload-only),验证上传阶段既有匹配器能否正确直中;文件在 full-template/,结果在 results/full-exact_20261026_113820.json。结果
files=364 sheet_success=361 sheet_fail=3
命中模板 id 与期望一致:345
命中模板 id 不一致:16
未命中(SHEET_TEMPLATE_MATCHED_FAIL):3
16 个错配(全部是子集模板先到先得)
| 期望模板 | 实际命中 |
|---|---|
| 57 同住址 | 56 同户人 |
| 88496327 成都银行交易流水纪委调单 | 1746257711 |
| 155064567 成都银行流水 | 1746257711 |
| 687030857 农村商业银行成都开户信息 | 1106262359 |
| 759819185 成都银行开户信息(DQXX) | 1720897654 |
| 772432199 电信通联记录 | 1940093260 |
| 798524514 语音清单 | 1940093260 |
| 816952604 平安银行交易流水纪委调单 | 1699503043 |
| 1018443797 四川银行 | 5 |
| 1020340504 单位工行开户信息 | 847945890 |
| 1149206312 成都银行交易(JYLS) | 95695751 |
| 1318768623 短信明细记录 | 584544982 |
| 1450844531 浦发银行调取账单流水 | 1302533924 |
| 1517022216 中国银行信用卡交易记录 | 294976394 |
| 1678225864 通信登记信息央地协查 | 23 电信通信登记信息 |
| 1820032851 工商银行收付款明细 | 11548586 |
3 个失败
| 模板 | 原因 |
|---|---|
| 63 在逃同案撤销人员 | table_info.headers 有「在逃人员DNA编号」,table_field 只有「在逃人员人员DNA编号」,差一个字,完全匹配失败 |
| 466958473(名称乱码) | 表头/字段中文均为乱码,且 headers 中有字段名不存在 |
| 1652978732(名称乱码) | 同上,多个表头字段在 table_field 中不存在 |
12 个模板存在 headers 字段不在 table_field 中
6 转入转出账号(日期时间): 日志号, 传票号
7 主账号与收付款账号组合(日期时间): 户名, 可用余额
10 主叫被叫: cell_id, BILLING_VISIT_AREA_CODE通话地
11 借贷方发生额(日期时间): 明细号
17 同航班服务: 乘机时间, 到达日期, 到达时间
19 铁路订票信息查询: 发车时间
24 快递收发子模版: 物品名称
58 同车违章: 车牌号码
63 在逃同案撤销人员: 在逃人员DNA编号
466958473 / 1652978732: 名称/字段乱码 + 多个字段缺失
543445511 郴州农商银行: 本方卡号
结论
table_info.headers 与 table_field 元数据不一致,以及既有匹配器「子集模板先到先得」的既有行为。复现 A(业务表 0 行但显示成功 2)
数据量 10 / 导入成功 2 / 导入失败 8;trans_record_all total=0。复现 B(UI 32 条,实际 30 行)
1792983591254364400:BILL-01_通用流水_标准.xlsx(标题行 + 表头 + 8 数据行,既有直中)、BILL-02(AI 10 行)、CALL-02(AI 12 行)。trans_record_all 8 行 + AI 表 10 行 + AI 表 12 行 = 30 行。ai_clean_batch.loaded_rows = 22(只统计 AI 写入),既有链路按 file_info.dataNum - failNum = 10 - 0 = 10 计入,合计 32。根因线索
DmService 完成统计:
int dataNum = 0, failNum = 0;
for (FileInfo child : fileInfoList) {
dataNum += child.getDataNum(); // 上传预览的物理行数(含标题/表头)
failNum += child.getFailNum();
}
int completedCount = dataNum - failNum;
AiSmartCleanService 终态统计同样用 dataNum - failNum 作为既有 sheet 的“成功行数”。
因此只要文件有标题行/表头行,dataNum 就比真实数据行多,完成页会多报;而 AI 动态表用的是 rowLoaded,两种口径混在同一张卡里。
期望
rowLoaded 混算。复现文件:bills/BILL-01_通用流水_标准.xlsx(直接命中模板 2,走既有清洗)。
实际
| 源值 | trans_record_all.transAmount |
|---|---|
1,234.50 |
0.00 |
-100.00 |
0.00 |
200.50 |
200.50 |
¥2,300.50 |
0.00 |
100.00 |
100.00 |
¥88.88 |
0.00 |
5,000.00 |
0.00 |
45.60 |
45.60 |
根因线索
TransStdDataCleaner(main_id=2 使用)对 MONEY 字段:
if (ReUtil.isMatch(PatternPool.MONEY, copyContent)) {
...
} else {
copyContent = "0.00"; // 不匹配就静默写 0,不报错
}
带千分位、货币符号、负号的金额不匹配 PatternPool.MONEY,被静默改写为 0.00;行仍然算成功。该问题在既有链路,不是 AI 模板匹配新增,但会被 AI 出口 A 复用,影响生产账单。
期望
复现
CALL-18_主被叫方向.xlsx(案件 25,AI 模板 2114559632961183744)源值只有 主叫/被叫/呼入/呼出。/aiClean/data/page?aiTemplateId=2114559632961183744 实际:
c3 = "主叫,短信发送话单,呼出,01,主叫话单"
c3 = "被叫,短信接收话单,呼入,02,被叫话单"
c3 = "呼入"
CALL-02、CALL-03、CALL-10 的 AI 表同样出现整串别名。
账单 AI 表 BILL-02 的 借贷方向 则保留 借/贷/D/C/收/付 原值,没有归一成统一方向。
根因线索
table_field.direction_conf 的格式是「标准方向 → 逗号分隔别名」:
{"主叫":"主叫,短信发送话单,呼出,01,主叫话单",
"被叫":"被叫,短信接收话单,呼入,02,被叫话单"}
AiRecordCleaner.translateDirection 直接 dict.getOrDefault(v, v):
主叫 时返回整串 主叫,短信发送话单,...;GlobalCache.directionConfMap 的 tableId_value → key 逻辑一致),或至少当输入等于 key 时返回 key。该函数对所有 DEBIT_CREDIT_FLAG / CALL_DIRECTION 列生效;账单因为对应字段没有 direction_conf,才侥幸保留原值。
期望
修复方式(2026-10-26 已实施)
AiRecordCleaner 的方向翻译改为反向建表:按「标准方向 → 逗号分隔别名」解析后,建立 别名/标准词 → 标准方向 映射(与 GlobalCache.directionConfMap 的 tableId_别名 → key 同构);输入 主叫 返回 主叫,输入 呼出/01/主叫话单 同样返回 主叫。DEBIT_CREDIT_FLAG 归一为 收入/支出(借=D=付 → 支出,贷=C=收 → 收入),CALL_DIRECTION 归一为 主叫/被叫(呼出 → 主叫,呼入 → 被叫);不认识的值仍原样保留。AiRecordCleanerTest#directionDict(生产字典格式、标准词命中、别名命中、无字典兜底、坏 JSON)。复现
zsjz.ai-clean.max-concurrent=3,一次批次 6 个 AI sheet。日志(logs/backend-test2.log):
11:06:42 ERROR LlmRetry: AI 模板匹配 第 1 次调用失败,1051ms 后重试:
模型调用超时(总时长 180s): Ollama:qwen
11:06:42 ERROR ... 第 1 次调用失败 ...
11:07:12 ERROR ... 第 1 次调用失败 ...
批次长时间停在 JUDGING,ai_judged_count 不增长;单并发后同一模型单次 30–120s 正常完成。
结论
max-concurrent=1,或按模型提供方的并发能力配置;同时在 README/启动参数中写明。复现
EDGE-04_说明.txt(非表格,触发文件 AI 识别)和一个待 AI 判定的表格 EDGE-06_表头第10行.xlsx。logs/backend-test5.log:
11:34:18 ERROR [file-ai-0] 文件 AI 识别 第 1 次调用失败: 模型调用失败[Ollama:qwen]:
HTTP transport error during streaming: SSE/NDJSON stream failed:
java.io.IOException: HTTP/1.1 header parser received no bytes
11:35:04 ERROR [ai-clean-14] 直连模型调用失败: model=Ollama:qwen,
error=HTTP transport error during streaming: ... header parser received no bytes
11:35:04 INFO AI 清洗任务结束: jobId=..., status=FAIL, 原因=模型未返回可解析的规则草稿
11:34:59 ERROR 文件 AI 识别失败: fileName=EDGE-04_说明.txt, 耗时=59939ms
同一时间段 zsjz.file-ai.max-concurrent 仍是 3(启动日志 并发=3),而 AI 清洗已经降到 1;两个调度器各自持有并发,底层模型只有一个会话。
结论
ai-clean、file-ai(以及其它直调 LlmService 的链路)需要共享同一个模型信号量,或至少把 file-ai.max-concurrent 也设为 1。EDGE-06 的 FAIL 就是由此触发。案件 19 完成页:
「BILL-02_通用流水_同义词.xlsx」·银行流水 → 已写入 10 行(表「银行交易流水」)
ai_clean_* 动态表,中文名与既有模板「银行交易流水」完全相同。修复方式(2026-10-26 已实施)
AiCleanService.buildTableNameCn 统一给 AI 动态表中文名加 AI_ 前缀(幂等,sheet 名本身已带 AI_ 时不重复叠加;仍受 200 字符列宽约束)。治理树、导入文件备注、处理日志都直接读该字段,所以一处生效。已写入 N 行(AI 动态表 · ···);导入文件页备注区分为「AI 匹配既有模板:X」与「AI 新建动态表:Y」。AiCleanTableNamingTest(前缀、幂等、长度上限)。BILL-09 的 ai_clean_job.draft:
{"templateNo":69, ...}
该路径只发送了 1 个候选(模板 48),模型却返回 69(越界);代码仍使用其 binds 做列映射。
filterBinds 会过滤不存在的字段名,所以没有直接写脏列,但:
建议:二次 askLlm 同样校验 templateNo == 1(唯一候选),不一致时丢弃 binds,转 NEED_REVIEW。
修复方式(2026-10-26 已实施)
askLlm 的候选先重编号为 #1(AiTemplateMatcher.singleCandidate),提示词序号与后端校验口径一致。AiTemplateMatcher.candidateOf(candidates, draft):序号缺失、≤0、越界一律返回 null,两条模型调用路径共用。AiCleanService.judge 在二次调用后强制校验:templateNo != 1 时记录 ERROR 日志、保留原始草稿(ai_clean_job.draft 便于复盘)、丢弃 binds 并转 NEED_REVIEW。AiTemplateMatcherTest#singleCandidateMustAnswerNumberOne(含 templateNo=69 场景)。修复方式(2026-10-26 已实施,按“不改安全阈值、只把出口说清楚”处理)
命中既有模板「X」,走出口 A 写入既有业务表。模型命中既有模板「X」,但出口 A 闸门(档位/置信度、必填覆盖、确定性分数、文件列覆盖)未通过,改走出口 B 写入 AI 动态表;候选是 AI 模板/按表头新建时也各自点名。已写入 N 行(AI 动态表 · <来源>);导入文件页备注区分为「AI 匹配既有模板」/「AI 新建动态表」,配合 F-05 的 AI_ 前缀可直接分辨。AiSmartCleanServiceTest#exitBLogExplainsExistingTemplateDowngrade。| 用例 | 文件 | 结果 |
|---|---|---|
| UI-01~07 | 案件 19 | 上传→智能清洗→进度/日志/结果页全链路可用;AI 阶段 0.95 判定;完成统计见 F-02 |
| BILL-01 | 标准通用流水 | 既有直中;金额清洗见 F-03 |
| BILL-02 | 同义通用流水 | 模型 0.95 选中「银行交易流水」;出口 B 10 行;跨案第二次 HEADER_CACHE 复用 |
| BILL-03 | 相似词通用流水 | SCORE 出口 A,模板 2,置信 0.9320 |
| BILL-04 | 乱序+增列+必填换词 | SCORE 出口 A,模板 2,置信 0.9275 |
| BILL-09 | 交行同义 | P0 错配模板 48,0 行落库 |
| BILL-10 | 光大同义 | SCORE 出口 A,模板 35,置信 0.9320 |
| BILL-11 | 工行同义 | SCORE 出口 A,模板 36,置信 0.9291 |
| BILL-12 | 招行同义 | 主模型调用完成,因停止批次未收口(未计入结论) |
| CALL-01/02/03/08/09/10/17/18/19 | 话单核心 | 标准话单直中模板 1/42;CALL-02/03/10/18 走 AI 出口 B(12/12/10/12 行);CALL-09/17/19 被模板 3 吞掉(F-01B) |
| ALL-01 | 364 模板同表头 | 345 正确 / 16 错配 / 3 失败(详见 F-01C) |
full-template-variants/,可按需补跑。ai_clean_job、clean_error_log、服务日志与导入文件 API 数据佐证。范围:仅 F-01、F-01B、F-03B、F-05、F-06、F-07。F-01C(模板元数据质量)、F-02(完成统计口径)、F-03(既有金额清洗)、F-04/F-04B(并发配置)按要求不修改。 验证方式:本轮改动全部有单测覆盖(
mvn -pl ai-server test的 aiclean / dm 相关用例全绿);未重跑端到端模型链路(本地模型单会话,见第 5 节)。
| 编号 | 代码位置 | 改动要点 |
|---|---|---|
| F-01 | HeaderScorer、AiTemplateMatcher#exitOf |
新增文件侧覆盖率并计入分数与出口 A 硬门槛(FILE_COVERAGE_MIN=0.6);收紧等长相似词规则(差异尾部必须共享至少 1 个字) |
| F-01B | PreDataListener#matchTemplate |
部分匹配要求重叠列 ≥ 0.6×max(文件列数,模板列数) 且 ≥3 列;不足则保持 SHEET_TEMPLATE_MATCHED_FAIL 交 AI |
| F-03B | AiRecordCleaner |
direction_conf 反向建表(别名/标准词 → 标准方向)+ 解析缓存 + 缺失字典时的收支/主被叫兜底 |
| F-05 | AiCleanService、AiSmartCleanService、DmService |
AI 动态表中文名统一 AI_ 前缀(幂等、≤200 字);处理日志与导入备注点名「AI 动态表 / 既有业务表」 |
| F-06 | AiTemplateMatcher、AiCleanService |
二次 askLlm 单候选重编号为 #1;templateNo 缺失/越界丢弃 binds 转 NEED_REVIEW,原始草稿仍落库供复盘 |
| F-07 | AiCleanService、AiSmartCleanService、DmService |
出口 A/B 的日志与备注显式区分;模型命中既有模板却被闸门挡下时,日志写明原因;安全阈值不放松 |
新增/更新的单测:
module/aiclean/match/HeaderScorerTest # F-01 文件侧覆盖率 + 相似词收紧
module/aiclean/match/AiTemplateMatcherTest # F-06 candidateOf / singleCandidate
module/aiclean/exec/AiRecordCleanerTest # F-03B 方向字典反向 + 兜底
module/aiclean/service/AiCleanTableNamingTest # F-05 AI_ 前缀
module/aiclean/service/AiSmartCleanServiceTest # F-07 出口 B 日志说明
module/dm/pre/PreDataListenerMatchTest # F-01B 上传匹配(真 xlsx/Fesod/DuckDB)
补充:全量
mvn -pl ai-server test存在一个与本轮改动无关的既有失败 (PreDataListenerRawTest#previewTableFeedsAiLoader在整包运行时SpringUtil解析到真实的CaseRoutingDataSource,初始化预览表抛「请先打开案件后再操作数据」;用改动前的代码跑整包同样复现, 该类单独运行通过),本轮未处理。
代码:
6396cac;后端重启加载最新构建;本地模型单并发。 单测:mvn -pl ai-server clean test -Dtest='HeaderScorerTest,AiTemplateMatcherTest,AiRecordCleanerTest,AiCleanTableNamingTest,AiSmartCleanServiceTest,PreDataListenerMatchTest'→ 62 tests, 0 failures, 0 errors(logs/retest-unit.log)。 复测样本/结果:docs/testing/fixtures/ai-llm-match-20261026/retest/(案件 27)。
| 编号 | 复测方式 | 结果 |
|---|---|---|
| F-01 | 新案件上传 BILL-09 同义表 + 智能清洗 | 上传阶段 SHEET_TEMPLATE_MATCHED_FAIL(不再直中模板 48);job SELF_HEADER、出口 B、8/8 行写入 AI_交行流水,业务表未写入。PASS(修复的错配/丢数据路径已关闭) |
| F-01B | 新案件上传 CALL-09/CALL-17/CALL-19/IND-03 | 四者上传全部 SHEET_TEMPLATE_MATCHED_FAIL,不再命中模板 3 / 子模板 2084276161 / 596849484;CALL-09/17/19 各自建 AI 表并全列入库 10/10/12 行。PASS(上传闸门修复生效);IND-03 因模型端点异常 FAIL,但未写错表。 |
| F-03B | 复用修复前的 CALL-18 / BILL-02 AI 模板(HEADER_CACHE,零模型) | CALL-18 CALL_DIRECTION distinct = [主叫, 被叫];BILL-02 DEBIT_CREDIT_FLAG distinct = [支出, 收入]。PASS |
| F-05 | 新表命名 + 导入文件备注 | 复测新建表:AI_交行流水、AI_导出话单、AI_通信记录日期时间、AI_通信记录、AI_银行流水;导入备注均为「AI 新建动态表:AI_xxx」。PASS |
| F-06 | 单测 AiTemplateMatcherTest#singleCandidateMustAnswerNumberOne |
通过;模型端点异常,未能在 E2E 中自然复现 templateNo=69。单测 PASS |
| F-07 | 单测 AiSmartCleanServiceTest#exitBLogExplainsExistingTemplateDowngrade |
通过;模型端点异常,未能在 E2E 中触发“模型命中既有模板但闸门未过”。单测 PASS |
复测重启后,本地模型端点出现新异常:
短提示(如“回复OK”、50 字以内)→ 200 正常流式返回
长提示(约 150–270 字以上)→ 连接被远端重置:
HTTP transport error during streaming: SSE/NDJSON stream failed:
java.io.IOException: HTTP/1.1 header parser received no bytes
SELF_HEADER 安全兜底,1 个(IND-03)FAIL: 模型未返回可解析的规则草稿。retest/retest_evidence.json、retest/retest_fixes.py、retest/RT_*.xlsx;.gitignore 忽略,不随提交):retest/retest_run.log、logs/backend-retest.log、logs/retest-unit.log。CALL-18/BILL-02 的新建 AI 表把「呼叫类型/借贷方向」推断为 TEXT,数据里仍是 主叫/被叫/呼入、借/贷/D,没有归一成 主叫/被叫、收入/支出;而带 CALL_DIRECTION/DEBIT_CREDIT_FLAG 类型的 HEADER_CACHE 表已正确归一(F-03B 复测通过)。AI_ 前缀只作用于新建模板;修复前已存在的 AI 模板(HEADER_CACHE 复用)保留原名,这是设计约定,复测中已确认。