news 2026/10/6 5:52:42

LangGraph构建可自我修正的代码生成Agent

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LangGraph构建可自我修正的代码生成Agent

1. 这不是“写完就交”的代码生成,而是会自己重写的Agent

我第一次把“用LangGraph写个能自测自修的代码生成Agent”这个需求丢给团队新人时,他花三天搭出了一个调用LLM返回Python函数的链路,跑通了Hello World。结果第二天产品提了个真实需求:生成一个带边界校验的JSON解析器,并附上单元测试。新人提交的代码在json.loads("{")时直接崩溃,而他的Agent连报错都没捕获——它只是把LLM吐出来的字符串原封不动塞进exec(),然后等SyntaxError炸开。

这暴露了一个被严重低估的事实:绝大多数所谓“代码生成Agent”,本质是高级版的Copilot——它不理解“生成”二字背后的工程闭环,更不承担“交付”责任。它生成的不是可运行的代码,而是待验证的提案;它输出的不是解决方案,而是需要人工兜底的半成品。而LangGraph的价值,恰恰在于它强制你把“验证-反馈-重写”这个闭环显式建模为图结构,而不是靠LLM的幻觉去覆盖所有边界。

“自我修正”四个字,在工程语境里从来不是玄学。它意味着三件事必须落地:第一,有明确的验证标准(比如单元测试是否全部通过、PEP8是否达标、类型注解是否完整);第二,有可执行的反馈通道(测试失败的具体行号、错误类型、期望vs实际值);第三,有结构化的重写指令(不是让LLM“再试一次”,而是告诉它:“第12行缺少try-except包裹,第17行assert应改为pytest.raises”)。LangGraph不提供魔法,但它给了你一张白纸和尺子——你可以画出从“生成”到“交付”的每一步路径,也能清晰看到哪一环断了。

这个项目的核心,就是用LangGraph把“写代码→跑测试→看失败→改代码→再跑测试”这个人类程序员每天重复上百次的动作,变成一个可追踪、可调试、可中断、可审计的图节点流。它不追求一次生成完美代码,而是确保每一次失败都成为下一次成功的燃料。关键词里的“自我修正”,在这里不是营销话术,而是图中每个check_test_result节点的return值决定下一个rewrite_code节点是否被触发的布尔逻辑。

如果你正在评估是否值得投入时间学习LangGraph来构建Agent,这个问题可以帮你快速判断:你的代码生成场景里,有没有哪怕一个需求,其正确性无法靠单次LLM输出保证,而必须依赖外部验证机制?如果有——那LangGraph不是加分项,而是必选项。因为真正的生产力提升,从来不在“生成更快”,而在“失败后修复更快”。

2. LangGraph图结构设计:为什么不用Chain,而必须用Graph?

很多开发者初接触LangGraph时,第一反应是:“我用LangChain的SequentialChain不就能串起‘生成→测试→修正’吗?”——这恰恰踩中了最典型的认知陷阱。Chain是线性流水线,Graph是状态驱动的工作流。当你的Agent需要根据动态反馈结果决定下一步动作时,Chain的刚性链条立刻崩解。

举个具体例子:假设Agent生成了一段处理CSV文件的代码,单元测试跑完返回3个失败用例。此时系统面临三种可能路径:

  • 如果失败是语法错误(如SyntaxError: invalid syntax),应该触发syntax_fix节点,聚焦于代码结构修复;
  • 如果失败是逻辑错误(如AssertionError: expected [1,2,3] but got [1,2,4]),应该触发logic_rewrite节点,要求LLM重新理解业务规则;
  • 如果失败是环境问题(如ModuleNotFoundError: No module named 'pandas'),应该跳过重写,直接进入dependency_install节点。

Chain无法表达这种分支逻辑。你只能写一堆if-else嵌套在单个Runnable里,把状态判断和业务逻辑混在一起,最终得到一个难以调试、无法复用、每次新增校验类型都要重写主逻辑的巨石函数。而LangGraph的State Schema + Conditional Edges,天然适配这种决策树:

