RAG 解决的是 LLM 的知识边界问题——把外部知识检索出来,注入到 prompt 里让模型去理解和回答。但注入的上下文如何组织、如何让模型区分检索到的知识和自身知识、如何要求模型引用来源——这些都是 prompt 设计问题。本文聚焦 RAG 场景下的提示词设计策略。
概述
LLM 有两个天然局限:知识截止日期和幻觉。RAG(Retrieval-Augmented Generation,检索增强生成)是目前最成熟的外挂补救方案:检索相关内容 → 注入 prompt → 让模型基于注入内容生成。
但实际应用中,大量精力被放在检索链路(向量数据库、embedding 模型、chunk 策略)上,最后一步——prompt 怎么写——往往被忽视。检索回来的内容质量再高,如果 prompt 没有设计好,模型照样可能忽略检索结果、自行编造,或者在多段检索内容之间产生混淆。
RAG 的 prompt 与普通 prompt 的根本区别:
普通 prompt:指令 + 输入
RAG prompt:指令 + 检索上下文 + 输入
多出来的”检索上下文”需要精心设计——格式、边界、权重,每个环节出问题都会影响最终输出质量。
RAG 流程中 Prompt 的角色
标准 RAG 流程:
1 2 3 4 5 6 7 8 9 10 11 12 13
| 用户提问 │ ▼ Query 处理(改写、扩展、翻译) │ ▼ 向量检索 → 重排序(Rerank) │ ▼ 构建 Prompt = System Prompt + 检索上下文 + 用户提问 │ ▼ LLM 生成回答
|
Prompt 在流程中的角色是承上启下:上游是检索系统返回的一段段文档 chunk,下游是用户期待的自然语言回答。Prompt 的任务是将检索到的内容”翻译”为模型能够正确理解并使用的形式。
System Prompt 设计
角色定义
RAG 场景下的角色与普通对话不同——不是自由发挥,而是信息整合者:
1 2
| 你是一个技术文档助手。你的知识来源是用户在消息中提供的文档片段。 你从不依赖自己的内部知识回答问题,除非文档中没有相关信息。
|
关键点:明确知识来源。这句话约束模型优先查阅提供的上下文,而非预训练记忆。
核心约束
RAG 的 System Prompt 需包含几项硬约束:
1 2 3 4 5 6
| ## 回答规则
1. **基于上下文回答**:优先基于提供的文档内容回答问题。文档中明确包含答案时,直接引用。 2. **不要编造**:文档中没有相关信息时,诚实地回答"提供的文档中没有涉及此问题",不尝试猜测或编造。 3. **区分来源**:同时使用文档内容和自身知识时,明确标注信息来源。 4. **保留不确定性**:文档内容之间有矛盾时,指出矛盾而非强行统一。
|
规则 2 最为关键——它直接决定 RAG 能否解决幻觉问题。RAG 系统如果频繁出现”看似正确但实际编造”的回答,根本原因通常是这条规则没有被严格执行。
空上下文的 Fallback
检索不是总能命中。如果不预先设计 fallback 策略,模型在空上下文下会基于预训练记忆强行”努力回答”,从而产生幻觉。
1 2 3 4 5 6
| ## 检索结果为空时的处理
如果提供的检索结果为空或无相关内容: 1. 直接回复:"抱歉,当前知识库中没有关于此问题的信息。" 2. 可以提供建议:用户可能想了解的相关主题方向 3. 绝对不要基于自己的训练数据尝试回答
|
这个 fallback 是必须的。没有它,模型在面对空上下文时退化为普通 LLM——它自然会”努力”回答。
上下文注入的格式设计
为什么格式重要
LLM 对 prompt 中不同部分的注意力权重是不均匀的。如果检索内容和指令混在一起,模型可能无法区分主次。
解决方案:用结构化的分隔符明确标记上下文边界。
XML 标签法
最常用的方式是用 XML 标签包裹检索内容:
1 2 3 4 5 6 7 8 9 10 11 12 13
| <context> <document id="1" source="python_docs_v3.12" relevance="0.95"> asyncio 是 Python 3.4 引入的标准库,用于编写单线程并发代码。 asyncio.run() 是运行异步程序的主要入口点,在 Python 3.7 中引入。 ... </document>
<document id="2" source="fastapi_docs" relevance="0.87"> FastAPI 基于 Starlette 构建,完全支持 asyncio。 可以在路径操作函数中使用 async def 来获得异步支持。 ... </document> </context>
|
XML 标签的三个优势:
- LLM 训练语料中有大量 XML/HTML,能天然理解标签的结构语义
- 标签提供明确的注意力锚点——
<context> 告诉模型”下面的内容需要重点关注”
- 属性可携带元信息——
id、source、relevance 等让模型知道每条内容的来源和可信度
元信息标注
每个文档 chunk 应附带元信息:
1 2 3 4 5 6
| <document id="chunk_42" source="PostgreSQL 16 官方文档 - 查询优化章节" date="2024-01-15" relevance="0.92" >
|
| 元信息字段 |
用途 |
id |
引用时精确定位来源 |
source |
人类可读的来源描述 |
date |
时效性判断——旧文档权重应更低 |
relevance |
检索相关度分数——模型据此决定信任程度 |
多文档的排序与截断
检索可能返回几十个 chunk,但 context window 有限。截断策略在代码层实现,prompt 层也需要配合:
- 按 relevance 降序排列:最重要文档放最前面(LLM 对 prompt 开头和末尾的注意力最高)
- 末尾显式告知截断:
1 2 3 4 5
| <context> ... (经过相关性排序的文档) ... </context>
注意:以上仅展示相关性最高的 {n} 个文档片段。如需更详细信息,请告知。
|
完整的 RAG Prompt 模板
将上述要点组合为一个完整模板:
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
| ## System Prompt
你是一个技术知识库助手。你的回答严格基于用户在 <context> 标签中提供的文档内容。
### 回答规则 1. 优先基于 <context> 中的内容回答。找到相关信息时直接引用原文。 2. 如果 <context> 中没有相关信息,回复:"当前知识库中未找到相关内容。"不要编造。 3. <context> 中信息之间有矛盾时,指出矛盾所在,不强制统一。 4. 使用引用格式标注信息来源:[来源: document_id]
### 引用格式 每个关键事实后标注来源:根据 PostgreSQL 16 文档,VACUUM 操作会回收死元组占用的空间 [来源: chunk_42]。
---
## User Message
<context> <document id="chunk_42" source="PostgreSQL 16 官方文档 - 例行维护" relevance="0.95"> PostgreSQL 使用多版本并发控制(MVCC)来管理并发访问。 当一行被更新或删除时,旧版本的行不会立即被物理删除——它们成为"死元组"。 VACUUM 命令负责清理这些死元组,回收存储空间,并更新统计信息以供查询规划器使用。 </document>
<document id="chunk_78" source="PostgreSQL 16 官方文档 - VACUUM 详解" relevance="0.91"> VACUUM 有两种模式:标准 VACUUM(不锁表,将空间释放给同一张表复用但不归还给操作系统) 和 VACUUM FULL(锁表,将空间归还给操作系统但会阻塞所有读写)。 对于大多数场景,推荐使用标准 VACUUM 配合 autovacuum 守护进程自动执行。 </document> </context>
用户提问:PostgreSQL 中的 VACUUM 是做什么的?它会锁表吗?
|
模型收到此 prompt 后:System prompt 约束回答规则 → <context> 明确标记可用知识范围 → 元信息支持精确引用 → 用户提问指向文档中的具体内容。
Citation(引用)设计
三种方案
方案一:内联引用
1 2 3 4
| VACUUM 操作会回收死元组占用的空间 [1],标准 VACUUM 不会锁表 [2]。
[1] PostgreSQL 16 官方文档 - 例行维护 [2] PostgreSQL 16 官方文档 - VACUUM 详解
|
适合问答场景,简洁直观。长回答中脚注可能与引用点距离较远。
方案二:段落级引用
1 2 3 4 5
| 根据官方文档,VACUUM 用于清理死元组并回收存储空间。 (来源:PostgreSQL 16 官方文档 - 例行维护)
标准 VACUUM 不会锁表,VACUUM FULL 会锁表。 (来源:PostgreSQL 16 官方文档 - VACUUM 详解)
|
适合每个段落引用一个来源的场景,结构清晰。
方案三:结构化引用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| { "answer": "VACUUM 用于清理死元组。标准 VACUUM 不锁表。", "citations": [ { "text": "VACUUM 用于清理死元组", "source": "chunk_42", "document": "PostgreSQL 16 官方文档 - 例行维护" }, { "text": "标准 VACUUM 不锁表", "source": "chunk_78", "document": "PostgreSQL 16 官方文档 - VACUUM 详解" } ] }
|
适合需要前端渲染引用高亮、点击跳转的场景,输出可编程验证。
用 Few-shot 教引用格式
引用格式是典型的”描述不如示例”场景:
1 2 3 4 5 6 7 8 9 10 11 12 13
| 示例: <context> <document id="doc_1" source="Python 3.12 发布说明"> Python 3.12 于 2023 年 10 月发布,主要新特性包括更快的解析器、 改进的错误消息和 f-string 增强。 </document> </context>
用户:Python 3.12 有哪些新特性?
回答:Python 3.12 引入了多项改进,包括更快的解析器(相比 3.11 提升约 5%)、 更友好的错误消息(精确定位错误位置)、以及 f-string 增强(支持嵌套表达式和 多行格式)[来源: doc_1, Python 3.12 发布说明]。
|
示例展示三个要点:引用标记位置(事实性陈述之后)、引用粒度(关键事实,不是每个句子)、引用格式([来源: id, 文档名])。
对话式 RAG
多轮对话的上下文管理
RAG 不只是一问一答。多轮对话中,每轮都要判断:
- 是否需要重新检索?
- 上一轮检索结果是否复用?
- 上下文窗口如何分配?
实用策略:
1 2 3 4 5
| ## 多轮对话规则
1. 用户新问题是上一轮的追问或细化 → 沿用之前检索结果,不重新检索 2. 用户切换话题 → 清空之前检索结果,重新检索 3. 每轮对话中,检索结果和对话历史的 token 配额比为 70:30
|
追问 vs 新话题的判断
这本身可以通过 Prompt 实现——用一个轻量分类 prompt:
1 2 3 4 5 6 7
| 判断以下用户输入是"追问"还是"新话题"。只输出一个词。
对话历史: 助手:PostgreSQL 的 VACUUM 用于清理死元组... 用户:那 VACUUM FULL 呢?
输出:追问
|
分类结果决定后续检索策略——追问不重新检索,直接沿用之前的检索上下文。
总结
RAG 的 prompt 设计,核心解决四个问题:
| 问题 |
解决方案 |
| 模型忽略检索结果,自行编造 |
System prompt 明确”知识来源”约束 |
| 检索结果为空时模型强行回答 |
预设 fallback 策略 |
| 多文档内容混淆 |
XML 标签 + 元信息 + relevance 排序 |
| 无法追踪信息来源 |
Citation 设计 + Few-shot 教学 |
核心原则:好的 RAG prompt 应让模型”不越界”——知道知识边界在哪,知道何时说”不知道”。