Skip to content

第7章 Prompt:可复用的交互模板

"A good prompt is not just a sentence—it is a reusable contract between the user and the model."

本章要点

  • 理解 Prompt 在 MCP 三大原语中的独特定位:用户控制平面
  • 掌握 Prompt 的完整数据结构:name、description、arguments、messages
  • 学会使用动态参数让 Prompt 成为真正的模板
  • 理解 Prompt 中嵌入资源(Embedded Resource)的机制
  • 对比 TypeScript SDK 和 Python SDK 中 prompt() 方法的注册方式
  • 通过代码审查、Bug 报告、数据分析三个模板,建立实战直觉

7.1 为什么需要 Prompt

在前两章中,我们已经认识了 MCP 的另外两个原语:Tool 让模型能够执行操作,Resource 让应用程序能够提供上下文。但有一个问题被悄悄忽略了——用户自己呢?

设想一个日常场景:你在使用 AI 编程助手,每次做代码审查时,你都要手动输入一段冗长的提示词:"请从安全性、性能、可读性三个维度审查以下代码,指出问题并给出改进建议……"。这段话你可能每天要输入十几次。

Prompt 就是为解决这个问题而生的。它本质上是服务器预定义的、用户可选择的交互模板——类似于聊天工具中的斜杠命令(/review/summarize),用户选中后,客户端从服务器获取完整的消息模板,填入参数,直接发送给模型。

关键词是用户控制。与 Tool 不同(Tool 由模型决定何时调用),Prompt 的触发权完全在用户手中。用户看到可用的 Prompt 列表,主动选择要使用哪一个,填入必要的参数,然后发起对话。

7.2 三大控制平面:设计哲学

在深入 Prompt 的技术细节之前,让我们先从宏观视角理解 MCP 的设计哲学。MCP 定义了三种原语,每一种对应一个不同的控制平面

原语控制者类比典型交互
ToolAI 模型函数调用模型判断需要查询数据库,自动调用 query_db
Resource应用程序文件附件IDE 将当前打开的文件作为上下文附加到对话中
Prompt用户斜杠命令用户输入 /review,选择代码审查模板

这三个控制平面的划分并非随意为之。它反映了一个核心设计原则:不同类型的决策应该由最合适的主体来做。模型擅长判断何时需要工具辅助;应用程序知道当前的工作上下文是什么;而用户最清楚自己的意图和工作流程。

Prompt 属于用户控制平面,意味着:

  1. 服务器暴露可用的 Prompt 列表给客户端
  2. 客户端展示这些 Prompt 给用户(通常以斜杠命令或菜单的形式)
  3. 用户主动选择一个 Prompt 并填入参数
  4. 客户端向服务器请求该 Prompt 的完整消息内容
  5. 消息内容被插入到对话中,发送给模型

7.3 Prompt 的数据结构

7.3.1 Prompt 定义

一个 Prompt 在协议层面由以下字段描述:

json
{
  "name": "code_review",
  "title": "Request Code Review",
  "description": "Asks the LLM to analyze code quality and suggest improvements",
  "arguments": [
    {
      "name": "code",
      "description": "The code to review",
      "required": true
    },
    {
      "name": "language",
      "description": "Programming language of the code",
      "required": false
    }
  ]
}

各字段的含义:

  • name:Prompt 的唯一标识符,用于在 prompts/get 请求中引用。类似于函数名,不可重复。
  • title:可选的人类可读标题,用于 UI 展示。如果你的 Prompt 名为 code_review,title 可以是"代码审查"这样更友好的文本。
  • description:可选的描述信息,帮助用户理解这个 Prompt 做什么。
  • arguments:参数列表,每个参数有 namedescriptionrequired 三个字段。参数是 Prompt 实现动态化的关键——同一个模板,填入不同参数,生成不同的消息内容。

7.3.2 Prompt 结果:消息数组

当客户端调用 prompts/get 获取一个 Prompt 时,服务器返回的是一个 GetPromptResult,其核心是一个 messages 数组

json
{
  "description": "Code review prompt",
  "messages": [
    {
      "role": "user",
      "content": {
        "type": "text",
        "text": "Please review this Python code:\ndef hello():\n    print('world')"
      }
    }
  ]
}

每条消息(PromptMessage)包含两个字段:

  • role"user""assistant",表示这条消息的角色。是的,Prompt 可以预设多轮对话——比如先用一条 assistant 消息设定角色("你是一位资深代码审查专家"),再用 user 消息提出具体请求。
  • content:消息内容,支持三种类型——文本(text)、图片(image)和嵌入资源(resource)。

