自己做AI Coding Agent的开发者十有八九都撞过同一堵墙项目稍微大一点Agent一上来就把整个仓库翻了个底朝天把几十个文件全塞进上下文然后窗口满了、响应稀烂、费用还蹭蹭涨。这其实是“喂料方式”出了问题——你给的不是材料是炸药。这一篇是这个系列的第3篇聚焦“路径上下文”这个设计思路核心就一句话——给模型材料的位置而不是一次性喂全部内容。让Agent像人一样先看目录、再查索引、最后精准翻阅需要的片段既给了足够的材料支撑又不至于把上下文窗口撑爆。这个方法尤其适合那些正在做代码助手、知识库问答、文件级工作流自动化的朋友同样是给料设计过的喂法能省掉90%的上下文浪费。1. 先想清楚Agent为什么会被“喂爆”1.1 上下文窗口是物理存在的天花板很多人对上下文窗口的理解就是“能装多少字”实际上它背后是硬性的计算与显存边界。Transformer的注意力计算量随序列长度增长是平方级的长上下文推理时KV Cache键值缓存会占据大量显存序列越长单次推理延迟越高成本也越高。就算模型支持几十万token的窗口那也意味着你能用的实际预算被大幅侵占——推理一次的时间和费用可能比小窗口模型高出几个量级。所以“喂爆”不单是“放不下了”而是你把昂贵的窗口空间用在了低价值的信息上。上下文窗口本质上是你和模型之间唯一的高速通道每一寸都应该是高信息密度内容而不是整仓库的代码噪音。我在项目中实测过一个十几万token的上下文窗口塞入一个中等规模仓库的完整代码后模型对于简单问题的响应速度会明显下降极端情况下还会出现“答非所问”的漂移——因为实在装不下了它会在截断的边缘强行“发挥”。1.2 全量塞入不只是浪费还会污染判断如果把上下文窗口比作办公桌那全量喂料就相当于把一整柜子资料全倒在桌面上。要命的是不只桌面不够放模型找资料时的“注意力”也被分散了。注意力机制的核心是给不同信息分配权重当无关代码和关键指令混在一起模型很容易被无关信息带跑忽略掉你真正想让它做的事。更隐蔽的问题是指令覆盖。你原本在系统提示词里写了很多约束和偏好但如果后续喂入的大段文件占据了绝对篇幅这些早期指令在注意力中的占比会被不断稀释。换句话说不是那些约束不存在了而是它们在被海量文件内容“淹没”。我见过最典型的场景让Agent修改某个函数它读完了全部源码后反而把原本正确的部分给“优化”坏了。原因很简单——它在海量上下文里“迷路”了分不清哪些是你要改的哪些是它自己脑补出来的。1.3 路径上下文是什么换个角度理解“给料”路径上下文的本质是改变“上下文里有什么”这件事。传统方式上下文里装的是文件内容Agent直接“读”到所有代码。 路径上下文方式上下文里装的是内容的位置信息和获取方式——文件路径、目录结构、函数签名、行号范围、读取工具。Agent先“看到”地图然后按需调用工具去获取具体内容。一句话定义路径上下文 告诉Agent材料在哪、长什么样、怎么拿而不是替它把所有材料搬过来。这个设计天然地利用了Agent的工具调用能力。大模型本身不会去硬盘里翻文件但它可以通过你提供的函数接口“按图索骥”。我们要做的就是把“给材料”的动作从“一次性搬运”改成“可寻址访问”。2. 设计路径上下文的三个层次2.1 第一层目录树作为全局地图最基础的路径上下文就是让模型看到项目的目录结构。不要小看这个看似简单的信息它提供的是全局坐标。想象你让一个新人去看一个旧项目他第一件事是什么一定是打开项目根目录看看有哪些文件夹、哪些文件、名字都叫什么。目录树给了模型这样一个“鸟瞰视角”让它在回答任何问题之前对项目形态有一个整体认知。实操上我会生成两种粒度的目录树完整树包含全部目录和文件名适合项目规模不大时使用。裁剪树排除掉node_modules、dist、build、.git等无关目录只保留源码和配置文件再标注“已忽略N个目录”。为什么裁剪很重要这些依赖目录和构建产物体积巨大塞进上下文既浪费token又完全没有信息价值。我的习惯是默认剔除这些噪音目录并且把结果压缩成紧凑格式一行一个路径而不是用那种带树形符号的ASCII图——后者可读性差对模型也不友好。2.2 第二层文件元数据作为材料索引卡只给目录名还是不够模型看到handlers/user.go这个名字并不知道这个文件里有没有它要找的函数。这时候第二层进场文件级元数据。我把每个文件的信息做成一张“索引卡”文件路径与相对位置文件行数关键定义函数名、类型名、常量名依赖关系这个文件import了谁、被谁引用这样模型就能回答更具体的问题例如“哪个文件定义了用户注册逻辑”就不需要打开文件去猜了。但这里有个度的问题所有文件的完整元数据也可能很大。我的策略是只提取“定义列表”用AST抽象语法树解析出函数和类型名丢弃实现体。一个几百行的文件提取后往往只剩几行索引信息。几百个文件的项目完整索引也能控制在几千token以内。2.3 第三层按需读取的内容片段地图和索引都有了最后一步才是“打开文件”。这一层的关键不是“能读文件”而是控制读取的颗粒度。第一次只给模型一个工具入口比如 “read_files(paths: string[], start_line?: number, end_line?: number)”。模型可以根据之前的索引信息只读取某个文件的某一行范围而不是整个文件。你可能会说那这个文件几百行相关的函数分散在各处怎么办我的做法是让Agent分多次小块读取每次只读它当前关心的范围。如果确实需要全量也允许它传完整文件路径但单次读取有token上限控制超过上限就截断并提示可用行号继续读取。这一步其实把“喂料”变成了“自助餐”——模型自己决定拿什么、拿多少而你要做的只是制定用餐规则、设置每道菜的上限。3. 从零实现一套“抽屉式”路径上下文3.1 工具层给Agent一组“手柄”而不是钥匙串路径上下文落地的第一步是把“读文件”这个能力封装成一组工具而不是简单暴露一个terminal让Agent自己去跑cat。直接给shell命令的坑我踩过Agent很容易cat一个大文件后把输出整个粘进上下文你要的读量控制完全失效。我提供的典型工具如下{ tools: [ { type: function, function: { name: list_files, description: 获取项目目录结构可指定路径和深度, parameters: { type: object, properties: { path: {type: string, description: 相对项目根目录的路径默认 .}, max_depth: {type: integer, default: 2, description: 最大递归深度避免爆炸} } } } }, { type: function, function: { name: file_index, description: 获取文件的函数名、类型名、行数等元数据不返回实现体, parameters: { type: object, properties: { path: {type: string, description: 目标文件路径} } } } }, { type: function, function: { name: read_files, description: 读取指定文件的内容必须指定行范围, parameters: { type: object, properties: { paths: {type: array, items: {type: string}}, start_line: {type: integer}, end_line: {type: integer} } } } } ] }这里有个非常关键的设计细节read_files的start_line和end_line我设置成了必填参数。模型如果偷懒不想填就会在这个工具上碰壁从而学会先调用file_index拿到行号范围再来读。这就是“用工具定义约束模型行为”比任何system prompt里的口头叮嘱都更有效。3.2 预算层给每次返回的“菜量”设上限只靠工具还不够还需要硬性控制单次工具返回结果的大小。我的做法是在工具执行层做两件事一是单次返回字符数上限。比如设置上限为6000字符超过就按完整行截断并且在返回内容末尾附上一行说明“内容过长已截断文件共N行当前返回第X至Y行如需继续请使用相同工具传入后续行号范围。”二是全局上下文预算管理器。我在每次对话循环中维护一个计数器累计所有消息内容的token数。当累计量超过阈值比如窗口大小的60%就开始进入“压缩模式”把较早的系统摘要和已读文件内容压缩为摘要信息只保留最近对话轮次和当前工具结果。为什么要60%而不是90%因为要给模型的输出预留空间。模型生成回答也要占用上下文如果对话历史已经把窗口塞到98%模型连完整的回答都可能写不出来这是新手最容易漏掉的一环。3.3 缓存层让同一份材料只进一次门另一个浪费大户是重复读取。Agent在推理过程中完全可能对同一个文件发起多次读取——第一次读某个函数第二次又为了确认上下文再读一遍。如果没有缓存机制一份内容被来回搬运预算很快见底。我的做法是维护一个文件级别的内容缓存表cache { src/handlers/user.go: { mtime: 2024-11-02 10:23:11, hash: 7d3a9f..., lines: 320, chunks: { 1-100: ..., 101-200: ... } } }当Agent请求读取一个文件时先比对文件的修改时间和哈希值如果和缓存一致直接从缓存取出对应行范围返回不再触发磁盘IO如果文件有变动则清除该文件的缓存重新读取。这个设计在文件短小、重复请求频繁的场景下收益极其明显。我曾在一个代码审查Agent里做测试开启缓存后整个任务流程的Token消耗降低了大约三成——仔细看日志才发现原来是同一个工具被反复触发了十几次以前全都在重复读一样的内容。4. 一个完整落地案例改造“项目问答Agent”4.1 场景还原我自己维护过一个叫“模拟项目X”的代码库结构大概是这样的前端页面逻辑、后端接口服务、数据处理层、配置文件加起来四五十个文件单文件最大有近千行。最初版本的项目问答Agent用户问“某个接口的调用链是什么”Agent直接read_files把整个后端目录的所有文件一次性读完Lightweight窗口直接爆掉然后陷入死循环。改造后就按照前面三条来先给目录树裁剪掉依赖目录约300 token再给核心文件索引最后让Agent按需分段读取。4.2 调用流程推演举一个用户提问“用户注册接口在哪里定义的它调了哪些函数”的例子看看路径上下文下的Agent实际调用流程第一步系统提示词中附带目录树Agent一眼看到api/handlers/user.go、services/user_service.go、models/user.go这几个路径初步锁定范围。第二步Agent调用file_index获取services/user_service.go的函数列表看到有CreateUser、ValidateUserInput、SendWelcomeEmail等函数同时通过file_index返回的依赖信息知道它还引用了models.User。第三步Agent调用read_files传paths[services/user_service.go], start_line40, end_line85精准读取CreateUser那段实现。第四步根据函数内引用的其他函数名继续用file_index查看对应文件循环迭代直到还原出完整调用链。整个流程下来上下文累计消耗大约2000 token——而传统全量读目录里的文件光services一个目录就可能吃掉6000到8000 token效果还不一定有这个好。4.3 收益复盘这套流程上线后我记录过几组数据单轮问答平均Token消耗降低了约60%左右少的部分主要来自不再读取无关文件。连续多轮对话的“掉线率”指因为上下文溢出导致对话必须重新开始从每月几十次降到了个位数。回答准确性反而提升了因为模型每次真正“读”到的内容都是它主动查询的和当前任务的相关性远高于被动塞入的文件堆。这个数据说明一件事上下文不是越多越好相关才是王道。给模型一堆它用不上的文件只会稀释它的判断力。5. 常见坑与排查记录5.1 Agent拿到目录树之后“迷路”了有段时间我遇到一个很尴尬的情况Agent拿到目录树后不做任何工具调用直接凭文件名猜内容回答问题比如看到order_service.go就直接说“这个文件里有订单创建逻辑”但它根本不知道文件里有没有这个函数。原因很直接——目录树信息密度太高模型没有意识到需要进一步调用工具。解决办法是从两个方向堵第一在工具描述里把read_files和file_index的使用场景写清楚比如“当需要确认文件内部结构时必须先调用 file_index”第二在系统提示词里加一条硬性规则“不要假设文件内容除非你已经通过工具读取过该文件。”5.2 上下文被早期历史对话占满了另一个经典问题Agent第一次调用工具时读多了后续对话一直在历史里滚越滚越大。就算你设置了预算旧的读取内容也会一直留在系统消息里变成“僵尸上下文”。光靠裁剪历史还不够我的做法是引入关键信息累计区。每轮工具调用结束后从结果里提取摘要写入一个固定的“项目状态”消息后续对话优先参考这个摘要而不是原始读取内容。比如Agent读过某函数后工具层自动追加一条“已确认services/user_service.go 的 CreateUser 负责创建用户调用 models.User 的 Save 方法。”这样即使历史窗口滚动把那两次工具调用挤掉了关键结论还留存在上下文里。5.3 文件一直被改动导致缓存失效频繁给缓存加mtime校验之后遇到一个坑某些编辑器的保存操作会无意义地更新修改时间导致缓存刚建好就失效Agent反复重新读文件效果反而变差。调整方案是给“无意义变更”加了一层容错——比对文件内容的哈希值只要内容没变哪怕mtime变了也直接用缓存。由于哈希计算本身也有成本我做了个折中只在mtime变了时才重新计算哈希如果内容一致就复用旧缓存。这样既保证不会读到过期数据也避免了文件系统层面频繁的全量比对。5.4 不要走极端全部改用路径上下文最后是一个方向性的提醒。路径上下文适合那些“文件数量多但单个任务只需要其中一小部分”的场景。如果一个小项目总共就三五个文件每个都很短那直接全量塞入反而更高效——省去反复工具调用的开销模型看到的内容也更完整。比如说写个小脚本、改一个配置文件这种单文件级别的任务别绕弯子直接用 read 读完那一两个文件就够了。路径上下文解决的是“中型以上项目的材料供给”问题不是所有场景的银弹。工具参数的必填约束这个技巧我后来发现还能更进一步——不止约束行号连读取路径的写法都可以统一。比如强制要求相对路径、不允许绝对路径这样一方面防止Agent乱读项目外的系统文件另一方面也让路径上下文始终保持在同一坐标系里模型不容易犯路径拼接类错误。这个系列后面我打算再写一篇“会话窗口中摘要与原始内容的切换策略”讲清楚什么时候该保留原文、什么时候可以用摘要替代。路径上下文解决的是“怎么喂不爆”的问题但喂进去之后怎么持续维持高质量是另一个值得深入的话题。
阅读完成 · 觉得有帮助?