首页 › tutorials › 提示词原语:构建参数化模板库

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

概述

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

前置条件

正文

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

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

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

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

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

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

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:

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 响应长这样:

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

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

步骤 3:加上参数(arguments schema)

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

@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。这把提示词从"死模板"升级成"活的指令生成器"。

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,这里展示协议真相以便理解):

{
  "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 来回试探。

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

@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:命令行注册(推荐)

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

方式 B:项目级 .mcp.json

在项目根目录创建 .mcp.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。

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

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

# /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()

注册并试跑:

# 注册
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 顺着给威胁表。同一台服务器,四种形态,全部由"提示词原语"统一承载。

常见问题与排查速查

整体架构与请求生命周期

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 基于注入内容产出审查结果

延伸阅读

训练营适用性评估

参考来源

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

[!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学通信知识星球海报-扫码加入