AI测试 LLMCase-V4 从策划文档到测试用例 - 第 4 章 generator·实体提取:让机器"读懂"文档结构

zhangbp · 2026年09月20日 · 22 次阅读

第 4 章 generator·实体提取:让机器"读懂"文档结构

本章目标:理解流水线最关键的一跳——md 怎么变成实体树。学完你能回答:
为什么要先提取实体再生成用例?LLM 逐节提取的两遍式结构是什么?
哪三类"不是功能的内容"如何被剥离?怎么判定一个实体是原子(可直接测)的?

4.1 为什么先提实体:测试的对象是功能,不是文字

假如跳过实体提取直接"文档 → 用例",LLM 会按段落顺序输出一堆用例——没有结构、没有归属、
没法回答"这个功能测全了吗"。而测试工程的第一问恰恰是覆盖:文档定义的每个功能都有要点吗?

所以先要把文档整理成一棵功能实体树

GameSystem(文档根:挂机二期)
 ├─ 挂机战斗(GameplayFeature)
 │   ├─ 战斗倍速(子功能)
 │   └─ 自动挑战规则(子功能)
 ├─ 队伍管理(GameplayFeature)
 │   ├─ 队伍人数上限(原子功能 ← 要点生成目标)
 │   └─ 队长退出与队伍解散(原子功能)
 └─ ...

4.2 md → section 树:切分与预处理

generator 拿到 SRC md 后的第一步(extractors/doc_preprocessor.pyparse_markdown
实体提取的唯一入口)是按标题切成 section 列表。切分前先跑固定顺序的预处理四连——顺序不能乱:

① 剥 SOURCE 标签     md 首行的 "SOURCE_TYPE: word/excel" 标记拆下来(决定后续走 word 路还是 excel 路)
② 提富媒体块         <ImgThinking>/<ExcelTableThinking>/<FlowChartThinking> 用正则提出为附件,
                      原位换成 IMG_REF/ETT_REF 占位符
③ 清噪               删"这是一张…"残留、目录行、内链等噪声
④ 切约定区           读 PREAMBLE_END 标记截掉头部约定区(见 3.6)

切出的每个 section 带:标题层级、标题、内容、header_path(标题面包屑,如"挂机二期 > 挂机战斗 > 战斗倍速",
后续向量化入库和层级修复都靠它)、以及全量附着的富媒体附件(img_contexts 等)。
附件为什么全量附节、不在切分期归属?因为"这张图属于哪个小节"是语义判断,切分期做 NLP 归属容易错;
正确姿势是消费期按占位符就近匹配——提取某节时,占位符落在哪节就带哪节的附件(两跳协议)。

4.3 两遍式提取:骨架 + LLM 逐节

实体提取的骨架在 extractors/entity_extractor.pyextract_entities,结构是"先搭骨架、再填肉、
最后后处理":

第一遍·搭骨架:从 section 标题直接建 skeleton 实体树(栈式 CONTAINS)。
搭法具体是:

  • 根结点 = 文档名(先洗掉 _SRC_时间戳 后缀,防生成以源文件名命名的"幽灵根"),类型 GAME_SYSTEM;
  • 每个标题 = 一个 GAMEPLAY_FEATURE 实体——您在树里看到的功能节点、中间节点、叶子, 全部由标题担当,类型后面再由内容分析修正;
  • 栈找父:遍历 sections 时维护一个"(层级, 标题)"栈,新标题入栈前先弹掉所有等级 ≥ 它的, 栈顶即它的最近上级标题——父加 CONTAINS 边、子写 parent_id,骨架的双通道就此建立;
  • 同名标题不混淆:实体唯一键用祖先路径拼成(设计方案/首加载整体流程/首加载)。

第二遍·填肉:5 线程并发,逐 section 调 LLM 提取功能实体。每趟调用的 prompt 组装顺序:

