第 5 章 generator·生成:测试要点与用例怎么来

本章目标:本教程的重心——LLM 生成的工程化。学完你能回答:
一次要点生成的 prompt 里都有什么?LLM 输出的 JSON 坏了怎么救?
完整性门怎么用"有界重试 + 温度阶梯"逼 LLM 补齐象限?用例的步骤契约是什么?
Self-RAG 反思和提速三旋钮是怎么回事?

5.1 LLM 生成的工程化:prompt、结构化输出与 JSON 修复

5.1.1 一次要点生成的 prompt 长什么样

要点生成入口是 TestPointGenerator.generate_test_points(注意一个反直觉事实:要点和用例
在同一个类里
,仓库里没有独立的 test_case_generator.py)。

先回答"从哪个节点开始"——不从根开始,也没有自顶向下的递归拆解。"拆"在提取阶段
(第 4 章)已经完成:骨架 +LLM+parser 把树建好;生成阶段做的是平铺筛选:遍历全树所有
节点跑原子三层判定(4.5.2),只有 is_atomic_functional=True 的节点(叶子为主)各自成为
独立生成单元——根结点和中间功能节点从不直接产要点,只作为祖先链上下文出场。
两个阶段的数据流靠文件交接,TESTS 边是点挂回树的关联:

ENT JSON(实体树)
 │ 要点阶段:平铺遍历 → 原子筛选 → per 原子节点一趟 LLM(四象限)
 │   每个要点 add_relationship(TESTS, 实体id) 挂回被测实体
 │   EP 聚合:同实体同策略类目 ≥2 条 → 合成复合要点(member_uuids 可逆拆解)
 ▼
PNT JSON(要点集——本身也是个 EntityCollection)
 │ 用例阶段:同时读 PNT + ENT 两份文件
 │   per 要点(含 EP)一趟:要点的 description 当检索钥匙
 │   从 TESTS 边找回被测实体 → 祖先链上下文 → 步骤契约版 prompt → LLM
 ▼
CAS JSON(用例集)→ evaluator(第 7 章)

分层之外还有一个承载结构信息的实体上下文 JSON(随⑥的任务说明一并发给模型),它是
"功能节点包裹原子节点"三层打包的落点——原子判定选出目标后,生成单元不是一个孤立实体,而是:

{ target_node:      原子节点本体(含描述),
  ancestors:        祖先链 [根 → 系统 → 功能 → 父功能],   ← 回答"这是哪个功能的"
  children_details: 目标节点的子节点(规则项/属性明细) }   ← 回答"具体规则是什么"

树在这里只当"生成计划"用:决定给谁生成、带什么结构上下文——真正的参考资料是③④两个检索块。

5.1.2 输出为什么是 JSON,以及 JSON 有多容易坏

结构化输出(JSON)是"LLM 输出能进流水线"的前提——字段名固定才能被代码消费。
本工程要求 LLM 按给定 schema 返回 JSON,但 LLM 的实际输出经常是这样的:

