<!-- title: 提示词原语：构建参数化模板库 -->

# 提示词原语：构建参数化模板库

## 概述

MCP（Model Context Protocol）一共有四大交互原语：Tools（工具）、Resources（资源）、Prompts（提示词）、Sampling（采样）。前三者已有大量实战文章覆盖，唯独 **Prompts（提示词原语）** 这块长期被一笔带过。本教程专攻这一原语，目标是用 Python FastMCP 从零搭建一个 **可参数化、可动态生成、可内嵌资源、可编排多消息** 的提示词模板库，并把它注册进 Claude Code 变成斜杠命令。适用人群：已跑通过至少一个 MCP 服务器、想理解"提示词为什么是独立原语"并把它工程化的开发者。

## 前置条件

- 已安装 **Node.js ≥ 18**（Claude Code 运行所需）和 **Python ≥ 3.10**
- 已安装 Claude Code CLI（`npm i -g @anthropic-ai/claude-code`），并能正常启动
- 已安装 FastMCP：`pip install fastmcp`
- 了解 MCP 基本概念（client/server、stdio 传输、JSON-RPC），看过任意一篇 Tools 实战文章即可

## 正文

### 步骤 1：先想清楚——提示词为什么是独立原语

很多人第一次接触 MCP 时会困惑：**"我已经有 Tools 了，为啥还要 Prompts？不就是把一段提示词塞进工具里返回吗？"** 这是一个关键的概念分水岭。MCP 规范用"控制权归属"来区分三大原语：

| 原语 | 谁来触发 | 触发方式 | 典型用途 |
|------|---------|---------|---------|
| **Tools（工具）** | **模型控制** | LLM 自己决定何时调用 | 查数据库、调 API、改文件 |
| **Resources（资源）** | **应用控制** | 客户端程序决定何时挂载 | 注入文件、配置、上下文文档 |
| **Prompts（提示词）** | **用户控制** | 人显式选择/触发 | 复用工作流、标准化指令模板 |

理解这张表，就理解了提示词原语的定位：**它不是给模型用的，是给人用的。** Tools 是 LLM 的"手脚"，Prompts 是人的"快捷指令"。当一段精心调试过的提示词（比如"代码审查七步法""提交信息生成器""安全审计开场白"）需要反复使用、需要带参数、需要在团队间共享时，把它做成 Prompt 原语，比每次手敲、或者藏在某个工具里要干净得多。

提示词原语在协议层只有两个方法：

- `prompts/list`：返回服务器上所有可用提示词的清单（名字、描述、参数定义）
- `prompts/get`：客户端传入 `{name, arguments}`，服务器返回**一组已经填好参数的消息**（messages）

注意 `prompts/get` 返回的是**最终消息**，而不是一个模板字符串。这意味着服务器可以在返回前做任何计算：读文件、查数据库、拼多轮对话。这是"动态提示词"的核心。

```mermaid
flowchart LR
    U([用户]) -->|点击斜杠命令| CC[Claude Code 客户端]
    CC -->|prompts/list| S[(MCP 服务器)]
    S -->|Prompt 清单\n含参数定义| CC
    U -->|填入参数| CC
    CC -->|prompts/get\nname+arguments| S
    S -->|运行时计算\n读文件/查库/拼消息| S
    S -->|messages 数组| CC
    CC -->|注入对话上下文| LLM([LLM])
    LLM -->|按指令产出| U
```

### 步骤 2：最简静态提示词（无参数）

先用一个零参数的提示词把链路跑通。创建 `review_prompts.py`：

```python
from fastmcp import FastMCP

mcp = FastMCP("代码审查提示词库")

@mcp.prompt()
def commit_message() -> str:
    """生成规范的 Git 提交信息"""
    return (
        "你是一位资深工程师。请阅读用户接下来提供的 diff，"
        "按 Conventional Commits 规范生成一条提交信息：\n"
        "格式为 `type(scope): summary`，空一行后用一两句写动机与影响。\n"
        "type 从 feat/fix/docs/refactor/test/chore 中选择。"
    )

if __name__ == "__main__":
    mcp.run()   # 默认 stdio 传输
```

**预期结果**：FastMCP 会自动把函数名 `commit_message` 注册成提示词名，把 docstring 作为描述。它最终向客户端暴露的 `prompts/list` 响应长这样：

```json
{
  "prompts": [
    {
      "name": "commit_message",
      "description": "生成规范的 Git 提交信息",
      "arguments": []
    }
  ]
}
```

