1. 为什么要做“代码库变成书”这件事先交代一个背景有段时间我一直在啃 Spring AI Alibaba 的源码。这个项目体量不算夸张但牵扯到模型接入、工具调用、Agent 编排、Spring 生态集成读起来经常是“翻开一个类跳转三个依赖回头忘了主线在讲什么”。IDE 里开二十个标签页最后变成“哪里都是入口哪里都不是入口”。这种体验让我想明白一件事读大型代码库真正的瓶颈不是信息不足而是阅读体验的结构缺失。代码本身是有结构的包、模块、类的划分都在那里但这种结构是“索引式”的不是“叙事式”的。你可以在 IDE 里随意跳转但你很难顺着一条逻辑主线把整个项目读完。就像一座图书馆书架、分类号、索引卡片都齐备但没有人告诉你该从哪本书开始看再到哪本书。市面上的工具在解决这个问题时思路都偏向“增强跳转”或“完善检索”很少有人把目标设定为“把代码库变成一本可以一页页读的书”。我想要的不是又一份 API 文档也不是一张巨大的依赖关系图而是一份带着章节结构、有前因后果、能按顺序阅读的源码导读——也就是“书”。这个需求驱动我做了 AiReadCode。简单说它把 Git 仓库或本地目录作为输入经过代码分析、模块拆分、语义理解、章节聚合几个阶段最后输出一份 Markdown 格式的“书”。这份书有完整的目录树有章节间的交叉引用也能针对某个模块单独展开精读。本文就把我对这套系统的设计思路、实现细节、踩坑过程全部讲一遍。1.1 读大型代码库的真正痛点有人说“读源码要顺着调用栈追”这话对但只说对了一半。追调用栈适合定位具体问题时用它天然是发散式的一个方法可能有十几个调用方你要判断哪个分支才是主线一个接口有多个实现你要先分清默认实现和扩展实现。追几层之后你会获得一片“点的知识”但很难拼出“面的结构”。更难的是“顺序感”的缺失。好的技术书会先给你一张地图说明第三章是对第二章的实现延展第四章是独立模块可以跳过。而代码库不会给你这种地图README或许能算但大多数项目的README停留在“怎么跑起来”和“模块简介”永远到不了“为什么要这样设计”的层次。我把这个痛点拆成了三个具体问题没有推荐路径不知道第一行代码该读哪个类。没有篇幅预期每个模块到底多大多深从源码树看不出来。没有上下文连贯性读Controller时不知道它对应的Service在哪读Service时又忘了Controller的内部逻辑。这三个问题的共同点就是缺一个“写作”的过程。代码是静态文本但阅读是动态过程把静态文本改造成动态阅读体验本质上就是“写书”的工作。1.2 现有工具的盲区我评估过几类常见方案它们的出发点都很好但和“变成书”这个目标之间有一步距离。方案代表解决什么做不到什么IDE 源码跳转IDE 内置功能快速定位定义、引用不提供阅读顺序和章节组织API 文档生成Swagger、JavaDoc接口/类说明只描述“有什么”不解释“为什么”依赖图工具Dependencies 图看清模块间依赖图适合检索不适合线性阅读代码问答机器人各种 LLM 辅助编程工具单个问题的对话式解答离线后没有整体叙事问一句给一句重点是最后一行。代码问答工具本质上还是“关键词驱动”它能回答“这个模块怎么实现的”但不会主动告诉你“你该先看模块 A再看模块 B因为 A 的抽象在 B 里被用到”。这不是模型能力不够而是工具的交互范式就不支持长篇叙事。你得先去翻代码找到合适的问题然后才能得到合适的答案。而一本书的范式是我按目录读读到第三章自然知道第四章该登场了不需要我先有自己的问题。所以 AiReadCode 的核心不是“又一个 AI 代码助手”而是“一个把源码转化成可阅读长文的生成系统”。它要做的不只是“理解”还要“编排”。1.3 “一本书”的样子在动手之前我先想清楚了一个问题既然目标是把代码库变成书那么“书”在数据结构上应该长什么样我最终定下的抽象是三层结构Book书整个仓库的阅读入口包含前言、目录总览、章节间的关系图。Chapter章节一个模块或者一个核心机制。章节有自己的标题、摘要、正文和交叉引用。Section小节章节内的具体知识点通常对应一个核心类、一个流程链路或一组设计决策。这三层结构和代码库的对应关系是Book 对应仓库本身。Chapter 对应顶层模块比如spring-ai-alibaba下的core、starter、server等。Section 对应模块内的核心类簇或机制比如“模型接入的 SPI 设计”“对话上下文管理”这类主题。为什么用“机制”而非“类”来做最小单元因为源码阅读中最有价值的知识单元不是“某个类提供什么方法”而是“类与类之间如何协作完成一个目标”。以类为单位会复刻 IDE 的碎片化以机制为单位才能形成章节感。有了这三个模型剩下的工作就变成了用静态分析识别模块骨架用 LLM 理解模块内语义再用规则和校验器把碎片拼装成有顺序、有引用的章节。整个过程我放在下一节展开。2. 从 Spring AI Alibaba 源码中带走的三个设计启发既然标题提到是读了 Spring AI Alibaba 源码后才做的工具那这部分我必须如实交代这个项目的源码给了我哪些具体启发。不是客套是真的有三个设计理念直接影响了 AiReadCode 的架构走向。2.1 分层与边界模块拆分的第一课读这个项目源码时最直观的感受是它的模块边界极其清晰。核心抽象层、Spring 集成层、模型实现层各司其职上层依赖下层接口而非实现。比如接入不同的模型供应商时底层只需要实现统一接口上层完全不需要感知模型差异。这件事启发了我一个判断LLM 是不擅长判断“边界”的但工程代码本身就带着边界信息。与其让 LLM 从一堆文件里“悟”出模块划分不如先用静态分析工具解析包结构、module-info、依赖方向把模块边界硬算出来。LLM 只负责在已知边界内部做内容理解。所以在 AiReadCode 里第一版设计就把“结构分析”和“语义理解”拆成了两个阶段绝不在同一轮 Prompt 里让 LLM 同时完成“划分模块”和“写章节内容”。后来所有生成质量问题里凡是出现“章节串味”“模块内容张冠李戴”的基本都能追溯到这两个阶段被混在一起。这一条是血泪教训后文会再说。2.2 可插拔与默认值先跑通再优化的启发Spring 框架生态里到处是“可插拔 提供默认实现”的组合。比如你可以自定义一个Bean覆盖默认行为也可以直接用配置项驱动默认逻辑。这个哲学落到工具设计上就是每一层都要允许使用者无配置跑通同时预留替换实现的口子。AiReadCode 的管线是“扫描 - 骨架构建 - 语义增强 - 章节生成”每一层我都定义了接口但默认实现是“零配置”的。你拿到工具安装依赖填一个模型 API Key直接跑就能得到一本书。扫描层默认用 Python 的正则和文件树逻辑语义增强层默认用指定的 LLM 接口所有参数都有保守默认值。这个设计带来的直接收益是用户抱怨的门槛大大降低而且每一层都可以被独立替换。有人用自己的代码解析器替换扫描层有人把语义增强层换成了本地小模型。工具本身成了一个框架而不是一个黑盒。2.3 以文档为中心的自觉代码里的“路标”读 Spring AI Alibaba 源码时我发现优秀仓库的文件组织本身就有“文档感”。核心类的注释、接口的since标注、README里的架构描述、模块内的package-info.java这些都是“代码自文档化”的路标。这个观察让我做了一个决定AiReadCode 在生成章节时必须优先消费这些既有文档信号而不是抛开它们让 LLM 重新“发明”模块含义。具体做法是把README、package-info.java、关键类的 Javadoc 作为高权重输入。生成章节摘要时优先引用代码注释和文档中的原话再补充解释。如果代码内没有任何文档信号才完全依赖 LLM 从代码语义反推。这个策略有两层好处。第一层是质量有原话作为锚点生成的章节语义不容易漂移。第二层是成本直接复用已有描述比让 LLM 从零总结一段万字代码更节省 token。后来我对比过“带路标”和“不带路标”两种 Prompt 的输出后者明显更爱编概念名经常出现“模型中不存在的类名”这种幻觉。3. AiReadCode 的整体架构代码库进来书出去这一节讲系统全貌。我不打算贴一大堆 UML 图用文字把分层和数据流说清楚配上核心代码示例足够你复刻一套简化版。3.1 三层管线扫描层、理解层、编排层AiReadCode 由三个进程内阶段组成对应三种不同的任务属性。扫描层Scanner职责是解析文件树、识别语言、提取类/方法/依赖关系、读取注释和文档。它完全基于静态分析不调用任何模型能力缺点是不会“理解”优点是快、便宜、可缓存。理解层Understander职责是对扫描层给出的“骨架”进行语义填充。它会为每个模块生成摘要、解释核心机制、提炼设计意图。这一层完全由 LLM 驱动输入是扫描层的结构化输出 源码片段。编排层Organizer职责是汇总理解层的结果进行章节排序、交叉引用校验、去重、最终排版。编排层会做大量规则性检查不负责生成新语义只负责把已有内容变成书。三个阶段的调用顺序是流水线式的扫描层完成后理解层才能开始理解层全部章节生成完成后编排层才能进行全局加工。为什么不采用“边生成边编排”的流式方案因为我希望交叉引用是基于全局信息的如果章节尚未全部生成引用关系就只能靠预测容易出现错链。3.2 数据模型从 FileNode 到 Chapter扫描层的核心输出是一个中间表示我定义了四个主要模型类。dataclass class FileNode: path: str file_type: str # python/java/... imports: list[str] exports: list[str] doc_comment: str | None content_hash: str dataclass class ModuleSkeleton: name: str path: str file_nodes: list[FileNode] dependencies: list[str] # 依赖的其他模块名 entry_points: list[str] # 对外暴露的核心类 dataclass class ChapterOutline: module_name: str sections: list[SectionOutline] reading_order: int dataclass class BookDraft: chapters: list[ChapterOutline] global_notes: list[str]FileNode是文件级的最小单元ModuleSkeleton是模块级骨架ChapterOutline是章节大纲BookDraft是最终的整书草稿。这套模型有一个特点所有层级都只存“结构信息”和“文档信号”不存大段源码内容。真正的大段源码仅在理解层 Prompt 中临时拼接最终输出章节时也不把源码全文粘贴进去而是“引用位置 解释”。这么设计是为了避免一本书变成“代码打印版”。书的核心价值是导读和解释源码本身读者自然会去 IDE 看不需要一字不落塞进书里。3.3 章节发现不靠 LLM 猜靠工程信号最关键的工程决策在这里章节划分不是让 LLM 自由发挥的而是由工程信号驱动的。我提取了几类高置信度的信号模块边界信号按包名前缀、模块描述文件、目录层级划分。入口信号启动类、主配置类、对外暴露的Facade/Factory。依赖信号一个模块的核心类是否被其他模块大量引用如果是它的阅读优先级更高。扩展信号注解标记、SPI 接口、策略模式的实现类列表。以这些信号构建出“骨架大纲”后LLM 的工作只有两种一是写章节前导语解释“为什么这是独立章节”二是给小节补充设计背景回答“这一类簇在解决什么问题”。LLM 完全不需要在“该把哪些文件放一起”这件事上做决策。实际效果是什么章节划分的稳定度明显上升。纯 LLM 生成大纲两次跑同一仓库可能给出不同的章节结构而用工程信号驱动后除非代码发生了结构变化否则大纲是稳定可复现的。这对后续“增量更新”非常关键——用户改了代码后只需要重新生成变化的模块不用整本书推倒重来。4. 生成一本书的关键实现细节前面讲了架构骨架这部分进入真正的实现细节也是大家最容易踩坑的地方。我挑四个最有代表性的问题展开上下文窗口管理、Prompt 设计、交叉引用生成、一致性校验。4.1 控制上下文长度一次只讲一个模块永远不整库塞入最初版本我试过把整个仓库的扫描结果打包成一个大 Prompt 交给 LLM。效果非常糟糕原因有三大型项目扫描后仅文件清单就有几万个字符标书式的目录把真正关键的源码片段挤出了注意力。LLM 在过长上下文中更严重地遗忘“很久以前”的内容后面的模块分析质量断崖式下降。单次请求会触达上下文窗口上限一旦超限失败重试成本极高。于是我把生成策略改成“模块级会话”一次只给模型一个模块的源码片段和结构信息让它只输出该模块的章节内容。每个模块独立成章结束后由编排层统一汇总。具体切分规则是如果模块源码总行数 800 行直接作为一个 Prompt 单元。 如果模块超过 800 行先依据骨架中的 SectionOutline 再次切分 每个 Section 作为一个独立 Prompt 单元。 一个章节的单元总数上限是 8 个超过后按依赖顺序分批。800 行这个阈值来自实测超过这个量模型在“识别核心机制”上的表现就变得不稳定。另外切分时我刻意保持“一个类的内容完整出现在一个单元中”避免把一个类的import、字段、方法体拆到两次请求里。那会造成非常滑稽的错误比如第二章引用时说“该类在第 4 章定义”。4.2 Prompt 设计让模型当“源码审校员”而不是“小说家”LLM 生成代码导读时最容易犯的毛病就是“语气像百科编辑内容像编的”。为了压制这种倾向我写 Prompt 时反复用了三个约束。严禁输出代码仓库中不存在的类名。一旦涉及“关于某类”必须先确认它出现在输入源码片段中。只解释“代码做了什么 为什么这么设计”不要输出教材式的泛泛而谈。可以引用源码注释和文档中的原话但必须标注引用位置。下面是一个简化的 Prompt 模板核心思路是“给角色 给材料 给限制”你是一名资深源码分析工程师。我会给你一个模块的完整目录、文件列表、 核心代码片段和模块摘要。请完成以下任务 1. 用300字以内概括该模块在整体架构中的职责。 2. 列出你认为最重要的3~5个核心类每个类用2~3句话说明它解决什么问题 必须引用你从代码中看到的真实类名。 3. 如果发现该模块中存在“扩展点”或“策略接口”单独用一段说明。 约束 - 严禁编造类名或方法名。 - 解释需要基于代码片段中实际出现的实现。 - 输出格式为 Markdown不用额外客套话。这个模板跑出来的结果比早期版本好很多。主要改进是模型开始“对着代码说话”而不是“对着话题说话”。早期版本经常出现“此类默认实现提供了标准化的能力”这种空话现在的输出基本能精确到位比如“ModelConfigParser这个类读取 yaml 文件后构建ChatOptions链”。4.3 交叉引用与章节聚合最后一道工艺每章单独生成后书还是一堆孤立章节。编排层要做的第一件事是计算章节间的引用关系。规则很简单不依赖模型扫描所有章节正文提取出现的类名和模块名。如果类 A在X 模块被定义但在Y 模块的章节中被提到就在Y 模块正文中生成一个可点击链接形式的引用。如果某一章节被其他章节引用次数大于 5则在该章节开头生成一个“本节被以下章节引用”的提示块。这个环节做不了“模型判断”因为模型判断带有不确定性。基于类名的规则匹配虽然笨但可验证任何一条引用都能在代码库中找到对应位置。用 LLM 判断“某两段内容是否语义相关”也可以但那是锦上添花不能作为基础的引用依据。我在实际项目中除非用户显式开启“智能交叉推荐”否则默认只用规则匹配。聚合阶段的另一件事是“阅读顺序”排序。我用了很朴素的策略有依赖关系的模块被依赖者优先没有任何依赖关系的模块按代码体积从大到小排列。排序结果会写入每章reading_order字段最终转换为书目录里的编号。4.4 一致性校验让“书”更像书的关键生成完初稿后还有一道关卡校验器。它会做三类检查。物理检查所有提及的类名、方法名是否真实存在于代码库中。这需要和扫描层的数据比对发现一次幻觉就要把该章节标记为“待重写”。逻辑检查章节摘要与模块实际职责是否相符。具体做法是抽取摘要中的关键名词看看这些名词对应的类是否真的属于该模块。引用完整性检查如果章节 A 引用了章节 B 的概念确保 B 确实存在且有对应描述。一旦检查出不一致我不会直接丢弃这一章节而是将校验器输出的问题描述重新作为额外 Prompt 输入让模型在“局部修正”模式下重写该章节。这个“生成-校验-重写”闭环是整本书质量的最后防线。5. 实测效果、成本与调优记录光讲设计不讲实测数据总觉得不够硬核。这部分我说一下用实际仓库跑出来的效果、成本开销以及三次我印象最深的调优改动。5.1 用 Spring AI Alibaba 试跑我拿到了什么样的“书”拿它做实验一方面是因为我对这个项目相对熟另一方面是这个项目的模块结构非常适合验证“边界分析”的正误。跑完之后生成的书目录类似这只是示例结构非逐字复制第1章 项目总览与模块地图 第2章 core 模块统一抽象层设计 2.1 模型接入接口的 SPI 定义 2.2 消息结构与对话上下文管理 2.3 工具调用的链路实现 第3章 starter 模块Spring Boot 自动装配 3.1 自动配置类的激活条件 3.2 自定义配置项到模型参数的映射 第4章 server 模块网关与应用示例 4.1 请求路由与鉴权适配 4.2 流式响应的封装方式对比手动通读源码这份目录已经能让人“按图索骥”。拿到书后一个没有读过该项目源码的人花费约二十分钟读完前两章就能回答“这个项目怎么扩展一个新模型”这种原本需要翻很多代码才能回答的问题。这是我觉得这套工具最值得的落地场景团队新成员上手老项目的认知加速器。5.2 token 成本与并发策略LLM 生成整本书不是免费的成本把控不当很容易失控。我记录的实测数据大概如下用某标准模型的价格折算阶段输入 token输出 token估算成本每千 token 计扫描层000理解层摘要核心类模块源码片段章节正文主要成本来源编排层章节全文修正报告中等成本校验重写章节正文问题描述重写章节平均每章额外 1/3 成本优化思路也很直接能不用 LLM 的环节就不用。比如摘要生成时如果README里已经有项目模块的一句话简介我直接复用简介再让模型扩写成 300 字而不是从零生成。这个改动让理解层输入 token 大约下降了 30%。并发上我采用“模块级并行 章节级串行”的策略。不同模块之间没有依赖同时跑 4~6 个模块没任何问题但同一个模块内的章节必须按顺序生成因为后一章的 Prompt 里可能引入前一章生成的小结作为参考。5.3 三次重要的调优改动第一次改动是“从整库 Prompt 改为模块级 Prompt”解决的是输出质量断崖问题前面已经详述。第二次改动是“加入静态扫描信号权重”。早期版本对代码结构信号和源码内容是一视同仁塞进 Prompt 的导致模型经常忽略import关系把两个无关类说成“紧密协作”。我把扫描得到的依赖关系独立成一段结构化文本并明确告诉模型“这是机器提取的准确依赖关系你的解释必须与之一致”生成逻辑一下就准了很多。第三次改动是“重写闭环”。初版没有校验和重写环节生成的书偶尔出现完全虚构的类名比如“该类通过DynamicProxyFactory实现代理”。如果读者按图索骥去找这个类会发现根本不存在这种错误非常伤信任。加了校验器后这类幻觉约 90% 会被拦截剩下 10% 会在“重写”环节被修复。6. 做这种工具最容易踩的坑最后一部分我总结几个做“代码理解生成”类工具最常遇到的问题提前说出来能帮你省掉不少弯路。6.1 上下文泄漏章节之间的越权引用这是最隐蔽的问题。生成第二章时模型可能“回忆起”第一章甚至全书的内容在自己的章节里过度引用其他模块。如果引用正确还好但大部分时候模型会猜错生成“如第三章所述”但第三章根本没讲过这件事。我的对策生成单个章节时Prompt 里只放该模块的信息如果确实需要引用其他模块必须走编排层的交叉引用通道由规则引擎来补充而不是让模型自己“联想”。模型的联想式引用最好让它在写作时禁止在聚合时再放开。6.2 LLM 幻觉的三种表现代码理解场景下的幻觉有固定的三种模式我总结成表幻觉类型表现拦截手段类名幻觉出现源码中不存在的类名扫描层实体名比对关系幻觉声称 A 调用 B实际没有依赖解析结果比对意图幻觉声称模块是为了支持某种场景实际代码毫无相关逻辑人工审阅 摘要关键词词频分析第三种最难缠因为它“看起来合理”。对付它的唯一有效手段是提高生成时的证据要求Prompt 里明确要求“你说出的每个设计意图必须伴随至少一个源码证据类名、方法名、注释原话”。没有证据支撑的断言在最终输出里应该被标记为“推测”而不作为事实叙述。6.3 成本曲线书越长重写越贵生成一本书的成本不是线性增长的。章节越多交叉引用越复杂校验器检查出的问题也越多重写比例会上升。实测中超过 10 个模块的大仓库重写成本占总成本比例从 25% 升到 40%。控制方法只有一个尽量在生成阶段就把质量做对。具体操作是严格控制每章输入信息量、保留文档信号、给模型足够的“证据要求”而不是指望校验环节反复返工。返工一方面贵另一方面模型在同一点上的修正效果会递减第二次重写往往很难再改善多少。6.4 输出稳定性同一仓库两次生成结果不一致怎么办LLM 的温度和随机性导致两次生成的书可能细节不一致。如果只是措辞差异问题不大但如果是章节结构或核心类选择不一致就很影响体验。我的处理方案是结构部分完全不用 LLM。章节划分、阅读顺序、核心类的候选集全部由扫描层规则决定。LLM 只在既定结构内填充内容和微调措辞。这样即使两次生成的书在具体句式上略有不同目录结构、章节顺序、核心类选定都是完全一致的。这一步执行到位后我几乎没有再收到过“两次生成完全对不上”的反馈。最后再分享一个实操技巧如果你也想做一个类似的东西最值得先看的地方不是 Prompt 写得多巧妙而是扫描层到底能不能提取出准确的实体关系。这不是 LLM 问题而是工程问题。LLM 负责“写得好不好”但“写得对不对”完全依赖扫描层给的输入。哪怕 Prompt 写得天花乱坠扫描层丢了一个关键依赖生成的内容就会在那一处失真。我自己在打磨 AiReadCode 时花在扫描层上的时间远超过 LLM Prompt 调优的时间。给每个语言写解析规则、处理别名导入、识别模块入口这些琐碎工作才是决定工具上限的环节。开始动手的朋友不妨先把时间投到这部分。另外一个小建议第一次跑通时选一个中规模的仓库比如两三百个文件的那种不要选巨型项目。小项目跑完整条链路后你会对结构和成本都有体感再去处理大项目就不会雾里看花。AiReadCode 现在的新手模式默认就是用中型仓库做演练。我的体会是做这类工具的迭代节奏最好是“小步快跑 高频手动审查”等质量稳定了再考虑自动化输出否则很容易在一堆不可控的生成内容里迷失方向。