RAG 技术与应用实战详解:从原理到 DeepSeek + Faiss 本地知识库搭建

RAG 技术与应用实战详解:从原理到 DeepSeek + Faiss 本地知识库搭建

本文系统梳理 RAG(检索增强生成)的核心原理与完整落地流程,包含 Embedding 模型选型、DeepSeek + Faiss 搭建本地知识库的完整代码实战,以及 Query 改写、Query + 联网搜索两大进阶技巧。适合想动手搭建自己知识库问答系统的同学。

目录

  • 一、大模型应用开发的三种模式:提示工程 VS RAG VS 微调
  • 二、什么是 RAG
  • 三、RAG 的核心原理与流程
  • 四、NativeRAG:索引、检索、生成三步走
  • 五、Embedding 模型怎么选
  • 六、实战:DeepSeek + Faiss 搭建本地知识库检索
  • 七、进阶一:Query 改写
  • 八、进阶二:Query + 联网搜索
  • 九、思考:如果 LLM 可以处理无限上下文,RAG 还有意义吗?
  • 十、总结

一、大模型应用开发的三种模式:提示工程 VS RAG VS 微调

在动手之前,先要回答一个问题:遇到大模型"答不好"的情况,到底该用哪种手段?

问题类型 表现 解决方案
没问清楚 具备各种能力的 AI,因为提问方式不对而给出错误回复 提示工程
缺乏背景知识 海量知识预训练的 LLM,不知道你业务里的私有数据 RAG
能力不足 模型本身不具备完成某类任务的能力 微调

简单记忆:问法不对用提示工程,知识不够用 RAG,能力不够用微调。


二、什么是 RAG

RAG(Retrieval-Augmented Generation,检索增强生成) 是一种结合信息检索(Retrieval)和文本生成(Generation)的技术。

RAG 通过实时检索相关文档或信息,并将其作为上下文输入到生成模型中,从而提高生成结果的时效性和准确性。

RAG 的优势

  • 解决知识时效性问题:大模型的训练数据通常是静态的,无法涵盖最新信息,而 RAG 可以检索外部知识库实时更新信息。
  • 减少模型幻觉:通过引入外部知识,RAG 能够减少模型生成虚假或不准确内容的可能性。
  • 提升专业领域回答质量:RAG 能够结合垂直领域的专业知识库,生成更具专业深度的回答。

三、RAG 的核心原理与流程

RAG 的完整流程分为三大步:数据预处理 → 检索 → 生成。

Step 1:数据预处理

  • 知识库构建:收集并整理文档、网页、数据库等多源数据,构建外部知识库。
  • 文档分块:将文档切分为适当大小的片段(chunks),以便后续检索。分块策略需要在语义完整性与检索效率之间取得平衡。
  • 向量化处理:使用嵌入模型(如 BGE、M3E、Chinese-Alpaca-2 等)将文本块转换为向量,并存储在向量数据库中。

Step 2:检索阶段

  • 查询处理:将用户输入的问题转换为向量,并在向量数据库中进行相似度检索,找到最相关的文本片段。
  • 重排序:对检索结果进行相关性排序,选择最相关的片段作为生成阶段的输入。

Step 3:生成阶段

  • 上下文组装:将检索到的文本片段与用户问题结合,形成增强的上下文输入。
  • 生成回答:大语言模型基于增强的上下文生成最终回答。

四、NativeRAG:索引、检索、生成三步走

最基础的 RAG(NativeRAG)可以概括为三个环节:

  • Indexing(索引) → 如何更好地把知识存起来。
  • Retrieval(检索) → 如何在大量的知识中,找到一小部分有用的,给到模型参考。
  • Generation(生成) → 如何结合用户的提问和检索到的知识,让模型生成有用的答案。

这三个步骤看似简单,但在 RAG 应用从构建到落地实施的整个过程中,涉及较多复杂的工作内容——分块策略、Embedding 选型、检索质量、Query 理解,每一环都值得深挖。


五、Embedding 模型怎么选

Embedding(嵌入)模型决定了"知识如何被表示",直接影响检索质量。选型时可以参考 MTEB 榜单,它比较了 1000 多种语言中的 100 多种文本嵌入模型。

1. 通用文本嵌入模型

模型 特点 适用场景
BGE-M3(智源研究院) 支持 100+ 语言,输入长度达 8192 tokens,融合密集、稀疏、多向量混合检索 跨语言长文档检索、高精度 RAG 应用
text-embedding-3-large(OpenAI) 向量维度 3072,长文本语义捕捉能力强,英文表现优秀 英文内容优先的全球化应用
Jina-embeddings-v2-small(Jina AI) 参数量仅 35M,支持实时推理(RT<50ms),适合轻量化部署 轻量级文本处理、实时推理任务

2. 中文嵌入模型

