进入实战篇。第一个练习是写一个让你团队所有项目共享的 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

  1. 单独 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
  1. 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": []
}
  1. 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。