news 2026/10/29 7:41:28

基于LangGraph的自我修正代码Agent:从一次生成到闭环修复

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于LangGraph的自我修正代码Agent:从一次生成到闭环修复

1. 为什么“一次生成、坐等结果”的代码 Agent 不靠谱

先聊一个所有搞 Agent 的人都会撞上的现实:大模型写代码,一次跑通的概率远比你想象的低。即使是最强的模型,面对稍复杂的仓库结构、不够明确的错误信息、前后端联调需求,第一版代码基本都能跑,但功能往往差一口气——要么边界条件没处理,要么 API 参数写错,要么引入一个奇怪的不兼容。

我见过很多团队把大模型包装成一个“代码生成接口”——输入需求描述,输出代码,然后让开发自己去测试、去改。这样不是不能用,但本质上你只是把“从搜索引擎复制代码改成自己项目”这件事,交给了大模型,后面的验证和修复还得靠人。更关键的是,你没法让这个过程自动化起来。

最早我发现这个问题,是在一个内部工具项目里:我需要生成一段从 Excel 读取数据、做清洗、再导入数据库的脚本。模型一次性给出的代码看起来非常专业,类型标注齐全、注释得体,但运行时报错——库没装、API 已废弃、字段名拼错。正常开发中这种问题找出来也就几分钟,但如果目标是“让 Agent 自己干活”,它必须能够看到测试失败的反馈,再回头改自己的代码,直到通过为止。

这就引出了所谓“自我修正 Agent”的核心需求:把“写代码”从一次生成变成有反馈的闭环——生成 → 运行验证 → 读取错误 → 修复 → 再验证。这个闭环不是靠叠加提示词“请仔细思考”能解决的,而是需要一个能编排状态、控制流程、支持条件分支的框架。LangGraph 恰好是干这个的。

我选择 LangGraph 而不是直接手写while循环,主要原因有这么几个:第一,它的状态管理是结构化的,代码、错误信息、已完成步骤、尝试次数都能放进共享状态,不会因为函数调用层级复杂而丢失;第二,条件边(conditional edge)能根据验证结果决定下一步走“修复”还是“收工”,这个分支逻辑在原生代码里写容易乱,换成图结构一目了然;第三,LangGraph 原生支持检查点(checkpointer),Agent 中途崩了可以从上次状态接着跑,省掉一大堆工程轮子。

如果你目前只把 Agent 做成“调一次模型拿个结果”,那这篇文章可能会改变你这个习惯——至少对代码生成这个场景而言,缺少反馈闭环的 Agent 是不完整的产品。

2. 整体架构:状态、节点、条件边如何编排修正闭环

2.1 先放下“链式调用”的思维,Agent 是一张图

很多人写 Agent 容易陷入一种固定套路:先调一次模型做规划,再调一次模型写代码,再调一次模型总结结果——本质上是一条直线。这个思路只能叫“串联脚本”,不是真正的 Agent。真正能具备自我修正能力的结构,是一张允许走回头路的图。

LangGraph 把整条执行路径抽象成三个核心部件:

  • 状态(State):全局共享的数据结构,所有节点都能读也能写。
  • 节点(Node):执行单一职责的函数,比如“生成代码”“执行测试”“提炼错误”。
  • 边和条件边(Edge / Conditional Edge):决定下一个节点是谁,普通边固定走向,条件边看状态动态跳转。

我设计的修正闭环是四步循环:Generate → Run Test → Parse Error → Fix Code,每一步对应一个节点,它们之间用条件边相连。

整体流程走下来是这样:

  1. 用户把任务描述传入“生成代码”节点,模型输出代码文本。
  2. 代码写入工作目录,触发“执行验证”节点——运行编译、静态检查或单元测试。
  3. 验证结果(包括报错信息、测试通过率等)写入状态,进入“分析反馈”节点。
  4. 分析节点把原始 stdout + stderr 整理成结构化的错误摘要,送进“修复代码”节点。
  5. 模型拿到“原代码 + 错误摘要 + 任务需求”后重写代码,再次回到第 2 步。

