<!-- title: 自建Skill：中文问数据库 -->
# 自建 Skill：中文问数据库——从零打造一个只读数据问答技能

## 概述

本教程带你在 30 分钟内从零创建一个完整的 Claude Code 自定义 Skill——`ask-db`：用户用一句中文提问，Claude 自动生成 SQL、在只读安全沙箱里执行、再用自然语言回答。你将掌握 2026 版 Skills 的全部核心能力：frontmatter 触发、动态上下文注入（`!`command``）、`scripts/` 工程化与 `${CLAUDE_SKILL_DIR}`、渐进式披露的参考文件、参数传递与 `allowed-tools`。适合已用过 Claude Code、想把"重复粘贴的查库流程"固化成可复用能力的开发者与数据分析师。

## 前置条件

- **Claude Code v2.1.145+**：本教程用到的 `${CLAUDE_SKILL_DIR}`、动态注入、`skill-creator` 评测等特性都需要较新版本；命令行执行 `claude --version` 确认。
- **Python 3.8+**：查询脚本只依赖标准库 `sqlite3`，无需 `pip install`。
- **sqlite3 CLI**（可选）：用于在 `SKILL.md` 里把 schema 实时拉进上下文。
- **一个 SQLite 数据库**：没有也行，文末实战案例会从零建一个 `sales.db`。
- **对 Claude Code 的基本操作**：会启动会话、会输入 `/skill-name`。

---

## 第一步：先决定"要不要做成 Skill"

很多人上手就想造 Skill，但 Skills 官方文档给了一个清晰的判据——**当你在对话里反复粘贴同一段指令、检查清单或多步流程时，或者当 `CLAUDE.md` 里某一节从"事实陈述"长成了一段"操作步骤"时，就该抽成 Skill**。区别在于生命周期：`CLAUDE.md` 的内容常驻上下文，而 Skill 的正文只在被触发时才加载，长篇参考材料平时几乎零成本。

把"用中文查数据库"这件事和另外两种扩展方式做个对照，你能清楚为什么它最适合做成 Skill：

| 维度 | CLAUDE.md | Skill（本教程） | MCP 服务器 |
|---|---|---|---|
| 加载时机 | 常驻 | 触发/手动调用时 | 工具被调用时 |
| 适合内容 | 事实、约定 | 多步流程 + 脚本 + 参考材料 | 高频原子工具、跨会话状态 |
| 改造成本 | 改文件即生效 | 改文件即生效（live change detection） | 写服务进程、定协议 |
| 查库场景 | 太重，浪费 token | ✅ 恰好 | 适合"查库存"这类高频原子调用 |

我们的场景是"理解一句中文 → 编排多步（生成 SQL、安全执行、组织回答）"，这正是 Skill 的甜区。高频的"查某个 SKU 库存"那种点查，才更值得做成 MCP 工具。理解这条边界，是写好 Skill 的前提。

## 第二步：规划目录结构

Skills 遵循开放的 **Agent Skills 标准**，每个 Skill 就是一个目录，入口是 `SKILL.md`。我们把它放在**个人级**目录，这样所有项目都能用：

```bash
mkdir -p ~/.claude/skills/ask-db/scripts
mkdir -p ~/.claude/skills/ask-db/reference
```

最终结构长这样：

```
ask-db/
├── SKILL.md              # 入口：frontmatter + 编排指令（必须，<500 行）
├── scripts/
│   └── run_query.py      # 只读查询执行器（被运行，不进上下文）
└── reference/
    ├── schema.md         # 完整表结构说明（按需加载）
    └── safety_rules.md   # SQL 安全红线（按需加载）
```

> **预期结果**：目录创建成功。Claude Code 会**实时监听** `~/.claude/skills/` 的文件变化——新建目录、改 `SKILL.md` 在当前会话内立即生效，无需重启。注意：若创建的是会话启动时不存在的**顶层** skills 目录，才需要重启一次让它被纳入监听。

