首页 / 资讯中心 / 文章详情

构建→测试→修复:AI Agent自主开发闭环的设计与实践

构建→测试→修复:AI Agent自主开发闭环的设计与实践 ★ FEATURED ARTICLE
最近和一些做智能体应用的朋友聊天发现大家几乎都卡在同一个地方让 AI Agent 写代码不难难的是让它“负责任”地把代码写完。生成一段看起来很合理的函数跑起来全是错修了一个 bug又引入三个新问题你不在旁边盯着它能把同一个错误来回犯三遍。这个问题的根源在于大多数 Agent 应用只做了“生成”这一步缺少了“验证”和“修正”的闭环。说白了它只会写不会测更不会为自己的错误买单。所以我花了些时间把“构建→测试→修复→循环”这套思路完整地落地到一个 AI Agent 项目里。这篇文章把整个方案的设计思路、核心实现、踩坑记录都整理出来算是给同样在做 Agent 开发的朋友一份可复用的参考。另外很多人问我说 Agent 和大模型到底什么关系像 DeepSeek 这种又属于哪一层我也会在破题阶段一起讲清楚。1. 闭环思路与整体设计1.1 为什么是“构建→测试→修复”循环先说一个我观察到的问题。很多人在开发里用大模型其实是把 LLM 当成一个“超级补全工具”给它一个 prompt让它输出代码然后人肉把代码粘进工程里跑。这种用法不是不对只是没有榨干模型的能力而且本质上还是在用“人”做闭环里最关键的验证环节——测试和修复。但如果你把 AI Agent 定义成一个“能自主完成任务的系统”它就必须具备自我纠错的能力。而自我纠错的前提是它能够获取到“自己做错了”的信号。这个信号从哪里来只有两个来源一个是构建阶段编译、依赖解析、静态检查另一个是测试阶段单元测试、集成测试、覆盖率。没有这些信号Agent 就只能在黑暗里瞎猜。所以“构建→测试→修复”这个循环本质上是在给 Agent 装上一套“感知器官”。每次迭代它都能感知到当前的代码处于什么状态然后根据反馈调整下一次生成策略。这比单纯让模型“多想想”要可靠得多因为反馈是真实的、来自环境的不是模型自嗨出来的。这里也顺便回答一个被问烂了的问题AI Agent、LLM 和 AI 模型到底有什么区别举个例子DeepSeek 这类产品核心是一个大语言模型它做的事情是“根据输入预测输出”本身没有手也没有脚不会主动去操作什么。而 AI Agent 是在模型外面包了一层“大脑皮层”它有权调用工具、读取文件、执行命令、观察结果然后根据结果决定下一步动作。模型是决策内核Agent 是完整的行为体。我们常说的“让 Agent 自己跑完开发闭环”就是给模型配上构建工具、测试工具和修复工具让它在一个循环里自主工作。1.2 架构选型模型、编排与执行器整个系统我拆成了三个层次决策层也就是大语言模型本身。负责根据当前状态代码内容、报错信息、测试结果决定下一步动作。选型上我当时对比了几款主流模型最后选了一个对工具调用支持比较友好的通用模型DeepSeek 也可以胜任这类任务关键是上下文窗口要够大至少能把失败信息和当前文件内容同时塞进去。编排层负责维护循环状态机。核心是一个循环构建 → 检查结果 → 测试 → 检查结果 → 修复 → 回到构建。每一轮要把上一轮的结果浓缩成尽量短的摘要再交给模型决策防止上下文被无关日志撑爆。执行层真正的命令行工具和文件系统操作。我封装了一个轻量的沙箱执行环境Agent 只能在指定工作目录里读写文件、运行命令不能触碰系统其他部分。我画过一张架构图但因为这里是文字版本用文字描述一下循环过程Agent 启动 → 读取任务描述 → 规划模型生成步骤清单 → 循环开始 → 构建执行编译/安装命令 → 检查构建结果成功/失败失败则提取错误 → 测试执行测试用例 → 检查测试结果失败则提取失败断言 → 修复将问题反馈给模型生成补丁 → 应用补丁 → 如果达到最大循环次数退出否则回到“构建” → 输出最终报告这个设计里最关键的一点是不要让模型自己决定“要不要测试”。很多 Agent 框架会让模型自由选择工具但模型天生懒惰你问它“代码写好了吗”它永远说写好了。所以我把构建和测试设计成强制执行的步骤模型跳不过去只能根据结果继续。这里需要解释一下选型的取舍。有人可能会问直接用现成的 Agent 框架不行吗市面上的框架很多但它们解决的问题偏“通用任务自动化”对于“代码开发”这种对反馈质量要求极高的场景反而需要你自定义很多细节失败日志怎么截断、补丁怎么应用、回归测试怎么触发。自己写一个轻量编排代码量不大可控性强太多。我也对比过要不要引入 Jenkins 这类 CI/CD 工具。我的结论是如果你已经有一套 Jenkins 流水线完全可以把 Agent 嵌进去当一个“智能修复节点”但如果是为了这个项目单独搭一套 CI那就太重了。Agent 开发更需要的是一套轻量级的“反馈回路”而不是一套完整的企业级发布流水线。2. 构建环节让 Agent 真正“写”出可运行工程2.1 工具层设计Agent 的“手”和“眼”很多 Agent 项目失败不是模型不行而是“手”和“眼”不行。所谓“手”就是它操作环境的能力所谓“眼”就是它观察结果的能力。我封装了四个基础工具全部以命令行接口的形式暴露给模型write_file(path, content)写文件。注意这里不是“追加”而是“覆盖”。并且规定 Agent 每次写代码前必须先把原文件读一遍防止直接覆盖掉之前的正确逻辑。read_file(path)读文件。这个工具也承担了“查看当前代码状态”的功能。run_command(cmd)执行任意 shell 命令。比如pip install、pytest、python -m compileall。list_files()列出工作目录结构。帮助 Agent 理解工程全貌不要在错误的位置创建文件。这四个工具看着简单但我在设计时加了很多限制。比如run_command有超时时间默认 30 秒超过直接杀掉进程并返回“命令执行超时”write_file会检查文件路径防止 Agent 写出工作目录之外安全考虑所有命令输出都截断到最近 2000 个字符因为大模型的上下文窗口再大也经不起让 Agent 看一整篇构建日志。我见过不少 Agent 项目在工具层上偷懒直接用“终端模拟器”让 Agent 随便敲。这样其实很危险一方面是安全问题另一方面是输出量太大模型根本分不清哪些信息重要。工具的设计原则应该是“让模型看到的信息恰好是它做决策需要的信息不多不少”。2.2 构建执行与日志采集中间态构建动作本身不复杂真正的难点在于日志的“中间态处理”。原始日志长什么样用过构建工具的人都知道几百行输出里真正有用的错误信息往往就几行。如果原样把日志丢给模型它很容易被无关信息干扰甚至胡乱修错地方。我的做法是加了一个“日志提炼层”专门负责从原始输出里抽出关键信息。具体规则有三条匹配错误关键字比如Error、Traceback、ImportError、SyntaxError、FAILED把匹配到的那一行和上下文 3 行摘出来折叠重复信息构建日志里经常有大量重复的警告比如deprecated提示全部折叠成一行路径归一化把绝对路径替换成相对路径比如/home/user/project/src/main.py变成src/main.py减少 token 占用。这个提炼层的效果立竿见影。最初我直接拿完整日志给模型让它修复一个 Python 语法错误它绕了四轮才定位到问题加了提炼层之后基本上一轮就能精准修复。这让我意识到一个非常重要的原则Agent 的能力上限不取决于模型有多聪明而取决于反馈信号有多清晰。构建阶段还有一个容易被忽视的点依赖安装。Agent 写完代码要跑测试第一件事往往是pip install。如果每次循环都重新装一遍依赖浪费大量时间不说还容易引入环境漂移。我的方案是构建脚本里加了一个“依赖缓存”机制只有当requirements.txt或pyproject.toml发生变化时才重新安装依赖否则直接复用之前的虚拟环境。这样一轮循环的构建时间从 2 分钟降到了 10 秒以内迭代速度提升非常明显。有一点想特别提醒别迷信“标准工程结构”。Agent 生成代码时特别喜欢创建一大堆毫无必要的抽象类、接口和目录。后来我在构建步骤里加了一条硬性规则除非任务描述中明确要求否则所有代码放在同一个目录、文件数量不超过3个。这听起来很粗暴但对于小规模开发任务比如写一个数据处理脚本、修一个 bug这种约束能显著降低 Agent 的混乱程度。3. 测试环节构建自动化的核心关卡3.1 测试用例的生成从“能跑”到“能测”一个残酷的事实是Agent 写的代码很多时候“能跑”但“不能测”。什么叫不能测就是函数没有返回值、副作用分散在多个全局变量里、或者接口设计得不可调用。所以我要求 Agent 写完业务代码之后必须紧接着写一组测试用例。测试用例的生成我用了两层策略第一层是“基于任务描述的测试生成”。模型在写业务代码之前先根据任务描述提取验收标准。比如任务说“实现一个函数输入 URL 返回网页标题”那么测试用例就应该覆盖正常 URL、404 URL、超时 URL、空字符串输入等边界条件。这一步跑在代码编写之前相当于“先定义标准再动手实现”。第二层是“基于代码结构的测试生成”。业务代码写完以后模型会读一遍代码根据实际实现的函数、参数、返回类型补充测试用例。这一层主要是防止第一层漏掉的边界情况。两层测试合在一起可能产生大量用例。但我不建议直接把所有用例都交给pytest跑因为有些用例可能本身就有问题比如预期值和实际功能定义不一致。我的做法是把生成的用例分成“核心场景”和“边界场景”两档第一轮循环只跑核心场景等核心场景全部通过后再引入边界场景做加固。这里用 Python 生态举个例子一个最小可用的 Agent 测试工具封装核心就是一个函数运行 pytest捕获结果把失败信息提炼成结构化摘要。import subprocess import re def run_tests(test_dirtests, timeout60): 运行指定目录下的 pytest 测试返回结构化结果摘要。 try: result subprocess.run( [pytest, test_dir, --tbshort, -q], capture_outputTrue, textTrue, timeouttimeout ) except subprocess.TimeoutExpired: return {status: timeout, summary: 测试执行超时} stdout result.stdout \n result.stderr # 提炼失败信息只保留 FAILED 行和错误断言附近的内容 failed_lines [] for line in stdout.splitlines(): if re.search(rFAILED|ERROR|AssertionError|Error, line): failed_lines.append(line.strip()) passed passed in stdout return { status: passed if passed and result.returncode 0 else failed, summary: \n.join(failed_lines[:30]) or stdout[-500:] }这段代码看着简单但有三个细节值得说。一是--tbshort参数它让 pytest 输出的 traceback 更简短避免模型被冗长的调用栈干扰二是-q安静模式去掉测试名列表只保留最后的汇总三是最关键的——summary字段只保留了 30 行失败信息如果失败信息太长就截断。原因前面说过上下文的每一寸土地都很宝贵。3.2 测试执行与失败信息提炼测试执行本身不复杂复杂的是“怎么让失败信息变成模型能直接消费的决策依据”。我最初做的时候直接把 pytest 的输出原样喂给模型。结果模型经常被中间的大段代码片段带偏跑去修复那些其实没问题的代码。后来我总结了失败信息提炼的三条原则必须包含“哪一个测试用例失败”如果模型不知道是test_add_normal失败还是test_add_empty_string失败它就不知道要修哪里。必须包含“期望值 vs 实际值”这是定位问题最快的线索。比如assert add(1,2) 3失败实际返回5这说明问题很可能出在参数拼接而不是逻辑分支。必须包含相关代码的局部上下文至少要给出失败函数的前几行实现让模型有足够的参考信息。基于这三条原则我把测试结果做成了一种半结构化的中间表示大概是这样的格式测试套件: test_calculator.py 用例: test_add_normal 结果: FAILED 期望: 3 实际: 5 相关代码第12-18行: def add(a, b): if isinstance(a, int) and isinstance(b, int): return a b 1 # 可疑这里多了一个 1 return str(a) str(b)这个格式的优点是模型一眼就能看到问题可能出在哪不需要去全文搜索。我在实际运行中统计过使用这种结构化摘要之后修复效率提升了至少 3 倍。另外一个容易被忽略的细节是测试用例本身也会写错。模型生成的测试用例有些过于苛刻比如期望一个处理“异常情况”的函数抛错但实现时吞掉了异常。这时候如果只修业务代码不改用例就会陷入“改来改去都过不了测试”的死循环。我的做法是如果同一个测试用例连续 3 轮循环都没通过就把“修改测试用例”也作为一个选项交给模型——但必须在最终报告里明确标注“测试用例被修改过”方便人工审查。4. 修复环节让 Agent 自主定位和打补丁4.1 失败信息的结构化回灌修复环节是整个闭环里最依赖“信息质量”的一环。构建失败和测试失败的本质不同构建失败通常是确定性的错误信息往往直接指出位置和类型测试失败则是语义性的光是“测试没通过”这句话模型根本不知道是业务逻辑错了、边界条件没处理好、还是测试用例本身的预期就不合理。所以我设计了一套“失败信息回灌模板”每次循环都强制把信息组织成固定结构喂给模型当前循环次数: 2/5 上一轮操作: 修改了 src/parser.py 的第34行 当前问题: - 构建状态: 成功 - 测试状态: 失败3个用例通过1个失败 - 失败用例: test_parse_invalid_input 期望: 抛出 ValueError 实际: 返回 None 相关代码: def parse(input_str): if not input_str: return None ... 修复建议: 无让模型自行分析这个模板的关键是“修复建议”这个字段默认为空。为什么因为一旦提示词里带了倾向性建议模型就会偷懒直接按照建议去改而不是去分析根因。只有当模型自己分析不出来、连续两次给出同样的错误修复时我才会在“修复建议”字段里注入一个提示比如“检查一下空字符串的分支处理”。这个小技巧显著提升了模型的深层推理能力。还有一点我觉得值得展开修复的时候别让模型“重写整个文件”。我最早是允许模型直接覆盖文件内容的结果它经常把原本正确的部分也改出问题来。后来我增加了约束如果模型判断问题只存在于某一处逻辑它应该用“补丁方式”修改——也就是指出要改的文件、原始片段、替换片段。这样既方便审查也大大降低了“改 A 坏 B”的风险。这里给出一个补丁应用的简化实现稍微封装了一下apply_patch的能力def apply_patch(file_path, original_snippet, new_snippet): 将文件中的 original_snippet 替换为 new_snippet。 替换前做严格校验确保原始片段存在且唯一。 with open(file_path, r) as f: content f.read() count content.count(original_snippet) if count 0: return {status: error, message: 原始片段不存在可能文件已被修改} if count 1: return {status: error, message: f原始片段存在 {count} 处无法确定替换位置} content content.replace(original_snippet, new_snippet) with open(file_path, w) as f: f.write(content) return {status: success}这个只支持“精确片段替换”的实现反而比让模型直接重写整个文件更好用。因为片段替换的失败模式很清晰——如果原始片段不存在说明 Agent 对文件的理解和实际状态不一致那就不再继续而是强制它重新read_file再看一遍。很多情况下问题就解决了模型重新读文件后发现自己记错了函数名或位置第二次修补就成功了。4.2 补丁生成、应用与回归验证修复动作完成之后必须立刻回到构建和测试环节。这一步最怕的就是“伪修复”Agent 把测试用例改了让旧的错误不再触发但实际业务代码还是错的。为了防这种情况我在回归验证环节加了两个保险。第一个保险构建和测试永远使用“唯一真实命令”不允许 Agent 自定义。比如测试命令固定是pytest tests/ -qAgent 只能运行这个命令不能换成pytest tests/test_specific.py -q。因为一旦允许 Agent 选择性地跑测试它就可能只跑自己改过的那个用例忽略了其他用例可能因为它的改动而挂掉。第二个保险在最终报告生成之前做一次“全量回归”。不管中间过程怎么修最后一轮循环必须跑全量测试并且记录当前的通过率。这个通过率会和第一轮的基线通过率对比如果第一轮是 60%最后一轮是 85%说明 Agent 是净收益如果第一轮是 60%最后一轮是 60%但中间某轮到了 90%——这说明 Agent 中途改出来的某些好东西在后期被它自己又改坏了。这个对比视角对人工审查很有价值。修复环节里我还踩过一个很典型的坑Agent 为了通过 lint 检查会过度“美化”代码结构比如把好好的函数拆成多个类、加一堆 docstring、引入不必要的设计模式。后来我加了一条硬规则所有非功能性的修改默认禁止。如果模型认为需要重构必须在报告里单独说明“这次重构解决了什么问题”否则恢复到重构前的版本。这条规则让很多无谓的代码变动大幅减少工程保持干净了非常多。5. 循环与收敛给 Agent 装上刹车和仪表盘5.1 循环退出条件与预算控制没有任何约束的循环最后一定会变成“死循环 token 燃烧殆尽”。我踩过最惨的一次让 Agent 修一个简单的 JSON 解析 bug结果它进入了一个“改测试用例 → 测试通过 → 改回业务代码 → 测试失败 → 再改测试用例”的循环连续跑了二十几轮钱烧了不少代码还越改越乱。所以循环退出条件必须是一个硬状态机成功退出所有测试通过 构建成功 最终全量回归通过。满足这个条件立即跳出循环生成报告。不需要追求“完美”只要满足验收标准就算完成。预算退出达到最大循环轮次比如 5 轮或 token 消耗上限比如 100 万 token时强制退出标记为“未完成”带着当前进展输出阶段性报告。停滞退出连续两轮循环测试通过率没有任何提升。这种情况说明 Agent 当前策略陷入僵局继续跑下去大概率还是在原地打转。退出后可以更换模型或调整提示词策略重新启动。预算控制这里有个很实用的指标叫“每轮修复的边际收益”。具体算法是本轮测试通过率减去上一轮测试通过率。如果两轮的边际收益都是零第三轮直接触发停滞退出。我加了这个逻辑之后整个系统的平均运行轮次从 7.2 轮降到了 3.8 轮成本直接砍了一半。5.2 记忆与上下文管理循环次数多了上下文管理就成了大头。模型每看一次文件、每读一次日志都要消耗 token。如果不做控制到第三轮的时候上下文里塞的全是旧信息模型反而容易糊涂。我的方案是给整个循环加了一个“轻量级记忆”机制。每一轮结束后系统自动生成一个结构化的“进度记忆”Round 3 记忆摘要: - 已完成: 编写核心逻辑 parser.py测试用例 test_parser.py 已生成 - 当前失败: test_parse_invalid_input 期望抛错但返回 None - 已尝试方案: 方案A(直接 raise ValueError) 失败原因(测试用例接口不匹配) - 待尝试方向: 修改 parse 函数对空字符串的预处理分支这个记忆只保留最近 3 轮的内容更早的信息直接丢弃或者压缩。模型每一轮只能看到“当前文件状态 最新失败信息 最近 3 轮记忆”不会看到第 1 轮的完整上下文。这个设计类似于人类的“工作记忆”机制你不会记得前几天犯错的每一个细节但你记得自己尝试过哪几个方案、效果如何。当然“长期记忆”也应该有但不是塞进上下文里而是沉淀成报告里的“开发记录”。等循环结束之后如果你想让 Agent 继续处理下一个相似任务可以把上一轮的总结作为先验知识注入提示词。这个思路在我后面的任务里效果很好Agent 遇到相似问题时会直接跳过踩坑阶段。6. 踩坑实录与常见问题速查6.1 实操中遇到的四个典型问题第一模型幻觉导致“越修越坏”。最常见的是模型在补丁里引用了一个不存在的函数或者记错了标准库的 API 签名。这个问题没有完美的解法只能靠“小步修改 频繁验证”来缓解。我的经验是把修复动作拆得尽可能小一次只改一个点验证完再改下一个点能从根上减少幻觉的破坏范围。第二测试期望写得太死。模型生成的测试用例有时候会“背答案”比如期望一个函数返回[1, 2, 3]完全不给变通空间。一旦业务逻辑稍微复杂一点这种测试用例反而成了伪需求。解法是要求模型在写测试用例时优先使用“属性测试”比如检查返回值的类型、长度、值域而不是精确匹配具体值。第三上下文被日志撑爆。我在前文说过很多次了——日志提炼绝对是一项值得投入的工程。后来我还做了一个更狠的优化根据当前失败类型只保留对应类型的日志片段。比如本轮是测试失败就只保留 pytest 的执行结果构建日志直接丢弃本轮是构建失败就只保留编译器的报错区域测试日志不进入上下文。这个“按需加载”策略让上下文占用下降了 60%。第四环境状态污染。我在连续跑多轮循环时发现前一轮的临时文件、环境变量、缓存目录经常会影响下一轮的运行结果。后来我在整个循环开始前强制git clean清掉未被跟踪的文件、每一次循环结束后都记录当前 Git 提交哈希如果发现代码被外部因素改动能立刻回滚到已知状态。这让整个系统具备了一个非常宝贵的性质可以重复复现。开发调试的时候这是救命稻草。6.2 常见问题速查表现象可能原因解决方法Agent 反复修改同一个文件但不解决缺少失败信息的结构化反馈检查日志提炼层确认模型能看懂具体错误测试通过率没变但代码面目全非模型在做无效重构禁止功能性修改以外的任何重构循环提前退出且无进展模型没有足够信息决策放宽 token 预算或补充项目结构信息测试用例也被越改越弱模型“作弊”绕过失败增加测试代码审计不允许绕过核心断言上下文占用越来越大历史信息未压缩使用“进度记忆”替代完整历史日志构建时依赖反复安装虚拟环境被反复重建引入依赖缓存仅在依赖文件变化时重装这个速查表是我在大量实验里整理出来的基本覆盖了常见故障的 80%。如果你在自己搭建的过程中遇到问题建议先对着表排查一轮。写在最后的经验总结整个项目跑下来我最大的感受是让 AI Agent 自主完成开发闭环瓶颈根本不在模型智商而在系统设计。模型再聪明如果你没有给它清晰的反馈信号、没有限制它的行动空间、没有控制它的迭代方向它一样会把事情做砸。反过来只要你把反馈做得足够精准、动作约束得足够合理哪怕是开源的小模型也能在闭环里完成相当复杂的开发任务。如果你是刚开始做类似项目我的建议是不要急着上复杂框架。先用一个最简单的循环写起来让你选择的模型写一段代码然后强制跑一遍测试把失败信息整理好喂回去循环几轮。等这个最小闭环跑通了再逐步加工具、加记忆、加并行任务。这个循序渐进的过程比直接套用重框架要踏实得多。另外还有一个很实用的小技巧分享给大家在整个循环跑完之后让 Agent 生成一份“开发复盘”内容包括当前代码的可运行状态、与验收标准的差距、本轮尝试过的思路、以及给下一个人或下一次 Agent 运行的建议。这份文档的价值不亚于代码本身——它就是下一次循环的“初始记忆”能让新的 Agent 站在上一轮的肩膀上继续干活而不是从零开始。
阅读完成 · 觉得有帮助?
咨询建站