接手某个遗留项目时,我面对的是一个接近一万个源文件的代码仓库。那一瞬间我脑子里冒出的第一个念头,是用AI帮我快速梳理这套系统——毕竟市面上的大模型工具已经能读懂代码,为什么不把整个仓库“喂”给它,让它告诉我架构、依赖、潜在问题?
这个想法在第三次尝试时彻底破碎了。不是AI不聪明,是“喂”这个动作本身就有问题:上下文窗口装不下,token成本扛不住,模型开始一本正经地编造不存在的接口。在试错过程中我慢慢意识到,真正的解法不是优化“怎么喂”,而是在喂之前先做一件看起来不起眼但决定成败的事——给整个仓库建一张“体检索引表”。这篇文章就聊聊我完整的踩坑和实操过程。
1. 第一次硬喂AI,翻车翻得很彻底
先说结论:把近万个源文件直接塞给AI,这个方向从根上就是错的。但我当初确实这么干了,而且翻了三次车才老实。
1.1 你以为的“全量喂给AI”到底意味着多少token
很多人对代码量的认知是“文件数”或“行数”,一旦换算成token就会吓一跳。业内有个粗算:一个中等复杂度源文件的平均长度在500到1000行之间,按每行代码约8到15个token估算,一个普通文件就要吃掉4000到15000个token。也就是说,近万个源文件全量转成token,总量奔着4000万到1亿token去。
这个数字有多大?目前主流模型支持的上下文窗口普遍在20万到100万token这个量级,你就算用最激进的大窗口方案,也只够装下全部文件的百分之一甚至千分之一。更别说token是按用量计费的,真把几千万token全推给模型,一次会话的费用就够买一台中端开发机了。
而且这里还有个隐藏问题:上下文越长,模型注意力越分散。我试过把某个模块下800多个文件塞进单次会话,结果模型对头部几个文件记得很清楚,对中间部分的概念开始模糊,到后面直接开始混淆两个功能完全不同的模块。这不是模型不行,而是“长上下文”本质上是让模型在一个超大信息池里捞针,针越多,捞错针的概率越大。
1.2 翻车现场:幻觉、张冠李戴、永远聊不到重点
第一次翻车很经典。我问AI“这个系统的核心业务链路是什么”,它洋洋洒洒给我画了一张结构图,看起来逻辑严密,但仔细核对后发现,它把A模块的入口函数当成了B模块的入口,然后顺着这条错误路径编了一个完整的故事。我顺着它指的路去读代码,浪费了整整一天才意识到方向错了。
第二次翻车是“张冠李戴”。因为全量喂进去时,不同文件被截断,模型只看到了每个文件的开头部分——而很多文件的头部是注释和import声明。结果AI把所有文件的注意力都放在了“这个文件依赖了谁”上,根本没有理解“这个文件做了什么”。我问它某个接口的调用链,它回答的其实是另一个同名常量在别处的用法。
第三次翻车最有代表性:它开始“编函数名”。某个功能它记不清具体实现,就直接在回答中生成了一个看起来很像样的函数名,还煞有其事地描述它的参数和返回值。等我核实的时候发现,这个函数在整个仓库里根本不存在。
这三次翻车让我彻底明白一个事儿:AI处理大代码库的核心瓶颈不是“智力”,而是“注意力分配”。你喂给它的信息量越大、越杂乱,它越难区分“关键信息”和“背景噪声”。就好比让一个新人第一天入职就看全部代码,他大概率也是懵的。正确的做法是先给他一张地图,告诉他“哪些街区住着哪些部门”,他才能带着问题去指定区域翻代码。
2. 核心那件事:先给近万文件建一张“体检索引表”
踩完坑我又仔细想了一下,发现问题的本质是“信息组织方式”。AI真正需要的不是所有代码,而是“代码的地图”——它得先知道这个仓库里有什么、哪里是核心、哪里是边缘、哪些文件是入口、哪些文件只被一个模块引用。
于是我先做了那件关键的事:建索引。
2.1 为什么先做索引,而不是先做筛选
有人可能会说:既然全量喂不行,那就筛选一下,挑重要的文件喂给AI不就行了?这句话听着对,但实际操作中有个大坑——“重要”这个词本身就需要上下文才能定义。
举个例子,一个仓库里被依赖次数最多的文件,可能是某个底层工具类;而业务入口文件虽然只被一个地方引用,却是理解全系统的钥匙。如果你不先把整个仓库的结构摸清楚,你根本不知道哪些文件值得挑出来。所以“筛选”听起来省事,其实是在没有地图的情况下猜重点;“建索引”听起来费事,却是在真正动手猜之前先把路走一遍。
我给这个工作起了个名字叫“体检索引表”,本质就是一句话:用结构化的方式回答六个问题——仓库里有什么类型的文件?文件在目录中如何分布?哪些目录是核心?文件之间的依赖方向是什么?哪些文件具有“入口”或“出口”特征?哪些文件基本无人问津?
2.2 这张表里必须有的六类信息
实践下来,我把索引表设计成六类信息,每一类都对应一个特定的AI使用场景:
第一类是类型分布。统计全部文件的扩展名、语言、数量和总代码行数。这一层解决“这个仓库大概有多复杂”的问题,也让AI知道应该用什么语言的语法习惯去理解代码。
第二类是目录结构树。把仓库的顶层目录和二级目录完整列出来,标出每个目录下的文件数和总行数。这层是让AI建立空间感。我在实际项目中就发现,很多系统从顶层目录就能看出设计意图——有的按业务模块分,有的按技术层分,有的干脆平铺一坨,后者的风险评估本身就值得单独聊。
第三类是核心目录识别。基于文件数量和依赖关系,标出哪些目录藏着系统的核心逻辑。这一层服务于“AI优先精读哪些区域”的决策。
第四类是依赖方向。统计“谁依赖了谁”,特别是import语句的数量和走向。这一层让AI理解模块之间是“上下游”关系还是“兄弟并列”关系。有了这个,AI回答调用链问题时就不会再乱编链路。
第五类是入口与出口特征文件。识别出main函数、初始化脚本、路由注册、对外API等“入口型文件”,以及被大量外部模块引用的“出口型文件”。这些是AI理解系统要重点盯防的位置。
第六类是重复与孤立文件。找出代码相似度过高的文件和依赖度接近零的“孤儿文件”。这一层对重构评估特别有用,AI可以根据这部分信息快速甄别哪些模块可以合并、哪些模块可能存在死代码。
我花了大半天时间把这六类信息全部整理出来,然后把它保存成一个结构化的Markdown文档,里面包含了表格、目录树、统计数据和关键文件的路径列表。这份文档本身只有几千行、几万token——相对于近万文件动辄几千万token的全量输入,小了三个数量级。
3. 从零开干:产出索引表的完整步骤与脚本
说了这么多,这部分我把实操步骤完整写出来。你不需要复制我的全部脚本,但核心思路可以照搬。整个流程分四步,请按顺序执行。
3.1 第一轮全量扫描:类型分布与体积估算
这步很机械但很重要。我当时用的是Linux环境下的基础命令组合,效果很好,核心是文件类型统计和目录体积排序。关键脚本逻辑大概是这样的:
# 统计各扩展名的文件数量 find . -type f | awk -F. 'NF>1 {ext=$NF; count[ext]++} END {for (e in count) print count[e], e}' | sort -rn | head -30 # 统计各文件的代码行数(跳过二进制文件) find . -type f \( -name "*.py" -o -name "*.ts" -o -name "*.js" \) -exec wc -l {} + | sort -n | tail -20第一个命令快速告诉你仓库里到底有多少种文件、哪几种占大头。第二个命令找到代码行数最多的文件——注意,行数最多不代表最重要,但它至少帮你估计“如果精读这个文件,AI大概要花多少token”。
接着按目录聚合,识别哪个令人头疼的“文件黑洞”在吞token:
# 按一级目录统计代码行数 find . -type f \( -name "*.py" -o -name "*.ts" \) -exec awk '{lines[FILENAME]++} END {for (f in lines) print lines[f], f}' {} + | awk '{split($2, a, "/"); dir="/"a[2]; dirlines[dir]+=$1} END {for (d in dirlines) print dirlines[d], d}' | sort -rn如果你嫌手写命令麻烦,也可以用现成的代码统计工具(比如带统计模式的命令行工具),能自动输出语言分布和目录结构。但我的建议是至少跑一遍上面的命令,因为原始数据拿到手里你才有体感,后面整理索引时也会更清楚。
3.2 依赖方向统计:找出真正的核心模块
类型分布讲的是“有什么文件”,依赖统计讲的是“谁离不开谁”。这一步的产出物是一张“被依赖次数排行榜”,榜单顶部的文件就是你仓库里的“权力的中心”。
我用的方法是扫描所有源文件中的import、include、require语句,归一化后排个序。以Python为例:
import os, re, collections root = "./src" deps = collections.Counter() pattern = re.compile(r'^\s*(?:from|import)\s+([a-zA-Z0-9_\.]+)', re.M) for dirpath, _, filenames in os.walk(root): for fn in filenames: if not fn.endswith(".py"): continue path = os.path.join(dirpath, fn) with open(path, "r", errors="ignore") as fp: content = fp.read() for m in pattern.findall(content): mod = m.split(".")[0] deps[mod] += 1 for mod, cnt in deps.most_common(30): print(cnt, mod)这里有个关键细节:我没直接用模块名排名,而是先把模块名映射回具体的源文件路径。因为不同文件可能都依赖一个包名,但那个包名背后可能对应着几十个文件。映射完以后,再对“文件被依赖次数”做排序,得到的结果才精确到“哪个物理文件最核心”。这一步做到位,AI后面答“改A会不会影响B”这种问题时就靠谱很多了。
3.3 抽样生成目录级摘要:让AI帮你读地图
有了结构信息和依赖排行榜,接下来是让AI读地图的关键环节:不要一次性给它整个目录的全部文件,而是每个目录先抽1到2个代表文件,让AI生成“这个目录是干嘛的”摘要。
抽样策略很讲究。我踩过的坑是“按文件大小抽样”——挑最大的文件给AI看,结果挑出来的全是数据字典和工具函数。后来换成更聪明的策略:优先抽依赖排行榜里排名靠前、且位于该目录下的文件;其次抽文件名带有入口特征的(如server、main、index、router、handler);最后再补一个体积中等的随机样本。
每个目录让AI读两三个文件后,让它用一段话回答四个问题:
- 这个目录的职责是什么?
- 它对外暴露了什么能力?
- 它最常被谁依赖?
- 如果只看这个目录,你觉得系统的哪个部分最值得深入?
然后把AI对每个目录的摘要汇总起来,就是一份“目录级地图”。这部分内容不需要精确到每个函数,但需要精确到“这个目录在系统里扮演什么角色”。我用它替换掉了原来人工一行一行写注释的笨办法,节省了很多时间。
3.4 把索引格式化成AI友好的输入
所有素材齐了之后,最后一步是格式化。这一步的目的是让你接下来每次和AI交互时,都能在一条消息里同时附上地图和任务,让AI带着“全览”去回答你的局部问题。
我最终形成的索引文档结构是下面这样的:
# 系统索引 ## 1. 总览 - 文件总数:9274 - 主语言:Python / TypeScript - 代码行数约:312万行 - 主要目录:7个 ## 2. 依赖排行 TOP 10 表格:文件路径 | 被依赖次数 | 所属目录 ## 3. 目录摘要 ### src/core 职责:XXX 暴露能力:XXX 主要被依赖方:src/api, src/engine 代表文件:xxx.py, yyy.py ### src/api 职责:XXX ... ## 4. 高风险区域 - src/legacy:重复代码占比高,孤立文件多 - src/core:被依赖过一次以上,改动影响面大这份文档的token开销我实测下来大概在3000到5000 token,比直接把整个目录塞进去便宜了几个数量级。有了它,我后面每次问AI“这个接口在哪儿定义的”或者“这个改动会影响谁”,都能把索引连同问题一起丢给它,回答准确率上涨得非常明显。
4. 索引喂给AI之后:效果对比与被忽略的坑
索引建完不代表万事大吉,实际使用的效果对比和坑同样值得聊。
4.1 “先看地图再进街区”和“蒙眼进城”的差别
建索引之前我让AI做架构分析,它给我的答案永远是一团浆糊;建索引之后再问同一个问题,它的第一句话往往就对了。这不是玄学,而是因为模型在回答前先看了一版“结构化地图”,它知道系统分几个大块、每块的核心文件在哪个位置,于是整个分析路径是从整体到局部的,而不是从某个零散文件的印象里硬猜。
举一个我实际验证过的场景:我想知道“用户登录后,创建会话以及写日志这条链路涉及哪些模块”。建索引之前,AI的回答漏掉了日志写入模块,还把数据库连接池和缓存层的位置搞混了。建索引之后,它先顺着索引里的目录摘要定位到api入口,再沿着依赖排行榜找到core下的会话模块,然后找到logging模块,链路一次说对。
另一个明显变化是幻觉函数名的频率大幅下降。原因不难理解:以前它面对的是上万个文件名混杂的噪声信息,无从分辨“印象中的函数”是否真实存在;现在它手里有了一份精确到“文件路径+目录职责”的地图,遇到找不到的函数时会主动说“索引中没有覆盖到这个符号”,而不是硬编一个出来。
4.2 用着用着才发现索引会过期
这里说一个我没提前料到、后来吃了亏的坑:索引是会过期的。
项目是活的,代码每天都在变。我建索引时仓库里还是9274个文件,两周后已经变成10600个,新增了一批API模块,同时有一个旧模块被整体删掉了。但我手里的索引导航文档还是旧版,AI按旧目录摘要给我分析新代码,结果就是牛头不对马嘴。
后来我给自己定了一条规矩:每次和AI进行重要会话前,先花两分钟同步索引。不是全量重跑,而是只做增量更新——跑一遍新增文件和删除文件列表,对新增文件走一次抽样摘要,对删掉文件的目录更新依赖排行榜。这样索引从“一次性产物”变成了“持续维护的活地图”。
这个习惯的养成帮我避开了连带反应:比如依赖排行榜更新不及时,AI会认为某个已经移除的核心模块还在系统里承担关键角色,给出的重构建议就会建立在错误前提上。
4.3 一个容易被忽视的细节:token预算分配
建好索引后,我还摸索出一套token预算的分配逻辑。既然索引已经告诉我哪些文件是核心、哪些文件是边缘,那么和AI交流时“给它看什么文件”就有了优先级。
我的分配方案是这样的:索引本身占10%,核心目录的代表文件占40%,你正在分析的具体问题涉及的文件占50%。也就是说,初始上下文中只放少量核心文件,不要一股脑把所有“可能相关”的文件都塞进去;等到AI真正指出“我需要看xxx文件的第几行实现”时,再定向补充那一个文件的详细内容。
这套“先地图、后街区、最后门牌号”的递进式喂法,让AI的平均上下文规模下降到了原来的十分之一左右,回答的精确度反而是之前的三到四倍。对一个有近万文件的仓库来说,真正的奢侈成本从来不是“多问几句话”,而是“无脑把所有文件一次性带上谈判桌”。
5. 这套打法的适用边界与进阶扩展
这套“先建索引再喂AI”的方法不是银弹,它有明确的适用边界。我把心得整理如下,免得你拿它去套所有场景然后骂我。
5.1 什么时候值得花这个功夫
如果你的仓库只有几十个文件、总量几百K,那老老实实直接把文件分批喂给AI完全没问题,建索引反而是浪费时间的过度工程。但如果你的仓库已经到几千甚至上万文件,或者你承担的是一个老旧系统的维护任务、需要频繁向AI询问跨模块链路,那么建索引的投资回报率极高。我第一次建索引花了大半天,但之后每次问架构问题都省下至少一两个小时,两三次就回本了。
还有一类场景也特别适合:“给AI布置一次性大型任务”之前。比如你想让AI帮你梳理系统重构方案,或者生成一份完整的技术债务报告,这种任务的回答质量极度依赖AI对全局的理解。这时候先花半小时把索引喂进去,产出的报告质量能高一个档次;不喂的话,AI大概率就是搜刮一些热门关键词然后拼凑印象流内容。
5.2 进阶:把索引自动化、版本化
这套方法跑顺之后,我做了两个自动化改造,现在已经稳定使用。
第一个是“索引自动生成脚本”。我把它挂到了代码仓库的定时任务里,每天晚上自动跑一遍全量扫描、依赖统计和目录摘要生成,完成后提交一份新的索引文档到仓库中。这样我任何时候开始和AI对话,都可以直接拉最新版索引,不用再手动检查文件新增导致的过期问题。
第二个是“索引版本对照”。我会保留最近三个版本的索引文档,方便看清楚仓库的演化方向——哪个目录膨胀了?依赖排行榜上的核心模块有没有位移?新增文件集中在哪个区域?这些变化本身就是很好的架构评审素材,用来回答“这个系统的复杂度增长集中在哪里”这类问题非常直观。
在我个人看来,给AI喂代码这件事,核心矛盾从来不是“模型理解能力不够强”,而是“人类没有把信息组织成模型能高效理解的形式”。先建一张索引表,你只是多付出了一天不到的时间,却把AI从一个“语言模型”变成了真正意义上的“系统分析师”。这个账,怎么算都划算。
最后聊个小技巧:每次你在索引里标记一个“高风险区域”,记得同时写上原因。比如某个目录被依赖次数多,就写“此目录是核心枢纽,改动影响面广”;某个目录有孤立文件,就写“疑似死代码,重构成本低”。这些备注不会消耗多少token,但会让AI在后续回答中多一层“风险意识”,效果比让它泛泛地看代码结构好得多。这套打法我已经用了很久,对这个动辄上万文件的“老朋友”仓库来说,它让我从“吃力地往上爬”变成了“带着地图逛公园”。