7.3.3 内容类型详解

文本内容是最常见的类型:

json
{
  "type": "text",
  "text": "请从安全性、性能、可读性三个维度审查以下代码……"
}

图片内容支持多模态交互,数据必须经过 Base64 编码:

json
{
  "type": "image",
  "data": "base64-encoded-image-data",
  "mimeType": "image/png"
}

嵌入资源是 Prompt 与 Resource 原语的交汇点,我们在 7.6 节会详细讨论。

7.4 协议消息流

理解了数据结构后,让我们看看 Prompt 在协议层面是如何工作的。

7.4.1 能力声明

服务器在初始化阶段必须声明 prompts 能力:

json
{
  "capabilities": {
    "prompts": {
      "listChanged": true
    }
  }
}

listChanged 表示服务器是否会在 Prompt 列表变化时发送通知。如果为 true,客户端可以在收到 notifications/prompts/list_changed 通知后重新拉取列表。

7.4.2 发现与使用

Prompt 的交互流程分为两步:先发现(list),再使用(get)。

prompts/list 请求用于获取服务器所有可用的 Prompt,支持分页(通过 cursor 参数):

json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "prompts/list",
  "params": {
    "cursor": "optional-cursor-value"
  }
}

prompts/get 请求用于获取特定 Prompt 的消息内容:

json
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "prompts/get",
  "params": {
    "name": "code_review",
    "arguments": {
      "code": "def hello():\n    print('world')"
    }
  }
}

服务器收到请求后,根据参数动态生成消息数组并返回。这里的"动态生成"是关键——Prompt 不是静态文本,而是由服务器端代码根据参数实时构造的。

7.5 动态 Prompt:参数作为变量

Prompt 的真正威力在于动态化。参数(arguments)使得同一个 Prompt 定义可以根据不同的输入生成完全不同的消息内容。

以一个数据分析 Prompt 为例。用户选择 /analyze_table,填入表名 users,服务器端的处理逻辑可能是:

  1. 根据表名查询数据库的表结构(schema)
  2. 采样若干行数据
  3. 将 schema 和样本数据组装成消息

返回的 messages 可能像这样:

json
{
  "messages": [
    {
      "role": "assistant",
      "content": {
        "type": "text",
        "text": "我是一位数据分析专家,擅长从数据中发现洞察。"
      }
    },
    {
      "role": "user",
      "content": {
        "type": "text",
        "text": "请分析 users 表。\n\n表结构:\nid INTEGER PRIMARY KEY\nname TEXT NOT NULL\nemail TEXT UNIQUE\ncreated_at TIMESTAMP\n\n样本数据:\n| id | name | email | created_at |\n|----|------|-------|------------|\n| 1 | 张三 | zhang@ex.com | 2024-01-15 |\n| 2 | 李四 | li@ex.com | 2024-02-20 |"
      }
    }
  ]
}

注意这里的多轮结构:第一条 assistant 消息为模型设定了角色,第二条 user 消息包含了服务器从数据库中实时查询到的结构和数据。用户只需要输入一个表名,服务器完成了所有繁重的上下文准备工作。

7.6 嵌入资源:Prompt 与 Resource 的交汇

Prompt 消息中不仅可以包含纯文本,还可以嵌入 MCP Resource。这意味着 Prompt 可以引用服务器管理的文件、文档、代码等资源,将它们直接注入到对话中。

嵌入资源的消息格式:

json
{
  "role": "user",
  "content": {
    "type": "resource",
    "resource": {
      "uri": "file:///project/src/main.py",
      "mimeType": "text/x-python",
      "text": "import os\nimport sys\n\ndef main():\n    ..."
    }
  }
}

资源内容可以是文本(text 字段),也可以是二进制数据(blob 字段,Base64 编码)。必须包含有效的 URI 和 MIME 类型。

这个机制有什么用?考虑一个代码审查 Prompt:用户选择 /review,传入文件路径作为参数,服务器读取文件内容,以嵌入资源的方式返回。这样做的好处是——客户端可以识别出这是一个资源引用,在 UI 中以特殊方式展示(比如显示为可折叠的代码块,带有文件名和语法高亮),而不是一堆原始文本。

7.7 SDK 实现:TypeScript 与 Python

7.7.1 TypeScript SDK

在 TypeScript SDK 中,通过 McpServer 类的 registerPrompt 方法注册 Prompt:

typescript
import { McpServer } from '@modelcontextprotocol/server';
import { z } from 'zod';

const server = new McpServer({
  name: 'my-server',
  version: '1.0.0'
});

