1. 项目概述:让文档和代码永远“同频呼吸”
你有没有经历过这样的崩溃时刻?——刚花三天写完一份详尽的API调用说明,上线后发现开发同事悄悄改了两个参数名,文档里还写着旧字段;或者新成员入职,拿着你精心整理的“核心模块流程图”去调试,结果发现图里画的分支逻辑早在上个月的重构中就被删掉了;更常见的是,每次发版前,技术文档团队像消防员一样满世界追着各模块负责人要“最新变更点”,最后交出来的PDF里,一半是准确的,一半是“据上次确认应该没变”。这不是懒,也不是不重视,而是传统文档工作流和代码演进节奏之间存在一道天然的、不可忽视的时差。这个项目标题里的“LLM-Powered Documentation that Always Stays True to latest codebases”,说的不是给文档加个AI聊天框,而是要从根本上消灭这种时差,让文档不再是代码的“滞后快照”,而成为代码的“实时镜像”。它背后的核心关键词是代码即文档源(Code-as-Source-of-Truth)、自动化提取(Automated Extraction)、语义增强(Semantic Enrichment)和持续同步(Continuous Synchronization)。简单说,就是把文档的“心脏”直接接到代码库的“动脉”上,每一次git push,都自动触发一次文档的“心跳检测”与“微更新”。它适合所有被“文档过期”问题困扰的团队:中小技术团队没有专职文档工程师,靠开发者兼职维护,急需降低维护成本;大型平台型产品,接口和SDK版本繁多,人工维护文档极易出错;开源项目维护者,希望贡献者能零门槛理解架构,同时确保社区文档与主干代码严格一致。这不是一个“锦上添花”的AI玩具,而是一个解决工程实践中真实痛点的基础设施级方案。
2. 整体设计思路:为什么必须放弃“人写文档”的老路?
2.1 传统文档模式的三大死结
要理解这个LLM驱动方案的价值,得先看清旧模式的病根在哪里。我带过三个不同规模的团队,踩过所有坑,总结下来,传统方式有三个无法靠“加强管理”或“增加人力”来根治的硬伤。
第一是时间不可逆性。代码的修改是原子性的、瞬时的,而人的认知和书写是线性的、耗时的。一个开发者修复一个安全漏洞,可能5分钟就提交了PR,但等他抽空更新对应的“安全配置指南”,可能要等到下周的站会之后。这中间的空白期,就是线上事故的温床。我们曾有个内部工具,其鉴权逻辑在一次热修复中从JWT切换到了Session+CSRF Token,但文档里还清晰地印着如何构造JWT Header。新来的运维同学按图索骥,反复失败,最后花了两小时才在Git历史里翻到那次提交。这不是他的问题,是流程的问题。
第二是信息粒度失配。开发者写代码时,关注的是函数签名、入参校验、异常抛出这些精确到字节的细节;而写文档时,却被迫切换成“用户视角”,要解释“为什么需要这个参数”、“这个错误码意味着什么业务场景”。这种视角切换本身就会丢失信息。更麻烦的是,代码里大量隐含的约束——比如某个字段只在特定状态机下才有效,或者某个回调函数的执行时机依赖于外部服务的响应延迟——这些根本不会出现在函数注释里,但对使用者至关重要。人工文档要么忽略,要么靠经验“脑补”,后者风险极高。
第三是维护动力衰减。这是最残酷的现实。当一个功能上线、一个Bug修复,开发者的成就感来自“代码跑通了”、“测试通过了”。更新文档?它既不产生可量化的业务指标,也不在任何CI/CD流水线里,甚至没有一个明确的Owner。久而久之,文档就成了那个“大家都知道该做,但谁都不想第一个动手”的灰色地带。我见过最典型的案例,是一个核心数据处理模块,其文档最后一次更新时间戳是2021年,而代码库在过去两年里经历了47次重大重构。团队里没人敢说自己100%理解当前逻辑,因为没人敢信那份文档。
2.2 LLM不是“写手”,而是“翻译器”与“连接器”
所以,这个方案里的LLM,绝不是用来替代人类写初稿的。它的角色定位非常清晰:一个高精度、可编程的语义翻译器,以及一个智能的上下文连接器。它的输入不是模糊的“请帮我写一份API文档”,而是结构化、可验证的代码元数据(AST、类型定义、注释块)和经过清洗的代码变更日志(Diff)。它的输出也不是一篇完整的散文,而是一组带有强约束的、可嵌入现有文档框架的结构化片段(如OpenAPI Schema、Mermaid流程图DSL、Markdown表格)。换句话说,LLM在这里不负责“创作”,而负责“转译”和“补全”。
举个具体例子。假设有一段Python函数:
def calculate_discount( base_price: float, user_tier: Literal["bronze", "silver", "gold"], is_first_order: bool = False, coupon_code: Optional[str] = None ) -> Dict[str, Union[float, str]]: """ Calculate final price after applying tier-based and coupon discounts. Note: Coupon discount is capped at 30% for silver users. """传统方式下,文档工程师要读这段代码,再结合业务知识,手动写出:
user_tier: 用户等级,枚举值:bronze,silver,gold。coupon_code: 可选优惠券代码。注意:银卡用户使用优惠券时,折扣上限为30%。
而LLM驱动的流程是:
- 静态分析器(如
pyright或mypy)提取出user_tier的类型是Literal["bronze", "silver", "gold"],coupon_code是Optional[str]; - 注释解析器提取出docstring中的关键约束:“capped at 30% for silver users”;
- LLM模型(经过微调的
CodeLlama-13b)接收这两份结构化输入,生成一个标准化的JSON Schema片段,并自动将“capped at 30% for silver users”这条业务规则,映射到Schema的description字段,同时在x-business-rules扩展属性里进行结构化存储。
这个过程的关键在于,LLM的“发挥空间”被严格限定在“如何把机器可读的代码事实,翻译成人类可读的、符合文档规范的自然语言描述”这一狭窄通道内。它不编造逻辑,不猜测意图,它的所有输出,都必须有上游的静态分析结果作为锚点。这就从根源上杜绝了“AI幻觉”污染文档的风险。
2.3 架构选型:为什么是“LLM + 静态分析 + Git Hooks”铁三角?
整个系统的骨架,由三个不可替代的组件构成,缺一不可,我称之为“铁三角”。
第一角:静态分析器(Static Analyzer)。这是整个系统的“眼睛”和“触觉”。它必须能深入代码的语法树(AST),精准识别函数、类、方法、参数、返回值、类型注解、文档字符串(Docstring)以及它们之间的关系。对于Python,我们首选pyright(微软出品,类型检查极其严谨);对于TypeScript,tsc --declaration生成的.d.ts文件是黄金标准;对于Java,则用javadoc配合javap反编译工具链。选择它们的唯一理由:结果100%可验证、可复现、无歧义。一个pyright报告的类型错误,和你在IDE里看到的,必须完全一致。这是信任的基石。
第二角:Git Hooks与CI/CD集成。这是系统的“神经中枢”和“执行引擎”。我们不依赖任何中心化的“文档服务器”去轮询代码库。相反,我们在每个开发者的本地pre-commit钩子,以及CI流水线的post-merge阶段,都部署了轻量级的同步脚本。pre-commit钩子的作用是:当你准备git commit -m "fix: update payment validation"时,脚本会自动扫描本次提交中所有被修改的.py文件,调用静态分析器提取变更点,然后触发LLM生成对应的文档增量更新,并将其以patch形式暂存。这样,你的每一次提交,都天然携带了“这次改了什么,文档该怎么同步”的元信息。而CI流水线里的post-merge则负责将这些patch合并、渲染,并推送到文档站点。这种设计的好处是极致的轻量和可靠——它不增加任何新的服务依赖,完全运行在现有的Git和CI基础设施之上。
第三角:微调后的领域专用LLM。这是系统的“大脑”,但它的“智力”是被精心驯化的。我们没有用通用大模型(如GPT-4)直接处理代码,而是基于CodeLlama-13b,用我们自己积累的10万行高质量代码-文档对进行监督微调(SFT)。训练数据全部来自我们过去三年里,那些被证明“从未出错”的、由资深架构师手写的文档片段。微调的目标非常明确:让模型学会在“代码事实”和“文档表达”之间建立最短、最可靠的映射路径。例如,当模型看到Optional[str]和"default value is empty string"的组合时,它必须稳定地输出“可选字符串,默认为空字符串”,而不是“字符串类型,可以为空”。这种确定性,是通用模型无法保证的。
这三个组件环环相扣:静态分析器提供绝对可信的事实,Git Hooks提供即时、分布式的触发机制,而微调LLM则在事实与表达之间架起一座高保真、低延迟的桥梁。任何试图绕过其中一角的方案,最终都会在“准确性”、“时效性”或“可靠性”上打折扣。
3. 核心细节解析:从代码到文档的每一处精微操作
3.1 静态分析:如何从一行代码里榨取出所有文档价值?
静态分析不是简单的正则匹配,而是一场对代码结构的深度考古。它的目标,是从冰冷的字符序列中,还原出开发者埋藏在代码里的所有“意图信号”。我以Python为例,详细拆解我们实际使用的分析流程。
首先,AST解析是起点,但绝非终点。ast.parse()能帮你构建语法树,但它对类型注解的支持非常有限。真正的关键,在于pyright的--lib模式。我们运行pyright --lib --outputjson src/,它会输出一个包含数千行JSON的pyright.json文件。这个文件里,每一个函数节点都附带了完整的类型信息,例如:
{ "name": "calculate_discount", "type": "function", "parameters": [ { "name": "base_price", "type": "float", "isOptional": false }, { "name": "user_tier", "type": "Literal[\"bronze\", \"silver\", \"gold\"]", "isOptional": false } ], "returnType": "Dict[str, Union[float, str]]", "docString": "Calculate final price after applying tier-based and coupon discounts.\nNote: Coupon discount is capped at 30% for silver users." }这份输出的价值在于,它已经完成了最难的一步:将动态的、可能出错的类型推断,固化为静态的、可验证的字符串。Literal["bronze", "silver", "gold"]这个字符串,就是我们后续所有文档生成的“圣杯”。它比任何人工写的“枚举值:bronze, silver, gold”都更权威,因为它直接来自类型检查器的结论。
其次,Docstring的结构化解析。很多团队的docstring是自由格式的,这给自动化带来巨大挑战。我们的解决方案是强制推行Google Style Docstring,并用pydocstyle进行CI门禁。一个合规的docstring长这样:
def calculate_discount(...): """Calculate final price after applying tier-based and coupon discounts. Args: base_price (float): The original price before any discount. user_tier (Literal["bronze", "silver", "gold"]): User's membership level. is_first_order (bool, optional): Whether this is the user's first order. Defaults to False. coupon_code (Optional[str], optional): A valid coupon code. Defaults to None. Returns: Dict[str, Union[float, str]]: A dict containing 'final_price', 'discount_amount', and 'reason'. Raises: ValueError: If coupon_code is invalid or user_tier is unknown. Note: Coupon discount is capped at 30% for silver users. """我们的解析器会将Args、Returns、Raises、Note等区块分别提取为独立的JSON字段。特别重要的是Note区块,它承载了那些无法被类型系统捕获的、至关重要的业务规则。这些规则,正是LLM最擅长处理的“非结构化知识”。
最后,变更差异(Diff)的语义化理解。git diff输出的是行级别的文本差异,但我们需要的是“语义差异”。例如,当一行代码从def process(data: List[str]) -> str:变成def process(data: List[Union[str, int]]) -> str:时,git diff只会显示List[str]变成了List[Union[str, int]]。但我们的分析器会进一步解读:这是一个参数类型的放宽(Widening),意味着函数现在能接受更广泛的输入,这通常意味着向后兼容,但文档中关于data参数的描述必须更新,强调其支持整数类型。这个“类型放宽”的语义标签,会被注入到LLM的提示词(Prompt)中,指导它生成更精准的变更说明。
提示:静态分析器的输出必须经过严格的Schema校验。我们为
pyright.json的输出定义了一个Pydantic Model,并在每次分析后运行model.validate()。任何不符合Schema的输出,都会导致整个文档生成流程中断并报警。宁可文档不更新,也不能让错误的文档上线。
3.2 LLM提示工程:如何给大模型下一道“不能答错”的指令?
很多人以为,给LLM写个Prompt,让它“根据代码生成文档”就完了。实测下来,这种做法的失败率超过90%。原因很简单:通用Prompt给了模型太多自由发挥的空间,而文档的首要原则是精确,不是“生动”。我们的Prompt设计,遵循“三明治”结构:强约束的开头 + 结构化的中间 + 硬性校验的结尾。
开头:设定绝对不可逾越的边界。我们开篇就用大写字母声明:
YOU ARE A DOCUMENTATION ENGINEER FOR A FINANCIAL PAYMENT SYSTEM. YOUR OUTPUT MUST BE 100% DERIVED FROM THE PROVIDED CODE METADATA. YOU MUST NOT INVENT, GUESS, OR ASSUME ANYTHING. IF A FACT IS NOT PRESENT IN THE INPUT, YOU MUST OMIT IT. YOUR OUTPUT IS A DIRECT TRANSLATION, NOT AN INTERPRETATION.这段话不是礼貌用语,而是法律条款。它在模型的“思维链”(Chain-of-Thought)启动前,就锁死了所有可能的幻觉路径。
中间:提供结构化模板与示例。我们不给模型自由写作的空间,而是提供一个填空式的JSON Schema模板,并附上两个高质量的Few-Shot示例。模板长这样:
{ "function_name": "string", "summary": "string, one sentence summary derived ONLY from docstring", "parameters": [ { "name": "string", "type": "string, derived from type annotation", "description": "string, derived from docstring's Args section, enriched with business rules from Notes", "required": "boolean" } ], "returns": { "type": "string, derived from return type annotation", "description": "string, derived from docstring's Returns section" }, "raises": [ { "exception": "string, derived from Raises section", "reason": "string, derived from Raises section" } ], "business_rules": ["string, extracted verbatim from Notes section"] }两个示例,一个是上面提到的calculate_discount函数,另一个是一个更复杂的、涉及状态机的process_payment函数。这两个示例,教会模型如何将Literal["bronze", "silver", "gold"]映射到type字段,如何将Note: ...的内容塞进business_rules数组,而不是揉进description里。
结尾:强制输出格式与校验。Prompt的最后一句是:
OUTPUT ONLY VALID JSON. DO NOT ADD ANY EXPLANATION, COMMENT, OR MARKDOWN. YOUR OUTPUT WILL BE PARSED BY A JSON SCHEMA VALIDATOR. IF IT FAILS VALIDATION, THE DOCUMENTATION BUILD WILL FAIL.这句话的效果立竿见影。它让模型明白,它的“考试卷”不是一段文字,而是一个必须能被json.loads()成功解析的字符串。任何多余的空格、换行、引号,都会导致构建失败。这种“零容忍”的压力,反而极大地提升了输出的稳定性。
注意:我们所有的Prompt都存储在Git仓库的
/docs/prompts/目录下,并随代码一起版本化。每一次文档生成失败,我们首先检查的,就是Prompt是否被意外修改。这确保了文档生成行为的可追溯性和可重现性。
3.3 文档渲染与发布:如何让LLM的输出真正“活”在文档网站上?
LLM生成的JSON,只是原材料。要让它变成开发者每天打开的、可搜索、可跳转的在线文档,还需要一套精密的渲染与发布流水线。这套流水线的设计哲学是:LLM负责“内容生成”,而渲染引擎负责“内容呈现”,二者解耦,互不干扰。
我们采用的是“文档即代码(Docs-as-Code)”的现代实践,整个文档站点基于Docusaurus v3构建。Docusaurus的核心优势在于,它原生支持从JSON、YAML等结构化数据源自动生成页面。我们的流程如下:
LLM输出归档:每次CI流水线运行,LLM生成的JSON会被保存为
/docs/generated/api/v1/calculate_discount.json。这个路径与函数名严格对应,形成了一种“约定优于配置”的映射关系。Docusaurus插件注入:我们编写了一个自定义的Docusaurus插件
@myorg/docusaurus-plugin-api-docs。这个插件在Docusaurus的loadContent生命周期中被调用,它会扫描/docs/generated/api/下的所有JSON文件,并将它们注册为Docusaurus的“数据源”。React组件动态渲染:在文档页面的MDX文件中,我们不再手写HTML,而是使用一个自定义的React组件
<ApiMethod method="calculate_discount" />。这个组件在运行时,会从Docusaurus的数据源中,按需加载calculate_discount.json,并将其内容渲染为一个标准的、带交互式参数表格、折叠式示例代码块的API卡片。
这个设计的妙处在于,它实现了内容与表现的彻底分离。如果哪天我们想把参数表格改成横向布局,或者给business_rules添加一个醒目的黄色警告图标,我们只需要修改<ApiMethod>组件的JSX代码,而无需碰任何一个JSON文件。反之,如果LLM的生成逻辑升级了,比如开始支持输出更丰富的错误码列表,我们只需要更新JSON Schema和LLM的Prompt,前端组件会自动适配,因为它的数据结构是强契约的。
此外,我们还集成了实时搜索索引。Docusaurus的@docusaurus/plugin-algolia插件,会自动将JSON中的summary、description、business_rules等字段,注入到Algolia搜索索引中。这意味着,当一个开发者在文档站搜索“silver user 30% cap”,搜索结果会直接高亮指向calculate_discount这个API,而不是让他在茫茫大海中寻找那句被淹没在长篇大论里的Note。这种“所搜即所得”的体验,是传统静态文档网站永远无法企及的。
4. 实操过程:从零搭建一个可运行的LLM文档系统
4.1 环境准备与工具链安装
搭建这个系统,不需要昂贵的GPU服务器,一台普通的开发机(16GB内存,8核CPU)就足以胜任。整个过程分为四个步骤,我建议你按顺序执行,每一步都附带了验证命令。
第一步:安装核心依赖
# 创建一个干净的Python虚拟环境 python3 -m venv llm-docs-env source llm-docs-env/bin/activate # 安装静态分析器 pip install pyright typeshed-client # 安装LLM推理框架(我们选用Ollama,轻量且跨平台) # macOS: brew install ollama && ollama pull codellama:13b-instruct-q4_K_M # Linux: curl -fsSL https://ollama.com/install.sh | sh && ollama pull codellama:13b-instruct-q4_K_M # Windows: 下载Ollama官方安装包,然后运行 `ollama pull codellama:13b-instruct-q4_K_M` # 安装文档站点框架 npm install -g docusaurus-cli第二步:初始化Docusaurus站点
# 在项目根目录下创建文档站点 npx create-docusaurus@latest my-docs --package-manager npm # 进入文档目录,安装我们自定义的插件 cd my-docs npm install @myorg/docusaurus-plugin-api-docs # 在docusaurus.config.js中注册插件 # 添加以下配置到plugins数组: // [ // '@myorg/docusaurus-plugin-api-docs', // { // path: '../docs/generated/api', // 指向LLM生成的JSON目录 // }, // ],第三步:编写第一个LLM提示词(Prompt)在项目根目录下创建/prompts/generate_api_doc.txt,内容如下(这就是我们前面提到的“三明治”Prompt):
YOU ARE A DOCUMENTATION ENGINEER FOR A FINANCIAL PAYMENT SYSTEM. YOUR OUTPUT MUST BE 100% DERIVED FROM THE PROVIDED CODE METADATA. YOU MUST NOT INVENT, GUESS, OR ASSUME ANYTHING. IF A FACT IS NOT PRESENT IN THE INPUT, YOU MUST OMIT IT. YOUR OUTPUT IS A DIRECT TRANSLATION, NOT AN INTERPRETATION. INPUT FORMAT: { "function_name": "string", "type_annotation": "string", "docstring": "string", "diff_type": "string (e.g., 'type_widening', 'signature_change')" } OUTPUT FORMAT (VALID JSON ONLY): { "function_name": "string", "summary": "string", "parameters": [{"name": "string", "type": "string", "description": "string", "required": "boolean"}], "returns": {"type": "string", "description": "string"}, "raises": [{"exception": "string", "reason": "string"}], "business_rules": ["string"] } EXAMPLE 1: INPUT: {"function_name": "calculate_discount", "type_annotation": "def calculate_discount(base_price: float, user_tier: Literal[\"bronze\", \"silver\", \"gold\"], is_first_order: bool = False, coupon_code: Optional[str] = None) -> Dict[str, Union[float, str]]", "docstring": "Calculate final price after applying tier-based and coupon discounts.\\n\\nArgs:\\n base_price (float): The original price before any discount.\\n user_tier (Literal[\"bronze\", \"silver\", \"gold\"]): User's membership level.\\n is_first_order (bool, optional): Whether this is the user's first order. Defaults to False.\\n coupon_code (Optional[str], optional): A valid coupon code. Defaults to None.\\n\\nReturns:\\n Dict[str, Union[float, str]]: A dict containing 'final_price', 'discount_amount', and 'reason'.\\n\\nRaises:\\n ValueError: If coupon_code is invalid or user_tier is unknown.\\n\\nNote:\\n Coupon discount is capped at 30% for silver users.", "diff_type": "none"} OUTPUT: {"function_name": "calculate_discount", "summary": "Calculate final price after applying tier-based and coupon discounts.", "parameters": [{"name": "base_price", "type": "float", "description": "The original price before any discount.", "required": true}, {"name": "user_tier", "type": "Literal[\"bronze\", \"silver\", \"gold\"]", "description": "User's membership level.", "required": true}, {"name": "is_first_order", "type": "bool", "description": "Whether this is the user's first order.", "required": false}, {"name": "coupon_code", "type": "Optional[str]", "description": "A valid coupon code.", "required": false}], "returns": {"type": "Dict[str, Union[float, str]]", "description": "A dict containing 'final_price', 'discount_amount', and 'reason'."}, "raises": [{"exception": "ValueError", "reason": "If coupon_code is invalid or user_tier is unknown."}], "business_rules": ["Coupon discount is capped at 30% for silver users."]} NOW PROCESS THE FOLLOWING INPUT:第四步:编写核心同步脚本创建/scripts/sync_docs.py,这是整个系统的“心脏”:
import json import subprocess import sys from pathlib import Path import ollama def run_pyright(): """运行pyright,提取代码元数据""" result = subprocess.run( ["pyright", "--lib", "--outputjson", "src/"], capture_output=True, text=True ) if result.returncode not in [0, 1]: # pyright返回1表示有类型错误,但不影响元数据提取 raise RuntimeError(f"pyright failed: {result.stderr}") return json.loads(result.stdout) def generate_doc_from_metadata(metadata): """调用LLM,根据元数据生成文档JSON""" prompt_file = Path("prompts/generate_api_doc.txt") prompt_content = prompt_file.read_text() # 构建完整的prompt full_prompt = prompt_content + json.dumps(metadata) # 调用Ollama API response = ollama.generate( model='codellama:13b-instruct-q4_K_M', prompt=full_prompt, options={'temperature': 0.1} # 温度设为极低,确保确定性 ) # 尝试解析LLM的JSON输出 try: return json.loads(response['response']) except json.JSONDecodeError as e: print(f"LLM output is not valid JSON: {e}") print(f"Raw response: {response['response']}") raise def main(): # 1. 提取元数据 metadata = run_pyright() # 2. 为每个函数生成文档 for func in metadata.get('functions', []): doc_json = generate_doc_from_metadata(func) # 3. 保存到生成目录 output_dir = Path("docs/generated/api/v1") output_dir.mkdir(parents=True, exist_ok=True) output_file = output_dir / f"{func['name']}.json" output_file.write_text(json.dumps(doc_json, indent=2)) print(f"Generated {output_file}") if __name__ == "__main__": main()实测心得:第一次运行
sync_docs.py可能会很慢(约5-10分钟),因为Ollama需要将13B模型加载到内存。但后续运行会快很多,因为模型已缓存。如果你的机器内存紧张,可以考虑换用更小的codellama:7b-instruct-q4_K_M模型,虽然精度略有下降,但对于大多数API文档场景,完全够用。
4.2 本地开发与调试:如何快速验证你的改动?
在本地开发时,最痛苦的不是写代码,而是调试。LLM的“黑盒”特性让调试变得异常困难。我们摸索出了一套高效的本地调试四步法:
第一步:隔离LLM,用Mock数据验证流程。在sync_docs.py的顶部,添加一个开关:
# DEBUG: 设置为True,跳过真实的LLM调用,使用预定义的Mock数据 DEBUG_USE_MOCK = True def generate_doc_from_metadata(metadata): if DEBUG_USE_MOCK: # 返回一个已知正确的Mock JSON return { "function_name": metadata['name'], "summary": "This is a mock summary.", "parameters": [{"name": "test_param", "type": "str", "description": "A test parameter.", "required": True}], "returns": {"type": "None", "description": "No return value."}, "raises": [], "business_rules": [] } # ... 原来的LLM调用逻辑这样,你可以先确保pyright提取、JSON保存、Docusaurus渲染整个链条是通畅的,再把LLM这个最不稳定的环节加进来。
第二步:用curl直连Ollama,观察原始输出。当LLM生成结果不理想时,不要在Python里猜。直接在终端运行:
curl http://localhost:11434/api/generate -d '{ "model": "codellama:13b-instruct-q4_K_M", "prompt": "YOU ARE A DOCUMENTATION ENGINEER... [粘贴你的完整Prompt]", "stream": false, "options": {"temperature": 0.1} }' | jq '.response'jq命令会帮你格式化输出,让你一眼就能看出LLM到底返回了什么。很多时候,问题不在于模型“不会”,而在于Prompt里某个标点符号错了,或者JSON模板里少了一个逗号。
第三步:在Docusaurus中启用“热重载”调试。在my-docs目录下运行:
npm start然后打开http://localhost:3000。此时,你对/docs/generated/api/v1/*.json的任何修改,都会被Docusaurus实时捕捉并刷新页面。你可以一边修改JSON文件,一边在浏览器里看效果,这是最快的学习方式。
第四步:构建最小可行文档(MVP)。不要试图一次性搞定所有模块。先挑一个最简单的、只有2-3个参数的函数,比如def ping() -> str:。确保这个函数的文档能从代码生成、渲染、搜索,全流程走通。当这个MVP跑起来的那一刻,你就已经战胜了80%的障碍。剩下的,都是复制粘贴和微调。
注意事项:在CI环境中,我们禁用了
DEBUG_USE_MOCK,并且为Ollama设置了--num_ctx 4096参数,以确保长上下文的Prompt能被完整处理。本地调试时,这个参数可以省略。
4.3 CI/CD集成:让文档更新像代码合并一样自然
文档的终极目标,是和代码一样,拥有自己的CI/CD流水线。我们使用GitHub Actions,将文档构建与代码合并深度绑定。以下是/.github/workflows/docs.yml的核心内容:
name: Build and Deploy Docs on: # 当主干分支有新的合并时,触发文档构建 push: branches: [main] paths: - 'src/**' - 'prompts/**' - 'scripts/**' jobs: build-docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.11' - name: Install dependencies run: | pip install pyright pip install -r requirements.txt # 包含ollama-python等 - name: Run Pyright & Generate Docs run: | # 启动Ollama服务(后台) ollama serve & sleep 10 # 运行我们的同步脚本 python scripts/sync_docs.py - name: Build Docusaurus Site run: | cd my-docs npm ci npm run build - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./my-docs/build这个流水线的关键设计点在于:
- 精准的触发路径:
paths配置确保只有当src/(代码)、prompts/(Prompt)、scripts/(脚本)发生变化时,才会触发构建。修改一个README.md,不会浪费CI资源。 - Ollama的后台启动:
ollama serve &将Ollama作为后台服务启动,sleep 10确保服务完全就绪后再调用sync_docs.py。 - 原子化部署:
peaceiris/actions-gh-pages插件会将构建好的静态文件,直接推送到gh-pages分支。整个过程无需人工干预,也无需额外的服务器。
当这个Workflow第一次成功运行后,你就会在项目的Settings > Pages里,看到一个全新的、由gh-pages分支托管的文档网址。每一次git push到main,这个网址上的内容都会自动更新,其时效性与代码库完全一致。
5. 常见问题与排查技巧实录:那些只有踩过坑才知道的事
5.1 “LLM输出的JSON总是解析失败!”—— 解决格式污染的终极方案
这是新手遇到的第一个、也是最普遍的“拦路虎”。报错信息通常是json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes。别急着怀疑模型,99%的情况,是LLM的输出里混入了不该有的东西。
问题根源:Ollama(以及其他LLM API)的默认响应,往往会在JSON字符串前后,加上一些“友好”的说明文字。例如:
Here is the generated documentation in JSON format: { "function_name": "ping", "summary": "Check service availability." }这个Here is the generated...前缀,就是JSON解析失败的罪魁祸首。
排查技巧:在generate_doc_from_metadata函数里,添加一行日志:
print(f"Raw LLM response: {response['response']!r}") # !r 会显示所有不可见字符运行后,你会在控制台看到类似'Here is the generated documentation in JSON format:\n{\n "function_name": "ping",\n "summary": "Check service availability."\n}'的输出。看到了吗?那个Here is...就是元凶。
终极解决方案:在解析前,用