模型 特点 适用场景
xiaobu-embedding-v2 针对中文语义优化,语义理解能力强 中文文本分类、语义检索
M3E-Base 针对中文优化的轻量模型(0.4G),适合本地私有化部署 中文法律、医疗领域检索任务
stella-mrl-large-zh-v3.5-1792 处理大规模中文数据能力强,捕捉细微语义关系 中文文本高级语义分析、NLP 任务

3. 指令驱动与复杂任务模型

  • gte-Qwen2-7B-instruct(阿里巴巴):基于 Qwen 大模型微调,支持代码与文本跨模态检索。适用于复杂指令驱动任务、智能问答系统。
  • E5-mistral-7B(Microsoft):基于 Mistral 架构,Zero-shot 任务表现优异。适用于动态调整语义密度的复杂系统。

4. 企业级与复杂系统

  • BGE-M3:适合企业级部署,支持混合检索,适用于企业级语义检索、复杂 RAG 应用(文件大小 2.3G)。
  • E5-mistral-7B:适合企业级部署,支持指令微调,适用于需要动态调整语义密度的复杂系统。

实战 1:bge-m3 的使用

from FlagEmbedding import BGEM3FlagModel

model = BGEM3FlagModel('BAAI/bge-m3', use_fp16=True)
# 设置 use_fp16=True 可以加快计算速度,性能仅有轻微下降

sentences_1 = ["What is BGE M3?", "Defination of BM25"]
sentences_2 = [
    "BGE M3 is an embedding model supporting dense retrieval, lexical matching and multi-vector interaction.",
    "BM25 is a bag-of-words retrieval function that ranks a set of documents based on the query terms appearing in each document"
]

embeddings_1 = model.encode(sentences_1,
                            batch_size=12,
                            max_length=8192,  # 如果不需要这么长的长度,可以设置更小的值来加速编码过程
                            )['dense_vecs']
embeddings_2 = model.encode(sentences_2)['dense_vecs']

similarity = embeddings_1 @ embeddings_2.T
print(similarity)

输出结果:

[[0.626  0.3477]
 [0.3499 0.678 ]]

代码解读:

  • embeddings_1 包含第一组句子的嵌入向量,形状为 [sentences_1的数量, 嵌入维度];
  • embeddings_2 包含第二组句子的嵌入向量,形状为 [sentences_2的数量, 嵌入维度];
  • embeddings_2.T 是转置操作,形状变为 [嵌入维度, sentences_2的数量];
  • @ 符号在 Python 中表示矩阵乘法运算。

通过矩阵乘法计算出两组句子之间的余弦相似度矩阵,结果 similarity 的形状为 [sentences_1的数量, sentences_2的数量]。

可以看出:

  • "What is BGE M3?" 与 "BGE M3 is an embedding model..." 的相似度为 0.626(较高,语义相关 ✅)
  • "What is BGE M3?" 与 "BM25 is a bag-of-words retrieval function..." 的相似度为 0.3477(较低)
  • "Defination of BM25" 与 "BM25 is a bag-of-words retrieval function..." 的相似度为 0.678(较高 ✅)

语义相关的句子相似度明显更高,这正是向量检索的工作原理。

实战 2:gte-qwen2 的使用

方式一:使用 sentence-transformers

from sentence_transformers import SentenceTransformer

model_dir = "/root/autodl-tmp/models/iic/gte_Qwen2-1___5B-instruct"
model = SentenceTransformer(model_dir, trust_remote_code=True)
# 如需减小最大长度,可以调整:
model.max_seq_length = 8192

queries = [
    "how much protein should a female eat",
    "summit define",
]
documents = [
    "As a general guideline, the CDC's average requirement of protein for women ages 19 to 70 is 46 grams per day. But, as you can see from this chart, you'll need to increase that if you're expecting or training for a marathon. Check out the chart below to see how much protein you should be eating each day.",
    "Definition of summit for English Language Learners. : 1 the highest point of a mountain : the top of a mountain. : 2 the highest level. : 3 a meeting or series of meetings between the leaders of two or more governments.",
]

query_embeddings = model.encode(queries, prompt_name="query")
document_embeddings = model.encode(documents)

scores = (query_embeddings @ document_embeddings.T) * 100
print(scores.tolist())

方式二:使用 transformers 原生 API(指令格式)

import torch
import torch.nn.functional as F
from torch import Tensor
from modelscope import AutoTokenizer, AutoModel

# 定义最后一个 token 池化函数
# 该函数从最后的隐藏状态中提取每个序列的最后一个有效 token 的表示
def last_token_pool(last_hidden_states: Tensor,
                    attention_mask: Tensor) -> Tensor:
    left_padding = (attention_mask[:, -1].sum() == attention_mask.shape[0])
    if left_padding:
        return last_hidden_states[:, -1]
    else:
        sequence_lengths = attention_mask.sum(dim=1) - 1
        batch_size = last_hidden_states.shape[0]
        return last_hidden_states[torch.arange(batch_size, device=last_hidden_states.device), sequence_lengths]

