graceful-prairie-perch.md 8.6 KB

智能清洗融合链路 · 端到端验证(真库 + 真模型 + 真浏览器)

Context

融合入口(POST /aiClean/smartClean)与前端三处接入已落地,离线单测 69 条全绿,但整条链路还没在真实环境里跑通过一次:单测只能证明「doClean 只发一次、判定先行」这类时序,证明不了三件真问题——

  1. 真实解析出来的 templateId/mainId/tableRuleList 能不能原样喂给既有清洗(报文契约是否真的对得上);
  2. 真模型对一个列名完全不同的表块,判定是否收敛到 DONE/PARTIAL(而不是卡在 JUDGING 或一路 NEED_REVIEW);
  3. AI 自建表的数据能不能在治理树上长出负 id 节点、并在新加的 id<0 分流里真的取到数。

当前环境已就绪:后端 :8980/js/a 已启动并就绪,前端 vite :3100 已起,浏览器已登录(/case,账号「系统管理员」),dev PG 里 5 张 ai_clean_* 表都在。本计划需要写入真实数据(dev PG 元数据 + 该案件 DuckDB + 一次真模型调用),所以先取批准。

已核到一个必须先修的造数错误:既有解析的匹配源是 table_field.field_name_cn(GlobalCache.ALL_FIELDS_MAP / ALL_MATCHED_FIELDS_MAP,GlobalCache.java:200-209),硬条件是「表头必须含该模板全部 matched=1 列」(PreDataListener.matchTemplate:340-395,纯 CollUtil.containsAll,无阈值、无列数相等要求,且不做全半角/括号归一,唯一清洗是 Str2NullConverter:21 去 \t\r\n'^ + trim)。我之前那份 e2e_ok_trans.xlsx 是照抄 table_info.headers 造的,用错了源,不保证命中 —— 必须按 field_name_cn 重造。

一、准备(只读 + 我这边 Temp 目录里的临时文件)

  1. 只读 SQL(192.168.0.103:5432/zsjz-ai)选一个 matched 列少、且必填列不苛刻的模板: select t.id, t.main_id, count(*) filter (where f.matched=1) mc, array_agg(f.field_name_cn) filter (where f.matched=1) from table_info t join table_field f on f.table_id=t.id group by t.id having count(*) filter (where f.matched=1) between 4 and 12 order by mc limit 15 取一个模板,把它全部 matched 列名逐字写进 e2e_ok_trans.xlsx 第 1 行(多放几列自己造的列没关系,只要保证「表头 ⊇ matched 列」就能命中;全字段 ⊇ 表头则算完全命中)。
  2. 保留 e2e_messy_acct.xlsx(表头 账户号码/账户名称/交易发生日期/交易发生时刻/收入金额/支出金额/账户余额/对手户名/对方开户网点/资金用途备注,第 1 行、无合并区,数据含 ¥、全角逗号千分位、2026年1月7日、空列、末行「合计」)—— 它必须不被任何模板命中(跑之前先对全部模板的 matched 列做一次 containsAll 反向校验,避免白跑)。
  3. 记下起始基线(用于事后比对,全部只读):ai_clean_job / ai_clean_batch / ai_clean_template 的行数与最大 id。

二、浏览器操作序列(browser-use,每步 take_snapshot 留证)

  1. /case → 点案件「AI验证案件-20260915」的「打开案件」(就是你为端到端建的那个案)。该案件里已有旧上传/旧 AI 表,不清理,所以下面所有比对都必须按「本次 batchId + 跑前基线 id 集合」过滤,不能对全表计数。
  2. 进上传页 → 依次上传 e2e_ok_trans.xlsx、e2e_messy_acct.xlsx(同一批次:第二个文件回传首个响应里的 batchId)。
  3. 断言入口修复:即使 messy 那份解析为 SHEET_TEMPLATE_MATCHED_FAIL,「智能」按钮仍可点(hasCleanableSheet),且不再要求 hasSuccessResult。
  4. 点「智能」→ 期望:请求 POST /js/a/aiClean/smartClean 200,立即跳 /data/cleanProgress?fused=1&batchId=...;页面不再自动发出第二个 POST /dm/doClean(网络面板只应看到 smartClean)。
  5. 观察进度页:上半张卡仍由 SSE 推既有清洗进度;下半张「未匹配文件的 AI 识别」卡的计数从 0/1 往上涨,直到 stage 进 DONE/PARTIAL/FAILED 后停止轮询(2s 一跳、状态请求数应等于阶段数×若干,不应无限)。
  6. 进「数据治理」树:出现 AI 节点(节点 id 为负)→ 点开 → 断言走的是 /aiClean/data/page(网络面板可见,aiTemplateId = -id),表头是中文列名、数据行有内容;再点一个既有正数节点,确认走的仍是 /gt/treeTablePage、行为与改动前一致。
  7. 若批次里出现「需人工确认」:卡片上的「去人工确认」→ 应跳 /data/cleaning?focusKey=<sheetId> 并定位到该 sheet。若本次没自然出现,这条改为只做一次代码级确认(读 handleReviewManual 与 cleaning.vue 对 focusKey 的消费),不改配置去强造。

三、后端与库侧核对(只读)

  1. /tmp/backend.log:智能清洗:合并 N 个 AI 命中的表与既有匹配的表,doClean 只调一次 出现恰好 1 次;未找到根节点文件,无需清洗 不出现(说明没白发);SSE 关闭只发生在既有清洗收尾处一次。
  2. ai_clean_batch(按 batchId):stage 终态、sheet_total=2、matched_count>=1、ai_judged_count=1、existing_hit_count / ai_load_count / need_review_count / failed_count 与日志逐条对得上;need_review_detail 是合法 JSON 数组。
  3. ai_clean_job(按 batch_id):messy 那条的 exit_code(A/A2/A3/B)、matched_by、tier、confidence、llm_calls(必须是 1,用来验证「草稿复用不二次调模型」在真实链路上成立)、row_total/row_loaded、status。
  4. ai_clean_template 新增行(若走出口 B):table_name_en = ai_t{id}、header_md5 唯一未冲突、status=ONLINE、category 落在 FileCategoryEnum 值域内;对应案件 DuckDB 里 ai_t{id} 表存在且 select count(*) = row_loaded。
  5. 治理侧:govern_tree 中 id < 0 的节点数 = 该案件有数据的 AI 表数,且 data_count 与真 count 一致;正数节点数量与跑前一致(既有链路零回归)。

四、通过标准与失败处理

  • 通过:二、三两组断言全部成立,且 stage ∈ {DONE, PARTIAL}。PARTIAL 可接受(真模型对陌生列名判成待人工是设计内行为),但要留下 need_review_detail 的原因文本作为 P1 改进依据。
  • 失败即定位、不猜:
    • 卡在 JUDGING → 看是否 Semaphore/CaseContextHolder 问题(日志里 AI 判定异常 的堆栈);
    • doClean 出现两次 → 说明 A 组合并逻辑漏了,回读 dispatchExisting 与前端实际提交的 fileInfos;
    • AI 节点点开 500 → id<0 分支没生效或 dataPage 的 head/pages 结构与 extractTablePayload 不匹配;
    • 真模型超时 → 记录耗时,把该批次的 cost_ms/llm_calls 写进文档「实现进度」,不当作功能失败。
  • 发现需要改代码的缺陷:先修 → 重跑离线单测(mvn -o -pl ai-server test -Dtest='com.zsjz.ai.module.aiclean.**.*Test')→ 再走浏览器复验,不攒着。

五、跑完的清理(都会先报备再动手)

  1. 上传落在仓库内 ai-server/QingJian/workspace/{caseId}/upload/{batchId}/:确认后只删本次 batchId 目录(不碰别的批次、不碰 QingJian/{caseId}/data.db 案件库)。
  2. ai_clean_job/ai_clean_batch/ai_clean_template/ai_clean_field/ai_clean_rule 里本次新行保留(是 P1 复放的样本),但把 id 列出来;如需回滚数据用现成 POST /aiClean/revert?jobId=,不手写 DELETE。
  3. git status 里那条 AD ai-server/QingJian/uy76tkp8/data.db(早前测试残留,已被 git add 进索引又在工作区删掉):提请确认是否 git restore --staged 摘掉;不擅自改索引。
  4. 文档 docs/design/ai-clean-parallel.md:把真实端到端结果(耗时、exit_code 分布、llm_calls=1 佐证、S1→S7 哪一步最先出问题)补进第十四节,并在「实现进度」里把「端到端未跑」这条划掉。

关键文件

  • 后端:ai-server/src/main/java/com/zsjz/ai/module/aiclean/service/AiSmartCleanService.java、.../api/AiSmartCleanController.java、.../service/AiCleanService.java(judge / loadViaAiTemplate / dataPage)
  • 前端:ai-frontend/src/case/views/data/upload.vue、cleanProgress.vue、index.vue、ai-frontend/src/case/api/govern/governApi.ts
  • 只读参照(判据来源,不改):ai-frontend/src/main/java/.../dm/pre/PreDataListener.java:260-281,340-395、common/cache/GlobalCache.java:200-209
  • 造数脚本:C:/Users/cc/AppData/Local/Temp/e2e/Gen.java(需按第一节的 matched 列改 OK_HEADERS 后重新生成)