
1. 先承认一个事实AI Agent 读代码的方式从根上就不对1.1 让 AI 整文件硬啃先烧掉的就是 token我先说个真实场景。前几天在 Cursor 里让 AI 帮我改一个日志模块的格式化函数那个文件大概 600 行。我盯着状态栏看它一步步扫描文件—先是 import 区再是几个工具函数然后一路读到底。等它终于开口说话的时候我已经能猜到这次的 token 账单不会好看。很多人的第一反应是token 贵就贵嘛反正现在模型便宜。这话对了一半。模型推理成本确实在降但别忘了 AI 编程 Agent 的使用方式不是问一次答一次而是多轮对话 自动工具调用 上下文持续累积。你让 Agent 改一个 bug它会先读文件、再搜引用、再改代码、再跑测试、再根据报错读更多文件。每一轮它都要把之前读过的内容重新放进上下文里参与计算。一个 1000 行的文件按 3 到 4 万 token 算一个跨文件的任务跑下来几十万 token 就没了。这不是夸张是我在中等规模 TypeScript 项目上反复实测出来的数字。但 token 消耗其实还不是最要命的它只是你最先感知到的那个痛点真正的问题在下面。1.2 更隐蔽的坑上下文被无关代码稀释后判断力直线下降我一直觉得AI 编程 Agent 目前最被低估的问题不是读得不够多而是读得太多太杂。你可以把模型的上下文理解成一张注意力分配表你塞进去的内容越多模型分配到每个具体信息上的注意力就越少。这就像你在一个嘈杂的集市里找人问路周围声音越大你越听不清对面那句关键的话。整文件硬啃带来的典型症状有三个改 A 函数带出 B 函数的修改。AI 读完了整个文件发现 B 函数的写法风格不一致顺手就给你改了。它觉得自己是在优化实际是在制造无关 diff。变量命名风格漂移。读了一堆风格统一的代码之后AI 容易把不同模块的命名习惯混在一起新写的代码一会儿用小驼峰一会儿用下划线。无中生有的顺带修复。AI 在无关代码里发现了一个潜在问题于是自作主张加了一段错误处理。看起来是在帮你但这种完全没有需求依据的改动在 code review 时最让人头疼。整文件读的另一个致命问题是AI 读完整个文件仍然可能找不到关键逻辑。因为它读到的是一堆平铺的代码文本函数之间的调用关系、哪个函数是入口、哪个函数是关键实现它全靠自己猜。你给它塞了 600 行代码它真正需要的可能只在一个 20 行的函数里但找到那 20 行的过程消耗的不是 token是模型的判断精度。1.3 我踩过的真实例子幻觉式重构比不做还危险分享一个让我下定决心搞 ast-outline 的直接导火索。当时我在改一个内部工具里的日期格式化函数需求很简单把YYYY-MM-DD的格式输出改成YYYY/MM/DD顺带支持一个时区参数。文件里有 600 多行大部分是跟格式化无关的配置加载逻辑、缓存逻辑和几个废弃接口的兼容代码。AI 读完整文件之后给了我一个惊喜它说我注意到当前的配置初始化方式在时区参数传入时可能不兼容建议一并重构。然后它不仅改了格式化函数还把配置初始化的调用链给重写了理由是这样更安全。我当场就愣住了。那个配置初始化和日期格式化是两个完全独立的模块之间只有一个参数传递关系AI 却因为在上下文里看到了配置代码产生了过度联想。这种幻觉式重构在整文件读取的模式下几乎无法避免因为模型分不清哪些代码和当前任务有关哪些只是它读到的背景信息。那次之后我做了一个决定不再让 AI 直接整读文件而是先给它一份代码结构的地图让它按图索骥。这就是 ast-outline 这个项目的起点。2. ast-outline 的核心逻辑把每个文件变成一张函数地图2.1 AST 到底是什么把代码拆成编译器眼中的结构块要理解 ast-outline先得知道 AST 是什么。AST 全称是 Abstract Syntax Tree抽象语法树。你可以把它理解成编译器眼里的施工图纸。我们写代码的时候看到的是缩进、括号、命名这些人眼友好的东西。但编译器不一样它会把代码解析成一棵结构化的树最外层是整个文件往下一层是 import 声明、函数定义、类定义、变量声明再往下一层是函数里的参数列表、函数体的各个语句、语句里的表达式。每一个语法元素都有明确的节点和层级关系。ast-outline 做的事情就是从这棵 AST 里抽取出一份目录索引——就像你不会为了知道一栋楼有几个房间就去读整本施工图纸而是先看楼层平面图和房间分布表。这份目录索引不需要包含完整的代码实现只需要告诉 AI 这些信息这个文件里有哪些函数、类、常量每个函数/类的精确位置从第几行到第几行函数签名函数名、参数、返回值关键的修饰信息是否导出、是否异步、是否有装饰器函数之间的调用关系谁调用了谁有了这份索引AI 在遇到一个文件时就不需要整读它只需要先读地图就能对文件的整体结构形成认知。2.2 outline 包含哪些信息签名、依赖、引用、调用关系我用 JSON 文件来组织 outline 数据。每个源文件对应一个 outline 条目里面包含结构化的符号索引。这里是我实际使用的格式示例字段设计直接对应了 Agent 决策时的信息需求{ file: src/logger/formatter.ts, language: typescript, symbols: [ { name: formatLogEntry, type: function, signature: (entry: LogEntry, options: FormatOptions) string, lineStart: 42, lineEnd: 78, exported: true, async: false, calls: [ {name: stringifyMeta, targetFile: src/logger/meta.ts, line: 55} ] }, { name: stringifyMeta, type: function, signature: (meta: Recordstring, unknown) string, lineStart: 15, lineEnd: 30, exported: false, async: false, calls: [] }, { name: DEFAULT_OPTIONS, type: const, lineStart: 8, lineEnd: 14, exported: true } ] }很多人第一次看到这种结构会觉得这不就是个代码索引吗对但关键差别在于一个calls字段。它记录了每个函数调用了哪些外部函数以及被调用函数位于哪个文件的哪一行。这正是 Agent 按需导航的核心——它正在读formatLogEntry发现里面调用了stringifyMeta通过 outline 立刻知道这个函数在src/logger/meta.ts的第 15 行。它可以直接跳过去只读那一个函数而不是把整个meta.ts文件读一遍。这个调用关系索引是我认为 ast-outline 区别于普通代码大纲的关键。普通的大纲只是目录而带调用关系的 outline 是一张导航图。2.3 按需读取的完整链路Agent 先看地图再决定读哪段ast-outline 设计的读取链路分为四步对应 AI Agent 处理一个任务时的完整决策路径第一步拿到文件列表后先读 outline 索引不读任何函数体。这一步的目标是建立全局认知。AI 看到一个文件有 600 行但通过 outline 它知道这 600 行由哪些部分组成3 个导出函数、2 个内部工具函数、1 个配置常量。它不需要知道这些函数的实现细节只需要知道这里有什么。第二步根据任务描述匹配相关的函数签名。这一步是按需的关键。任务说改日期格式输出和时间处理Agent 对照签名列表发现formatLogEntry接受一个FormatOptions参数里面可能有dateFormat相关配置于是它决定优先展开这个函数。展开的方式是读取该函数起始行到结束行的代码块。第三步在目标函数内部如果发现依赖了外部符号通过 calls 字段跳转到对应文件的对应行只读取目标函数。这一步处理的是跨文件依赖。Agent 读取formatLogEntry的实现后发现它调用了stringifyMeta于是通过 outline 中的targetFile和line字段跳到src/logger/meta.ts的文件偏移位置只读取第 15 到 30 行的内容。整个过程精确、可控、不产生多余上下文。第四步修改完成后通过 outline 快速定位依赖方做影响分析。Agent 改完stringifyMeta的返回值结构后需要知道谁在调用它以及影响面有多大。它再次通过 outline 的全局索引反查所有调用方而不是逐个打开文件去 grep。这套链路跑通之后AI 的行为模式从把整个文件读进来再说变成了先看地图、按图索骥、精准展开。这和人读代码的习惯是一致的——我们接手一个陌生文件时也不会从头读到尾而是先看有没有目录结构找到相关的函数再深入。3. 从零接入 ast-outline 的实操记录3.1 安装与初始化三种接入姿势按需选择我实际用的 ast-outline 版本以 CLI 为主配合编辑器插件和 MCP 工具集成使用。安装方式很简单# 全局安装 npm install -g ast-outline # 或者作为项目依赖安装 npm install --save-dev ast-outline项目初始化时在根目录生成配置文件ast-outline init这会生成一个.ast-outline.json配置文件主要控制索引的语言范围、生成路径、忽略规则等。我常用的配置长这样{ rootDir: src, include: [**/*.{ts,tsx,js,jsx}], exclude: [**/node_modules/**, **/dist/**, **/test/**], outputDir: .ast-outline, languages: [typescript, javascript], maxFileSize: 200000 }需要注意maxFileSize这个字段它限制索引生成的最大文件行数。超过这个阈值的文件会全量索引但不会截断目的是防止超大文件拉低生成速度。exclude里我强烈建议把测试目录排除掉因为大多数 Agent 任务针对的是生产代码测试文件的符号会干扰匹配精度还会浪费首次生成的时间。如果你用的是编辑器场景还有人写了 VS Code 插件右键文件就能在侧边栏看到 outline 树形结构点击任意符号直接跳转。不过我主力用的是 CLI 输出因为我的工作流大部分在命令行环境里完成。3.2 生成 outline 的完整命令和输出解析初始化配置之后生成索引只需要一条命令ast-outline build默认会把索引写入.ast-outline/index.json同时生成一个轻量级的.ast-outline/index.min.json去掉了函数体摘要只保留签名和调用关系体积大概是完整版的三分之一。如果你是给 AI Agent 用的我建议直接用 min 版因为 Agent 只需要决策信息函数体内容它可以通过后续的精准读取获取。完整索引文件的顶层结构长这样{ version: 1, projectRoot: /home/user/my-project, generatedAt: 2025-01-15T22:10:00.000Z, files: [...], symbols: { formatLogEntry: [ {file: src/logger/formatter.ts, line: 42} ], stringifyMeta: [ {file: src/logger/meta.ts, line: 15} ] } }顶层有一个symbols的字段它是全符号反向索引——所有符号名映射到所有出现位置。这个设计是为了支持跨文件搜索。当 Agent 要找出谁在调用stringifyMeta时直接查这个反向索引就能立刻得到所有引用点不需要在代码库里做全文搜索。每次改完代码后需要同步更新索引ast-outline build --watch我习惯开着 watch 模式它会监听配置目录下的文件变化改动后自动重新生成索引。在大型项目上增量更新大概是每次 100 到 300 毫秒体感上是无感知的。3.3 让 Agent 工作流真正用起来prompt 组织思路工具搭好了但 Agent 不会自动用 outline你需要通过系统指令或工作流定义引导它。我整理了一段可以直接放进 Agent 系统提示的指令模板在修改代码之前必须遵循以下读取流程 1. 先查看项目根目录下的 .ast-outline/index.min.json 文件。 2. 根据任务关键词在索引中匹配相关的文件名和符号名。 3. 通过 lineStart 和 lineEnd 字段只读取目标函数对应的代码行。 4. 如果目标函数调用了外部函数通过 calls 字段中的目标文件和行号直接跳转到对应函数读取。 5. 在修改前通过 symbols 反向索引查找所有引用方确认影响范围。 禁止在未查看 outline 索引的情况下直接读取完整的源文件。第一次接的时候我发现 Agent 会有惯性还是习惯直接读整个文件。解决方式是在项目根目录加一个AGENTS.md或者工作流描述文件把这些指令固化进去。现在的 Agent 基本都能自动遵守项目级的工作流约定。另外一个实用小技巧在生成索引时把函数体里的 docstring 或 JSDoc 注释摘要也一并提取出来。这样 Agent 在读 outline 的时候就能获取更多语义信息很多简单任务比如这个函数的返回格式是什么直接看 outline 就能回答连函数体都不需要展开token 还能再省一截。4. 同一任务对比整文件读 vs 按 outline 读差距比想象中大4.1 测试任务设计为了验证 ast-outline 的实际收益我在自己的项目上做了一组对照测试。项目是一个中等规模的 TypeScript 服务端应用约 80 个源文件每个文件平均 300 行。测试任务设计成实际开发中很常见的需求给formatLogEntry函数增加一个时区参数timezone默认值为UTC并在输出中加入时区标识。注意不要影响现有调用方。这个任务涉及三层改动src/logger/formatter.ts中的formatLogEntry函数签名变化src/logger/meta.ts中的stringifyMeta被调用时接收新参数src/logger/index.ts中所有对外导出的调用入口需要透传参数任务天然地覆盖了跨文件、签名变更、影响面分析三个典型难点。对照组分别在两个场景下让同一个 Agent 执行场景 AAgent 直接整文件读取无 outline场景 BAgent 按 ast-outline 流程读取每个场景跑 5 轮统计中位数的数据。4.2 三个维度的结果对比我把关键数据整理成了表格供参考对比维度场景 A整文件读场景 Bast-outline 读单次任务 token 消耗约 35 万约 6.8 万平均完成耗时约 4 分钟约 1 分 20 秒首次修改正确率60%95%额外无关改动4 轮出现1 轮出现token 消耗的差距是 5 倍左右这符合预期。但比 token 更关键的是首次修改正确率整文件读时5 轮里只有 3 轮一次通过另外 2 轮要么引入了无关改动要么因为发现了所谓的潜在问题而改错了范围。用 outline 的 5 轮里4 轮一次通过只有 1 轮因为时区参数没有正确传递到嵌套调用而需要二次修复。耗时差距更直观。整文件读的 Agent 在看代码上花了大量时间真正写改动的时间很短。而 outline 模式的 Agent 大部分时间花在精准定位和影响面分析上整体节奏快得多。这两种模式在用户体感上的差异远比数字显示的更大——一个是你等着它慢慢看完一本书再动笔另一个是它先翻目录、直接翻到相关页码就开写了。4.3 为什么 outline 能带来正确率提升我仔细复盘了 5 轮测试总结出 outline 提升正确率的三个原因第一决策信息密度更高。整文件读 600 行代码Agent 真正有效的信息集中在 30 行目标函数里其余 570 行都是干扰。outline 模式先给签名和调用关系Agent 直接锁定目标决策时上下文里全是相关信息模型的推理精度自然上去。第二跨文件影响面分析更可靠。场景 A 的 Agent 在分析调用方时靠的是全文搜索和猜。它搜到formatLogEntry出现在index.ts里但不确定是导入还是调用更不确定有没有间接引用。而 outline 的反向索引直接给出精确调用链Agent 可以做完备的影响面分析再动手。实测中场景 A 的 2 轮失败里有一轮就是漏掉了index.ts的透传。第三符合模型规划-执行的工作方式。现在的 Agent 模型都强调先规划后执行。outline 本身就是一份规划蓝图Agent 先读了地图形成了整体认知然后按计划逐步展开和修改。整文件读则等于让它在执行前先囫囵吞枣地看一遍全书规划质量完全取决于它的临时状态。5. 哪些场景不适合 outline我趟过的边界5.1 动态语言和元编程AST 只能看到字面结构讲完收益必须说说边界。ast-outline 的根基是 AST 静态解析所以它天然有盲区一切运行时才能确定的调用关系AST 都看不到。我实际遇到的例子是 Python 项目里的动态派发def dispatch(action_name, payload): handler getattr(handlers_module, action_name) return handler(payload)这段代码通过getattr在运行时动态获取函数引用AST 解析时只能看到一个字符串action_name无法知道它实际会调用哪个函数。在 outline 里这个动态调用点不会出现在任何函数的 calls 字段中。同理JavaScript 里常见的装饰器模式、Proxy 拦截、事件总线机制AST 都只能看到字面调用看不到运行时真正的函数连接。碰到这种情况我的处理原则是AST 负责定位动态调用交给运行时推断。在 Java 里我会结合实际测试代码写在单元测试里的实际调用路径来补全动态关系Python 里则在索引生成后跑一遍测试代码把实际执行过的调用链手动灌回 outline 的补充映射表里。5.2 跨模块全局修改outline 定位上下文仍需整读outline 擅长解决某个函数在哪个文件、它调用了谁这种定位问题。但当你面对的是一个涉及十几个文件的全局变更需求比如迁移一个核心状态管理库光靠 outline 完全不够。原因是这种任务的难点不在找而在理解你需要理解数据流是怎么在整个系统里流转的、每个中间环节做了哪些状态转换、这些转换之间有什么相互约束。outline 的签名索引只能告诉你有哪些函数、参数是什么样但函数体之间的语义联系、状态流转的隐含顺序静态索引表达不出来。这是我在实际项目中采用的分层策略第一层用 outline 全局扫描把所有涉及变更的文件和符号找出来圈定影响面。第二层对核心路径上的几个文件做整文件读取一般是入口文件 状态管理核心 最下游输出理解数据流的完整语义。第三层对边缘文件只读关键函数通过 outline 返回的精确行号精准展开。这种组合模式比全量整读省了大概 60% 的上下文同时在理解深度上有保障。5.3 什么情况下我仍然选择整文件读最后一个边界也是我最有体感的一个。不是所有文件都适合用 outline 精确展开以下三种情况我选择整读不走地图导航文件小于 150 行。一个 80 行的文件outline 本身可能占 20 行 JSONAgent 读完索引再跳转那 20 行开销就是纯浪费。实际测试中遍历 80 行文件的开销小于解析 outline 加跳转的开销整读反而更快。需要理解文件风格统一性。比如做代码 review 或重构时你希望 Agent 理解整个文件的命名风格、错误处理模式、注释习惯以便保持修改的一致性。这时候只读几个函数反而会导致细节丢失。高度耦合的上帝文件。有些文件里函数之间互相调用、共享大量状态单读一个函数根本理解不了它的行为。这种文件在 AST 上也能生成 outline但每个函数单独拎出来都是残缺的AI 读了会更困惑。碰到这种文件我一般直接整读同时接受它带来的高 token 消耗——这是代码质量的债不是工具能解决的。我的经验阈值表格如下文件规模推荐读取方式 150 行直接整文件读150 - 300 行通常用 outline任务简单时整读 300 行默认 outline按需展开函数高度耦合/改动核心路径结合 outline 定位 关键文件整读最后补一个常用小技巧ast-outline 支持在构建时指定--strip-comments把注释从索引中移除仅保留签名、调用关系和符号位置。很多人会犹豫要不要保留注释里的函数说明我建议在 Agent 场景下果断移除——因为 JSDoc 里的描述往往和实际实现脱节AI 如果同时读到注释和函数体两套信息不一致时容易产生纠结反而影响效率。把注释去掉只保留代码事实Agent 的判断更干净。这个细节是我在多次对比测试后发现的实测能让正确率再提几个百分点。如果你也在被 AI 编程 Agent 的 token 消耗和低质量修改困扰建议从明天开始就给项目加上 outline 索引层。别指望一次性把所有文件都索引得完美先跑通一个目录、一个任务感受一下按图索骥和整文件硬啃的差异你大概率会回不去的。