class CodeGenState(TypedDict): code: str test_code: str test_result: Optional[str] error_type: Optional[str] # "syntax" | "logic" | "dependency" | "none" attempt_count: int max_attempts: int = 3 def route_after_test(state: CodeGenState) -> Literal["syntax_fix", "logic_rewrite", "dependency_install", "success"]: if state["error_type"] == "syntax": return "syntax_fix" elif state["error_type"] == "logic": return "logic_rewrite" elif state["error_type"] == "dependency": return "dependency_install" else: return "success"

这个route_after_test函数就是图的“神经中枢”。它不关心LLM怎么生成代码,也不操心pytest怎么跑测试——它只做一件事:读取state中的error_type字段,决定数据流向哪个节点。每个节点(syntax_fix,logic_rewrite)都是独立的、可单独测试的模块,它们的输入输出严格遵循CodeGenState契约。这种解耦带来的好处是实打实的:

  • 调试成本直降:当测试失败时,你不需要重放整个流程。直接加载失败时的state快照,注入到logic_rewrite节点单独运行,5分钟内就能确认是提示词问题还是模型能力瓶颈;
  • 灰度发布可行:你想先上线syntax_fix能力,把logic_rewrite设为fallback人工介入,只需修改路由函数的返回逻辑,无需动任何节点代码;
  • 可观测性拉满:每个节点执行耗时、输入输出、错误率都能被LangGraph的checkpointer自动记录,你甚至能画出“某类逻辑错误平均需2.3次重写才能通过”的热力图。

我见过太多团队用Chain硬扛复杂流程,最后在attempt_count > 3的判断里堆砌了200行条件逻辑。LangGraph的真正价值,不是语法糖,而是把“状态驱动决策”这个软件工程基本范式,以声明式方式刻进Agent的DNA里。它强迫你提前想清楚:我的Agent有哪些确定性状态?哪些状态转移是必须支持的?哪些失败模式需要独立处理路径?——这些问题的答案,直接决定了你的Agent是玩具还是生产级工具。

3. 单元测试生成与执行:让Agent自己当最严苛的Reviewer

“自我修正”的根基,是Agent能对自己生成的代码进行可编程的、可复现的、可量化的质量检验。这里的关键不是“生成测试”,而是“生成可执行的、高保真的测试”。很多教程教你怎么让LLM写assert add(1,2)==3,但这离真实工程需求差了十万八千里。

我们采用的方案是:由Agent生成完整可运行的pytest模块,包含fixtures、parametrize用例、异常测试,并在隔离沙箱中执行。具体分三步走:

3.1 测试生成:超越“hello world”的提示工程

LLM生成测试的质量,90%取决于输入提示的结构化程度。我们不给它看原始需求描述,而是喂给它三样东西:

  • 被测函数签名(含类型注解、docstring)
  • 典型输入输出对(来自需求文档的示例)
  • 常见错误模式清单(如空输入、边界值、类型错误)

提示词核心片段如下:

你是一名资深Python测试工程师。请为以下函数生成pytest测试模块: {function_signature} 关键要求: 1. 必须覆盖:正常路径(使用示例输入)、边界值(如空列表、极大数值)、异常路径(如传入None、字符串); 2. 使用@pytest.mark.parametrize参数化测试,每个用例包含input, expected, raises三元组; 3. 对于异常路径,必须用with pytest.raises(ExpectedException)捕获; 4. 所有测试函数名以'test_'开头,添加详细docstring说明测试意图; 5. 输出仅包含Python代码,不要解释,不要markdown格式。

这个提示的设计逻辑很务实:它不追求LLM“理解”业务,而是把它当作一个结构化模板填充器。@pytest.mark.parametrize强制要求输入输出成对出现,避免LLM自由发挥写出无法断言的测试;raises字段明确区分正常返回和异常抛出,为后续错误分类提供结构化依据。

3.2 沙箱执行:杜绝“本地能跑,线上爆炸”

