【AI应用开发】 RAG篇(四):Prompt 工程与进阶技术
前言
前几篇搞定了怎么存、怎么搜,本篇进入 RAG 的"最后一公里"------Prompt 工程和进阶优化技术。检索结果再好,Prompt 写得烂,答案是垃圾;检索质量卡在瓶颈,不上混合检索和查询优化,天花板就是 70 分。
本文从 Prompt 模板设计原则讲起,深入拆解混合检索(Hybrid Search)、Re-ranking 重排序、查询改写(Query Rewriting)、HyDE 假设性文档嵌入和多查询检索等进阶技术。读完你会掌握让 RAG 系统从"能用"到"好用"的全部关键技巧。
目录
- [Prompt 工程设计](#Prompt 工程设计)
- 1.1 Prompt 模板的五大要素
- 1.2 基础模板设计四原则
- 1.3 高级模板技巧
- 1.4 生产级 Prompt 模板实战
- 进阶检索技术
- 2.1 混合检索:向量 + 关键词互补
- 2.2 RRF 融合算法详解
- 2.3 Re-ranking 重排序
- 2.4 元数据过滤
- 查询优化技术
- 3.1 查询改写
- 3.2 HyDE:假设性文档嵌入
- 3.3 多查询检索
- 3.4 查询优化策略选择指南
1. Prompt 工程设计
好的 Prompt 模板是 RAG 系统的"最后一道关"。即使检索完美,Prompt 写得不好,答案质量也会大打折扣。
1.1 Prompt 模板的五大要素
一个完整的 RAG Prompt 应包含五个要素:

| 要素 | 说明 | 是否必须 | 示例 |
|---|---|---|---|
| 角色设定 | 定义 LLM 的身份和行为方式 | 推荐 | "你是一个专业的技术文档助手" |
| 上下文文档 | 检索到的 chunk 原文 | 必须 | 用分隔符包裹的文档内容 |
| 用户问题 | 用户的原始提问 | 必须 | "公司年假怎么申请?" |
| 行为指令 | 告诉 LLM 如何利用文档 | 必须 | "仅基于文档回答,不知道就说不知道" |
| 输出格式 | 指定返回格式 | 可选 | "请用 Markdown 格式回答" |
1.2 基础模板设计四原则
原则 1:用分隔符隔离上下文
使用 """、--- 或 XML 标签把文档内容框起来,防止 LLM 混淆指令和文档内容。这是防止 Prompt Injection 的第一道防线。
❌ 错误写法:
参考以下文档回答问题:{documents}。问题:{query}。请基于文档回答。
✅ 正确写法:
请基于以下参考文档回答问题。
<reference_documents>
{documents}
</reference_documents>
<user_question>
{query}
</user_question>
请严格基于以上 <reference_documents> 中的内容回答用户问题。
XML 标签优于普通分隔符的原因:现代 LLM 在训练中见过大量 XML/HTML 结构化数据,对标签边界有更好的认知。
原则 2:明确"不知道"策略
必须告诉 LLM:"如果文档中没有相关信息,请明确说不知道,不要编造。" 这是对抗幻觉的关键。
python
ANTI_HALLUCINATION_INSTRUCTION = """
如果参考文档中没有足够信息回答用户问题,请明确回复:
"抱歉,根据现有的文档资料,我无法回答这个问题。"
严禁编造或猜测文档中不存在的信息。
"""
一个常见的坑:只写"不知道就说不知道",但 LLM 可能因为"角色扮演惯性"仍然尝试回答。加上"严禁编造"会更有效。
原则 3:要求引用来源
让 LLM 在回答中标注信息来源,既提高可信度,也方便人工核验。
python
CITATION_INSTRUCTION = """
在回答中引用具体的参考文档,格式为 [来源X],其中 X 是文档编号。
如果多个文档提供相同信息,请列出所有相关来源。
"""
原则 4:限制只用给定文档
强调"仅基于提供的文档信息回答",阻止 LLM 使用训练时学到的、可能与文档冲突的先验知识。
请仅基于 <reference_documents> 中的内容回答。
不要使用你的训练数据中的知识,即使你认为文档内容有误,
也请以文档为准。如需指出文档中的矛盾,请明确说明。
1.3 高级模板技巧
① 思维链引导(Chain-of-Thought)
在 Prompt 中加入推理步骤引导,让 LLM 按步骤分析再给出答案:
请按以下步骤分析并回答:
步骤 1:逐一阅读每段参考文档,提取与用户问题相关的关键信息。
步骤 2:判断各段信息之间的关系(互补、重复、矛盾)。
步骤 3:综合所有信息,形成完整答案。
步骤 4:给出最终回答,并标注信息来源。
用户问题:{query}
参考文档:{documents}
为什么有效:显式列出推理步骤,LLM 会按顺序执行,避免跳过关键分析环节。
② 多文档对比
当涉及多个 chunk 时,要求 LLM 指出各文档之间的关联或矛盾:
如果多个参考文档提供了相同主题但不同细节的信息,
请指出它们之间的异同。如果存在矛盾,请明确指出冲突点。
③ Few-shot 示例
在 Prompt 中给出 1-2 个问答范例,帮助 LLM 理解期望的回答风格和格式:
示例问答:
问题:公司年假有多少天?
参考文档:根据公司员工手册第三章,员工每年享有 10 天带薪年假。
回答:
- 公司每年提供 10 天带薪年假 [来源1]。
- 具体请参考员工手册第三章。
- 如需了解年假申请流程,请进一步提问。
---
现在请以相同的风格回答以下问题:
问题:{query}
参考文档:{documents}
④ 结构化输出
要求 LLM 以 JSON 格式返回,方便程序解析:
请以 JSON 格式返回你的回答,格式如下:
{
"answer": "你的详细回答",
"sources": [1, 3],
"confidence": "high | medium | low",
"needs_clarification": false
}
1.4 生产级 Prompt 模板实战
综合以上所有原则,一个生产可用的完整 Prompt 模板:
python
RAG_PROMPT_TEMPLATE = """
你是一个专业的知识库助手。你的任务是基于参考文档回答用户问题。
## 行为准则
1. 仅基于参考文档内容回答,不要使用训练数据中的先验知识
2. 如果文档中没有足够信息,明确回复"抱歉,无法回答",严禁编造
3. 回答需标注信息来源,格式为 [来源X]
4. 如果多个文档提供相关信息,综合所有来源
5. 如果文档间存在矛盾,指出冲突点
## 参考文档
<reference_documents>
{documents}
</reference_documents>
## 用户问题
<user_question>
{query}
</user_question>
## 回答步骤
1. 逐一分析参考文档中的相关信息
2. 综合信息形成答案
3. 标注来源并给出最终回答
## 输出格式
请用清晰的中文 Markdown 格式回答。如需要可使用列表、表格等方式组织信息。
"""
2. 进阶检索技术
基础 RAG 用纯向量检索通常能解决 70% 的问题,剩下的 30% 需要用进阶技术来补齐。

2.1 混合检索:向量 + 关键词互补
纯向量检索的三个致命局限:
| 局限 | 说明 | 翻车案例 |
|---|---|---|
| 精确匹配不敏感 | 语义相近但字面不同 | 搜"AK-47"返回"突击步枪概述",却没返回标题带"AK-47"的文档 |
| 数字/日期难以区分 | 语义空间位置太近 | "2024年Q3财报"和"2023年Q3财报"语义几乎相同,但答案天差地别 |
| 专有名词丢失 | 产品代号、项目名区分度不足 | "Project Titan" 和 "Project Titan-2" 被当作语义近似 |
解决方案:向量检索(Dense)+ 关键词检索(Sparse)= 混合检索。
关键词检索:BM25
BM25 是 TF-IDF 的进化版,基于词频统计的经典检索算法:
- 精确匹配很强:搜 "AK-47" 一定能找到包含这个词的文档
- 数字和日期敏感:"2024" 和 "2023" 在 BM25 的世界里完全不同
- 零训练成本:不需要 GPU,不需要模型,开箱即用
- 天然适合代码/编号检索:工单号、API 版本号、协议编号
python
# BM25 检索示例(使用 rank_bm25 库)
from rank_bm25 import BM25Okapi
import jieba
# 中文分词后构建 BM25 索引
tokenized_docs = [list(jieba.cut(doc)) for doc in documents]
bm25 = BM25Okapi(tokenized_docs)
# 关键词检索
tokenized_query = list(jieba.cut("AK-47 生产批次"))
bm25_results = bm25.get_top_n(tokenized_query, documents, n=10)
2.2 RRF 融合算法详解
RRF(Reciprocal Rank Fusion)是目前最主流的混合检索融合算法。

核心公式:
RRF_score(d) = Σ 1 / (k + rank_i(d))
其中 rank_i(d) 是文档 d 在第 i 个检索列表中的排名,k 是平滑常数(通常取 60)。
直观理解:
向量检索排名 关键词检索排名 RRF 融合后排名
─────────── ────────────── ──────────────
#1 Doc_A #1 Doc_C #1 Doc_A (向量#1 + 关键词#3)
#2 Doc_B #2 Doc_A #2 Doc_C (向量#5 + 关键词#1)
#3 Doc_D #3 Doc_E #3 Doc_B (向量#2 + 关键词#8)
#4 Doc_F #4 Doc_B #4 Doc_D (向量#3 + 关键词#6)
#5 Doc_C #5 Doc_G #5 Doc_E (向量#7 + 关键词#3)
Doc_A 虽然关键词检索只排第 3,但因为向量检索排第 1,总分最高。Doc_C 刚好反过来。RRF 让两种检索方式的"共识"成为最终排序依据。
为什么 RRF 优于直接加权:
- 向量相似度分数(如 0.92)和 BM25 分数(如 15.3)量纲完全不同
- 简单线性加权
α × vec_score + β × bm25_score需要反复调 α、β - RRF 只依赖排名而非原始分数,天然解决量纲不一致问题
python
def reciprocal_rank_fusion(results_list, k=60):
"""
results_list: 多路检索结果列表,每路是一个按排名排序的文档ID列表
"""
scores = {}
for results in results_list:
for rank, doc_id in enumerate(results, start=1):
if doc_id not in scores:
scores[doc_id] = 0
scores[doc_id] += 1 / (k + rank)
# 按 RRF 分数降序排列
return sorted(scores.items(), key=lambda x: x[1], reverse=True)
2.3 Re-ranking 重排序

检索阶段追求"快"和"全"(高 Recall),用轻量级检索先捞一批候选。重排序阶段追求"准"(高 Precision),用更强的模型对候选重新打分。
两种方案对比:
| Bi-encoder(向量检索) | Cross-encoder(Re-rank) | |
|---|---|---|
| 工作方式 | 分别编码 query 和 doc,再算相似度 | query 和 doc 拼接后一起编码 |
| 交互 | 无交互,独立编码 | 有交互,attention 跨 query 和 doc |
| 速度 | 快(doc 向量可预计算) | 慢(每个 pair 各自编码) |
| 精度 | 相对低 | 显著更高 |
| 角色 | 粗筛(Recall) | 精排(Precision) |
落地流程:
向量检索 Top-20 → Cross-encoder 逐一打分 → 选 Top-3~5 → 送入 LLM
python
from FlagEmbedding import FlagReranker
reranker = FlagReranker('BAAI/bge-reranker-v2-m3', use_fp16=True)
# 对检索结果重排序
pairs = [(query, doc) for doc in retrieved_docs]
scores = reranker.compute_score(pairs)
# 按分数降序取 Top-3
reranked = sorted(
zip(retrieved_docs, scores),
key=lambda x: x[1],
reverse=True
)[:3]
常用 Re-ranker 选型:
| 模型 | 语言 | 部署方式 | 适合场景 |
|---|---|---|---|
| BAAI/bge-reranker-v2-m3 | 多语言 | 本地部署 | 中文场景首选 |
| Cohere Rerank v3 | 多语言 | API 调用 | 不想管运维 |
| ms-marco-MiniLM-L-6-v2 | 英文 | 本地部署 | 英文轻量级 |
2.4 元数据过滤
在检索前加过滤条件,缩小搜索范围,提升精度。这也是企业 RAG 系统的标配能力。
| 过滤维度 | 示例 | 使用场景 |
|---|---|---|
| 时间范围 | created_at >= "2024-01-01" |
只搜今年的政策文档 |
| 文档分类 | category = "产品手册" |
排除会议纪要、聊天记录 |
| 权限控制 | access_level <= user.level |
只搜用户有权看的文档 |
| 来源系统 | source = "confluence" |
跨系统时按来源筛选 |
实现关键:索引入库时把元数据和向量一起存储。 Milvus、Qdrant、Weaviate、pgvector 都有原生支持。
python
# Qdrant 元数据过滤示例
from qdrant_client.models import Filter, FieldCondition, MatchValue
search_result = client.search(
collection_name="knowledge_base",
query_vector=query_embedding,
query_filter=Filter(
must=[
FieldCondition(key="category", match=MatchValue(value="产品手册")),
FieldCondition(key="created_year", match=MatchValue(value=2024)),
]
),
limit=10
)
3. 查询优化技术
很多时候 RAG 效果不好,不是因为检索或生成有问题,而是用户问的问题本身就不够好。查询优化在检索前对用户问题进行预处理。

3.1 查询改写(Query Rewriting)
问题 1:指代不明
用户:"它的价格是多少?"(多轮对话中,"它"指什么?)
↓ 查询改写(结合对话历史)
改写:"iPhone 15 Pro 256GB 的价格是多少?"
问题 2:缩写/行话
用户:"RAG 的 embedding 怎么选?"
↓ 查询改写(展开术语)
改写:"检索增强生成(RAG)系统中,文本嵌入模型(Embedding Model)应该如何选择?"
问题 3:口语化表达
用户:"这玩意儿咋老搜不准呢?"
↓ 查询改写(规范化)
改写:"RAG 系统的检索准确率低,如何排查和优化?"
实现方式:用 LLM 在检索前做一次查询改写,将对话历史 + 当前问题 → 独立、规范的检索查询。
python
QUERY_REWRITE_PROMPT = """
你是一个查询优化助手。请将用户的多轮对话问题改写为独立、规范、信息完整的检索查询。
## 对话历史
{history}
## 用户当前问题
{query}
## 改写规则
1. 补全代词和指代(如"它"、"这个"等)
2. 展开缩写和专业术语
3. 规范化口语表达
4. 保持原意,不要添加用户没提到的信息
## 输出
只输出改写后的查询文本,不要加任何解释。
"""
# 调用 LLM 改写查询
rewritten_query = llm.invoke(QUERY_REWRITE_PROMPT.format(
history=chat_history,
query=user_query
))
3.2 HyDE:假设性文档嵌入
HyDE(Hypothetical Document Embeddings)是一个极其巧妙的思路。

核心洞察:问题和答案的措辞风格差异很大。
用户问题:"怎么申请年假?" → 短、口语化、疑问句
目标文档:"年假申请流程:第一步..." → 长、书面化、陈述句
用用户问题的向量去检索目标文档,天然存在"语言风格不匹配"的问题。
HyDE 解法:
传统流程:用户问题 → Embedding(问题) → 检索文档
HyDE流程:用户问题 → LLM 生成"假设答案" → Embedding(假设答案) → 检索文档
让 LLM 先"脑补"一篇假答案!假答案是陈述句,和真实文档的措辞风格更接近,用它做的检索向量命中率更高。
python
HYDE_PROMPT = """
请根据以下问题,写一段假设性的回答文档(约 200-300 字)。
不需要事实准确,只需要在语言风格上像一篇真实的参考文档。
问题:{query}
假设性文档:
"""
# Step 1: 生成假设文档
hypothetical_doc = llm.invoke(HYDE_PROMPT.format(query=user_query))
# Step 2: 用假设文档的向量去检索
hyde_embedding = embedding_model.encode(hypothetical_doc)
retrieved_docs = vector_db.search(hyde_embedding, top_k=10)
适用 / 不适用场景:
| 适合 HyDE | 不适合 HyDE |
|---|---|
| 问题和文档措辞风格差异大 | 纯事实性问答("2024 年 GDP?") |
| 查询简短但目标文档详细 | 低延迟要求(多一次 LLM 调用) |
| 需要生成式回答的场景 | 答案明确唯一、不需要生成 |
3.3 多查询检索(Multi-Query Retrieval)
一个问题换几种问法,各自检索,合并结果------覆盖更广。
原问题:"如何申请年假?"
↓ 生成变体
变体 1:"年假申请的完整流程是什么?"
变体 2:"请年假需要什么材料和步骤?"
变体 3:"员工休假政策中的年假规定有哪些?"
↓ 分别检索 → 合并去重 → RRF 融合排序
最终结果:覆盖面更广,漏检率更低
python
MULTI_QUERY_PROMPT = """
请为以下用户问题生成 {n} 个不同表述的检索查询变体。
每个变体应聚焦原问题的不同方面或用不同措辞表达。
原问题:{query}
请直接列出 {n} 个变体,每行一个,不要编号。
"""
# 生成变体
variants = llm.invoke(MULTI_QUERY_PROMPT.format(query=query, n=4))
# 并行检索 + RRF 融合
all_results = []
for q in [query] + variants:
results = vector_db.search(embed(q), top_k=10)
all_results.append([r.id for r in results])
final = reciprocal_rank_fusion(all_results)
3.4 查询优化策略选择指南
| 场景 | 推荐策略 | 原因 |
|---|---|---|
| 多轮对话 | 查询改写 | 补全指代,独立化查询 |
| 用户用缩写/行话多 | 查询改写 | 展开术语,提高检索匹配度 |
| 问答风格差异大 | HyDE | 用假设答案的措辞风格做检索桥梁 |
| 用户问题很简短 | 多查询检索 | 多方面覆盖,降低漏检 |
| 高精度要求 | 多查询 + RRF | 多重覆盖 + 融合排序 |
| 低延迟要求 | 查询改写(轻量版) | 一次 LLM 调用,开销最小 |
进阶组合:查询改写 → HyDE → 多查询检索 → RRF 融合。效果最好但延迟最高,按场景取舍。
本篇小结
| 知识点 | 一句话总结 |
|---|---|
| Prompt 五要素 | 角色 + 文档 + 问题 + 指令 + 格式,缺一不可 |
| 分隔符隔离 | 用 XML 标签包裹文档,防 Prompt Injection |
| 反幻觉指令 | "不知道就说不知道"+"严禁编造",双重保险 |
| 思维链引导 | 显式列出推理步骤,LLM 按步骤执行更靠谱 |
| 混合检索 | 向量管语义、BM25 管精确,两者互补 |
| RRF 融合 | 不看分数看排名,天然解决量纲不一致 |
| Re-rank | 粗筛快 + 精排准,Cross-encoder 比 Bi-encoder 精度高一个量级 |
| 查询改写 | 补全代词、展开缩写、规范化口语,检索前提质 |
| HyDE | 用 LLM 先脑补一篇假答案,用它做检索向量 |
| 多查询检索 | 一个问题多种问法,各自检索合并,覆盖面更广 |
本文覆盖了将 RAG 从 70 分提升到 90 分的全部关键技巧。下一篇作为系列终章,将聚焦 RAG 系统的效果评估、生产化部署、失败模式对策以及前沿技术方向。