探索与实践 —— 用生成式 AI 理解遗留代码库

探索与实践 —— 用生成式 AI 理解遗留代码库

一次技术探索的真实记录 :globe_with_meridians:

随着生成式 AI 的快速发展,其在代码处理领域的潜力引发了广泛关注。尤其是面对复杂的遗留代码库,它似乎为开发者打开了一扇新的大门:通过 AI 辅助,快速解析和优化代码。然而,在理论与现实之间,往往隔着一条充满挑战的探索之路。

我最初的灵感来自 2024 年 10 月 技术雷达 发布的最新报告,其中提到了一种引人注目的技术趋势:利用生成式 AI 理解和重构遗留代码库。报告指出,这项技术的目标在于通过生成式 AI 的强大语言能力,将复杂的遗留代码抽象为更易理解的文档或重构建议,进而帮助开发者重新激活那些被遗忘的项目资产。

12.利用生成式AI理解遗留代码库

试验

生成式 AI (Gen AI) 和大型语言模型 (LLMs) 能帮助开发人员编写和理解代码。尤其是在处理遗留代码库时,这种帮助显得尤为有用,特别是当文档质量差、过时或具有误导性时。自我们上次讨论此话题以来,利用生成式 AI 理解遗留代码库的技术和产品得到了进一步的发展,我们已经成功实践了一些方法,尤其是在帮助大型机现代化改造的逆向工程中。我们使用的一个特别有前景的技术是 retrieval-augmented generation (RAG)方法,其中信息检索基于代码库的知识图谱。知识图谱可以保留代码库的结构化信息,而这往往超出了 LLM 从文本代码中推导的内容。这对于那些不具备自我描述性和一致性的选留代码库尤其有帮助。另一个提升代码理解的机会是,图谱可以通过现有的或 AI 生成的文档、外部依赖关系、业务领域知识等内容进一步丰富,从而让 AI 的工作更加轻松。

对于我来说,这无疑触动了痛点。作为一名技术爱好者,手头积压了不少“历史遗留项目”(比如那些年刚学 Java 时开发的 Minecraft 插件)。这些项目在开发之初充满激情,但随着时间推移,代码变得越来越难以维护。面对这些“历史债务”,如果生成式 AI 能够提供一种高效的解析方式,岂不是开发者的福音?

于是,我决定尝试用生成式 AI 破解这些代码的奥秘,我来看看怎么个事。

生成式 AI 与遗留代码:目标与痛点 :dart:

我的目标是什么?

对遗留代码库的探索,主要集中在以下几点:

快速理解:通过生成式 AI,从代码中提取核心逻辑、功能描述以及可能的改进方向。
提升效率:减少人力对大量代码逐行分析的依赖,加速开发周期。
迭代优化:不仅理解代码,还希望生成式 AI 为重构提供灵感和建议,甚至将部分重构工作交给 AI 完成。

我遇到的痛点?

尽管生成式 AI 功能强大,但在处理复杂代码库时,仍然存在一些常见的瓶颈:

信息读取的不完整性:将代码以压缩包形式上传时,生成式 AI 无法全面解析文件结构。
上下文窗口限制:即便将项目转化为文本格式,超长的代码文本依然超出了 AI 模型的处理范围。
工具局限:现有工具在支持超长文档、处理全局逻辑上表现不足。

这些问题,使得实现目标的过程远比预期复杂,下面是我做过的一些探索与尝试。


探索历程:从失败到最终方案 :milky_way:

第一步:直接上传项目,碰壁的开始

我最初的尝试十分直接:将项目打包成压缩文件,然后上传给大语言模型,期待它能解析出文件夹结构、代码功能甚至可能的优化建议。然而,AI 模型返回的结果却让我困惑:

  • 它只能看到压缩包中的部分文件,且对项目全貌一无所知。
  • 输出结果片面且割裂,完全没有办法帮助我理解代码逻辑。

问题根源:生成式 AI 对文件夹和压缩包缺乏原生支持,它的“视野”被格式限制住了。


第二步:用 project2txt,将项目转化为文本

吸取了第一次的教训后,我手动编写了一个小工具 project2txt。它的功能很简单:将整个项目展开,并将所有代码文件内容合并成一个超长的纯文本文件。理论上,这种方法可以让 LLM 获取项目的所有信息。

Python 代码
import os


def output_file_name(output_path, file_name, file_index):
    return f"{output_path}/{file_name}_{file_index:02}.txt"