整个循环由一条条件边把关:如果验证通过了,跳向“完成”终点;如果没通过且尝试次数没超限,跳回“修复代码”;如果尝试次数超限,也要中断退出,避免无限循环烧钱。

2.2 状态设计:把“代码、需求、错误、历史”都装进同一个结构体

状态设计是最容易被低估的环节。很多人一开始只放一个code字段,跑着跑着发现不对——出错后要恢复历史版本、要区分哪条错误是上一次修复引入的、要看当前是第几次尝试,这些信息如果不在状态里,后面根本没法写逻辑。

我用 TypedDict 定义状态,大致是这样:

from typing import TypedDict, List, Optional class CodeGenState(TypedDict): requirement: str # 原始需求,全程不做修改 code: str # 当前代码 attempts: int # 当前尝试次数 max_attempts: int # 最大尝试次数,防止死循环 error_feedback: str # 最近一次的结构化错误摘要 raw_logs: List[str] # 每次验证的原始日志,便于追溯 generation_history: List[str] # 每次生成的代码快照 passed: bool # 最新一次验证是否通过

这里有两个细节值得说明。generation_history是我在后来的迭代中加上去的——为什么?因为模型修复代码时经常把原来对的东西改错了,或者反复进入同一种错误模式。有历史快照的好处是,状态里可以直接把“上一版代码”作为修复提示的一部分给模型看,让它知道发生了什么变化,而不是只看到最终报错。另一个是raw_logs,一开始我觉得只保留error_feedback就够了,后来发现排查问题时原始日志不可替代,比如某些偶发性的网络超时,模型自己都判断不出是代码问题还是环境问题,只有看原始输出才明白。

2.3 节点划分:每个节点只干一件事,避免“大杂烩”函数

我最开始写的时候犯过一个错误:把所有逻辑塞进两个节点里,一个负责生成和修复,一个负责测试和解析。跑了两轮就发现,模型报错后重新生成代码时,会把“修复代码的正确意图”和“生成初始代码的创作意图”混在一起,导致节点又当翻译又当 debugger,效果很差。

划分职责时我遵循一个原则:一个节点对应一个模型调用或一个确定性工具,不在同一个节点里既思考又动手。

我最终的节点列表是:

节点名称职责是否调用模型核心输出
generate_code根据需求生成第一版代码是代码文本
run_validation写文件并运行测试/编译否原始退出码和日志
analyze_error从日志中提炼错误要点是结构化错误摘要
fix_code结合错误摘要和原代码重写代码是修正后的代码

节点间最大的区别在于“会不会改变状态里的code”。run_validation永远不碰代码内容,它是一个纯粹的执行器——好处是,如果验证逻辑稳定,你可以先本地跑一遍,确认无误后再接到图里,减少 Agent 链路里的不确定因素。很多踩坑案例其实都不是模型的问题,而是测试工具本身的输出格式不稳定,导致“反馈信号”是脏的,后面再怎么修都白搭。

2.4 条件边:让循环“知道”什么时候该停

条件边是整张图里最关键的一环。它不调用大模型,只根据状态字段做简单的判断:

def should_continue(state: CodeGenState) -> str: if state["passed"]: return "finish" if state["attempts"] >= state["max_attempts"]: return "finish" # 也可以走一个专门的“超限失败”终点 return "fix_code"

有人可能会问:为什么需要 max_attempts?模型不是越修越好吗?实测中并不总是这样——有相当的机率,模型在第三、第四次修复时会突然把整个逻辑推翻重写,甚至删除之前正确的代码。限定次数既是对成本的控制,也是质量兜底:与其让 Agent 无限试错,不如让它停下来,把已有进展和失败原因交给人处理。

条件边书写时还有一种容易踩的问题:返回值必须是目标节点的名称字符串,如果拼错了,LangGraph 不会在构造图时报错,而是在运行时抛异常。这个坑挺隐蔽的,建议节点名全部用常量集中管理,而不是裸写字符串。

