ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

LangGraph实战:构建具备自我修正能力的代码生成Agent

LangGraph实战:构建具备自我修正能力的代码生成Agent 1. 为什么“能跑”的代码生成 Agent 远远不够代码生成这件事很多人第一反应是“让大模型写一段不就行了”。我一开始也这么想直到把它放进真实项目里跑了两周才发现问题根本不在“能不能生成”而在“生成错了之后怎么办”。一个只会一次性输出的代码生成 Agent本质上就是个高级代码补全器它没有回头路没有自我怀疑更没有“我写错了我改”的机制。而 LangGraph 实战里最有价值的部分恰恰是给 Agent 装上“自我修正”这条回路。所谓“自我修正”的代码生成 Agent说白了就是让模型写完代码后自己先跑一遍、看一眼结果发现报错或者不符合预期就带着错误信息重新生成循环往复直到通过或者达到重试上限。这个思路听起来简单但真正落地时会遇到一堆细节问题错误信息怎么喂回去、重试几次合适、怎么防止它在同一个坑里反复横跳、生成和执行的边界在哪里。LangGraph 提供的状态图编排能力正好能把这条“生成—执行—反馈—再生成”的链路显式地画出来而不是藏在一堆 if-else 里。这篇文章适合两类人看。一类是已经用过大模型 API 做过代码生成、但被“一次性输出”坑过的开发者另一类是正在学 Agent 开发、想找一个有真实反馈闭环的练手项目的人。我会把整个 Agent 的图结构、状态设计、修正逻辑、踩过的坑全部摊开讲代码可以直接抄参数可以照着调。核心关键词就四个LangGraph、Agent、代码生成、自我修正后面所有内容都围绕它们展开。2. 整体架构设计把“修正”画进图里2.1 为什么选 LangGraph 而不是普通链式调用普通 LangChain 的 Chain 是线性的A 完了到 BB 完了到 C中间想根据执行结果回头重来就得自己写循环和状态管理。代码生成加自我修正天然是个带环的结构生成节点可能回到自己执行节点可能触发重新生成。LangGraph 的核心抽象是“状态图”节点是函数边是流转条件状态在节点之间传递和累积。你可以用条件边明确表达“如果执行失败回到生成节点如果成功走向结束”这比在代码里塞 while 循环清晰太多。我实测下来用 LangGraph 写这个 Agent图结构一眼就能看懂调试时也能清楚知道当前卡在哪个节点、状态里有什么。更重要的是它天然支持“检查点”和“中断”后面如果想加人工审核环节直接在图上插一个节点就行不用重构整个流程。这就是选它的核心理由修正逻辑是图的一部分不是藏在代码角落里的补丁。2.2 状态设计Agent 的“记忆”里该放什么状态是整个 Agent 的中枢设计得好不好直接决定修正能不能有效进行。我最终用的状态结构包含这几个字段task存原始需求描述code存当前生成的代码error存最近一次执行报错attempts记录重试次数history存历次生成的代码和对应错误status标记当前是成功还是失败。这里有个关键点history不能省。因为模型在第二次生成时如果只看到最近一次错误很容易改了一个错又引入另一个错把之前对的改坏。把历史错误一起喂回去它才能知道“这个坑我已经踩过了”。另一个细节是attempts的上限。我一开始设成 5结果发现有些简单任务模型第一次就对了有些复杂任务 5 次也修不好纯粹浪费 token。后来改成默认 3并且加了一个“如果连续两次错误信息完全相同就提前终止”的判断省了不少调用。状态字段的类型也要注意error用字符串存原始报错history用列表存字典每个字典包含code和error这样在生成节点里可以方便地拼成提示词。2.3 图结构总览四个节点两条回路整个图我设计了四个核心节点generate负责根据任务和历史生成代码execute负责在沙箱里跑代码并捕获结果evaluate负责判断执行结果是否算通过finalize负责整理最终输出。边的关系是generate无条件到executeexecute到evaluateevaluate根据状态走条件边——通过就到finalize不通过且没超次数就回到generate超次数也到finalize但标记失败。这样就有两条回路一条是正常的成功路径一条是失败重试路径。这里有个容易忽略的点evaluate节点不能只看“有没有抛异常”。有些代码跑是跑通了但输出结果明显不对比如要求算阶乘却返回了字符串。所以我在evaluate里加了一层简单的断言检查针对任务类型做基础校验。这一步不需要太复杂但能挡住不少“假成功”。图结构定下来之后后面所有代码都是围绕这四个节点和状态流转来写的。3. 核心节点实现生成、执行、评估的细节拆解3.1 生成节点提示词里必须有的三样东西生成节点的提示词设计是成败关键。我试过很多版本最后稳定下来的模板里必须包含三块内容原始任务描述、当前已有的代码如果有、以及历史错误列表。任务描述要原样传入不要自己改写因为改写可能丢失约束条件。当前代码在第一次生成时为空后续重试时传入上一版代码让模型知道“基于这个改”。历史错误列表则用清晰的格式列出每次的错误信息我用的格式是“第 N 次尝试错误信息”这样模型能看出错误有没有重复。提示词里还要明确告诉模型输出格式只输出代码不要解释不要 markdown 代码块标记。这一点非常重要因为执行节点需要直接拿代码去跑如果模型输出里混了“好的以下是代码”这种话执行就会失败。我一开始没强调结果模型十次有三次会加前缀后来在提示词里用加粗强调“只输出纯代码”并且加了一个后处理函数做兜底清洗才稳定下来。另外温度参数我设成 0.2太低会死板太高会乱改0.2 在代码生成任务里比较平衡。3.2 执行节点沙箱隔离与超时控制执行节点是整个 Agent 里最需要小心的地方因为你要跑的是模型生成的、未经审查的代码。我用的方案是子进程加超时把生成的代码写进临时文件用subprocess起一个独立进程去执行设置 10 秒超时捕获标准输出和标准错误。为什么不用exec直接在当前进程跑因为模型生成的代码可能包含死循环、大量内存分配甚至文件删除操作在当前进程跑等于把整个 Agent 暴露在风险里。子进程隔离虽然重一点但安全边界清晰。超时时间设 10 秒是权衡的结果。太短了一些需要安装依赖或计算量稍大的任务跑不完太长了一个死循环能卡住整个流程。我实测下来大部分代码生成任务的执行时间在 2 秒以内10 秒足够覆盖正常情况又能及时掐掉异常。捕获错误时要注意不仅捕获stderr还要捕获非零返回码因为有些错误不会打印到 stderr 但返回码非零。另外临时文件用完要删掉不然跑多了磁盘会堆一堆垃圾。3.3 评估节点什么算“通过”什么算“失败”评估节点的逻辑看起来简单其实最容易出问题。我的判断顺序是先看执行有没有超时或崩溃有就直接判失败并把错误信息写入状态再看返回码是否为零非零判失败最后看输出内容是否符合任务预期。第三步需要针对具体任务写校验逻辑比如任务是“写一个函数返回两个数的和”那就检查输出里有没有正确的计算结果。这一步不能太严否则模型稍微换个输出格式就被判失败也不能太松否则错误结果被当成成功。我踩过的一个坑是模型生成的代码里带了print语句输出里混了调试信息导致我的校验逻辑误判。后来我在评估节点里加了一步“提取纯结果”用正则把关键数值或字符串抠出来再比对。还有一个坑是浮点数比较直接用会因为精度问题判失败后来改成允许误差范围。这些细节看起来琐碎但正是它们决定了 Agent 是真的能用还是只是个玩具。3.4 修正回路错误信息怎么喂回去才有效修正回路的核心是“错误信息怎么组织”。我试过直接把原始 stderr 丢回去效果一般因为 stderr 里经常包含一堆路径和堆栈信息模型抓不住重点。后来我加了一个清洗步骤提取错误类型和错误消息去掉文件路径和行号中的临时目录部分只保留关键信息。比如FileNotFoundError: [Errno 2] No such file or directory: /tmp/xyz/data.txt清洗成FileNotFoundError: 文件不存在模型一看就懂。另一个技巧是在提示词里加一句“请分析错误原因只修改导致错误的部分不要重写整个代码”。这句话能显著减少模型“推倒重来”的概率。我对比过不加这句话时模型经常把之前对的逻辑也改掉导致错误越修越多加上之后它更倾向于做局部修改。还有历史错误列表不要无限增长我限制最多保留最近 3 次太早的错误信息参考价值不大反而占 token。4. 完整实操流程从零跑通一个自我修正 Agent4.1 环境准备与依赖安装先把环境搭起来。我用的 Python 3.10LangGraph 版本是 0.2.xLangChain 用 0.3.x模型接口走的是通用的 OpenAI 兼容格式。安装命令很简单pip install langgraph langchain langchain-openai如果你用的是其他模型服务把langchain-openai换成对应的包就行。这里要注意版本兼容性LangGraph 0.2 和 0.1 的 API 有差异网上很多老教程用的是 0.1 的写法直接抄会报错。我建议锁定版本在requirements.txt里写死langgraph0.2.60这种避免以后重装时行为不一致。另外执行节点需要用到subprocess和tempfile这两个是标准库不用额外装。环境变量里配好模型服务的地址和密钥我用的是OPENAI_API_KEY和OPENAI_BASE_URL这两个标准变量。如果你用的是本地模型把 base url 指向本地服务即可。配好之后先跑一个最简单的模型调用测试确认能通再往下走不然调试 Agent 时容易把模型连接问题和逻辑问题混在一起。4.2 状态定义与图构建代码状态用TypedDict定义这样类型清晰LangGraph 也能正确合并状态更新。核心代码如下from typing import TypedDict, List, Optional class AgentState(TypedDict): task: str code: str error: str attempts: int history: List[dict] status: str图构建部分先创建StateGraph传入状态类型然后添加四个节点再定义边。条件边用add_conditional_edges传入一个路由函数根据status和attempts返回下一个节点名。路由函数的逻辑是如果status success返回finalize如果attempts 3返回finalize否则返回generate。最后设置入口点为generate编译图。编译时可以传入checkpointer做持久化但初学阶段不传也行。这里有个细节节点函数的返回值只需要包含要更新的状态字段LangGraph 会自动合并。比如generate节点返回{code: new_code, attempts: attempts 1}其他字段保持不变。这个设计很省心不用手动维护完整状态。但要注意如果返回的字段类型和状态定义不一致会在运行时静默出错所以类型标注要认真写。4.3 生成节点的完整实现生成节点的函数接收状态拼提示词调模型清洗输出。提示词模板我用的是字符串拼接没有用 LangChain 的 PromptTemplate因为拼接更直观、调试更方便。核心逻辑是如果history为空提示词里只有任务描述否则把历史错误格式化后加进去。模型调用用ChatOpenAI设置temperature0.2max_tokens设 2000 足够大部分代码生成任务。输出清洗函数要处理几种情况去掉 markdown 代码块标记python 和去掉开头可能的“好的”“以下是”等前缀去掉结尾的解释性文字。我的做法是用正则匹配第一个代码块如果匹配到就取代码块内容如果没匹配到就按行过滤去掉明显不是代码的行。这个清洗函数我迭代了三四版才稳定建议你也多准备几个测试用例把模型可能输出的各种格式都覆盖到。4.4 执行节点与沙箱细节执行节点的核心是subprocess.run参数设置很关键。我用的是result subprocess.run( [python, temp_file], capture_outputTrue, textTrue, timeout10 )capture_outputTrue同时捕获 stdout 和 stderrtextTrue让输出是字符串而不是字节timeout10设置超时。超时会抛TimeoutExpired异常要单独捕获并写入状态。临时文件用tempfile.NamedTemporaryFile创建deleteFalse执行完手动删除。为什么要deleteFalse因为 Windows 上如果文件还被占用自动删除会失败手动删更可控。还有一个安全细节执行前检查代码里有没有明显的危险操作比如os.system、shutil.rmtree、subprocess嵌套调用。我加了一个简单的黑名单过滤匹配到就直接判失败不执行。这个过滤不能做到百分百安全但能挡住大部分低级风险。如果你要在生产环境用建议上更严格的沙箱方案比如容器隔离。4.5 评估节点与结果校验评估节点先检查error字段是否为空不为空直接判失败。然后检查返回码非零判失败。最后做任务特定的校验。校验逻辑我写成了一个独立函数根据任务类型分派。比如任务是数学计算就用eval安全地算一遍预期结果和实际输出比对任务是字符串处理就检查关键子串是否存在。这里要注意eval有风险只能用于你自己构造的预期表达式不能用于模型生成的代码。校验通过后把status设为success并把最终代码和输出整理好。校验失败时把错误信息写入error把当前代码和错误追加到historystatus设为failed。这里有个顺序问题history的追加要在判断之前还是之后我的做法是先追加再判断这样即使这次失败了历史里也有记录下次生成时能看到。如果先判断再追加失败的那次就不会进历史模型可能重复犯同样的错。4.6 跑通第一个案例写一个带边界检查的除法函数我用一个具体任务来演示整个流程写一个 Python 函数safe_divide(a, b)要求处理除零情况返回None而不是抛异常。第一次生成模型可能写出没有边界检查的版本执行时传入b0会抛ZeroDivisionError。执行节点捕获这个错误评估节点判失败错误信息写入状态。第二次生成提示词里带上“ZeroDivisionError: division by zero”模型就会加上if b 0: return None。再执行通过流程结束。这个案例我跑了十几次成功率在 90% 以上大部分任务两次以内能修好。失败的情况通常是模型第二次改错了地方比如把return None写成了return 0这时候第三次生成会带着新错误继续修。如果三次都没修好finalize节点会输出最后一次的代码和错误信息标记为失败让人工介入。整个流程的日志我建议打详细一点每个节点的输入输出都记下来方便复盘。5. 常见问题与排查技巧实录5.1 模型输出格式不稳定怎么办这是最高频的问题。模型有时候输出纯代码有时候加解释有时候用 markdown 代码块有时候用缩进代码块。我的应对策略是三层清洗第一层用正则匹配 markdown 代码块匹配到就提取第二层按行过滤去掉以“好的”“以下”“注意”等开头的行第三层检查剩余内容里有没有def或import等代码特征没有就判为无效输出触发重新生成。三层下来格式问题基本能解决。如果还不行就在提示词里用更强的语气强调比如“你的输出将被直接执行任何非代码字符都会导致失败”。5.2 修正回路陷入死循环怎么破死循环的表现是模型反复生成同样的错误代码错误信息也一模一样。我的解法是加一个“错误指纹”检查每次评估失败时把错误信息做哈希如果和上一次的哈希相同说明模型没改对地方直接终止循环不再浪费调用。这个检查放在评估节点里命中就设status stuck路由到finalize。另外attempts上限设 3 也是硬性保护超过就停。我实测下来大部分死循环发生在任务描述有歧义的时候模型理解偏了怎么改都改不对。这时候与其让它硬修不如把任务描述改清楚重新跑。5.3 执行超时和资源占用怎么控制超时问题前面提过10 秒是经验值。但有些任务确实需要更长时间比如涉及大量循环计算。我的做法是给任务加一个“预期复杂度”标记简单任务 5 秒中等任务 10 秒复杂任务 20 秒。这个标记由人工在提交任务时指定不靠模型判断。资源占用方面子进程默认继承父进程的资源限制我额外加了内存限制用resource模块设置RLIMIT_AS防止模型生成的内存炸弹代码把机器拖垮。这些限制在 Linux 上有效Windows 上支持有限跨平台部署时要注意。5.4 常见问题速查表问题现象可能原因排查方向解决手段模型输出带解释文字提示词约束不够检查提示词是否有“只输出代码”加强提示词加后处理清洗执行报文件找不到临时文件路径问题检查临时文件是否创建成功用绝对路径执行前确认文件存在修正后错误变多模型重写了整个代码检查提示词是否要求局部修改加“只改错误部分”约束死循环不终止错误信息重复检查错误指纹是否相同加哈希比对相同则终止执行超时代码有死循环或计算量大检查代码逻辑和任务复杂度调超时时间加复杂度标记评估误判成功校验逻辑太松检查校验条件是否覆盖边界加边界用例提取纯结果比对5.5 几个我踩过的坑和对应技巧第一个坑是状态字段类型不一致。我一开始attempts有时是 int 有时是字符串导致路由函数比较时出错。后来严格用 int并且在节点函数入口做类型断言问题就没了。第二个坑是模型调用超时。网络不稳定时模型接口可能几十秒不返回整个 Agent 卡住。我加了request_timeout30参数超时抛异常捕获后当作生成失败处理重试一次。第三个坑是历史记录太长导致提示词超长。我限制history最多 3 条每条只保留错误类型和消息不保留完整堆栈token 消耗降了一半。还有一个技巧是给生成节点加“温度衰减”。第一次生成用 0.2第二次用 0.1第三次用 0.05让模型越来越保守减少乱改的概率。这个技巧在多次重试场景下效果明显尤其是任务描述比较严格的时候。另外finalize节点输出时我不仅输出最终代码还输出尝试次数和历次错误摘要方便人工判断是任务太难还是模型能力不够。这些细节看起来小但积累起来就是 Agent 能不能真正落地的差距。6. 扩展方向从单 Agent 到多 Agent 协作这个自我修正 Agent 跑通之后我试着往两个方向扩展。一个是加“代码审查”节点在生成和执行之间插一个独立的模型调用专门检查代码质量和潜在问题审查不通过就直接打回生成不浪费执行资源。另一个是拆成多 Agent一个负责生成一个负责执行一个负责评估各自独立提示词和模型参数通过 LangGraph 的状态图协作。多 Agent 的好处是每个角色可以专注自己的任务提示词更精简但代价是调用次数增加延迟变高。我实测下来单 Agent 加自我修正已经能覆盖大部分代码生成场景多 Agent 更适合任务复杂、需要多轮讨论的场景。如果你刚开始学 LangGraph建议先把单 Agent 的修正回路吃透再考虑拆分。另外这个架构稍作修改就能用到其他领域比如 SQL 生成加执行校验、配置文件生成加语法检查核心思路是一样的生成、执行、反馈、修正。把这条回路跑通你对 Agent 开发的理解会上一个台阶。
返回列表