# 将任务描述和查询组合成特定格式的指令
def get_detailed_instruct(task_description: str, query: str) -> str:
    return f'Instruct: {task_description}\nQuery: {query}'

# 每个查询都必须附带一句描述任务的指令
task = 'Given a web search query, retrieve relevant passages that answer the query'

queries = [
    get_detailed_instruct(task, 'how much protein should a female eat'),  # 女性应该摄入多少蛋白质
    get_detailed_instruct(task, 'summit define')  # summit(顶峰)的定义
]
# 检索文档(文档侧无需添加指令)
documents = [
    "As a general guideline, the CDC's average requirement of protein for women ages 19 to 70 is 46 grams per day. But, as you can see from this chart, you'll need to increase that if you're expecting or training for a marathon. Check out the chart below to see how much protein you should be eating each day.",
    "Definition of summit for English Language Learners. : 1 the highest point of a mountain : the top of a mountain. : 2 the highest level. : 3 a meeting or series of meetings between the leaders of two or more governments.",
]

# 将查询和文档合并为一个输入文本列表
input_texts = queries + documents

# 设置模型路径
model_dir = "/root/autodl-tmp/models/iic/gte_Qwen2-1___5B-instruct"
# 加载分词器,trust_remote_code=True 允许使用远程代码
tokenizer = AutoTokenizer.from_pretrained(model_dir, trust_remote_code=True)
# 加载模型
model = AutoModel.from_pretrained(model_dir, trust_remote_code=True)

max_length = 8192
# 对输入文本进行分词
batch_dict = tokenizer(input_texts, max_length=max_length, padding=True,
                       truncation=True, return_tensors='pt')
outputs = model(**batch_dict)
# 使用 last_token_pool 函数从最后的隐藏状态中提取每个序列的表示
embeddings = last_token_pool(outputs.last_hidden_state, batch_dict['attention_mask'])

# 归一化向量并计算相似度得分
embeddings = F.normalize(embeddings, p=2, dim=1)
scores = (embeddings[:2] @ embeddings[2:].T) * 100
print(scores.tolist())

输出:

[[78.49689483642578, 17.042858123779297],
 [14.924483299255371, 75.37962341308594]]

两个查询都正确地与各自相关的文档获得了高分(78.5 和 75.4)。

gte-Qwen2-instruct 小结:

  • 定位:基于 Qwen2 的指令优化型嵌入模型。
  • 指令优化:经过大量指令-响应对的训练,特别擅长理解和生成高质量的文本。
  • 性能表现:在文本生成、问答系统、文本分类、情感分析、命名实体识别和语义匹配等任务中表现优异。
  • 适合场景:复杂问答系统、多步推理问题。
  • 优势:指令理解和执行能力强;多语言支持;文本生成和语义理解任务表现优异。
  • 局限:计算资源需求较高,适合资源充足的环境。

六、实战:DeepSeek + Faiss 搭建本地知识库检索

这是本文的核心实战环节:用一份《银行客户经理考核办法》PDF 作为知识库,搭建一个可以回答业务问题并溯源页码的问答系统。

先看一下最终效果:

问:客户经理被投诉了,投诉一次扣多少分?

答:根据文件内容,客户经理被投诉一次扣 2 分。具体规定如下:(二)服务质量考核:2. 客户服务效率低,态度生硬或不及时为客户提供维护服务,有客户投诉的,每投诉一次扣 2 分。

问:客户经理每年评聘申报时间是怎样的?

答:根据文件内容,客户经理每年评聘申报时间如下:第十一条 每年一月份为客户经理评聘的申报时间,由分行人力资源部、个人业务部每年二月份组织统一的资格考试。考试合格者由分行颁发个金客户经理资格证书,其有效期为一年。因此,客户经理每年评聘的申报时间是一月份。

1. 架构与技术选型

RAG 架构:

  • 检索(Retrieval):使用向量相似度搜索从 PDF 文档中检索相关内容
  • 增强(Augmentation):将检索到的文档片段作为上下文
  • 生成(Generation):基于上下文和用户问题生成答案

技术栈选择:

组件 选型
向量数据库 Faiss(高效的向量检索)
嵌入模型 阿里云 DashScope 的 text-embedding-v1
大语言模型 deepseek-v3
文档处理 PyPDF2(PDF 文本提取)

2. 程序的逻辑结构

Step 1:文档预处理 —— PDF文件 → 文本提取 → 文本分割 → 页码映射

  1. PDF 文本提取:逐页提取文本内容;记录每行文本对应的页码信息;处理空页和异常情况。
  2. 文本分割策略:使用递归字符分割器;分割参数 chunk_size=1000, chunk_overlap=200;分割符优先级:段落 → 句子 → 空格 → 字符。
  3. 页码映射处理:基于字符位置计算每个文本块的页码;使用众数统计确定文本块的主要来源页码;建立文本块与页码的映射关系。