def write_file_contents(folder_path, max_file_size=10000 * 1024):
    whitelist_file_type = ('.java', '.gradle', '.yml')
    file_index = 1
    current_file_size = 0
    if not os.path.exists('output'):
        os.mkdir('output')
    output_file = output_file_name("output", "project", file_index)

    out_file = open(output_file, 'w', encoding='utf-8')

    for root, _, files in os.walk(folder_path):
        for file in files:
            if file.endswith(whitelist_file_type):

                file_path = os.path.join(root, file)

                header = f"%File-Head%: {file_path}\n\n"
                content = ""
                try:
                    with open(file_path, 'r', encoding='utf-8') as f:
                        content = f.read()
                except Exception as e:
                    content = f"Error reading file {file_path}: {e}"

                entry = header + content
                entry_size = len(entry.encode('utf-8'))

                if current_file_size + entry_size > max_file_size:
                    out_file.close()
                    file_index += 1
                    current_file_size = 0
                    output_file = output_file_name("output", "project", file_index)
                    out_file = open(output_file, 'w', encoding='utf-8')

                out_file.write(entry)
                current_file_size += entry_size

    out_file.close()
    print(
        f"All specified files have been written to output_XX.txt with size limit {max_file_size} bytes per file.")


_folder_path = 'project-folder'

write_file_contents(_folder_path)

结果如何?
虽然模型可以读取完整的项目内容,但新的问题随之而来:上下文窗口限制

  • LLM 的窗口限制:生成式 AI 能处理的上下文内容是有限的(通常在几千到几十万 tokens),当文本长度超出限制时,模型只能读取其中一部分。
  • 核心信息丢失:因为上下文截断,模型无法对项目提供完整而准确的总结。

第三步:尝试 256k 超长上下文模型

为了解决上下文窗口限制,我将目光投向了支持超长上下文的混元模型(256k tokens 上下文能力)。它的设计初衷正是为了处理超长文本,比如大规模文档和代码库。理论上,它应该能完美解决我的问题。

然而现实再一次让我失望:

  • 高昂的运行成本:256k 模型的调用费用远高于常规模型,这对个人开发者来说并不友好。
  • 效果未达预期:尽管模型接收了完整内容,但输出的逻辑性和精准度并没有显著提高。

仅仅靠“塞更多数据”来解决问题并不现实,内容的提炼和优化同样重要,因此我们需要更聪明的策略来辅助其发挥作用。


第四步:用 Dify 分块,仍然不够理想

我尝试了 Dify,一个基于工作流的低代码 AI 平台。它允许用户将大规模文档分块处理,再通过模型逐块分析,最终整合输出结果。这种方法有效避开了上下文窗口的限制。

但实际使用中的问题依然存在:

缺乏全局视角:分块后的模型输出,难以连接成完整的逻辑流。
工具局限:平台本身的 Bug 和功能限制,影响了我的体验。

尽管 Dify 为解决超长文档处理提供了一种思路,但它在代码库场景中依然力不从心。


第五步:MapReduce 框架的启发

在 11 月 9 日,我看到了这样一则新闻:

大模型的记忆限制被打破了,变相实现“无限长”上下文。
最新成果,来自清华、厦大等联合提出的LLMxMapReduce长本文分帧处理技术。
来源:量子位

于是,我尝试了 LLM_MapReduce,一个开源框架,基于最新提出的 LLMxMapReduce 的分布式思想:

  • Map 阶段:将项目文件分解成多个模块,并行处理。
  • Reduce 阶段:整合模型的输出,生成全局总结。

这一方法在理论上完美契合我的需求,但实际操作中依然面临挑战:

部署复杂:框架需要本地模型支持,我需要 Ollama 或者 vLLM,而且项目依赖 Pytorch 环境。我受限于网络条件,无法完成完整的部署。(不足 10 Mbps 的流量网我有点破防)
云端支持不足:框架似乎并不适配云端第三方 LLM 服务,需要自己编写相关适配器。

因此我选择放弃这个方案。但尽管尝试失败,MapReduce 的分布式思想为我未来的开发提供了方向性指引。


最终方案:Cursor + Codebase 的组合拳

在多次试错后,我选择了 CursorCodebase 的组合方案。

  • Cursor 的核心能力
    作为一款高效的代码编辑工具,它可以快速处理代码片段,并在上下文切换上表现优异。

  • Codebase 的云端整合
    借助 RAG(检索增强生成)技术,Codebase 能够检索大型代码库并生成连贯的分析报告,突破了传统上下文限制。

尽管 Codebase 的云端操作存在代码泄露风险,但对于我的开源项目来说,这种风险在可接受范围内。

通过编写完备的提示词,我把基础的重构工作交给了 Cursor 自主完成,过程中我只需要少量的 review 参与,这帮助我 大大缩短 了项目代码重构的周期(原本项目已经乱的无法维护了,可以称为史山了),但目前还做不到真正的全自动重构,必须要架构师参与干涉,不干涉的话重构效果并不会太理想。


最后:技术的魅力在于创造与重塑

在重构遗留代码的过程中,我逐渐意识到,真正被重构的,是我们的思维方式。技术是一面镜子,它照见的不仅是问题的复杂性,也映射出我们对未来的想象力和对本质的追求。最终,技术的边界与我们认知的边界相连,而每一次探索,都在重塑我们看待世界的方式。

