这次我们来看一个来自 Hacker News“Show HN”思路的代码文档项目:Get agents to do what I want with code documentation。标题翻译过来很直白——让 AI Agent 按照我的预期去写代码文档。很多人一开始觉得这种项目无非就是“把代码丢给大模型,让它生成 README”,但真正做起来才发现,文档生成的难点从来不在“能不能写”,而在“写出来的东西是不是我要的”。命名是否一致、接口是否有真实示例、依赖变更有没有同步到安装说明、废弃函数是否有标注,这些细节才是文档质量的分水岭。
这篇文章会把这个项目思路展开成一整套可落地的工程方案。我会先给你一个核心能力速览,然后按“需求结构化 -> 环境准备 -> Agent 核心流程 -> 测试验证 -> 接口化与批量任务 -> 资源占用 -> 常见问题”的顺序拆开讲。你可以直接照着一套最小可运行例子搭起来,把它接进自己的仓库里。内容偏工程实践,适合已经在用 AI 编程工具、准备把文档流程自动化,或者正在做内部 AI Agent 工具链的开发者。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 项目类型 | AI Agent + 代码文档生成与维护 |
| 核心目标 | 让 Agent 按预设规范生成、校验、更新代码文档 |
| 典型工作方式 | 读取仓库 -> 分析 diff -> 生成文档 -> 校验 -> 提交 |
| 输入 | 源码目录、git diff、已有文档、文档规范文件 |
| 输出 | README、模块说明、API 文档、CHANGELOG 草稿、架构说明 |
| 运行平台 | Linux / macOS / Windows 都可以 |
| 启动方式 | 命令行脚本、FastAPI 包装、定时任务 |
| 硬件要求 | 取决于你使用的基础模型;API 调用不需要本地显卡;用本地模型需按实际模型测试 |
| 是否支持 API | 可以封装成 HTTP 服务 |
| 是否支持批量任务 | 支持,按目录 / 模块 / 文件批量处理 |
| 适合读者 | 开源维护者、内部项目组、技术文档负责人 |
这个方案本质上不是单个工具,而是一个“文档 Agent 工作流模板”。你可以理解成:先定义一个“文档规范文件”,再把仓库变更历史和源码片段塞给 Agent,让它按规范产出文档;产出之后不是直接采用,而是跑一遍链接检查、命令检查、格式检查,最后再决定是否合入。这套结构里真正要花心思的是规范文件和验证层,Agent 本身只需要一个稳定的对话接口。
2. 适用场景与使用边界
这个项目思路适合解决三类问题。第一类是仓库文档常年滞后,代码改了注释没改,注释改了 README 没改;第二类是文档风格不统一,有人写得很细,有人一句话带过,混在一起阅读成本很高;第三类是文档里的代码示例没有经过验证,用户复制出来根本跑不通。把这三件事交给 Agent 自动做,可以省掉大量重复劳动。
但它也有明确的不适用场景。如果你的项目处于架构频繁调整的早期阶段,文档还在快速探索期,这时候让 Agent 自动化生成反而会放大混乱。另外,它不应该替代人工审核,尤其是涉及安全边界、加密算法、权限模型这一类文档,必须由熟悉系统的人确认之后才能发布。
合规方面也要注意几件事。接入外部大模型 API 时,不要把包含内部密钥、客户数据、未公开商业信息的文件丢进上下文;代码仓库本身如果有特殊许可证,Agent 生成的文档也属于仓库内容,发布前要确认合规;如果以后扩展成“自动提交 PR”的模式,一定要有权限控制,不能让它绕过 Code Review 直接推到主干分支。
3. 让 Agent 理解“我要什么”:需求结构化
很多 Agent 项目跑偏,原因不是模型能力不够,而是你根本没有把一个可执行的“标准”告诉它。让 Agent 写文档,至少要在 prompt 或规范文件里明确几个维度。
第一是目标读者。README 面对的是使用者,模块文档面对的是二次开发者,API 文档面对的是调用方。读者不同,详略和用词完全不同。第二是风格约束。比如代码块必须带语言标注、函数说明必须包含参数类型和返回值、标题层级不允许跳级。第三是示例要求。文档里出现的命令必须真正可执行,接口示例必须和当前代码签名一致。第四是变更范围。一次任务只处理当前 diff 涉及到的文件,不要顺手把全部文档重写一遍。
为了达成这些约束,我建议在仓库根目录维护一个文档规范文件,例如docs/SPEC.md,Agent 每次生成前都要读取它。
# 文档规范 ## 目标读者 - README:项目使用者,第一次接触项目的人 - docs/api.md:接口调用方 ## 风格要求 - 所有代码块必须标注语言 - 函数说明格式:作用 / 参数 / 返回值 / 示例 - 不写空泛的形容词,比如“非常强大”“易于使用” ## 示例要求 - 代码示例必须与当前代码签名一致 - bash 命令必须可执行 - 禁止使用未定义的变量名 ## 变更范围 - 只处理本次 git diff 涉及的文件 - 不重写与本次变更无关的章节把规范文件放在仓库里还有一个好处:每次变更都能沉淀经验。你发现 Agent 写的文档哪类问题多,就直接往 SPEC 里加一条规则,下一轮生成就会收敛很多。这个迭代过程比频繁改 prompt 更可控。
除了规范文件,还需要把“什么是这次任务的目标”写清楚。可以定义一个简单 JSON 任务描述:
{ "task": "update_api_doc", "repo": "./my-project", "change_scope": ["src/auth/login.py", "src/auth/token.py"], "target_doc": "docs/api.md", "max_output_tokens": 3000 }这样每次调用 Agent 之前,只需要更新这个任务文件。脚本读它、拼上下文、调模型、写回文档;人负责审核。整个过程保持单一职责,Agent 不需要猜测你要干什么,你也不需要反复在 prompt 里描述同一件事。
4. 环境准备与最小可运行架构
这一步我们先不管 Agent 本身有多聪明,先把能跑起来的环境准备好。
如果按最小依赖来搭,你需要:
- Python 3.10 以上
- Git
- 一个可用的 LLM API 客户端环境(OpenAI 兼容接口或本地推理服务)
- 目标代码仓库一份克隆到本地
requests、python-dotenv两个 Python 依赖
目录结构建议这样组织:
doc-agent/ ├── agent.py # Agent 核心逻辑 ├── spec_reader.py # 读取文档规范 ├── diff_tool.py # git diff 采集 ├── validator.py # 文档验证 ├── server.py # FastAPI 包装层(可选) ├── tasks/ │ └── task.json # 当前任务定义 ├── outputs/ # 生成的文档 └── .env # 环境变量:API Key、模型名环境准备阶段最值得注意的不是 Python 版本,而是 API 地址和模型名的配置方式。不要把 Key 硬编码在脚本里,建议用环境变量管理。
# .env 示例 LLM_API_URL=https://your-endpoint.example/v1/chat/completions LLM_API_KEY=your_key_here LLM_MODEL=your-model-name如果你用的是 OpenAI 兼容接口,可以直接把这个 URL 换成自己的服务地址;如果你用本地部署的模型,也只需要改 URL 和模型名。整个 Agent 代码里不要写死任何厂商相关的逻辑,只认“messages 进、文本出”这个标准行为。这样后面换模型成本几乎为零。
运行之前检查一下 git 仓库是否干净,避免把生成的文档和手头的修改混在一起:
cd my-project git status --short如果输出为空,再开始跑 Agent。这一步很重要,因为后面要做 diff 采集,如果工作区本身有未提交修改,Agent 生成的“变更分析”就会包含你的临时改动,文档内容会被污染。
5. 实现一个最小文档 Agent 核心流程
Agent 核心流程可以拆成五步:采集变更、组装上下文、生成文档、写回文件、验证输出。每一步都单独封装函数,方便以后加日志、缓存和失败重试。
先写一个简单的 LLM 调用函数。这里用 OpenAI 兼容接口,但实际调用地址需要按你自己的服务配置替换。
import os import requests API_URL = os.environ.get("LLM_API_URL", "").rstrip("/") API_KEY = os.environ.get("LLM_API_KEY", "") MODEL = os.environ.get("LLM_MODEL", "") def call_llm(messages: list, temperature: float = 0.2) -> str: if not API_URL or not API_KEY or not MODEL: raise RuntimeError("请先配置 LLM_API_URL / LLM_API_KEY / LLM_MODEL") resp = requests.post( f"{API_URL}/chat/completions", headers={"Authorization": f"Bearer {API_KEY}"}, json={ "model": MODEL, "messages": messages, "temperature": temperature, }, timeout=180, ) resp.raise_for_status() return resp.json()["choices"][0]["message"]["content"]注意我把temperature默认设成了 0.2。文档生成不是创意写作,温度越高越容易跑偏,低温度更适合保持一致性和严谨性。
接下来是采集 git diff。只关心本次变更涉及的文件名,以及对应的代码变更内容。这里有一个经验:不要一次性把整个仓库源码都塞给模型,而是先拿 diff。diff 比源码更聚焦,token 成本也更低。
import subprocess import json def get_changed_files(base: str = "HEAD") -> list: result = subprocess.run( ["git", "diff", "--name-only", base], capture_output=True, text=True, ) files = [line.strip() for line in result.stdout.splitlines() if line.strip()] return files def get_file_diff(file_path: str, base: str = "HEAD") -> str: result = subprocess.run( ["git", "diff", base, "--", file_path], capture_output=True, text=True, ) return result.stdout再写一个最简的 spec 读取函数。这个函数只需要把规范文件和任务文件读出来,拼成 Agent 的 system prompt。
from pathlib import Path def load_text(path: str) -> str: return Path(path).read_text(encoding="utf-8") def build_messages(task: dict) -> list: spec = load_text("docs/SPEC.md") changed = task.get("change_scope", []) context_blocks = [] for f in changed: context_blocks.append(f"## {f}\n\n{get_file_diff(f)}") context_text = "\n\n".join(context_blocks) return [ { "role": "system", "content": ( "你是一个代码文档维护助手。请严格遵循文档规范," "只处理任务指定的变更范围,不要重写无关内容。\n\n" f"文档规范:\n{spec}" ), }, { "role": "user", "content": ( f"需要更新的文档:{task.get('target_doc')}\n\n" f"本次变更文件:\n{changed}\n\n" f"相关 diff:\n{context_text}\n\n" "请根据 diff 更新目标文档,保持原有风格。" ), }, ]实际生成时,你会希望把 target_doc 的旧内容也一起作为上下文发过去。这样 Agent 不是从零写,而是基于旧文档做增量修改,风格延续性会好很多。上面的示例只是为了展示核心链路,真实使用时可以把这个迭代过程改成:读旧文档 -> 拼 diff -> 生成新文档 -> 写回。
最后写回文件。这里推荐先写到一个临时文件,验证通过后再覆盖原文件,避免 Agent 输出一半导致文档损坏。
def write_doc(path: str, content: str) -> None: Path(path).write_text(content, encoding="utf-8")如果你希望更稳妥一点,可以先把生成内容输出到outputs/目录,人在本地 diff 确认之后再用。让生成和合入解耦,这是文档 Agent 项目里非常关键的设计。
6. 用测试验证文档“真的能跑”
文档生成之后,最容易被忽略的一步是验证。Agent 写出了 README,但里面的命令是编的,链接是失效的,示例参数和真实函数签名对不上。不验证的话,这份文档还不如不生成。
这里可以参考当前 Agent 圈子里流行的“test agents”思路:让另一个独立流程去验证文档产物,而不是在同一个生成 prompt 里让模型自我检查。模型自我检查天然有盲区,它容易把“看起来完整的例子”当成“能运行的内容”。
最简单的验证项目有三个。
第一个是代码块语言标注检查。确保每个 Markdown 代码块都有语言标注,防止出现没有语法高亮的裸代码块。
import re def check_code_fences(md_text: str): fences = re.findall(r"```(\w*)", md_text) return [f for f in fences if f == ""]这个函数返回所有没有语言标注的代码块位置。如果列表不为空,就说明文档不合规。
第二个是 bash 命令可执行性检查。从文档里提取 ```bash 代码块,逐个做 dry-run。
import re import subprocess def extract_bash_blocks(md_text: str) -> list: return re.findall(r"```bash\n(.*?)```", md_text, re.S) def dry_run_commands(md_text: str): for block in extract_bash_blocks(md_text): for line in block.splitlines(): line = line.strip() if not line or line.startswith("#"): continue print(f"[dry-run] {line}") # 注意:不要直接执行未知命令。这里只做语法检查或白名单检查。这里要特别说明一下安全限制:文档里的命令可能包含删除、覆盖、联网下载等操作,绝对不能直接用subprocess.run(shell=True)去执行。更安全的做法是维护一个命令白名单,比如pip install、python -m pytest、docker build这些常见命令可以拆成参数结构去检查,其他命令一律标记为“需人工确认”。
第三个是链接检查。Markdown 里的相对链接和绝对链接都可能是死链。网络请求可能不稳定,可以先只检查锚点和本地文件路径。
from pathlib import Path import re def check_local_links(md_text: str, base_dir: Path) -> list: links = re.findall(r"\[.*?\]\((.*?)\)", md_text) broken = [] for link in links: if link.startswith(("http://", "https://", "#")): continue target = (base_dir / link).resolve() if not target.exists(): broken.append(link) return broken这一套验证代码是独立于生成逻辑的。你可以把它接在 CI 里,也可以作为 Agent 生成后的自动检查环节。我的建议是直接做成一个validator.py,每次生成完先跑一遍,失败就重新生成,最多重试两次。如果两次还失败,说明当前上下文信息不足,应该停下来让人介入,而不是无限循环。
7. 接口 API 化与批量任务处理
命令行跑通之后,下一步就是接口化和批量处理。文档 Agent 做成 HTTP 服务的好处是可以接入 CI/CD、聊天机器人、内部工具平台,让团队成员不用本地配环境就能调用。
用 FastAPI 做一个轻量包装是非常简单的。这里不要求项目本身必须提供 API,而是给你一个通用的封装模板,拿到自己的 Agent 逻辑上就能用。
from fastapi import FastAPI, HTTPException from pydantic import BaseModel app = FastAPI() class DocTask(BaseModel): repo_path: str change_scope: list[str] target_doc: str max_retry: int = 2 @app.post("/generate_doc") def generate_doc(task: DocTask): try: messages = build_messages(task.dict()) for attempt in range(task.max_retry): content = call_llm(messages) broken_links = check_local_links(content, repo_path) if not broken_links: return {"status": "ok", "content": content} return {"status": "needs_review", "content": content, "warning": "验证未通过,请人工处理"} except Exception as exc: raise HTTPException(status_code=500, detail=str(exc))这个接口的输入是 repo_path、change_scope、target_doc,输出是生成结果和验证状态。注意一点:如果服务部署在服务器上,repo_path这个参数意味着调用者可以指定任意本地路径,风险很大。实际使用时必须做路径白名单校验,只允许访问预设的仓库根目录,防止路径穿越。
批量任务可以按目录或按模块组织。一个简单的批量队列可以做成这样:
# batch_tasks.json [ { "repo_path": "./project-a", "change_scope": ["src/auth/", "docs/auth.md"], "target_doc": "docs/auth.md" }, { "repo_path": "./project-a", "change_scope": ["src/payment/", "docs/payment.md"], "target_doc": "docs/payment.md" } ]批量执行时要重点控制并发。不要一个任务结束立刻上十个任务,否则 API 限流会把整个队列打崩。建议用ThreadPoolExecutor(max_workers=1)先单线程跑一轮,确认稳定后再慢慢加并发。每个任务记录开始时间、结束时间、token 消耗、验证结果,写成一个 JSON 日志文件。失败任务单独进入 retry 队列,超过重试次数就标记为failed,等人处理。
8. 资源占用与成本观察
做文档 Agent 和做图像、视频生成不同,它的主要资源瓶颈不是显存,而是 token 消耗。一次文档生成会同时消耗输入 token 和输出 token。输入 token 包括 spec 文件、旧文档、diff、源码片段;输出 token 就是新文档本身。如果你用的是按 token 计费的 API,很快就能感受到上下文越长成本越高。
我在前面反复强调“收集 diff 而不是整个源码”,就是为了降低输入成本。一个 diff 通常只有几百到几千字符,而整个仓库可能是几十万字符,差了一个数量级。
如果想进一步控制成本,可以做三层优化。
第一层是缓存。同一个文件的 diff 如果没变过,上一次生成的结果可以直接复用。你可以在本地维护一个cache.json,key 是“文件路径 + git diff 的 hash”。
{ "src/auth/login.py:::a1b2c3d4...": { "content": "生成的文档内容", "timestamp": "2025-01-01T12:00:00" } }第二层是分块。如果变更涉及大量文件,不要一次性全丢给 Agent。按模块拆成多个小任务,每个任务只处理一个模块。这样每个请求的上下文长度更稳定,Agent 跑偏的概率也低。
第三层是选择更小的模型。先拿一个小模型做“文档草稿”,再用大模型做“审校修正”。很多情况下小模型产出的草稿已经能用,大模型只做摘要、补示例、纠正格式。这个分层机制能显著降低总成本,而且质量不一定比单次大模型生成差。
如果选择本地模型,显存占用就看模型大小了。这里我不能替你估算一个固定数字,因为不同量化等级、不同上下文长度、不同并发数都会影响显存。更稳妥的判断是:先从 API 方式跑通流程,等确认这套方案对你有价值,再考虑本地模型。本地模型的好处是隐私可控,代价是部署和调优成本更高。
9. 常见问题与排查方法
文档 Agent 在实际运行中会遇到的问题,很多和普通 AI 应用是共同的。我整理了一个排查表,可以直接按表格定位。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 生成的文档与代码完全无关 | 上下文里放错了文件,或 diff 采集为空 | 检查 git status 和 diff 输出 | 确保工作区干净,确认 change_scope 路径真实存在 |
| 生成的文档风格和旧文档不一致 | 没有把旧文档内容作为上下文传入 | 检查 messages 里是否包含 target_doc 原文 | 把旧文档内容加入 user prompt |
| 代码块没有语言标注 | 规范文件没生效,或模型忽略了规范 | 检查 SPEC.md 是否被读取 | 在 system prompt 中增加硬性要求,并在验证层拦截 |
| 文档里的命令无法执行 | 模型生成的命令是虚构的 | 检查 bash 代码块和 dry-run 结果 | 加强验证层,命令必须白名单匹配 |
| API 请求超时 | 上下文太长或模型推理速度慢 | 查看请求耗时,检查上下文 token 数 | 减少 diff 范围,分模块处理,调大 timeout |
| 批量任务跑到一半卡住 | 某个任务上下文异常,或 API 限流 | 查看任务日志,定位卡住的任务 | 增加每任务超时时间,失败自动重试并记录 |
| token 消耗突然升高 | 引入无关文件,或 Agent 重写了整个文档 | 检查请求日志和 token 用量 | 在 prompt 里明确“只处理 diff 涉及部分” |
| Agent 被中断后状态丢失 | 没有保存中间步骤状态 | 检查是否有任务状态文件 | 每个任务写一个 state.json,记录当前进度 |
| 模型反复推荐删除已废弃接口 | 模型对项目背景不了解 | 检查 prompt 是否包含足够背景 | 在上下文中加入接口的弃用声明或历史原因 |
其中“任务中断后状态丢失”是很容易被忽略的问题。Agent 可能已经生成了三份文档,突然遇到超时或网络抖动,整个任务从头再来。建议在每一份文档写盘之后,立即更新 state.json,标记完成状态。这样重启后可以跳过已完成文件,只续跑未完成的部分。
10. 最佳实践与延伸
把文档 Agent 接入团队工作流之后,有几条工程建议可以让你少踩坑。
第一,第一次跑的时候把参数调小。只选一个模块、一次 diff、一个小模型,先看流程是否走通,不要一上来就整个仓库全量生成。全量生成的失败排查成本会高很多。
第二,保留一套最小可运行配置。把.env.example、tasks/task.example.json、SPEC.md都放进 git 仓库,新机器上克隆下来,改一下 API Key 就能跑。这样不管是换电脑还是新手加入团队,五分钟就能复现环境。
第三,目录分层管理。原始文档、Agent 草稿、验证报告、人工确认后的文档,分别放在不同目录。不要让 Agent 直接覆盖正式文档,至少要先经过 diff 审核。
第四,批量任务必须加日志和失败重试。日志里至少要包含每个任务的输入文件列表、输出 token、耗时、验证结果。没有日志,你无法判断一次批量生成是成功还是“表面成功但内容全错”。
第五,接口服务要限制访问范围。暴露在网络上的文档生成 API,一定要做好鉴权、路径白名单、请求体大小限制。否则别人可以传一个repo_path=/etc来探测你的服务器文件。
第六,涉及内部代码时要注意数据合规。发送给外部大模型 API 的代码片段,如果包含未公开的业务逻辑,建议先用本地化部署模型或脱敏工具处理。
从“让 Agent 按预期生成代码文档”这个 Show HN 思路出发,我们能延伸出来的方向其实不少。比如把验证层做深,接入 Playwright 之类的自动化测试,让文档中的前端示例代码真的跑一遍浏览器测试;也可以把“interrupt”机制加入 Agent 执行流程,在人工发现生成方向错误时,允许随时打断并向 Agent 追加修订意见;还可以把多个文档 Agent 合并成一个流水线,生成文档、写 Changelog、整理 commit message,一套链路全部自动完成。最有价值的第一步,是先在你的一个真实仓库里跑通“采集 diff -> 生成文档 -> 验证链接和命令”这个最小闭环。跑通之后,你才会真正理解哪些地方需要 Agent 更聪明,哪些地方其实是工程化就能解决的问题。