> **常见问题**：报错"No such file"——多半是 `~` 没展开，用 `echo $HOME` 确认绝对路径。Windows/PowerShell 环境把内联 shell 默认从 bash 换成 powershell 需设 `CLAUDE_CODE_USE_POWERSHELL_TOOL=1`。

## 第三步：写 frontmatter——把触发做精准

`SKILL.md` 顶部是 YAML frontmatter（`---` 之间），告诉 Claude **何时**用这个 Skill；下面是 markdown 正文，告诉它**怎么做**。先写 frontmatter：

```yaml
---
description: 用中文自然语言查询 SQLite 数据库并给出人类可读的回答。当用户想"查一下/统计/算一下"数据库里的数据、问销售/库存/用户等问题、或要求生成 SQL 时使用。不用于修改数据。
when_to_use: 触发词包括"查一下""帮我统计""有多少""按…分组""上个月/最近"等数据提问，以及"对 sales.db 跑个查询"。
argument-hint: "[数据问题]"
allowed-tools: Bash(python3 *) Bash(sqlite3 *)
---
```

这里有几条 2026 版的关键规则，踩中才能精准触发：

1. **`description` 把核心用例放第一句**。`description` 和 `when_to_use` 拼接后在技能列表里会被**截断到 1,536 个字符**，而且 Claude 是靠它做语义匹配决定是否自动加载的——第一句写清"做什么 + 何时用"，比堆关键词有效。
2. **用 `when_to_use` 补充触发短语**，而不是往 `description` 里塞。两者分工不同：前者是"能力 + 场景"，后者是"例子请求"。本教程末尾"已覆盖主题"里那些泛泛而谈的标题，多半就是 `description` 写得太宽。
3. **不设 `disable-model-invocation: true`**。这个字段会让 Claude 永不自动触发、只能你手动 `/ask-db`。我们希望用户说"查一下上月销量"就自动用，所以保留默认。
4. **`allowed-tools` 预批准权限**：列出 `Bash(python3 *)` 后，Claude 跑查询脚本时不再每次弹窗问你要不要批准。注意，它只是"免询问"，并不限制可用工具范围。

> **预期结果**：保存后在会话里输入 `/` 应能看到 `ask-db` 出现在补全列表；说"帮我查一下数据"应能被自动触发。

> **常见问题一：Skill 不触发**——八成是 `description` 太抽象。把"数据库助手"改成"用中文查 SQLite 并回答数据问题"这种带动词和场景的句子。
> **常见问题二：触发太频繁**——`description` 写了"回答任何问题"，Claude 就会在无关对话里也加载它。把范围收窄到"数据/统计/SQL"。
> **常见问题三：description 被截断**——精简到 1500 字符内，把长例子挪进 `reference/`。

## 第四步：动态注入实时表结构

NL-to-SQL 最大的难点是 Claude 得先知道库里有哪些表、哪些列。**动态上下文注入**（`!`command``）正好解决：在 `SKILL.md` 正文里写一行 shell，Claude Code 会**先跑这条命令、把输出原地替换进正文**，Claude 看到的就是真实 schema 而非命令本身。

在 `SKILL.md` 正文区写下骨架（完整版见第六步）：

```markdown
## 当前数据库结构

!`sqlite3 sales.db ".schema" 2>/dev/null || echo "（未找到 sales.db，请向用户确认数据库路径）"`

> 上面是实时拉取的表结构。若为空，先问用户数据库文件在哪，或参考 `reference/schema.md` 的约定。
```

语法要点：

