LLM 模板匹配测试与夹具.md 22 KB

LLM 模板匹配测试与夹具

本文引用的文件

  • ai-template-match-test-cases.md
  • manifest.json
  • generate.py
  • run_tests.py

目录

  1. 引言
  2. 被测对象与测试分层
  3. 测试环境与运行前置条件
  4. 测试夹具体系:manifest、generate、run_tests
  5. 锚定模板与三类行业场景
  6. A/E 类测试场景逐项说明
  7. 执行命令与结果读取
  8. 失败排查步骤
  9. NEED_REVIEW 行为解读
  10. F1–F6 发现项与建议
  11. 架构与数据流总览
  12. 结论

引言

本文件基于 docs/testing/ai-template-match-test-cases.md,系统化整理“AI 清洗模板匹配链路”的严格测试用例文档及其配套夹具。核心目标是帮助读者理解:

  • 被测系统是“三腿匹配 → 绑定 → 计划 → 落库”的 AI 清洗链路,并与既有匹配器进行对照;
  • 测试环境依赖 PostgreSQL、Redis、本地 Ollama qwen 模型、bge-m3 嵌入服务、DuckDB 案件库;
  • 夹具目录 fixtures/ai-llm-match-20261026 通过 manifest.json 描述清单,由 generate.py 生成 xlsx/csv 夹具,再由 run_tests.py 调用 HTTP API 上传并比对 matched_by、exit、llm_calls 等结果。

被测对象与测试分层

测试文档明确将验证范围分为 A/B/C/D/E 五层:

层级 含义 是否确定性 主要验证点
A 离线匹配规则(指纹、归一化、同义词、模糊词、打分) 是 JUnit 36 用例全绿
B 绑定与列计划(filterBinds 域解析、改写、歧义、列计划) 是 JUnit 含于上述
C E2E 真链路(上传 → 智能清洗 → job/批次/数据/治理树) 否 HTTP API + dev 库核验
D 异常容错(空表头、损坏文件、去重、跨案件) 否 E2E
E 全量 table_info 模板覆盖 否 362 个模板逐一同类型文件

被测流程可抽象为:

flowchart TD
    Upload["上传文件"] --> PreMatch["既有匹配器<br/>PreDataListener 精确命中"]
    PreMatch -->|命中| ExistingFlow["走既有清洗流程<br/>出口 A"]
    PreMatch -->|未命中| TemplateMatch["模板匹配三腿<br/>指纹/打分/LLM"]
    TemplateMatch --> Bind["字段绑定<br/>同义词/模糊/歧义处理"]
    Bind --> Plan["列计划<br/>requiredCovered/形状闸门"]
    Plan --> Exit{"出口判定"}
    Exit -->|写既有业务表| ExistingFlow
    Exit -->|AI 动态表| SelfBuilt["自建表落库<br/>出口 B"]
    Exit -->|校验失败| NeedReview["NEED_REVIEW<br/>人工复核"]

图表来源

  • ai-template-match-test-cases.md:1-111

章节来源

  • ai-template-match-test-cases.md:1-111

测试环境与运行前置条件

根据测试文档和脚本常量,测试环境应满足以下要求:

组件 要求 依据
数据库 PostgreSQL,地址 192.168.0.106:5432,库名 zsjz-ai,用户 postgres,密码 postgres generate.py、run_tests.py 中 DB 配置
Redis 本机 dev 环境存在 Redis,作为后端缓存与会话相关依赖 测试文档说明
Ollama 本地运行 qwen 模型,供 LLM 匹配使用 测试文档说明
bge-m3 本地提供向量嵌入能力 测试文档说明
DuckDB 本地案件库,用于加载案件数据 测试文档说明
工作目录 服务以仓库根为工作目录启动 测试文档 F6 指出 PathConst 依赖 user.dir
应用服务 HTTP API 监听 http://127.0.0.1:8980/js/a run_tests.py 中 API 常量
登录账号 测试默认使用 admin/admin123 run_tests.py 中 CREDS

需要注意:

  • generate.py 连接 PG 获取 table_info 元数据,因此生成夹具前需确保 PG 已就绪;
  • run_tests.py 不仅调 HTTP API,还直连 PG 查询 ai_clean_job、ai_clean_batch、ai_clean_template 等表回填结果;
  • 当前脚本默认使用 case id 20,但测试文档中主场景为 id=16、跨案件为 id=17;如需复现旧结果,可通过 --case 参数切换。

