AI 辅助题库建设:用 LLM 自动生成题目变体与测试用例
一、深度引言与场景痛点:造题比刷题难十倍
刚开始做刷题系统时,我以为最难的部分是写判题引擎。后来才发现,最难的是持续产出高质量的题目和测试用例。
手动造一道好题的过程是这样的:先想一个核心算法点,设计输入输出格式,编写题目描述,构造 10~20 组测试用例(包括边界条件),再用多种语言写一遍参考答案。这一套下来,一道中等难度的题至少要 4 个小时。如果题库目标是 500 道,一个人全职做也需要近一年。
但问题的另一面是:LLM 在"生成变体"这件事上表现出奇地好。给一个经典题目(如"两数之和"),它能生成多个变体(三数之和、最接近的三数之和、四数之和),并且保持核心算法思路的一致性。本文记录我在实践中使用 LLM 辅助题库建设的完整方案。
二、底层机制与原理深度剖析
LLM 自动生成题目的核心链路如下:
这条流水线的核心思想是:LLM 负责发散,自动化校验负责收敛。LLM 在发散阶段可以产生大量候选题目,校验层则像一道滤网,确保只有真正可用的题目才进入题库。
三、生产级代码实现与最佳实践
题目变体生成的提示词设计
# LLM 题目变体生成器 —— 核心在于提示词的约束设计
import json
from openai import OpenAI
class ProblemVariantGenerator:
"""基于种子题目,使用 LLM 生成多个变体"""
SYSTEM_PROMPT = """你是一位资深算法竞赛出题人。你的任务是根据给定的种子题目,生成 {count} 个变体。
必须严格遵守以下规则:
1. 变体必须保留种子的核心算法思想,但可以改变场景、数据范围或约束条件
2. 每道变体必须有一个独特的角度,不能是简单的参数修改
3. 测试用例必须覆盖:正常情况、边界情况(空输入、极值、单元素)、性能极限情况
4. 输出必须是合法的 JSON,格式与输入一致
5. 如果生成过程中发现无法产生有意义的变体,在这个位置返回 null"""
def __init__(self, api_key: str, model: str = "gpt-4"):
self.client = OpenAI(api_key=api_key)
self.model = model
self.validator = ProblemSchemaValidator()
def generate_variants(
self,
seed_problem: dict,
count: int = 3,
temperature: float = 0.8
) -> list[dict]:
"""生成题目变体列表
Args:
seed_problem: 种子题目的完整 JSON
count: 期望生成的变体数量
temperature: 创造性参数,0.8 在多样性和可控性间取得平衡
Returns:
通过校验的变体列表,可能少于 count(部分被过滤)
"""
user_prompt = self._build_variant_prompt(seed_problem, count)
response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": self.SYSTEM_PROMPT.format(count=count)},
{"role": "user", "content": user_prompt}
],
temperature=temperature,
# response_format 确保返回合法 JSON,减少格式解析失败的概率
response_format={"type": "json_object"}
)
raw_output = json.loads(response.choices[0].message.content)
variants = raw_output.get("variants", [])
# 校验 + 过滤
valid_variants = []
for v in variants:
if v is None:
continue
try:
self.validator.validate(json.dumps(v))
valid_variants.append(v)
except Exception as e:
print(f"[变体被过滤] 原因: {e}")
return valid_variants
def _build_variant_prompt(self, seed: dict, count: int) -> str:
return f"""种子题目:
```json
{json.dumps(seed, ensure_ascii=False, indent=2)}
请为这道题生成 {count} 个变体。返回格式如下:
{{
"variants": [
{{…}}, // 变体 1 的完整题目 JSON
{{…}}, // 变体 2 的完整题目 JSON
null // 如果无法生成更多变体,用 null 占位
]
}}
变体设计要点:
- 每个变体的 id 使用新的编号(P{seed['id'][1:]}X1 ~ P{seed['id'][1:]}X{count})
- difficulty 可以与种子不同(如果变体引入了新的复杂度)
- 测试用例至少 10 组,其中至少 3 组是边界条件
- description 必须完整,不能引用"同种子题目"之类的表述"""
### 测试用例的交叉验证
```python
# 测试用例交叉验证器 —— 确保测试用例本身是正确的
class TestCaseValidator:
"""验证生成的测试用例是否自洽"""
def validate_cross(self, problem: dict, reference_solution: str) -> dict:
"""交叉验证:用参考答案逐个运行测试用例
核心逻辑:
1. 用已知正确的参考答案执行每个测试用例
2. 如果参考答案的输出与 expectedOutput 不一致,
说明测试用例本身有误(输入或期望输出写错了)
3. 这可以过滤掉 LLM 幻觉产生的不一致数据
"""
test_cases = problem.get("testCases", [])
results = {"passed": [], "failed": [], "error": []}
for i, tc in enumerate(test_cases):
try:
actual = run_code_with_input(
reference_solution, tc["input"], timeout_ms=2000
)
expected = tc["expectedOutput"].strip()
# 考虑到浮点数精度问题,使用模糊比较
if self._fuzzy_match(actual, expected):
results["passed"].append(i)
else:
results["failed"].append({
"index": i,
"expected": expected,
"actual": actual
})
except TimeoutError:
results["error"].append({
"index": i,
"reason": "参考答案执行超时,测试用例可能过大"
})
except Exception as e:
results["error"].append({
"index": i,
"reason": str(e)
})
return results
def _fuzzy_match(self, actual: str, expected: str, eps: float = 1e-6) -> bool:
"""模糊匹配:处理浮点数精度问题"""
actual = actual.strip()
expected = expected.strip()
if actual == expected:
return True
# 尝试按数值比较
try:
actual_nums = [float(x) for x in actual.split()]
expected_nums = [float(x) for x in expected.split()]
if len(actual_nums) != len(expected_nums):
return False
for a, e in zip(actual_nums, expected_nums):
if abs(a – e) > eps:
return False
return True
except ValueError:
return False
四、边界分析与架构权衡
LLM 生成的质量控制
实践中发现,LLM 生成的题目有三个常见问题:
测试用例逻辑错误:LLM 在生成测试用例时,有时会写一个输入,然后拍脑袋写一个输出——这个输出不一定是正确的。必须用参考答案交叉验证,否则这些错误用例会被用户当作"标准答案"。
题目描述的前后矛盾:比如"In the first line…"后面又说"Each line contains…"。这个问题在长文本生成中很常见,目前没有完美的自动检测方案,必须人工审核。
难度标注不准:LLM 倾向于把大多数题标为 MEDIUM。需要额外的难度评估模型(这个在下一篇会详细讲)。
成本分析
以 GPT-4 为例,每生成 10 个变体(含完整测试用例),Token 消耗约为:
- 输入:~3000 tokens(种子题目 + 提示词)
- 输出:~8000 tokens(10 道变体)
单次生成成本约 $0.30。当题库达到 500 道时,采用此方案比纯人工节省约 90% 的时间成本。
人工审核不能省略
自动化流水线的目标是减少人工工作量,而不是消除人工审核。每道 AI 生成的题目,在加入题库前仍然需要至少一个人过一遍。但审核 10 道 AI 生成的题目(每道约 2 分钟),远比手写 10 道题(每道约 4 小时)快。
五、总结
用 LLM 辅助题库建设的核心经验是:让 LLM 做它擅长的事(发散生成),用规则和代码做它不擅长的事(精确校验)。
这个方案的三个关键环节:
题库的质量永远是第一位的——宁可少 100 道题,也不要有 10 道错题混进去。AI 只是加速器,不是质量保证的替代品。