- **内联形式** `!`command`` 只在 `!` 处于**行首或紧跟空白**时才被识别。写成 `KEY=!`cmd`` 会被当字面量、不执行。
- **多行命令**用围栏代码块开头写 ` ```! `：
  ````markdown
  ```!
  sqlite3 sales.db ".tables"
  sqlite3 sales.db "SELECT COUNT(*) FROM customers"
  ```
  ````
- 这是**预处理**：命令只对原始文件跑一次，输出当纯文本塞进去，**不会**被二次扫描，所以一条命令吐出的内容里若含 `!`cmd`` 不会被再展开——天然安全。
- 想全局禁用这种执行（比如托管环境），在 settings 里设 `"disableSkillShellExecution": true`，每条命令会被替换成 `[shell command execution disabled by policy]`。

> **预期结果**：触发 Skill 时，Claude 的 prompt 里直接是 `CREATE TABLE customers (...)`，而不是一条 sqlite3 命令。

> **常见问题**：schema 很大、几十张表——别全塞进 `SKILL.md`，那会让 Skill 正文每轮都占上下文。改为只注入 `.tables` 列表，把详细结构放进 `reference/schema.md`（下一步），让 Claude 按需读。

## 第五步：写安全的查询脚本（scripts 工程化）

Skill 正文应该简洁（官方建议 **<500 行**），重活交给 `scripts/` 里的脚本——脚本是被**执行**的，不进上下文，零 token 成本。我们写一个**只读**查询执行器，这是整个 Skill 的安全基石。保存到 `~/.claude/skills/ask-db/scripts/run_query.py`：

```python
#!/usr/bin/env python3
"""只读 SQLite 查询执行器：供 ask-db skill 调用。

安全设计：
1. 以 mode=ro 只读方式打开数据库，物理上无法写入。
2. 只允许 SELECT/WITH/EXPLAIN/PRAGMA 开头的查询，拒绝一切 DDL/DML。
3. 只执行单条语句（sqlite3 遇到多余语句会抛错），防止语句堆叠。
4. 强制行数上限与执行时限，防止全表扫描拖垮机器。
"""
import sqlite3, sys, time, re, json

MAX_ROWS = 500
TIMEOUT_MS = 5000
ALLOW_PREFIX = ("select", "with", "explain", "pragma")
FORBIDDEN = re.compile(
    r"\b(insert|update|delete|drop|alter|create|truncate|attach|detach|"
    r"replace|merge|vacuum|reindex)\b", re.IGNORECASE)

def run(db_path: str, sql: str) -> None:
    sql = sql.strip().rstrip(";")
    if not sql:
        print(json.dumps({"error": "空查询"}, ensure_ascii=False)); return
    first = sql.split(None, 1)[0].lower()
    if not sql.lower().startswith(ALLOW_PREFIX) or FORBIDDEN.search(sql):
        print(json.dumps({"error": "仅允许只读 SELECT 查询"}, ensure_ascii=False)); return

    # 以只读 URI 打开；找不到库会直接报错
    con = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True, timeout=1)
    con.row_factory = sqlite3.Row

    deadline = [time.monotonic()]
    def progress():
        if (time.monotonic() - deadline[0]) * 1000 > TIMEOUT_MS:
            return 1  # 中止查询
        return 0
    con.set_progress_handler(progress, 1000)  # 每处理 1000 个虚拟机指令检查一次

    wrapped = f"SELECT * FROM ({sql}) AS _q LIMIT {MAX_ROWS}"
    try:
        cur = con.execute(wrapped)  # execute 只跑首条语句，多语句会抛错
        rows = cur.fetchall()
        out = {"columns": [d[0] for d in cur.description],
               "rows": [list(r) for r in rows],
               "rowcount": len(rows),
               "truncated": len(rows) >= MAX_ROWS}
        print(json.dumps(out, ensure_ascii=False, default=str))
    except sqlite3.Error as e:
        print(json.dumps({"error": f"SQL 执行失败: {e}"}, ensure_ascii=False))
    finally:
        con.close()

if __name__ == "__main__":
    if len(sys.argv) != 3:
        print(json.dumps({"error": "用法: run_query.py <db> <sql>"}, ensure_ascii=False))
        sys.exit(2)
    run(sys.argv[1], sys.argv[2])