Step 2:知识库构建 —— 文本块 → 嵌入向量 → Faiss索引 → 本地持久化

  1. 向量数据库构建:使用 DashScope 嵌入模型生成向量;将向量存储到 Faiss 索引结构。
  2. 数据持久化:保存 Faiss 索引文件(.faiss);保存元数据信息(.pkl);保存页码映射关系(page_info.pkl)。

Step 3:问答查询 —— 用户问题 → 向量检索 → 文档组合 → LLM生成 → 答案输出

  1. 相似度检索:将用户问题转换为向量;在 Faiss 中搜索最相似的文档块,返回 Top-K 相关文档。
  2. 问答链处理:使用 LangChain 的 load_qa_chain;采用 stuff 策略组合文档;将组合后的上下文和问题发送给 LLM。
  3. 答案生成与展示:输出答案并展示来源页码。

3. 代码实现

(1)PDF 文本提取与页码记录

from PyPDF2 import PdfReader
from langchain.chains.question_answering import load_qa_chain
from langchain_community.callbacks.manager import get_openai_callback
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_community.vectorstores import FAISS
from typing import List, Tuple

def extract_text_with_page_numbers(pdf) -> Tuple[str, List[int]]:
    """
    从PDF中提取文本并记录每行文本对应的页码

    参数:
        pdf: PDF文件对象

    返回:
        text: 提取的文本内容
        page_numbers: 每行文本对应的页码列表
    """
    text = ""
    page_numbers = []

    for page_number, page in enumerate(pdf.pages, start=1):
        extracted_text = page.extract_text()
        if extracted_text:
            text += extracted_text
            page_numbers.extend([page_number] * len(extracted_text.split("\n")))
        else:
            Logger.warning(f"No text found on page {page_number}.")

    return text, page_numbers

(2)文本分割与向量存储构建

def process_text_with_splitter(text: str, page_numbers: List[int]) -> FAISS:
    """
    处理文本并创建向量存储

    参数:
        text: 提取的文本内容
        page_numbers: 每行文本对应的页码列表

    返回:
        knowledgeBase: 基于FAISS的向量存储对象
    """
    # 创建文本分割器,用于将长文本分割成小块
    text_splitter = RecursiveCharacterTextSplitter(
        separators=["\n\n", "\n", ".", " ", ""],
        chunk_size=1000,
        chunk_overlap=200,
        length_function=len,
    )

    # 分割文本
    chunks = text_splitter.split_text(text)
    print(f"文本被分割成 {len(chunks)} 个块。")

    # 创建嵌入模型
    embeddings = DashScopeEmbeddings(
        model="text-embedding-v1",
        dashscope_api_key=DASHSCOPE_API_KEY,
    )

    # 从文本块创建知识库
    knowledgeBase = FAISS.from_texts(chunks, embeddings)
    print("已从文本块创建知识库。")

    # 存储每个文本块对应的页码信息
    knowledgeBase.page_info = {chunk: page_numbers[i] for i, chunk in enumerate(chunks)}

    return knowledgeBase

(3)读取 PDF 并构建知识库

# 读取PDF文件
pdf_reader = PdfReader('./浦发上海浦东发展银行西安分行个金客户经理考核办法.pdf')

# 提取文本和页码信息
text, page_numbers = extract_text_with_page_numbers(pdf_reader)
print(f"提取的文本长度: {len(text)} 个字符。")

# 处理文本并创建知识库
knowledgeBase = process_text_with_splitter(text, page_numbers)

运行输出:

提取的文本长度: 3881 个字符。
文本被分割成 5 个块。
已从文本块创建知识库。

(4)问答查询与来源页码展示

from langchain_community.llms import Tongyi

llm = Tongyi(model_name="deepseek-v3", dashscope_api_key=DASHSCOPE_API_KEY)

# 设置查询问题
query = "客户经理被投诉了,投诉一次扣多少分"
# query = "客户经理每年评聘申报时间是怎样的?"

if query:
    # 执行相似度搜索,找到与查询相关的文档
    docs = knowledgeBase.similarity_search(query)

    # 加载问答链
    chain = load_qa_chain(llm, chain_type="stuff")

    # 准备输入数据
    input_data = {"input_documents": docs, "question": query}

    # 使用回调函数跟踪API调用成本
    with get_openai_callback() as cost:
        # 执行问答链
        response = chain.invoke(input=input_data)
        print(f"查询已处理。成本: {cost}")

    print(response["output_text"])

    # 显示每个文档块的来源页码
    print("来源:")
    unique_pages = set()
    for doc in docs:
        text_content = getattr(doc, "page_content", "")
        source_page = knowledgeBase.page_info.get(text_content.strip(), "未知")
        if source_page not in unique_pages:
            unique_pages.add(source_page)
            print(f"文本块页码: {source_page}")

