LLM 代码产物的验证鸿沟:用测试生成闭环把住质量门
一、生成代码与质量缺口
LLM 写代码,几秒钟一段。产物堆积如山,没人补测试。代码合进去,跑得起来就是好的。线上崩了才回头查,发现边界根本没人测。
LLM 生成的代码,问题不在能不能跑。而在"能不能扛住输入全集"。它常按字面意思实现,忽略空值、并发、超时。人审看不过来,靠手感判断易漏。
测试是最后一道闸门。但人写测试比写代码还慢,普遍欠账。让 LLM 反过来生成测试,形成闭环。这是当前唯一可行的质量兜底路径。
但生成测试本身,也有幻觉风险。断言可能太松,永远过。可能太严,把对的判错。必须把测试生成纳入工程流水线,做闭环验证。
二、测试生成闭环的数据机制
闭环不是"调一次 LLM 出测试"就完事。它是一组可验证、可回归、可审计的步骤。输入是 LLM 生成的源码。先做静态解析,抽函数签名与契约。
再让 LLM 基于契约推边界用例,而非随机生成。用例产出后落库,标注来源版本。执行用例,捕获通过率与覆盖率。最后用覆盖率回归检测:新测试是否真的覆盖了新代码。
若覆盖率不增反降,判定生成失败,触发人工复核。下面是闭环的数据流向:
flowchart TD
A[LLM 生成源码] –> B[AST 解析: 抽契约]
B –> C[LLM 推边界用例]
C –> D[用例落库+版本标注]
D –> E[沙箱执行]
E –> F[覆盖率回归]
F –>|覆盖率不增| G[标记可疑: 人工复核]
F –>|覆盖率达标| H[断言可信度校验]
H –>|断言过松| G
H –>|断言合理| I[合入测试套件]
style G fill:#ffebee
style I fill:#e8f5e9
关键在"可信度校验"那一步。不是跑通就算数。要查断言是否对实际输出做了真实判断。空断言、恒真断言,都要在合入前剔除。
三、生产级实现
下面用代码描述测试生成的核心流水线。代码省略 LLM 调用细节,但保留闭环骨架。重点展示错误处理与可信度校验。
import ast
import subprocess
import json
from dataclasses import dataclass, field
from typing import Callable, Optional
@dataclass
class FuncContract:
"""函数契约:签名、参数、返回类型。
从 AST 抽取,作为生成测试的上下文基础。
没有契约直接喂 LLM,会得到泛泛而谈的测试。"""
name: str
args: list[str]
returns: str
def parse_contracts(source: str) -> list[FuncContract]:
"""解析源码抽函数契约。
遇到语法错误直接抛出,避免污染后续流程。
设计上宁可失败,不吞错误。"""
tree = ast.parse(source)
contracts: list[FuncContract] = []
for node in ast.walk(tree):
if not isinstance(node, ast.FunctionDef):
continue
# 跳过私有函数:测试入口约定为公开 API
if node.name.startswith("_"):
continue
args = [a.arg for a in node.args.args]
ret = ast.unparse(node.returns) if node.returns else "Any"
contracts.append(FuncContract(node.name, args, ret))
return contracts
@dataclass
class TestCase:
func: str
inputs: dict
expected: object
source_version: str # 标注来源版本,便于回归追溯
@dataclass
class GenResult:
"""生成结果:用例集 + 可信度评分。
评分低于阈值的整体丢弃,避免污染套件。"""
cases: list[TestCase] = field(default_factory=list)
trust_score: float = 0.0
def generate_via_llm(
contract: FuncContract,
llm_call: Callable[[str], str],
source_version: str,
timeout: float = 30.0,
) -> GenResult:
"""调用 LLM 生成测试用例。
网络与超时风险高,必须做超时与异常隔离。
LLM 返回非 JSON 时降级为空结果,不抛断流。"""
prompt = (
f"为函数 {contract.name} 生成边界测试用例,"
f"入参: {contract.args}, 返回: {contract.returns}。"
"输出 JSON 数组,每项含 inputs 与 expected。"
)
try:
raw = llm_call(prompt)
items = json.loads(raw)
except (TimeoutError, json.JSONDecodeError) as e:
# 失败要可见:记日志不静默吞掉
print(f"[gen] {contract.name} 失败: {e}")
return GenResult()
cases = [
TestCase(
func=contract.name,
inputs=item.get("inputs", {}),
expected=item.get("expected"),
source_version=source_version,
)
for item in items
]
# 可信度初评:用例数过少视为可疑
score = min(1.0, len(cases) / 5.0)
return GenResult(cases=cases, trust_score=score)
def run_with_coverage(test_file: str) -> dict:
"""执行测试并采集覆盖率。
子进程隔离执行,避免失败影响主流程。
返回结构化结果便于后续判断。"""
try:
proc = subprocess.run(
["pytest", "–cov=json", "–cov-report=term", test_file],
capture_output=True,
text=True,
timeout=120,
)
return {
"ok": proc.returncode == 0,
"stdout": proc.stdout,
"stderr": proc.stderr,
}
except subprocess.TimeoutExpired:
# 超时单独标记:可能是测试死循环或网络调用
return {"ok": False, "stdout": "", "stderr": "timeout"}
def regression_check(
before_cov: float,
after_cov: float,
min_delta: float = 0.02,
) -> bool:
"""覆盖率回归校验:新增测试必须带来正向增量。
阈值设过低,会放过无效测试;过高则误杀有效补充。
min_delta=2% 是经验值,可按团队调。"""
return (after_cov – before_cov) >= min_delta
def detect_weak_assertion(case: TestCase, runner: Callable) -> bool:
"""检测断言是否过松。
思路:把 expected 替换为随机值再跑一次。
若仍通过,说明断言根本没真正校验输出。"""
import random
tampered = TestCase(
func=case.func,
inputs=case.inputs,
expected=random.random(),
source_version=case.source_version,
)
try:
return runner(tampered)
except Exception:
# 抛异常说明断言确实在执行,判为有效
return False
if __name__ == "__main__":
# 骨架演示:解析契约后走生成-执行-校验闭环
src = "def add(a, b): return a + b\\n"
contracts = parse_contracts(src)
for c in contracts:
print(f"契约: {c}")
真实系统会接 Git hook 与 CI 门禁。PR 提交时自动生成测试,跑不过门禁不许合。并对接版本管理,旧测试随源码演进一起更新。
四、LLM 代码产物的验证鸿沟的代价与边界
闭环很美,但每一环都藏着坑。
断言幻觉。LLM 生成的断言可能恒真。比如 assert result is not None。看着通过,实际没校验业务正确性。
必须做弱断言检测,把空校验剔出去。
覆盖率陷阱。覆盖率涨了不代表测了重点。LLM 喜欢写简单的整数用例,回避复杂路径。要追分支覆盖与变异测试,而非行覆盖。
否则数字好看,漏洞仍在。
生成测试本身可信吗。LLM 写测试时也会写错。常见的是预期值算反。需要拿"已通过的人写测试"做锚点校准。
新测试与锚点冲突时,触发复审而非自动合。
执行环境隔离。生成的测试可能调真实依赖。比如读数据库、发外部请求。必须在沙箱跑,限定依赖为 mock。
否则测试会污染生产数据或耗光配额。
成本失控。每个 PR 都调 LLM 生成测试,token 费用累积。应按函数复杂度过滤,简单 getter 不必生成。并对失败的生成做缓存去重,不重复扣费。
测试生成的闭环不只是"多出几条测试"。它在工程上有几个被忽视的要点。第一,测试要随源码演进,不能生成后丢着不管。否则很快与实现脱节,反而成为误导。
第二,生成测试的版本与源码版本必须绑定。回滚源码时测试要同步回滚,避免拿旧测试去对新材料。第三,闭环要在 CI 强制生效,而非靠开发者自觉调用。否则工具再好也落不了地。
最后,要定期做"变异测试"反向验证。故意改坏源码看测试能不能抓到,抓不到说明断言是空摆设。
五、总结
LLM 生成代码的速度,超过了人工补测试的速度。把测试生成也交给 LLM,并纳入闭环校验,是当前可行的兜底。机制上以契约驱动生成、覆盖率回归、弱断言检测,守住可信度。工程上靠沙箱执行与 CI 门禁落地。
落地路线:先做 AST 契约抽取;再接 LLM 生成边界用例;接沙箱执行与覆盖率回归;上 CI 门禁强制生效;最后定期做变异测试反查。代码可以 LLM 写,但质量门必须工程化把住。
网硕互联帮助中心


![[Ai Agent] 10 MCP基础:快速编写你自己的MCP服务器(Server)-网硕互联帮助中心](https://www.wsisp.com/helps/wp-content/uploads/2026/07/20260725065047-6a645cc7279a1-220x150.png)

评论前必须登录!
注册