当前位置: 首页 > news >正文

我把 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){StringclassName=clazz.getName();StringpackageName=clazz.getPackage();StringclassJavadoc=clazz.getJavadoc();StringclassAnnotations=extractAnnotations(clazz);// 类摘要作为一个独立的 chunk(用于回答“这个类是干嘛的”)indexChunk(id=packageName+"."+className+"#class",content=buildClassSummary(packageName,className,classJavadoc,classAnnotations),metadata={"type":"class","package":packageName});// 每个方法作为一个 chunkfor(MethodDeclarationmethod:clazz.getMethods()){StringmethodName=method.getName();StringmethodBody=method.getBody();StringmethodJavadoc=method.getJavadoc();Stringparams=extractParams(method);StringreturnType=method.getReturnType();Stringannotations=extractAnnotations(method);// 关键:把调用关系也塞进 content 里Set<String>calledMethods=extractMethodCalls(methodBody);indexChunk(id=packageName+"."+className+"#"+methodName,content=buildMethodContent(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 模型(中文 + 代码混合场景表现不错),向量维度 1024,Chunk 大小控制在 512 tokens 以内(因为一个 Java 方法通常不会太长)。

检索时,我用了混合检索(Hybrid Search)

  1. 向量检索:用余弦相似度召回 Top 20
  2. BM25 关键字检索:针对类名、方法名、注解名做精确匹配,召回 Top 10
  3. 融合排序(RRF,Reciprocal 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 分支时,自动触发增量索引更新——只重新索引变更了的文件,而不是全量重建。全量重建放在每周日凌晨执行一次,用于修复可能出现的索引碎片。


九、这套方案的扩展空间

做完基础版后,我又加了两个小功能,极大提升了实用性:

  1. 代码变更溯源:对于检索到的每个方法,如果用户问“为什么这么写”,助手会去查询 Git 历史中最近 3 次 commit 的 message,结合 Jira 单号(需要配置内部 Jira 的 API),告诉用户“这个改动是为了修复订单超时场景下的并发问题,对应 TICKET-1234”。

  2. 新人主动引导:当检索到某个方法有超过 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 + JGit
Embedding 模型BAAI/bge-m3
向量数据库Qdrant(亦可使用 Milvus/Chroma)
图存储Neo4j(小规模可用 NetworkX 内存缓存)
关键字检索Elasticsearch(BM25)或 SQLite FTS5
重排序BAAI/bge-reranker-v2-m3
大模型Qwen-72B(私有化部署)
调度APScheduler(增量更新 + 全量重建)
接口FastAPI + WebSocket(支持流式输出)

(全文完)
推荐阅读:看我如何管理我的电子书籍

http://www.jsqmd.com/news/1364932/

相关文章:

  • Python a0-baas-sdk包解析与BaaS开发实战
  • 【Python20260808】03
  • 2026年8月正规的净化板翻新源头厂家找哪家测评,净化板翻新服务与源头企业分析 - 海棠依旧大
  • Spring IoC与DI核心原理及最佳实践解析
  • 终极Mac鼠标滚动优化方案:让外接鼠标如触控板般顺滑的完整指南
  • NOOA框架:面向对象设计简化AI智能体开发与工程化实践
  • 苹果中国官网“千问”文档“一日游”:谁先说为何这么敏感?
  • UE5 C++开发必备:5个VS2022工作区优化技巧,告别卡顿与漫长编译
  • 2026年7月广州市黄埔区二手房价格深度分析报告
  • Codex实战:基于LLM的PR自动化安全审查如何提升开发效率与代码安全
  • 2026做网站的平台,企业建站工具与官网搭建方案指南
  • 降低Agent运行成本的6个方法:从模型选型到Token复用
  • 北京至臻至美祛斑正规吗深度解析:行业专家视角 - GrowthUME
  • Flutter async_field组件HarmonyOS移植实践
  • 泗县装修公司怎么选?本地主流家装品牌深度对比解析 - 国麟测评
  • 2026年7月广州市番禺区二手房价格深度分析报告
  • 动态规划解决棋盘礼物最大值问题
  • 二、启发式算法在瓦解问题中的效率-张君杰
  • NVIDIA Profile Inspector架构解析:深入探索显卡性能调优的技术实现
  • 实战解析:如何高效配置LAV Filters实现专业级视频播放解决方案
  • 2026年7月广州市天河区二手房价格深度分析报告
  • 成本账单可视化:构建Agent运营看板监控每次调用的费用明细
  • Python + MySQL + Tkinter 桌面版图书借阅管理系统
  • XGBoost竞赛实战:从原理到Kaggle夺冠技巧
  • 虚幻引擎Pak文件分析工具UnrealPakViewer:从编译到实战应用全解析
  • C语言游戏移植WebAssembly实战:从环境搭建到性能优化全流程
  • 长沙退役军人职业技能培训:退役军人事务员薪资待遇怎么样 - 优企甄选
  • 大学生校园之星评选活动怎么做(众选星实测教程,实操无难度) - 优企甄选
  • 基于AHP-模糊综合评价的工程实践能力量化系统
  • obsidian设置护眼色