**常见问题**：如果服务器启动后客户端看不到任何提示词，绝大多数原因是 **docstring 缺失**。FastMCP 把 docstring 当作描述，没有 docstring 的函数在某些版本里会被跳过或触发校验告警。给每个 prompt 函数都写一行 docstring，是第一条纪律。

### 步骤 3：加上参数（arguments schema）

静态提示词只是热身。提示词原语的真正价值在于**参数化**。FastMCP 会根据函数签名自动推断参数定义：

```python
@mcp.prompt()
def code_review(
    language: str,
    focus: str = "readability",
    severity: str = "major",
) -> str:
    """按指定语言和关注点生成代码审查指令。

    Args:
        language: 编程语言，如 python / typescript
        focus: 审查关注点 security / performance / readability
        severity: 只报告该级别以上的问题 minor / major / critical
    """
    checklist = {
        "security": "输入校验、越权、注入、敏感信息泄露",
        "performance": "时间复杂度、N+1 查询、不必要的拷贝、锁粒度",
        "readability": "命名、函数长度、副作用、注释与代码一致性",
    }[focus]

    return (
        f"你是 {language} 代码审查专家。\n"
        f"本次只关注【{focus}】方面，检查清单：{checklist}。\n"
        f"只报告 {severity} 及以上级别的问题，忽略更轻微的瑕疵。\n"
        "输出格式：用 Markdown 表格列出【位置 | 问题 | 建议 | 严重度】，"
        "最后给一句话总结是否可合并。"
    )
```

**预期结果**：`prompts/list` 现在会返回带三个参数的清单，其中 `language` 标记为必填（无默认值），`focus` 和 `severity` 为可选。用户在 Claude Code 里触发这个命令时，会被依次询问这些参数。

**常见问题**：参数名不要用 Python 关键字或包含空格；可选参数一定要带默认值，否则会被识别成必填。类型注解尽量只用 `str`/`int`/`float`/`bool`，复杂类型（dataclass、自定义类）在参数推断上不可靠，应放到函数体内构造。

### 步骤 4：动态生成（运行时读取文件）

回到那个关键认知：`prompts/get` 返回的是**计算后的消息**。所以服务器可以在返回前读文件、查数据库、调别的 API。这把提示词从"死模板"升级成"活的指令生成器"。

```python
from pathlib import Path

SAFE_ROOT = Path("/home/gprshome/repo")  # 限定可读根目录，防止越界

@mcp.prompt()
def review_file(path: str, focus: str = "readability") -> str:
    """读取本地源文件并生成审查指令（运行时动态加载内容）。

    Args:
        path: 相对仓库根的文件路径
        focus: 审查关注点
    """
    # 安全：解析后必须仍在受控根目录内
    target = (SAFE_ROOT / path).resolve()
    if not str(target).startswith(str(SAFE_ROOT)):
        return f"拒绝访问：{path} 超出允许的根目录。"

    if not target.exists():
        return f"文件不存在：{target}"

    content = target.read_text(encoding="utf-8")
    return (
        f"请审查以下文件，关注【{focus}】：\n\n"
        f"```{target.suffix.lstrip('.')}\n{content}\n```\n"
        "只指出需要修改的地方，给出行号与改法。"
    )
```

**预期结果**：用户调用 `/mcp__review-prompts__review_file`，填入 `path=src/auth.py focus=security`，服务器现场读出该文件全文，连同审查指令一起作为一条 user 消息注入对话。LLM 立刻就能基于真实代码开始审查。

**常见问题**：
- **路径越权**：动态读文件务必做路径收敛校验（如上面的 `startswith` 检查），否则用户构造 `../../etc/passwd` 之类路径就能读到敏感文件。
- **大文件爆上下文**：先判断 `len(content)`，超过阈值（如 20k 字符）就只截取头部并提示"文件过大，已截断"。提示词原语不自带分页，全靠你自己控制返回体积。

### 步骤 5：内嵌资源（embedded resource）

当提示词需要携带一段**有明确 URI 和 MIME 类型的数据**时（比如一个文件、一张图、一段配置），不要把内容直接拼进字符串，而应该用 `embedded_resource`。这样客户端知道这是一段结构化资源，可以按 MIME 类型正确渲染（图片显示成图、代码高亮、二进制还原）。

下面是 `prompts/get` 最终返回的消息结构（FastMCP 会帮你生成这样的 JSON，这里展示协议真相以便理解）：