章节来源

  • ai-template-match-test-cases.md:1-15
  • ai-template-match-test-cases.md:100-111
  • generate.py:1-30
  • run_tests.py:1-30

测试夹具体系:manifest、generate、run_tests

夹具目录 fixtures/ai-llm-match-20261026 采用“清单驱动 + 生成器 + 执行器”的结构。

manifest.json:测试清单

manifest.json 是测试执行的唯一输入源。它包含:

  • generated_at:清单生成时间;
  • case_id:本次测试使用的案件 id;
  • counts:按分类统计的文件数量;
  • files:每个测试文件的详细条目。

每个文件条目通常包括:

字段 含义
test_id 测试编号,如 BILL-01
category 分组类别,如 bill、call、industry、edge、full-exact、full-variant
path 相对于 fixtures 目录的夹具路径
template_id 预期命中模板 id
expected_template_id 期望模板 id,用于断言
mutation 对原始表头的变异策略
sheet 目标 sheet 名称
rows 数据行数
notes 人类可读说明
format 文件格式,通常为 xlsx

mutation 支持多种变异方式:

  • 直接键值替换,例如把“查询卡号”改为“银行卡号”;
  • _order:控制列序,例如 "reversed" 表示倒序;
  • _extra:追加额外列;
  • _drop:删除某必填列;
  • _keep:仅保留指定列;
  • _corrupt:标记为损坏文件。

章节来源

  • manifest.json:1-120

generate.py:夹具生成器

generate.py 的职责是把模板元数据转化为生产风格的 xlsx/csv 夹具。其入口模式如下:

flowchart TD
    Start["运行 generate.py"] --> Mode{"选择模式"}
    Mode -->|targeted| Targeted["生成账单/话单/跨行业/异常夹具"]
    Mode -->|full| Full["生成 table_info 全量模板夹具"]
    Mode -->|all| All["生成 targeted + full"]
    Targeted --> ReadDB["连接 PG 读取 table_info"]
    Full --> ReadDB
    ReadDB --> GenerateXlsx["用 openpyxl/xlwt 生成 xlsx"]
    GenerateXlsx --> WriteFiles["写入 fixtures/ai-llm-match-20261026 下子目录"]
    WriteFiles --> End["完成"]

图表来源

  • generate.py:1-20
  • generate.py:21-40

关键实现特征:

  • 固定随机种子 20261026,保证生成数据可重复;
  • 内置姓名、身份证号、手机号、银行卡号、金额、日期、通话时长等数据生成函数;
  • 针对银行流水、通信记录、企业主体等字段生成语义合理的假数据;
  • 连接 PG 时硬编码开发库地址;
  • 同时支持 xlsx 和 csv 输出。

章节来源

  • generate.py:1-120

run_tests.py:批量执行器

run_tests.py 负责:

  1. 从 manifest.json 读取测试清单;
  2. 按 category 分组执行;
  3. 逐个上传文件到 /dm/preFileUpload;
  4. 非只上传模式下调用 /aiClean/smartClean;
  5. 轮询 /aiClean/smartClean/status 直到 DONE/PARTIAL/FAILED/TIMEOUT;
  6. 直连 PG 查询 job、batch、ai_clean_template;
  7. 把结果写入 results/<group>_<timestamp>.json。

执行流程如下:

