LLM驱动的增量式代码文档自动生成系统 1. 项目概述让文档和代码永远同步不是理想而是可落地的工程实践你有没有经历过这样的场景新同事入职第一天兴冲冲打开团队 Wiki想快速上手核心服务结果发现“用户鉴权流程”那一页画的还是三年前用 JWT 的老架构而实际代码里早就切到了基于 OAuth2.1 的动态策略引擎或者你花了一下午时间 painstakingly 补全了某个 SDK 的 API 文档刚提交 PR隔壁组就合并了一个重构 PR把所有函数签名全改了——你的文档还没来得及在 CI 里跑完测试就已经过期了。这不是个别现象这是软件工程里一个被长期忽视的“隐性债务”。我们总说“代码即文档”但现实是代码是真相而文档是快照快照一旦拍下就注定开始失真。标准答案“勤更新”根本没用它把一个系统性问题错误地归因于个人责任心。真正的解法不是让人更努力地对抗熵增而是把文档生成这件事从“人驱动的手动任务”彻底重构成“代码驱动的自动流水线”。这正是 CocoIndex 这个项目要解决的核心问题。它不是一个又一个漂亮的 Markdown 模板也不是一个能自动生成 docstring 的 IDE 插件。它是一个可编程的、增量式的、LLM 驱动的文档构建系统。它的核心思想非常朴素把文档看作源代码的一种“编译产物”。就像你写 C 代码.cpp是源码.o是中间目标文件.so是最终共享库一样Python 文件是源码FileAnalysis是结构化中间表示ProjectSummary是聚合后的语义摘要最终的project.md就是面向人类的“可执行文档”。这个过程不是单次的、静态的而是持续的、响应式的。你改一行代码系统只重新“编译”那个文件对应的文档片段你新增一个模块系统只“链接”那个模块的摘要你升级了 LLM 提示词系统会智能地识别出整个“编译规则”变了从而触发全量重编译。这种设计直接把 LLM 的调用成本从“每次全量扫描”降维打击到“只对变更点做微调”。我实测过一个中等规模的 Python 项目约 45 个.py文件首次全量生成耗时约 3 分钟花费约 $0.8后续仅修改其中 1 个文件并触发更新整个流程耗时 12 秒花费不到 $0.02。90% 的成本节省不是靠压缩 token而是靠工程化的思维重构了整个工作流。它适合所有正在被“文档与代码不同步”问题困扰的团队尤其是那些已经建立了成熟 CI/CD 流程却还在用 Confluence 手动维护技术文档的中大型研发组织。你不需要成为 LLM 专家只需要理解“文档即产物”这个理念就能立刻上手把文档维护这件苦差事变成 Git 提交后自动完成的后台任务。2. 核心设计思路为什么是 Pydantic Instructor 增量处理而不是别的方案2.1 结构化提取为什么不用正则、AST 或纯提示词在动手写任何一行代码之前我们必须先回答一个根本问题如何从一团杂乱的 Python 源码中稳定、可靠地抽取出我们真正关心的“元信息”常见的思路有三种用正则表达式暴力匹配class和def关键字用 Python 自带的ast模块解析语法树或者直接给大模型发一段提示词让它“看着代码告诉我里面有什么类、什么函数”。这三种方案我在过去五年里都踩过坑它们各自的问题非常典型。正则表达式方案看起来最“轻量”但它是典型的“短视方案”。它只能看到表面的字符串模式完全无法理解上下文。比如一个class Config:可能是 Pydantic 的配置类也可能是某个业务模块里的内部工具类正则根本分不清。更致命的是它对代码格式极度敏感。只要开发者多加了一个空格、换了一种装饰器写法app.routevsapp.get正则就可能失效。我曾经维护过一个用正则解析 Flask 路由的脚本光是为了解决app.route(/api, methods[GET])和app.get(/api)这两种写法就写了三版正则最后还是因为一个同事用了app.api_route而全线崩溃。AST 方案技术上最“正确”它能精确地理解代码的语法结构。但它的代价是巨大的复杂性和脆弱性。你需要手动遍历整个 AST 节点树区分ClassDef、FunctionDef、Assign等各种节点并且要自己处理作用域、继承关系、装饰器参数解析等所有细节。一个简单的coco.function(memoTrue)装饰器其 AST 结构就包含了Call、Attribute、Name等多个嵌套节点要准确提取出memoTrue这个参数值代码量轻松破百行。而且AST 是语言特定的今天是 Python明天你想支持 TypeScript就得重写一整套解析器。这违背了我们“文档即产物”的初衷——我们想要的是一个通用的、可移植的框架而不是一个绑死在某种语言 AST 上的专用解析器。纯提示词方案也就是直接让 LLM “自由发挥”听起来最“智能”但恰恰是最不可控的。大模型的输出是概率性的它可能会把一个私有方法_helper()错误地识别为公共接口也可能把import numpy as np里的np当成一个独立的依赖包。更麻烦的是它的输出格式完全不一致。一次返回 JSON一次返回 YAML甚至某次心血来潮给你返回了一段 Markdown 表格。没有强约束就没有稳定性。而文档生成恰恰是最需要稳定性的场景——你不能接受今天生成的文档里key_methods是一个列表明天就变成了一个字符串。Pydantic Instructor 的组合完美地规避了以上所有陷阱。Pydantic 的BaseModel不仅仅是一个数据验证类它本质上是一个声明式的、可执行的 Schema。当你定义name: str Field(descriptionFunction name)时你不是在写注释你是在向 LLM 下达一条精确的、不容置疑的指令“你输出的 JSON 里必须有一个叫name的字段它的值必须是字符串类型它的含义就是函数的名字。” Instructor 则是这条指令的“执行官”和“质检员”。它会把你的 Pydantic Schema 自动转换成一段极其精准的系统提示词system prompt并附在每一次 LLM 请求的开头。更重要的是它会在 LLM 返回结果后立刻进行反向验证如果返回的 JSON 缺少name字段或者is_entry_point的值不是布尔型Instructor 会毫不犹豫地拒绝这个结果并自动发起重试直到拿到一个 100% 符合你 Schema 的、结构化、强类型的输出。这相当于给 LLM 套上了一个“数字紧身衣”让它再也不能“自由发挥”只能在你划定的轨道上精准运行。这不仅是技术选型更是工程哲学的体现用确定性的代码去约束不确定的 AI而不是反过来。2.2 增量处理为什么 memoization 是 LLM 工程化的分水岭假设你已经解决了结构化提取的问题现在面临第二个更严峻的挑战成本。LLM API 不是免费的午餐尤其是当你需要处理几十个甚至上百个文件时成本会像滚雪球一样失控。一个 naive 的想法是“我写个脚本每天凌晨 2 点用 cron 调用一次全量扫描所有项目生成一遍新文档。” 这个方案在小项目上或许可行但在真实世界里它会迅速走向失败。原因很简单绝大多数时候代码库是静止的。一个拥有 50 个 Python 文件的项目一天之内可能只有 1-2 个文件发生了实质性变更。如果你每次都全量调用 50 次 LLM那么 95% 的调用都是在为“不变”的内容付费。这不仅是金钱的浪费更是时间的浪费——全量扫描可能需要几分钟而你的 CI 流程往往要求文档在代码合并后几秒内就可访问。CocoIndex 的coco.function(memoTrue)装饰器就是为了解决这个“静默成本”问题而生的。它的原理非常精妙却又异常简单以文件内容的哈希值如 SHA256作为缓存的 key。当你第一次调用extract_file_metadata(file_content, file_path)时CocoIndex 会先计算file_content的哈希值然后去本地缓存默认是 SQLite 数据库里查找是否存在这个 key 对应的FileAnalysis记录。如果没有就走正常的 LLM 调用流程拿到结果后连同输入的哈希值一起存入缓存如果找到了就直接返回缓存的结果跳过所有昂贵的网络请求和模型推理。这个设计的威力在于它把“是否需要重分析”这个决策从一个需要人工判断的、模糊的“逻辑问题”转化成了一个可以由机器 100% 精确执行的“数学问题”。文件内容哪怕只改了一个字符哈希值就会天翻地覆缓存必然失效文件内容一字未动哈希值就纹丝不动缓存必然命中。这种确定性是任何基于“文件修改时间戳”或“Git commit hash”的方案都无法比拟的。因为修改时间戳可以被轻易篡改touch命令而 Git commit hash 只能反映文件在仓库中的状态无法反映你本地正在编辑、尚未提交的草稿。CocoIndex 的哈希缓存是真正意义上的“内容感知”它让你的文档生成系统拥有了和 Git 一样的、对代码内容变化的终极敏感度。这才是 LLM 工程化从“玩具”走向“生产级”的关键一步。2.3 并发与编排为什么是 asyncio.gather()而不是多进程或 Celery在确定了“提取”和“缓存”这两个核心环节后最后一个关键的设计决策是如何高效地处理大量文件一个包含 100 个 Python 文件的项目是应该串行地一个一个分析还是并行地一起分析如果是并行该用哪种并发模型首先排除多进程multiprocessing。多进程虽然能利用多核 CPU但它启动开销巨大每个子进程都需要加载完整的 Python 解释器、所有依赖库包括litellm、instructor等对于一个 I/O 密集型主要是网络请求的任务来说这是巨大的资源浪费。而且进程间通信IPC比线程间通信慢得多管理起来也更复杂。再来看 Celery 这样的分布式任务队列。Celery 非常强大适合处理长时间运行、需要持久化、需要重试的后台任务。但对于我们的场景——一个最多持续几秒的、轻量级的 LLM API 调用——Celery 就像是用航空母舰去打蚊子。它引入了 Redis/RabbitMQ 等额外的基础设施依赖增加了部署和运维的复杂度而带来的收益却微乎其微。我们的目标是“轻量、快速、可嵌入”而不是“高可用、可伸缩、企业级”。asyncio.gather()是这个问题的黄金解法。它基于 Python 的异步 I/O 模型核心思想是“单线程多路复用”。当你的代码发起第一个await client.chat.completions.create(...)调用时它并不会傻等网络响应回来而是立刻挂起这个协程转而去执行第二个、第三个……第 N 个调用。所有的网络请求都在同一个事件循环event loop里并发地发出。当某个请求的响应数据到达时事件循环会自动唤醒对应的协程继续执行后续的解析和缓存逻辑。整个过程你只用了一个 Python 进程几乎没有额外的内存开销启动速度极快且与你的主程序无缝集成。我做过一个对比实验在一个有 20 个文件的项目上串行处理耗时约 42 秒平均每个 LLM 调用 2.1 秒而使用asyncio.gather()并发处理耗时仅为 2.3 秒。性能提升了近 18 倍。这背后的原因是LLM API 的延迟主要来自于网络往返RTT和服务器端的模型推理而这些时间CPU 其实是空闲的。asyncio就是把这段空闲时间充分利用了起来让 CPU 在等待 A 的响应时去发起 B、C、D 的请求。这是一种教科书级别的、针对 I/O 密集型任务的优化范式它让我们的文档生成流水线拥有了与现代 Web 服务相媲美的吞吐能力。3. 实操详解从零开始搭建你的第一个“活文档”流水线3.1 环境准备与依赖安装避开那些隐藏的“坑”在开始编码之前环境配置是成功的一半。我强烈建议你不要直接在全局 Python 环境里安装所有依赖而是创建一个干净的虚拟环境。这不仅能避免不同项目间的依赖冲突更能确保你的文档生成脚本在任何一台新机器上都能一键复现。以下是经过我反复验证的、最稳妥的步骤# 1. 创建并激活虚拟环境推荐使用 venv无需额外安装 python -m venv .doc-env source .doc-env/bin/activate # Linux/Mac # .doc-env\Scripts\activate.bat # Windows # 2. 升级 pip 到最新版非常重要旧版 pip 可能无法正确解析某些依赖的版本约束 pip install --upgrade pip # 3. 安装核心依赖 # 注意这里我们指定了 litellm 的版本因为 1.45.0 版本修复了一个与 Instructor 1.5.0 的兼容性 bug pip install litellm1.45.0 instructor pydantic cocoindex # 4. 可选但强烈推荐安装一个用于本地测试的轻量级 LLM # 如果你没有 Gemini API Key或者想在离线环境下快速验证流程可以安装 Ollama # curl -fsSL https://ollama.com/install.sh | sh # ollama pull llama3.2:1b # 下载一个 1B 参数的小模型足够用于文档提取 # pip install litellm[local] # 安装 litellm 的本地模型支持提示如果你在安装cocoindex时遇到ModuleNotFoundError: No module named setuptools_scm的错误不要慌。这是cocoindex的一个已知打包问题。解决方案是先单独安装setuptools-scmpip install setuptools-scm然后再重新运行pip install cocoindex。这个小插曲是开源世界里再常见不过的“版本依赖地狱”的一个缩影提前知道它能帮你省下至少半小时的 Google 时间。安装完成后最关键的一步是API Key 的配置。CocoIndex 默认使用 LiteLLM 作为统一的模型网关这意味着你可以无缝切换 Gemini、Claude、GPT 等任意支持的模型而无需修改一行业务代码。但前提是你得告诉它你的密钥在哪里。最安全、最符合 Unix 哲学的方式是通过环境变量# Linux/Mac export GEMINI_API_KEYyour_actual_api_key_here # 如果你用的是 OpenAI就换成 # export OPENAI_API_KEYsk-... # Windows (PowerShell) $env:GEMINI_API_KEYyour_actual_api_key_here注意绝对不要把 API Key 写死在 Python 代码里也千万不要提交到 Git 仓库这是一个基本的安全红线。我见过太多团队因为把OPENAI_API_KEY提交到公开仓库导致 API 配额在几小时内被刷爆账单飙升到数千美元。环境变量是隔离密钥和代码的最简单、最有效的方式。3.2 定义你的文档 Schema用 Pydantic 描绘“文档蓝图”Schema 是整个流水线的“宪法”它定义了你希望从代码中提取出什么样的信息以及这些信息应该长什么样子。一个设计良好的 Schema能让后续的所有工作事半功倍。让我们从一个最基础、但也最实用的FileAnalysis开始from pydantic import BaseModel, Field from typing import List, Optional class FunctionInfo(BaseModel): 描述一个公共函数的元数据 name: str Field(description函数的精确名称例如 process_data) signature: str Field(description函数的完整签名包括 async/def、参数类型和返回值例如 async def process_data(items: List[Item], timeout: float 30.0) - Result) summary: str Field(description用一句话概括该函数的核心职责聚焦于它‘做什么’而非‘怎么做’) is_entry_point: bool Field(description该函数是否是外部可调用的入口点例如被 app.route、coco.function 或 click.command 装饰的函数) class ClassInfo(BaseModel): 描述一个公共类的元数据 name: str Field(description类的精确名称例如 DataProcessor) summary: str Field(description用一句话概括该类的核心目的和职责) key_methods: List[str] Field(default_factorylist, description该类中最重要、最常被外部调用的公有方法名列表例如 [process, validate]) class FileAnalysis(BaseModel): 对单个 Python 源文件的完整结构化分析 file_path: str Field(description文件相对于项目根目录的路径例如 src/core/processor.py) summary: str Field(description用 2-3 句话概述该文件的整体功能和在项目中的角色避免罗列细节) public_classes: List[ClassInfo] Field(default_factorylist, description文件中所有公有类名称不以下划线开头的分析列表) public_functions: List[FunctionInfo] Field(default_factorylist, description文件中所有公有函数名称不以下划线开头的分析列表) dependencies: List[str] Field(default_factorylist, description该文件直接导入的关键外部依赖包名例如 [pandas, requests, fastapi]不包括标准库和本项目内的模块) mermaid_diagram: str Field(default, description一个 Mermaid flowchart 代码块清晰地展示该文件内主要类和函数之间的调用关系或数据流向。格式必须严格为graph TD; A -- B; B -- C;)这段代码看似简单但每一行都蕴含着深思熟虑的设计。Field(description...)不是注释它是你写给 LLM 的“操作手册”。summary字段的描述里强调了“聚焦于它‘做什么’而非‘怎么做’”这就是在引导 LLM 去做高层次的抽象而不是陷入琐碎的实现细节。dependencies字段明确限定了“外部依赖包名”并排除了“标准库和本项目内的模块”这就避免了 LLM 把import os或from .utils import helper这样的内容也当成依赖。mermaid_diagram字段的描述甚至精确到了语法层面graph TD; A -- B;这极大地提高了 LLM 输出格式的稳定性。我曾经尝试过不加这个限制结果 LLM 有时输出graph LR有时输出flowchart TD导致后续的 Markdown 渲染器无法识别。一个小小的、精确的描述就能换来 90% 的格式成功率。3.3 编写核心提取函数用 Instructor 将 LLM 变成你的“文档工人”现在Schema 已经定义好了接下来就是让 LLM 按照这张蓝图去干活。这就是instructor库大显身手的地方。下面是一个完整的、可直接运行的extract_file_metadata函数import instructor from litellm import acompletion from typing import Awaitable # 1. 创建一个被 Instructor 包装过的、支持异步的 LLM 客户端 # 这里我们使用 Gemini Flash因为它速度快、成本低非常适合文档提取这类任务 client instructor.from_litellm( acompletion, modeinstructor.Mode.JSON, # 强制 LLM 输出 JSON 格式 ) async def extract_file_metadata(file_content: str, file_path: str) - FileAnalysis: 使用 LLM 从 Python 源码中提取结构化元数据。 Args: file_content: Python 文件的原始文本内容 file_path: 文件的相对路径用于在提示词中提供上下文 Returns: 一个完全符合 FileAnalysis Schema 的、结构化的分析结果 # 构建一个精心设计的提示词Prompt # 核心原则清晰、具体、有上下文、有约束 prompt f 你是一位资深的 Python 架构师正在为一个自动化文档系统工作。 你的任务是严格根据以下 Pydantic Schema对提供的 Python 源码进行深度分析并输出一个完美的 JSON 对象。 【Schema 定义】 {FileAnalysis.model_json_schema()} 【待分析文件】 文件路径{file_path} 文件内容 python {file_content}【分析指令】聚焦公共 API只分析名称不以下划线开头的class和def。忽略所有__dunder__方法、_private函数和__all__之外的内部实现。总结要抽象summary字段必须是高层级的、面向业务的描述。例如不要写“这个函数调用了 pandas.read_csv”而要写“负责从 CSV 文件批量加载原始交易数据”。依赖要精准dependencies只包含import xxx或from xxx import yyy中的xxx。如果xxx是标准库如os,json或本项目内模块如from .models import User请忽略。Mermaid 图表要简洁只画最重要的 3-5 个组件及其关系。使用graph TD;语法箭头--表示调用或数据流向。例如DataProcessor -- DatabaseClient; DatabaseClient -- CacheService;输出必须是纯 JSON不要有任何额外的解释、Markdown 代码块包裹或前缀后缀。只输出一个合法的 JSON 对象。现在请开始你的分析。 # 2. 发起 LLM 调用指定 response_model 为我们的 FileAnalysis # Instructor 会自动处理 Schema 验证、重试和错误处理 response await client.chat.completions.create( modelgemini/gemini-2.5-flash, # 也可以是 gpt-4o 或 claude-3-haiku messages[{role: user, content: prompt}], response_modelFileAnalysis, # 这是魔法发生的地方 temperature0.1, # 降低温度让输出更确定、更一致 ) return response这段代码的精华就在 response_modelFileAnalysis 这一行。它告诉 Instructor“嘿我要的不是一个随意的字符串而是一个必须严格符合 FileAnalysis 这个类定义的 JSON 对象。” Instructor 会自动将 FileAnalysis 的 JSON Schema 注入到 LLM 的系统提示词中并在收到响应后用 Pydantic 的 model_validate_json() 方法进行校验。如果 LLM 返回了格式错误的 JSONInstructor 会捕获异常并自动构造一个新的、更加强调“必须输出 JSON”的提示词再次发起请求。这个过程最多重试 3 次确保你最终拿到的一定是一个可以放心使用的、强类型的对象。这比你自己写一堆 try...except json.loads() 然后手动检查字段的代码要优雅、健壮和可维护得多。 ### 3.4 实现增量处理与项目聚合让“活文档”真正活起来 有了单个文件的提取能力下一步就是把它们“组装”成一个项目的全景图。这一步同样需要增量处理的思想。我们不会为每一个文件都生成一个独立的 Markdown而是先聚合再生成。聚合函数 aggregate_project 的设计体现了“早退出”early exit的工程智慧 python from pydantic import BaseModel class ProjectSummary(BaseModel): 对整个项目的高层级、结构化摘要 name: str Field(description项目名称通常为项目根目录的文件夹名) summary: str Field(description用 2-3 句话描述项目的整体目标、核心价值和主要功能领域) key_components: List[str] Field(default_factorylist, description项目中最关键的 3-5 个组件类或模块的名称列表) architecture_diagram: str Field(default, description一个 Mermaid flowchart展示项目顶层的架构例如各微服务、核心模块之间的关系) coco.function(memoTrue) # 再次使用 memoTrue对整个聚合过程进行缓存 async def aggregate_project(project_name: str, file_analyses: List[FileAnalysis]) - ProjectSummary: 将多个文件的分析结果聚合成一个项目的统一摘要。 # 【早退出优化】如果只有一个文件直接提升其摘要无需 LLM 合成 if len(file_analyses) 1: fa file_analyses[0] return ProjectSummary( nameproject_name, summaryfa.summary, key_components[c.name for c in fa.public_classes] [f.name for f in fa.public_functions], architecture_diagramfa.mermaid_diagram, ) # 【多文件合成】构建一个更复杂的提示词引导 LLM 进行跨文件的关联分析 files_summary \n\n.join( f**{fa.file_path}**: {fa.summary}\n f核心类: {, .join(c.name for c in fa.public_classes) or 无}\n f核心函数: {, .join(f.name for f in fa.public_functions) or 无} for fa in file_analyses ) prompt f 你是一位系统架构师正在为一个名为 {project_name} 的项目编写一份面向新工程师的入门指南。 你已经获得了该项目下所有 Python 文件的详细分析报告如下所示。 你的任务是超越单个文件的视角提炼出整个项目的宏观图景。 【文件分析报告】 {files_summary} 【合成指令】 1. **项目概述**写一段 2-3 句话的概述回答“这个项目是干什么的”、“它解决了什么业务问题”、“它的核心价值是什么”。不要罗列文件要讲清楚故事。 2. **关键组件**从所有文件的分析中挑选出 3-5 个最核心、最常被其他部分引用的组件类或函数列出它们的名称。 3. **架构图谱**生成一个 Mermaid flowchart (graph TD;)展示这些关键组件之间的顶层关系。例如APIGateway -- AuthService; AuthService -- UserService; UserService -- Database;。只画最重要的主干忽略细节分支。 请严格按照 ProjectSummary Schema 输出 JSON。 response await client.chat.completions.create( modelgemini/gemini-2.5-flash, messages[{role: user, content: prompt}], response_modelProjectSummary, temperature0.2, # 比单文件提取略高一点允许一些创造性综合 ) return response注意aggregate_project函数本身也被coco.function(memoTrue)装饰了。这意味着它的缓存 key是由project_name和file_analyses这个列表的哈希值共同决定的。只要任何一个FileAnalysis对象的内容变了比如某个文件被修改了整个file_analyses列表的哈希值就会变aggregate_project就会重新执行。这种“缓存链式反应”保证了整个流水线的最终一致性。你改了一个函数不仅那个函数的文档会更新整个项目的architecture_diagram也会随之更新因为聚合层感知到了输入的变化。3.5 生成最终 Markdown从数据到可读文档的最后一步最后一步是将结构化的ProjectSummary和List[FileAnalysis]渲染成人类可读的 Markdown。这一步看似简单但却是用户体验的最终落点。一个设计良好的 Markdown 模板能让生成的文档立刻变得专业、易读def generate_markdown(project: ProjectSummary, files: List[FileAnalysis]) - str: 将结构化的项目摘要和文件分析渲染为美观、标准的 Markdown 文档。 lines [] # 1. 标题和概述 lines.extend([ f# {project.name}, , ## 概述, , project.summary, , ]) # 2. 架构图如果存在 if project.architecture_diagram.strip(): lines.extend([ ## 系统架构, , mermaid, project.architecture_diagram.strip(), , , ]) # 3. 关键组件列表 if project.key_components: lines.extend([ ## 核心组件, , 以下是项目中最关键的几个组成部分, , ]) for comp in project.key_components: lines.append(f- {comp}) lines.append() # 4. 详细文件清单仅当文件数 1 时才显示避免单文件项目显得冗余 if len(files) 1: lines.extend([ ## 详细文件说明, , ]) for fa in files: lines.extend([ f### {fa.file_path}, , fa.summary, , ]) # 如果该文件有重要的类或函数也列出来 if fa.public_classes: lines.append(**核心类:**) for cls in fa.public_classes: lines.append(f- {cls.name}: {cls.summary}) lines.append() if fa.public_functions: lines.append(**核心函数:**) for func in fa.public_functions: lines.append(f- {func.name}: {func.summary}) lines.append() return \n.join(lines) # 【实操现场记录】 # 我用上面的模板为一个真实的、包含 7 个文件的 FastAPI 项目生成了文档。 # 最终的 Markdown 在 GitHub 上渲染效果极佳 # - 标题层级清晰## 概述 和 ## 系统架构 让人一眼抓住重点 # - Mermaid 图表自动渲染为交互式流程图点击可放大 # - ## 核心组件 列表让新成员能快速定位到 UserService、DatabaseClient 这样的关键入口 # - ## 详细文件说明 部分则提供了按需展开的深度信息。 # 整个过程从运行命令到看到生成的 project.md耗时不到 8 秒。这个generate_markdown函数没有使用任何复杂的模板引擎如 Jinja2而是用最朴实的字符串拼接。原因很简单它足够简单足够可控且完全符合“文档即产物”的哲学——我们追求的是确定性而不是灵活性。一个固定的、经过充分测试的模板远比一个可以被任意定制、但可能引入 Bug 的模板更可靠。它生成的 Markdown是标准的、GitHub 原生支持的这意味着你无需任何额外的构建步骤就可以直接将生成的.md文件推送到你的项目仓库它就会在 GitHub 的 README 页面上完美呈现。4. 常见问题与排查技巧实录那些只有亲手做过才会懂的“坑”4.1 LLM 输出不稳定为什么我的mermaid_diagram总是为空这是新手遇到的第一个高频问题。你满怀期待地运行脚本结果生成的 Markdown 里## 系统架构那一节永远是空的。别急这几乎从来不是代码 bug而是提示词prompt和模型能力的博弈。根本原因Mermaid 图表的生成是整个提取任务中难度最高的一环。它要求 LLM 不仅要理解代码的静态结构还要推断出动态的、运行时的调用关系或数据流向。这对于一个只“看”了源码的模型来说本身就是一项超纲任务。很多模型尤其是小参数量的会直接选择“安全策略”既然我不确定那我就什么都不画。排查与解决先确认模型能力在你的终端里直接用litellm的 CLI 工具手动测试一下模型对 Mermaid 的理解。litellm --model gemini/gemini-2.5-flash --messages [{role: user, content: 请为一个简单的 Flask 应用生成一个 Mermaid flowchart包含 app.py, models.py, and views.py 三个文件以及它们之间的关系。}]如果返回的是一段乱码或完全无关的文本说明这个模型本身就不擅长 Mermaid。果断换一个gpt-4o或claude-3-opus是目前公认的最佳选择。强化提示词约束回到你的extract_file_metadata函数找到构建prompt的地方。在关于 Mermaid 的指令后面加上一句强有力的、带例子的引导# 在 prompt 字符串里添加这一行 4. **Mermaid 图表要简洁且有依据**只画你在源码中明确看到的 import、from ... import、obj.method() 或 Class() 这样的调用关系。例如如果 views.py 里有 from models import User 和 user User(), 那么图中必须有 views -- models 的箭头。这句话的关键在于“有依据”。它把一个模糊的、需要想象力的任务转化成了一个严格的、基于文本证据的模式匹配任务大大