4. 本案例小结

  1. PDF 文本提取与处理:使用 PyPDF2 的 PdfReader 提取文本;提取过程中记录每行文本对应的页码,便于后续溯源;使用 RecursiveCharacterTextSplitter 将长文本分割成小块,便于向量化处理。
  2. 向量数据库构建:使用 DashScopeEmbeddings 将文本块转换为向量表示;使用 FAISS 向量数据库存储文本向量,支持高效的相似度搜索;为每个文本块保存对应的页码信息,实现查询结果溯源。
  3. 语义搜索与问答链:基于用户查询,使用 similarity_search 在向量数据库中检索相关文本块;使用语言模型和 load_qa_chain 构建问答链;将检索到的文档和用户问题作为输入,生成回答。
  4. 成本跟踪与结果展示:如果使用的是 OpenAI 模型,可以用 get_openai_callback 跟踪 API 调用成本;展示问答结果和来源页码,方便用户验证信息。

5. LangChain 中的 4 种问答链(chain_type)

chain_type 工作方式 适用场景
stuff 直接把文档作为 prompt 输入给 LLM 文档拆分得比较小、一次获取文档比较少的情况。调用 LLM 的次数也比较少,能用 stuff 就用 stuff
map_reduce 对于每个 chunk 做一个 prompt(回答或摘要),然后再做合并 将每个 document 单独处理,可以并发调用;但每个文档之间缺少上下文
refine 在第一个 chunk 上做 prompt 得到结果,然后合并下一个文档再输出结果 能部分保留上下文,token 的使用能控制在一定范围
map_rerank 对每个 chunk 做 prompt,然后打个分,根据分数返回最好的文档中的结果 会大量调用 LLM,每个 document 之间独立处理

踩坑提示:如果发现检索出的文本块页码不对,需要回头检查页码的计算逻辑(文本按行记录页码、分割后块与页码的映射方式),可以借助 Cursor 等 AI 编程工具辅助排查完善。


七、进阶一:Query 改写

为什么需要 Query 改写?

RAG 的核心在于"检索-生成"。如果第一步"检索"就走偏了,那么后续的"生成"质量也会降低。

问题在于:

  • 用户提出的问题往往是口语化的、承接上下文的、模糊的,甚至包含情绪;
  • 而知识库里的文本(切片/Chunks)通常是陈述性的、客观的。

因此需要一个"翻译官"的角色,将用户的"口语化查询"转换成"书面化、精确的检索语句"。具体做法就是:通过精心设计的 Prompt 来引导 LLM 完成这项任务。

类型 1:上下文依赖型改写

"""上下文依赖型Query改写"""
instruction = """
你是一个智能的查询优化助手。请分析用户的当前问题以及前序对话历史,判断当前问题是否依赖于上下文。
如果依赖,请将当前问题改写成一个独立的、包含所有必要上下文信息的完整问题。
如果不依赖,直接返回原问题。
"""

prompt = f"""
### 指令 ###
{instruction}

### 对话历史 ###
{conversation_history}

### 当前问题 ###
{current_query}

### 改写后的问题 ###
"""

示例:

对话历史:
用户: "我想了解一下上海迪士尼乐园的最新项目。"
AI: "上海迪士尼乐园最新推出了'疯狂动物城'主题园区,这里有朱迪警官和尼克狐的互动体验。"
用户: "这个园区有什么游乐设施?"
AI: "'疯狂动物城'园区目前有疯狂动物城警察局、朱迪警官训练营和尼克狐的冰淇淋店等设施。"

当前查询: 还有其他设施吗?

改写结果: 除了疯狂动物城警察局、朱迪警官训练营和尼克狐的冰淇淋店之外,'疯狂动物城'园区还有其他设施吗?

类型 2:对比型改写

"""对比型Query改写"""
instruction = """
你是一个查询分析专家。请分析用户的输入和相关的对话上下文,识别出问题中需要进行比较的多个对象。
然后,将原始问题改写成一个更明确、更适合在知识库中检索的对比性查询。
"""

prompt = f"""
### 指令 ###
{instruction}

### 对话历史/上下文信息 ###
{context_info}

### 原始问题 ###
{query}

### 改写后的查询 ###
"""

示例:

对话历史:
用户: "我想了解一下上海迪士尼乐园的最新项目。"
AI: "上海迪士尼乐园最新推出了疯狂动物城主题园区,还有蜘蛛侠主题园区"

当前查询: 哪个游玩的时间比较长,比较有趣

改写结果: 哪个游玩时间更长、更有趣:上海迪士尼乐园的疯狂动物城主题园区和蜘蛛侠主题园区?

类型 3:模糊指代型改写

