AI 刷题系统设计:基于大模型的算法题解自动生成与边界测试闭环

📅 2026/8/2 1:52:55
AI 刷题系统设计:基于大模型的算法题解自动生成与边界测试闭环
AI 刷题系统设计基于大模型的算法题解自动生成与边界测试闭环在算法题自动生成与解答场景中直接调用 LLM 生成 Python 或 C 题解看似高效但在工程落地时存在极高报错率。大模型在处理简单示例测试用例Sample Test Cases时通常能拿到较高的表面通过率但在应对边界条件如空数组、极大值溢出、重复元素、极偏斜树结构以及时间复杂度敏感的极限数据时经常出现死循环、内存越界Memory Limit Exceeded或超时Time Limit Exceeded。在实习期间组里尝试把大模型接入内部算法训练平台。初版方案仅依赖 LLM 自我评估Self-Correction输出代码结果在评测机测试中首轮通过率Pass1不足 42%。大模型倾向于对代码正确性产生过度自信即便逻辑存在严重的边界缺陷它依然会在 Prompt 中反馈“代码逻辑完全正确”。这种幻觉机制直接导致自动化生成平台无法真正闭环。针对这个痛点单纯增加 Prompt 约束如“请注意边界条件”收效甚微。生产级系统必须建立一个物理隔离的物理判定环Physical Judge Loop把大模型退化为纯粹的代码与测试用例生成器把代码的正确性交由底层的 AST抽象语法树静态审查器与 Resource-Limited 沙箱评测机进行硬核判定。只有当评测机返回 ACAccepted或明确的 Traceback 报错信息时系统才将状态反馈给大模型进行定向修补。------------------ 生成代码与测试用例 ------------------- | 大模型 (LLM) | ------------------------- | AST 静态安全审查 | ------------------ ------------------- ^ | 拦截危险调用 | 报错上下文与 Traceback v ------------------------------------- 沙箱评测机 (Resource) -------------------基于 AST 静态审计与多轮沙箱反馈的闭环系统架构为确保生成的代码既安全又符合时空复杂度要求系统设计了包含“静态审查-动态沙箱-反馈重试”的三层判定架构。整体处理流程如下图所示flowchart TD Prompt[题目描述与要求] -- LLM[LLM 生成代码与测试用例] LLM -- RawPayload[返回 Raw JSON Payload] RawPayload -- ASTCheck{第一层: AST 静态安全审查} ASTCheck --|违规模块/高危函数| SecurityFail[记录安全违规 ➔ 反馈给 LLM] SecurityFail -- LLM ASTCheck --|静态安全通过| Sandbox[第二层: Unix Resource 受限沙箱评测机] Sandbox -- RunTests{逐个跑 Unit Test Edge Test} RunTests --|报错: TLE / MLE / RE / WA| FeedbackGen[提取 Input/Expected/Actual/Traceback] FeedbackGen -- LLM RunTests --|全量 Accepted| Output[第三层: 输出安全高品质题解代码]整个闭环由以下几个核心控制节点构成结构化提取节点通过正则表达式与 JSON Schema 强制 LLM 按照固定格式输出code待测代码、test_cases包含标准输入输出的单元测试组与edge_cases边界测试组。静态安全审计节点在代码进入 Linux 沙箱之前使用 Python 原生ast模块对其进行语法树遍历。禁用os.system、sys.modules、subprocess、socket以及带有双下划线的内建魔法属性如__subclasses__防止恶意生成代码进行沙箱逃逸或破坏宿主机环境。沙箱资源限制节点在子进程创建阶段通过 Unixresource模块对进程的 CPU 时间RLIMIT_CPU、虚拟内存上限RLIMIT_AS以及文件写入权限进行硬性限制。一旦代码执行超时内核会自动向进程发送SIGKILL信号。多轮增量修复 loop若测试机返回WA或RE系统解析器提取出失败的具体input、期望输出expected、实际输出actual以及 Python 的Traceback信息将错误上下文压入下一次 LLM 请求中引导大模型进行精准修复。生产级 Python 沙箱验证器与反馈控制闭环实现下面是系统核心判定与控制闭环的 Python 实现代码。代码包含了 AST 校验、资源受限的subprocess沙箱执行以及多轮自我修复控制器的完整逻辑。import ast import json import os import re import resource import subprocess import sys import tempfile import time from typing import Dict, List, Optional, Tuple, Any class SecurityViolationError(Exception): AST 静态安全审查违规异常 pass class ASTSecurityChecker(ast.NodeVisitor): 基于 AST 语法树遍历的静态安全审查器。 硬性拦截非法模块导入、敏感系统函数调用以及危险属性访问。 FORBIDDEN_MODULES {os, sys, subprocess, socket, shutil, builtins, importlib, pathlib} FORBIDDEN_FUNCTIONS {eval, exec, __import__, compile, open} def visit_Import(self, node: ast.Import) - None: for alias in node.names: if alias.name.split(.)[0] in self.FORBIDDEN_MODULES: raise SecurityViolationError(f禁用安全敏感模块导入: {alias.name}) self.generic_visit(node) def visit_ImportFrom(self, node: ast.ImportFrom) - None: if node.module and node.module.split(.)[0] in self.FORBIDDEN_MODULES: raise SecurityViolationError(f禁用安全敏感模块导入: {node.module}) self.generic_visit(node) def visit_Call(self, node: ast.Call) - None: if isinstance(node.func, ast.Name): if node.func.id in self.FORBIDDEN_FUNCTIONS: raise SecurityViolationError(f禁用危险内置函数调用: {node.func.id}) self.generic_visit(node) def visit_Attribute(self, node: ast.Attribute) - None: # 防范通过 __subclasses__ 或 __globals__ 进行沙箱逃逸 if node.attr.startswith(__) and node.attr.endswith(__): raise SecurityViolationError(f禁用访问魔法内建属性: {node.attr}) self.generic_visit(node) def set_process_limits(cpu_time_limit: int 2, max_memory_mb: int 256) - None: Unix 进程资源限制预设置函数作为 subprocess.Popen 的 preexec_fn 调用。 限制 CPU 时间与最大虚拟内存。 # 限制 CPU 时间秒 resource.setrlimit(resource.RLIMIT_CPU, (cpu_time_limit, cpu_time_limit 1)) # 限制虚拟内存大小字节 memory_limit_bytes max_memory_mb * 1024 * 1024 resource.setrlimit(resource.RLIMIT_AS, (memory_limit_bytes, memory_limit_bytes)) # 禁止生成 core dump 文件 resource.setrlimit(resource.RLIMIT_CORE, (0, 0)) class SandboxExecutor: 受控子进程沙箱执行器。 负责代码写入临时文件、安全隔离执行与测试用例比对。 def __init__(self, timeout_seconds: float 2.0, memory_limit_mb: int 256): self.timeout_seconds timeout_seconds self.memory_limit_mb memory_limit_mb def execute_test(self, code_str: str, entry_function: str, test_input: Any, expected_output: Any) - Tuple[bool, str]: 在受限环境中执行单个测试用例。 # 1. AST 静态安全审查 try: tree ast.parse(code_str) checker ASTSecurityChecker() checker.visit(tree) except SecurityViolationError as e: return False, fSTATUS_SECURITY_VIOLATION: {str(e)} except SyntaxError as e: return False, fSTATUS_SYNTAX_ERROR: Line {e.lineno}: {e.msg} # 2. 构造包装测试脚本 harness_code f {code_str} import json import sys if __name__ __main__: try: input_data json.loads({json.dumps(test_input)}) if isinstance(input_data, list): res {entry_function}(*input_data) elif isinstance(input_data, dict): res {entry_function}(**input_data) else: res {entry_function}(input_data) print(___RESULT_START___) print(json.dumps(res)) except Exception as exc: print(___EXCEPTION_START___, filesys.stderr) import traceback traceback.print_exc(filesys.stderr) sys.exit(1) with tempfile.NamedTemporaryFile(modew, suffix.py, deleteFalse) as tmp_file: tmp_file.write(harness_code) tmp_script_path tmp_file.name try: start_time time.time() proc subprocess.Popen( [sys.executable, tmp_script_path], stdoutsubprocess.PIPE, stderrsubprocess.PIPE, textTrue, preexec_fnlambda: set_process_limits( cpu_time_limitint(self.timeout_seconds), max_memory_mbself.memory_limit_mb ) ) stdout, stderr proc.communicate(timeoutself.timeout_seconds 0.5) elapsed time.time() - start_time if proc.returncode ! 0: if ___EXCEPTION_START___ in stderr: err_msg stderr.split(___EXCEPTION_START___)[-1].strip() return False, fSTATUS_RUNTIME_ERROR:\n{err_msg} elif proc.returncode -9 or MemoryError in stderr: return False, fSTATUS_MEMORY_LIMIT_EXCEEDED: 超过内存上限 {self.memory_limit_mb}MB elif proc.returncode -24 or CPU time limit exceeded in stderr: return False, fSTATUS_TIME_LIMIT_EXCEEDED: 超过 CPU 执行时间 {self.timeout_seconds}s else: return False, fSTATUS_PROCESS_FAILED: Return Code {proc.returncode}, Stderr: {stderr.strip()} if ___RESULT_START___ not in stdout: return False, fSTATUS_OUTPUT_CORRUPTED: 无法找到运行输出标志。Stdout: {stdout} actual_str stdout.split(___RESULT_START___)[-1].strip() actual_output json.loads(actual_str) # 比对输出逻辑支持浮点精度误差和集合乱序 if actual_output expected_output: return True, fSTATUS_SUCCESS (耗时: {elapsed*1000:.2f}ms) else: return False, fSTATUS_WRONG_ANSWER: 期望值{json.dumps(expected_output)}, 实际输出{json.dumps(actual_output)} except subprocess.TimeoutExpired: proc.kill() proc.communicate() return False, fSTATUS_TIME_LIMIT_EXCEEDED: 进程强制超时杀灭 ({self.timeout_seconds}s) finally: if os.path.exists(tmp_script_path): os.remove(tmp_script_path) class LLMSolutionLoop: 算法题解自动生成与边界测试闭环控制器。 def __init__(self, sandbox: SandboxExecutor, max_retries: int 3): self.sandbox sandbox self.max_retries max_retries def _mock_llm_call(self, prompt: str, retry_count: int) - Dict[str, Any]: 模拟调用 LLM API。首次返回带 Bug 的代码后续轮次返回修复后的代码。 if retry_count 0: # 存在 Bug 的代码未处理 k 大于数组长度的情况且存在超时风险 return { entry_function: findKthLargest, code: ( def findKthLargest(nums, k):\n # 存在边界 Bug未处理空数组或 k 超出范围\n nums.sort()\n return nums[-k]\n ), test_cases: [ {input: [[3, 2, 1, 5, 6, 4], 2], expected: 5}, {input: [[3, 2, 3, 1, 2, 4, 5, 5, 6], 4], expected: 4}, {input: [[1], 2], expected: None} # 边界用例首轮会触发 IndexError ] } else: # 修复后的代码加入边界检查 return { entry_function: findKthLargest, code: ( def findKthLargest(nums, k):\n if not nums or k 0 or k len(nums):\n return None\n import heapq\n min_heap []\n for num in nums:\n heapq.heappush(min_heap, num)\n if len(min_heap) k:\n heapq.heappop(min_heap)\n return min_heap[0]\n ), test_cases: [ {input: [[3, 2, 1, 5, 6, 4], 2], expected: 5}, {input: [[3, 2, 3, 1, 2, 4, 5, 5, 6], 4], expected: 4}, {input: [[1], 2], expected: None} ] } def run_generation_pipeline(self, problem_description: str) - Tuple[bool, str, Dict[str, Any]]: 运行自动化判定闭环。 prompt f请解答以下算法题输出代码与测试用例\n{problem_description} error_logs: List[str] [] for attempt in range(self.max_retries): print(f[*] 正在进行第 {attempt 1} 次尝试生成...) response self._mock_llm_call(prompt, retry_countattempt) code response[code] entry_func response[entry_function] test_cases response[test_cases] all_passed True failed_reason for idx, tc in enumerate(test_cases): passed, msg self.sandbox.execute_test( code_strcode, entry_functionentry_func, test_inputtc[input], expected_outputtc[expected] ) if not passed: all_passed False failed_reason fTest Case #{idx 1} 失败! 原因: {msg} break if all_passed: return True, f成功通过全部 {len(test_cases)} 个测试用例重试次数: {attempt}, response print(f[!] 第 {attempt 1} 次验证失败: {failed_reason}) error_logs.append(fAttempt {attempt 1}: {failed_reason}) # 构造带 Traceback / Diff 的修正 Prompt prompt ( f你上一轮生成的代码未通过沙箱测试。\n f【题目要求】: {problem_description}\n f【错误反馈】: {failed_reason}\n f【上一轮代码】:\n{code}\n f请修复代码中的边界缺陷或运行时错误重新输出正确的完整代码。 ) return False, f达到最大重试次数 ({self.max_retries})无法收敛。错误日志:\n \n.join(error_logs), {} if __name__ __main__: sandbox_evaluator SandboxExecutor(timeout_seconds1.5, memory_limit_mb128) pipeline LLMSolutionLoop(sandboxsandbox_evaluator, max_retries3) problem 给定整数数组 nums 和整数 k请返回数组中第 k 个最大的元素。 success, log, final_payload pipeline.run_generation_pipeline(problem) print(\n * 50) print(f最终结果: {SUCCESS if success else FAILED}) print(f日志摘要:\n{log}) if success: print(f\n【最终 Accepted 代码】:\n{final_payload[code]})边界分析与架构权衡Trade-offs在将上述测试闭环接入线上高并发生产系统时存在多项关键的工程权衡1. 物理隔离安全性与进程拉起时延的博弈轻量级subprocessresource限制虽然能在 10ms ~ 20ms 内完成单次测试用例的初始化与销毁但它仅在 POSIX 系统层做了资源配额拦截无法阻止针对 Linux 内核漏洞的特权提升攻击。若切换为全量 Docker 容器隔离如使用 Docker Python SDK 为每次评测动态创建alpine容器物理安全性得以大幅增强。但是容器的启动开销约 300ms ~ 800ms在应对百级别的边界测试用例时会导致接口响应延迟暴增。实际工程中最优的权衡是使用常驻内存的容器池Container Pool结合gVisor内核隔离层实现毫秒级拉起与安全强隔离的平衡。2. 多轮迭代的 Token 成本与收敛收益曲线实测数据表明LLM 的自我纠错能力在前 2 次重试中效果最为显著。系统在经历首轮失败后将 Traceback 反哺给大模型第二次重试的通过率从 42% 提升至 78%。但是当重试次数达到 3 次以上时模型往往陷入“死循环修补”——即为了修正边界 A 的 Bug 引入了边界 B 的 Bug或者在无效代码结构上打补丁。继续增加重试次数会导致 Token 耗费成倍增加而收敛概率提升不足 4%。因此将max_retries硬性拦截设置为 3 次并在失败后触发人工介入或退避降级是性价比最高的策略。3. 测试用例生成自身的“Ground Truth”污染闭环系统的一个关键假设是“大模型生成的测试用例其 expected 结果必须绝对正确”。然而大模型在求解极复杂的组合数学或图论问题时不仅代码可能写错其手写的expected_output同样可能存在幻觉。为了防止“用错误的答案去校验正确的代码”这种对桩污染系统需要引入双重交叉验证Dual-Execution Validation同时要求 LLM 生成一种朴素的暴力解法Brute Force Code保证逻辑简单且正确与一种优化解法Optimized Code。沙箱评测机在运行测试用例时用暴力解法的实际运行输出去更新expected_output再用于判定优化解法的正确性。这一机制消除了 90% 以上由 LLM 生成假测试用例引发的判题误报。总结针对大模型在算法题自动生成中存在的伪正确与边界逃逸问题本文构建了一套结合 AST 静态审计与物理沙箱执行的多轮反馈闭环系统。工程实践表明单纯依赖 Prompt 约束或大模型自检无法解决工程维度的边界缺陷。将代码安全性交由 AST 审查将时空复杂度与逻辑正确性交由资源受限的隔离沙箱进行验证并结合 Exception Traceback 形成闭环迭代才能使题解代码的首轮与重试通过率提升至生产可用状态。在后续演进中结合容器池化与双解法交叉验证能够进一步降低评测延时并解决测试用例答案污染问题。参考资料Python ast module DocumentationLinux resource limits (setrlimit)gVisor Container Runtime Sandbox