LazyLLM黑科技 | 拒绝机翻,如何实现中英双语的API文档?
很多开源框架默认只提供英文 API 文档。对于中文用户而言,阅读这些文档通常需要借助机器翻译或查阅社区文章,这带来了两大痛点: LazyLLM 认为:多语言支持不是锦上添花,而是基础能力。 我们既要提供原生的中文使用体验,又要保证文档与代码的严格同步。 在工程实现上,维护一套高质量的双语文档系统并不像看起来那么简单,主要面临以下陷阱: 1️⃣源码臃肿与污染 如果将中英文 Docstring 同时写在源码中(例如一段英文后紧跟一段中文),会导致源码文件被大量说明文字淹没,严重影响代码的可读性与维护效率。 2️⃣ 多语言同步维护困难 如果不同语言的文档分散在不同位置(如 Wiki、外部站点),或者由不同的人群(官方 vs 社区)维护,很容易出现结构不一致或版本不同步的问题。 3️⃣ IDE 与包分发的可见性难题 即使在 docs/ 目录下把文档写得再好,如果发布的 Python 包中没有包含原生的 Docstring,开发者在 IDE(如 VS Code, PyCharm)中进行悬浮提示(Hover)或代码补全时,依然只能看到空白或英文,无法享受“原生”的中文开发体验。 综上,“在不污染源码的前提下,实现工程化的原生双语 API 文档”是一个极具挑战的目标。 LazyLLM 的核心思想是“文档独立于实现维护,但在构建与运行时按需注入”。关键策略如下: 简而言之:API 文档由人工编写在外部(lazyllm/docs/*.py),工程工具负责将其“注入”回代码对象。既保证了源码的轻量化,又提供了原生的用户体验。 LazyLLM 利用 Python 的动态特性,允许在导入包时通过环境变量自动加载文档。这不会修改磁盘上的文件,仅影响内存中的对象。 可得到如下输出: 开发者或文档贡献者可以通过以下流程参与文档维护和站点生成。 首先安装LazyLLM及其依赖: 文档主要分为两部分: 我们使用特定的注册函数来关联代码与文档,主要涉及三类内容: 为了让 IDE(VS Code, PyCharm)能够显示中文提示,我们需要将文档物理写入到源码文件中。这是一个可逆的操作。 注:此步骤修改了磁盘文件。在提交代码前,通常建议清理或恢复源码,除非是发布流程的一部分。 下图当鼠标悬停在 TrainableModule 上就可以显示出对应的文档: 类似的,当设置语言为英文后完成上述流程,可获得: 站点构建脚本会结合静态 Markdown 和动态注入的 API 文档生成完整的 HTML 网站。 启动后,打开浏览器填入地址就可访问本地部署的文档了: 类似的,当设置语言为英文后完成上述流程,可获得: LazyLLM 利用 [Read the Docs](https://readthedocs.org/) 托管在线文档,为用户提供能够无缝切换的中英双语阅读体验。其双语构建与部署流程如下: 1️⃣项目结构配置 在 Read the Docs 上创建两个独立的项目:主项目(英文版)和子项目(中文版)。 将中文项目配置为英文项目的“Translation”子项目。这样,URL 会根据语言自动路由,例如 /en/latest/和 /zh/latest/。 2️⃣构建环境区分 这是实现双语的关键。我们在 Read the Docs 的后台管理界面中,分别为两个项目配置不同的环境变量: 3️⃣动态构建流程 当 Read the Docs 触发构建时,构建脚本会读取上述环境变量,执行以下差异化操作: 通过这种“同一份代码,不同环境配置”的策略,我们无需维护两份割裂的代码库,即可自动生成完全同步的双语 API 文档站点。 预期效果小结: LazyLLM 的文档工程化主要由以下几个核心脚本支撑: 这是文档与代码解耦的基石。 机制: Python 允许在运行时修改对象的 \_\_doc\_\_ 属性。在 lazyllm/docs/init.py 中,我们检查 LAZYLLM\_INIT\_DOC 环境变量。如果启用,则调用 utils.py 中的逻辑,利用反射机制(getattr, \_\_dict\_\_)定位到内存中的类或函数,将预加载的文本挂载上去。 优势: 实现“零侵入”。源码在磁盘上保持纯净,适应生产环境对加载速度的极致要求(直接关闭文档加载),同时满足开发环境的文档查阅需求 入口: docs/add_docstrings.py。 核心逻辑: 该工具使用 lazynote.manager.SimpleManager 遍历 lazyllm 包。它支持两种模式: Fill(注入):将内存中的文档写入磁盘源码文件。 Clear(清理):清除源码中的 Docstring,恢复代码的“裸”状态。 作用: 解决了静态分析工具(如 IDE)无法识别运行时修改的问题。它是连接“动态文档”与“静态源码”的桥梁。 为了从机制上杜绝“代码更新但文档滞后”的问题,我们引入了强制性的检查脚本 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 将直接失败。这确保了主干分支上的每一行代码,都时刻处于“文档齐备”的状态。 SimpleManager 是 LazyLLM 文档注入工具的核心执行器,其主要任务是将内存中动态加载的文档(Runtime Docstring)准确地回写到静态源代码(Static Source Code)中。这一过程并非简单的文本替换,而是涉及AST(抽象语法树)操作与运行时对象映射的复杂协同。 实现代码主要位于 docs/scripts/lazynote/manager/simple.py 及其依赖的 BaseManager 和 BaseEditor 中。我们将从三个维度剖析其关键逻辑: 这是该模块最核心的技术难点:如何知道源码文件中的某一个 def foo(): 对应内存中的哪个对象,从而获取其 docstring? 即使我们在源码中没有写文档,LazyLLM 的启动机制(lazyllm/docs/\_\_init\_\_.py)已经将文档挂载到了内存对象(如 Pipeline 类)的 \_\_doc\_\_ 属性上。SimpleManager 利用 BaseEditor (基于 LibCST) 实现了这种连接: 我们可以从 editor/base.py 中看到这一关键逻辑: 通过这种方式,SimpleManager 充当了“搬运工”,把内存里“注入好的文档”搬回了“源代码文件”里。 SimpleManager 本身逻辑非常轻薄,它将具体的修改行为委托给 DocstringHandler。这不仅仅是为了代码整洁,更是为了支持“注入(fill)”、“清除(clear)”等多种操作模式。 在 manager/simple.py 中: 当我们运行 python docs/add_docstrings.py --replace 时,实际上是先执行了一次 clear 策略(清洗源码),再执行一次 fill 策略(从内存回填),从而实现了文档的彻底更新或语言切换。 为什么不使用 Python 内置的 ast 模块或正则表达式? SimpleManager 继承自依赖 LibCST (Concrete Syntax Tree) 的架构。LibCST 的最大优势是 保留代码风格(Preserve Formatting)。 当插入或修改 Docstring 时,它能智能处理缩进和换行,确保注入文档后的代码依然符合 PEP 8 规范,且不会破坏代码的其他部分(如注释、空行)。 SimpleManager 的精妙之处在于它打通了 Source Code (文件) -> AST (结构) -> Runtime Object (内存) -> Source Code 的闭环。 它使得 LazyLLM 能够拥有“一份干净的代码”和“多份丰富的文档”,并在需要时随时将二者融合。 为了更直观地理解上述机制,类关系图展示了 SimpleManager 如何继承基础能力,并未借助 LibCST 的 Transformer 机制修改代码;而序列图则揭示了从脚本启动遍历,到 AST 解析,再到从运行时获取文档并回写的完整闭环。 类关系图 (Class Structure) 上图展示了模块间的静态依赖关系: 核心执行流程 (Execution Sequence) 上图还原了 add_docstrings.py 运行时的动态过程: 1️⃣启动与遍历 脚本初始化 SimpleManager,开始扫描 lazyllm 包下的所有模块。 2️⃣AST 变换 对每个模块,先解析成 AST 树,然后启动 BaseEditor 访问器遍历树中的每个节点(类或函数)。 3️⃣运行时桥接 这是最关键的一步。BaseEditor 在访问 AST 节点时,使用全限定名(Qualified Name)在内存中查找对应的 Python 对象,并提取其 \_\_doc\_\_ 属性——这个属性里正包含了我们预先注入好的中文或英文文档。 4️⃣回写源码 修改后的 AST 树被转换回代码字符串,并覆盖写入原文件,从而完成了“从内存到文件”的文档固化。 LazyLLM 的文档方案通过“运行时动态挂载”和“基于 LibCST 的静态注入”,成功解决了开源界长期存在的双语文档维护难题。 这不仅仅是翻译工作,更是一次关于“开发者体验(DX)”的工程化实践。 欢迎升级体验 LazyLLM最新版本,请大家去github上点一个免费的star,支持一下~ LazyLLM项目仓库链接🔗: 更多技术内容,欢迎移步 gzh "LazyLLM" 讨论!
一、问题
二、难点
三、LazyLLM 的解决方案
四、使用示例与预期产出
4.1 运行时动态插入文档
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 文档开发与站点构建
第一步:环境准备
git clone https://github.com/LazyAGI/LazyLLM.git
cd LazyLLM
pip install -r requirements.txt
pip install -r docs/requirements.txt # 安装文档生成工具链依赖
第二步:文档维护
# 示例:为 `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'
""")
第三步:源码静态注入
export LAZYLLM_INIT_DOC=True # 启用文档初始化
export LAZYLLM_LANGUAGE=CHINESE # 设置语言为中文 (或 ENGLISH)
# 运行注入脚本,这会修改本地的 Python 源码文件
python docs/add_docstrings.py # 向代码对象**写入**文档


第四步:生成文档站点
# 准备静态资源
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 # 启动本地预览


第五步:在线双语文档




五、我们是如何做到的
5.1 主要仓库脚本体系
1. 运行时动态注入 (Runtime Injection)
2. Docstring 注入工具 (Static Injection)
3. 自动化配置 (Configuration as Code)
4. 文档一致性校验 (Integrity Check)
5.2 关键技术点剖析:SimpleManager 的工作原理
1. 静态与动态的“双重映射” (The Runtime-Static Bridge)
# 伪代码逻辑展示 (提取自 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)2. 修改策略模式 (Strategy Pattern)
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 None3. 基于 LibCST 的无损修改
# 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)4. 架构与流程可视化 (Architecture & Flow)


六、总结