"""模糊指代型Query改写"""
instruction = """
你是一个消除语言歧义的专家。请分析用户的当前问题和对话历史,找出问题中 "都"、"它"、"这个" 等模糊指代词具体指向的对象。
然后,将这些指代词替换为明确的对象名称,生成一个清晰、无歧义的新问题。
"""

prompt = f"""
### 指令 ###
{instruction}

### 对话历史 ###
{conversation_history}

### 当前问题 ###
{current_query}

### 改写后的问题 ###
"""

示例:

对话历史:
用户: "我想了解一下上海迪士尼乐园和香港迪士尼乐园的烟花表演。"
AI: "好的,上海迪士尼乐园和香港迪士尼乐园都有精彩的烟花表演。"

当前查询: 都什么时候开始?

改写结果: 上海迪士尼乐园和香港迪士尼乐园的烟花表演都什么时候开始?

类型 4:多意图型改写(分解查询)

"""多意图型Query改写 - 分解查询"""
instruction = """
你是一个任务分解机器人。请将用户的复杂问题分解成多个独立的、可以单独回答的简单问题。以JSON数组格式输出。
"""

prompt = f"""
### 指令 ###
{instruction}

### 原始问题 ###
{query}

### 分解后的问题列表 ###
请以JSON数组格式输出,例如:["问题1", "问题2", "问题3"]
"""

示例:

原始查询: 门票多少钱?需要提前预约吗?停车费怎么收?

分解结果: ['门票多少钱?', '需要提前预约吗?', '停车费怎么收?']

类型 5:反问型改写

"""反问型Query改写"""
instruction = """
你是一个沟通理解大师。请分析用户的反问或带有情绪的陈述,识别其背后真实的意图和问题。
然后,将这个反问改写成一个中立、客观、可以直接用于知识库检索的问题。
"""

prompt = f"""
### 指令 ###
{instruction}

### 对话历史 ###
{conversation_history}

### 当前问题 ###
{current_query}

### 改写后的问题 ###
"""

示例:

对话历史:
用户: "你好,我想预订下周六上海迪士尼乐园的门票。"
AI: "正在为您查询... 查询到下周六的门票已经售罄。"
用户: "售罄是什么意思?我朋友上周去还能买到当天的票。"

当前查询: 这不会也要提前一个月预订吧?

改写结果: 迪士尼乐园门票是否需要提前一个月预订?

进阶:自动识别 Query 类型并改写

把上面五种类型合并成一个"意图识别 + 自动改写"的 Prompt:

"""自动识别Query类型并进行改写"""
instruction = """
你是一个智能的查询分析专家。请分析用户的查询,识别其属于以下哪种类型:

1. 上下文依赖型 - 包含"还有"、"其他"等需要上下文理解的词汇
2. 对比型 - 包含"哪个"、"比较"、"更"、"哪个更好"、"哪个更"等比较词汇
3. 模糊指代型 - 包含"它"、"他们"、"都"、"这个"等指代词
4. 多意图型 - 包含多个独立问题,用"、"或"?"分隔
5. 反问型 - 包含"不会"、"难道"等反问语气

说明:如果同时存在多意图型、模糊指代型,优先级为多意图型 > 模糊指代型

请返回JSON格式:
{
    "query_type": "查询类型",
    "rewritten_query": "改写后的查询",
    "confidence": "置信度(0-1)"
}
"""

prompt = f"""
### 指令 ###
{instruction}

### 对话历史 ###
{conversation_history}

### 上下文信息 ###
{context_info}

### 原始查询 ###
{query}

### 分析结果 ###
"""

实际运行效果:

查询 识别类型 改写结果 置信度
还有其他游乐项目吗? 上下文依赖型 除了之前提到的游乐项目之外,还有哪些其他游乐项目? 0.95
哪个园区更好玩? 模糊指代型 哪个园区更好玩?(请明确是哪些园区) 0.95
都适合小朋友吗? 模糊指代型 哪些产品或活动适合小朋友? 0.95
有什么餐厅?价格怎么样? 多意图型 有哪些餐厅?这些餐厅的价格怎么样? 0.95
这不会也要排队两小时吧? 反问型 这需要排队两小时吗? 0.95

八、进阶二:Query + 联网搜索

本地知识库解决不了时效性问题。以"迪士尼 RAG 助手"为例,以下情况用户 Query 需要联网:

类型 关键词特征 示例查询 原因说明
时效性 最新、今天、现在、实时、当前 上海迪士尼乐园今天开放吗? 需获取当前时间的最新信息
价格信息 多少钱、价格、费用、票价 下周六的门票多少钱? 价格信息经常变动,需实时查询
营业信息 营业时间、开放时间、闭园时间、是否开放 迪士尼乐园现在开门吗? 营业状态可能因特殊情况调整
活动信息 活动、表演、演出、节日、庆典 最近有什么特别活动? 活动信息具有时效性和动态性
天气信息 天气、下雨、温度 明天去迪士尼天气怎么样? 天气信息需要实时获取
交通信息 怎么去、交通、地铁、公交 从浦东机场怎么去迪士尼? 交通信息可能因施工、活动等变化
预订信息 预订、预约、购票、订票 需要提前多久预订? 预订政策可能随时调整
实时状态 排队、拥挤、人流量 现在人多不多? 实时状态需即时获取

