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 做它擅长的事(发散生成),用规则和代码做它不擅长的事(精确校验)。
这个方案的三个关键环节:
- 精心设计的提示词(控制变体的多样性和质量)
- 多层自动校验(Schema 校验 + 交叉验证 + 难度评估)
- 必过人工审核(最后的兜底防线)
题库的质量永远是第一位的——宁可少 100 道题,也不要有 10 道错题混进去。AI 只是加速器,不是质量保证的替代品。