ARTICLE DETAIL

资讯详情

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

Agent 改代码,为什么总把仓库改坏?我读完了 Codex 的源码,问题出在「手」上

Agent 改代码,为什么总把仓库改坏?我读完了 Codex 的源码,问题出在「手」上 Agent 改代码为什么总把仓库改坏我读完了 Codex 的源码问题出在「手」上你大概也遇到过让 Agent 修一个小 bug它把无关代码删了或者改着改着文件只剩半截更离谱的是它把测试删了让测试变绿。多数人把锅甩给模型“这个模型代码能力不行。” 但我把 CodexOpenAI 的编程 Agent的源码读了一遍之后发现大部分模型不行的现场根因是编辑文件这个工具的设计不行。模型只负责想真正碰你仓库的是工具——手不行脑子再好也白搭。Codex 甚至为改文件这一件事专门造了一个独立的 cratecodex-rs/apply-patch/Rust 版本文基于 commit2685e3a4。一个专门做产品级 Agent 的团队认为改文件值得一个独立模块这件事本身就说明它不简单。这篇文章讲清楚三件事模型改文件为什么这么容易翻车、Codex 的方案特殊在哪、你自己造 Agent 时该怎么选。一、三种改文件方式失败代价天差地别主流方案就三种按模型输出与实际效果的耦合度排列方案工作原理典型失败模式失败代价整文件重写模型输出完整文件内容直接覆盖写入无关代码被丢掉输出被 max_tokens 截断文件半截损坏灾难级原始内容已被覆盖没有 diff 可回看search/replace 块Aider 风格模型输出旧文本块 → 新文本块工具做精确匹配替换旧文本多处出现改错位置缩进不一致匹配失败可恢复失败时文件未被改动patchdiff 类模型输出结构化补丁工具解析后应用行号漂移、上下文行对不上可恢复但失败率高时反复重试浪费轮次核心差异一句话重写把正确生成 100% 的内容作为前提patch 只要求正确描述 1% 的改动。文件越大整文件重写的失败概率越接近 1——不是模型变笨了是输出的每多一行都多一次出错机会而且任何一次截断都是不可逆损坏。失败代价的不对称才是选型的关键整文件重写失败 → 文件损坏靠 git 或备份恢复可能丢失用户未提交的工作search/replace 失败 → 文件原样不动错误信息直接喂回模型重试patch 失败 → 同样零副作用且错误信息 补丁全文都能喂回去。所以工程排序是能不用整文件重写就不用必须用的时候套上快照 校验护栏。还有一个反直觉的驱动因素模型天然偏爱整文件重写。你给它一个write_file工具它会很乐意用——重写比从记忆里精确摘出要改的那三行更容易生成。Aider 的做法最极端主循环里压根没有写文件工具只有 search/replace。这是用工具面tool surface约束模型行为的典型案例两个工具都给模型一定挑省事的重写只给结构化编辑工具重写这条路模型根本走不到。二、大多数产品的第一个坑让模型手写 unified diff很多团队的第一反应是“diff 是行业标准模型训练语料里到处都是直接让它生成 unified diff 最自然。” 然后产品就死在这里。unified diff 是为机器对机器设计的格式不是为模型手写设计的。它假设生成方已经精确知道目标文件的状态——这个假设对模型不成立。四个坑坑一行号漂移。hunk 头 -120,7 120,8 里的行号来自模型某次读文件时的快照而文件可能已经被它自己或并行的另一个 Agent、或用户的编辑器改过。看一个例子模型在第 100 行读到defcheckout(cart):itemscart.items()totalsum(p.priceforpinitems)# - 想改这行returncharge(total)它生成一份基于第 100 行的 diff -100,1 100,1 - total sum(p.price for p in items) total sum(p.price * p.qty for p in items)而此刻文件里第 100 行早就不是这行代码了——模型自己在前一轮插入了 4 行日志。更隐蔽的是模型对第 120 行的信仰远比对包含fn checkout的那一段的信仰脆弱。它天生不擅长数数但天生擅长认文本。坑二上下文行错位。diff 的容错机制是上下文行hunk 前后各 3 行不变内容但模型经常顺手修正上下文行——把实际的let x compute(v)写成它认为更合理的let x compute(v, ctx)。上下文行对不上整个 hunk 拒绝应用。坑三缩进与尾随空格。diff 是逐字节匹配的。模型写 Python 时把 4 空格缩进规范化、丢掉行尾空格、把 CRLF 当 LF——每一个都会让匹配失败而且失败信息hunk #2 failed对模型来说几乎无法自我诊断。坑四格式本身复杂。\ No newline at end of file、重命名标记、二进制 diff、多 hunk 行号联动——模型很少作为生成者见过这些细节。归因一下unified diff 把定位和内容耦合在一起行号 上下文行而模型恰恰在精确定位上系统性不可靠。解法于是清晰了把定位从行号换成模型可靠的东西——文件名 它刚读过的代码文本本身。这就是 Codexapply_patch的设计起点。一个反直觉的注脚模型生成 diff 不可靠但读懂 diff 很可靠。所以编辑工具的返回值里可以用 unified diff 展示给模型看Codex 的ApplyPatchFileChange::Update内嵌一份unified_diff供回显codex-rs/apply-patch/src/lib.rs:160——只是不让它当生产者。三、Codex 的答案apply_patchCodex 把改文件做成独立 crate源码结构本身就传达了设计决策parser 与 applier 彻底分离、错误是富类型、失败时保留已成功应用的部分的信息。3.1 自定义格式没有行号的 patch*** Begin Patch *** Update File: src/server.rs fn route_handler let req parse(request); - let resp handle(req); let resp handle(req, ctx)?; send(resp); *** Add File: src/ctx.rs pub struct Context; *** End Patch全部的魔法在两个标记上parser.rs:35-47是可选的上下文锚点——一行通常写类名、方法名或函数签名的代码后面跟着要替换的行块。没有行号。定位靠在文件里搜索这段文本而不是跳到第 100 行。对比 unified diff 的改进是结构性的定位靠锚点文本不依赖计数块内 old/new 行紧挨着写模型不用假装自己知道哪些行没变解析器容忍模型的坏习惯Codex 的解析默认跑在 Lenient 模式PARSE_IN_STRICT_MODE falseparser.rs:53甚至会剥掉模型误加的 heredoc 包裹——注释里明说这是为 gpt-4.1 的已知行为兜底。还有个容易被忽略的入口设计Agent 场景里模型有时会把补丁文本顺手当成普通文本输出或在没有声明的情况下试图执行。Codex 专门有一个错误类型ImplicitInvocation——检测到这看起来是一个补丁但没有显式声明用 apply_patch时返回的错误直接告诉模型该怎么改调用方式lib.rs:107。错误信息即使用说明这类细节最影响模型行为的稳定性。3.2 处理管线失败也分两种图解注意两个分叉——解析失败和应用失败是不同类型的错误前者连文件都没碰后者可能已部分写盘无论哪个分叉终点都是错误文本回模型而不是直接终止任务。parser 与 applier 分离还带来一个实用收益流式解析。StreamingPatchParser提供push_delta与finishstreaming_parser.rs:22/139/154模型还没输出完*** End Patch解析就已经在进行——大补丁的首字延迟明显下降。四、可靠性不靠一次成功靠降级链编辑工具的可靠性不来自永远一次成功来自失败时逐级降级 每级失败都可解释。Codex 的定位函数seek_sequenceseek_sequence.rs:12实现了严格度递减的三级匹配defseek_sequence(lines,pattern,start,eof):# 第一级逐字节精确匹配if(idx:exact_match(lines,pattern,start))isnotNone:returnidx# 第二级忽略行尾空白rstrip——模型最爱丢行尾空格if(idx:rstrip_match(lines,pattern,start))isnotNone:returnidx# 第三级两端空白都忽略——缩进风格漂移的容忍returntrim_match(lines,pattern,start)外加两个防御性细节源码注释里明确写着eoftrue的补丁块优先从文件末尾反向定位避免改文件末尾的补丁匹配到文件中部的相同代码pattern比文件还长时直接返回 None——这个守卫是 2025 年一次真实越界 panic 换来的。只有降级还不够。search/replace 最经典的坑是旧文本在文件里出现多次、改错位置。Codex 的对策不是检测到歧义就报错而是从机制上压缩歧义空间三件套块间有序UpdateFile的多个 chunk 必须按文件顺序排列parser.rs:73无序直接视为错误上下文锚点锚点先把搜索范围推进到函数/类附近搜索起点单调推进compute_replacementsfile_update.rs:87维护line_index每个 chunk 从上一个 chunk 的结束位置继续向后找——前面的重复文本天然不会被选中。五、错误信息设计整个闭环成败的细节报错回模型这一步的工程质量决定了闭环的成败。Codex 找不到匹配时返回的错误长这样file_update.rs:215 附近Failed to find expected lines in src/server.rs: let resp handle(req); send(resp);带上文件路径、带上模型当时写的 old_lines 全文。模型拿到的信息足以自己判断哦那一行我写错了或这段代码我记错了重新读文件。对比反例只返回patch apply failed。模型不知道是格式错、内容错还是文件变了只能原样重发三个轮次后开始幻觉。图解整个环里文件只在最后一步被写失败的尝试对文件系统零副作用——这是 patch 类方案相对整文件重写的根本安全优势。但错误信息再好也不能假设模型永远能修正。要给同一位置的编辑失败设重试预算经验值 2-3 次超限后不要让模型继续重试而是强制改变它的信息状态把目标文件的当前内容重新读进来塞回上下文让它基于事实而不是记忆重写补丁。Codex 失败信息里带文件路径正是为了支持失败后强制重读。没有重试预算的产品会遇到经典场景模型每轮重发几乎相同的补丁token 烧完用户看着轮次计数器发呆。六、安全手伸到哪里就要防到哪里编辑工具是唯一默认改用户资产的工具。最容易被路径校验已做的错觉掩盖的洞是链接workspace/innocent.c可以是指向~/.ssh/authorized_keys的软链——路径字符串校验完全拦不住hard link 更隐蔽补丁路径即使全部落在可写根内也可能是硬链到可写根之外的同 inode 文件。Codex 安全评估的注释明确指出所以有沙箱时 patch 仍要在沙箱里跑core/src/safety.rs:104。结论分两层工具层做路径规范化与符号链接策略OS 沙箱兜底 inode 级攻击。两层不可互相替代——字符串校验是必要的但 inode 级的攻击只能靠内核级沙箱挡。另外两个细节值得照抄一是apply_patch全程走文本通道二进制文件在读取阶段天然被拒二是它会保留每行的行尾符风格LF/CRLF新插入的行跟随文件主风格——否则改一次 CRLF 文件就制造一次全文件 diff 噪声污染 git blame。七、选型决策一张表收束产品形态推荐编辑原语必配护栏可以省略IDE inline 补全/编辑模型产片段编辑器做 diff 预览人工确认本身就是护栏沙箱、原子性机制CLI Agentsearch/replace 块或 apply_patch三级降级匹配、结构化错误、路径校验环境快照依赖 git云端自主 Agentapply_patch 类补丁OS 沙箱 整环境快照 失败重跑精细的交互式审批三个形态的共同不变量只有一条绝不让模型裸写整个文件。整文件重写只作为小文件的兜底路径存在且永远在可恢复的前提下。已有产品从整文件重写迁移到结构化编辑分三步走每步可独立上线先加护栏再加能力保留重写工具但写入前做新旧内容相似度检查——删除占比过高比如丢掉 30% 以上的行时拒绝执行。只改工具不改 prompt立刻消灭大部分灾难性丢失双工具并存 引导新增 search/replace 工具工具描述写清修改已存在的文件必须优先用本工具重写工具对超过阈值如 300 行的文件直接报错收敛把重写收缩为新建文件专用存量文件一律增量编辑——这就是 CodexAdd / Update / Delete三分的天花板形态。迁移期最大的坑是半信半疑既提供增量工具又允许重写却不做第 1 步的相似度检查——模型的重写失败模式一个不少还多了两套工具的维护成本。写在最后回到标题的问题Agent 改代码为什么总把仓库搞坏因为改文件看起来是已解决的问题实际是 Agent 工程里失败模式最密集的地方。三句话带走失败代价的不对称是选型第一视角整文件重写不可逆patch/search-replace 可恢复不要让模型手写 unified diff定位交给文件名 上下文锚点内容才是模型的活可靠性 降级链 结构化错误 重试预算而不是换一个更聪明的模型。我是源码派正在连载《编程 Agent 开发避坑指南》——12 章、46 张图讲清楚怎么造一个类 Codex 的编程 Agent上下文工程、文件编辑、沙箱审批、提示词注入攻防。每章都有源码证据Codex commit2685e3a4/ opencode v1.18.34。完整目录 可运行示例源码见我的掘金/知乎主页「源码派」。agent获取。首发声明本文首发于掘金/知乎/CSDN同名「源码派」转载请保留本声明与作者信息。这里写自定义目录标题一、三种改文件方式失败代价天差地别二、大多数产品的第一个坑让模型手写 unified diff三、Codex 的答案apply_patch3.1 自定义格式没有行号的 patch3.2 处理管线失败也分两种四、可靠性不靠一次成功靠降级链五、错误信息设计整个闭环成败的细节六、安全手伸到哪里就要防到哪里七、选型决策一张表收束写在最后欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注脚注释也是必不可少的KaTeX数学公式新的甘特图功能丰富你的文章UML图表流程图FLowchart流程图导出与导入导出导入欢迎使用Markdown编辑器你好 这是你第一次使用Markdown编辑器所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章了解一下Markdown的基本语法知识。新的改变我们对Markdown编辑器进行了一些功能拓展与语法支持除了标准的Markdown编辑器功能我们增加了如下几点新功能帮助你用它写博客全新的界面设计将会带来全新的写作体验在创作中心设置你喜爱的代码高亮样式Markdown将代码片显示选择的高亮样式进行展示增加了图片拖拽功能你可以将本地的图片直接拖拽到编辑区域直接展示全新的KaTeX数学公式语法增加了支持甘特图的mermaid语法1功能增加了多屏幕编辑Markdown文章功能增加了焦点写作模式、预览模式、简洁写作模式、左右区域同步滚轮设置等功能功能按钮位于编辑区域与预览区域中间增加了检查列表功能。功能快捷键撤销Ctrl/CommandZ重做Ctrl/CommandY加粗Ctrl/CommandB斜体Ctrl/CommandI标题Ctrl/CommandShiftH无序列表Ctrl/CommandShiftU有序列表Ctrl/CommandShiftO检查列表Ctrl/CommandShiftC插入代码Ctrl/CommandShiftK插入链接Ctrl/CommandShiftL插入图片Ctrl/CommandShiftG查找Ctrl/CommandF替换Ctrl/CommandG合理的创建标题有助于目录的生成直接输入1次#并按下space后将生成1级标题。输入2次#并按下space后将生成2级标题。以此类推我们支持6级标题。有助于使用TOC语法后生成一个完美的目录。如何改变文本的样式强调文本强调文本加粗文本加粗文本标记文本删除文本引用文本H2O is是液体。210运算结果是 1024.插入链接与图片链接: link.图片:带尺寸的图片:居中的图片:居中并且带尺寸的图片:当然我们为了让用户更加便捷我们增加了图片拖拽功能。如何插入一段漂亮的代码片去博客设置页面选择一款你喜欢的代码片高亮样式下面展示同样高亮的代码片.// An highlighted blockvarfoobar;生成一个适合你的列表项目项目项目项目1项目2项目3计划任务完成任务创建一个表格一个简单的表格是这么创建的项目Value电脑$1600手机$12导管$1设定内容居中、居左、居右使用:---------:居中使用:----------居左使用----------:居右第一列第二列第三列第一列文本居中第二列文本居右第三列文本居左SmartyPantsSmartyPants 是一个文本转换工具主要功能是将普通的 ASCII 标点符号自动转换为更美观的印刷体标点符号。例如原始符号转换后说明引号“引号”直引号变弯引号单引号‘单引号’直单引号变弯单引号--–两个连字符变短破折号---—三个连字符变长破折号...…三个点变省略号创建一个自定义列表MarkdownText-to-HTMLconversion toolAuthorsJohnLuke如何创建一个注脚一个具有注脚的文本。2注释也是必不可少的Markdown将文本转换为HTML。KaTeX数学公式您可以使用渲染LaTeX数学表达式 KaTeX:Gamma公式展示Γ ( n ) ( n − 1 ) ! ∀ n ∈ N \Gamma(n) (n-1)!\quad\forall n\in\mathbb NΓ(n)(n−1)!∀n∈N是通过欧拉积分Γ ( z ) ∫ 0 ∞ t z − 1 e − t d t . \Gamma(z) \int_0^\infty t^{z-1}e^{-t}dt\,.Γ(z)∫0∞​tz−1e−tdt.你可以找到更多关于的信息LaTeX数学表达式here.新的甘特图功能丰富你的文章2014-01-072014-01-092014-01-112014-01-132014-01-152014-01-172014-01-192014-01-21已完成进行中计划一计划二现有任务Adding GANTT diagram functionality to mermaid关于甘特图语法参考 这儿,UML图表可以使用UML图表进行渲染例如下面产生的一个序列图王五李四张三王五李四张三李四想了很长时间, 文字太长了不适合放在一行.你好李四, 最近怎么样?你最近怎么样王五我很好谢谢!我很好谢谢!打量着王五...很好... 王五, 你怎么样?关于UML图表语法参考 这儿,流程图链接长方形圆圆角长方形菱形关于Mermaid语法参考 这儿,FLowchart流程图我们依旧会支持flowchart.js的流程图语法Created with Raphaël 2.3.0开始我的操作确认结束yesno关于Flowchart流程图语法参考 这儿.导出与导入导出如果你想尝试使用此编辑器, 你可以在此篇文章任意编辑。当你完成了一篇文章的写作, 在上方工具栏找到文章导出生成一个.md文件或者.html文件进行本地保存。导入如果你想加载一篇你写过的.md文件在上方工具栏可以选择导入功能进行对应扩展名的文件导入继续你的创作。mermaid语法说明 ↩︎注脚的解释 ↩︎
返回列表