```

四个安全层次是刻意的：**只读连接**是物理兜底；**前缀白名单 + 关键词黑名单**是语义校验；**单语句执行**防堆叠注入；**行数/时限**防资源耗尽。即使 Claude 生成了 `DROP TABLE`，这里也会被拦下。

路径用 `${CLAUDE_SKILL_DIR}` 引用，无论 Skill 装在个人、项目还是插件级都正确：

```bash
python3 ${CLAUDE_SKILL_DIR}/scripts/run_query.py sales.db "SELECT ...;"
```

> **预期结果**：手动跑 `python3 scripts/run_query.py sales.db "SELECT 1"` 返回 `{"columns":["1"],"rows":[[1]],...}`；跑 `DELETE FROM t` 返回 `{"error":"仅允许只读 SELECT 查询"}`。

> **常见问题一**：`${CLAUDE_SKILL_DIR}` 没展开——它是 Skills 提供的字符串替换变量，**只在 Skill 正文/动态注入里有效**，别在普通对话或子代理里用。
> **常见问题二**：子查询包裹 `SELECT * FROM (...) LIMIT N` 报错——极少数含特定窗口函数的语句会有歧义，让 Claude 换写法即可，安全优先。
> **常见问题三**：查询慢——`set_progress_handler` 会在超时后中止并报错，提示 Claude 加 `WHERE` 或建索引。

## 第六步：编排完整工作流

现在把 frontmatter、动态注入、脚本调用串成一条清晰的工作流。这是完整 `SKILL.md` 正文：

```markdown
# ask-db：中文问数据库

你是只读数据分析师。面对用户的数据问题，按以下步骤工作：

## 1. 确认数据源
默认库为 `sales.db`（当前工作目录）。schema 见上方注入块；若为空，先问用户库文件路径。
详细字段含义见 reference/schema.md（仅当注入的 schema 不够清晰时再读）。

## 2. 把问题翻译成只读 SQL
- 只生成 SELECT/WITH 开头的查询；禁止任何写操作。
- 列名用双引号、字符串用单引号；日期用 `DATE('now','-1 month')` 等。
- 复杂查询先在脑中验证聚合粒度，避免笛卡尔积。

## 3. 执行（必须走安全脚本，禁止直接 sqlite3 写命令）
用 ${CLAUDE_SKILL_DIR}/scripts/run_query.py 跑你的 SQL：
```
python3 ${CLAUDE_SKILL_DIR}/scripts/run_query.py <db> "<你的SQL>"
```
脚本会返回 JSON：`columns`/`rows`/`rowcount`/`truncated` 或 `error`。
若 `error`，修正 SQL 重跑；若 `truncated:true`，告知用户结果被截断到 500 行并建议加条件。

## 4. 用自然语言回答
不要把原始 JSON 丢给用户。用表格/要点呈现结论，先给答案再给关键数字，必要时附上你用的 SQL。

