ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于LLM与多智能体架构的自动化测试修复系统设计与实践

2026/8/22 11:32:33 拓冰建站 浏览量
基于LLM与多智能体架构的自动化测试修复系统设计与实践 1. 项目概述当测试修复走向“自动驾驶”最近在跟几个做测试平台和DevOps的朋友聊天大家普遍有个痛点随着微服务和敏捷开发的普及测试用例的数量和复杂度呈指数级增长。一个功能改动可能引发几十个甚至上百个测试用例的失败。传统的修复流程——开发人员定位、分析、修改、验证——耗时耗力尤其是在回归测试阶段大量精力被消耗在重复、机械的失败测试分析上。这让我开始思考一个更激进的问题测试修复这件事能不能实现一定程度的“自动驾驶”不是简单的断言失败提示而是让系统自己发现问题、分析根因、生成修复方案甚至自动验证。听起来像是天方夜谭但大语言模型LLM和多智能体Multi-Agent架构的兴起让这个想法有了落地的可能。我们团队最近就基于这个思路做了一次深入的探索性实践项目代号就叫“自动驾驶测试修复”。这个项目的核心目标不是要打造一个能解决所有问题的“银弹”而是想摸清楚在当前技术条件下自动化测试修复的“实用边界”在哪里。我们想知道LLM驱动的智能体Agent在多大程度上能独立完成从“发现失败”到“自我纠正”的闭环以及在哪些环节上人类的介入仍然是不可或缺的。我们选择使用 LangGraph 作为多智能体编排框架因为它对状态流转和循环控制的支持非常契合我们设想的“发现-诊断-修复-验证”的迭代流程。如果你也在为海量测试失败而头疼或者对LLM和智能体如何落地到具体工程场景感到好奇那么这次实践的经验和教训或许能给你带来一些启发。接下来我会详细拆解我们的设计思路、实现细节、踩过的坑以及最重要的——我们发现的那些“自动驾驶”的极限。2. 核心架构设计多智能体协同的“手术团队”要实现测试的自主修复我们首先得把修复过程拆解成一系列标准化的、可被AI理解的任务。这不像写一个万能脚本而更像组建一个各司其职的“手术团队”。每个智能体Agent就是一个专家负责一个特定环节它们通过一个清晰的协作流程Orchestration来共同完成“手术”。2.1 为什么选择多智能体与LangGraph最初我们考虑过用单个超级LLM来搞定一切但很快发现这行不通。测试修复涉及多个差异巨大的子任务需要理解代码语义、分析测试日志、推理失败原因、生成补丁代码、评估修复风险等。让一个模型在单次对话中兼顾所有这些很容易导致指令混淆、上下文过长和思维链断裂。多智能体架构的优势就凸显出来了职责分离每个智能体可以针对特定任务进行优化使用最合适的提示词Prompt和工具Tools。状态管理修复过程是有状态的。例如诊断结果会影响修复策略的选择。多智能体架构天然适合管理这种复杂的状态流转。容错与回溯当某个环节如生成的修复代码编译失败出错时系统可以回溯到上一步由另一个智能体重新诊断或尝试不同策略而不是整体失败。在框架选型上我们评估了 LangChain 和 LangGraph。LangChain 的链Chain式调用对于线性流程很友好但我们的修复流程充满了条件分支和循环比如“修复-验证-再修复”。LangGraph的图Graph概念和它对循环、状态State的显式支持更符合我们的需求。它允许我们直观地定义智能体之间的工作流并精确控制每个决策点。2.2 智能体角色定义与协作流程我们设计了四个核心智能体它们共同构成了修复流水线侦察兵Scout Agent职责监控测试执行结果精准捕获失败用例。它不只是看“通过/失败”状态还要收集丰富的上下文信息为后续分析提供“现场证据”。输入原始测试报告如JUnit XML, pytest输出。输出结构化的失败用例信息包括测试方法名、所属类、错误类型AssertionError, NullPointerException等、完整的错误堆栈跟踪Stack Trace、失败时的控制台日志片段、以及关联的源代码文件路径。工具主要使用文本解析和模式匹配工具。LLM在这里的作用是理解非标准化的日志格式准确提取关键信息。诊断专家Diagnostician Agent职责这是整个系统的“大脑”。它需要根据侦察兵提供的信息结合项目源代码推理出测试失败的根本原因。输入侦察兵输出的结构化失败信息 相关的源代码文件内容。输出一份诊断报告包括最可能的根本原因如某个函数的返回值逻辑错误、边界条件未处理、依赖的外部服务Mock数据过期、受影响的代码范围需要修改的具体函数或行号、修复策略建议如修正条件判断、补充空值检查、更新测试数据。工具这是LLM能力核心体现的地方。我们为它提供了代码检索工具能根据堆栈跟踪定位到具体代码块、项目知识库查询工具检索类似的修复历史或API文档。它的提示词工程最复杂需要引导模型进行逐步推理Chain-of-Thought。外科医生Surgeon Agent职责根据诊断报告生成具体的代码修复补丁Patch。这是将分析转化为行动的一步。输入诊断专家输出的诊断报告。输出符合项目代码风格的代码差异Diff通常以git diff的格式呈现。它只生成修改建议不直接写入文件。工具代码生成和格式化工具。提示词会强调遵循项目的编码规范、不要引入不相关的改动、以及优先采用最小化修改原则。审核员Auditor Agent职责对“外科医生”生成的补丁进行安全和有效性审核。评估补丁本身的质量并执行快速验证。输入诊断报告 生成的代码Diff。输出审核结果通过/需修改/高风险及理由。如果通过它会尝试在隔离环境中应用补丁并重新运行失败的测试用例及相关联的测试。工具代码静态分析工具如检查语法、简单的代码风格、安全扫描工具基础规则、以及一个轻量级的沙盒测试运行环境。这四个智能体在 LangGraph 中被组织成一个有状态的工作流。状态State是一个共享的数据结构包含了当前失败用例的信息、诊断结果、生成的补丁、审核结果等。工作流的基本路径是Scout - Diagnostician - Surgeon - Auditor。如果审核员发现补丁有问题如测试仍未通过工作流会根据错误类型选择回溯到诊断专家重新分析或外科医生重新生成补丁形成一个循环直到成功或达到重试上限。注意这个架构是“中心化编排”的由LangGraph图控制流程智能体之间不直接通信。这简化了状态管理和错误处理但也意味着编排器的逻辑需要设计得非常健壮。3. 实现细节与核心环节拆解有了架构蓝图接下来就是如何让每个智能体真正“聪明”地工作。这里充满了细节挑战也是LLM应用从演示走向生产的关键。3.1 状态State设计工作流的记忆中枢在LangGraph中State是所有智能体共享和操作的上下文。设计一个好的State结构至关重要。我们的State定义大致如下以Pydantic模型为例from typing import List, Optional, Any from pydantic import BaseModel class TestFailure(BaseModel): test_name: str class_name: str error_type: str stack_trace: str log_snippet: str source_file_path: str class Diagnosis(BaseModel): root_cause: str affected_code_blocks: List[dict] # 包含文件路径和行号范围 repair_strategy: str confidence: float # 诊断置信度0-1之间 class CodePatch(BaseModel): diff: str original_file_content: str patched_file_content: str class RepairState(BaseModel): # 输入 raw_test_report: str # 各环节产出 identified_failure: Optional[TestFailure] None diagnosis: Optional[Diagnosis] None proposed_patch: Optional[CodePatch] None # 流程控制 audit_result: Optional[str] None # “PASSED”, “NEEDS_REVISION”, “RISKY” audit_feedback: Optional[str] None # 审核员的反馈意见 retry_count: int 0 max_retries: int 3 current_agent: str “scout” # 用于调试跟踪设计要点强类型化使用Pydantic确保数据类型安全方便智能体理解和使用。可追溯性保留了从原始报告到最终补丁的所有中间产物便于调试和复盘。流程控制字段audit_result,retry_count等字段直接决定了LangGraph图中边的走向即下一步该执行哪个节点。3.2 提示词Prompt工程与智能体有效沟通提示词是驱动智能体的“咒语”。我们的原则是清晰、具体、结构化、提供范例。以诊断专家Diagnostician为例其系统提示词System Prompt核心部分如下你是一个资深的软件测试诊断专家。你的任务是分析失败的测试用例并结合源代码找出最可能的根本原因。 请严格遵循以下步骤进行推理 1. 信息提取仔细阅读提供的测试失败信息堆栈跟踪、错误信息、日志。 2. 代码定位根据堆栈跟踪找到项目中疑似出错的源代码位置。我将为你提供相关文件的代码内容。 3. 根因假设基于代码逻辑和错误信息提出一个最合理的失败原因假设。常见原因包括逻辑错误、边界条件缺失、数据状态问题、依赖项不匹配、并发问题等。 4. 影响分析指出需要修改的代码范围。尽量精确到函数或代码块。 5. 策略建议提出一种具体的代码修复策略。 输出格式必须是严格的JSON { “root_cause”: “一段简洁的文字描述说明你认为的根本原因”, “affected_code_blocks”: [ {“file_path”: “src/main/java/.../Service.java”, “start_line”: 45, “end_line”: 52, “description”: “calculatePrice 函数内的折扣计算逻辑”} ], “repair_strategy”: “具体的修复策略例如‘修改第48行的条件判断将 改为 ’”, “confidence”: 0.85 } 以下是两个参考案例 [案例1和案例2的具体内容...] 现在开始分析当前的测试失败。关键技巧角色设定强化其专家身份。思维链Chain-of-Thought引导用“请遵循以下步骤”明确要求模型展示推理过程。虽然最终输出是JSON但我们在开发调试阶段会要求模型输出完整的思考过程。结构化输出强制要求JSON格式便于程序化解析。这是实现智能体间自动化协作的基础。少样本Few-Shot学习提供1-2个高质量、贴近项目实际的诊断案例能极大提升模型在特定领域的表现。置信度要求模型输出对自己的判断的置信度。当置信度低于某个阈值如0.6时工作流可以提前转入人工审核分支。3.3 工具Tools赋能给智能体“手脚”智能体不能只靠“想”还得靠“做”。我们为每个智能体配备了必要的工具。侦察兵RegexParserTool,LogStructureExtractorTool。这些工具封装了正则表达式和启发式算法用于从杂乱的日志中提取测试名、错误类型等。诊断专家CodeSearchTool。这个工具能接收文件路径和行号或符号名从代码仓库中获取具体的代码片段。它背后可能是ripgrep或基于LSIF的代码索引。外科医生CodeFormatterTool。在生成Diff后调用此工具确保代码风格符合项目要求如blackfor Python,prettierfor JS。审核员SandboxTestRunnerTool。这是最复杂的工具。它需要1) 在一个干净的临时目录中克隆当前代码库2) 应用生成的Diff3) 运行失败的测试用例4) 捕获结果。我们使用Docker容器来提供隔离、一致的运行时环境。工具开发心得工具应保持简单和专注一个工具只做好一件事。例如SandboxTestRunnerTool只负责运行测试并返回通过与否不负责分析原因。错误处理要健壮工具执行失败如编译错误时必须返回清晰的错误信息供智能体或工作流判断下一步动作。考虑性能特别是CodeSearchTool和SandboxTestRunnerTool可能涉及I/O和进程启动。需要加入缓存机制和超时控制。4. 工作流编排与循环控制LangGraph实战这是将各个智能体串联成自动化流程的核心。我们使用LangGraph来定义这个有状态的工作流。4.1 图Graph定义与节点Node每个智能体被实现为一个LangGraph的节点函数。这个函数接收当前的RepairState调用相应的LLM和工具更新State并返回更新后的State。import operator from langgraph.graph import StateGraph, END # 假设我们已经有了智能体的具体实现函数scout_node, diagnostician_node, surgeon_node, auditor_node workflow StateGraph(RepairState) # 添加节点 workflow.add_node(“scout”, scout_node) workflow.add_node(“diagnostician”, diagnostician_node) workflow.add_node(“surgeon”, surgeon_node) workflow.add_node(“auditor”, auditor_node) # 设置入口点 workflow.set_entry_point(“scout”)4.2 边Edge与条件路由节点之间的流转不是固定的而是根据State中的条件决定的。我们通过add_conditional_edges来实现。def route_after_audit(state: RepairState) - str: “”“根据审核结果决定下一步”“” if state.audit_result “PASSED”: return “END” # 修复成功结束 elif state.audit_result “NEEDS_REVISION” and state.retry_count state.max_retries: # 需要修订且重试次数未超限 state.retry_count 1 # 可以根据audit_feedback决定是回溯到诊断还是直接重新生成补丁 if “diagnosis” in state.audit_feedback.lower(): return “diagnostician” else: return “surgeon” else: # 高风险或重试超限转入人工处理 return “human_intervention” # 定义边 workflow.add_edge(“scout”, “diagnostician”) workflow.add_edge(“diagnostician”, “surgeon”) workflow.add_edge(“surgeon”, “auditor”) workflow.add_conditional_edges( “auditor”, route_after_audit, # 条件判断函数 { “END”: END, “diagnostician”: “diagnostician”, “surgeon”: “surgeon”, “human_intervention”: “human_intervention_node” # 另一个处理人工节点的节点 } )循环控制逻辑 这个设计实现了“修复-验证”循环。如果审核不通过系统不会直接失败而是带着反馈信息audit_feedback例如“生成的补丁引入了语法错误”或“测试仍然失败原因可能是XXX”回到上游节点。retry_count防止了无限循环。4.3 人工干预节点这是理解“实用边界”的关键。我们必须设定明确的规则让系统知道何时应该“举手求助”。在我们的工作流中以下情况会路由到human_intervention_node诊断专家的置信度持续低于阈值。审核员标记补丁为“高风险”例如修改了核心逻辑文件且影响范围很大。循环重试次数超过max_retries我们设为3。工具执行过程中出现不可预料的系统错误。人工节点会将当前所有的State信息失败详情、诊断、补丁、历史尝试格式化后发送到团队的协作平台如Slack、钉钉群或创建一个工单等待开发人员处理。同时它也会暂停这个工作流实例。5. 实测挑战与“自动驾驶”的极限我们在一个中等规模的Java Spring Boot微服务项目约5万行代码1200个单元/集成测试上进行了为期两周的实测让系统处理了历史积累的87个随机筛选的测试失败案例。结果很有启发性成功案例约40%系统能够完全自主修复。这些案例通常是简单的空指针异常NPE诊断专家能准确定位到未做空值检查的变量外科医生能生成添加if(obj ! null)的补丁。明显的断言值错误例如测试期望值是100实际输出是90。诊断专家能推断出是某个计算系数错误外科医生能修正它。过时的静态测试数据如硬编码的日期过期了。诊断专家能识别出日期比较逻辑外科医生能更新为一个未来的日期。部分成功/需人工微调约35%系统提出了正确的修复方向但补丁不完美。诊断正确补丁啰嗦外科医生生成的代码功能正确但不符合项目简洁的代码风格需要人工调整格式。修复策略正确实现有瑕疵例如知道要加一个异常处理块try-catch但捕获的异常类型不精确或日志语句不恰当。需要更广泛的上下文修复一个函数却影响了其他调用该函数的地方。系统缺乏全局影响分析能力。失败案例约25%系统无法处理或给出完全错误的建议。涉及复杂业务逻辑的深层Bug失败原因隐藏在多层业务交互和状态流转中。LLM基于代码片段和堆栈跟踪的“快照式”分析难以理解这种动态上下文。并发或时序问题这类问题在单次测试运行中可能无法稳定复现诊断专家缺乏足够信息。对外部依赖的误解测试失败是因为Mock的对象行为设置不正确而系统试图去修改真实的生产代码逻辑。重构或设计问题测试失败暗示了更深层的设计缺陷如类职责不清而系统只能提出表面化的修补方案。5.1 我们遇到的典型问题与排查技巧LLM的“幻觉”与自信矛盾问题诊断专家有时会以高置信度0.9给出一个看似合理但完全错误的根因分析例如将网络超时错误解释为某个算法逻辑错误。排查我们引入了“交叉验证”机制。让诊断专家不仅输出一个主要假设还输出1-2个备选假设置信度较低。在后续流程中如果主要假设生成的补丁被审核员多次驳回工作流会尝试使用备选假设重新开始。这增加了系统的鲁棒性。工具调用成本与延迟问题SandboxTestRunnerTool启动Docker、拉代码、构建、运行测试非常耗时一次调用可能需要30秒到2分钟。在循环中多次调用会导致整个修复流程长达十分钟。优化我们实现了两级缓存。第一级如果生成的Diff与之前某次尝试完全相同则直接使用之前的测试结果。第二级在沙盒中对项目依赖进行预缓存避免每次构建都重新下载所有JAR包。这平均减少了40%的等待时间。状态管理的复杂性问题当工作流因为各种原因如LLM API超时、工具异常中途失败时State可能处于不一致的状态。重试或调试困难。解决我们为State实现了序列化/反序列化方法并将每个重要步骤后的State快照持久化到数据库中。这让我们可以随时恢复某个工作流实例也方便进行事后分析和模型训练数据收集。提示词的脆弱性问题稍微调整测试用例的描述方式或错误信息格式有时会导致LLM的输出格式偏离我们设定的JSON结构造成解析失败。加固除了在提示词中强调输出格式我们在解析LLM响应时增加了“后处理”层。如果JSON解析失败会尝试用一个轻量级的LLM调用如gpt-3.5-turbo去修复这个JSON串或者从中提取关键信息。这是一种防御性编程思维。6. 经验总结与未来展望这次“自动驾驶测试修复”的探索让我们对LLM和多智能体在软件工程自动化中的能力与局限有了更切实的体会。核心价值与适用场景效率提升对于那40%的简单、模式化的测试失败系统可以秒级响应并修复将开发者从繁琐的“体力活”中解放出来。永不疲倦的初级助手它可以7x24小时处理CI/CD流水线中涌现的测试失败进行第一轮筛选和修复只将真正复杂的问题提交给人。这相当于为团队配备了一个不知疲倦的初级开发工程师。知识沉淀与标准化成功的修复案例可以被记录下来形成项目的“修复知识库”用于优化提示词或训练更专有的模型。当前不可逾越的“实用边界”对复杂性和上下文的无力LLM本质上是基于模式的统计模型它擅长处理局部、有大量类似示例的问题。对于需要深度理解系统架构、业务领域知识和长链条因果推理的复杂缺陷它目前还无法替代人类的系统化思维。创造性与设计能力缺失系统只能基于现有代码进行“修补”。如果问题的根源是糟糕的设计它无法提出重构方案。它是在“编辑”代码而不是“设计”代码。责任与安全边界完全自主地将代码修改应用到主分支是危险的。我们的系统始终将“审核员”和“人工干预”作为必须的关卡。生成的补丁必须经过至少一次自动化测试验证并且对于核心模块的修改必须强制要求人工审核。经济成本考量频繁调用高性能LLM如GPT-4和运行沙盒环境会产生可观的成本。这需要与它节省的人力时间成本进行权衡。目前更适合在失败率高、修复模式清晰的场景下选择性使用。给想要尝试的团队的建议从小处着手不要一开始就追求全自动。可以从一个特定的、高重复性的失败类型开始如空指针检查、日期更新。人机协同而非替代将系统定位为“增强工具”它的目标是扩大开发者的能力范围而不是取代开发者。设计流畅的人工接管流程至关重要。重视评估与迭代建立明确的评估指标如自主修复成功率、平均修复时间、人工干预率。持续用真实数据优化你的智能体提示词和工作流逻辑。基础设施是基石稳定的工具链代码检索、沙盒运行、可靠的状态管理和错误处理机制是智能体系统能否稳定运行的基础其重要性不亚于LLM本身。这次实践让我们看到虽然完全的“自动驾驶”尚不可及但“高级驾驶辅助系统”已经可以投入实用。它已经能够处理大量常规任务让工程师能更专注于那些真正需要创造力和深度思考的复杂问题。技术的边界正在快速移动而我们现在要做的就是找到最适合踩下油门的那个场景。