整个功能拆成三步:识别 → 改写 → 生成搜索策略。

核心功能 1:识别查询是否需要联网搜索

def identify_web_search_needs(query, conversation_history):
    """
    输入:
        query: 用户查询
        conversation_history: 对话历史上下文
    输出:
        need_web_search: 是否需要联网搜索
        search_reason: 搜索原因
        confidence: 置信度(0-1)
    """

对应的 Prompt:

instruction = """
你是一个智能的查询分析专家。请分析用户的查询,判断是否需要联网搜索来获取最新、最准确的信息。

需要联网搜索的情况包括:
1. 时效性信息 - 包含"最新"、"今天"、"现在"、"实时"、"当前"等时间相关词汇
2. 价格信息 - 包含"多少钱"、"价格"、"费用"、"票价"等价格相关词汇
3. 营业信息 - 包含"营业时间"、"开放时间"、"闭园时间"、"是否开放"等营业状态
4. 活动信息 - 包含"活动"、"表演"、"演出"、"节日"、"庆典"等动态信息
5. 天气信息 - 包含"天气"、"下雨"、"温度"等天气相关
6. 交通信息 - 包含"怎么去"、"交通"、"地铁"、"公交"等交通方式
7. 预订信息 - 包含"预订"、"预约"、"购票"、"订票"等预订相关
8. 实时状态 - 包含"排队"、"拥挤"、"人流量"等实时状态

请返回JSON格式:
{
    "need_web_search": true/false,
    "search_reason": "需要搜索的原因",
    "confidence": "置信度(0-1)"
}
"""

prompt = f"""
### 指令 ###
{instruction}

### 对话历史 ###
{conversation_history}

### 用户查询 ###
{query}

### 分析结果 ###
"""

核心功能 2:为联网搜索改写查询

def rewrite_for_web_search(query, search_type="general"):
    """
    输入:
        query: 原始查询
        search_type: 搜索类型
    输出:
        rewritten_query: 改写后的查询
        search_keywords: 搜索关键词列表
        search_intent: 搜索意图
        suggested_sources: 建议搜索来源
    """

对应的 Prompt:

instruction = """
你是一个专业的搜索查询优化专家。请将用户的查询改写为更适合搜索引擎检索的形式。

改写技巧:
1. 添加具体地点 - 如"上海迪士尼乐园"、"香港迪士尼乐园"
2. 添加时间范围 - 如"2024年"、"今天"、"本周"
3. 使用关键词组合 - 将长句拆分为关键词
4. 添加搜索意图 - 明确搜索目的
5. 去除口语化表达 - 转换为标准搜索词
6. 添加相关词汇 - 增加同义词或相关词

请返回JSON格式:
{
    "rewritten_query": "改写后的搜索查询",
    "search_keywords": ["关键词1", "关键词2", "关键词3"],
    "search_intent": "搜索意图",
    "suggested_sources": ["建议搜索的网站类型"]
}
"""

prompt = f"""
### 指令 ###
{instruction}

### 原始查询 ###
{query}

### 搜索类型 ###
{search_type}

### 改写结果 ###
"""

核心功能 3:生成搜索策略

def generate_search_strategy(query, search_type="general"):
    """
    输入:
        query: 用户查询
        search_type: 搜索类型
    输出:
        primary_keywords: 主要关键词
        extended_keywords: 扩展关键词
        search_platforms: 搜索平台
        search_tips: 搜索技巧
        verification_methods: 验证方法
    """

对应的 Prompt:

instruction = f"""
你是一个搜索策略专家。请为用户的查询制定详细的搜索策略。

当前日期:{current_date}

搜索策略包括:
1. 主要搜索词 - 核心关键词
2. 扩展搜索词 - 相关词汇和同义词
3. 搜索网站 - 推荐的搜索平台
4. 时间范围 - 具体的搜索时间范围

请返回JSON格式:
{{
    "primary_keywords": ["主要关键词"],
    "extended_keywords": ["扩展关键词"],
    "search_platforms": ["搜索平台"],
    "time_range": "具体的时间范围"
}}
"""

prompt = f"""
### 指令 ###
{instruction}

### 用户查询 ###
{query}

### 搜索类型 ###
{search_type}

### 搜索策略 ###
"""

实际运行效果

示例 1:

对话历史:
用户: "我想去上海迪士尼乐园玩"
AI: "上海迪士尼乐园是一个很棒的选择!"

当前查询: 上海迪士尼乐园今天开放吗?现在人多不多?

