第5篇:Vibe Coding时代:LangGraph 测试闭环实战,让 Agent 自动生成代码、运行测试并修复失败
一、问题场景:Agent 写完代码后,没人知道它到底能不能跑
很多 AI Coding Demo 到“生成代码”就结束了。
但是做过真实开发都知道:
代码生成出来,只是第一步;能不能跑,测试说了算。
我之前做过一个自动生成接口的 Agent。
模型输出看起来很完整:
@app.post("/login")deflogin(...):...但实际运行发现:
- 依赖没写全
- import 路径错误
- Pydantic 版本用法混乱
- 测试用例跑不通
- 生成了不存在的函数
- 异常返回格式不一致
如果没有测试闭环,这些问题都要人工发现。
所以本文要实现:
生成代码 → 写入文件 → 生成测试 → 执行 pytest → 失败后修复 → 再测试这才更接近真实 Vibe Coding。
二、流程设计
用户需求 ↓ 生成项目文件 ↓ 生成测试文件 ↓ 写入 workspace ↓ 执行 pytest ↓ 测试是否通过? ├── 通过:输出结果 └── 失败:把错误信息传回代码生成节点核心点:
测试错误信息必须进入 State,并传回修复节点。
否则模型不知道怎么修。
三、项目结构
vibe-agent-test-loop/ ├── app.py ├── graph.py ├── state.py ├── chains.py ├── tools.py ├── workspace/ └── requirements.txt四、依赖安装
langchain langchain-openai langgraph python-dotenv pytest fastapi uvicorn httpx安装:
pipinstall-rrequirements.txt五、定义工具
创建tools.py:
importsubprocessfrompathlibimportPath BASE_DIR=Path(__file__).parent.resolve()WORKSPACE=BASE_DIR/"workspace"WORKSPACE.mkdir(exist_ok=True)defsafe_path(path:str)->Path:target=(WORKSPACE/path).resolve()ifnotstr(target).startswith(str(WORKSPACE)):raiseValueError("非法路径")returntargetdefwrite_file(path:str,content:str)->str:target=safe_path(path)target.parent.mkdir(parents=True,exist_ok=True)target.write_text(content,encoding="utf-8")returnstr(target)defrun_pytest()->str:result=subprocess.run(["pytest","-q"],cwd=str(WORKSPACE),capture_output=True,text=True,timeout=30,)return(f"returncode={result.returncode}\n\n"f"stdout:\n{result.stdout}\n\n"f"stderr:\n{result.stderr}")注意:
cwd限制在 workspacetimeout防止测试卡死- stdout/stderr 都要保存
六、定义 State
创建state.py:
fromtypingimportTypedDict,ListclassFileItem(TypedDict):path:strcontent:strclassTestLoopState(TypedDict):requirement:strfiles:List[FileItem]test_result:strretry_count:interrors:List[str]final_answer:str七、生成代码和测试
创建chains.py:
importjsonfromlangchain_openaiimportChatOpenAIfromlangchain_core.promptsimportChatPromptTemplate llm=ChatOpenAI(model="gpt-4o-mini",temperature=0.1)defgenerate_project_files(requirement:str,test_result:str="")->list[dict]:prompt=ChatPromptTemplate.from_messages([("system","你是一名资深 Python 工程师。""请生成可运行项目文件和 pytest 测试文件。""必须输出 JSON 数组,不要输出 Markdown。"),("user",""" 用户需求: {requirement} 上一次测试结果: {test_result} 请输出 JSON 数组: [ {{ "path": "main.py", "content": "代码内容" }}, {{ "path": "test_main.py", "content": "测试内容" }} ] 要求: 1. 代码必须可运行 2. 测试必须能用 pytest 执行 3. 如果有上一次测试错误,请针对错误修复 """)])chain=prompt|llm response=chain.invoke({"requirement":requirement,"test_result":test_result,})returnjson.loads(response.content)八、构建 LangGraph
创建graph.py:
fromlanggraph.graphimportStateGraph,ENDfromstateimportTestLoopStatefromchainsimportgenerate_project_filesfromtoolsimportwrite_file,run_pytest MAX_RETRY=2defgenerate_node(state:TestLoopState)->TestLoopState:try:files=generate_project_files(requirement=state["requirement"],test_result=state["test_result"],)state["files"]=filesexceptExceptionase:state["errors"].append(f"生成失败:{str(e)}")returnstatedefwrite_node(state:TestLoopState)->TestLoopState:forfileinstate["files"]:try:write_file(file["path"],file["content"])exceptExceptionase:state["errors"].append(f"写入失败:{file.get('path')}{str(e)}")returnstatedeftest_node(state:TestLoopState)->TestLoopState:try:state["test_result"]=run_pytest()exceptExceptionase:state["test_result"]=f"测试执行异常:{str(e)}"state["errors"].append(state["test_result"])returnstatedefshould_retry(state:TestLoopState)->str:if"returncode=0"instate["test_result"]:return"finish"ifstate["retry_count"]>=MAX_RETRY:return"finish"state["retry_count"]+=1return"retry"deffinal_node(state:TestLoopState)->TestLoopState:state["final_answer"]=("## 测试结果\n\n"f"{state['test_result']}\n\n""## 重试次数\n\n"f"{state['retry_count']}\n\n""## 错误信息\n\n"f"{state['errors']}")returnstatedefbuild_graph():graph=StateGraph(TestLoopState)graph.add_node("generate",generate_node)graph.add_node("write",write_node)graph.add_node("test",test_node)graph.add_node("final",final_node)graph.set_entry_point("generate")graph.add_edge("generate","write")graph.add_edge("write","test")graph.add_conditional_edges("test",should_retry,{"retry":"generate","finish":"final",})graph.add_edge("final",END)returngraph.compile()九、运行入口
创建app.py:
fromgraphimportbuild_graphdefmain():app=build_graph()state={"requirement":"生成一个 FastAPI Hello World 接口,GET / 返回 {'message': 'hello'}","files":[],"test_result":"","retry_count":0,"errors":[],"final_answer":"",}result=app.invoke(state)print(result["final_answer"])if__name__=="__main__":main()运行:
python app.py十、验证结果
理想情况下输出:
returncode=0 stdout: . [100%] 1 passed in 0.42s如果第一次失败,可能类似:
ModuleNotFoundError: No module named 'main'这个错误会传回generate_node,模型会基于错误重新生成代码。
十一、踩坑记录:不要只让模型“自我审查”
很多人会写:
请检查你生成的代码是否有问题模型通常会回答:
代码整体没有明显问题。但实际运行可能直接报错。
所以工程上更可靠的闭环是:
模型审查 + 真实测试执行模型审查发现风格问题,测试执行发现运行问题。
两者不能互相替代。
十二、踩坑记录:测试命令必须设置 timeout
错误写法:
subprocess.run(["pytest"])如果测试卡住,整个 Agent 会一直挂着。
正确写法:
subprocess.run(["pytest","-q"],timeout=30,capture_output=True,text=True)所有外部命令都应该设置超时。
十三、踩坑记录:测试失败信息必须完整传回模型
不要只传:
测试失败这没用。
要传:
returncode stdout stderr 完整 traceback模型修复代码时,非常依赖错误细节。
十四、适合收藏:测试闭环步骤
1. 生成业务代码 2. 生成测试代码 3. 写入 workspace 4. 执行 pytest 5. 捕获 returncode/stdout/stderr 6. 判断是否通过 7. 失败时把错误传回生成节点 8. 限制最大重试次数 9. 通过后输出最终结果 10. 失败后保留最后一次错误十五、避坑清单
| 坑点 | 表现 | 解决方案 |
|---|---|---|
| 只生成代码不测试 | 不知道能不能跑 | 加 pytest 节点 |
| 只传“失败” | 模型无法修复 | 传完整 stdout/stderr |
| 没有 timeout | 流程卡死 | subprocess 加 timeout |
| 无限重试 | 成本失控 | 设置 MAX_RETRY |
| 工作目录错误 | 找不到文件 | cwd 指向 workspace |
| 测试文件缺依赖 | pytest 失败 | 生成 requirements 或固定依赖 |
十六、总结
这一篇我们让 Coding Agent 从“生成代码”升级成了“生成 + 测试 + 修复”的闭环。
这是 Vibe Coding 进入工程化的关键一步。
我的经验是:
没有测试闭环的 AI Coding,只能叫代码建议;有测试闭环,才开始接近自动开发。
后续还可以继续扩展:
- 执行 ruff 检查
- 执行 mypy 类型检查
- 执行安全扫描
- 生成覆盖率报告
- 对失败用例做归因分析
Agent 不应该只会写代码,它还应该知道代码是否真的能工作。