5 minutes
实战一:构建一个 Docs Skill
进入实战篇。第一个练习是写一个让你团队所有项目共享的 docs-update Skill——专治"AI 帮我改完代码但文档没同步"。一个实战把第 20-24 章 Skill 五篇理论全部跑一遍。
25.1 任务定义
场景:AI 改了代码 → 你 review 通过 → 但 docs 没动 → 几天后用户文档跟现实不符。
目标 Skill:在每次代码改动 review 完成后,主动让 Agent 检查、必要时改 docs:
- 触发:commit / PR 阶段
- 不要触发:debug session、解释性问题
- DONE:所有公开 API、命令行参数、配置文件均与现有 docs 一致 / 已提出 docs 改动建议
- 失败模式:找不到 docs/、docs 在另一个 repo、docs 仅图片形式
25.2 写 SKILL.md
.opencode/skills/docs-update/SKILL.md:
---
name: docs-update
version: 0.1.0
description: |
After code changes are staged or committed, check whether the related docs
are still accurate. Propose doc patches where they drifted.
last_updated: 2026-07-19
---
# docs-update
## Trigger
Read this skill when EITHER:
- The user has run `git commit` / `gh pr create` / `task release`
- The user explicitly says "review 这个 diff" / "整理 PR"
- After 8+ lines of diff in tracked source files
DO NOT read when:
- User is asking an explanatory question
- The diff is purely docs/comments changes
- User explicitly says "no docs"
## Flow
1. Use `git diff --stat HEAD~1 HEAD` to get list of changed files.
2. Classify each changed file:
- public API surface (exported fns/classes in src/)
- CLI commands / args
- configuration schema (yaml/toml keys)
- private internals (less critical)
3. For each public-surface change, search docs/ for references:
- rg "<function_name>" docs/
4. Propose doc patches in a `docs-changes-pending.md` artifact:
- Old line: "..."
- New line: "..."
- File path
## DONE
When either:
- docs/ unchanged in tracked files AND docs-changes-pending.md is empty
- docs/ patches are written to docs-changes-pending.md and user confirms
## Failure Modes
- docs/ doesn't exist:
- suggest "create docs/" with README + API.md templates
- docs are in another repo:
- offer to open a sibling issue in that repo
- diffs are huge (>500 lines):
- sample 50 random lines for docs references, don't check all
## Self-report
All responses from this skill MUST be prefixed with `[docs-update]`.
25.3 写 references
references/api-mapping.md:
# API File Path → Doc File Mapping
## Python (src/...)
function `src/foo/bar.py::connect` → docs/api/foo.md#connect
class `src/foo/baz.py::Client` → docs/api/foo.md#Client
## TypeScript (src/...)
export const → docs/api.md
export class → docs/api.md
## CLI
cli.py:main args → docs/cli.md
config keys (yaml) → docs/config.md
references/anti-patterns.md:
# Anti-patterns to avoid
## Don't check tests/ folder for docs
Tests are not docs. Their public API changes don't need user docs update.
## Don't be exhaustive on internal files
Don't propose doc updates for `src/internal/foo.py` — those are not public surface.
## Don't generate new pages for unused code
If a new function is added but `_looks internal_` (prefixed `_`, lowercase only, etc.) → mark in docs-changes-pending.md as `[candidate for internalization]` instead.
25.4 第一版跑通
测试场景:
# 改一个公开函数
# 修改 src/foo.py: def is_email(...)
# 让代码 commit
opencode
> commit 我刚加的 src/foo.py 的更严格 email 校验
预期流程:
[tdd-reminder skipped] reason: one-line fix # 其他 skill
[docs-update] about to check docs
Discover changes:
- src/foo.py (is_email, is_handle)
API mapping:
- is_email → docs/api/foo.md#is_email
- is_handle → docs/api/foo.md#is_handle
Existing docs refs:
- docs/api/foo.md:42: "is_email(s): >=3 chars, accept + aliases"
Drift:
- The change adds length limit 64 chars. Doc says no length limit.
Propose patch:
docs/api/foo.md:42:
- "is_email(s): >=3 chars, accept + aliases"
+ "is_email(s): 3-64 chars, accept + aliases"
慢半拍 review 看到差异:直接 update docs 后 commit。
25.5 trigger test 5 个 probes
tests/probes.toml:
[[probe]]
text = "commit 我刚加的 src/foo.py 的更严格 email 校验"
expect = "engaged"
[[probe]]
text = "为什么 Python 的 email 校验需要折腾?"
expect = "skipped"
[[probe]]
text = "整理这个 PR"
expect = "engaged"
[[probe]]
text = "如何用 RAG 找文档?"
expect = "skipped"
[[probe]]
text = "task release"
expect = "engaged"
tests/test_trigger.py:
import toml, subprocess, json
PROBES = toml.load("tests/probes.toml")["probe"]
def run_probe(probe_text: str) -> str:
result = subprocess.run(
["opencode", "trigger-check", "--skill", "docs-update",
"--probe", probe_text],
capture_output=True, text=True,
)
return json.loads(result.stdout)["triggered"]
def test_probes():
for p in PROBES:
assert run_probe(p["text"]) == p["expect"], \
f"probe failed: text={p['text']}, expect={p['expect']}"
25.6 Behavior test:让 Skill 真的 produce
# tests/test_behavior.py
import subprocess, pytest
@pytest.fixture
def setup_repo(tmp_path):
# 创建一个 minimal git repo with src/foo.py + docs/api/foo.md
(tmp_path / "src").mkdir()
(tmp_path / "src" / "foo.py").write_text("def is_email(s): return True\n")
(tmp_path / "docs").mkdir()
(tmp_path / "docs" / "api").mkdir()
(tmp_path / "docs" / "api" / "foo.md").write_text("# is_email\n>=3 chars, any aliases, no length limit\n")
subprocess.run(["git", "init"], cwd=tmp_path, check=True)
subprocess.run(["git", "add", "."], cwd=tmp_path, check=True)
subprocess.run(["git", "commit", "-m", "init"], cwd=tmp_path, check=True,
capture_output=True)
# change the source
(tmp_path / "src" / "foo.py").write_text("def is_email(s): return 3 <= len(s) <= 64\n")
subprocess.run(["git", "add", "."], cwd=tmp_path, check=True)
subprocess.run(["git", "commit", "-m", "add length limit"], cwd=tmp_path, check=True,
capture_output=True)
return tmp_path
def test_docs_update_proposes_patch(setup_repo):
proc = subprocess.run(
["opencode", "--cwd", str(setup_repo), "oneshot",
"review 我刚的改动, 看看 docs"],
capture_output=True, text=True, timeout=120,
)
output = proc.stdout
assert "[docs-update]" in output
assert "3-64 chars" in output # 应反映新 const
assert "no length limit" in output # 应指出旧 doc
25.7 把 skill 推到团队 plugin repo
- 单独 git repo
ai-skill-docs-update/
├── README.md
├── LICENSE (Apache-2.0)
├── CHANGELOG.md
├── plugin.json
├── skills/
│ └── docs-update/
│ ├── SKILL.md
│ └── references/
└── tests/
├── probes.toml
├── test_trigger.py
└── test_behavior.py
- plugin.json
{
"name": "docs-update",
"version": "0.1.0",
"author": "your_org/ai-skills",
"compatible_with": ["opencode>=0.5"],
"skills": ["skills/docs-update"],
"mcp": [],
"hooks": []
}
- CI workflow(关键)
name: Skill regression
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: opencode-ai/setup-opencode@v1
- run: opencode trigger-check --skill skills/docs-update
- run: pytest tests/
- run: ruff check skills/
25.8 release 流程
git tag v0.1.0 -m "first release"
git push origin v0.1.0
# 发布 release notes
gh release create v0.1.0 --generate-notes
# 同时更新 plugin repo
# 安装方:
opencode plugin install github:your_org/ai-skill-docs-update@v0.1.0
25.9 真实 lab 一次端到端
3 周后看 trigger log:
rg "\[docs-update\]" .opencode/log/ --since '3 weeks ago' | wc -l
# 应得成正比的数,如 27 次
rg "\[docs-update\]" .opencode/log/ | awk -F'reason:' '{print $2}' | sort | uniq -c
# 1 user-skip, 1 doc-in-other-repo, 25 engaged
trace 中找失败模式 → 写进 SKILL.md 的 Failure Modes → 升 0.2.0。
如此三周一次迭代。一季度内你会让此 skill 升到 0.5.0,外面其他团队也会想用。
25.10 一个真实有意义的产出指标
| 指标 | 量化方法 | 期望 |
|---|---|---|
| 触发率 | session 数 / [docs-update] 出现次数 |
60-80% |
| accuracy | 提示对的 patches / 提出的 patches | 90%+ |
| 采纳率 | 采纳的 patch 数 / 提议的 patch 数 | 70%+ |
每次低于阈值时, trigger 优化或 failure mode 加项。
25.11 小结
- docs-update Skill 在 30 分钟内可蓝图完成:“改代码触发 → search docs → propose patch”
- trigger 用 5 个 probe + behavior test; 写到团队 plugin repo + GitHub Actions 跑 CI
- 装上后 3 周内数据迭代;稳步 90% accuracy 与 70% 采纳率是产价值
下一篇:《26 实战二:开发 MCP Server 集成内部工具》——把本公司的内部工具沉淀成可发现、可调度的 MCP server。
Summary: docs-update Skill 端到端 30 分钟起手,CI 跑回归、3 周数据迭代;从 0.1.0 升到 0.5.0 走完整 lifecycle。