生成的测试代码如果直接在主进程执行,会带来两个致命风险:一是测试污染全局状态(如修改sys.path),二是恶意代码执行(虽然概率低,但生产环境零容忍)。我们的解决方案是基于subprocess的轻量沙箱:

def execute_test_in_sandbox(test_code: str, target_code: str) -> TestResult: # 构建临时目录,写入target.py和test_target.py temp_dir = tempfile.mkdtemp() try: with open(f"{temp_dir}/target.py", "w") as f: f.write(target_code) with open(f"{temp_dir}/test_target.py", "w") as f: f.write(test_code) # 在干净环境中执行pytest result = subprocess.run( ["pytest", "test_target.py", "-v", "--tb=short"], cwd=temp_dir, capture_output=True, text=True, timeout=30 ) return parse_pytest_output(result.stdout, result.stderr, result.returncode) finally: shutil.rmtree(temp_dir)

这个沙箱的关键细节在于:

  • 超时控制(timeout=30):防止无限循环测试拖垮整个Agent;
  • 输出解析(parse_pytest_output):不依赖pytest的JSON报告插件(增加部署复杂度),而是用正则精准提取失败用例的line number、error type、expected/actual值;
  • 临时目录隔离:每个测试执行都是全新环境,彻底规避模块缓存、全局变量污染。

3.3 错误分类:把“测试失败”翻译成“重写指令”

测试执行返回的原始信息是文本,但Agent需要的是结构化决策信号。我们构建了一个轻量级分类器,将pytest的stderr映射到error_type:

pytest stderr片段error_type后续动作
SyntaxError: invalid syntaxsyntax提取错误行号,发送给syntax_fix节点
AssertionError: assert 1 == 2logic提取assert语句,发送给logic_rewrite节点
ModuleNotFoundError: No module named 'xxx'dependency提取模块名,发送给dependency_install节点
TypeError: xxx() takes 2 positional arguments but 3 were givensignature提取函数签名,发送给signature_align节点

这个分类器不是AI模型,而是精心编写的正则匹配规则。原因很现实:正则100%可靠,而微调一个小模型去识别pytest错误类型,其维护成本远高于收益。更重要的是,它让整个流程完全透明——你能一眼看出为什么Agent选择了某个重写路径,而不是面对一个黑盒分类结果干瞪眼。

提示:在实际部署中,我们发现约15%的“逻辑错误”其实源于LLM对需求理解偏差(比如把“大于等于”理解成“大于”)。为此,我们在logic_rewrite节点的提示词中强制要求:“请逐字对照需求文档中的约束条件,指出当前代码违反了哪一条,并重写满足所有约束的版本”。这比单纯说“修复逻辑错误”有效得多。

4. 自我修正循环的临界点:为什么3次重试是工程最优解?

几乎所有教程在讲“重试机制”时,都轻描淡写地说“设置max_attempts=3”。但这个数字绝非拍脑袋决定——它是我们在237个真实代码生成任务(涵盖数据清洗、API封装、算法实现三类)中,通过A/B测试得出的成本效益拐点。

我们监控了两个核心指标:

  • 单任务平均耗时(从需求输入到最终通过)
  • 单任务LLM token消耗(按gpt-4-turbo计费)

测试结果呈现清晰的边际效应递减:

max_attempts平均耗时(秒)平均token消耗一次性通过率三次内通过率
14.21,20038%38%
212.72,80061%61%
328.54,10079%87%
449.35,90085%92%
576.87,30089%95%

关键洞察在于:从第3次到第4次重试,token消耗增长44%,但成功率仅提升5%。而更致命的是,第4次重试的耗时(49.3秒)已接近人工编写同功能代码的平均时间(约45秒)。这意味着,当重试次数超过3次,Agent的“自动化优势”开始消失。

因此,我们将max_attempts=3写死在State Schema里,并设计了明确的fallback策略:

  • 当attempt_count == 3且仍失败时,Agent不继续重试,而是生成一份诊断报告,包含:
    • 原始需求文本
    • 三次生成的代码diff(用difflib生成可读对比)
    • 每次失败的pytest错误摘要
    • 一条建议:“建议人工检查需求歧义点:XXX(如‘处理空列表’未定义行为)”