## 红线
- 永不绕过 run_query.py 直接执行写语句。
- 安全规则的细节见 reference/safety_rules.md（有争议时再查）。
```

注意第 4 步那句话——**"skill 看起来在影响行为"和"它真的影响了"是两回事**。Skill 正文被调用后作为一条消息留在会话里整个任务期间有效，Claude 不会每轮重读文件。所以把"贯穿任务的红线"写成**常驻指令**（如"必须走脚本"），把一次性步骤写清楚，行为才会稳定。

补两个轻量参考文件。`reference/schema.md`：

```markdown
# sales.db 表结构约定
- customers(id, name, city, signup_date)
- products(id, name, category, price)
- orders(id, customer_id, order_date, total) — total 为订单金额
- order_items(order_id, product_id, qty, unit_price) — 明细
时间字段统一存为 ISO 字符串 'YYYY-MM-DD'。
```

`reference/safety_rules.md`：把脚本里那四层防护用中文复述一遍，供 Claude 在用户质疑"为什么不让我 UPDATE"时引用。这两个文件**默认不加载**，只有 SKILL.md 里引用它们、且 Claude 判断需要时才被读入——这就是**渐进式披露**，长参考材料平时零成本。

## 第七步：测试、评测与迭代

先做两轮**人工冒烟**（务必在新会话里测，老会话有编写时的残留上下文会掩盖缺陷）：

- **自动触发**：直接说"上个月销量最高的三个商品是谁"，看是否自动加载 `ask-db`、是否走脚本。
- **手动调用**：`/ask-db 按城市统计订单数`，看 `$ARGUMENTS` 是否被正确传入问题。

**手动验证技巧**：分别"开 Skill 跑一次"和"关掉 Skill 跑一次"对比结果——这是官方推荐的基线对照法。

接着上**自动化评测**。`skill-creator` 插件把"开/关对照"循环自动化了：

```text
/plugin install skill-creator@claude-plugins-official
# 若提示找不到，先：
/plugin marketplace update claude-plugins-official
/reload-plugins
```

装好后对 Claude 说"用 skill-creator 评测我的 ask-db skill"。它会：

1. 在 `evals/evals.json` 里存测试用例（prompt + 输入文件 + 期望行为）；
2. **每个用例开一个隔离子代理**跑（干净上下文），记录 token 与耗时；
3. 按断言打分写入 `grading.json`；
4. 把"开/关 Skill"的通过率、耗时、token 汇总进 `benchmark.json`，让你看到**加这个 Skill 到底提升了多少通过率、付出了多少 token**；
5. 还能对两个版本做盲测 A/B，确认改动是改进再提交，并自动生成"该触发/不该触发"样例、测算命中率、建议 `description` 调整。

> **预期结果**：`benchmark.json` 里"有 Skill"通过率显著高于"无 Skill"，且 token 开销可接受。

> **常见问题**：评测显示"命中率低、该触发没触发"——回到第三步，把 `description`/`when_to_use` 里缺的触发短语补上重测。

## 实战案例：30 秒跑通 sales.db

从零建一个库并提问。先造数据：

```bash
sqlite3 sales.db <<'SQL'
CREATE TABLE customers(id INTEGER PRIMARY KEY, name TEXT, city TEXT, signup_date TEXT);
CREATE TABLE products(id INTEGER PRIMARY KEY, name TEXT, category TEXT, price REAL);
CREATE TABLE orders(id INTEGER PRIMARY KEY, customer_id INT, order_date TEXT, total REAL);
CREATE TABLE order_items(order_id INT, product_id INT, qty INT, unit_price REAL);

INSERT INTO customers VALUES (1,'张三','上海','2024-01-15'),(2,'李四','北京','2025-05-20');
INSERT INTO products VALUES (1,'无线鼠标','外设',99),(2,'机械键盘','外设',399),(3,'显示器','显示',1299);
INSERT INTO orders VALUES (1,1,'2026-05-28',498),(2,2,'2026-05-30',399),(3,1,'2026-06-02',1299);
INSERT INTO order_items VALUES (1,1,1,99),(1,2,1,399),(2,2,1,399),(3,3,1,1299);
SQL
```

在 `sales.db` 所在目录启动 `claude`，直接问：

> 上个月（2026-05）销量金额最高的三个商品是哪些？

Claude 会自动触发 `ask-db`，注入 `sales.db` 的 schema，生成形如：

```sql
SELECT p.name, SUM(oi.qty*oi.unit_price) AS revenue
FROM order_items oi JOIN products p ON p.id=oi.product_id
JOIN orders o ON o.id=oi.order_id
WHERE o.order_date LIKE '2026-05%'
GROUP BY p.name ORDER BY revenue DESC LIMIT 3;
```

经 `run_query.py` 执行后，你会得到一段自然语言结论（例如"机械键盘 798 元居首，其次……"），而非原始 JSON。

整个过程的工作流：

```mermaid
flowchart TD
    A[用户中文提问] --> B{Claude 语义匹配<br/>description/when_to_use}
    B -->|命中| C[加载 ask-db Skill 正文]
    C --> D["动态注入 !sqlite3 .schema<br/>拿到真实表结构"]
    D --> E[Claude 生成只读 SELECT SQL]
    E --> F["运行 scripts/run_query.py<br/>只读+白名单+行数时限"]
    F --> G{执行结果}
    G -->|error| E
    G -->|JSON rows| H[Claude 组织自然语言回答]
    H --> I[表格/要点交付给用户]
    G -->|truncated| J[提示加 WHERE 重试]
    J --> E