```json
{
  "description": "审查 src/auth.py 的安全性",
  "messages": [
    {
      "role": "user",
      "content": {
        "type": "resource",
        "resource": {
          "uri": "file:///home/gprshome/repo/src/auth.py",
          "mimeType": "text/x-python",
          "text": "def login(user, pwd):\n    ..."
        }
      }
    },
    {
      "role": "user",
      "content": {
        "type": "text",
        "text": "请基于上面的代码做安全审查，重点查越权与注入。"
      }
    }
  ]
}
```

**为什么用 embedded_resource 而不是把代码塞进 text？**
1. 客户端能按 MIME 类型做语法高亮，可读性更好。
2. URI 让这段数据有"身份"，方便调试时溯源（"这条指令引用了哪个文件"）。
3. 二进制内容（图片、PDF）只能走 `blob`（base64），无法塞进纯文本。

**常见问题**：MIME 类型尽量填准（`.py` 用 `text/x-python`，`.ts` 用 `text/typescript`），填错不会报错，但会丢失高亮。资源 URI 用规范的 `file://`、`https://` 或自定义 scheme（如 `repo://src/auth.py`）。

### 步骤 6：多消息序列与角色编排

一条提示词可以返回**多条消息**，每条带不同 `role`（`user` / `assistant`）。这能实现"预编排对话"——把一个多轮工作流的开场，一次性塞进上下文，省去和 LLM 来回试探。

下面这条提示词用"用户提需求 → 假装助手先表态 → 用户追加约束"的三段式，给安全审计定好基调：

```python
@mcp.prompt()
def security_audit(target: str):
    """编排多轮安全审计开场白。

    Args:
        target: 审计目标，如 '用户认证服务'
    """
    return [
        {
            "role": "user",
            "content": {"type": "text",
                "text": f"我们要对【{target}】做一次安全审计。"}
        },
        {
            "role": "assistant",
            "content": {"type": "text",
                "text": "明白。我会先枚举信任边界与攻击面，"
                        "再逐项核查常见漏洞（OWASP Top 10）。"}
        },
        {
            "role": "user",
            "content": {"type": "text",
                "text": "很好。请先输出威胁清单（表格），"
                        "再按修复成本给出优先级排序。不要急着改代码。"}
        },
    ]
```

**预期结果**：调用后，对话历史里会直接出现这三条消息，LLM 读到的上下文像是"已经聊过两轮"，于是它会顺着既定结构产出威胁清单，而不是漫无边际地发散。

**常见问题**：`assistant` 消息不要写得太满（不要替 LLM 把答案说完），否则它会直接复述。assistant 消息的作用是"定调子、定结构、定约束"，而不是"给答案"。

### 步骤 7：在 Claude Code 注册并以斜杠命令触发

服务器写好后，注册进 Claude Code 有两种方式。

**方式 A：命令行注册（推荐）**

```bash
claude mcp add review-prompts -- python /home/gprshome/review_prompts.py
```

**方式 B：项目级 `.mcp.json`**

在项目根目录创建 `.mcp.json`：

```json
{
  "mcpServers": {
    "review-prompts": {
      "command": "python",
      "args": ["/home/gprshome/review_prompts.py"]
    }
  }
}
```

**注册后验证**：

1. 重启 Claude Code 会话。
2. 输入 `/mcp` 查看已连接服务器，确认 `review-prompts` 状态为 connected。
3. 提示词会以**斜杠命令**形式出现，形如 `/mcp__review-prompts__code_review`。输入该命令，Claude Code 会依次询问你 `language`、`focus`、`severity` 的值，填完后服务器计算消息并注入对话。

**常见问题**：
- **命令不出现在列表**：用绝对路径指定 Python 解释器和脚本，避免 venv 未激活导致 `ModuleNotFoundError: fastmcp`。
- **触发后报"prompt not found"**：确认函数名（注册名=函数名，下划线保留），且该函数有 docstring。
- **想换名字**：用 `@mcp.prompt(name="audit")` 显式指定，注册名就变成 `/mcp__review-prompts__audit`。

## 实战案例：代码审查专家提示词服务器

把上面六个步骤整合成一个完整可运行的服务器。这个服务器对外提供四个提示词，覆盖静态、参数化、动态读文件、多消息编排四种形态：