这份报告不是甩锅,而是把Agent的“认知盲区”转化为人类可操作的信息。实践中,82%的此类报告能帮开发者在5分钟内定位到需求文档的模糊表述,比让Agent盲目重试高效得多。

这个设计背后是深刻的工程哲学:Agent的价值不在于替代人类,而在于把人类从重复试错中解放出来,聚焦于真正需要创造力的环节。当重试成本超过人类干预成本时,及时止损并移交,才是负责任的Agent设计。

5. 生产环境避坑指南:那些文档里不会写的血泪教训

LangGraph官方文档优雅简洁,但真实生产环境像一片布满地雷的沼泽。以下是我们在金融、电商、IoT三个领域落地时,踩出的五个必须绕开的坑:

5.1 Checkpointing不是可选功能,而是生命线

很多教程把MemorySaver当作“保存聊天历史”的锦上添花功能。但在自我修正Agent里,它是故障恢复的唯一救命稻草。想象这个场景:Agent正在执行第2次重写,LLM API突然超时,整个进程崩溃。没有checkpoint,你只能从头开始——第三次生成又要重跑前两次的测试,浪费算力且延长交付时间。

我们的实践是:每个节点执行完毕后,立即调用checkpointer.put()保存完整state。关键细节:

  • 使用sqlite后端而非内存版,确保进程重启后state不丢失;
  • 在checkpointer.get()时增加重试逻辑(网络抖动可能导致首次读取失败);
  • 为每个state添加timestamp和node_name字段,便于排查“卡在哪个节点”。

注意:不要在checkpointer.put()里存大对象(如完整的pytest stdout文本)。我们只存结构化结果({"passed": False, "failed_tests": ["test_edge_case"]}),原始日志另存对象存储。

5.2 LLM调用必须带“防呆”超时

LangGraph默认的RunnableLambda不继承LLM的timeout设置。这意味着,当OpenAI API响应缓慢时,整个graph会卡死,后续所有请求排队等待。我们的解决方案是在每个LLM节点外层包一层超时控制:

from langchain_core.runnables import RunnableTimeout # 错误示范:直接调用llm.invoke() # correct_chain = llm | parser # 正确做法:用RunnableTimeout包装 correct_chain = ( RunnableTimeout.create( llm | parser, timeout=15, # 秒 fallback=lambda: {"error": "LLM timeout, please retry"} ) )

这个fallback返回的结构化错误,会被后续的route_after_llm节点捕获,导向专门的llm_timeout_recovery路径——比如降级到更便宜的模型,或返回缓存的相似案例。

5.3 单元测试生成的“幻觉陷阱”

LLM生成测试时,最大的危险不是写错assert,而是虚构不存在的函数或参数。我们曾遇到Agent生成了test_with_invalid_encoding(),但被测函数根本没定义encoding参数。pytest执行时报AttributeError,却被错误分类为logic错误,导致重写方向完全错误。

破局方法是:在测试执行前,用AST静态分析验证测试代码的合法性。我们写了一个轻量解析器:

import ast def validate_test_references(test_code: str, target_code: str) -> List[str]: # 解析target_code,提取所有函数名、参数名 target_tree = ast.parse(target_code) target_funcs = set() for node in ast.walk(target_tree): if isinstance(node, ast.FunctionDef): target_funcs.add(node.name) for arg in node.args.args: target_funcs.add(arg.arg) # 解析test_code,检查所有函数调用是否在target_funcs中 test_tree = ast.parse(test_code) errors = [] for node in ast.walk(test_tree): if isinstance(node, ast.Call) and isinstance(node.func, ast.Name): if node.func.id not in target_funcs: errors.append(f"Call to undefined function '{node.func.id}'") return errors

这个检查在测试执行前运行,发现引用错误立即终止流程,归类为reference_error,触发专门的signature_validation节点——它会重新解析被测函数AST,生成准确的函数签名供LLM参考。

