【三个月 AI Agent 实战学习】Day 51:完善迷你软件公司 —— 代码评审、迭代修复与工具共享
Day 51:完善迷你软件公司 —— 代码评审、迭代修复与工具共享
欢迎来到第五十一天!昨天我们搭建了迷你软件公司的骨架,完成了"需求 → 设计 → 编码 → 测试"的单向流程。但真实的软件开发不是一次性的——测试发现问题后,程序员需要修复 Bug,架构师需要评审代码质量,然后再次测试,直到所有验收标准通过。今天我们将为这个项目增加代码评审和迭代修复环节,形成闭环,并让 Agent 共享代码执行工具,让测试从"纸上审查"升级为"实际运行验证"。
一、今日学习目标
- 理解当前单向流程的局限性:测试发现问题后无法自动修复。
- 学会设计"编码 → 评审 → 测试 → 修复"的迭代闭环流程。
- 掌握在 CrewAI 中实现迭代逻辑:用 Python 循环包裹 Crew 执行,根据测试结果决定是否继续迭代。
- 实现代码执行工具,让测试员能够实际运行生成的代码并验证功能。
- 完成迷你软件公司的闭环版本,实现自动修复和重新测试。
二、详细实现步骤
步骤 1:分析当前流程的局限性
昨天的流程是:
需求 → 设计 → 编码 → 测试(结束)问题在于:
- 测试员发现 Bug 后,流程就结束了,Bug 没有被修复。
- 代码质量没有被评审,可能不符合架构设计。
- 没有形成"发现问题 → 修复 → 重新验证"的闭环。
改进后的流程:
需求 → 设计 → 编码 → 代码评审 → 测试 → 是否通过? │ ┌─────────┴─────────┐ ▼ ▼ 通过 不通过 │ │ ▼ ▼ 结束 程序员修复 → 重新评审 → 重新测试 (最多迭代 N 次)步骤 2:设计迭代控制策略
在 CrewAI 中,Crew 本身是一次性执行的,不支持内建循环。我们需要在 Python 层面用while循环包裹 Crew 的执行:
max_iterations=3# 最多迭代 3 次foriterationinrange(max_iterations):# 执行 Crew(编码 → 评审 → 测试)result=crew.kickoff()# 检查测试是否通过iftest_passed(result):break# 通过则退出else:# 不通过则进入下一轮修复continue关键问题:如何判断测试是否通过?
方案一:让测试员在报告中输出明确的结论(如"测试结论:通过"或"测试结论:不通过"),然后用字符串匹配判断。
方案二:让测试员输出结构化的 Pydantic 对象,包含passed: bool字段。
我们采用方案二,更可靠。
步骤 3:定义测试报告的结构化模型
classTestReport(BaseModel):passed:bool=Field(description="测试是否全部通过")total_cases:int=Field(description="测试用例总数")passed_cases:int=Field(description="通过的用例数")failed_cases:List[str]=Field(description="失败的用例列表及原因")bugs_found:List[str]=Field(description="发现的 Bug 列表")suggestions:List[str]=Field(description="改进建议")步骤 4:实现代码执行工具
为了让测试员能够实际运行代码,我们定义一个代码执行工具。这个工具接受代码文件路径,执行它,返回输出或错误信息。
importsubprocessimportsysfromlangchain_core.toolsimporttoolfromtypingimportAnnotated@tooldefexecute_python_file(file_path:Annotated[str,"要执行的 Python 文件路径,例如 output/todo.py"])->str:"""执行指定的 Python 文件,返回标准输出和标准错误。用于验证代码是否能正常运行。"""try:# 使用 subprocess 执行,设置超时防止死循环result=subprocess.run([sys.executable,file_path],capture_output=True,text=True,timeout=10,# 最多执行 10 秒cwd="."# 在当前目录执行)output=result.stdout.strip()error=result.stderr.strip()ifresult.returncode==0:returnf"执行成功。\n输出:{outputifoutputelse'(无输出)'}"else:returnf"执行失败(返回码{result.returncode})。\n错误:{error}"exceptsubprocess.TimeoutExpired:return"执行超时(超过 10 秒),可能存在死循环。"exceptExceptionase:returnf"执行异常:{e}"注意:这个工具只适合执行不会阻塞等待用户输入的程序。如果生成的代码需要input()交互,执行会卡住。我们可以在测试员的任务描述中说明"请只测试不需要交互的功能",或者让程序员生成支持命令行参数调用的代码。
步骤 5:定义代码评审员 Agent
增加一个新角色:代码评审员(可以由架构师兼任,也可以单独定义)。我们单独定义,让职责更清晰。
code_reviewer=Agent(role="代码评审专家",goal="审查程序员提交的代码,检查是否符合架构设计、代码规范和质量要求",backstory="""你是一位严格的代码评审专家,有 10 年代码审查经验。 你检查代码时关注: 1. 是否实现了架构设计中定义的所有模块和接口 2. 代码是否符合 PEP8 规范(命名、缩进、注释) 3. 是否有明显的逻辑错误或边界问题 4. 是否有安全风险(如 eval、硬编码密码) 5. 是否有充分的错误处理 你的评审意见要具体,指出问题所在行或函数,并给出修改建议。 如果代码质量合格,输出"评审通过";否则输出"评审不通过"并列出问题。""",llm=llm,verbose=True,allow_delegation=False)步骤 6:定义评审任务和修复任务
6.1 代码评审任务
task_review=Task(description="""请审查程序员生成的代码文件 output/todo.py。 审查要点: 1. 对照架构设计文档,检查是否实现了所有模块和接口 2. 检查代码规范(命名、缩进、docstring) 3. 检查逻辑错误和边界问题 4. 检查是否有安全风险 请在报告中明确输出"评审通过"或"评审不通过",如果不通过,列出具体问题。""",expected_output="代码评审报告,包含评审结论和具体问题列表",agent=code_reviewer,context=[task_coding,task_architecture],output_file="output/code_review.md")6.2 测试任务(使用代码执行工具)
task_testing=Task(description="""请测试 output/todo.py 的代码。 步骤: 1. 先使用 execute_python_file 工具运行代码,检查是否能正常启动 2. 对照需求文档的验收标准,设计测试用例 3. 对于需要交互的功能,可以检查代码逻辑是否正确 4. 输出结构化的测试报告 注意:如果代码需要 input() 交互,请只做静态逻辑审查,不要尝试执行。""",expected_output="结构化的测试报告,包含是否通过、测试用例、发现的 Bug 和改进建议",agent=tester,context=[task_coding,task_requirement],tools=[execute_python_file],# 给测试员配备代码执行工具output_pydantic=TestReport,output_file="output/test_report.md")6.3 修复任务(仅在测试不通过时执行)
修复任务不是每次都需要,而是在测试不通过时动态创建。我们不在 Crew 的固定任务列表中包含它,而是在循环中根据情况创建新的 Crew。
步骤 7:实现迭代闭环
defrun_iteration(iteration:int,previous_test_report:TestReport=None):"""执行一轮迭代:编码 → 评审 → 测试"""# 如果是修复轮次,修改编码任务的描述,加入上一轮的测试报告ifprevious_test_report:coding_description=f"""根据上一轮的测试报告,修复代码中的问题。 上一轮测试报告: - 通过的用例:{previous_test_report.passed_cases}/{previous_test_report.total_cases}- 失败的用例:{previous_test_report.failed_cases}- 发现的 Bug:{previous_test_report.bugs_found}- 改进建议:{previous_test_report.suggestions}请修改 output/todo.py,修复上述问题。输出完整的修复后代码。"""else:coding_description="""根据架构设计文档,编写完整的 Python 代码。 要求: 1. 代码必须可直接运行(`python todo.py` 即可启动) 2. 只使用 Python 标准库 3. 每个函数有 docstring 4. 代码符合 PEP8 规范 5. 将代码保存到 output/todo.py 请输出完整的代码,不要省略任何部分。"""# 动态创建任务task_coding=Task(description=coding_description,expected_output="完整的、可运行的 Python 代码文件",agent=programmer,context=[task_architecture]ifnotprevious_test_reportelse[task_architecture,task_review],output_file="output/todo.py")task_review=Task(description="请审查 output/todo.py 的代码,检查是否符合架构设计和代码规范。",expected_output="代码评审报告",agent=code_reviewer,context=[task_coding,task_architecture],output_file="output/code_review.md")task_testing=Task(description="请测试 output/todo.py,输出结构化测试报告。",expected_output="结构化的测试报告",agent=tester,context=[task_coding,task_requirement],tools=[execute_python_file],output_pydantic=TestReport)crew=Crew(agents=[programmer,code_reviewer,tester],tasks=[task_coding,task_review,task_testing],process=Process.sequential,verbose=True)result=crew.kickoff()# 从最后一个任务中获取结构化输出test_report=task_testing.output.pydanticreturntest_report# 主循环max_iterations=3test_report=Noneforiinrange(max_iterations):print(f"\n{'='*60}")print(f"第{i+1}轮迭代")print(f"{'='*60}")test_report=run_iteration(i,test_report)print(f"\n测试结果:{'通过'iftest_report.passedelse'不通过'}")print(f"通过用例:{test_report.passed_cases}/{test_report.total_cases}")iftest_report.passed:print("所有测试通过,项目完成!")breakelse:print(f"发现{len(test_report.bugs_found)}个 Bug,进入下一轮修复...")else:print(f"\n已达到最大迭代次数{max_iterations},仍有未解决的问题。")步骤 8:优化测试员的 Prompt
为了让测试员更有效地使用代码执行工具,在 System Prompt 中增加指导:
tester=Agent(role="质量保障工程师",goal="编写全面的测试用例,使用代码执行工具验证代码功能,发现潜在 Bug",backstory="""你是一位严谨的测试工程师,擅长设计边界测试和异常测试用例。 你手头有一个工具 execute_python_file,可以执行 Python 文件并返回输出。 在执行代码前,先检查代码是否有需要 input() 的地方——如果有,不要执行,只做静态审查。 对于不需要交互的功能,尽量实际运行验证。 你的测试报告必须包含明确的"通过/不通过"结论。""",llm=llm,verbose=True,allow_delegation=False)步骤 9:完整流程测试
运行完整流程,观察:
- 第一轮:程序员编码 → 评审员评审 → 测试员测试(可能发现 Bug)。
- 如果测试不通过,第二轮:程序员根据测试报告修复 → 评审 → 测试。
- 重复直到通过或达到最大迭代次数。
预期输出示例:
第 1 轮迭代 程序员:生成 todo.py ... 评审员:评审通过(或列出问题) 测试员:测试不通过,发现 2 个 Bug:删除功能未处理不存在的 ID;JSON 文件不存在时未初始化 第 2 轮迭代 程序员:修复了删除功能的边界问题,增加了文件初始化逻辑... 评审员:评审通过 测试员:测试通过,5/5 用例通过 项目完成!三、常见问题与调试
Q1:代码执行工具返回超时,因为生成的代码需要 input() 交互。
→ 在测试任务描述中明确"如果代码需要交互,不要执行,只做静态审查"。也可以让程序员生成支持命令行参数调用的代码(如python todo.py add "买牛奶"),这样执行工具可以带参数调用。
Q2:task_testing.output.pydantic报错,提示没有 pydantic 属性。
→ 确保output_pydantic=TestReport已正确设置,且 CrewAI 版本支持。如果版本较旧,可以改用output_json,然后手动解析。
Q3:迭代次数过多,Token 消耗大。
→ 设置合理的max_iterations(如 2-3 次)。在修复任务中要求程序员"一次修复所有已知问题",避免逐个小问题反复迭代。
Q4:评审员和测试员的意见冲突(评审通过但测试不通过)。
→ 这是正常的——评审关注代码质量,测试关注功能正确性。以测试结果为准决定是否继续迭代。可以在评审任务中增加"功能正确性"的检查项,减少冲突。
Q5:修复后的代码引入了新 Bug。
→ 这正是迭代闭环的价值——下一轮测试会发现新 Bug。如果连续多轮无法收敛,可以增加"回滚"机制:保留上一版代码,如果修复后更差则回滚。
Q6:代码执行工具的安全问题。
→ 我们使用subprocess执行代码,存在安全风险(如代码中写os.system("rm -rf /"))。生产环境应使用 Docker 沙箱或 RestrictedPython。当前演示环境下,可以让程序员 Agent 在 Prompt 中承诺"不包含危险操作"。
四、今日总结与作业
今天你完成了:
- ✅ 理解了单向流程的局限性,设计了迭代闭环流程。
- ✅ 增加了代码评审员角色,实现了代码质量审查。
- ✅ 实现了代码执行工具,让测试员能够实际运行代码。
- ✅ 使用 Python 循环包裹 Crew 执行,实现了"测试不通过则自动修复"的闭环。
- ✅ 定义了结构化的测试报告,便于程序判断测试是否通过。
今日作业(必做):
- 运行完整流程,测试"命令行待办事项工具"的生成。记录迭代次数、每轮测试结果和最终代码质量。
- 修改测试任务,让测试员在执行代码时传入命令行参数(如
python todo.py --help),验证程序是否支持帮助信息。提示:需要修改execute_python_file工具,增加args参数。 - 为代码评审员增加一个"安全检查"项:检查代码中是否包含
eval、exec、os.system等危险函数。如果发现,评审不通过。测试效果。 - (思考题)当前迭代闭环是"测试驱动"的——测试不通过就修复。如果评审员发现严重的架构问题(如模块划分不合理),需要回到架构设计阶段重新设计,你会如何扩展这个流程?请描述你的方案。
明日预告:我们将学习 AutoGen 的嵌套对话和工具共享机制,探索如何将 CrewAI 和 AutoGen 结合使用,取长补短。同时,我们将对迷你软件公司项目进行最终复盘,整理代码和文档,完成第四阶段的第三个项目。
有任何问题欢迎随时评论交流!