3. 核心实现:从“报错日志”到“修复指令”的完整链路

3.1 验证节点:不只是跑一次命令那么简单

run_validation节点在很多人看来就是一个subprocess.run(),实则不然。代码生成场景有一个天然张力:你没法确定模型生成的代码需要什么解释器、什么依赖、以及用什么方式运行验证。如果验证方式本身不稳定,整个循环的反馈信号就是噪声。

我的做法是:先做一次“探测”——在验证代码前检查几件事:

  • 生成的文件是什么类型?.py、.js、.go,还是混合?
  • 有没有requirements.txt或package.json?
  • 项目有没有测试文件?还是只有一段独立脚本?

根据探测结果选择验证策略,比如 Python 代码优先收集py_compile的编译检查结果,其次是执行内置的测试用例;如果有 pytest 文件就优先跑 pytest,没有的话就做一个简单的冒烟执行。探测逻辑看起来繁琐,但能显著提升信号质量。

一个最小可用的验证节点大致长这样:

import subprocess import tempfile from pathlib import Path def run_validation(state: CodeGenState) -> CodeGenState: # 把当前代码落盘到工作目录,方便编译/运行定位行号 with tempfile.TemporaryDirectory() as tmp: workdir = Path(tmp) code_file = workdir / "generated_code.py" code_file.write_text(state["code"]) result = subprocess.run( ["python", "-m", "py_compile", str(code_file)], capture_output=True, text=True, timeout=30 ) raw_output = result.stdout + result.stderr state["raw_logs"].append(raw_output) state["attempts"] += 1 if result.returncode == 0: # 再跑一个冒烟执行,验证不是只过语法关 smoke = subprocess.run( ["python", str(code_file)], capture_output=True, text=True, timeout=15 ) state["passed"] = smoke.returncode == 0 if smoke.returncode != 0: state["error_feedback"] = smoke.stdout + smoke.stderr else: state["passed"] = False state["error_feedback"] = raw_output return state

这里有一个工程细节:py_compile只做语法检查,一次通过的代码往往运行时秒挂。所以必须加一层冒烟执行,哪怕没有正式的测试用例,至少验证“导入不报错、脚本能跑完”。如果项目本身带单元测试,直接把冒烟换成pytest -q,反馈信号的质量会再上一个台阶。

3.2 错误提炼节点:把“天书”翻译成模型看得懂的“处方”

跑完验证之后,手里是一堆原始日志。不建议直接把几百行 traceback 全量塞给修复节点——原因有两个:

一是上下文长度限制。多轮循环后,每条日志都推给模型,很快generation_history加上日志就会超出窗口,而且模型在超长上下文中更容易忽略真正关键的错误行。

二是错误主次不分。一份 traceback 里往往有“直接错误”和“根因错误”两层信息,模型要自己从原文里找根因,浪费一次推理不说,还容易挑到表面信息去修。

所以analyze_error节点的作用是:让模型把原始报错压缩成一种结构化的“错误处方”。我会强制模型按固定 JSON 格式输出:

{ "error_type": "AttributeError", "error_location": "line 23, in process_data", "root_cause": "DataFrame 列名不匹配,期望 col_a,实际只有 col_b", "suggestion": "读取文件后先重命名列,或改用列索引访问" }

解析 JSON 时我做了兜底处理:如果模型输出里混入了评论或 Markdown 代码块,先正则提取 JSON 片段,解析失败则整段原始文本作为纯文本继续传入修复节点。这是因为实测中模型偶尔会不按 JSON schema 输出,硬解析直接崩掉不合算。

3.3 修复节点:比“重写代码”多一层对比信息

修复节点和初始生成节点共享同一个模型接入方式,但系统提示词差异很大。生成节点拿到的指令是“根据需求写代码”,而修复节点看到的是三样东西:原始需求、当前代码、错误处方,并且被要求“只针对错误处方中提到的点做最小化修改”。