```python
# /home/gprshome/review_prompts.py
from pathlib import Path
from fastmcp import FastMCP

mcp = FastMCP("代码审查提示词库")
SAFE_ROOT = Path("/home/gprshome/repo")
MAX_CHARS = 20000  # 单文件上限，防止爆上下文

# 1) 静态：提交信息生成器
@mcp.prompt()
def commit_message() -> str:
    """生成规范的 Conventional Commits 提交信息"""
    return (
        "阅读用户提供的 diff，按 Conventional Commits 规范生成提交信息：\n"
        "`type(scope): summary`，空一行写动机与影响。\n"
        "type 选自 feat/fix/docs/refactor/test/chore。"
    )

# 2) 参数化：可调语言/关注点/严重度的审查指令
@mcp.prompt()
def code_review(language: str, focus: str = "readability",
                severity: str = "major") -> str:
    """生成参数化的代码审查指令。

    Args:
        language: 编程语言，如 python / typescript
        focus: 关注点 security / performance / readability
        severity: 只报该级别以上 minor / major / critical
    """
    checklist = {
        "security": "输入校验、越权、注入、敏感信息泄露",
        "performance": "复杂度、N+1 查询、冗余拷贝、锁粒度",
        "readability": "命名、函数长度、副作用、注释一致性",
    }.get(focus, "通用质量")
    return (
        f"你是 {language} 审查专家，只看【{focus}】：{checklist}。\n"
        f"只报告 {severity} 及以上问题。"
        "输出表格【位置 | 问题 | 建议 | 严重度】，末尾给合并结论。"
    )

# 3) 动态读文件 + 内嵌资源
@mcp.prompt()
def review_file(path: str, focus: str = "readability"):
    """读取仓库内源文件并生成审查指令。

    Args:
        path: 相对仓库根的文件路径
        focus: 关注点
    """
    target = (SAFE_ROOT / path).resolve()
    if not str(target).startswith(str(SAFE_ROOT)):
        return {"role": "user", "content": {"type": "text",
                "text": f"拒绝访问：{path} 超出允许根目录。"}}
    if not target.exists():
        return {"role": "user", "content": {"type": "text",
                "text": f"文件不存在：{target}"}}

    content = target.read_text(encoding="utf-8")[:MAX_CHARS]
    return [
        {"role": "user", "content": {"type": "resource", "resource": {
            "uri": target.as_uri(),
            "mimeType": f"text/x-{target.suffix.lstrip('.') or 'plain'}",
            "text": content,
        }}},
        {"role": "user", "content": {"type": "text", "text": (
            f"基于上面文件，关注【{focus}】，列出需要修改之处"
            "（带行号与改法）。"
        )}},
    ]

# 4) 多消息编排：安全审计开场
@mcp.prompt()
def security_audit(target: str):
    """编排多轮安全审计开场白。

    Args:
        target: 审计目标，如 '用户认证服务'
    """
    return [
        {"role": "user", "content": {"type": "text",
            "text": f"我们要对【{target}】做安全审计。"}},
        {"role": "assistant", "content": {"type": "text",
            "text": "明白。我先枚举信任边界与攻击面，再按 OWASP Top 10 核查。"}},
        {"role": "user", "content": {"type": "text",
            "text": "请先输出威胁清单（表格），再按修复成本排序，先别改代码。"}},
    ]

if __name__ == "__main__":
    mcp.run()
```

**注册并试跑**：

```bash
# 注册
claude mcp add review-prompts -- python /home/gprshome/review_prompts.py

# 进入任意项目，启动 Claude Code 后：
/mcp                                   # 确认 connected
/mcp__review-prompts__code_review      # 选 python / security / critical
/mcp__review-prompts__review_file      # 输入 src/auth.py
/mcp__review-prompts__security_audit   # 输入 用户认证服务
```

你会观察到：`code_review` 只注入一段纯文本指令；`review_file` 注入了带高亮的代码块加指令；`security_audit` 直接在对话里铺好三段开场，LLM 顺着给威胁表。同一台服务器，四种形态，全部由"提示词原语"统一承载。

## 常见问题与排查速查