server.registerPrompt(
  'review-code',
  {
    title: 'Code Review',
    description: 'Review code for best practices',
    argsSchema: z.object({
      code: z.string(),
      language: z.string().optional()
    })
  },
  ({ code, language }) => ({
    messages: [
      {
        role: 'user' as const,
        content: {
          type: 'text' as const,
          text: `Please review this ${language ?? ''} code:\n\n${code}`
        }
      }
    ]
  })
);

几个值得注意的设计点:

  1. argsSchema 使用 Zod:TypeScript SDK 使用 Zod 等 Standard Schema 兼容库定义参数结构,框架自动将其转换为 JSON Schema 暴露给客户端,并在请求到达时执行校验。
  2. 回调函数返回 GetPromptResult:回调接收经过校验的参数,返回包含 messages 数组的对象。
  3. 注册时自动处理列表和获取registerPrompt 内部同时设置了 prompts/listprompts/get 的请求处理器,开发者无需手动处理协议细节。

7.7.2 Python SDK

Python SDK 使用装饰器风格注册 Prompt,更加简洁:

python
from mcp.server.mcpserver import MCPServer

server = MCPServer(name="my-server")

@server.prompt()
def analyze_table(table_name: str) -> list[Message]:
    """Analyze a database table structure and content."""
    schema = read_table_schema(table_name)
    return [
        {
            "role": "user",
            "content": f"Analyze this schema:\n{schema}"
        }
    ]

@server.prompt()
async def analyze_file(path: str) -> list[Message]:
    """Analyze a file with embedded resource."""
    content = await read_file(path)
    return [
        {
            "role": "user",
            "content": {
                "type": "resource",
                "resource": {
                    "uri": f"file://{path}",
                    "text": content
                }
            }
        }
    ]

Python SDK 的设计特点:

  1. 装饰器 @server.prompt():注意括号不能省略。SDK 源码中专门做了检查——如果你写了 @server.prompt(不带括号),会抛出 TypeError 并给出明确提示。
  2. 从函数签名推导参数Prompt.from_function(func) 会通过 Python 的内省机制从函数签名中自动提取参数名、类型和是否必填,无需手动声明 arguments
  3. 支持同步和异步:回调函数可以是普通函数,也可以是 async 函数。
  4. 可选参数:装饰器接受 name(默认使用函数名)、titledescriptionicons 等可选参数。

7.7.3 两种 SDK 的对比

维度TypeScript SDKPython SDK
注册方式registerPrompt() 方法@server.prompt() 装饰器
参数定义显式 argsSchema(Zod)从函数签名自动推导
返回类型GetPromptResult 对象list[Message]
变更通知sendPromptListChanged()框架自动处理
禁用支持enabled 属性通过 PromptManager 管理

7.8 实战用例

7.8.1 代码审查模板

python
@server.prompt(
    name="code_review",
    description="Multi-dimensional code review"
)
def code_review(code: str, language: str = "python") -> list[Message]:
    return [
        {
            "role": "assistant",
            "content": "I am a senior code reviewer. I will analyze code "
                       "from three perspectives: security, performance, "
                       "and readability."
        },
        {
            "role": "user",
            "content": f"Please review the following {language} code. "
                       f"For each issue found, indicate its severity "
                       f"(critical/warning/info) and provide a fix.\n\n"
                       f"```{language}\n{code}\n```"
        }
    ]

这个模板的设计要点:先用 assistant 消息设定专家角色,再用 user 消息提出结构化的审查请求。language 参数有默认值,为可选参数。

7.8.2 Bug 报告模板

python
@server.prompt(
    name="bug_report",
    description="Generate a structured bug report"
)
def bug_report(
    title: str,
    steps: str,
    expected: str,
    actual: str
) -> list[Message]:
    return [
        {
            "role": "user",
            "content": f"Please help me write a detailed bug report.\n\n"
                       f"**Title**: {title}\n"
                       f"**Steps to reproduce**:\n{steps}\n"
                       f"**Expected behavior**: {expected}\n"
                       f"**Actual behavior**: {actual}\n\n"
                       f"Please analyze the possible root cause and "
                       f"suggest debugging approaches."
        }
    ]

这个模板将用户的碎片化输入(标题、步骤、期望行为、实际行为)组装成结构化的 Bug 报告,同时请求模型分析根因。四个参数全部为必填。

7.8.3 数据分析工作流

