前几天我接了个看上去很小的需求:给某个库存管理后台的订单列表加上按仓库维度筛选。我打开常用的编程 Agent,把需求丢进去,让它先看一下订单模块的代码。前面两步它表现得很正常,顺利找到了订单查询入口。但改到第三轮,它突然开始把筛选逻辑往一个早已废弃的旧查询方法里塞,还一本正经地告诉我“这里已经是新链路的唯一入口”。我盯着它给出的引用路径,确认它访问的确实是另一个模块的文件——那一刻我才意识到,它不是偷懒,是真的“忘了”我们前两轮已经绕过了那个废弃方法。
这种“跑着跑着就失忆”的现象,在百万行规模的仓库里几乎是常态。问题不在模型智商,也不在某款工具的质量,而在上下文工程:你有没有用正确的方式,把最重要的信息持续放在模型能看见、且看得清的位置。这篇文章我打算把背后的机制、我自己摸索出来的完整流程、以及对比过几种主流做法后的实测结果一次讲透。适合正在深度使用编程 Agent 做日常开发、尤其是工作仓库规模比较大的朋友参考,也能帮刚开始接触这类工具的人少走弯路。
1. 先聊聊「失忆」是怎么回事
1.1 上下文窗口:一块会被写满的白板
编程 Agent 的“记忆”不像是人类的长时记忆,更像一块白板。模型每次推理时,能看到的全部信息都被限制在一个叫上下文窗口的范围内,单位是 token。这个窗口里面放着什么,模型就基于什么推理;窗口外面的内容,对它来说等同于不存在。
现在的模型窗口已经做得很大,标称值动辄几十万甚至上百万 token,听起来很宽裕。但只要你换算一下就知道了:一个典型业务文件 300~600 行,折算成 token 大约有 1500~3000;一个百万行仓库通常有几千个文件,全塞进去就算窗口装得下,模型也没有能力对所有内容保持同等关注。更何况 Agent 每执行一轮操作,都会产生新的工具返回——搜索结果是 token、读取文件的输出是 token、命令执行日志也是 token。这些内容会源源不断地写入白板,最早写上去的重要约定,很可能在某个时刻被悄悄挤到窗口边缘,甚至直接被截断。
我常用一个类比:上下文窗口是火车站里的临时寄存柜,不是身后的书架。你可以临时放几件随身行李,但不可能把整个书房搬进去。编程 Agent 在大型仓库里工作,本质上就是在做一个“柜子不够用、东西又太多”的收纳难题。
1.2 三种典型失忆现场
我复盘了自己和周围不少开发者踩过的坑,编程 Agent 的“失忆”大体逃不出下面三种表现:
第一种是跨文件契约失忆。改接口 A 的时候,Agent 在前面几轮已经确认过接口签名是queryByWarehouse(warehouseId, page)。改到后面的调用方文件时,它又按旧签名写成了queryByWarehouse(whId, limit),理由是“根据对现有代码的理解”。可这个现有代码它明明读过,只是那个信息已经在窗口里被冲淡了。
第二种是任务目标漂移。原本目标是“给订单列表加仓库筛选”,Agent 改着改着开始顺手“优化”了周边几个无关函数,还重构了一处它觉得“写法不太优雅”的工具类。最初的约束条件被推到窗口深处之后,模型会越来越倾向于执行它自己认为合理的操作,而不是执行你最初交代的操作。
第三种是风格约定消失。你明确定义过错误处理要用if err != nil的提前返回风格,前几轮它也遵守了。等到改到第四个文件时,它突然换成了try/catch风格,而且因为窗口里已经没有当初那句约定了,你还很难跟它“讲道理”。
这三种失忆的共同点是:不是模型能力不行,而是信息在上下文窗口中的“位置权重”发生了变化。最近的、刚刚写入的内容权重高,早期的、被大量中间内容淹没的内容权重低。这就引出了下一个关键结论。
1.3 为什么长上下文不等于长记忆
很多人以为失忆的解决方案是“把窗口调大”。我试过,效果有限。窗口变大后,模型确实能看到更多 token,但它对长文本中段内容的注意力会明显衰减,这个现象在业界有专门的说法,叫“注意力中间丢失”。也就是说,一份 10 万 token 的对话历史,模型真正有效利用的部分可能只有首尾两端的几千 token。中间的内容更像“似曾相识的噪声”。
这和人很像:你周一听了一场长会议,周五还能清楚记住开头定下的目标和结尾的待办事项,但中间过程早就模糊了。模型在长对话里的近因效应比人类更严重,它几乎总是优先相信最近几步工具调用的输出,至于你前三轮强调过的“不要动缓存刷新逻辑”,可能早就成了被遗忘的旧闻。
所以,真正的上下文工程目标不是“塞更多”,而是精确控制“什么信息在什么时候出现在窗口里的哪个位置”。模型本身不会替你安排优先级,这件事必须由你来设计。
2. 百万行仓库:难点从来不是行数,而是「上下文地图」太大
2.1 三层复杂度:文件内、模块间、架构级
百万行仓库难处理,不是因为代码行数多,而是因为信息分成三个层级,每一层都可能截断 Agent 的视野。
第一层是文件内复杂度。一个上千行的核心文件里,状态变化、数据流、函数调用关系盘根错节。Agent 如果只读了文件的一部分就动手,大概率漏掉关键状态分支。
第二层是模块间复杂度。订单列表改个筛选条件,看起来只动一个 Service 方法,但这个方法可能被库存锁定、对账报表、消息推送同时调用。改动牵一发动全身时,Agent 必须能意识到“影响面”,而影响面信息散布在多个文件里,天然不利于聚焦。
第三层是架构级复杂度。目录划分逻辑、服务边界、领域模型中的概念约定、技术规范,这些通常不会写进某一个具体文件,而是散落在各种文档、注释和历史提交里。Agent 在缺少这张“架构地图”的时候,会把一次局部改动做得很漂亮,但完全没有遵守项目的基本架构约束。
百万行仓库的可怕之处,是这三层复杂度同时叠加:一个“局部正确”的修改,随时可能破坏远处某个隐藏假设。
2.2 大型仓库为什么特别容易触发失忆
在小仓库里,Agent 就算不读上下文,凭模型自带的编程知识也能把改动猜个大概。但百万行仓库不行,它有很多历史包袱和僵尸代码。举个例子,我那个库存后台里,订单查询存在新旧两套链路,旧链路被流量切走后没有再被使用,但代码还留在原处,注释也还挂着。Agent 在快速检索时,很容易把这条废弃的旧链路当作唯一正确路径。
同时,仓库越大,Agent 自己找东西的信息成本就越高。每次关键词搜索都会返回大量结果,它把这些结果一股脑读进上下文,重要信息很快被淹没。更致命的是,大型仓库中同名函数、相似模块比比皆是,检索命中率很高,但“命中的不是你要的那个文件”的情况也经常发生。于是 Agent 基于残缺和错误信息,构建出一个“自信的错误模型”,改出来的东西错得特别认真。
2.3 检索代替记忆:看似聪明,实则埋雷
有些人会提出一个思路:既然窗口装不下,那就别喂了,让 Agent 自己搜索、自己读、自己找。这个说法理论上成立,但实操中检索本身就是概率行为。关键词选得不准、搜索排序不理想、返回的 Top-K 结果没有覆盖真正的依赖文件,任何一个环节出错,Agent 就会在信息不全的前提下开工。
我见过最典型的翻车现场就是:Agent 搜索“order list query”找到了一个只读配置类,它误以为这就是查询入口,然后围绕这个配置类写了一套筛选逻辑,最后编译直接失败。检索可以作为辅助手段,但不能作为唯一的上下文来源。上下文工程的核心思路,其实是在检索之前就缩小搜索范围、提供权威入口,让 Agent 不需要在千万个文件里碰运气。
3. 上下文工程不是「多喂代码」,是「把 Agent 当新同事带」
3.1 一套任务说明书应该包含哪些字段
如果你招了一个新同事,让他去改订单模块,你会怎么交代?你一定会告诉他:背景是什么、目标是什么、哪些文件是关键入口、哪些地方别碰、怎样算完成。但很多人用编程 Agent 的时候,却只丢一句话“帮忙给订单加个仓库筛选”,然后就期待它能自己搞定一切。
上下文工程的第一个动作,就是把“给新同事交代任务”的形式固定下来,写成一份任务说明书。我自己的模板大致长这样:
## 任务目标 - 在订单列表查询中支持按仓库维度筛选,返回结果按仓库ID过滤 ## 约束条件 - 不能破坏库存占用逻辑 - 不改变订单列表现有 API 的返回结构字段 ## 成功标准 - 新增筛选条件后,单测覆盖正常、空仓库、非法仓库三种情况 - 相关接口手工验证通过 ## 不做范围(Out of Scope) - 不改 UI 样式 - 不重构订单模块其它无关方法 ## 权威入口文件 - 订单查询 Service:internal/order/service/query.go - 仓库元数据:internal/warehouse/model/warehouse.go - 缓存刷新:internal/order/job/refresh.go(只读,禁止修改) ## 已知坑 - 搜索「OrderList」会命中废弃旧链路,新链路入口统一走 order/service/query.go写这个文档一般花我五分钟,但它解决的问题非常大。它把目标、边界、入口、坑都放到了窗口最靠前的位置。模型每次推理时都能看到任务书的完整内容,相当于给自己建立了一个“永不删掉的会议纪要”。
3.2 四字原则:少喂、显式、新鲜、指路
深化任务说明书的设计时,我总结出四个字:少、显、鲜、指。
少,是少喂。只给与当前任务相关的核心信息,不要因为“反正窗口够大”就把无关代码也粘进去。窗口内的信息是互相竞争的,垃圾多了,精华就会被挤掉。
显,是显式。任何约定都要落到文字上,不能依赖模型“自己看出来”。比如“注意仓库筛选要优先走缓存,不要直接查库”必须写出来,模型不会因为你贴了一段缓存代码就自动推断出这个规则。
鲜,是新鲜。仓库在持续演进,旧架构文档可能已经过时。与其相信一篇半年前写的设计说明,不如让 Agent 基于当前代码现场精读关键入口。过期信息比没有信息更有害,因为它是“自信的错误”。
指,是指路。你不是要把所有代码塞进窗口,而是告诉 Agent“先读哪个文件、跳过哪些文件、哪个目录别碰”。这一步能把 Agent 的探索路径从“全仓库搜索”缩短为“定点阅读”,效果立竿见影。
3.3 上下文预算:给每个会话设定信息配额
我后来把上下文当成预算来管理,每次会话开始前先做一个粗略的额度分配。比如一个常见中型改造任务,我会这样分配:
| 信息类别 | 预算占比 | 说明 |
|---|---|---|
| 任务说明书 | 约 5% | 目标、约束、成功标准 |
| 核心文件精读 | 约 50% | 只精读相关函数的上下文片段 |
| 依赖接口说明 | 约 15% | 外部依赖只保留接口签名与返回值约束 |
| 进度摘要 | 约 10% | 每轮滚动更新的“当前状态” |
| 工具输出余量 | 约 20% | 编译、测试、搜索返回等动态信息 |
这个预算不是精确计算,而是提醒自己:窗口资源是稀缺的,每次往里面塞东西前,先问一句“这个信息真的有必要吗”。实际工作中,我发现预算失控最常见的原因是“顺手多贴了几个文件”。多贴一个文件看似无关痛痒,但多贴十几个之后,核心目标就被挤出高权重区域了。一旦发现预算超标,正确做法不是继续挤,而是开新会话,并把进度摘要迁移过去。
4. 我在大型仓库里用的一套上下文管理流程
4.1 第一步:启动会话前,先写任务说明书
不管任务大小,我开工前都会先花几分钟把任务说明书写好。可能有人觉得小题大做,但我的实测经验是:写任务书花五分钟,省下的是后面两小时返工。特别是那些“看起来很简单”的需求,最容易因为上下文不清而被 Agent 带偏。
举个具体例子,还是那个“订单列表加仓库筛选”的需求。我的任务书里明确写了三条约束:不能改库存占用逻辑、不改变 API 返回结构、不做范围外重构。这三条写下来,基本就杜绝了 Agent 在后期随意“自由发挥”的可能性。
4.2 第二步:生成仓库地图并固化为持久化说明文档
第一次在一个大型仓库里做改动时,我会让 Agent 花一轮时间输出一份“仓库地图”:目录结构、构建入口、测试命令、核心模块位置、常见命名习惯、废弃代码清单。这份地图我会手动整理后放进项目根目录下的 AGENTS.md 文件里。
之后每次新会话的第一条指令都是“先读 AGENTS.md”。这个动作的意义是:把第一轮探索的成果固化为跨会话记忆,而不是每次从零开始。很多人的 Agent 每开一个新会话就“失忆”,根源就在于探索结果没有被沉淀下来。仓库地图就是 Agent 的“长期记忆外挂”,它不占对话窗口,但每次都能被快速载入。
AGENTS.md 里不写大段废话,只写高价值信息。例如:“构建命令是make build;单测入口在pkg/service/**/*_test.go;搜索 OrderList 时注意跳过 legacy 目录;所有仓库查询必须先读 warehouse 元数据缓存”。这些都是后面所有会话共享的“项目常识”。
4.3 第三步:让 Agent 复述方案,确认后再动手
这一步可能是我所有流程里性价比最高的一个动作。Agent 读完任务说明书和相关代码后,我不让它直接改,而是先输出一份简短方案,内容包括三块:它理解的改动步骤、涉及的文件清单、识别出的风险点。
刚开始我嫌麻烦,后来发现这个“复述验证”能拦截掉至少一半的幻觉。比如有一次,Agent 在复述方案里写“我会通过修改orderCache的过滤逻辑来实现筛选”,但我明明确确在任务书里标了“缓存刷新文件只读、禁止修改”。如果没有复述环节,它可能已经动手改坏了缓存模块。现在我会让它在动手前把所有“要改什么、要碰什么”先亮出来,我确认无误后才放行。
4.4 第四步:原子化改动 + 变更摘要滚动
即使 Agent 整体方向正确,长时间连续干活也仍然会慢慢“失忆”。我用的对抗手段是把任务拆成若干原子步骤,每完成一小步就让 Agent 输出一次变更摘要。摘要格式固定为三行:改了哪些文件、改动原因、验证方式。
比如订单筛选这个任务,我会拆成:先改查询 Service 增加筛选参数;再改仓储层查询逻辑;最后补单测与接口验证。每完成一步,都让 Agent 把“已完成”的事项和“下一步要做”的事项明确写出来。这个滚动摘要的作用,是让最重要的状态信息始终以最新鲜、最靠前的形式出现在窗口里。
如果会话拖得太长,我甚至会让 Agent 在确认进度摘要后,把摘要内容粘到任务说明书下方,然后新开一个会话继续。这种“中途换车、信息接力”的做法,能有效避免长对话后期的注意力中间丢失问题。
4.5 第五步:收尾阶段的影响面检查
Agent 终于说“改完了”,这时候还不能收工。我的固定动作是让它执行一次只读的影响面检查:列出本次涉及的文件清单,并对比任务说明书里的“不做范围”,确认没有触碰无关模块。
具体来说,我会让它运行类似git diff --stat的命令,再逐个文件确认“这个文件为什么需要改动”。如果发现它偷偷改了任务范围之外的文件,我会直接要求回退。这个检查之所以必要,是因为 Agent 在长对话末端特别容易出现“顺手收拾”的冲动,它可能会把几处风格不喜欢的代码一起改掉,而它自己甚至意识不到越界了。
5. 实测对比:三种上下文策略在真实改动任务上的差距
5.1 测试任务与场景设计
我拿模拟项目“某库存管理后台”(规模大约一百二十万行)做了个小实验。任务是给订单列表模块增加按仓库维度筛选,并且明确“不允许影响库存占用逻辑”。我用同一款支持 Agent 模式的编程工具,分别用三种上下文策略各跑几次,记录效果。样本不大,但趋势非常有代表性。
这个任务的真实难点在于:订单模块接口被库存锁定、对账报表等多处引用,而且仓库里存在新旧两条查询链路。Agent 如果没有足够好的上下文组织,很容易选错链路或改坏影响面。
5.2 方法A:直接对话式
方法 A 最简单,打开会话,把需求一句话丢给 Agent,让它自己探索。结果超过预期地差:Agent 第一次探索时找到了正确的查询入口,但没有读取库存占用相关的调用方,导致改动后订单接口被库存模块的测试用例打破;中段它又一头扎进了废弃链路。整个任务反反复复,越到后面修改质量越差。
我在旁边记账:首轮方案方向正确的情况只占一小部分,返工次数多,光是人工纠偏就花了大把时间。这也是大多数人第一次在大型仓库里用编程 Agent 的体验——偶尔惊艳,经常翻车。
5.3 方法B:全量喂送式
方法 B 是我曾以为最正确的做法:“窗口大,干脆把所有相关代码都贴进去”。我把订单模块的 Service、仓储层、控制器、以及能想到的上下游调用文件,全部粘进提示词里,试图一步到位。
结果非常尴尬:首轮响应就明显变慢,因为输入实在太大了;推理到中后期,模型开始“抓瞎”,两次引用了根本未导入的类,还对着一个工具类的旧实现长篇大论。全量喂送把窗口塞满之后,重要的接口契约被大量冷门工具类信息挤到低权重位置,失忆反而更严重。我后来认清了:全量喂送不是上下文工程,而是把预算全部花在了开头,后面的工作根本没有余量。
5.4 方法C:上下文工程式
方法 C 就是上一章介绍的完整流程:先写任务说明书;让它先读 AGENTS.md 仓库地图;动手前要求它复述方案;把任务拆成三个子任务,每步维护变更摘要;最后做影响面检查。
实际跑下来,第一次尝试就准确找到了新链路入口;三个子任务依次完成,过程中没有越界改动;到了后期,它依然记得“缓存刷新模块只读不可改”这条约束——因为这条约束一直在任务书和滚动摘要里反复出现,始终处于窗口高权重位置。对比之下,方法 C 的总轮次最少,人工介入最少,最终效果也最干净。
5.5 对比结论表格
| 评估项 | 方法A:直接对话式 | 方法B:全量喂送式 | 方法C:上下文工程式 |
|---|---|---|---|
| 首轮方案方向正确率 | 偏低 | 时好时坏 | 高 |
| 越界改动次数 | 多 | 多 | 几乎没有 |
| 遗漏约束条件次数 | 多 | 很常见 | 很少 |
| 总对话轮次 | 多 | 多 | 少 |
| 人工纠偏时间 | 高 | 很高 | 低 |
| 后期失忆严重程度 | 明显 | 严重 | 基本可控 |
这里的核心结论不是模型变聪明了,而是上下文工程改变了模型推理时的信息质量。方法 C 让“目标、约束、当前进度”始终在模型最关注的位置重复出现,模型不需要从一堆垃圾信息里大海捞针,自然也就不容易“失忆”。
6. 反复踩过的坑,以及相应的处理办法
6.1 坑1:让 Agent「保持现有风格」结果风格四分五裂
我早期经常在任务书里写“请保持现有代码风格”。这条指令基本等于没说,模型根本不知道“现有风格”锚定哪个文件。后来改成“新代码对齐internal/order/service/query.go的命名风格与错误处理方式”,效果立刻不一样,因为它有了具体的参照物。
这个坑的本质是:显式信息永远优于模糊指令。你给模型一个明确文件路径,它就能提取出风格特征;你只给一句抽象要求,它就随机发挥。
6.2 坑2:搜索结果太多占满上下文
我发现 Agent 搜索一个关键词后,喜欢把搜索结果里所有匹配文件全部读进上下文,哪怕这些文件跟当前任务毫无关系。这会让窗口瞬间被垃圾信息占满,核心任务反而被挤到边缘。而且搜索返回的代码片段往往是被截断的,读起来上下文不全,模型很容易误解。
我的处理办法是:在任务书里提前约束搜索行为,明确要求“搜索后先列出文件路径与一句话摘要,由我挑选后再精读,每次精读不超过两到三个文件的相关片段”。这样既保留了检索能力,又控制了上下文预算。
6.3 坑3:跨会话记忆断档,第二天像换了个人
另一个高频痛点是:头天跑得正顺利,第二天继续新会话,Agent 完全不记得之前已经确认的事实。我吃过这个亏以后,会给每个会话设置“交接摘要”机制:会话结束时强制让 Agent 输出一份简短的交接文档,里面写清已完成事项、已验证事实、待办清单和下一步计划,然后我把它粘贴到 AGENTS.md 的“当前任务状态”区域,或者单独存成一个session-handoff.md文件。
第二天新会话第一句就是“请先读 AGENTS.md 和 session-handoff.md,然后继续完成待办清单”。这样做之后,跨会话记忆断档的问题基本消失了。逻辑也很简单:模型的短期记忆不能持久,但文件系统可以。你要做的就是把要记住的事情写进文件,而不是指望模型自带记忆。
6.4 坑4:Agent 自信地改错了地方,还坚持说改过
最让人头疼的一种情况,是 Agent 在长对话后期基于幻觉记忆告诉你“修改已完成”,但实际代码里根本没有那个改动,或者改错了位置。它甚至能描述出虚拟的“改动后的代码行”给你看。
我的对策是立了一条规矩:凡涉及“确认改完”的汇报,必须附带机器可验证的证据。要么给出grep -n定位到具体函数行,要么给出git diff中对应文件的具体片段。如果它给不出来,或者给出的证据和描述对不上,就说明它正在幻觉,这时候我会直接终止当前会话,把交接摘要拉到新会话里重新锚定,而不是继续在越陷越深的错误上下文里纠缠。
如果你现在正被编程 Agent 的“失忆”折磨,我的建议是先别急着换工具、换更大窗口的模型。你把一个百万行仓库交给任何人,他都需要项目背景、入口指引和阶段性确认;编程 Agent 也一样。上下文工程听起来玄乎,落到日常其实就是三件事:写任务说明书、留一份仓库地图、维护滚动变更摘要。我在项目根目录里放的那份 AGENTS.md,价值怎么强调都不过分——它等于把所有探索过的经验沉淀成了可以反复读取的长期记忆。最后再分享一个小技巧:在任务说明书开头明确写一句“本次只做 X,不要做 Y”。这句简单的边界声明,能极大抑制 Agent 在长对话中后期自由发挥的冲动,对于防止失忆非常有效。