最小化修改这个要求,是我反复强调的核心。模型默认倾向是“重写整个文件”——一旦重写,原来已经验证通过的那部分逻辑又被推翻,下一轮验证可能冒出新错误。要让模型遵守这个约束,除了提示词之外,关键技巧是把上一版代码也放进消息里:

您上一轮生成的代码为: {previous_code} 最新测试结果指出以下問題: {error_feedback} 请基于最小改动原则修复代码,尽量保留已经能工作的部分。

previous_code从generation_history里取倒数第二个快照。实践证明,有了这个对比,模型“推倒重来”的概率大幅下降。

4. 循环控制与状态治理:防止 Agent 跑飞烧钱

4.1 最大尝试次数要配“自适应阈值”

我在第一节提过max_attempts,但想聊得更细。很多人直接写成固定 3 次或 5 次,实际用下来不够聪明——有些错误是“第一版就没抓住需求”,需要更多轮次;有些错误是纯粹的环境问题,跑多少次都没用。

我采用的方案是双阈值:硬性上限设为 6 次,前 3 次按正常修复逻辑跑;第 4 次开始时如果仍然失败,就会切换到一个“激进修复”参数——模型被要求可以考虑重构或者补充断言,但如果第 6 次还没过,直接停止。等于给了 Agent 两个阶段:先用保守方式修正,再允许放大范围重写,最后止损。

这个想法来自真实观察:代码 Agent 失败,通常不是一次能救回来的,但也不能无休止地自我怀疑。

def should_continue(state: CodeGenState) -> str: if state["passed"]: return "finish" if state["attempts"] >= state["max_attempts"]: return "finish_limited" # 走到一个专门的止损终点 return "fix_code"

finish_limited终点节点里我会额外输出一句话给用户:当前代码已经过了几次尝试仍未通过验证,建议人工介入。这句话看似简单,但在工程协作里很重要——它让调用方知道这是非正常结束,而不是 Agent 成功完成了任务。

4.2 Token 预算管理比循环次数更现实

很多人忽略一个问题:LangGraph 控制的是“节点跳转”,但每次模型调用的 token 消耗是累计的。哪怕只跑了 3 轮,如果每轮的代码都在 800 行、错误日志又是全量传入,总 token 数也可以轻松破十万。成本飙升之外,还有一个被忽视的问题:长上下文的注意力稀释。模型在长上下文里做局部修改时,经常改错位置。

我的 Token 预算策略有三条:

  • 传入修复节点的代码不是全量代码,而只传“相关代码片段”——办法是让 analyze_error 输出错误位置,然后用行号切片,取错误点上下 50 行。
  • 传入的日志最多保留最近 2 轮的错误摘要,更早的只保留一行标记(“第 2 轮曾出现某个类型错误”)。
  • 所有历史代码快照在状态里保留完整,但不直接进消息,由外部逻辑决定是否截断。

这样处理之后,每轮的 token 占用从“近乎全文”降到“局部上下文”,模型修复的准确率和成本同时得到优化——这是一个很少人提但在实际场景里非常有效的优化点。

4.3 状态治理:清理“脏”状态和并发的坑

LangGraph 图本身没有并发问题,因为它按顺序跑。但你要处理一个隐含场景:同一个 Agent 服务被多个用户调用,每个用户的 Session 内部状态不能串。这就要用到checkpointer和thread_id。

LangGraph 的检查点机制允许你把每次运行的状态快照持久化,之后用同一个thread_id恢复。我在服务化部署时是这么接入的:

from langgraph.checkpoint.memory import MemorySaver checkpointer = MemorySaver() app = builder.compile(checkpointer=checkpointer) config = {"configurable": {"thread_id": user_request_id}}

MemorySaver适合低并发演示或单机小流量。多实例部署时,换成 Redis 或 Postgres 检查点,防止进程重启丢状态。这个切换不用改图逻辑,只需换 checkpointer 实现,LangGraph 在设计上留好了口子。

