1. 先想清楚:AI 到底为什么读不懂百万行代码
前阵子帮一个朋友梳理他们那套几十万行的老业务系统,他上来就问我一个问题:为什么我每次把那个大仓库丢给 AI,它都只会“装作听懂”,给的方案看起来挺像回事,真要提刀上马去改,就驴唇不对马嘴?
这个问题我太熟了。把“百万行代码仓库”直接塞给 AI,就像让一个人把整座图书馆的书全背下来再回答问题,而不是让他带着检索目录、按需去查。你问的明明是第三排第五个书架上一本书的第 42 页,他却只能凭封面猜。工具不对,策略更不对。
先说一个物理事实:AI 模型的上下文窗口是有限的。主流模型一次对话大概能处理十几万到几十万个 token,换算成代码,乐观估计也就够装下几千行到一两万行。而百万行代码是什么概念?哪怕平均一行只算 20 到 30 个 token,那也是两千万 token 起步,超出模型承接能力两三个数量级。所以“让 AI 读懂整个仓库”这句话,字面意义上当前就不成立,未来即使窗口继续涨,成本和时间也撑不住。
但问题比“读不完”更麻烦的,是“读不准”。你给 AI 塞了 2 万行代码,它看似都能读到,但真正相关的可能就 200 行。剩下的 19800 行全是干扰项。做过长文本测试的朋友应该都有感觉:信息放在开头和结尾容易记得住,埋在中间就容易被忽略,专业点说这叫“迷失在中间”。代码仓库比普通长文更夸张——它由几百上千个文件、复杂的调用关系、隐性的工程约定组成,相关性从来不是“距离最近”就能找到的。你要改一个下单流程,相关代码可能分布在 controller、service、mapper、lombok 生成的内部逻辑、一大堆配置类里,彼此隔着十几层目录。
所以核心矛盾根本不是“上下文窗口不够大”,而是“AI 缺少一套像人类工程师那样的找代码、读代码、抽结论的流程”。人类接手陌生仓库,不会从第一个文件读到最后一个文件,而是先看目录结构,理解模块分层,从需求入口往深处钻,找到关键函数顺着调用链扒拉,再回到入口确认影响面。别人之所以能读懂百万行仓库,靠的是方法和经验,不是记忆力。AI 想做到同样的事情,必须把“记忆负担”外包出去,用索引、检索和探测工具撑起一个可以增量读取的外挂大脑,这才是整篇文章要聊的正事。
1.1 “读不完”只是表象,真正的瓶颈是相关性筛选
我见过很多团队的第一反应是:既然上下文不够,那就换更大的窗口模型呗。这个思路不能说错,但在百万行仓库面前等于没解。哪怕未来窗口真的能装下一两百万行,你把整个仓库一次读进去,模型要在一大堆无关代码里找出真正有用的调用链,注意力会被严重稀释。更何况长输入的响应延迟和成本都是线性甚至超线性增长的,每次提问都烧全量,开发效率反而不升将降。
你真正需要的,是让 AI 在接到任务时只看到三类东西:第一,仓库的整体地图,也就是模块结构、目录目的、技术栈;第二,与当前需求高度相关的代码片段与符号定义,比如某个函数、某个配置项、某个接口的上下游;第三,在需要时还能继续去“翻书”的能力,比如主动执行搜索、读指定文件、看调用方。这三件事恰恰对应了本文后面要讲的 Repo Map、检索召回和 Agent 工具链。
1.2 代码理解与文本理解,底层逻辑完全不同
还想再强调一个容易忽略的点:代码不是普通文本,它的“语义”藏在结构里。你读一篇散文,按段落切分、算关键词相似度,基本能抓住大意。但代码不一样,“同样一句代码放在 A 类里表示状态初始化,放在 B 类里可能是事件回调”,只看字面相似度会把模型带偏。符号定义、作用域、类型约束、函数调用图,这些才是真正的语义载体。
这意味着,想让 AI 理解百万行代码仓库,我们不能简单套用“下载全文、分块、存向量、做语义检索”的通用 RAG 套路,必须加入代码特有的结构信息。我的做法是搭一个三层知识体系:符号索引层、语义检索层、Agent 感知层,缺一不可。
2. 换个思路:从“装进上下文”到“按需取用”
前面说清楚了问题,现在聊解法。想让 AI 读懂大仓库,最朴素也最有效的思路,是放弃“一次全读进去”,改成“按需取用 + 增量阅读”。这个思路说穿了很简单:给 AI 配一套“外置记忆力”,它自己知道仓库里有哪些东西,需要时再调用工具去精确取用,一次只把真正相关的内容送进上下文。
我习惯把整个过程比喻成看病:你不会让医生把全套医学教材背下来再给你诊断,你会挂一个专科号,医生问你症状,开检查单,拿到检查报告后结合自己的专业知识给出结论。AI 理解大仓库也一样,目录结构就是分诊台,符号索引就是检查项目清单,检索和 Agent 工具就是检查设备,模型的专业能力则负责做最后判断。
2.1 为什么“纯嵌入向量检索”撑不住百万行仓库
先把最容易踩的坑摆出来:很多人一上来就把整个仓库丢给 embedding 模型,切成小块丢进向量数据库,然后让 AI 做相似度检索。这个方案在小项目、几百个文件里可能表现不错,一旦上到百万行,就会遇到几个硬伤。
第一个硬伤是切块方式。按固定长度切代码,会把一个函数拦腰截断,语义不完整;按函数切,又可能漏掉跨文件的调用关系。第二是相关性计算,两个函数可能都在操作“订单状态”,一个是电商订单,一个是工单系统,文本相似度极高,但业务上下文完全不相干。第三是查询召回率,你问“用户登录后创建会话的逻辑在哪”,embedding 模型未必能把它跟名为SessionManager.createSession的代码片段关联起来,因为自然语言和代码语言之间存在很大的表达鸿沟。
所以我的结论是:纯向量检索可以当辅助,但不能当支柱。真正能扛住百万行仓库的,必须是在符号索引之上的混合检索体系。
2.2 三层知识体系:外置记忆的核心设计
我用过一阵子之后,沉淀出了一套非常好用的三层结构,每一层解决一类问题。
第一层叫结构层,对应 Repo Map。它回答的是“这个仓库里有哪些模块,每个模块大概是干嘛的”。实现方式是把目录树整理出来,给关键目录和文件补充一句人类可读的摘要,比如“common/ 存放通用工具与异常定义”“order/ 负责订单创建和状态流转”。这一层的信息密度很低,几千个 token 就能装下整张地图,让 AI 快速建立全局感,避免在错误的方向盲目搜索。
第二层叫符号层,对应代码的类、函数、变量、导入关系等结构化信息。它回答的是“某某函数在哪个文件、哪些地方调用了它、它依赖了哪些定义”。实现方式是用 Tree-sitter 或者 LSP 解析代码,提取符号索引存下来。这一层是代码特有的,普通文档 RAG 完全不会建。
第三层叫检索层,包含关键词搜索和语义向量搜索两套通路。它回答的是“跟我描述的业务概念最接近的代码在哪里”。关键词搜索用rg扫代码,语义搜索用 embedding 模型算相似度,两者结果合并后再做一次重排,把真正的候选文件找出来。
三层配合的意义在于,AI 不是直接从 100 万个文件里大海捞针,而是先看地图缩小范围,再查符号确定候选,最后用检索精确定位。每走一步,上下文里积累的都是与任务强相关的信息,而不是整座仓库的无效噪声。
2.3 Agent 式探索:让 AI 自己当“查代码的实习生”
有了地图、符号和检索,还差最后一块拼图:Agent。前两层是静态资料,Agent 是动态执行者。模型在推理过程中可以反复调用工具,第一次先看 Repo Map,确定了大概模块,再用 grep 搜关键词,打开了几个候选文件,发现不够,向上追踪调用方,然后回头修改最初定位的代码。整个过程像极了一个靠谱的实习生接手陌生项目时的动作:先看文档、再搜代码、顺着调用链深挖、最后确认修改影响面。
这里的关键是“工具可以被调用”,而不是“让模型自己脑补”。我见过不少项目试图在提示词里写“请想象一下这个仓库里有哪些文件和函数”,这在小库上偶尔能蒙对,在百万行仓库里就是灾难。你必须把真实工具暴露给模型,并且明确规定每个工具的用途、输入输出格式、可调用的次数上限,它才能真正“看”到代码。
3. 落地方案:五步搭一条百万行代码理解管道
理论聊够了,直接进入实操部分。我以自己最近搭过的一套方案为例,完整走一遍从零开始搭建“AI 读码助手”的流程。这套方案不依赖特定云服务,大部分组件都是开源可自托管的,按需替换即可。
3.1 第一步:用 Tree-sitter 生成符号级索引
符号索引是整个体系的底座,它决定了 AI 能不能快速定位“某某函数在哪”。我用 Tree-sitter,因为它对语法错误的代码容忍度高,还能增量解析,百万行仓库全量解析一次也能在几十秒内完成。你如果更习惯传统方案,也可以先用 Universal Ctags 生成 tags 文件,两者目的一致,只是 Tree-sitter 能拿到更细的 AST 信息。
下面是一个简化版的 Python 示例,用 Tree-sitter 提取文件里的函数和类定义,并记录它们的位置信息:
from tree_sitter import Language, Parser import glob def extract_symbols(filepath, language): parser = Parser.create(language) with open(filepath, 'r', encoding='utf-8') as f: code = f.read() tree = parser.parse(code.encode()) symbols = [] def walk(node): if node.type in ('function_definition', 'class_definition', 'method_definition'): name_node = node.child_by_field_name('name') if name_node: symbols.append({ 'type': node.type, 'name': code[name_node.start_byte:name_node.end_byte], 'file': filepath, 'start_line': node.start_point[0] + 1, 'end_line': node.end_point[0] + 1 }) for child in node.children: walk(child) walk(tree.root_node) return symbols # 实际工程中建议配合 ctags 或 SCIP/LSIF 使用,这里只保留最小可运行示例 all_symbols = [] for file in glob.glob('/your/repo/**/*.py', recursive=True): all_symbols.extend(extract_symbols(file, python_language))这一步生成的索引,我会存入一个轻量数据库或者直接存成 JSON 文件。索引字段至少包含:符号类型、符号名、所属文件、起始行、结束行,再加上它的导入关系和父节点。有了这些信息,AI 拿到“调用某个函数的所有地方”时,不需要逐一打开文件去翻,可以先在符号索引里查一遍。
3.2 第二步:做混合检索,关键词与向量双通道并跑
代码检索不是非此即彼,关键词和向量各管一段。关键词检索擅长精确匹配函数名、类名、配置项,严格查PaymentService就是全仓库精确找;向量检索擅长语义匹配,你描述“根据用户等级计算折扣”它能把calculateDiscount找出来,即使字面上没有“等级”和“折扣”。
我的实现是“BM25 + 向量”召回,再加一层重排。BM25 我用的是rank_bm25这个库,向量部分选择开源的 embedding 模型,比如bge-m3或者gte-large,它们都能在本地跑,没有外部调用的顾虑。切块方式上有一个非常重要的细节:代码切块必须按函数或类边界切,而不是按固定字符数切。一个函数就是一个完整语义单元,切碎了你让模型怎么理解?
# 伪代码示意,示意一个简单混合检索接口 def hybrid_search(query, symbol_index, vector_store, k=20): bm25_hits = bm25_search(query) # 精确/关键词召回 vector_hits = vector_store.search(query) # 语义召回 merged = merge_and_rerank(bm25_hits, vector_hits, symbol_index) return merged[:k]合并之后我会用符号索引再做一次过滤:如果候选文件里包含查询中出现的函数名,则加权;如果候选文件里引用了查询涉及的模块,也加权。重排逻辑不用太复杂,关键是把“符号命中”这个强信号排到前面去。
3.3 第三步:构建 Repo Map,给 AI 一张仓库全局地图
AI 需要地图,但地图不能太占上下文。我的做法分两步:先输出目录树的精炼版,再为每个关键目录附一句摘要。摘要哪里来?可以开发时手工维护一份 README,也可以让 AI 先快速扫描每个目录下的文件头和注释,自动生成一句话描述。
举个例子,一个 Spring Boot 微服务仓库的 Repo Map 长这样:
repo-root/ ├── order-service/ # 订单服务:订单创建、状态流转、超时处理 │ ├── controller/ # HTTP 入口,接收下单/查询请求 │ ├── service/ # 核心业务逻辑,交易规则与状态机 │ └── mapper/ # 数据库访问层,SQL 与 ORM 映射 ├── user-service/ # 用户服务:账号、登录态、权限 ├── common/ # 公共组件:异常、工具类、统一返回包装 └── gateway/ # 网关:路由转发、鉴权过滤这样一张地图大概几百个 token,AI 一眼就知道该往哪个目录钻。它不解决具体问题,但它避免了“把整个仓库翻一遍”的无效探索。我搭好这套之后,AI 第一次做功能修改时,几乎没有发生过找错模块的情况。
3.4 第四步:Agent 工具链,让模型像工程师一样查代码
有了索引和地图,还要给 AI 提供可调用工具。编码 Agent 最常用的工具大概四个:list_files看目录结构、search_symbol查符号定义与引用、grep_code全文检索、read_file读取指定文件内容。稍微高级一点的还可以加run_tests和apply_patch,不过在实际生产环境中我建议至少保留一个“输出修改方案供人确认”的环节。
工具定义很简单,就是给模型提供一段 JSON Schema 描述,说明工具名、参数含义和返回值。核心是每个工具的 prompt 要写清楚使用时机:先看地图,再搜符号,命中候选后再读文件,避免模型一上来随机抓几个文件硬看。
这里分享一个我自己整理的 Agent 工作流示例:
- 接收任务“给订单超时关闭功能增加可配置的延迟时间”。
- 读一遍 Repo Map,锁定
order-service。 - 搜索符号
timeout、closeOrder,定位到OrderTimeoutService。 - 查看该服务所在文件,找到写死超时时间的常量。
- 向上查调用方,确认这个常量是否暴露到配置系统。
- 综合信息后给出修改方案,提示需要修改
OrderTimeoutService、配置文件、以及测试用例。
整个过程模型只读了 3 到 5 个文件,而不是全仓库,上下文消耗极小,准确率反而比塞一堆无关代码高得多。
3.5 第五步:串成增量式阅读流程,控制上下文消费
最后一公里是把上面的组件全部串起来,形成一套可重复执行的流程。我把它叫做“增量式阅读”:每一轮推理都只读当前任务必需的最小信息集,信息不足时才追加读取。
为此我给上下文消费做了一个预算控制,大致分配如下:系统提示和 Repo Map 占 10% 左右,检索结果显示的候选片段占 20%,真正打开的文件正文占 60%,剩下 10% 作为 Agent 工具返回预留。这个比例不是绝对的,但能提醒你一件事:地图和工具结果别贪多,真正决定答案质量的是那几个精读文件。
每次 Agent 执行完一轮工具调用,我会把“已读文件清单”和“当前结论摘要”写回上下文的固定位置,防止它重复读同一个文件,也防止它在推理中把之前的信息给忘了。说白了,这是在替模型做一个简单的“记忆管理”,让有限的上下文始终服务于主线任务。
4. 实测:在 80 万行的老仓库里验证效果
理论说得再好,不如真刀真枪跑一遍。我拿一个 80 万行左右的 Java 微服务仓库做了一次实验,里面大概有三四十个业务模块,光 Mapper 层就有上万行手写 SQL。任务也很典型:让 AI 帮一个新功能加上“灰度开关”,期望它定位到配置入口、业务判断点和相关测试。下面是我的实施记录和真实数据。
4.1 做准备工作时需要注意的几个工程细节
先把环境搭好。索引阶段我用 Tree-sitter 解析 Java 源码,80 万行全量解析大概花了不到一分钟,生成约 8 万个符号节点。向量化阶段,我按函数和类边界切了约 1.6 万个代码块,用本地模型跑了大概二十分钟,这中间吃了不少内存,八核机器勉强够用。如果你的仓库更大,可以考虑把向量化任务丢到后台分批跑,或者只给高价值目录做向量,比如核心 service 层、域模型层,而不是全仓库一刀切。
一个容易忽略的坑:老仓库里常常有大量生成代码、第三方依赖源码、历史遗留死代码,这些垃圾数据会严重污染索引。我在建索引之前先做了一轮清理,用.gitignore排除target/、build/、node_modules/这类目录,再扫描一遍仓库里有没有明显的大文件或重复代码。把无关代码排除掉,索引质量直接上了一个档次。
4.2 执行任务的完整记录
我给 Agent 下达的任务是:“给订单模块增加一个按用户维度生效的灰度开关,默认关闭,能通过配置中心动态开启。”如果让 AI 直接硬猜,它大概率会答非所问。但在我们这套体系下,它执行的过程是这样的:
第一轮,它先看 Repo Map,锁定了order-service/模块。第二轮,它搜索“灰度”“feature”“switch”等关键词,命中了一个FeatureConfig类,同时向量检索找到了一段“根据用户 ID 判断是否可访问新功能”的代码。第三轮,它打开FeatureConfig所在文件,看到里面有类似isEnabled(key, userId)的方法,再顺着调用链查到下单入口的 controller 层。第四轮,它生成修改方案:新增一个开关 key,在OrderCreateController的入口处增加判断,命中灰度组则走新逻辑,否则走旧逻辑,同时补充一条单元测试。
整个过程它只读了 4 个核心文件,消耗大约 9000 个 token,耗时不到两分钟。生成的方案我给团队里的开发看了,除了一个小瑕疵——没考虑到这个老系统里有一个自定义的配置加载优先级——整体定位基本准确,可以直接进入实现阶段。
4.3 三组方案对比:混装方案完胜单一方案
为了心里有数,我顺手做了个对比实验。第一组方案是纯语义向量检索,不给 Agent 工具,只把检索结果塞进上下文;第二组是纯符号索引加关键词搜索,不加向量检索;第三组是本文推荐的混合方案。每组各跑 10 个类似的任务,统计成功率与上下文消耗,结果如下:
| 方案 | 平均读取文件数 | 平均消耗 token | 一次定位成功比例 |
|---|---|---|---|
| 纯向量检索 | 14 | 26000 | 40% |
| 纯符号+关键词 | 8 | 15000 | 60% |
| 混合+Agent 工具 | 5 | 11000 | 90% |
纯向量检索最不走运,经常召回一堆“看起来相关但实际无关”的代码,把有限上下文填满还不够。纯符号方案定位代码定义没问题,但对需求自然语言的理解比较弱,经常需要二次调整查询词。混合方案准确率和成本都是最优的,这个结论在我后来的几个仓库里也反复验证过。
4.4 一个容易被忽略的事情:先测量后优化
写到这里我特别想说一句,工程上最忌讳的是凭感觉调参。你觉得自己检索结果不准,先别急着换 embedding 模型,把每次查询的召回结果记录下来,看看到底是哪一层出的问题:是关键词没命中,是向量相似度排错了,还是 Repo Map 引导错误?我自己的做法是每次查询都打日志,把符号命中和检索命中分开统计,哪个环节失败率高就优化哪个环节。没有数据支撑的调优,基本属于给瞎子算命。
5. 常见坑与排查经验
踩坑是必然的,关键是踩完之后能不能把坑总结出来。我把自己在这套方案上遇到的几类典型问题整理成了一份速查表,附带我的处理思路,希望对你有用。
5.1 向量检索召回了大量“形似而神不似”的代码
这是最典型的翻车现场。描述“订单超时关闭”搜出来一堆订单列表查询的代码,两个业务可能在字面上高度重叠,但一个涉及定时任务扫描,一个涉及前端接口列表。问题根源在于纯向量模型捕捉不到代码中的“关系语义”——调用方向、事件触发、状态流转。
我的处理办法是三重召回加约束:关键词负责锁定符号名和固定搭配,符号索引负责关联调用链,向量只做兜底语义扩展。最终结果合并后做一个简单的重排,把“文件里出现查询关键词”“文件属于目标模块”这种强信号放大。这样即使向量结果跑偏,也能被关键词和符号结果拉回来。
5.2 Repo Map 生成得太粗或太细,起了反作用
地图太粗,AI 只知道模块名,不知道里面装了什么,还得靠瞎猜;地图太细,上下文直接爆炸,Agent 光看地图就消耗掉大半预算。解决思路是分级:第一级只给目录树和一句话模块摘要,第二级按需展开指定目录的详细文件清单,第三级才真正读取文件内容。我把这三步对应为三个不同的工具函数,Agent 只有认为当前地图信息不够时才会继续往下展开。这个“菜鸟一步步点击展开文件夹”的设计,能让地图的上下文开销控制在极低水平。
5.3 Agent 在大型仓库中“迷路”,反复进行低效搜索
有时候模型会像一个无头苍蝇一样搜完一个词又搜另一个词,打开文件十来个,最后仍然没有给出可靠答案。我在排查后发现问题不在工具本身,而是缺少“路线约束”。后来我在系统提示里强制要求它按“地图定位→符号检索→读文件→找调用方→形成方案”的固定顺序执行,并且每一步都要说明“为什么走这一步”。这迫使 Agent 的推理过程更接近真实工程师的思考路径,迷路率下降非常明显。另外我还限制了搜索深度和重复读取:同一个文件只允许读一次,读过的结论必须写入摘要区,如果累计工具调用超过 15 次则停下来向用户报告“需要人工指导”。
5.4 老仓库的无效依赖和生成代码污染索引
工商业务仓库里,什么妖魔鬼怪都有,尤其是历史遗留的废旧模块和生成的 DTO 文件,动辄上千行,又丑又没用。它们如果被索引进去,会大量占用向量空间,还会把检索结果误导到错误区域。建议在索引前做一轮清理规则:排除自动生成目录、排除明显废弃的包路径、排除体积超大的单体文件。还有一个纪律是保持增量索引:每次代码合入主干后只对变化文件做重新解析,而不是整仓重建,不然后面跑着跑着你会发现索引和实际代码已经不同步了。
5.5 搜索“函数被谁调用”这类关系性问题时,符号索引兜不住
符号索引解决了“函数定义在哪”,但“谁调用了它”涉及完整的调用图构建,尤其面对重载、多态、动态代理的时候,光靠静态文本肯定不够。如果你真的需要跨模块梳理这层关系,建议接入 LSP 或者 SCIP 生成的语义索引。它们能拿到类型解析后的真实调用关系,精度远高于纯字符串搜索。代价是要引入更复杂的索引管线,所以我的建议是:先评估你的任务到底需不需要这类能力。如果只是改代码、加日志、修 bug,符号索引和关键词检索完全够用了;只有当你要做大规模重构、跨层移动逻辑时,才值得上 LSP 级别的关系索引。
6. 写在最后:我踩过几次坑之后的体会
搭完这套东西,我最大的感想是:让 AI 读懂百万行代码仓库,技术上没有玄学,就是老老实实把“找代码”的工作从模型脑子里搬出来,变成一个可检索、可增量读取的工程系统。你既不能指望模型靠大窗口硬吃,也不能指望纯粹的向量搜索直接开挂。真正可靠的路径,永远是“结构索引 + 混合检索 + Agent 按需探索”的三角组合。
我个人在实际操作中还有一个隐藏心得:别一上来就想搞全能 Agent,先把“给 AI 配一张仓库地图”这件小事做好,然后加一个关键词搜索,再逐步叠加向量和自动探索。每加一层都先度量它对具体任务的成功率有没有提升,没提升就撤掉。无数次事实证明,几条配置得当的简单链路,远胜一个华丽但经常暴走的复杂系统。
如果你现在手上正好也有一个几十万行甚至百万行的老仓库,我建议你按本文的步骤先走一遍。索引不用建得那么复杂,搞懂 Tree-sitter 的符号提取,配一条混合检索,再给 Agent 挂上读文件与搜索两个工具,一套最简版本几个小时就能跑通。等你实际跑出第一版效果,再回头优化重排逻辑和处理边界条件,会比只听别人讲一万遍理论更有效。到时候你大概也会和我有同样的感觉:AI 当然读不完整个仓库,但只要给它一张地图、一套目录和一个认真看代码的流程,它自己能读得很不错。