5.4 并发安全:别让多个Agent实例踩进同一个沙箱

当Agent部署为FastAPI服务时,多个请求并发执行,tempfile.mkdtemp()生成的临时目录名可能冲突(尤其在高负载下)。我们的解决方案是:用请求ID作为沙箱目录名前缀,并加锁确保唯一性。

import threading sandbox_lock = threading.Lock() def get_unique_sandbox_dir(request_id: str) -> str: with sandbox_lock: # 确保同一request_id不会重复创建 if request_id not in _sandbox_dirs: _sandbox_dirs[request_id] = tempfile.mkdtemp(prefix=f"agent_{request_id}_") return _sandbox_dirs[request_id]

同时,我们在沙箱清理时采用shutil.rmtree(..., ignore_errors=True),避免因目录已被其他进程删除而报错。

5.5 日志不是为了看,是为了告警

初期我们只用print()输出节点执行日志,结果在线上环境完全无法定位问题。现在,每个节点都集成结构化日志:

import logging logger = logging.getLogger(__name__) def syntax_fix_node(state: CodeGenState) -> CodeGenState: logger.info("syntax_fix_node start", extra={ "request_id": state.get("request_id", "unknown"), "attempt": state["attempt_count"], "error_line": extract_error_line(state["test_result"]) }) # ... 处理逻辑 logger.info("syntax_fix_node end", extra={"fixed_lines": len(fixed_code_lines)})

这些日志被接入ELK,我们设置了关键告警:

  • syntax_fix_node执行超10秒 → 可能LLM陷入死循环;
  • execute_test_in_sandbox返回timeout错误超阈值 → 沙箱资源不足;
  • 连续3次logic_rewrite失败 → 需要人工介入需求澄清。

最后分享一个真实教训:某次上线后,dependency_install节点频繁失败。日志显示pip install pandas超时。排查发现是沙箱容器没配DNS。这个告警让我们在5分钟内定位到基础设施问题,而不是花半天怀疑LLM能力。日志的价值,永远在故障发生前就已埋下。

6. 从Demo到生产:如何让这个Agent真正“下地干活”

看到这里,你可能已经能跑通一个本地Demo。但真正的挑战在于:如何让它在真实业务场景中稳定交付?我们总结出三条不可妥协的落地原则:

6.1 需求输入必须结构化,拒绝自然语言“小作文”

让产品经理直接粘贴一段需求描述(如“写个函数,把用户订单按金额排序,金额一样的按时间倒序”)给Agent,是99%失败的开端。LLM对模糊表述的解读千差万别。我们的解决方案是:强制前端提供结构化表单。

表单包含:

  • 函数签名草案(开发者填写,如def sort_orders(orders: List[dict], reverse_amount: bool = True) -> List[dict]:)
  • 输入输出示例(表格形式,至少3行:正常、边界、异常)
  • 约束条件清单(勾选框:□ 支持空列表 □ 时间格式为ISO8601 □ 金额精度保留2位小数)

这个表单看似增加前端工作量,实则把需求歧义消灭在源头。Agent收到的不再是“小作文”,而是机器可解析的契约。实践中,结构化输入使一次性通过率从38%提升至72%,且大幅降低logic_rewrite节点的无效重试。

6.2 “自我修正”不等于“自我负责”,必须有人类守门员

我们给Agent设定了一条铁律:所有生成代码必须经过人工Code Review才能合并。Agent的作用是把Review者从“找bug”升级为“审设计”。具体流程:

  • Agent生成代码+测试+覆盖率报告(用pytest-cov生成);
  • CI流水线自动运行测试,通过后生成PR;
  • Review者只关注三点:1)测试用例是否覆盖业务场景;2)代码架构是否符合团队规范;3)是否有潜在性能陷阱(如N+1查询)。

这个流程让资深工程师的Review时间从平均45分钟降至8分钟,因为他们不再需要逐行检查边界条件——Agent已用20个测试用例证明了这点。

