
一、问题
很多开源框架默认只提供英文 API 文档。对于中文用户而言,阅读这些文档通常需要借助机器翻译或查阅社区文章,这带来了两大痛点:
- 翻译失真:专有名词翻译不准确,导致语义偏差,增加了理解成本。
- 文档滞后:一旦代码接口更新,非官方的中文说明往往来不及跟进,导致文档与代码脱节。
LazyLLM 认为:多语言支持不是锦上添花,而是基础能力。
我们既要提供原生的中文使用体验,又要保证文档与代码的严格同步。
二、难点
在工程实现上,维护一套高质量的双语文档系统并不像看起来那么简单,主要面临以下陷阱:
1️⃣源码臃肿与污染
如果将中英文 Docstring 同时写在源码中(例如一段英文后紧跟一段中文),会导致源码文件被大量说明文字淹没,严重影响代码的可读性与维护效率。
2️⃣ 多语言同步维护困难
如果不同语言的文档分散在不同位置(如 Wiki、外部站点),或者由不同的人群(官方 vs 社区)维护,很容易出现结构不一致或版本不同步的问题。
3️⃣ IDE 与包分发的可见性难题
即使在 docs/ 目录下把文档写得再好,如果发布的 Python 包中没有包含原生的 Docstring,开发者在 IDE(如 VS Code, PyCharm)中进行悬浮提示(Hover)或代码补全时,依然只能看到空白或英文,无法享受“原生”的中文开发体验。
综上,“在不污染源码的前提下,实现工程化的原生双语 API 文档”是一个极具挑战的目标。
三、LazyLLM 的解决方案
LazyLLM 的核心思想是“文档独立于实现维护,但在构建与运行时按需注入”。关键策略如下:
- 源码整洁:源码中不保留任何冗长的 API 文档,保持代码逻辑的纯净。
- 集中维护:中英文 API 文档以人工校对的形式,统一维护在 lazyllm/docs/*.py 映射文件中。
- 构建时注入(For IDE/Release):提供工具链,在打包发布或本地开发时,将指定语言的文档自动“回写”入 Python 源码的 \_\_doc\_\_ 中,确保 IDE 的静态分析能正确抓取文档。
- 运行时注入(For REPL/Runtime):支持运行时挂载,利用环境变量驱动文档的动态加载,使得在线文档站点构建和交互式环境(REPL)体验互为补充。
- 强制检查(CI Guardrails):集成严格的自动化文档检查机制,确保每一个新增或修改的接口都必须同步更新文档,否则无法通过 CI 验证。
简而言之:API 文档由人工编写在外部(lazyllm/docs/*.py),工程工具负责将其“注入”回代码对象。既保证了源码的轻量化,又提供了原生的用户体验。
四、使用示例与预期产出
4.1 运行时动态插入文档
LazyLLM 利用 Python 的动态特性,允许在导入包时通过环境变量自动加载文档。这不会修改磁盘上的文件,仅影响内存中的对象。
export LAZYLLM_INIT_DOC=True # 启用文档初始化
export LAZYLLM_LANGUAGE=CHINESE # 设置语言为中文 (或 ENGLISH)
python # 进入 Python 交互环境
>>> from lazyllm import pipeline # 导入 pipeline
>>> help(pipeline) # 查看帮助文档
可得到如下输出:
Help on class Pipeline in module lazyllm.flow.flow:
class Pipeline(LazyLLMFlowsBase)
| Pipeline(*args, post_action=None, auto_capture=False, save_result=None, **kw)
|
| 一个形成处理阶段管道的顺序执行模型。
|
| ``Pipeline`` 类是一个处理阶段的线性序列,其中一个阶段的输出成为下一个阶段的输入...
...
4.2 文档开发与站点构建
开发者或文档贡献者可以通过以下流程参与文档维护和站点生成。
第一步:环境准备
首先安装LazyLLM及其依赖:
git clone https://github.com/LazyAGI/LazyLLM.git
cd LazyLLM
pip install -r requirements.txt
pip install -r docs/requirements.txt # 安装文档生成工具链依赖
第二步:文档维护
文档主要分为两部分:
- 教程与指南:Markdown 文件,分别位于 docs/zh 和 docs/en (这部分非本文重点)。
- API 文档:Python 脚本,维护在 lazyllm/docs/*.py 中(本文的重点)。
我们使用特定的注册函数来关联代码与文档,主要涉及三类内容:
# 示例:为 `Pipeline` 类添加中文文档
add_chinese_doc('Pipeline', """\
一个形成处理阶段管道的顺序执行模型。
...""")
# 示例:为 `Pipeline` 类添加英文文档
add_english_doc('Pipeline', """\
A sequential execution model that forms a pipeline of processing stages.
...""")
# 示例:为 `Pipeline` 添加示例代码
add_example('Pipeline', """\
>>> import lazyllm
>>> ppl = lazyllm.pipeline(
... stage1=lambda x: x+1,
... stage2=lambda x: f'get {x}'
... )
>>> ppl(1)
'get 2'
""")
第三步:源码静态注入
为了让 IDE(VS Code, PyCharm)能够显示中文提示,我们需要将文档物理写入到源码文件中。这是一个可逆的操作。
export LAZYLLM_INIT_DOC=True # 启用文档初始化
export LAZYLLM_LANGUAGE=CHINESE # 设置语言为中文 (或 ENGLISH)
# 运行注入脚本,这会修改本地的 Python 源码文件
python docs/add_docstrings.py # 向代码对象**写入**文档
注:此步骤修改了磁盘文件。在提交代码前,通常建议清理或恢复源码,除非是发布流程的一部分。
下图当鼠标悬停在 TrainableModule 上就可以显示出对应的文档:

类似的,当设置语言为英文后完成上述流程,可获得:

第四步:生成文档站点
站点构建脚本会结合静态 Markdown 和动态注入的 API 文档生成完整的 HTML 网站。
# 准备静态资源
cp -r docs/assets docs/zh # 复制静态资源到中文目录
cp -r docs/assets docs/en # 复制静态资源到英文目录
python docs/gen_mkdocs_yaml.py # 根据语言变量生成 mkdocs.yml
mkdocs serve -a localhost:1314 # 启动本地预览
启动后,打开浏览器填入地址就可访问本地部署的文档了:

类似的,当设置语言为英文后完成上述流程,可获得:

第五步:在线双语文档
LazyLLM 利用 [Read the Docs](https://readthedocs.org/) 托管在线文档,为用户提供能够无缝切换的中英双语阅读体验。其双语构建与部署流程如下:
1️⃣项目结构配置
在 Read the Docs 上创建两个独立的项目:主项目(英文版)和子项目(中文版)。

将中文项目配置为英文项目的“Translation”子项目。这样,URL 会根据语言自动路由,例如 /en/latest/和 /zh/latest/。

2️⃣构建环境区分
这是实现双语的关键。我们在 Read the Docs 的后台管理界面中,分别为两个项目配置不同的环境变量:
- 英文项目:默认配置(或显式设置 LAZYLLM_LANGUAGE=ENGLISH)。
- 中文项目:显式设置环境变量 LAZYLLM_LANGUAGE=CHINESE。

3️⃣动态构建流程
当 Read the Docs 触发构建时,构建脚本会读取上述环境变量,执行以下差异化操作:
- 配置文件生成:docs/gen\_mkdocs\_yaml.py 脚本根据语言变量,动态生成对应的 mkdocs.yml(加载中文导航 nav\_zh.yml 或英文导航 nav\_en.yml)。
- API 文档注入:构建过程中导入 lazyllm 包时,初始化逻辑会根据语言变量,将 lazyllm/docs/*.py 中对应的中文或英文文档注入到内存对象中。
- 页面渲染:最终,MkDocs 生成器从内存对象中提取出已经是目标语言的 Docstring,渲染成 HTML 页面。
通过这种“同一份代码,不同环境配置”的策略,我们无需维护两份割裂的代码库,即可自动生成完全同步的双语 API 文档站点。

预期效果小结:
- IDE/REPL:悬浮查看源码时,看到的是当下环境语言对应的原生中文 Docstring。
- Web 站点:API 文档页面准确显示中文描述(因为构建时注入了中文 Docstring)。
- 流程一致性:无论 Web 端还是 IDE 端,数据源均来自同一份 lazyllm/docs/*.py,杜绝版本分裂。
五、我们是如何做到的
5.1 主要仓库脚本体系
LazyLLM 的文档工程化主要由以下几个核心脚本支撑:
1. 运行时动态注入 (Runtime Injection)
这是文档与代码解耦的基石。
机制:
Python 允许在运行时修改对象的 \_\_doc\_\_ 属性。在 lazyllm/docs/init.py 中,我们检查 LAZYLLM\_INIT\_DOC 环境变量。如果启用,则调用 utils.py 中的逻辑,利用反射机制(getattr, \_\_dict\_\_)定位到内存中的类或函数,将预加载的文本挂载上去。
优势:
实现“零侵入”。源码在磁盘上保持纯净,适应生产环境对加载速度的极致要求(直接关闭文档加载),同时满足开发环境的文档查阅需求
2. Docstring 注入工具 (Static Injection)
入口:
docs/add_docstrings.py。
核心逻辑:
该工具使用 lazynote.manager.SimpleManager 遍历 lazyllm 包。它支持两种模式:
Fill(注入):将内存中的文档写入磁盘源码文件。
Clear(清理):清除源码中的 Docstring,恢复代码的“裸”状态。
作用:
解决了静态分析工具(如 IDE)无法识别运行时修改的问题。它是连接“动态文档”与“静态源码”的桥梁。
3. 自动化配置 (Configuration as Code)
- mkdocs.yml 不是静态文件,而是由 docs/gen\_mkdocs\_yaml.py 根据 LAZYLLM_LANGUAGE 动态生成。
- 允许中英文文档拥有完全独立的目录结构(docs/zh vs docs/en)和导航菜单(docs/nav_zh.yml),实现了不同语言版本文档的独立演进能力。
4. 文档一致性校验 (Integrity Check)
为了从机制上杜绝“代码更新但文档滞后”的问题,我们引入了强制性的检查脚本 tests/doc\_check/test\_doc\_api\_check.py。
基于 Inspect 的全量扫描:
脚本利用 Python 标准库 inspect 递归遍历 lazyllm 包下的所有类与公开方法(Public Methods)。它会模拟文档注入过程,然后断言内存中的每一个 API 对象是否存在非空的 \_\_doc\_\_ 属性。
智能继承识别:
检查机制具备语义理解能力。如果子类覆盖了父类方法但未重写文档,或者直接继承了父类行为,检查逻辑会自动回溯父类(Ancestors)的文档状态,确保继承链上的文档完整性,避免误报。
CI 流水线集成:
我们将此检查作为不可绕过的关卡集成到了 .github/workflows/main.yml 中。在 doc_check 任务里,任何 Pull Request 如果引入了未编写文档的新接口,CI 将直接失败。这确保了主干分支上的每一行代码,都时刻处于“文档齐备”的状态。
5.2 关键技术点剖析:SimpleManager 的工作原理
SimpleManager 是 LazyLLM 文档注入工具的核心执行器,其主要任务是将内存中动态加载的文档(Runtime Docstring)准确地回写到静态源代码(Static Source Code)中。这一过程并非简单的文本替换,而是涉及AST(抽象语法树)操作与运行时对象映射的复杂协同。
实现代码主要位于 docs/scripts/lazynote/manager/simple.py 及其依赖的 BaseManager 和 BaseEditor 中。我们将从三个维度剖析其关键逻辑:
1. 静态与动态的“双重映射” (The Runtime-Static Bridge)
这是该模块最核心的技术难点:如何知道源码文件中的某一个 def foo(): 对应内存中的哪个对象,从而获取其 docstring?
即使我们在源码中没有写文档,LazyLLM 的启动机制(lazyllm/docs/\_\_init\_\_.py)已经将文档挂载到了内存对象(如 Pipeline 类)的 \_\_doc\_\_ 属性上。SimpleManager 利用 BaseEditor (基于 LibCST) 实现了这种连接:
- AST 遍历:不仅解析文件结构,还维护上下文(如当前类名)。
- 运行时反射:在遍历 AST 节点(FunctionDef, ClassDef)时,实时去内存模块中查找对应的 Python 对象。
我们可以从 editor/base.py 中看到这一关键逻辑:
# 伪代码逻辑展示 (提取自 BaseEditor)
def leave_FunctionDef(self, original_node, updated_node):
# 1. 构建当前函数的全名 (e.g., "Pipeline.flow")
full_name = f"{self.current_class}.{original_node.name.value}"
# 2. 从加载的 module 中获取真实的运行时对象
obj = self._get_obj_by_name(full_name)
# 3. 获取运行时对象上的 docstring (这里包含了注入的中文/英文文档)
docstring = obj.__doc__ if obj else None
# 4. 更新 AST 节点
return self._update_node_with_new_docstring(original_node, updated_node, docstring)
通过这种方式,SimpleManager 充当了“搬运工”,把内存里“注入好的文档”搬回了“源代码文件”里。
2. 修改策略模式 (Strategy Pattern)
SimpleManager 本身逻辑非常轻薄,它将具体的修改行为委托给 DocstringHandler。这不仅仅是为了代码整洁,更是为了支持“注入(fill)”、“清除(clear)”等多种操作模式。
在 manager/simple.py 中:
class DocstringHandler:
@staticmethod
def handle_fill(old_docstring, node_code):
# 填充模式:主要用于将内存 docstring 写入文件
# 如果文件中已有(old_docstring),通常 logic 会保留它;
# 但结合 BaseEditor 逻辑,如果 Runtime 有值且文件无值,这里即实现了注入。
if old_docstring:
return f"{old_docstring}"
return None
@staticmethod
def handle_clear(old_docstring, node_code):
# 清理模式:返回 None,指示 AST 转换器删除文档节点
return None
当我们运行 python docs/add_docstrings.py --replace 时,实际上是先执行了一次 clear 策略(清洗源码),再执行一次 fill 策略(从内存回填),从而实现了文档的彻底更新或语言切换。
3. 基于 LibCST 的无损修改
为什么不使用 Python 内置的 ast 模块或正则表达式?
- 正则表达式:无法处理复杂的嵌套结构和多行字符串。
- Python \`ast\` 模块:在解析和回写时会丢弃源码中的注释和格式信息(Parsing is destructive)。
SimpleManager 继承自依赖 LibCST (Concrete Syntax Tree) 的架构。LibCST 的最大优势是 保留代码风格(Preserve Formatting)。
当插入或修改 Docstring 时,它能智能处理缩进和换行,确保注入文档后的代码依然符合 PEP 8 规范,且不会破坏代码的其他部分(如注释、空行)。
# editor/base.py 中的 update 逻辑
def _update_node_with_new_docstring(self, ..., docstring):
# 构建新的三引号字符串节点
new_docstring_node = cst.SimpleStatementLine(...)
# 智能插入到函数体/类定义的开头,处理缩进
# ...
return updated_node.with_changes(body=new_body)
SimpleManager 的精妙之处在于它打通了 Source Code (文件) -> AST (结构) -> Runtime Object (内存) -> Source Code 的闭环。
它使得 LazyLLM 能够拥有“一份干净的代码”和“多份丰富的文档”,并在需要时随时将二者融合。
4. 架构与流程可视化 (Architecture & Flow)
为了更直观地理解上述机制,类关系图展示了 SimpleManager 如何继承基础能力,并未借助 LibCST 的 Transformer 机制修改代码;而序列图则揭示了从脚本启动遍历,到 AST 解析,再到从运行时获取文档并回写的完整闭环。
类关系图 (Class Structure)

上图展示了模块间的静态依赖关系:
- BaseManager 提供了通用的遍历(file traverse)和文件读写能力。
- SimpleManager 关注于具体的文档生成逻辑(即 gen_docstring),通过委托给 DocstringHandler 实现了注入、清理等不同策略的解耦。
- BaseEditor 则是 AST 操作的实施者,它继承自 LibCST 的 Transformer,负责深入到代码的类和函数定义中进行精细化修改。
核心执行流程 (Execution Sequence)

上图还原了 add_docstrings.py 运行时的动态过程:
1️⃣启动与遍历
脚本初始化 SimpleManager,开始扫描 lazyllm 包下的所有模块。
2️⃣AST 变换
对每个模块,先解析成 AST 树,然后启动 BaseEditor 访问器遍历树中的每个节点(类或函数)。
3️⃣运行时桥接
这是最关键的一步。BaseEditor 在访问 AST 节点时,使用全限定名(Qualified Name)在内存中查找对应的 Python 对象,并提取其 \_\_doc\_\_ 属性——这个属性里正包含了我们预先注入好的中文或英文文档。
4️⃣回写源码
修改后的 AST 树被转换回代码字符串,并覆盖写入原文件,从而完成了“从内存到文件”的文档固化。
六、总结
LazyLLM 的文档方案通过“运行时动态挂载”和“基于 LibCST 的静态注入”,成功解决了开源界长期存在的双语文档维护难题。
- 对开发者:源码清爽,无维护负担。
- 对用户:所有接触点(Web、IDE、REPL)均能获得原生的母语支持。
这不仅仅是翻译工作,更是一次关于“开发者体验(DX)”的工程化实践。
欢迎升级体验 LazyLLM最新版本,请大家去github上点一个免费的star,支持一下~
LazyLLM项目仓库链接🔗:
更多技术内容,欢迎移步 gzh "LazyLLM" 讨论!