```

## 进阶：把 Skill 跑在隔离子代理里

当库很大、查询很多时，主会话上下文会被 schema 和中间结果撑爆。给 frontmatter 加两行，让整个 Skill 在**隔离子代理**里跑、只把结论回传：

```yaml
context: fork
agent: Explore
```

`context: fork` 让 `SKILL.md` 正文成为驱动子代理的 prompt；`agent: Explore` 用内置的只读探索代理（它**跳过 CLAUDE.md 和 git status**，上下文极小）。注意：`fork` 只适合"有明确任务"的 Skill；若是纯约定类（"用这些 SQL 规范"）放进 fork，子代理拿到规范却没有可执行任务，会空手返回。

## 分发与共享

- **个人级**：放 `~/.claude/skills/ask-db/`，你所有项目可用。
- **项目级**：提交 `.claude/skills/ask-db/` 到版本控制，团队 clone 即用（首次需接受 workspace 信任对话框，因为 `allowed-tools` 在信任后才生效——审阅项目 Skill 再信任，别盲目信任来路不明的仓库）。
- **插件化**：在 Skill 目录加 `.claude-plugin/plugin.json`，它就变成 `<name>@skills-dir` 插件，还能捆绑 hooks、agents、MCP 服务器，通过 marketplace 分发。
- 同名时优先级：**企业托管设置 > 个人 > 项目**，任一级别还能覆盖同名内置 Skill（如自定义 `/code-review`）。

## 延伸阅读

- 官方 Skills 总览（frontmatter 全字段、动态注入、子代理、评测）：`code.claude.com/docs/en/skills`
- skill-creator 评测循环与基准/版本对比：agentskills.io 的 "Evaluating skill output quality"
- Agent Skills 开放标准跨工具说明：anthropics/skills 仓库 README
- 决策指南——何时用 CLAUDE.md / Skill / Subagent / MCP：Claude 官方博客《Steering Claude Code》

## 训练营适用性评估

- **适合**：企业内"AI 编程实战"5 天营的第 3 天（Skill 工程化日）、数据分析方向的 2 天速成营、安全方向的"安全护栏"模块（本 Skill 的四层防护是绝佳教学案例）。
- **预计教学时长**：讲解 + 跟做 90 分钟；含 skill-creator 评测与子代理进阶约 3 小时。
- **前置**：能独立用 Claude Code 跑过一个项目。**不适合**零基础第一课——需要先理解 frontmatter 触发与会话上下文。

## 参考来源

1. [Extend Claude with skills — Claude Code Docs](https://code.claude.com/docs/en/skills) — 官方权威：frontmatter 字段表、动态注入 `!`command``、`${CLAUDE_SKILL_DIR}`、`context: fork`、skill-creator 评测流程
2. [Steering Claude Code: skills, hooks, subagents and more — Claude Blog](https://claude.com/blog/steering-claude-code-skills-hooks-rules-subagents-and-more) — CLAUDE.md / Skill / Subagent / MCP 的官方决策框架与扩展心智模型
3. [anthropics/skills — GitHub](https://github.com/anthropics/skills) — Agent Skills 开放标准官方仓库，含可复用示例 Skill 与目录约定
4. [Claude Code Skills: The Complete Guide (2026) — Duet](https://duet.so/guides/claude-code-skills-complete-guide) — 第三方完整指南，覆盖 SKILL.md/MCP/子代理/团队协作
5. [Claude Code Customization Guide (2026) — alexop.dev](https://alexop.dev/posts/claude-code-customization-guide-claudemd-skills-subagents/) — CLAUDE.md vs Skills vs Subagents 的对比决策表

> [!tip] 51学通信知识星球
> **星主：爱卫生**（微信：gprshome201101）——公众号「51学通信」创办者，19 年通信行业运维与讲师经验，著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书（各大电商平台有售）。
> **星球定位**：深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域，主打运维岗位与信令分析。
> **当前规模**：5200 篇主题 / 990 个回答，198 元/年。
> **加入福利**：加入星球后请加爱卫生微信，拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题，另有群内分享。
> **AI 加持**：51学通信知识星球已作为 skill 接入 WorkBuddy，使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例，做总结、找文档和 PPT 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