如果你喜欢我在 LINUX DO 发布的原创文章,不妨点个关注吧 ~ 大家的关注就是我更新的动力!下期见 ~

35 Likes

大佬太强了

感谢你的分享

很有意义哇。粗粗看了一眼,就感觉会对我很有帮助,帮我理解传下来的屎山代码。

待会认真看一遍

1 Like

没太看懂codebase是怎么使用的。

看了看codebase的官方描述,(可能是我看漏了)没看到RAG。

现在挺疑惑最终方案的实现方式

佬能解解惑吗?万分感谢

感谢分享大佬厉害啊

好!!!!!!

感谢分享,最终达到什么效果了呢?

codebase 是在 Cursor 中通过提示词嵌入直接跟大模型交互来使用的,像这样:


项目文件会自动同步到 codebase 的仓库中,以提供给大模型访问:

关于 RAG 的介绍可以自行了解,这里的 codebase 充当的就是一个大模型(或者称为智能体)的外接知识库的角色,用这种方式巧妙地规避上下文长度限制

2 Likes

效果不太直观,毕竟是项目重构,对整个项目都做了大幅度的更改。
主要就是:

2 Likes

谢谢佬的回答!(๑╹ヮ╹๑)ノ

看了一下cursor的文档,又对照了一下佬的图片。

好像佬并没有用到超链接指向的codebase。所以没有使用codebase这个工具,是这样吗?

晚上试试佬说的方法4。

感觉,4方法和佬说的3方法概念上差不多。都是 ①map:对块进行处理②reduce:利用块的信息进行输出。

4方法可能多个 Collapse Stage

1 Like

codebase 是作为一个小功能嵌入在 cursor 中的,就像我回复的图2,我不需要怎么配置,偶尔点一下 Resync Index 同步下代码到 codebase 的仓库就行

2 Likes

我可以这样理解吗:cursor自动的帮忙处理好了codebase,对于使用者而言,是一个不需要考虑的细节。但是在实际过程中,切切实实应用到了codebase。

2 Likes

挺有希望的工作的,我就很想将一些不被LLM所认知的代码文档下下来,丢给他们,并据此来开发工程,但是网站上的文档下的比较麻烦,很多相关项目要用npm,如果能出一个工具直接将文档作为模型上下文进行项目开发能省力不少

1 Like

感谢大佬教程!

1 Like

试了一下,没试成功。

我打开Github之后,发现提供的示例代码是调用OpenaiAPI的,直接跑了一下,发现它代码其实是给本地部署的模型的(直接代码调用model.tokenizer),一点为API做到调整都没有,好没诚意

我用GPT帮忙修改库文件,使得不调用tokenizer。使用4omini,我在给的例子上运行,query就是一样的获取关键点。得到的结果比原文还长。

我又试了一下对51万字符的文本(matlab代码+md)进行一样的query,不确定是我参数设置问题还是程序不合理(个人倾向这个),请求了17次。给了个没用的输出。

这个时候我感觉不太对劲。我开始看源码,所谓的map,就是"query+块",直接塞给模型;collapse,就是map的重复操作。reduce,还是map的重复操作。
真的是太离谱了

在github中提到的StructuredInfoProtocol、ConfidenceCalibrator,全是写文章的屁话。所谓的结构只是:

    {
        "extracted_info": result.get("text", ""),
        "rationale": result.get("rationale", ""),
        "answer": result.get("answer", "NO INFORMATION"),
        "confidence_score": self.calibrator.calibrate_score(result.get("rationale", ""))
    }

这里面前三个点全是模型生成的文本的复制粘贴(同一个“query+块”);第四个点只是一个 “特定词是否出现在rationale” 的操作。

你没有尝试方案四是正确的。我觉得你的方案3都比他有料。

整个代码太离谱,以至于我写着一段话的时候反复在确认我有没有误会他

没有试本地部署模型,太离谱了。

用的api是这里的,感谢rick佬(这里要不要at呢,我怕打扰awa)

哈哈哈 是的,我也试着顺着他的源码去适配第三方提供的 Open AI 模型,自己写 Wrapper、尝试 tiktoken 和 hugging face 提供的 tokenizer,结果还是有一大堆问题要解决,索性就放弃了,我没有细看他的源码

我方案3要借助 Dify 平台,关键是这个平台功能也不尽健全。我本来想着应该保存文件才是,但平台没有提供相关功能。那干脆点直接全部丢给 LLM 回复消息得了,结果前端也有 bug,大模型输出太长的文本会直接卡消失掉(很迷
最后不得已我尝试一下保存为语雀文档,好家伙语雀开发者 API 要高级会员
然后我又试了飞书,飞书的开发者平台保存文档又不知道给我保存到哪里去了,这个工作流也没有提供更进一步的日志给我查看,完全没办法排查问题

就挺折腾的说实话(

2 Likes

隐约感觉claude的project能做些事情

Cursor记得开隐私模式,RAG确实做的挺好的。