十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Claude Code的AI循环工程:自动化代码测试与修复实践

基于Claude Code的AI循环工程:自动化代码测试与修复实践 1. 项目概述当AI学会“自己改Bug”最近在开发者圈子里一个叫“Loop Engineering”的概念突然火了起来。简单来说它描述的是一种让AI编程助手比如Claude Code能够自动、循环地验证和修复代码直到所有测试用例全部通过也就是“全绿”的工作流。这听起来有点像科幻电影里的场景——你把一段半成品代码扔给AI它就开始自己运行测试、分析失败原因、修改代码、再运行测试……如此循环直到屏幕上出现那个令人愉悦的“All tests passed”。我第一次看到这个标题时心里是既兴奋又怀疑的。兴奋在于如果真能实现那简直就是“懒人”程序员的终极梦想尤其是面对那些繁琐的调试和边界条件处理时。怀疑则在于目前的AI真的能可靠地完成这种需要逻辑推理和上下文理解的复杂任务吗它会不会陷入死循环或者把代码改得面目全非经过一番折腾和实测我发现这个“三个文件让Claude Code循环验证代码直到全绿”的方案其核心并非依赖某个神秘的黑科技而是巧妙地组合了现有工具链主要是Claude Code的API能力和一个精心设计的“监督循环”脚本。它解决的痛点非常明确将开发者从“运行测试 - 看错误 - 手动提示AI修改 - 再运行测试”的重复劳动中解放出来把迭代过程自动化。这特别适合那些测试用例明确但实现起来琐碎、或者需要反复调整算法细节的场景比如刷LeetCode、实现某个特定算法、或者为现有代码库添加功能并确保回归测试通过。接下来我就把自己搭建和思考这个循环验证系统的全过程拆解给你看。你会发现它不需要高深的机器学习知识本质上是一个“胶水脚本”但其中关于提示工程、错误处理和工作流设计的门道才是真正值钱的经验。2. 核心思路与架构拆解这个项目的目标很清晰构建一个自动化系统输入是一个包含待实现功能或待修复Bug的代码文件以及对应的测试文件输出是一个所有测试用例都通过的代码文件。整个过程无需人工干预。2.1 为什么是“三个文件”标题里提到的“三个文件”是整个系统的骨架。它们各司其职构成了一个最小可行闭环主控脚本 (runner.py / controller.py)这是系统的大脑。它负责启动整个循环流程调用Claude Code API获取修改后的代码执行测试分析测试结果判断是继续循环还是成功退出。它包含了核心的业务逻辑和循环控制。任务描述与约束文件 (spec.md / requirements.txt)这是系统的需求说明书。它定义了要做什么、不能做什么。内容通常包括要实现的函数签名、功能描述、输入输出示例、边界条件、性能要求、代码风格约束如不能使用某些库、以及最重要的——测试套件的运行命令如pytest test_file.py。这个文件的质量直接决定了AI修改的方向是否准确。初始代码文件 (initial_code.py)这是系统的起点。可以是一个空函数框架只有def和pass也可以是一个有Bug的初始实现甚至是一段伪代码。它为AI提供了修改的上下文和代码结构。这个“三文件”架构的精妙之处在于它的关注点分离和极简主义。主控脚本处理流程和决策任务描述文件定义目标初始代码文件提供操作对象。任何复杂的任务都可以被分解进这个框架里。2.2 核心组件选型背后的逻辑整个系统的运转依赖于几个关键组件每个选择都有其考量AI引擎Claude Code为什么是Claude Code而不是其他在尝试了多个模型后Claude Code特别是Claude 3系列中的Code变体在代码理解和生成任务上表现出了更强的逻辑一致性和对指令的遵循能力。对于循环验证这种需要严格根据测试错误信息进行推理的任务模型“听话”比“聪明”更重要。Codex或GPT-4有时会过度发挥引入未要求的优化或改变接口而Claude Code在遵循spec.md中的约束方面更为稳定。API vs. 桌面版为了实现自动化必须使用API。桌面版虽然交互友好但无法被脚本调用。项目中使用的是Claude的API通常是claude-3-opus或claude-3-sonnet兼顾效果与成本。测试框架Pytest (Python为例)普适性与清晰输出Pytest是Python社区的事实标准它提供的错误报告非常清晰能明确指出哪个测试用例失败、期望值和实际值是什么。这份错误信息是AI进行下一轮修改的核心依据。其他语言也有类似选择如JUnit for Java, Jest for JavaScript。循环控制逻辑这不是一个现成的库而是需要我们在主控脚本中实现的逻辑。核心循环如下读取初始代码和任务描述。将“当前代码 任务描述 上一轮的错误输出”组合成提示词调用Claude Code API。获取AI返回的新代码覆盖原文件。运行测试命令捕获输出。判断如果测试全部通过则成功退出如果失败则提取错误信息回到步骤2如果循环超过最大次数如10次则失败退出避免无限循环消耗资源。注意最大的风险在于AI可能会“瞎改”。例如为了通过一个测试它可能会直接修改测试用例本身或者用一个极其取巧但不符合要求的方式绕过问题。因此在spec.md中必须明确强调“不允许修改测试文件”并且主控脚本在每次迭代后可以简单校验测试文件是否被意外更改。3. 实操搭建从零构建你的循环工程师理论说再多不如动手做一遍。我们以Python环境为例搭建一个针对“实现一个函数计算列表平均值”的循环验证系统。3.1 环境准备与依赖安装首先确保你的环境已经就绪。# 1. 创建项目目录并进入 mkdir loop_engineering_demo cd loop_engineering_demo # 2. 创建虚拟环境推荐避免包冲突 python -m venv venv # Windows: venv\Scripts\activate # Mac/Linux: source venv/bin/activate # 3. 安装核心依赖 pip install pytest # 安装Claude SDK以anthropic库为例 pip install anthropic你需要一个Claude API密钥。前往Claude官网注册开发者账号并获取密钥然后将其设置为环境变量这是最安全的方式。# Linux/Mac export ANTHROPIC_API_KEYyour-api-key-here # Windows (PowerShell) $env:ANTHROPIC_API_KEYyour-api-key-here3.2 三个核心文件的创建与编写现在我们来创建那三个灵魂文件。文件一spec.md(任务描述与约束)这个文件要写得像给一个认真但有点刻板的实习生看的任务书必须详尽无歧义。# 任务实现 calculate_average 函数 ## 函数签名 python def calculate_average(numbers: list[float]) - float:功能描述计算给定数字列表的算术平均值。平均值定义为所有元素之和除以元素个数。输入输出示例calculate_average([1.0, 2.0, 3.0])应该返回2.0calculate_average([0.0])应该返回0.0calculate_average([-1.0, 0.0, 1.0])应该返回0.0边界条件与要求如果输入列表为空函数应该抛出ValueError异常异常信息为 List cannot be empty。函数必须能正确处理正数、负数和零。只允许使用Python标准库不能导入任何第三方库如numpy。绝对禁止修改测试文件test_average.py。代码需符合PEP 8风格指南。测试方法保存你的实现到average.py文件后在终端运行以下命令进行验证pytest test_average.py -v只有当所有测试用例都通过显示“PASSED”或“全绿”时任务才算完成。**文件二average.py (初始代码)** 我们从一个有明显Bug的实现开始这样更能体现循环修复的过程。 python # 初始版本存在Bug def calculate_average(numbers: list[float]) - float: # 错误1没有处理空列表 # 错误2使用了整数除法如果sum是整数 return sum(numbers) / len(numbers)文件三test_average.py(测试文件)这是我们的“真理标准”。测试必须全面。import pytest from average import calculate_average def test_basic_average(): assert calculate_average([1.0, 2.0, 3.0]) 2.0 assert calculate_average([5.0, 5.0, 5.0, 5.0]) 5.0 def test_with_negatives_and_zero(): assert calculate_average([-1.0, 0.0, 1.0]) 0.0 assert calculate_average([-2.0, -4.0]) -3.0 def test_single_element(): assert calculate_average([7.5]) 7.5 assert calculate_average([0.0]) 0.0 def test_empty_list_raises_error(): with pytest.raises(ValueError) as exc_info: calculate_average([]) # 检查异常信息是否匹配 assert str(exc_info.value) List cannot be empty # 可选增加一个非整数的测试 def test_float_numbers(): assert abs(calculate_average([1.5, 2.5]) - 2.0) 1e-103.3 主控脚本loop_engineer.py的完整实现这是最核心的部分。我们将一步步构建它。import subprocess import sys import time import anthropic import os class LoopEngineeringAgent: def __init__(self, api_key: str, model: str claude-3-sonnet-20240229): self.client anthropic.Anthropic(api_keyapi_key) self.model model self.max_iterations 10 # 防止无限循环 self.iteration 0 def read_file(self, filepath: str) - str: 读取文件内容 with open(filepath, r, encodingutf-8) as f: return f.read() def write_file(self, filepath: str, content: str): 写入文件内容 with open(filepath, w, encodingutf-8) as f: f.write(content) def run_tests(self, test_command: str) - (bool, str): 运行测试命令返回是否成功输出信息 使用subprocess来捕获测试结果 try: # 超时设置防止某些测试卡死 result subprocess.run( test_command, shellTrue, capture_outputTrue, textTrue, timeout30 ) # 返回码为0通常表示所有测试通过 if result.returncode 0: return True, result.stdout else: return False, result.stderr if result.stderr else result.stdout except subprocess.TimeoutExpired: return False, Test execution timed out after 30 seconds. except Exception as e: return False, fError running tests: {str(e)} def extract_error_context(self, test_output: str) - str: 从pytest错误输出中提取最相关的错误信息。 这是一个简单的实现你可以根据实际输出格式增强它。 # 查找典型的失败断言行 lines test_output.split(\n) error_lines [] for i, line in enumerate(lines): if FAILED in line or AssertionError in line or Error: in line: # 捕获错误行及其前后几行作为上下文 start max(0, i - 3) end min(len(lines), i 4) error_lines.extend(lines[start:end]) if not error_lines: # 如果没有找到典型错误格式返回最后一部分输出 return \n.join(lines[-20:]) return \n.join(error_lines) def call_claude_for_fix(self, current_code: str, spec: str, error_context: str ) - str: 构造提示词调用Claude API获取修复后的代码。 这是提示工程的关键部分。 system_prompt 你是一个资深软件工程师擅长根据测试失败信息修复代码Bug。你的任务是严格遵循需求规格说明书spec修改提供的代码使其通过所有测试用例。只返回修改后的完整代码文件内容不要有任何额外的解释、标记或注释。 user_prompt f # 需求规格说明书 (Specification) {spec} # 当前需要修复的代码文件内容 python {current_code}最近一次测试运行失败信息{error_context if error_context else 这是第一次运行尚未有错误信息。}请严格分析测试失败原因。根据spec的要求修复average.py文件中的代码。重要只修改average.py中的calculate_average函数。绝对不要修改测试文件test_average.py或任何其他文件。确保你的修改完全符合spec中描述的所有功能、边界条件和约束。返回完整的、修改后的average.py文件内容且仅此内容。 try: response self.client.messages.create( modelself.model, max_tokens2000, temperature0.1, # 低温度确保输出确定性高不“乱写” systemsystem_prompt, messages[ {role: user, content: user_prompt} ] ) # 提取返回内容并尝试清理可能存在的markdown代码块标记 new_code response.content[0].text # 移除可能的python ...包装 if new_code.startswith(python): new_code new_code.split(\n, 1)[1] # 移除第一行 if new_code.endswith(): new_code new_code.rsplit(\n, 1)[0] # 移除最后一行 return new_code.strip() except Exception as e: print(f调用Claude API时出错: {e}) return Nonedef run(self, code_file: str, spec_file: str, test_cmd: str): 主循环 print(f 开始循环工程任务目标文件: {code_file}) spec_content self.read_file(spec_file)for self.iteration in range(1, self.max_iterations 1): print(f\n--- 迭代第 {self.iteration} 轮 ---) current_code self.read_file(code_file) print(f当前代码预览:\n{current_code[:200]}...) # 运行测试 print(f执行测试命令: {test_cmd}) success, test_output self.run_tests(test_cmd) if success: print(f✅ 所有测试通过) print(f测试输出:\n{test_output[-500:]}) # 打印最后一部分输出 print(f 任务成功完成于第 {self.iteration} 轮迭代。) return True else: print(f❌ 测试失败。) error_context self.extract_error_context(test_output) print(f错误摘要:\n{error_context}) # 调用AI进行修复 print( 调用Claude分析错误并修复代码...) new_code self.call_claude_for_fix(current_code, spec_content, error_context) if new_code is None or new_code current_code: print(⚠️ AI未返回有效修改或代码无变化可能已陷入僵局。终止循环。) break # 保存修复后的代码 self.write_file(code_file, new_code) print(f 代码已更新。等待1秒后继续...) time.sleep(1) # 简单限流避免API速率限制 print(f\n 已达到最大迭代次数({self.max_iterations})仍未成功。任务失败。) return Falseifname main: # 从环境变量获取API密钥 api_key os.environ.get(ANTHROPIC_API_KEY) if not api_key: print(错误请设置 ANTHROPIC_API_KEY 环境变量。) sys.exit(1)agent LoopEngineeringAgent(api_keyapi_key, modelclaude-3-sonnet-20240229) # 配置任务参数 CODE_FILE average.py SPEC_FILE spec.md TEST_CMD pytest test_average.py -v # -v 参数输出详细信息便于AI分析 success agent.run(CODE_FILE, SPEC_FILE, TEST_CMD) sys.exit(0 if success else 1)## 4. 运行、观察与结果分析 现在让我们在终端运行这个脚本观察AI是如何一步步“思考”和“动手”的。 bash python loop_engineer.py你会看到类似如下的输出流这是一个非常有趣的“现场直播” 开始循环工程任务目标文件: average.py --- 迭代第 1 轮 --- 当前代码预览: def calculate_average(numbers: list[float]) - float: return sum(numbers) / len(numbers)... 执行测试命令: pytest test_average.py -v ❌ 测试失败。 错误摘要: test_average.py::test_empty_list_raises_error FAILED ... def test_empty_list_raises_error(): with pytest.raises(ValueError) as exc_info: calculate_average([]) E Failed: DID NOT RAISE class ValueError ... 调用Claude分析错误并修复代码... 代码已更新。等待1秒后继续... --- 迭代第 2 轮 --- 当前代码预览: def calculate_average(numbers: list[float]) - float: if not numbers: raise ValueError(List is empty) return sum(numbers) / len(numbers)... 执行测试命令: pytest test_average.py -v ❌ 测试失败。 错误摘要: test_average.py::test_empty_list_raises_error FAILED ... E AssertionError: assert List is empty List cannot be empty ... 调用Claude分析错误并修复代码... 代码已更新。等待1秒后继续... --- 迭代第 3 轮 --- 当前代码预览: def calculate_average(numbers: list[float]) - float: if not numbers: raise ValueError(List cannot be empty) return sum(numbers) / len(numbers)... 执行测试命令: pytest test_average.py -v ✅ 所有测试通过 ... test_average.py::test_basic_average PASSED test_average.py::test_with_negatives_and_zero PASSED test_average.py::test_single_element PASSED test_average.py::test_empty_list_raises_error PASSED test_average.py::test_float_numbers PASSED 任务成功完成于第 3 轮迭代。过程分析第一轮初始代码没有处理空列表test_empty_list_raises_error测试失败。AI识别到问题添加了空列表检查但异常信息与spec要求不符“List is empty” vs “List cannot be empty”。第二轮测试再次失败但这次是异常信息不匹配。AI准确地读取了测试输出中的断言错误将异常信息修正为List cannot be empty。第三轮所有测试通过循环终止。最终average.py文件被自动修改为符合要求的版本def calculate_average(numbers: list[float]) - float: if not numbers: raise ValueError(List cannot be empty) return sum(numbers) / len(numbers)整个过程中开发者只需要启动脚本然后泡杯咖啡等待即可。系统自动完成了“运行-报错-分析-修改-再运行”的完整闭环。5. 深入解析提示工程与错误处理的精髓这个项目的效果八成取决于提示词Prompt和错误处理逻辑的设计。这里有几个我踩过坑才总结出的要点。5.1 构造高成功率提示词的技巧给AI的指令必须像法律条文一样精确减少歧义空间。系统提示词定角色system_prompt里明确“资深软件工程师”、“只返回代码不要解释”这设定了AI的行为模式减少了无关输出。用户提示词结构化将spec、当前代码、错误信息清晰分块提供。AI尤其是Claude对这种结构化的上下文理解得更好。强调禁令反复强调“绝对不要修改测试文件”。这是防止AI“作弊”的最关键约束。我曾在早期版本中忽略这点结果AI直接把测试用例里的assert改成了pass让人哭笑不得。明确输出格式要求“返回完整的、修改后的average.py文件内容且仅此内容”。这避免了AI在代码前后加上“Heres the fix:”之类的废话方便脚本直接提取和写入。控制“创造力”设置temperature0.1。这个参数控制输出的随机性。在代码修复任务中我们需要的是确定性和一致性而不是天马行空的“创意”。低温度值能确保AI更严格地遵循指令和已有模式。5.2 健壮的错误提取与循环控制测试框架的输出可能很冗长。如何把最关键的错误信息喂给AI直接影响下一轮修复的效率。不要扔给AI整个终端日志那样会浪费大量TokenAPI成本还可能让AI分心。extract_error_context函数的作用就是做信息过滤。一个更健壮的实现可以解析pytest的特定输出格式精准定位到第一个失败的测试及其堆栈跟踪。设置迭代上限和超时max_iterations和subprocess.run的timeout参数是安全网。防止因AI陷入逻辑死循环比如反复修改同一个地方或某个测试用例卡死而导致脚本永远运行。检测无效修改在脚本中我们有一个简单检查if new_code is None or new_code current_code:。这可以防止因为API调用失败或AI返回无意义内容导致的原地踏步。更高级的检测可以包括语法检查例如用ast.parse。5.3 应对复杂场景的策略上面的例子很简单。面对更复杂的函数或项目时你需要调整策略。多文件项目如果项目涉及多个互相引用的.py文件你需要将整个项目目录的上下文提供给AI。一种方法是将相关文件的内容都读出来放在提示词中。另一种更优雅的方式是使用Claude Code的“文件上传”功能如果API支持但提示词中仍需明确指出哪个是主修改文件。编译型语言对于C/Java等流程类似但测试命令变为编译命令运行测试如g -o test_program test.cpp ./test_program。错误信息通常是编译器错误compile error或运行时错误runtime errorAI同样能处理。模糊或复杂的失败有时测试失败信息很隐晦比如性能不达标或逻辑错误导致结果偏差。这时需要在spec.md中更详细地描述算法逻辑或性能要求。你也可以让AI在修改代码时添加一些调试打印语句但需在最终版本中移除不过这增加了提示词的复杂性。6. 常见问题、局限性与进阶玩法在实际使用中你肯定会遇到各种问题。下面是我整理的一些“坑”和应对方法。6.1 常见问题速查表问题现象可能原因解决方案AI反复修改同一处测试始终失败。1. 错误信息不清晰AI无法理解。2. AI的修改方向根本就是错的。3. spec本身有矛盾或不可能实现。1. 增强extract_error_context提供更精确的错误行和期望值。2. 在提示词中加入“请从不同角度思考问题”。3. 检查spec.md和测试用例的逻辑正确性。AI返回的代码有语法错误导致下一轮测试无法运行。AI偶尔会“胡言乱语”生成无效代码。在主控脚本中添加一个语法验证步骤。在写入文件前用ast.parse(Python)或类似方法检查代码语法。如果无效则用上一轮的有效代码重试并在提示词中告知AI“上次生成的代码有语法错误”。成本失控迭代次数过多。问题太复杂或初始代码离目标太远。1. 设置更小的max_iterations如5。2.分而治之不要试图一次修复所有问题。先让AI通过基础测试再逐步增加复杂测试。3. 使用更便宜的模型如claude-3-haiku进行前期探索。AI修改了测试文件或其他不该改的文件。提示词中的约束不够强力。在系统提示词和用户提示词中多重申明禁令。甚至可以在每次迭代后用脚本校验测试文件的哈希值是否改变如果改变则拒绝并警告AI。API调用速度慢循环间隔长。网络延迟或模型推理耗时。1. 适当增加time.sleep的时间。2. 考虑使用异步请求如果SDK支持。3. 对于简单任务可以尝试更快的模型。6.2 当前技术的局限性Loop Engineering很酷但它不是银弹。高度依赖测试质量Garbage in, garbage out。如果测试用例覆盖不全或有误AI可能会生成一个能通过测试但实际功能错误的“投机”实现。它是在优化“通过测试”这个目标而非理解真正的业务需求。无法进行高层次设计它擅长在既定框架内进行局部修改和调试但无法进行架构设计、模块拆分或算法创新。你无法只给一个“做一个电商网站”的spec就指望它从头构建。上下文长度限制Claude等模型有Token限制。对于大型代码库你无法将全部上下文塞进提示词。需要精心选择最相关的代码片段和错误信息。“怪癖”与不确定性AI有时会表现出奇怪的固执或误解。你可能需要手动调整提示词或者在中途进行人工干预给一点“小提示”。6.3 进阶玩法与扩展思路当你掌握了基础循环后可以尝试这些更有趣的扩展多智能体协作设想两个AI角色一个“开发”负责写代码一个“测试”负责审阅代码并生成新的边界测试用例。让它们相互对抗、迭代直到代码足够健壮。这需要更复杂的协调脚本。集成到CI/CD流水线将这套循环验证机制作为代码提交前的自动检查步骤。当开发者提交一个未通过测试的PR时自动触发这个Agent尝试修复如果修复成功可以自动合并或通知开发者。用于代码重构spec.md可以描述重构要求如“将所有的for循环改为列表推导式同时保持功能不变”初始代码是待重构的旧代码测试套件保证重构前后行为一致。AI可以自动完成这种机械性但易出错的重构工作。跨语言翻译将一种语言的算法实现附测试翻译成另一种语言。你需要提供两种语言的测试套件并让AI在两种语言上下文间切换验证。我个人在实际工作中最常用的场景其实是“单元测试补全”。我写好一个复杂的函数然后手动写几个关键的测试用例剩下的边界用例就让这个Loop Engineering Agent去尝试生成并验证它会在循环中不断添加新的测试用例并调整实现直到覆盖率达到一个令我满意的水平。这比我自己绞尽脑汁想各种边界情况要高效得多。最后想说的是Loop Engineering代表的是一种思维转变从“把AI当作一个问答机”到“把AI当作一个可以自主执行复杂工作流的智能体”。它的核心价值不在于完全替代程序员而在于接管那些定义清晰、重复性高、但执行起来繁琐的编码子任务让我们能更专注于真正需要创造力和系统思维的部分。搭建这样一个系统本身就是对自动化、可靠性和人机协作的一次深刻实践。
返回列表