AI Agent 写代码为什么容易失控?核心不是 Prompt,而是上下文工程

发布时间:2026/7/30 13:49:46
AI Agent 写代码为什么容易失控?核心不是 Prompt,而是上下文工程 生产级 AI Agent 写代码真正难的地方不是怎么写一句更聪明的提示词而是怎么管理它每一步看到的信息、能调用的工具、必须遵守的边界、以及做完以后怎么验证。很多人第一次用 Coding Agent都会经历一个很微妙的阶段。一开始很惊艳。你丢给它一个需求它会读文件、找依赖、改代码、跑测试甚至自己修复报错。看起来像一个不知疲倦的初级程序员。但多用几次以后问题就来了。它会把一个很小的 bug 修成一组重构会为了让测试通过顺手改掉测试本身会读错项目里的旧实现会把日志里的一行异常当成根因更麻烦的是它有时不是马上错而是第一步小错第二步补错第三步把错误合理化。很多人把这归因于一句话Prompt 没写好。这个判断不算错但太浅。生产级 AI Agent 写代码真正难的地方不是怎么写一句更聪明的提示词而是怎么管理它每一步看到的信息、能调用的工具、必须遵守的边界、以及做完以后怎么验证。这件事现在有一个更准确的名字上下文工程Context Engineering。Agent 和脚本的根本区别脚本是确定性的。你写一个脚本扫描目录、改文件、执行测试它的控制逻辑在代码里。哪一步读文件哪一步写文件哪一步退出都是人提前写死的。Agent 不一样。Agent 的控制逻辑有一部分交给了模型。它不是简单执行一段固定流程而是在每一轮根据当前上下文决定下一步做什么。一个最小的 Coding Agent大概可以抽象成这样while task_not_done: context build_context(goal, repo, memory, tool_results) action model.decide(context) result tools.execute(action) context.append(result) verify_or_continue()这个循环看起来简单但危险也在这里。因为 Agent 每一次决策都依赖当时的上下文。如果上下文里混进了旧日志、无关文件、过期设计、错误假设模型就会基于这些材料做出下一步判断。工具执行后的结果又会被塞回上下文成为下一轮判断依据。所以 Agent 的错误不是一次性错误而是循环系统里的状态污染。这也是为什么 Agent 比普通脚本更难管。脚本最多是你写错了逻辑。Agent 是它会自己生成下一步逻辑而且每一步逻辑都被上一轮观察影响。写脚本时我们主要关心代码有没有 bug。设计 Agent 时我们还要关心它看到了什么、没看到什么、误解了什么、被什么工具输出带偏了。Prompt 只是启动参数Context 才是运行时很多人以为 Prompt 是 Agent 的大脑。更准确地说Prompt 只是大脑收到的一部分输入。在真实的 Coding Agent 里模型每一轮看到的内容远不止用户那句话。它通常还会看到系统指令、项目规则、目录结构、最近打开过的文件、搜索结果、命令输出、测试失败日志、历史对话、工具定义、长期记忆、当前 diff。这些东西加起来才是 Agent 的运行时状态。这就解释了一个常见现象同一句 Prompt在不同项目里效果完全不同。不是模型性格变了而是上下文变了。如果一个仓库有清晰的 README、明确的测试入口、稳定的模块边界、规范的错误日志Agent 会表现得像一个靠谱同事。如果一个仓库里到处是历史包袱、重复实现、过期文档、没有测试、命名混乱Agent 很容易像一个刚入职又不敢问人的新人它会努力推理但推理材料本身就是脏的。这也是为什么“给 AI 多塞点资料”不一定能提升效果。上下文窗口不是无限工作台它更像模型的注意力预算。你塞进去的每一段内容都会和其他内容竞争注意力。高信号材料会帮它判断低信号材料会稀释判断。一个成熟的 Agent 系统不应该把整个仓库一股脑丢给模型而应该回答三个问题第一本轮任务必须知道哪些事实第二哪些材料只是可能有用应该通过工具按需检索第三哪些历史信息已经过期继续保留只会制造噪声这就是上下文工程和提示词工程的差别。提示词工程关心“怎么说”。上下文工程关心“让模型在什么时候看到什么”。为什么 Agent 会越改越乱Agent 写代码越改越乱通常不是因为它不会写代码而是因为循环系统缺少刹车。一个典型失控链路是这样的。第一步Agent 读到不完整上下文。比如你让它修一个接口超时问题它只读到了 controller 和 service没有读到网关超时配置也没有读到调用方重试逻辑。于是它把问题判断成 service 内部性能问题。第二步它生成一个局部合理、全局错误的计划。它可能会加缓存、改线程池、改 SQL甚至重构方法。但真正问题可能只是网关配置和客户端超时不一致。第三步工具权限过宽。如果 Agent 可以随意改文件、删文件、运行命令它就会用“完成任务”的视角行动。模型不会天然知道哪些文件是业务核心哪些脚本不能碰哪些测试不能为了通过而修改。第四步观察结果被误读。它跑了一部分测试看到通过就以为问题解决了。但局部测试通过不代表行为正确。更糟的是测试失败时它可能不是回到根因而是继续修补刚刚引入的新错误。第五步错误被写回上下文。一旦 Agent 把“我已经确认这是缓存问题”这样的错误判断带进下一轮后面的每一步都会围绕这个假设展开。它会越来越自洽也越来越偏。这就是为什么你会看到一种很熟悉的翻车方式Agent 很努力日志很多diff 很大解释也很完整但方向错了。技术上看这不是“AI 不认真”而是闭环控制系统缺少独立验证。如果生成计划的是它执行工具的是它解释结果的是它判断完成的还是它那整个系统其实只有一个脑子。这个脑子一旦进入错误轨道很难自己跳出来。工具边界比 Prompt 更重要很多团队在接入 Agent 时第一反应是写一大段系统提示不要乱改代码。不要删除文件。不要修改测试。先理解项目再动手。这些要求有用但不够。因为提示词是软约束工具权限才是硬边界。如果你不希望 Agent 删除数据库就不要给它直接执行危险 SQL 的能力。如果你不希望它改生产配置就不要把生产配置写权限暴露给它。如果你不希望它大范围重构就把写操作限制在明确文件集合里。一个技术判断很简单凡是你不敢让一个实习生直接做的动作也不应该直接开放给 Agent。真正的 Agent 工具设计应该按风险分级。只读工具风险最低比如搜索文件、读取文件、查看 git diff、查询日志。可逆写工具风险中等比如在工作区生成 patch、修改临时分支、创建草稿文件。不可逆或高影响工具风险最高比如删除文件、执行数据库变更、发布、改权限、触发生产任务。低风险工具可以自动执行。中风险工具要有 diff 和验证。高风险工具必须有人确认或者根本不暴露给通用 Agent。MCP 这类协议的价值也在这里。它不只是“让模型连更多工具”更重要的是把资源、提示模板、工具能力用统一接口暴露出来。接口统一以后团队才有机会做权限、审计、灰度和隔离。但要注意工具越多不等于 Agent 越强。工具集合臃肿以后模型会面临选择困难。两个工具功能重叠、参数含义模糊、返回结果很长都会增加误用概率。一个人类工程师都分不清该用哪个工具Agent 通常不会表现得更稳定。所以生产级 Agent 的工具设计应该追求三个标准工具职责单一。参数含义明确。返回结果短而有信息量。不要让工具返回几千行日志然后期待模型自己抓重点。更好的做法是让工具先结构化错误摘要、关键堆栈、失败测试、相关文件、建议下一步。Agent 不怕工具少怕工具乱。记忆不是聊天记录而是状态压缩很多人一听 Agent 记忆就想到“它能记住我说过的话”。对 Coding Agent 来说这个理解太消费级了。生产级 Agent 的记忆不是完整聊天记录而是可复用、可检索、可更新的工程事实。比如这个项目的测试入口是什么。某个模块为什么不能直接改。上一次排查已经排除了哪些方向。一次迁移任务当前完成到哪一步。哪个设计决策已经被团队确认。这些信息如果每次都靠模型从历史对话里捞不稳定也浪费上下文。更好的方式是让 Agent 在长任务中维护结构化笔记。你可以把它理解成 Agent 版本的工作日志任务目标修复订单导出超时 已确认事实 - 超时主要发生在导出超过 5 万条记录时 - Controller 层没有分页流式输出 - 网关超时为 60s客户端重试 2 次 已排除方向 - 不是数据库连接池耗尽 - 不是权限校验接口阻塞 下一步 - 检查导出链路是否支持异步任务 - 补充大数据量导出测试这种记忆的价值不是“模型更像人”而是把长任务拆成可延续的工程状态。长任务一定会遇到上下文窗口限制。即使窗口越来越大也不能指望把所有历史都塞进去。更可控的方式是压缩。压缩不是简单总结而是保留对后续决策有影响的信息丢掉已经不需要的原始输出。比如一次测试失败的完整日志可能有 2000 行但下一轮真正需要的只有失败用例名。异常类型。关键堆栈。疑似相关文件。是否由本轮改动引入。这就是上下文工程里的一个核心动作把原始观察变成决策材料。生产级 Agent 必须有 Verifier如果只能给 Coding Agent 加一个模块我会优先加 Verifier。原因很直接生成器不能同时充当最终裁判。Verifier 的职责不是继续写代码而是判断当前结果是否满足验收条件。它可以是自动化测试、静态检查、类型检查、安全扫描、规则校验也可以是另一个更窄职责的模型。关键是它必须独立于生成链路。比如 Agent 修改接口后Verifier 不能只问一句“你觉得修好了吗”。它应该检查相关测试是否运行。失败测试是否和本次改动相关。公开接口是否发生破坏性变化。配置文件是否被意外修改。新增代码是否绕过权限、日志、事务、幂等。diff 是否超出任务范围。这一步越工程化Agent 越稳定。很多 Agent 翻车的本质不是没有写出正确代码而是没有判断“什么时候该停”。没有停止条件的 Agent会把每一次失败都当成继续修改的理由。跑测试失败继续改改完又失败再继续改最后它可能已经偏离原任务很远。生产级系统必须设置硬停止条件最多迭代几轮。最多修改多少文件。最多扩大多少 diff。遇到哪些文件必须暂停。测试连续失败几次必须交还给人。这些限制听起来会降低 Agent 自主性但恰恰是让它可用的前提。自主性不是没有边界。没有边界的自主性在工程系统里通常叫事故源。一个能落地的 Coding Agent 分层如果把生产级 Coding Agent 拆开我建议至少分成七层。第一层是任务入口。它要把用户需求转成明确任务不明确就追问。比如“优化一下代码”不是好任务“把订单导出超过 5 万条时的接口超时问题定位并给出最小修复方案”才是好任务。第二层是 Router。它判断任务类型。是解释代码、生成测试、修 bug、小重构、跨模块迁移还是只需要查资料。不同任务应该进入不同流程。不是所有任务都需要 Agent 模式小任务用一次模型调用甚至更稳。第三层是 Planner。它先输出计划不直接写代码。计划里要包含读取哪些文件、验证什么假设、预计改哪些位置、什么情况需要暂停。第四层是 Context Manager。它负责把项目规则、相关文件、历史决策、工具结果组织成高信号上下文。这里最忌讳“全量塞入”。正确做法是先少量核心上下文再通过搜索、文件读取、测试结果逐步展开。第五层是 Tool Policy。它决定当前阶段允许哪些工具。探索阶段只读实现阶段允许受限写验证阶段允许测试和检查发布或数据库变更必须人工确认。第六层是 Verifier。它不写业务代码只做验收。它可以跑测试也可以检查 diff 是否越界。它的输出应该是结构化的通过、失败、阻塞、需要人工判断。第七层是 Rollback 和 Audit。所有高影响动作都要能追踪。改了哪些文件为什么改哪一轮改验证结果是什么失败时如何回退。这些东西不是“企业流程”而是 Agent 时代的基本安全设施。把这七层放在一起你会发现模型只是其中一个组件。真正的 Agent 系统是模型、上下文、工具、权限、验证和审计共同组成的工程系统。程序员应该怎么改变用法如果你只是个人用 Cursor、Claude Code、Codex 或其他 AI 编程工具也不需要一下子搭完整平台。但你可以立刻改掉几个习惯。第一不要上来就让它改代码。先让它读项目、列假设、给计划。你要看的不是它写得快不快而是它理解得对不对。第二把任务切小。“重构用户系统”这种任务很适合翻车。“把登录失败的错误码从字符串改成枚举并补充对应测试”就稳定很多。第三明确禁止范围。比如告诉它不要改数据库脚本不要改公共 API不要修改测试期望不要碰支付模块。更好的做法是在工具或工作流层面限制而不是只写在 Prompt 里。第四要求它先给 diff 摘要。你不要只看最终代码要看它改了哪些文件、每个文件为什么改、有没有超出任务范围。第五用测试结果约束它而不是用感觉约束它。让它跑具体测试让它解释失败原因让它说明哪些失败和本次修改相关。没有测试的项目Agent 的价值会下降很多因为它缺少环境反馈。第六长任务要让它维护笔记。尤其是迁移、排障、跨模块改造不要把所有历史都留在聊天里。让它维护一个任务笔记记录已确认事实、已排除方向、待办项、风险点。第七发现它开始大面积改动时停下来。大 diff 不是能力强的表现。很多时候大 diff 只是 Agent 失去边界感的信号。最后说一句实话AI 编程工具正在从补全代码走向自己读仓库、自己做计划、自己调用工具、自己验证结果。这个方向不会停。但越是走向 Agent越不能只讨论“哪个模型更强”。模型能力当然重要但在真实工程里决定可用性的往往是更朴素的东西上下文是否干净。工具是否清晰。权限是否收敛。验证是否独立。失败是否能回滚。日志是否能追踪。很多人用 AI 写代码越用越累不是因为 AI 没价值而是他们把 Agent 当成一个更聪明的输入框。Agent 不是输入框。它更像一个会行动的工程参与者。你不能只给它一句话然后期待它天然懂项目边界、业务风险、团队规范和生产事故代价。真正成熟的 AI 编程工作流不是让 Agent 替你做所有决定而是把它放进一个设计良好的工程系统里让它在该探索时探索在该执行时执行在该停下时停下在该交还给人时交还给人。未来程序员的竞争力也不会只是“会不会写 Prompt”。更重要的是你能不能把一个不稳定但强大的模型组织成一个可控、可验证、可交付的工程流程。这才是 AI Agent 写代码真正的门槛。