最近在带Agent开发时,我踩了一个大坑:Agent从代码库里扒了一段废弃代码当参考,然后照着那个过时API写了一大堆实现,跑起来全是编译错误。我连续调了三小时,才反应过来问题不在模型,而在上下文——我给了它一个“脏”上下文。这个坑让我总结了一套方法,我管它叫GSD(Goal-driven Context Sanitization),也就是目标驱动的上下文清洗。核心就一句话:别让Agent在脏上下文里写代码。
这篇文章不聊高深理论,主要聊聊我在实际项目里怎么治理Agent的上下文,包括什么是脏上下文、它为什么会破坏输出、以及一套已经验证可行的实操方法。如果你在用Cursor、Codex、Claude Code这类工具,或者自己在搭Agent框架,这篇文章应该能帮你少走不少弯路。
1. 什么是“脏上下文”,它到底怎么污染Agent的输出?
1.1 一个真实场景:Agent把坏代码当成了金科玉律
前两周我负责维护一个内部工具,需要Agent帮我把一个老模块从Python 2迁移到Python 3。我图省事,直接把整个仓库目录丢给Agent,让它自己找。结果Agent在仓库里找到一份好几年前的旧模块,里面充斥着xrange、iteritems这类早已消失的语法,更离谱的是它还把那份旧代码当成“标准实现”,用同样的风格开写新代码。最终生成的东西不仅不能运行,还把我们的代码风格带偏了。
事后复盘,根子就在我给Agent的上下文里塞满了“历史垃圾”。当时的目录里既有新旧版本共存的代码,又有几百行废弃的helper函数,还有一堆带# TODO: remove标记的测试函数。Agent的注意力机制不具备“这是废代码,不要看”的先验判断,它只会觉得所有出现在上下文里的代码都是同等重要的参考材料。
1.2 脏上下文的三大来源:代码库噪点、对话历史累积、工具返回失控
我总结下来,脏上下文主要来自三个地方,基本每个项目踩坑都是这三个源头在作怪。
第一个是代码库噪点。这是最常见的坑,包括各种旧版本文件、被注释掉的代码块、临时调试代码、第三方库示例里夹带的非标准写法,甚至还有藏在node_modules或venv里的一些示例文件。很多人喜欢把整个仓库打包给Agent,这和给一个刚入职的实习生扔一整个硬盘让他自己找资料没区别——他能找到,但大概率会找到一堆过期文档。
第二个是对话历史累积。在同一个会话里持续修改代码时,前面几轮的错误代码、失败日志、反复尝试过的方案都会留在上下文里。如果你中途不清理,这些历史“伤疤”会持续干扰模型判断。比如你让Agent先改A函数,改错了,接着让它改B函数,结果Agent可能还惦记着A函数的那套失败实现,把A的错误模式也带进B里。
第三个是工具返回失控。Agent通过工具调用拿到的返回内容,往往比我们想象的要大得多。比如一个grep -r可能返回几百个文件路径,一个cat file.js可能把几千行代码一次性扔进来,而实际需要修改的可能只有其中10行。如果不对工具返回做限制,这些无关内容就成了“噪音”,把真正重要的信号给淹没了。
这三类脏东西叠加起来,相当于你让Agent在一个堆满旧图纸的办公室里画新图纸——不是它能力不行,是你给的材料有毒。
2. 为什么Agent如此脆弱?从原理上理解上下文的重要性
2.1 注意力机制一视同仁,模型不会自动过滤“垃圾”
如果你理解Transformer的注意力机制,就能明白为什么上下文质量如此关键。Attention的计算本质上是对所有输入token进行加权求和,模型在训练过程中学会了关注哪些token之间有关系,但它并没有一个“这条信息是不是历史遗留”的标签。在你喂给它的文本里,一份2020年的废弃代码和一份当前规范代码,在注意力计算时是被同等对待的。
换句话说,模型缺乏“垃圾回收机制”。自动过滤噪点这种能力目前还得靠人来完成。既然模型不会过滤,那我们就得在上游帮它把脏东西摘干净。这正是GSD想解决的问题:与其指望模型变聪明,不如先把输入变干净。
2.2 上下文窗口是稀缺资源,垃圾多了,干货就没位置了
现在大家都在追大上下文,128K、200K听着很唬人。但仔细想想,窗口再大也是物理上限。你往里面塞了50K的日志和无关代码,剩下给有效代码的空间就只剩一丁点。即使有128K,一个大项目的核心业务代码可能也有几十万行,你根本塞不完,更别说模型对中间部分的注意力还会衰减。
我经常看到有人把一份几千行的文件直接cat给Agent,然后说“帮我改一下中间那个函数”。Agent确实看到了文件内容,但它要在这几千行里找到你指定的函数,还得记住前面的实现逻辑,再结合任务思考修改方案。有效的上下文被大量无关代码稀释,输出质量自然下降。我自己的经验是,在同等模型下,只给Agent一段20行的函数定义加调用处的30行上下文,要比给它整个800行文件的效果好得多。少即是多,在Agent上下文这里体现得淋漓尽致。
2.3 指令遵循的“最后指令效应”,上下文污染会造成指令矛盾
还有一个隐蔽问题:模型对后面出现的信息往往有更强的“服从倾向”,也就是所谓的近因偏好。当你把一坨字面意思矛盾的内容塞进上下文时,Agent很可能会优先采纳最后看到的那份“过时技术”或“错误纠偏”,导致行为混乱。
比如你在一开始明确说“不要使用旧API”,但上下文里某段历史代码使用了旧API,而这段代码恰恰紧邻任务描述,部分模型就会照葫芦画瓢。我遇到过Agent在我提供的示例里看到pkg_resources就跟着用,哪怕系统指令里明确写了“优先使用importlib.metadata”。因为它看到的实例代码离任务更近,权重更高。上下文不干净,连指令都变得不再可靠。
理解这几点后,你就会明白:治理上下文不是“锦上添花”,而是Agent代码质量的生死线。
3. GSD的核心方法论:给Agent一个干净的可执行上下文
3.1 什么是GSD:目标驱动的上下文清洗,而不是简单的删除
GSD的全称是Goal-driven Context Sanitization,目标驱动的上下文清洗。它的核心思想不是把所有信息减少到最简,而是围绕当前任务目标,把与目标相关的有效信息组织成高度可用的结构。就好比不是给实习生一台装满了全公司文件的电脑,而是把“本期任务”相关的三份文件放到桌面上,并把过期版本移走。
GSD和“简单压缩上下文”的区别在于:压缩是为了省token,而GSD是为了提升信噪比和指令一致性。省token只是副产品,真正的收益是模型能更准确地聚焦任务、更少被无关内容干扰。
3.2 步骤一:用任务描述替代模糊需求,让Agent知道该干什么
脏上下文往往始于一个模糊的任务描述。你如果只写“帮我改一下这个函数”,Agent就必须自己猜测你的目标,而猜测的依据就是上下文里那些乱七八糟的东西。所以GSD的第一步,就是先写出清晰的任务描述。
一个合格的任务描述应该包含:目标、输入、期望输出、约束条件和验收标准。举个例子。
目标:在 src/utils/dateFormat.js 中实现一个 formatDate 函数。 输入:Date 对象。 期望输出:格式化字符串 "YYYY-MM-DD HH:mm:ss"。 约束:不要修改其他文件;不要引入第三方库;使用纯 JavaScript 实现。 验收:运行 npm test -- date-format 通过所有用例。我在实际操作中,会把这段描述放在对话的最前面,作为整个上下文的锚点。这样即使后面穿插了代码片段,模型也知道一切内容都是为了服务这个目标。
3.3 步骤二:用代码检索代替“喂整个仓库”,只捞相关片段
很多人给Agent上下文的第一反应是“把整个项目目录拖进去”。这非常不推荐。正确的做法是先定位相关代码,再只喂相关片段。
我常用的方式是ripgrep。比如要修改某个模块,我会先搜索:
rg -n "formatDate|parseDate|dateFormat" src --type js然后根据搜索结果,只把符合的文件路径和行号抽出来,再通过查看代码片段决定带哪些部分。如果你在用Cursor或类似的Agent工具,直接在对话框里#引用文件也是可以的,但建议只引用和任务相关的文件,别把整个目录扔进去。
在Agent框架里,这个动作可以抽象成“限制检索范围”。比如在AutoGen或LangGraph里,给Agent的检索工具只开放src目录的权限,排除tests、examples、legacy这类容易混入噪声的路径。
3.4 步骤三:对检索到的内容做“去噪+归一化”,去除伪相关
这段代码真的需要全部放进上下文吗?不一定。在检索结果里,经常会有大段注释、被注释掉的旧实现、模板生成的无意义占位代码。这些都属于“伪相关”——看起来相关,实际只会增加干扰。
我在实际操作中,会给检索结果做一遍轻量清洗:去掉注释块(除非注释里有关键信息),去掉console.log/print调试语句,去掉明显废弃的代码分支,统一缩进和引号风格。这些操作可以用简单的脚本完成。
# 提取 src/utils/dateFormat.js 中第1-80行,并去除注释 sed -n '1,80p' src/utils/dateFormat.js | sed '/^\s*\/\/\s*/d'去噪之后,我会把代码片段放进上下文的“代码参考区”,并在前面标注来源路径和版本状态,比如:
[参考代码] 文件:src/utils/dateFormat.js (当前工作版本,第1-80行)这个标注很重要,它告诉Agent这段代码是“当前要工作的代码”,而不是“历史参考”。
3.5 步骤四:用结构化上下文模板把信息打包给Agent
与其让Agent从一串连续的文本中去“领悟”哪些是任务、哪些是参考、哪些是约束,不如把信息结构化。我会用Markdown模板组织上下文,保持一致的格式,这样每次Agent都能快速定位信息。
我常用的一套上下文模板如下:
## 任务 [一句话描述当前任务] ## 输入 [输入参数或文件内容的关键部分] ## 依赖代码 [与本次任务直接相关的函数、类或文件片段,标注来源] ## 约束条件 [禁止的行为、必须遵守的代码规范、不允许修改的文件] ## 验收标准 [如何判断结果正确] ## 示例 [如果可能,提供一个正确的输入输出示例]这个模板的好处有两点。第一,它能让模型快速建立“任务-代码-约束”的映射关系,减少歧义。第二,它把“参考代码”的位置固定下来,避免模型把参考代码误认为是任务本身。在实际项目中,我会为每个Agent任务生成一个这样的Markdown文件,然后让Agent读取这个文件作为主要上下文。
3.6 步骤五:控制对话历史长度,适时开启新会话并摘要上下文
一个会话里如果已经进行了很多轮对话,脏东西会越积越多。即使每一步你都小心翼翼,前面的失败尝试、报错信息依然会残留在上下文里。我的习惯是:每完成一个子任务,就开一个新会话,把上一个会话的结论浓缩成一个摘要,然后把摘要粘贴到新会话中作为上下文的一部分。
摘要模板也不复杂,就写三个内容:做了什么、最终结果是什么、还有哪些遗留问题。例如:
[会话摘要] 已完成formatDate的实现,测试通过3个用例。遗留问题:handleInvalidInput分支未覆盖,下一步需要补上。当前工作文件 src/utils/dateFormat.js。这样做能有效避免对话历史里的旧错误污染新任务。
4. 实操:在Cursor/VSCode/Codex中践行GSD的几个具体配置
4.1 在System Prompt里写清Agent行为和红线
System Prompt是你控制Agent行为的第一个抓手。我建议在System Prompt里明确写出与上下文治理相关的规则,尤其是“不要做什么”。我给自己的Agent设置了一套“红线提示词”,你们可以按需取用。
[System] 你是一名资深软件工程师。在开始任务前,请先阅读上下文中的所有代码片段,确认它们与任务目标相关。 规则: 1. 只使用上下文中明确标注为“当前版本”的代码,不要使用历史或废弃代码。 2. 如果上下文中的API信息不够确定,不要盲目调用,先搜索或询问。 3. 不要修改与当前任务无关的文件。 4. 输出代码时,请附带简短的修改说明和影响范围。 5. 如果上下文已经超过1000行,请提示用户精简,而不是继续基于大段上下文操作。这条System Prompt解决了很多我的痛点,特别是第1条和第5条。它可以强制Agent在做决定前意识到“这个代码可能是过期的”,也提醒上下文已经膨胀时需要精简。
4.2 用受控检索代替“全选粘贴”:以VSCode为例
很多人在VSCode里让Agent写代码时,习惯性地会把整个文件内容复制粘贴到对话窗口里。其实VSCode的搜索功能远比你想的好用。比如你要让Agent修改某个函数的实现,可以先用Ctrl+Shift+F搜索函数名,找到函数定义的位置和所有调用点。然后只复制相关行,而不是整页代码。
如果你用的是Cursor这类支持“引用文件”的编辑器,也可以直接在对话中@file引用,但要注意引用的文件数量。我通常限制在5个以内。超过5个文件,基本可以判断你的子任务划分得太大,需要拆分。或者在给Agent的上下文中,只保留这5个文件中与你改动相关的函数定义,其余部分用注释省略。
4.3 让Codex/Claude只读必要文件,而不是整个工作区
在使用Codex CLI或Claude Code这类工具时,默认行为往往是把整个工作区的文件索引都加载进来。虽然它们有层次的代码感知,但依然可能把你仓库根目录下的README.md里的示例代码也当成参考,导致风格跑偏。
我通常在工具的配置里指定一个allowlist,限制Agent只能访问src目录,排除掉legacy、examples、generated等目录。在Claude Code里,可以像这样配置:
{ "permissions": { "allow": ["Read(src/**)"], "deny": ["Read(legacy/**)", "Read(generated/**)", "Read(node_modules/**)"] } }在Codex CLI里,则可以在会话开始时显式告诉它“本次任务只关注src/utils下的文件,其他目录不需要读取”。这一步虽然简单,但能非常有效地把脏上下文的入口堵住。
4.4 用项目摘要文件(PROJECT_CONTEXT.md)作为最外层上下文
如果你的项目有一定的复杂度,光靠临时检索还不够。我会在项目根目录放一个PROJECT_CONTEXT.md文件,专门给Agent(和未来的自己)看。这个文件不是代码,而是对整个项目架构、技术栈、核心约定、废弃代码位置的描述。每次让Agent做任务前,先让它读这个文件。
# PROJECT_CONTEXT.md ## 项目架构 - src/:源码 - legacy/:旧版本,不可修改,仅作参考 - generated/:自动生成代码,禁止手改 ## 技术栈 - Node.js 18+,TypeScript - 使用importlib 规范,不支持CommonJS ## 约定 - API文档见 src/api/README.md,过期的API在docs/obsolete.md中标记 - 日期格式化使用 ISO-8601,不使用 Date.toLocaleString() ## 废弃代码位置 - legacy/legacyDateParser.js:旧的日期解析器,不要去读,也不要去借鉴有了这个文件,Agent就能在进入具体代码前先建立“什么可以看、什么不能看”的框架。我个人觉得这是性价比最高的上下文治理手段,它相当于给Agent画了一张地图,避免它在地雷区乱逛。
5. 常见问题排查:当Agent还是在脏上下文里乱写怎么办?
即使有了GSD,还是会遇到一些意外。这里我整理了几个高频问题和对应的排查思路。
5.1 Agent引用了不存在的API或函数
现象:生成的代码调用了某个函数,但在项目里根本没有定义。
根因:Agent在上下文里看到了一个过时的调用示例,以为该API仍然存在。这最常见于legacy代码或第三方库的旧版本文档被塞进上下文的情况。
解决:先检查你的上下文参考区是否混入了过期代码。用GSD模板重新组织上下文,并且加入一条System Prompt:“如果调用某个API前不确定它是否存在于当前项目中,请先搜索项目代码确认,不要盲写。”如果项目有类型定义,把类型定义文件(如.d.ts)也放进去。
5.2 修复一个Bug却引入两个新Bug
现象:Agent在修复某个问题时,改动范围超出了任务边界,结果出现了新的错误。
根因:对话历史里包含了之前的错误状态,Agent试图“修正”错误时,把不该动的代码也改了。或者上下文里的参考代码太多,Agent分不清哪些是依赖,哪些是它可以修改的区域。
解决:立刻开新会话,只保留当前错误信息和最少的上下文。新会话的任务描述改为“修复getUser函数中的空指针错误,只允许修改getUser函数体,其他函数只能读取”。这个限制能让Agent把注意力集中在目标区域。
5.3 Agent在VSCode写C代码没有代码提示
现象:Agent生成C代码时,在VSCode里没有任何智能提示,看起来像“瞎写”。
根因:VSCode的C/C++提示依赖compile_commands.json或c_cpp_properties.json,Agent生成的上下文缺少头文件路径和宏定义,导致IDE无法解析符号。本质上也是上下文不完整——你给了它代码,却没给它“编译配置上下文”。
解决:在给Agent的任务描述中,附加上编译配置的关键信息。比如“项目使用CMake,根目录包含compile_commands.json,头文件在include/目录下”。对于Agent生成的C代码,要求它确保#include路径正确。我在实际中还会让Agent先读取一遍compile_commands.json,再让它写代码。
5.4 Agent开始问无关问题或者跑偏
现象:你让它改一个函数,它突然开始研究项目里的其他模块,甚至问你要不要重构整个目录结构。
根因:工具的检索结果里包含了大量无关内容,Agent把“无关内容”当成了“潜在任务线索”。
解决:缩小工具返回范围。比如给搜索工具加上输出限制,只返回文件名+行号+相关代码片段,不返回全文。在Agent框架中,如果使用grep工具,可以约定返回值每行不超过200字符,并且只匹配与关键词出现的行。
5.5 常见问题速查表
| 问题现象 | 根因 | GSD解法 |
|---|---|---|
| 引用不存在的API | 上下文含过期代码 | 限制检索范围,加入“先搜索后调用”规则 |
| 修复A引入B新bug | 对话历史污染 | 新会话,隔离任务边界 |
| C代码无提示 | 缺少编译配置上下文 | 带入compile_commands.json/头文件路径 |
| 偏离任务,问无关问题 | 工具返回过多无关内容 | 控制返回行数,限定只返回相关片段 |
| 生成风格与项目不符 | 参考了legacy代码 | 在PROJECT_CONTEXT.md中明确风格规范 |
| 大段上下文导致漏改 | 有效信息被稀释 | 使用结构化模板,减少参考片段数量 |
6. 踩坑心得与进一步思考
6.1 别追求大上下文,小而精才是王道
我一开始也迷信“上下文越大,Agent记得越清楚”,后来发现完全不是这么回事。当上下文窗口塞满几万行代码后,模型往往会在代码的海洋里迷路。我做过一个对比实验:同一个任务是修复一个日期解析函数,方案A给Agent 6000行的整个工具模块,方案B只给Agent 40行的函数定义加20行测试代码。结果方案B不仅在更短时间内生成正确代码,而且改动范围非常精准。方案A则花了很长时间在无关的日志解析代码上纠缠。
这件事让我坚定了一个原则:上下文的大小应该以任务所需的必要信息为上限。能10行解决,绝不放100行。
6.2 上下文清洗要自动化,否则很难坚持
一开始我觉得手工清洗上下文太麻烦,每次都要先搜索、再筛选、再写模板。但用了几次之后,我发现这些步骤完全可以脚本化。我写了一个小脚本,传入一个需求描述,自动从代码库检索相关片段、剥离注释、生成GSD格式的Markdown文件。这样我只用把文件路径和任务描述写清楚,剩下的清洗工作交给脚本。如果你在用Agent框架,你还可以让“上下文清洗Agent”去做这件事——先让一个Agent负责检索和清洗,再把干净上下文传给你真正写业务代码的Agent。
6.3 参考吴恩达的Agent教程,把任务分解,每个子任务用独立上下文
吴恩达在Agent课程里反复强调过“任务分解”的价值。我后来把GSD和任务分解结合起来,效果非常明显。原来我让一个Agent一口气完成“重构整个模块”,它总是越改越乱。现在我会拆成几个子任务:先让Agent在干净上下文中梳理模块接口,再让它在第二个干净上下文中实现核心算法,最后让它在第三个上下文中补测试。每个子任务都是一个小而干净的上下文,互不污染。
这样做的额外好处是,你可以并行处理多个子任务。因为上下文隔离,每个Agent都是独立工作,不会互相干扰。
6.4 最后一点:把Agent当实习生,你提供的资料质量决定它的产出
经历了这么多,我最大的感触就是:Agent在当前阶段更像一个能力很强但经验不足的实习生。你给它一份整洁的任务说明书和几段相关代码,它就能交出像样的活;如果你给它一坨千头万绪的仓库转储,再指望它自己“提炼重点”,那你大概率会失望。脏上下文不是Agent的错,是我们没有尽到“管理上下文”的责任。
我自己在实际项目中把GSD当成日常流程后,Agent的产出质量提升非常明显。以前我总在改Agent烂代码和Agent反复横跳之间消耗精力,现在基本可以做到“给什么上下文,得到什么结果”。如果你最近也被Agent的“脑洞代码”折磨得头疼,建议从下一个任务开始,先清一清上下文,再让它动手。你会发现,很值得。