"好的,以下是提取结果:```json
{"entities": [{"name": "挂机战斗", "type": "GameplayFeature", ...

六类常见病:markdown 围栏包裹、多余/缺失逗号、键名没加引号、省略号占位("…")、
输出超长被截断、字符串里有未转义的引号。直接 json.loads 会当场炸。

对策是 model_core/utils/json_utils.py 的 safe_json_loads——4 个越来越激进的修复 pass:

Pass1 原样解析 → Pass2 格式修复(剥围栏/补逗号/修键名/删省略号/截断处理)
→ Pass3 控制字符转义后重试 → Pass4 剥前后散文取首个完整 JSON 块
→ 全失败:记 500 字符现场日志,抛错或按调用方默认值降级

设计哲学:便宜的先试、有破坏性的后试,截断优先截取(完整前缀比臆造补全可靠)、
补全兜底(栈跟踪括号深度补闭括号)。全工程"解析 LLM 输出"的入口统一走它——
新人写任何新的 LLM 调用点,禁止裸 json.loads。

5.1.3 温度:LLM 的"发散旋钮"

温度(temperature)控制 LLM 输出的随机性:0 = 尽量确定(每次几乎一样),高 = 发散(每次不同)。
本工程对它的用法很讲究:首轮生成用默认温度,重试轮逐级升温(0 → 0.2 → 0.4 → 0.6)——
逻辑是"同样的 prompt 再要一次大概率得到同样的错误答案,升温才能得到不同的尝试"(5.3 节的完整性门)。

5.2 四象限生成与 EP 聚合

拿到一个原子实体(比如"队伍人数上限"),LLM 按 prompt 要求输出四象限要点阵列:
positive/boundary/negative/non_functional 各至少一条(或声明"不适用"并给理由——
强制四象限但不强迫编造无意义要点,这是 prompt 措辞里的平衡艺术)。

一次完整的"实体 → 要点"转换实例(输入输出按真实契约示意)。一趟 LLM 调用的输入 =
三层打包的实体上下文(5.1.1)+ 检索块,实体上下文长这样:

{ "target_node": {"name": "队伍人数上限", "description": "控制单支队伍成员数量上限"},
  "ancestors": ["挂机二期文档", "队伍管理"],
  "children_details": [
    {"name": "人数上限", "description": "默认上限 5 人"},
    {"name": "VIP 加成",  "description": "VIP6 上限提升至 6 人"},
    {"name": "满员限制",  "description": "满员后其他玩家无法申请加入"}],
  "siblings": ["队长退出与队伍解散"] }

prompt 的核心指令(model_core/prompts.py 的四象限模板)要求模型扮演资深游戏 QA,
把上面的功能描述翻译成验证清单:四个数组必须齐、每象限至少 1 条、每条要点必须
"含输入/预期/判定"(例:"资源不足 (输入) 时合成按钮置灰 (预期),可判定为禁用态 (判定)")、
children_details 覆盖率 ≥80%、涉及数值规则必须有依据不得臆造。模型输出(示意):

{ "name": "队伍人数上限_测试", "type": "TestDimension",
  "properties": {
    "positive_points":        ["5 人以内(输入)组队成功且成员列表正确(预期),组队功能正常(判定)",
                               "VIP6 玩家(输入)可组建 6 人队(预期),上限加成生效(判定)"],
    "boundary_points":        ["已有 5 人时第 6 名玩家(输入)申请被拒(预期),提示队伍已满(判定)"],
    "negative_points":        ["满员后踢出 1 人(输入),新玩家可立即申请加入(预期),上限动态生效(判定)"],
    "non_functional_points":  ["多名玩家并发申请最后一个席位(并发)不产生超员(预期)"] } }

代码侧把这段 JSON 实体化为 1 个 TestDimension 实体(命名约定"X_测试";四象限数组装在
它的 properties 里,一个功能实体通常对应一个要点实体,模型偶发返回多节点时逐个实体化),
挂 TESTS 边回被测实体,metadata 记录检索血缘与覆盖率软指标;随后完整性门查"四象限齐不齐",
不齐带反馈重试(5.3 节)。

所以"功能实体怎么拆成要点"的方法论答案就是四象限本身:LLM 天生偏爱正常流
(Happy Path Bias),同一个功能被强制从正向/边界/反向/非功能四个视角各发问一遍
"要验证什么"——四类问题问完,要点就拆全了。

要点生成后有一层EP 聚合(等价类聚合):同一实体、同一策略类目(strategy_category)的多条要点,
≥2 条就合并成一条复合要点(EP,等价类代表),原始成员的 uuid 存进 member_uuids(可逆拆解)。
后续用例生成以 EP 为驱动单元——好处是用例天然按等价类分组、象限覆盖可按要点结算。

产物的软指标三件套(children_coverage_rate 子功能覆盖率 / concreteness_gap 具体度缺口 /
sibling_coverage 兄弟覆盖)只记 metadata 供评估观察,不在生成期拦截——软指标观察、硬门拦截的分工。

5.3 完整性门:有界重试与温度阶梯

LLM 经常偷懒:某个象限漏了、字段缺了。完整性门(completeness gate)是纯函数检查 + 有界重试的组合拳:

生成 → checker 检查(纯函数)
   ├─ 过 → 收下
   └─ 不过 → 把缺口写成反馈追加进 prompt("只补充缺失的 negative 象限,不要重复已生成的")
            → 重试(温度升一档)→ 累加去重收集 → 再检查 → …
            → 重试耗尽(cap=4)→ 显式标 completeness=incomplete 落盘

两级检查口径:point 级(每个原子实体 ≥1 条要点且策略满四象限);case 级(要点期望象限 vs
用例 test_quadrant 计数,每象限 ≥ 最小条数)。

三个工程要点:

  1. 有界:cap=4,不许无限重试——成本可控,耗尽就诚实标 incomplete(不静默丢、不假成功), attempts/cap/missing 全落进 metadata 和 [GenComplete] 结构化日志,评估可归因;
  2. 反馈只列缺口不重贴全量要求(token 经济);重试产物累加去重(按 (象限, 名称) 键去重,防重试轮次翻倍);
  3. checker 是纯函数且 dict/Pydantic 双兼容——可在单测里穷举形态,不碰 LLM。

5.4 测试用例生成与步骤契约

用例生成入口 generate_test_cases_with_rag:逐条要点(常为 EP 复合要点)一趟 LLM 调用,
检索注入与要点生成同构(第 6 章)。产出的每条用例(TestScenario)带:

5.5 Self-RAG 反思与提速三旋钮

5.5.1 Self-RAG 反思

Self-RAG(自反思检索增强生成):生成完成后,再让 LLM 对自己的产出打分反思——
三维评分(覆盖/具体度/结论)、列出缺口(gaps)、给出裁决(verdict:healthy/needs_revision)。
裁决为 needs_revision 时带缺口反馈重生成一次(cap=1,重生成失败回退原要点不硬来),
反思记录落 metadata.self_rag_reflections。

它的价值有实证:全量 on/off 对照实验 overall +1.72。但代价是每节点多一趟 LLM 调用——
由此引出下面的提速改造。

5.5.2 提速三旋钮(2026-09 生产定档)

13:59 批次量化诊断:要点阶段 ≈9h(811 节点 × ~200s/节点 ÷ 5 并发),单节点 200s 里
主生成 1 趟 + Self-RAG 反思 1 趟 + 有界重生成 ≤1 趟。三个杠杆:

旋钮 机制 默认 → 生产值
GEN_WORKER_POOL 生成并发池(原硬编码 5) 5 → 8
SELF_RAG_TRIGGER_MODE 反思触发式:仅完整性门未过/要点模糊时才反思(gated),健康节点全免反思趟 always → gated
GEN_PROMPT_COMPACT prompt 紧凑序列化(剔除 None/空值字段,列表长度不变防索引漂移) false → true

gated 模式的关键裁决依据是数据:上批 561 要点的自反思覆盖率呈双峰(85% = 0.0 / 8% = 1.0),
无区分度——按 0.5 阈值触发会退化成"总是反思",所以覆盖率触发默认关闭(-1),
只留完整性门 + 模糊要点两个精准触发。

定档结论(B 链真实批次 A/B,同文档一期旧链对照):三旋钮全开下要点吞吐 2.8×
(252 节点/h)、用例 1.28×(2/3 杠杆在要点侧);质量六项全线上升无一回退——
overall 40.82→47.34、entity 99.48→99.97、point 74.39→82.98、case 68.58→79.08、
geval 32.35→53.7、watchdog 覆盖 0.841→0.985;gated 反思触发率仅 1.4%
(reflect=10 / skip=683)。三个值(8/gated/true)已作为生产值写进 .env。
教训点:LLM 优化先看分布数据再定阈值,拍脑袋阈值会被双峰分布打脸;提速类改造的验收
必须含同文档质量对照,快了但变差不算过。

另一条相关机制是空响应退避([GenBackoff]):LLM 网关偶发瞬态故障返回空 content,
连续快败会烧穿节点——对策是指数退避 5→10→20→40s 封顶、非空即重置,健康网关零等待。
姊妹护栏 TruncGuard(model_core/utils/llm_client.py):思考型模型输出超长被网关截断
(finish=length 且 content 非空)时告警并跳过响应缓存——截断的半截回答一旦进了缓存,
后续同 prompt 回放全是残废品,这个坑比单次截断本身更毒。


↙↙↙阅读原文可查看相关链接,并与作者交流