[Prompt Engineering] RAG 中的提示词设计

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 标签的三个优势:

  1. LLM 训练语料中有大量 XML/HTML,能天然理解标签的结构语义
  2. 标签提供明确的注意力锚点——<context> 告诉模型”下面的内容需要重点关注”
  3. 属性可携带元信息——idsourcerelevance 等让模型知道每条内容的来源和可信度

元信息标注

每个文档 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 层也需要配合:

  1. 按 relevance 降序排列:最重要文档放最前面(LLM 对 prompt 开头和末尾的注意力最高)
  2. 末尾显式告知截断
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. 上下文窗口如何分配?

实用策略:

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 应让模型”不越界”——知道知识边界在哪,知道何时说”不知道”