✓ 需要联网搜索
搜索原因: 查询上海迪士尼乐园今天是否开放属于营业信息,'现在人多不多'涉及实时状态(人流量),两者都需要联网获取最新、准确的信息。
置信度: 0.98

改写查询: 上海迪士尼乐园 2024年今天开放时间及人流情况
搜索关键词: ['上海迪士尼乐园', '开放时间', '2024年', '人流量', '游客数量']
搜索意图: 获取上海迪士尼乐园今日是否开放以及当前游客密度信息,用于出行规划
建议来源: ['官方旅游网站', '携程/飞猪等旅游平台', '大众点评或美团用户评价', '本地生活类公众号']

搜索策略:
- 主要关键词: ['上海迪士尼乐园今天开放吗?现在人多不多?']
- 扩展关键词: []
- 搜索平台: ['百度', '谷歌']
- 时间范围: 最近一周

示例 2:

当前查询: 下周六的门票多少钱?需要提前多久预订?

✓ 需要联网搜索
搜索原因: 查询下周六的门票价格和预订时间,涉及价格信息和预订信息,需要联网获取最新、最准确的数据。
置信度: 0.98

改写查询: 下周六上海迪士尼乐园门票价格及预订时间要求
搜索关键词: ['下周六', '上海迪士尼乐园', '门票价格', '预订时间', '提前多久预订']
搜索意图: 获取特定日期的门票价格和预订政策信息
建议来源: ['官方网站', '旅游预订平台(如携程、飞猪)', '景点官方社交媒体账号']

搜索策略:
- 主要关键词: ['下周六的门票多少钱?需要提前多久预订?']
- 扩展关键词: []
- 搜索平台: ['百度', '谷歌']
- 时间范围: 最近一周

延伸:对接真实搜索 API

如果后续要接入 Tavily MCP 进行具体的联网搜索,可以引导 LLM 生成对应的调用参数:

参数名 类型 是否必填 默认值 说明
query string 是 - 搜索关键词
search_depth string 否 "basic" 搜索深度,可选值:"basic" 或 "advanced"
time_range string 否 "week" 时间范围,如 "day"、"week"、"month"、"year" 或 "all"
max_results int 否 5 最大返回结果数量
include_images bool 否 false 是否包含图片
include_answer bool 否 false 是否包含由 LLM 生成的简短回答
include_raw_html bool 否 false 是否包含原始 HTML 内容
topic string 否 "general" 搜索主题,可选值:"general" 或 "news"
domains list 否 - 限定搜索的域名白名单,如 ["bbc.com", "techcrunch.com"]
exclude_domains list 否 - 排除的域名黑名单

九、思考:如果 LLM 可以处理无限上下文,RAG 还有意义吗?

这是个经典的面试题,答案是依然有意义,原因如下:

  • 效率与成本:LLM 处理长上下文时计算资源消耗大,响应时间增加。RAG 通过检索相关片段,减少输入长度。
  • 知识更新:LLM 的知识截止于训练数据,无法实时更新。RAG 可以连接外部知识库,增强时效性。
  • 可解释性:RAG 的检索过程透明,用户可查看来源,增强信任。LLM 的生成过程则较难追溯。
  • 定制化:RAG 可针对特定领域定制检索系统,提供更精准的结果,而 LLM 的通用性可能无法满足特定需求。
  • 数据隐私:RAG 允许在本地或私有数据源上检索,避免敏感数据上传云端,适合隐私要求高的场景。

结论:结合 LLM 的生成能力和 RAG 的检索能力,可以提升整体性能,提供更全面、准确的回答。二者是互补关系,而非替代关系。


十、总结

本文从理论到实战完整走了一遍 RAG 技术栈:

  1. 选型判断:提示工程解决"没问清楚",RAG 解决"缺乏背景知识",微调解决"能力不足"。
  2. 核心流程:数据预处理(分块+向量化)→ 检索(相似度+重排序)→ 生成(上下文组装+LLM回答)。
  3. Embedding 选型:通用场景选 BGE-M3,中文轻量部署选 M3E-Base,复杂指令任务选 gte-Qwen2-instruct,具体可参考 MTEB 榜单。
  4. 工程实战:DeepSeek + Faiss + LangChain 即可快速搭建一个支持来源页码溯源的本地知识库问答系统;问答链优先用 stuff,文档多了再考虑 map_reduce / refine / map_rerank。
  5. 进阶优化:Query 改写解决"口语化查询与书面化知识库"的错位问题(上下文依赖、对比、模糊指代、多意图、反问五大类型);Query + 联网搜索解决知识库的时效性问题。

动手建议:找一份你自己业务场景里的 PDF(产品手册、规章制度、操作文档都可以),按本文第六节的代码跑一遍,再逐步加上 Query 改写和联网搜索,就是一个真正能用的 RAG 应用了。

posted @ 2026-09-17 20:39  coderliu_cn  阅读(22)  评论(0)    收藏  举报