[模板章节守卫约束块(前置)]        ← 4.4 节
[系统角色设定 + 输出格式说明]
[aux 背景(overview 摘要 ≤500 字)] ← 4.4 节
[该节内容 + 就近匹配的图片理解块]
[输出 JSON 结构示例]

第三遍·后处理(五步,顺序是 spec 契约):关系对称化(BELONGS_TO↔CONTAINS 互认)→
孤儿挂接(无父实体挂回来源章节)→ Pass2 跨节依赖("依赖/需要/消耗"关键词预筛 + 轻量 LLM 确认,
默认拒绝建边——宁缺毋滥)→ 原子性标注(4.5 节)→ 守卫软清理 + 指导块落 metadata。

4.4 防污染三件套:剥离"不是功能的内容"

第 1 章说过,策划文档里混着大量"不是功能定义"的内容。三件套各管一类:

4.4.1 约定区切分(消费侧)

convert2md 已经在产物里落了 PREAMBLE_END 标记,generator 消费侧三级识别:
标记优先(直读行号)→ 无标记时头部连续白名单标题推断 → LLM 兜底(默认 off)。
提取路丢弃约定区(A/B 区不产实体);同时指导路把它分档过滤后存进 metadata.preamble_guidance
反哺后续生成(3.6 节已讲)。词表和算法的单一真相源在 modules/common/template_chapters.py——
convert2md 落标、generator 切分、dashboard 回显三方共用,禁止复制副本。

4.4.2 辅助段落分流(aux-section)

约定区之外的正文里还有辅助章节("设计核心提要""前言""更新记录")。命中 modules/common/aux_sections.py
词表的章节按两档分流:

  • overview 档(设计提要族):不提取实体,但摘要截 500 字注入后续每节提取 prompt 作背景 (帮 LLM 理解全局意图,附"禁止从中提取实体"声明);
  • boilerplate 档(前言/规范/更新记录族):直接跳过,不进 prompt、不进实体树、不进覆盖率分母。

4.5 层级完整性与原子身份:两个"裁判"机制

4.5.1 层级完整性门禁(ENTITY_HIERARCHY_GATE)

2026-09 之前,实体树层级屡出事故(一级平铺十几个、子节点挂空)。复盘结论:层级事实分散在
四个通道(BELONGS_TO/CONTAINS/parent_id/children_ids),由"LLM 提取 + 多个删并后处理"逐步塑造,
每个删除者都是潜在孤儿工厂。逐点打补丁必复发,于是上了机制化的末端门禁
extractors/hierarchy_gate.pyensure_hierarchy,跑在 extract_entities 最末端):

四条不变量:无悬挂(引用不存在的父 → 沿标题面包屑祖先链重挂,回落文档根)、无孤儿
(无人认领 → 挂靠候选)、单根收敛(全树必须收敛到唯一文档根)、通道一致(四个通道互相同步 +
死引用清理)。门禁只重挂/挂靠/清死边,不增删实体(谁该删由守卫决定,门禁保证删后无孤);
健康树零修复零日志;幂等(二跑零变更)。G10 验证用真实事故标本复放:811 实体的破损树过门禁后
根 3→1、悬挂 0。生产已开启。

4.5.2 原子身份判定(三层优先级)

树建好了,谁是可以直接写用例的"原子功能"?判定函数 _is_atomic_functional_node 三层:

  1. L1 显式标注is_atomic=True 直通(人工标注永远赢,连排除类型都可覆盖);
  2. L2 类型排除:结构/容器类型(GameSystem/TestDimension 等)恒非原子; Resource/ConfigItem 默认排除;
  3. L3 结构启发式:子节点全是描述型 → 叶子是原子;含功能子 → 容器;纯分组节点剔除。

每层返回 reason 写进 atomic_functional_reason——可审计,"为什么这个实体没生成要点"永远答得出来。
判定结果写回 is_atomic_functional 字段,成为下游全链(要点生成目标集/完整性门/评估覆盖率分母/
dashboard 展示)的单一真相源。一个判定函数三处消费(提取预写/要点筛选/dashboard),改它要跑全量回归。

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册