另外要提醒一个状态设计的坑:TypedDict里的字段如果不小心写成非必填,并发的请求又复用了同一个键,可能出现状态残留。我的习惯是每次新的运行开始,显式调用reset_state()或者直接新建一个 dict 来初始化,防止前一次的error_feedback或code残留污染下一次运行。

5. 真实落地中的踩坑记录与调优经验

5.1 报错日志太长导致上下文爆炸——我如何用“摘要裁剪”救回来

第一次完整跑通闭环后,我把一个真实需求的 6 轮修正日志打印出来,发现第 5 轮传给模型的内容里,仅 traceback 文本就占了 1.2 万 tokens。而真正定位问题只需要其中的 4、5 行关键信息。

于是我在analyze_error节点里新增了一步:除了让模型输出 error_type、error_location、root_cause、suggestion 这四件套,我还要求它输出一条 100 字以内的one_line_summary。后续进入 fix_code 时,上下文里只传 summary 和前一次的错误类型,完整日志只在调试开关打开时透传。这看起来像个小改动,实际效果非常明显——修复准确率不降反升。

这里有一个值得学习的基础逻辑:反馈的质量不是由体积决定的,而是由“信噪比”决定的。模型在长篇日志里找重点的能力远远弱于你帮它标好重点后的表现,这个结论我在多个模型上都验证过。

5.2 修复反而引入新 bug——引入回归检测机制

自我修正 Agent 有一个特别让人头疼的“劣化模式”:第一次生成代码 70 分,验证失败后模型自信地改了一版,直接掉到 40 分。为什么会这样?因为模型只看到了当前错误,不知道之前哪些地方是对的,改着改着就把原来的正确逻辑破坏了。

我在修复节点加了一个“回归提示”后,情况改善了一些,但没有根治。后来我意识到,关键是建立自动回归检测——每次修复后,不仅要跑最新的测试,还要重放前几轮的测试用例。

回归测试设计: - 第 1 轮生成的代码,保存当时的测试用例(如果没有,就用冒烟执行结果作为基线)。 - 第 2 轮修复后,除了跑本轮的新测试,还要确保第 1 轮的基线用例不失败。 - 凡是回归失败的,标记为 regression_error,作为错误摘要的优先级最高项。

这样做的代价是验证时间变长,但换来的是发现问题后的“方向感”——模型知道自己不只是修一个点,还破坏了一个点,它下一次修复时会更收敛。

实际代码里我把它实现成一个regression_flag字段,当连续 3 轮都没有跑出新错误,而回归反复出现时,让 Agent 走finish_limited终止并请求人工介入。这个规则帮我们拦住了好几次模型“自我捣乱”的情况。

5.3 从“闷头跑”到“可视化调试”——LangGraph 提供的观测工具

刚开始跑图的时候完全是黑盒,你只知道最终结果不行,但不知道卡在哪一步。LangGraph 有一个很实用的功能:可以把整个执行过程的每一步状态流输出成文本日志。

我用的是 JSON 格式的状态转储,每次节点执行完,把attempts、passed、error_type、代码长度等关键字段打出来。这样排查问题的时候,不需要去猜,一眼就能看出循环是在哪个节点出状况:

=== Attempt 2 === generate_code: code_len=1243, passed=False run_validation: exit_code=1, error_type=ModuleNotFoundError analyze_error: root_cause=缺少 openpyxl 依赖 fix_code: reused_previous=True, code_len=1288

这个习惯帮我省了非常多时间。尤其在条件边判断逻辑出错时——比如should_continue的返回值大小写写错——没有可视化日志的话,到处加 print 调试会非常痛苦。LangGraph 自带的可视化画图功能(app.get_graph().draw_ascii())也值得用,虽然输出简陋,但能直观看到节点之间的连接是否符合预期。

5.4 不是所有错误都值得自动修——人工兜底范围的设计

