我把 10 万行祖传代码喂给了 AI:用 RAG 搭建“代码考古“助手,新人 1 天看懂老项目

发布时间:2026/8/10 8:48:41
我把 10 万行祖传代码喂给了 AI:用 RAG 搭建“代码考古“助手,新人 1 天看懂老项目 我把 10 万行祖传代码喂给了 AI用 RAG 搭建代码考古助手新人 1 天看懂老项目入职第一天Leader 丢给我一个 GitHub 仓库链接说“你先看看这个项目熟悉一下业务逻辑下周开始迭代需求。”我打开仓库愣住了——10 万行 Java 代码最后一次提交记录是两年前。没有文档没有注释包名还带着公司上一个项目的缩写。唯一的“说明”是一个 README.md里面只写了一句话“本项目基于 Spring Boot 2.1.3 构建。”我花了三天时间在 IDE 里跳转来跳转去试图搞清楚“订单状态机”到底有几个状态、支付回调的入口在哪里、那个叫handleLegacyProcess的方法为什么有 800 行。第四天我决定不再硬扛。我把这 10 万行祖传代码全部喂给了 AI。不是用 ChatGPT 复制粘贴而是搭建了一个基于RAG检索增强生成的“代码考古”助手。效果立竿见影——新来的实习生用这个助手1 天之内就能回答出原本需要两周才能搞明白的 80% 的代码问题。这篇文章我就把整个搭建过程、技术选型、踩坑经验以及最重要的——从祖传代码里提取出来的“知识图谱”该怎么设计一次性全部分享出来。一、为什么直接扔给 ChatGPT 不行在动手之前我先试过最朴素的做法把整个项目的核心代码分批次复制到 ChatGPT 里问它“这个类是干什么的”。结果很糟糕上下文窗口不够。10 万行代码哪怕用 Claude 200K也塞不下完整的调用链。更别提还要保留对话历史。丢失全局视角。问 A 类时AI 不知道 B 类怎么调用它问模块 C 时AI 不知道模块 D 对它的依赖。每次都要重复解释。新来的每个人都要把同样的代码重新“喂”一遍毫无复用性。代码更新后历史答案作废。万一某天有人重构了核心类所有历史问答全部失效。问题的本质是AI 没有“项目记忆”。它只能看到你当前给的这几段代码看不到整个项目的结构、调用关系、分层设计。RAG 正是用来解决这个问题的——先把代码库向量化建立索引每次提问时先从索引里检索最相关的代码片段再把这些片段连同问题一起发给大模型。这样一来AI 每次回答都“带着项目上下文”而且可以随时更新索引实现项目知识的持久化。二、整体架构一个“代码考古”助手长什么样我的目标是搭建一个内部工具团队成员可以随时提问例如“订单取消的完整调用链路是什么”“退款金额是怎么计算的涉及哪些类”“这个Deprecated的接口还能用吗有没有替代方案”“如果要新增一种支付方式需要改哪些地方”架构分成三层2.1 数据层代码知识库代码仓库的完整源码只保留.java/.kt/.xml/.properties等Git 提交历史提取 commit message 和 diff用于理解代码变更意图历史文档如果有的话哪怕只有几篇 Wiki 或 Jira 描述2.2 索引层向量检索 结构化元数据对每个 Java 文件进行方法级拆分而不是整个文件做向量化提取类名、方法名、注解、参数、返回值、调用关系等结构化信息同时构建两种索引向量索引语义检索和关键字索引精确匹配类名/方法名2.3 问答层RAG LLM接收用户问题 → 多路召回向量 关键字→ 重排序 → 组装 Prompt → 调用大模型支持多轮对话每轮对话会保留历史上下文但始终基于最新索引三、核心难点 1代码该怎么“切”成向量这是整个项目中最关键的决策。我试过三种粒度粒度做法效果文件级整个.java文件作为一个 chunk检索太粗糙问一个小方法却返回整个大文件Prompt 塞满无用信息类级按类拆分略好但一个类有 20 个方法时还是太臃肿方法级按 public/private 方法拆分最优。每个 chunk 只包含一个方法及其签名、注解、Javadoc最终我选择了方法级拆分并额外保留类级别的摘要信息作为“元数据”。具体拆分逻辑用 JavaParser 实现// 伪代码示意for(ClassDeclarationclazz:allClasses){StringclassNameclazz.getName();StringpackageNameclazz.getPackage();StringclassJavadocclazz.getJavadoc();StringclassAnnotationsextractAnnotations(clazz);// 类摘要作为一个独立的 chunk用于回答“这个类是干嘛的”indexChunk(idpackageName.className#class,contentbuildClassSummary(packageName,className,classJavadoc,classAnnotations),metadata{type:class,package:packageName});// 每个方法作为一个 chunkfor(MethodDeclarationmethod:clazz.getMethods()){StringmethodNamemethod.getName();StringmethodBodymethod.getBody();StringmethodJavadocmethod.getJavadoc();StringparamsextractParams(method);StringreturnTypemethod.getReturnType();StringannotationsextractAnnotations(method);// 关键把调用关系也塞进 content 里SetStringcalledMethodsextractMethodCalls(methodBody);indexChunk(idpackageName.className#methodName,contentbuildMethodContent(packageName,className,methodName,methodJavadoc,params,returnType,annotations,methodBody,calledMethods// 显式写出调用了哪些方法),metadata{type:method,className:className,package:packageName,calls:calledMethods,annotations:annotations});}}为什么要显式提取calledMethods因为向量检索只能捕捉语义相似性但“A 调用了 B”这种关系是结构化的向量很难精确表达。把调用关系单独存成元数据后我可以在检索时做“图扩展”——如果一个方法被检索到就自动把它的上下游方法也一并召回。四、核心难点 2如何提取调用关系构建轻量级代码图谱光靠向量检索不够。当用户问“订单支付的流程是什么”时他想要的是一条完整的调用链而不是几个零散的方法。我的做法是在索引阶段构建一个简易的调用关系图Call Graph存储在图数据库中我用了 Neo4j轻量场景下用 NetworkX 内存图也行。提取方式有两种4.1 静态分析主方案用 JavaParser 遍历 AST对每个方法体里的所有MethodCallExpr解析出“调用方 → 被调用方”的关系。如果被调用方在当前项目内则建立边如果是第三方库如springframework.*则忽略或只记录注解。4.2 Git 历史辅助增强方案用 JGit 分析 commit 历史如果一个方法在最近半年内被频繁修改说明它是“热点代码”如果一个方法已经两年没变动说明它可能是“稳定沉淀层”。这些信息可以标注在元数据里让检索时优先返回高频变更的代码——因为大概率用户现在要改的就是它。最终我得到了一张图节点类、方法、接口、枚举边调用calls、实现implements、继承extends、注解标记annotated_by当用户提问时RAG 先做一次向量检索得到 Top 5 个方法然后在这张图上做2 跳以内的邻居扩展把相关的调用者/被调用者也一并加入上下文。这样 Prompt 里天然就包含了一条调用链的雏形。五、向量检索的具体调优经验我用了BAAI/bge-m3作为 Embedding 模型中文 代码混合场景表现不错向量维度 1024Chunk 大小控制在 512 tokens 以内因为一个 Java 方法通常不会太长。检索时我用了混合检索Hybrid Search向量检索用余弦相似度召回 Top 20BM25 关键字检索针对类名、方法名、注解名做精确匹配召回 Top 10融合排序RRFReciprocal Rank Fusion把两路结果合并重排取 Top 5之所以需要 BM25是因为很多代码问题是“精确命名”的比如“OrderStatus这个枚举里有没有CANCELLED状态”——这种问题语义向量反而会跑偏必须靠关键字精确命中。重排序Re-ranking也很关键。初次召回的 20 个结果里可能前 5 个都是同一个类的不同方法而另一个类的重要方法排在第 15 位。我用了一个轻量的 cross-encoder 模型BAAI/bge-reranker-v2-m3对 Top 20 重新打分确保多样性。六、Prompt 工程怎么让 AI 像资深同事一样回答问题检索到的代码片段是“原材料”怎么让大模型产出高质量的答案取决于 Prompt 的设计。我最终的 Prompt 模板长这样精简版你是一个资深的代码审查专家正在帮助新人理解一个老项目。 ## 项目背景 - 技术栈Spring Boot 2.1.3, MyBatis, Redis, RocketMQ - 核心业务电商订单履约系统 - 代码库总行数约 10 万行Java ## 当前问题的相关代码片段按相关性排序 {retrieved_chunks} ## 调用关系上下文 {call_graph_context} ## 用户问题 {question} ## 回答要求 1. 先用 1-2 句话概括核心答案。 2. 如果涉及调用链路用「A → B → C」格式清晰列出。 3. 如果代码中存在潜在风险如空指针、事务边界不清请友善指出。 4. 如果问题超出已知代码范围请明确说“当前代码库中没有找到相关信息”不要编造。 5. 最后附上涉及的关键类名和文件路径方便我进一步查看。其中{call_graph_context}是用图数据库查出来的邻居节点列表格式化成调用关系 - OrderController.cancelOrder() 调用 OrderService.cancelOrder() - OrderService.cancelOrder() 调用 OrderStatusMachine.transit() - OrderStatusMachine.transit() 调用 OrderRepository.save()这样一来AI 的回答不再是“泛泛而谈”而是精确到具体类名和方法名。新人在 IDE 里直接搜索就能定位到代码行。七、实际效果从 3 天到 1 小时我用这个助手做了一个测试把团队里 5 个不同年限的新人包括 2 个实习生分成两组A 组传统方式给文档实际上几乎没有自己读代码遇到问题问老员工B 组使用 RAG 代码考古助手随意提问结果指标A 组传统B 组RAG能说清订单主流程的时间2.5 天0.5 天能独立定位一个 Bug 根因3 天1.5 小时问“为什么这里要加分布式锁”能答出背景仅 1 人靠猜全部答出因为检索到了 Git commit 记录老员工被打扰次数平均 12 次/人平均 1.5 次/人最让我意外的是有两位实习生通过这个助手在一个小时内就发现了代码里一个隐藏多年的事务传播级别配置错误——因为助手在回答另一个问题时顺带提到了Transactional(propagation Propagation.REQUIRES_NEW)和外部调用之间的嵌套关系。八、踩过的坑希望你不再踩8.1 不要把整个 XML 配置文件塞进向量一开始我把 MyBatis 的 Mapper XML 也拆成 chunk 向量化了结果检索时常把 SQL 片段误当作 Java 逻辑。后来我把 XML 单独建了一个索引并打了type: sql标签只在用户问题明显涉及“SQL”或“查询”时才召回。8.2 处理循环依赖和过深调用栈有些祖传代码有 15 层调用嵌套图扩展 2 跳还能接受3 跳以上 Prompt 就爆了。我的策略是只扩展 2 跳但如果用户追问“再往上一层呢”就触发第二次检索带着上一轮的结果做定向扩展。8.3 大模型的“幻觉”会编造不存在的类在早期测试中AI 经常回答说“调用OrderValidator.check()”但项目里根本没有这个类。后来我在 Prompt 里明确加了一条规则“你只能引用{retrieved_chunks}和{call_graph_context}里出现的类名和方法名不要编造新的。” 并且在后处理阶段用正则检查回答中的所有[A-Z][a-zA-Z0-9]*是否在索引中有记录如果有不存在的类名强制重试一次。8.4 索引更新策略代码是会变的。我设置了一个 Git Hook每次 push 到 master 分支时自动触发增量索引更新——只重新索引变更了的文件而不是全量重建。全量重建放在每周日凌晨执行一次用于修复可能出现的索引碎片。九、这套方案的扩展空间做完基础版后我又加了两个小功能极大提升了实用性代码变更溯源对于检索到的每个方法如果用户问“为什么这么写”助手会去查询 Git 历史中最近 3 次 commit 的 message结合 Jira 单号需要配置内部 Jira 的 API告诉用户“这个改动是为了修复订单超时场景下的并发问题对应 TICKET-1234”。新人主动引导当检索到某个方法有超过 3 个调用者时助手会自动提示“这个方法被多个地方调用修改前建议先确认影响范围调用方列表如下…”十、成本与收益整个系统跑在一台 8 核 32G 的服务器上加上一个 PostgreSQL存元数据和一个 Neo4j存调用图。Embedding 模型用的开源模型本地部署不需要调用外部 API数据安全可控。大模型用的是内部的私有化部署Qwen-72B每次问答平均消耗 1500 tokens成本约 0.002 元/次。上线三个月累计处理了 2300 多次问答总成本不到 5 块钱。最大的收益不是省钱而是降低了团队的“隐性知识依赖”。以前新人来了必须由老员工“口传心授”的业务细节现在 80% 都可以通过助手自助解决。老员工终于可以安心写代码而不是每天当“人肉 Wiki”。结语代码会衰老但知识可以复活那 10 万行祖传代码依然在那里依然没有注释依然有些地方连原作者都不记得为什么这么写。但现在每一个新来的开发者都有一个“考古助手”陪在身边。它不眠不休它记得每一次 Git 提交它能在 200 毫秒内从 10 万行代码里找出最相关的 5 个方法然后用流畅的中文讲给你听——这段代码是做什么的谁写的什么时候改的以及你改它的时候要小心什么。代码考古不是让你去崇拜过去的遗迹而是让你站在巨人的肩膀上更快地走向未来。如果你也在维护一个超过 5 万行的老项目不妨试试这个方案。整个代码不超过 1000 行 Python核心索引部分 配置文件完全可以在两周内搭建完成。而它为你团队节省的时间将是成千上万倍。附录技术栈清单供参考组件选型代码解析JavaParser JGitEmbedding 模型BAAI/bge-m3向量数据库Qdrant亦可使用 Milvus/Chroma图存储Neo4j小规模可用 NetworkX 内存缓存关键字检索ElasticsearchBM25或 SQLite FTS5重排序BAAI/bge-reranker-v2-m3大模型Qwen-72B私有化部署调度APScheduler增量更新 全量重建接口FastAPI WebSocket支持流式输出全文完推荐阅读看我如何管理我的电子书籍