Prompts 的机制:动态逻辑执行
普通的模板(如 Jinja2, Mustache)是静态的,只有字符串替换
但 MCP Prompt 是代码
当你调用一个 Prompt 时,服务端可以执行任意 Go/Python 代码来决定最终返回给 LLM 什么内容
场景:Code Review 助手
- 普通模板:
"请帮我 Review 一下这段代码:"- 缺点:用户必须手动复制粘贴代码到输入框里
- MCP Prompt:
- 客户端参数:
filename(用户只输入文件名 “main.go”) - 服务端逻辑:
- 接收
main.go - 代码执行:去文件系统读取
main.go的最新内容 - 代码执行:读取
git blame信息,看谁最后修改的 - 代码执行:读取相关的
linter报错日志 - 组装:把以上所有信息组合成一段 2000 字的 Prompt
- 接收
- 返回给 LLM:包含代码、Git 信息、报错日志的完整上下文
- 客户端参数:
结论:填空填进去的不仅仅是“参数”,而是触发了一系列后端逻辑
Prompts 的机制:服务端控制的对话编排
普通的 Prompt 模板通常只能生成一段纯文本(Text)
而 MCP Prompts 允许服务端返回一个完整的 消息对象列表(List of Messages)
这赋予了服务端直接控制 LLM “对话历史”和”思考路径”的权力
核心价值不是”能发 JSON”
而是服务端能通过协议做以下三件事:
A. 权限隔离(System/User 物理分层)
服务端可以强制指定
哪些信息是”系统指令”(System Prompt),哪些是”用户数据”(User Content)
普通模板:把指令和数据拼成一个大字符串
风险:用户数据里如果包含恶意指令(Prompt Injection),LLM 容易搞混
MCP 编排:服务端明确返回 role: system 和 role: user 的独立对象
优势:LLM 会赋予 System 更高的权重,且从结构上隔离了数据污染风险
B. 预设思维链(Assistant Prefill / Thinking Injection)
服务端可以伪造或预填充一条 role: assistant 的消息作为对话的”最新回复”
场景:你希望 LLM 在审查代码前,必须先列出所有引用的库
MCP 编排:服务端在返回的列表中,最后加一条:
1
2
JSON:
{ "role": "assistant", "content": "好的,我已经阅读了代码。首先,我将列出该文件引入的所有第三方库:\n1." }
效果:当 LLM 接收到这个列表时,它会被迫顺着这个”开头”继续写下去
这比在 User Prompt 里写“请你先列出库”要强效得多
这种方法叫做 Prefilling(预填充)
C. 少样本学习注入
服务端可以根据当前的上下文,动态插入几组”一问一答”的历史消息作为示例
场景:用户在写 SQL,服务端检测到是用 MySQL
MCP 编排:
服务端在 System 和 User 之间,动态插入 3 对 user + assistant 的历史消息,展示 MySQL 的特定语法
效果:无需改变 System Prompt,通过伪造历史对话,让 LLM 瞬间学会当前场景的特定风格
Prompts 的机制:Resource 内容的结构化嵌入
MCP Prompts 不仅仅是返回提示词文本
它能在协议层面将服务端实时读取的资源内容(如代码文件、数据库记录)包装成结构化对象
直接注入到对话消息中
为什么需要这个机制?
在没有 MCP 之前,如果你想把一个文件的内容发给 LLM,通常的做法是”字符串拼接”:
1
2
// ❌ 旧做法(非结构化):粗暴拼接
prompt := "请分析下面的代码:\n" + string(fileContent) + "\n代码结束"
这种做法有两个弊端:
- 语义模糊:LLM 看到的只是一大坨文本,很难区分哪里是指令,哪里是数据
- 元数据丢失:文件是什么类型(MIME Type)?来自哪里(URI)?这些上下文信息都丢失了
MCP Prompts 引入了 Embedded Resource 机制,解决了这个问题
技术实现:服务端 I/O + 结构化注入 当客户端调用 GetPrompt 时
服务端(MCP Server)会立即执行代码(例如 os.ReadFile/数据库查询)将读取到的实际内容封装进 JSON 返回
协议层面的 JSON 结构如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// JSON-RPC Response
{
"jsonrpc": "2.0",
"id": 1,
"result": {
// 1. 描述信息 (Optional)
"description": "Code review context for main.go",
// 2. 消息列表 (Required) —— 这就是 Prompt 的核心本体
"messages": [
// 第一条消息:System Prompt (纯文本)
{
"role": "system",
"content": {
"type": "text",
"text": "你是一个资深的 Go 语言专家,请专注于并发安全问题的审查。"
}
},
// 第二条消息:User Message (混合了 文本 + 资源)
{
"role": "user",
"content": {
"type": "resource", // 这里是资源嵌入
"resource": {
"uri": "file:///project/main.go", // 标识符
"mimeType": "text/x-go", // 类型提示
"text": "package main\n\nimport \"fmt\"..." // 实际文件内容
}
}
},
// 第三条消息:User Message (补充指令)
{
"role": "user",
"content": {
"type": "text", // 这里是普通文本
"text": "请检查上面代码中是否存在 Goroutine 泄露的风险。"
}
}
// 其余消息:
// type: text (指令、话术)、type: resource (文件内容、数据)、type: image (图片内容)
]
}
}
那为什么resource放在了”role”: “user”下面,而不是”role”: “system”
答:技术上可以放在 System 里,但工程上强烈建议放在 User 里
背后的逻辑:
- 语义定义的区别:身份 vs 素材
System 中的参数:定义”我是谁”
当你在 System Prompt 里填参数时,通常是在配置模型的人设、风格或边界
数据属于 “规则的一部分”,所以放在 System 里
User 中的 Resource:定义”我要处理什么”
当你把 Resource 放在 User Prompt 时,这是在提供待处理的原材料
数据属于 “任务的素材”,所以放在 User 里
- 安全边界的区别:可控 vs 不可控
System 参数:通常是”配置”
System Prompt 里的参数,通常来自你的系统配置,或者用户从下拉菜单里选的选项
所以它是可信的,可以放在 System 这个”最高权限区”
Resource 数据:通常是”素材”
Resource 里的数据,通常是文件内容、网页抓取结果或用户输入的长文本
所以它是不可信的
如果你把它放在 System 里,它的权重太高,容易覆盖掉你的安全指令
放在 User 里,模型会把它当做”用户说的话”来处理,防御力更强
放在 System 里:模型可能把它当成一条”最高指令”去执行,因为 System 代表权威
放在 User 里:模型更容易将其识别为”用户提供的一段包含恶意文本的数据”,从而保持 System 设定的人设不崩塌
将不可信的外部数据(Resource)与可信的内部指令(System Prompt)物理隔离
是防御 Prompt注入 的第一道防线
- 有没有 Resource 必须放在 System 的情况?有
有时候,你会检索出几条公司内部的高可信文档(比如《员工手册》)
你希望模型严格按照这些文档回答,绝对不能自由发挥
这时,为了提高权重,我们会把这些检索到的 Resource 放进 System里
为什么这里敢放?
数据源可信:这是公司内部审核过的文档,没有恶意注入
目的不同:这里的 Resource 是作为”法律”存在的,而不是”案件”
- 缓存策略
现在很多大模型(如:Claude, Gemini)都支持 Context Caching(上下文缓存)以降低长文本的成本和延迟
System Prompt 通常是静态的:你的助手人设(”你是一个专家…“)几周都不变
这部分非常适合被缓存
Resource 通常是动态的:用户一会传 main.go,一会传 user.go
如果混在一起:
如果你把 main.go 的内容塞进了 System 字段
那么每次文件变了,整个 System Prompt 的指纹(Hash)就变了,缓存就失效了
最佳实践:
System: 静态指令(长期缓存)
User: 动态指令 + Resource(短期缓存或不缓存)
需要注意的是:MCP 不能直接操作 Context Caching(Prompt Caching)
Context Caching(Prompt Caching) 是底层模型 API 的特性
只能由 客户端client 在调用模型 API 时自动管理
缓存基于提示前缀(prefix)的 hash
如果保持不变,就能命中缓存,省成本/延迟
MCP 只是一个协议层(基于JSON-RPC)
用于客户端 ↔ MCP server 的通信(暴露 tools、resources、prompts 等)
不包含控制底层 API 缓存的原语或方法
但 MCP 的使用可以间接优化或兼容 caching,也就是上面说的缓存策略
它通过 “决定数据放在哪(System 还是 User)”
实际上就是在 隐性操控 客户端的缓存命中率
Prompts 性能:会增加 LLM 推理耗时吗?
答案:不会。 MCP Prompts 和 MCP Tools 的运行机制完全不同
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
❌ 误区(类似 Tools 的流程)
━━━━━━━━━━━━━━━━━━━━━━━━━
用户提问
↓
Dify 把问题发给 LLM
↓
LLM 思考:"我需要调用工具"(消耗 Token,耗时)❌
↓
LLM 拿到模板,再次思考(消耗 Token,耗时)❌
✅ 真实流程(预处理/拼接)
━━━━━━━━━━━━━━━━━━━━━━━━━
Step 1:Client 填好参数(极快,几毫秒,纯用户侧)
Step 2:Dify 向 MCP Server 发送极小请求(网络开销 10-50ms)
Step 3:Server 进行字符串替换(代码逻辑,不涉及 AI,极快)
Step 4:Dify 把已拼装好的完整文本发给 LLM
Step 5:LLM 看到的就是一段完整指令,直接开始生成
结论:LLM 根本不知道这个提示词来自 MCP,还是手写的
所以 AI 推理时间(延迟)= 零增加
| MCP 原语 | 机制 | LLM 是否需要额外思考? | 耗时增加 |
|---|---|---|---|
| Prompts | 文本预处理/拼接 | ❌ No(发送前已完成) | 🟢 极低(仅网络传输) |
| Tools | 函数调用 (Function Calling) | ✅ Yes(需判断何时调用) | 🟡 中(多一轮交互) |
| Resources | 上下文挂载 (RAG) | ❌ No(作为背景知识预加载) | 🟢 极低(仅数据加载) |