本文引用的文件
本文件基于 docs/testing/ai-template-match-test-cases.md,系统化整理“AI 清洗模板匹配链路”的严格测试用例文档及其配套夹具。核心目标是帮助读者理解:
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/>人工复核"]
图表来源
章节来源
根据测试文档和脚本常量,测试环境应满足以下要求:
| 组件 | 要求 | 依据 |
|---|---|---|
| 数据库 | 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 等表回填结果;20,但测试文档中主场景为 id=16、跨案件为 id=17;如需复现旧结果,可通过 --case 参数切换。章节来源
夹具目录 fixtures/ai-llm-match-20261026 采用“清单驱动 + 生成器 + 执行器”的结构。
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:标记为损坏文件。章节来源
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["完成"]
图表来源
关键实现特征:
20261026,保证生成数据可重复;章节来源
run_tests.py 负责:
manifest.json 读取测试清单;category 分组执行;/dm/preFileUpload;/aiClean/smartClean;/aiClean/smartClean/status 直到 DONE/PARTIAL/FAILED/TIMEOUT;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
图表来源
关键字段对比:
| 结果字段 | 含义 | 典型断言 |
|---|---|---|
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 |
章节来源
测试文档明确三个锚定模板,分别代表车辆登记、银行交易流水、通信记录。
| 锚 | 模板 | 关键列 | 出口 | 说明 |
|---|---|---|---|---|
| 车辆登记 | AI 模板 2111790826933268480(15 列) |
号牌号码、机动车所有人、身份证明号码 | B | 通过 AI 清洗生成动态表 |
| 银行交易流水 | 既有 table_info#2 trans_record(30 字段) |
查询卡号、借贷标志、交易金额、交易时间 | A | 写既有业务表 |
| 通信记录 | 既有 table_info#1 call_record(17 字段) |
本机号码、对方号码、话单类型、呼叫开始时间 | 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 等底层组件。
章节来源
E 类覆盖全部 362 个 table_info 模板,每个模板按其 table_field 列头生成同类型 xlsx,包含 3 行生产风格假数据。
结果要点:
这意味着 E 类更多用于暴露既有匹配器的固有限制,而不是证明 AI 一定更好。
章节来源
| 目的 | 命令 |
|---|---|
| 列出可用分组 | 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 包含:
章节来源
http://127.0.0.1:8980/js/a 可达;/case/open 能打开目标案件。如果登录返回 401,run_tests.py 会自动重试一次:先重新 login,再 open_case,然后继续请求。
如果 upload 结果为 DATA_NULL,说明上传响应没有 data。常见原因:
generate.py 生成;manifest.json 中 path 与实际文件名不一致;如果 smartClean 返回 code 非 200,查看结果 JSON 中的 smart_clean 字段。可能原因:
查看 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 相关异常。
如果模板命中不符合预期,重点比较:
template_id:实际上传阶段命中的模板;expected_template_id:manifest 中的期望模板;matched_by:指纹、打分、LLM 还是既有 header cache;exit_code:出口 A 还是 B。章节来源
NEED_REVIEW 不是普通失败,而是“防御性拦截”。
触发逻辑通常是:
文档中多个案例体现这一行为:
因此,NEED_REVIEW 应理解为:
章节来源
章节来源
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
图表来源
这套测试体系的核心价值在于:
对于维护者而言,最直接的实践建议是:
--upload-only 先验证上传阶段;