sequenceDiagram
    participant User as "测试人员"
    participant Runner as "run_tests.py"
    participant API as "HTTP API"
    participant PG as "PostgreSQL"
    
    User->>Runner: 指定 group / ids / limit
    Runner->>Runner: 加载 manifest.json
    Runner->>API: POST /auth/login
    Runner->>API: POST /case/open
    loop 每个文件
        Runner->>API: POST /dm/preFileUpload
        API-->>Runner: {batchId, file, sheets}
    end
    alt 非 upload-only
        Runner->>API: POST /aiClean/smartClean
        loop 轮询状态
            Runner->>API: GET /aiClean/smartClean/status
            API-->>Runner: {stage, aiJudgedCount, needReviewCount, ...}
        end
    end
    Runner->>PG: 查询 ai_clean_job / ai_clean_batch / ai_clean_template
    PG-->>Runner: job/batch/template 数据
    Runner->>Runner: 写入 results/*.json

图表来源

  • run_tests.py:1-319

关键字段对比:

结果字段 含义 典型断言
upload 上传阶段结果 OK、DATA_NULL、EXCEPTION:...
sheets[].file_status sheet 模板匹配状态 SHEET_TEMPLATE_MATCHED_SUCCESS、SHEET_TEMPLATE_MATCHED_FAIL
jobs[].matched_by 匹配来源 HEADER_CACHE、SCORE、LLM、SELF_HEADER
jobs[].exit_code 出口 A(既有业务表)、B(AI 动态表)、None
jobs[].llm_calls LLM 调用次数 用于判断是否走了 LLM 腿
jobs[].row_loaded 成功入库行数 用于判断出口 B 是否落库
chunk_batch.stage 批次最终阶段 DONE、PARTIAL、FAILED、TIMEOUT

章节来源

  • run_tests.py:1-319

锚定模板与三类行业场景

测试文档明确三个锚定模板,分别代表车辆登记、银行交易流水、通信记录。

锚定模板对照表

锚 模板 关键列 出口 说明
车辆登记 AI 模板 2111790826933268480(15 列) 号牌号码、机动车所有人、身份证明号码 B 通过 AI 清洗生成动态表
银行交易流水 既有 table_info#2 trans_record(30 字段) 查询卡号、借贷标志、交易金额、交易时间 A 写既有业务表
通信记录 既有 table_info#1 call_record(17 字段) 本机号码、对方号码、话单类型、呼叫开始时间 A 写既有业务表

注意:

  • 银行交易流水和通信记录的“出口 A”意味着它们命中的是既有模板;
  • 车辆登记模板是 AI 模板,因此可能走出口 B;
  • “既有直中”发生在上传阶段,由 PreDataListener 进行逐字精确匹配,不会进入 AI 兜底链路。

章节来源

  • ai-template-match-test-cases.md:15-28

A/E 类测试场景逐项说明

A 类:离线匹配规则

A 类测试是确定性单元测试,覆盖指纹、归一化、括号语义、空表头、重复列、模糊正向/负向、同义词、必填覆盖、形状闸门、列序、无关表头、短词边界等。

编号 场景 预期 结果
A1 同表头同序指纹 vs 列序变化 前者 md5 相等、后者不等 PASS
A2 归一化:全角/半角、空格、括号、后缀剥离 归一后相等 PASS
A3 括号语义保留 不相等 PASS
A4 空表头打分 score=0,不抛异常 PASS
A5 重复列不膨胀 Jaccard 分数与去重后一致 PASS
A6 模糊正向 均判 SIMILAR,整表 STRONG 直通 PASS
A7 模糊负向 分数 < WEAK PASS
A8 同义词词条 EXACT 命中或无命中 PASS
A9 必填覆盖参与评分 requiredCovered=true,允许直通 PASS
A10 形状闸门不受模糊影响 passesStrong=false PASS
A11 列序乱序短模板 passesStrong=false PASS
A12 完全无关表头 score < WEAK PASS
A13 短词边界 MISS PASS

这些用例主要验证 HeaderScorer、AiTemplateMatcher、AiTemplateMatchEdge、AiCleanSupport 等底层组件。

章节来源

  • ai-template-match-test-cases.md:29-56

E 类:table_info 全量模板覆盖

E 类覆盖全部 362 个 table_info 模板,每个模板按其 table_field 列头生成同类型 xlsx,包含 3 行生产风格假数据。

结果要点:

  • 362/362 上传即被既有匹配器直中;
  • 命中正确模板 346 个,命中率 95.0%;
  • 16 个错配均为“字段子集模板族”的固有歧义;
  • AI 兜底链路对错配 sheet 的校正能力由 C2/C3 场景佐证。

这意味着 E 类更多用于暴露既有匹配器的固有限制,而不是证明 AI 一定更好。

章节来源

  • ai-template-match-test-cases.md:74-90

执行命令与结果读取

基本运行命令

目的 命令
列出可用分组 python docs/testing/fixtures/ai-llm-match-20261026/run_tests.py list
执行某一组定向用例 python docs/testing/fixtures/ai-llm-match-20261026/run_tests.py bill
只上传不发起智能清洗 python docs/testing/fixtures/ai-llm-match-20261026/run_tests.py bill --upload-only
限制执行条数 python docs/testing/fixtures/ai-llm-match-20261026/run_tests.py bill --limit 10
指定特定 test_id python docs/testing/fixtures/ai-llm-match-20261026/run_tests.py bill --ids BILL-02,BILL-05
指定案件 id python docs/testing/fixtures/ai-llm-match-20261026/run_tests.py bill --case 16
自定义批次大小 python docs/testing/fixtures/ai-llm-match-20261026/run_tests.py bill --batch-size 6
自定义超时 python docs/testing/fixtures/ai-llm-match-20261026/run_tests.py bill --timeout 1800
生成 targeted 夹具 python docs/testing/fixtures/ai-llm-match-20261026/generate.py targeted
生成全量模板夹具 python docs/testing/fixtures/ai-llm-match-20261026/generate.py full
生成所有夹具 python docs/testing/fixtures/ai-llm-match-20261026/generate.py all

结果文件位置

每次运行会在 fixtures 目录下生成:

docs/testing/fixtures/ai-llm-match-20261026/results/<group>_<timestamp>.json

该 JSON 包含:

  • 每个测试文件的上传结果;
  • 每个 sheet 的 sheet_id、sheet_name、file_status、template_id;
  • 对应 job 的 matched_by、exit_code、llm_calls、row_loaded、fail_reason 等;
  • 创建的 AI 模板元数据;
  • 批次最终 stage。

章节来源

  • run_tests.py:200-319

失败排查步骤

第一步:确认服务可达

  1. 确认 http://127.0.0.1:8980/js/a 可达;
  2. 确认登录接口返回 token;
  3. 确认 /case/open 能打开目标案件。

如果登录返回 401,run_tests.py 会自动重试一次:先重新 login,再 open_case,然后继续请求。

第二步:检查 fixture 是否存在

如果 upload 结果为 DATA_NULL,说明上传响应没有 data。常见原因:

  • 文件路径不存在;
  • 文件尚未由 generate.py 生成;
  • manifest.json 中 path 与实际文件名不一致;
  • 操作系统路径分隔符导致找不到文件。

第三步:检查 smartClean 是否被拒绝

如果 smartClean 返回 code 非 200,查看结果 JSON 中的 smart_clean 字段。可能原因:

  • 文件解析失败;
  • 没有有效 sheet;
  • 权限或会话异常;
  • 服务内部校验拦截。

第四步:检查批次状态

查看 chunk_batch.stage:

阶段 含义
DONE 全部完成
PARTIAL 部分完成
FAILED 批次失败
TIMEOUT 轮询超时,默认 1200 秒

第五步:检查数据库结果

run_tests.py 会查 PG:

  • ai_clean_job:sheet 级清洗结果;
  • ai_clean_batch:批次汇总;
  • ai_clean_template:AI 模板定义;
  • ai_clean_field:AI 模板字段定义。

若脚本无法连接 PG,会抛出 psycopg2 相关异常。

第六步:核对 expected_template_id

如果模板命中不符合预期,重点比较:

  • template_id:实际上传阶段命中的模板;
  • expected_template_id:manifest 中的期望模板;
  • matched_by:指纹、打分、LLM 还是既有 header cache;
  • exit_code:出口 A 还是 B。

章节来源

  • run_tests.py:31-120
  • run_tests.py:200-319

NEED_REVIEW 行为解读

NEED_REVIEW 不是普通失败,而是“防御性拦截”。

触发逻辑通常是:

  1. 模板匹配命中某个模板;
  2. 模型完成字段绑定;
  3. 抽样校验发现必填列缺失、类型错误、或绑定质量不足;
  4. 系统不直接写业务表,也不盲目创建不可信数据;
  5. 将任务标记为 NEED_REVIEW,转人工处理。

文档中多个案例体现这一行为:

  • C2/C3:账单同义词/相似词命中 trans_record 族,但模型漏绑“借贷标志”,被出口 A 抽样 verify 拦截;
  • C6:微信风格账单列头差异大,绑定质量不足;
  • C8/C9:话单词条+模糊、主叫被叫风格,模型倾向勉强命中;
  • C14:完全无关表头被模型解释为通讯录类模板,必填抽样错误率高。

因此,NEED_REVIEW 应理解为:

  • 安全性优先:宁可让人工复核,也不写入低置信数据;
  • AI 不是万能:模型在语义相近但结构不可靠时容易“硬凑”;
  • verify 闸门有效:即使模型误判,抽样校验仍能阻止错误出口。

章节来源

  • ai-template-match-test-cases.md:57-90

F1–F6 发现项与建议

F1:既有直中优先级

  • 现象:与模板逐字同表头的文件在上传阶段即被既有匹配器命中,不走 AI 腿。
  • 原因:AI 是“既有失败”的兜底链路,符合设计。
  • 建议:要测 AI 腿,必须使用同义词、相似词、乱序、缺列、噪音表头等变体。

F2:qwen 偶发漏绑必填列

  • 现象:同义/模糊改写场景偶发漏绑个别必填列,如借贷标志。
  • 建议改进:在出口 A 抽样 verify 前,如果发现“必填列未绑定”,可追加一次定向补绑,提升可用性。

F3:无关表头模型倾向硬凑

  • 现象:无关表头可能被模型解释为语义相近模板,而非回退到 templateNo=0。
  • 建议观察:加强“语义相近但结构不可靠”时的回退策略;目前安全由 verify 兜住。

F4:跨案重放 SCORE 而非 HEADER_CACHE

  • 现象:同一 xlsx 跨案重放走 SCORE 腿,零模型效果等价但未复用指纹。
  • 建议排查:probe 表头行/空列归一与模板存档 md5 是否存在细微差异。

F5:既有匹配器字段子集歧义

  • 现象:字段子集模板族存在先到先得歧义,命中率上限约 95%。
  • 性质:既有行为,非本轮改动引入。
  • 意义:AI 兜底链路可用于校正这类误配。

F6:工作目录与登录

  • 运维要求:服务启动工作目录必须为仓库根,PathConst 依赖 user.dir。
  • 登录要求:sa-token 单端登录下,测试脚本需要 401 自动重登。

章节来源

  • ai-template-match-test-cases.md:90-111

架构与数据流总览

graph TB
    subgraph "夹具层"
        Manifest["manifest.json"]
        Generator["generate.py"]
        Fixtures["xlsx/csv 夹具"]
    end
    
    subgraph "执行层"
        Runner["run_tests.py"]
        Results["results/*.json"]
    end
    
    subgraph "应用层"
        API["HTTP API<br/>/js/a"]
        PreListener["PreDataListener<br/>既有匹配器"]
        Matcher["模板匹配<br/>指纹/打分/LLM"]
        Binder["字段绑定"]
        Planner["列计划<br/>requiredCovered/形状闸门"]
        Verifier["抽样校验<br/>verify"]
    end
    
    subgraph "数据层"
        PG["PostgreSQL"]
        Jobs["ai_clean_job"]
        Batch["ai_clean_batch"]
        Templates["ai_clean_template"]
    end
    
    Manifest --> Runner
    Generator --> Fixtures
    Runner --> API
    API --> PreListener
    PreListener --> Matcher
    Matcher --> Binder
    Binder --> Planner
    Planner --> Verifier
    Verifier --> Jobs
    Verifier --> Batch
    Verifier --> Templates
    Runner --> PG
    Runner --> Results

图表来源

  • manifest.json:1-120
  • generate.py:1-40
  • run_tests.py:1-319
  • ai-template-match-test-cases.md:1-111

结论

这套测试体系的核心价值在于:

  1. 分层清晰:A/B 层保证底层匹配与绑定逻辑确定;C/D/E 层覆盖真实链路、异常和全量模板;
  2. 夹具可再生:通过 manifest 描述意图,generate.py 生成数据,run_tests.py 驱动执行;
  3. 既有与 AI 对照:既有匹配器优先,AI 作为兜底链路;两者不是互斥,而是互补;
  4. 防御优先:NEED_REVIEW 表明系统在模型不确定时选择人工介入,而不是信任低质量绑定;
  5. 可观测性强:matched_by、exit、llm_calls、row_loaded、fail_reason 等字段足以定位问题;
  6. 仍需改进:qwen 偶发漏绑、跨案指纹复用、无关表头回退策略、既有模板族歧义,都是后续优化方向。

对于维护者而言,最直接的实践建议是:

  • 新增场景先写 manifest 条目;
  • 必要时更新 generate.py 夹具生成逻辑;
  • 使用 --upload-only 先验证上传阶段;
  • 再用完整流程验证 job、batch、AI 模板和落库;
  • 对 NEED_REVIEW 用例重点分析绑定质量和 verify 拦截原因。