- **`prompts/list` 为空**：99% 是没写 docstring，或函数用了不可推断的参数类型。
- **斜杠命令没出现**：检查 `.mcp.json` 路径与解释器；MCP 连接失败时 `/mcp` 会显示具体报错（如 `ModuleNotFoundError`）。
- **大文件导致上下文爆炸**：在 `prompts/get` 里截断，或只返回摘要+URI，让用户决定是否进一步展开。
- **想把提示词变成团队共享**：把 `.mcp.json` 和脚本提交到仓库，队友拉取后 `/mcp` 自动加载；提示词随仓库版本化，比散落在各人 `~/.claude/commands/` 里更易维护。
- **提示词与 Claude Code 原生 slash command 的取舍**：原生命令（`~/.claude/commands/*.md`）轻量、纯文本、单文件；提示词原语胜在**可参数化、可动态计算、可跨客户端复用**（同一个 MCP 服务器在 Claude Code、Cursor、IDE 插件里都能用）。需要逻辑与参数就上提示词原语，只是一段固定文案就用原生命令。

## 整体架构与请求生命周期

```mermaid
sequenceDiagram
    participant U as 用户
    participant CC as Claude Code(客户端)
    participant S as 提示词服务器(FastMCP)
    participant FS as 本地文件系统

    Note over CC,S: 启动阶段
    CC->>S: spawn stdio 进程
    S-->>CC: initialize 完成
    CC->>S: prompts/list
    S-->>CC: [commit_message, code_review, review_file, security_audit]

    Note over U,FS: 使用阶段
    U->>CC: 输入 /mcp__...__review_file path=src/auth.py
    CC->>S: prompts/get {name: review_file, arguments:{path, focus}}
    activate S
    S->>FS: 读取 + 路径收敛校验
    FS-->>S: 文件内容
    S->>S: 截断 + 拼消息 + 内嵌 resource
    S-->>CC: messages[user:resource, user:text]
    deactivate S
    CC->>CC: 注入对话历史
    CC->>U: LLM 基于注入内容产出审查结果
```

## 延伸阅读

- **MCP 官方规范 · Prompts**：`modelcontextprotocol.io/specification/2025-06-18/server/prompts`——`prompts/list`、`prompts/get` 的权威协议定义。
- **FastMCP 文档**：`gofastmcp.com`——`@mcp.prompt()` 装饰器、消息返回类型的最新用法。
- **对比阅读**：本系列已发布的《三原语合一的 MCP 服务器实战》《Resources 原语：构建可订阅知识源》——把 Tools / Resources / Prompts 三者放在一起看，控制权分工会更立体。
- **进阶**：把动态提示词与 Sampling（让工具反向调用 LLM）结合，可实现"提示词自动迭代优化"的闭环。

## 训练营适用性评估

- **适合训练营类型**：MCP 开发进阶营、AI 工程化实战营、团队效能提升营。
- **前置要求**：学员须先完成至少一个 Tools 类 MCP 服务器动手实验，否则"控制权分工"这一核心概念会缺乏对照。
- **预计教学时长**：3 小时（讲解 45 分钟 + 编码实验 90 分钟 + 联调排错 45 分钟）。
- **可裁剪**：若时间紧张，步骤 5（内嵌资源）与步骤 6（多消息编排）可降为演示性讲解，保留步骤 2–4 作为必做实验。
- **考核点**：能否独立实现一个"带参数 + 动态读文件 + 内嵌资源"的提示词，并在 Claude Code 中以斜杠命令触发并验证消息注入正确。

## 参考来源

1. [Prompts — Model Context Protocol 官方规范](https://modelcontextprotocol.io/specification/2025-06-18/server/prompts) — 提示词原语的权威协议定义，含 `prompts/list`、`prompts/get`、arguments schema、embedded resource 与多消息结构。
2. [MCP Prompt Templates and Dynamic Content — Grizzly Peak Software](https://www.grizzlypeaksoftware.com/library/mcp-prompt-templates-and-dynamic-content-00pppbcy) — 系统讲解模板变量、上下文注入、多步提示词链与从数据源动态生成的完整指南。
3. [MCP Primitives Explained: Tools, Resources, and Prompts — Nomidl](https://www.nomidl.com/generative-ai/mcp-primitives-explained/) — 用真实例子对比三大原语，帮助理解"控制权归属"这一核心区分。
4. [mcp-prompts — GitHub (moarshy)](https://github.com/moarshy/mcp-prompts) — 开源提示词原语服务器参考实现，含动态内容生成与资源内嵌示例代码。
5. [MCP Prompts Explained (including how to actually use them) — Medium](https://medium.com/@laurentkubaski/mcp-prompts-explained-including-how-to-actually-use-them-9db13d69d7e2) — 从客户端视角说明提示词如何被发现与触发，补充本教程的"斜杠命令"链路。

> [!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)