最后聊一个比较“产品化”的心得。代码生成 Agent 并不是所有错误都应该由模型自己修,有些错误修多少次也没用,比如:

  • 依赖根本没装好(环境问题,Agent 内部网络又访问不了)
  • 需求本身有歧义(模型反复修也没法猜中用户意图)
  • 外部服务不稳定(测试失败是偶发性的,不是代码问题)

我在结构中加了一个“错误分类器”节点,在 analyze_error 之后跑一遍轻量的规则判断(不调用模型),识别出这几类问题就直接终止,不再进入fix_code。这个设计极大地提升了整体效率——原本 6 轮不仅烧 token,更重要的是用户等半天拿到一个错的答案;现在 1 轮就可以给出结论“这个问题不建议自动修复,原因如下……”。

人工兜底的另一个角度是保留“人工接管的入口”。当 Agent 走到finish_limited终点时,我把它的完整执行日志、代码快照、错误摘要打包成一个 JSON 结果返回给调用方。调用方可以决定是丢弃、还是让人修改后重新触发,或者作为“人工前置把关”的输入。这是工程落地时非常重要的一环——纯 Agent 的容错能力终究有限,但一个好的编排框架应该让“AI + 人”的组合比两者单独工作都强。

我的实践体会是,finish_limited从来不是失败,它是一个精心设计的安全出口。能在真实场景里控制住 Agent 的边界,比让它在“无限潜力”里继续烧钱重要得多。

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

MATLAB实现MSK/GMSK误码率仿真全链路指南

简介:本资源是一套面向通信工程专业本科生、研究生及MATLAB初学者的数字调制技术实践仿真包,聚焦MSK与GMSK两种关键恒包络调制方式的原理理解与误码性能验证。资源包含完整的MATLAB 2021a可运行代码(121个.m文件)、I/Q信道仿真结果…

作者头像 李华
网站建设 2026/10/29 7:40:04

Windows桌面端思维导图完整指南:配置、快捷键、布局与导出优化

在Windows上把思维导图当成日常生产力工具用,和偶尔打开网页画一张图,完全是两码事。前者意味着你要处理几十上百个节点的复杂结构、要反复导入导出、要在多个项目和多个文件之间来回切换;后者只要画得出来、看得过去就行。这篇东西讲的就是前…

作者头像 李华
网站建设 2026/10/29 7:39:31

Seedance 2.0:AI视频生成技术的核心突破与应用

1. 项目概述:Seedance 2.0的技术革命最近在数字内容创作圈里,Seedance 2.0这个工具突然火了。作为一个长期关注AI视频生成技术的从业者,我第一时间测试了这个号称"好莱坞级AI视频一键生成"的工具。说实话,它的表现确实超…

作者头像 李华
网站建设 2026/10/29 7:39:16

Gandalf靶场实战:AI安全对抗技术与防御策略

1. Gandalf靶场与AI安全实战背景这个名为"Tongue Tied Gandalf"的靶场是近年来AI安全领域极具代表性的实战训练环境。它模拟了大型语言模型(LLM)在实际业务场景中可能遇到的各种对抗攻击场景,特别是针对模型输出内容限制的绕过技术。我在实际测试中发现&a…

作者头像 李华
网站建设 2026/10/29 7:39:15

ADE:面向智能体开发的控制平面与多路调度架构

1. 从“写代码的IDE”到“调度智能体的ADE”:一次开发范式的静默迁移你有没有过这种体验:在VS Code里敲完一段Python,调用一个LangChain Chain,再跑通一个RAG流程——整个过程流畅得像呼吸,但回过头看,你其…

作者头像 李华
网站建设 2026/10/29 7:38:46

LSTM与GRU原理详解及金融预测实战对比

1. 为什么需要3分钟搞懂LSTM/GRU?在金融预测、语音识别这些实时性要求高的场景里,传统RNN的"记忆衰退"问题就像用漏水的桶打水——信息传递不到几步就流失殆尽。2014年提出的LSTM(长短期记忆网络)和其轻量版GRU&#xf…

作者头像 李华