6.3 持续进化:用失败案例反哺提示词工程

每个失败的Agent任务,都是提示词优化的金矿。我们建立了自动化收集管道:

  • 当attempt_count == 3失败时,自动将原始需求、三次生成代码、最终诊断报告存入向量数据库;
  • 每周用相似度检索,找出高频失败模式(如“日期解析错误”、“嵌套字典遍历遗漏”);
  • 由工程师针对TOP3模式,重写对应节点的提示词,并A/B测试效果。

这个闭环让我们在三个月内,将logic_rewrite节点的单次修复成功率从41%提升至68%。提示词不是写一次就完事的文档,而是需要持续迭代的代码。

最后说一句掏心窝的话:LangGraph不是银弹,它不会让你的Agent一夜之间媲美十年经验的工程师。但它是一把精准的手术刀,帮你把“智能”从LLM的混沌输出中,切割、缝合、加固成可预测、可管理、可审计的工程模块。当你不再问“Agent能不能做”,而是问“这个节点的输入输出契约是否清晰”、“这条边的路由条件是否覆盖所有失败模式”、“这个checkpoint能否支撑故障恢复”——你就真正跨过了从Demo到生产的门槛。真正的生产力革命,永远始于对确定性的执着追求,而非对不确定性的浪漫幻想。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/6 5:52:40

非游戏开发者用AI做微信小游戏:从开发到备案上线全记录

1. 一个非游戏开发者的真实起点我做了八年后端开发,主要写Java和Python,游戏开发的经验基本为零。Unity没碰过,Cocos只会新建项目,微信小游戏对我来说一直是个“看起来不难但不知道从哪下手”的东西。直到去年年底,我想…

作者头像 李华
网站建设 2026/10/6 5:52:29

AI短剧工具实测:从剧本到成片的完整工作流与踩坑指南

这周在杭州待了几天,正好赶上圈里几个人凑在一起聊AI短剧。有个老哥直接掏出手机给我放了一条刚刚跑完的样片,古风、滤镜、镜头语言都像模像样,结果主角的脸在第三分钟换了一张。他摊手说这是第五稿,崩脸问题始终解决不掉。我问他…

作者头像 李华
网站建设 2026/10/6 5:52:27

Trae:AI原生IDE的配置与工作流实践

1. 这不是又一个“AI插件”,而是一次IDE底层逻辑的重写Trae 不是 VS Code 上装个 Copilot 插件、也不是 JetBrains 里加个 AI Assistant 就完事的那种“增强型 IDE”。它从第一天起就拒绝把 AI 当作锦上添花的装饰——而是直接把大模型推理引擎、代码理解图谱、上下…

作者头像 李华
网站建设 2026/10/6 5:51:50

Agent技能统一管理:跨平台分发与适配实战

开头:这几年AI编程工具像雨后春笋一样往外冒:Claude Code、Cline、Trae、OpenCode、Continue……个个都支持Agent技能(skills),但我很快发现一个扎心的问题——这些工具的技能格式、存放目录、加载方式全都不一样。维护…

作者头像 李华
网站建设 2026/10/6 5:51:23

iframe跨域通信与鉴权实战:从postMessage到多端适配

先说一个现实场景:做前端这些年,iframe是我又爱又恨的技术。爱的是页面隔离足够干净,恨的是只要涉及跨域通信和鉴权,坑就一个接一个。最近在做一个SaaS主站集成外部BI报表系统的项目,同时还要适配PC浏览器和移动端H5&a…

作者头像 李华
网站建设 2026/10/6 5:50:47

OpenSSH 8.8p1 源码编译升级实战:从依赖准备到故障回退

简介:OpenSSH 8.8p1 源码压缩包面向运维工程师、系统管理员及安全研究人员,用于在 Linux/Unix 环境中部署安全远程登录与文件传输服务,解决明文通信带来的数据泄露与身份冒用风险。包内共 854 个文件,以 287 个 C 源文件、123 个头…

作者头像 李华