typescript
server.registerPrompt(
  'analyze-dataset',
  {
    title: 'Dataset Analysis',
    description: 'Comprehensive analysis of a dataset',
    argsSchema: z.object({
      table_name: z.string(),
      focus: z.enum(['trends', 'anomalies', 'correlations']).optional()
    })
  },
  async ({ table_name, focus }, ctx) => {
    // 服务器端动态查询数据库
    const schema = await getTableSchema(table_name);
    const stats = await getBasicStats(table_name);

    return {
      messages: [
        {
          role: 'assistant' as const,
          content: {
            type: 'text' as const,
            text: 'I am a data analyst. I will provide insights based on the data structure and statistics.'
          }
        },
        {
          role: 'user' as const,
          content: {
            type: 'resource' as const,
            resource: {
              uri: `db://schemas/${table_name}`,
              mimeType: 'application/json',
              text: JSON.stringify(schema, null, 2)
            }
          }
        },
        {
          role: 'user' as const,
          content: {
            type: 'text' as const,
            text: `Basic statistics:\n${JSON.stringify(stats, null, 2)}\n\n` +
                  `Please perform a comprehensive analysis` +
                  (focus ? `, focusing on ${focus}` : '') + '.'
          }
        }
      ]
    };
  }
);

这个示例展示了 Prompt 的高级用法:异步回调中执行数据库查询,结合嵌入资源和文本内容构建多条消息,可选的 focus 参数控制分析方向。

7.9 错误处理

服务器在处理 Prompt 请求时应返回标准的 JSON-RPC 错误码:

  • -32602(Invalid params):Prompt 名称不存在,或缺少必填参数。在两个 SDK 中,如果请求的 Prompt 未注册或已被禁用,框架会自动返回此错误。
  • -32603(Internal error):服务器内部处理错误,比如回调函数中的数据库查询失败。

从 TypeScript SDK 源码可以看到具体的错误处理逻辑:

typescript
const prompt = this._registeredPrompts[request.params.name];
if (!prompt) {
    throw new ProtocolError(
        ProtocolErrorCode.InvalidParams,
        `Prompt ${request.params.name} not found`
    );
}
if (!prompt.enabled) {
    throw new ProtocolError(
        ProtocolErrorCode.InvalidParams,
        `Prompt ${request.params.name} disabled`
    );
}

除了 not found 之外,还有 disabled 状态——这意味着 Prompt 可以被注册但暂时禁用,客户端在 prompts/list 中将不会看到它。

7.10 设计指南

在实际开发 MCP Server 时,设计 Prompt 应遵循以下原则:

1. Prompt 是用户意图的快捷方式,不是万能工具。 如果一个操作应该由模型自主决定是否执行,它应该是 Tool 而不是 Prompt。如果一个数据源应该自动附加到对话中,它应该是 Resource 而不是 Prompt。

2. 参数命名要直观。 用户在填写参数时需要理解每个参数的含义。codeinput_data 更好,languagelang_type 更好。description 字段不要省略。

3. 善用多轮消息结构。 assistant 角色的预设消息可以有效引导模型的行为模式。一条好的角色设定消息,往往比在 user 消息中反复强调"你是一个专家"更有效。

4. 考虑嵌入资源的时机。 当你的 Prompt 需要引用文件、数据库记录或其他结构化数据时,优先使用嵌入资源而非纯文本。嵌入资源让客户端能够更智能地处理和展示这些内容。

5. 校验参数。 服务器在处理 prompts/get 时应验证所有必填参数是否已提供,参数值是否合法。SDK 会帮你完成类型级别的校验,但业务级别的校验(比如表名是否存在)需要你自己处理。

7.11 本章小结

Prompt 是 MCP 三大原语中最贴近用户的一个。它的设计看似简单——不过是预定义消息模板——但其背后蕴含了重要的设计哲学:将控制权交给最合适的主体

回顾本章的核心内容:

  • Prompt 属于用户控制平面,由用户主动选择和触发,与 Tool(模型控制)和 Resource(应用程序控制)形成三足鼎立
  • 一个 Prompt 由 namedescriptionarguments 定义,通过 prompts/get 返回 messages 数组
  • Messages 支持 textimageresource 三种内容类型,可以包含多条不同角色的消息
  • 动态参数让 Prompt 从静态模板变成可编程的消息工厂——服务器可以根据参数执行数据库查询、文件读取等操作
  • 嵌入资源将 Prompt 与 Resource 连接起来,让模板能够引用服务器管理的任意资源
  • TypeScript SDK 通过 registerPrompt() 方法注册,Python SDK 通过 @server.prompt() 装饰器注册

在下一章中,我们将进入 SDK 实战环节,从源码层面深入理解 TypeScript SDK 的服务器实现。

基于 VitePress 构建