首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >RAG 检索效果差?多种查询预处理方案帮你搞定

RAG 检索效果差?多种查询预处理方案帮你搞定

作者头像
tunsuy
发布2026-04-16 15:51:33
发布2026-04-16 15:51:33
1060
举报

❝你的 RAG 系统检索效果不好?80% 的原因出在查询预处理上。本文系统梳理 7 种查询预处理方案,重点讲清原理、关键细节和提示词设计。 ❞

一、为什么查询预处理如此重要?

在 RAG 系统中,有一个被大多数人忽略的关键环节——「查询预处理(Query Preprocessing)」

看一个典型场景:

代码语言:javascript
复制
用户:什么是 Transformer?
助手:Transformer 是一种基于自注意力机制的神经网络架构……
用户:它和 RNN 有什么区别?

最后一句"它和 RNN 有什么区别?"——如果直接拿这句话去向量数据库检索,「向量数据库完全不知道"它"指的是 Transformer」,检索结果大概率跑偏。

查询预处理要解决的核心问题就是:「将用户的原始查询转化为更适合检索的形式。」

一个好的查询预处理,可以将检索的 「Recall@10 提升 15%~40%」,直接决定最终回答的质量。


二、查询预处理的整体架构

代码语言:javascript
复制
┌──────────────┐
│   用户提问    │
└──────┬───────┘
       ↓
┌──────────────────────────┐
│   第一层:查询预处理       │  ← 本文重点
│  查询改写/意图识别/查询扩展 │
└──────────┬───────────────┘
           ↓
┌──────────────────────────┐
│   第二层:向量检索(粗排)  │
└──────────┬───────────────┘
           ↓
┌──────────────────────────┐
│   第三层:精排 Reranker    │
└──────────┬───────────────┘
           ↓
┌──────────────────────────┐
│   第四层:LLM 生成回答     │
└──────────────────────────┘

在深入各方案之前,先看一下接口抽象。不管用哪种方案,对外暴露的接口应该是统一的:

代码语言:javascript
复制
// Enhancer 查询增强器接口
type Enhancer interface {
    EnhanceQuery(ctx context.Context, req *Request) (*Enhanced, error)
}

type Request struct {
    Query   string    // 用户当前查询
    History []Message // 对话历史
}

type Enhanced struct {
    Enhanced string   // 增强后的查询
    Keywords []string // 提取的关键词
}

有了统一接口,不同方案可以自由切换和组合。接下来逐一剖析。


三、方案一:Passthrough(直接透传)

原理

最简单的方案——「什么都不做」,直接把用户的原始查询传给检索模块。

代码语言:javascript
复制
用户查询: "Go语言怎么做依赖注入?"
    ↓
检索查询: "Go语言怎么做依赖注入?"  (原封不动)

关键实现

代码语言:javascript
复制
type PassthroughEnhancer struct{}

func (p *PassthroughEnhancer) EnhanceQuery(
    ctx context.Context, req *Request,
) (*Enhanced, error) {
    return &Enhanced{
        Enhanced: req.Query,
        Keywords: []string{req.Query},
    }, nil
}

适用场景

  • 「单轮对话」,用户查询本身完整、自包含
  • 「MVP 快速上线」,先跑通 RAG 全流程
  • 「成本敏感」,不愿为每次查询额外调用模型
  • ❌ 多轮对话,口语化查询

一句话总结

❝零延迟、零成本、零依赖,但完全依赖用户输入质量。适合作为起步方案和降级兜底。 ❞


四、方案二:规则/统计方法(Rule-Based)

原理

通过预定义的规则和统计方法对查询进行预处理:

代码语言:javascript
复制
原始查询: "请问一下 trasformer 的 self atention 机制是怎么运作的呢?"
    ↓ 停用词移除 → "trasformer self atention 机制 运作"
    ↓ 拼写纠错   → "transformer self attention 机制 运作"
    ↓ 同义词扩展 → keywords: ["transformer", "self-attention", "自注意力", "机制"]

四个核心操作:

  1. 「停用词移除」——去掉"的"、"了"、"请问"等无意义词
  2. 「同义词扩展」——将"ML"扩展为"Machine Learning"
  3. 「拼写纠错」——将"Trasformer"纠正为"Transformer"
  4. 「词干提取」——将"running"还原为"run"(英文场景)

关键实现

核心数据结构就是两张表——停用词表和同义词表:

代码语言:javascript
复制
type RuleBasedEnhancer struct {
    stopWords map[string]bool       // 停用词表
    synonyms  map[string][]string   // 同义词映射
}

func (e *RuleBasedEnhancer) EnhanceQuery(
    ctx context.Context, req *Request,
) (*Enhanced, error) {
    tokens := tokenize(req.Query)

    // 1. 移除停用词
    filtered := make([]string, 0, len(tokens))
    for _, t := range tokens {
        if !e.stopWords[strings.ToLower(t)] {
            filtered = append(filtered, t)
        }
    }

    // 2. 同义词扩展(扩充关键词列表,不改变主查询)
    keywords := make([]string, 0)
    for _, t := range filtered {
        keywords = append(keywords, t)
        if syns, ok := e.synonyms[strings.ToLower(t)]; ok {
            keywords = append(keywords, syns...)
        }
    }

    return &Enhanced{
        Enhanced: strings.Join(filtered, " "),
        Keywords: keywords,
    }, nil
}

「关键细节:同义词表的维护策略」

同义词表不是随便写的,需要按优先级分层:

层级

来源

示例

通用缩写

行业通用

k8s→kubernetes, db→database

领域术语

业务知识库

DI→dependency injection→依赖注入

口语映射

用户行为日志

挂了→服务不可用, 卡了→性能问题

适用场景

  • ✅ 有明确的行业术语/缩写词表
  • ✅ 延迟要求极高(<1ms)
  • ✅ 搜索引擎式关键词匹配场景
  • ❌ 无法处理语义理解、多轮上下文

一句话总结

❝延迟极低、可控性强、可解释,但需要人工维护词表,无法处理复杂语义。 ❞


五、方案三:基于小模型的 NER + 关键词提取

原理

使用轻量 NLP 模型从查询中提取结构化信息:

代码语言:javascript
复制
原始查询: "谷歌在2017年提出的Transformer架构是怎么处理长序列问题的?"
    ↓ NER 实体提取
    实体: [谷歌(ORG), 2017(DATE), Transformer(TECH)]
    ↓ 关键词提取 + TF-IDF 排序
    关键词: ["Transformer", "长序列", "架构"]
    ↓ 组合
    增强查询: "Transformer架构 长序列处理"

两个核心任务:

  1. 「命名实体识别(NER)」——提取人名、组织名、技术名词等实体
  2. 「关键词提取」——用 TF-IDF 或词性标注识别最重要的术语

关键实现

Go 中常用 jieba 分词 + 词性标注来实现:

代码语言:javascript
复制
type NERKeywordEnhancer struct {
    segmenter *gojieba.Jieba
    idf       map[string]float64
}

func (e *NERKeywordEnhancer) EnhanceQuery(
    ctx context.Context, req *Request,
) (*Enhanced, error) {
    // 分词 + 词性标注
    words := e.segmenter.Tag(req.Query)

    // 只保留名词(n)、专有名词(nr/ns/nt)、英文(eng)
    keywords := make([]string, 0)
    for _, w := range words {
        word, pos, _ := strings.Cut(w, "/")
        if strings.HasPrefix(pos, "n") || pos == "eng" {
            keywords = append(keywords, word)
        }
    }

    // TF-IDF 排序,保留 Top-5
    sort.Slice(keywords, func(i, j int) bool {
        return e.idf[keywords[i]] > e.idf[keywords[j]]
    })
    iflen(keywords) > 5 {
        keywords = keywords[:5]
    }

    return &Enhanced{
        Enhanced: strings.Join(keywords, " "),
        Keywords: keywords,
    }, nil
}

「关键细节:词性过滤规则」

不是所有词性都有检索价值,经验上保留的词性标签:

保留

词性

说明

n/nr/ns/nt

名词、人名、地名、机构名

eng

英文词(通常是技术术语)

vn

动名词(如"编程"、"部署")

v/d/p/c/u

动词、副词、介词、连词、助词

适用场景

  • ✅ 混合检索(关键词检索 + 向量检索)
  • ✅ 延迟可接受 10~50ms
  • ✅ 可离线部署,不依赖外部 API
  • ❌ 无法处理多轮上下文和深度语义理解

一句话总结

❝低延迟可离线,适合从查询中提取结构化信息辅助检索,但语义理解能力有限。 ❞


六、方案四:LLM 查询改写(Query Rewriting)⭐

❝这是当前「最主流」的方案,重点讲。 ❞

原理

将用户的「对话历史」「当前查询」发给 LLM,LLM 理解上下文后输出一个「独立的、适合检索的查询」

代码语言:javascript
复制
对话历史:
  用户: 什么是 Transformer?
  助手: Transformer 是一种基于自注意力机制的神经网络架构...
  用户: 它和 RNN 有什么区别?
                ↓
         [LLM 查询改写]
                ↓
改写结果: "Transformer架构与RNN循环神经网络的区别对比"

🔑 提示词设计(核心)

提示词是这个方案的灵魂。设计要点:

「System Prompt」——定义角色和规则:

代码语言:javascript
复制
你是一个查询改写助手。你的任务是将用户的对话式查询改写为适合向量数据库检索的独立查询。

规则:
1. 改写后的查询必须是独立的,不依赖对话上下文即可理解
2. 解析所有指代词("它"、"这个"、"那个"等),替换为具体名词
3. 保留关键技术术语,不要过度简化
4. 改写结果应该是一个陈述性的短语或短句,不要用疑问句
5. 如果当前查询已经足够明确,保持原样即可,不要过度改写
6. 只输出改写后的查询,不要解释

「User Prompt」——动态构建:

代码语言:javascript
复制
对话历史:
用户:什么是 gRPC?
助手:gRPC 是 Google 开发的高性能 RPC 框架...

当前查询:它支持哪些语言?

请改写上述查询。

「为什么提示词要这样写?几个关键细节:」

  1. 「"陈述性短语,不要疑问句"」——向量数据库中的文档是陈述性的,用陈述句检索相似度更高
  2. 「"不要过度改写"」——有些 LLM 会把简单查询"润色"得面目全非,反而降低检索效果
  3. 「"只输出改写后的查询"」——避免 LLM 输出"好的,改写结果如下:..."这类废话
  4. 「Temperature 设为 0」——查询改写需要确定性输出,不需要创造性

关键实现

代码语言:javascript
复制
const queryRewriteSystemPrompt = `你是一个查询改写助手。你的任务是将用户的对话式查询改写为适合向量数据库检索的独立查询。

规则:
1. 改写后的查询必须是独立的,不依赖对话上下文即可理解
2. 解析所有指代词("它"、"这个"、"那个"等),替换为具体名词
3. 保留关键技术术语,不要过度简化
4. 改写结果应该是一个陈述性的短语或短句,不要用疑问句
5. 如果当前查询已经足够明确,保持原样即可,不要过度改写
6. 只输出改写后的查询,不要解释

示例:
对话历史:
用户:什么是 gRPC?
助手:gRPC 是 Google 开发的高性能 RPC 框架...
当前查询:它支持哪些语言?
改写结果:gRPC 支持的编程语言列表`

type LLMQueryRewriter struct {
    client LLMClient
    model  string
}

func (r *LLMQueryRewriter) EnhanceQuery(
    ctx context.Context, req *Request,
) (*Enhanced, error) {
    prompt := r.buildUserPrompt(req)

    resp, err := r.client.ChatCompletion(ctx, &ChatRequest{
        Model: r.model,
        Messages: []Message{
            {Role: "system", Content: queryRewriteSystemPrompt},
            {Role: "user", Content: prompt},
        },
        MaxTokens:   200,   // 改写查询不需要太长
        Temperature: 0.0,   // 确定性输出
    })
    if err != nil {
        // 降级:返回原始查询,不要让预处理失败阻断整个流程
        return &Enhanced{Enhanced: req.Query, Keywords: []string{req.Query}}, nil
    }

    enhanced := strings.TrimSpace(resp.Content)
    return &Enhanced{Enhanced: enhanced, Keywords: extractKeywords(enhanced)}, nil
}

func (r *LLMQueryRewriter) buildUserPrompt(req *Request) string {
    var sb strings.Builder
    iflen(req.History) > 0 {
        sb.WriteString("对话历史:\n")
        for _, msg := range req.History {
            role := "用户"
            if msg.Role == "assistant" {
                role = "助手"
            }
            sb.WriteString(fmt.Sprintf("%s:%s\n", role, msg.Content))
        }
        sb.WriteString("\n")
    }
    sb.WriteString(fmt.Sprintf("当前查询:%s\n\n请改写上述查询。", req.Query))
    return sb.String()
}

模型选择

查询改写任务「不需要大模型」,7B~8B 的轻量模型完全够用:

模型

延迟

成本

推荐度

GPT-4o-mini

~300ms

~$0.0001/次

⭐⭐⭐⭐⭐

DeepSeek-V3

~400ms

~$0.0001/次

⭐⭐⭐⭐⭐

Qwen2.5-7B(自部署)

~100ms

自部署成本

⭐⭐⭐⭐

适用场景

  • 「多轮对话」(必须方案)
  • ✅ 口语化查询、跨语言检索
  • ❌ 超低延迟场景(200~500ms 额外延迟)
  • ❌ 完全离线环境

一句话总结

❝效果最好的通用方案,多轮对话的刚需。注意降级策略和 Temperature 设为 0。 ❞


七、方案五:HyDE(假想文档嵌入)

原理

HyDE(Hypothetical Document Embedding)由 CMU 在 2022 年提出,思路非常巧妙:

「不直接用查询去检索,而是先让 LLM 生成一个"假想的回答文档",用这个假想文档的 Embedding 去检索真实文档。」

代码语言:javascript
复制
用户查询: "Go语言怎么做依赖注入?"
        ↓ LLM 生成假想回答
假想文档: "在Go语言中,依赖注入通常通过构造函数参数传递来实现。
          常见模式包括:构造函数注入、Functional Options 模式、
          Wire 框架的编译时注入、接口驱动设计……"
        ↓ 对假想文档做 Embedding
        ↓ 用该向量检索真实文档

「为什么有效?」

向量空间中,「"问题"和"回答"的向量距离通常较远」。用问题向量检索回答文档,天然存在语义鸿沟:

代码语言:javascript
复制
向量空间:

    问题向量 ●
         ↗         ↖  (距离远)

真实文档A ●           ● 真实文档B
      ↑(距离近)
假想文档 ●  ← 用这个向量检索,和真实文档天然更近

假想文档本身就是"回答"的形式,它的向量和知识库中的真实文档向量天然更接近。

🔑 提示词设计(核心)

HyDE 的提示词有两个关键要求:

  1. 「生成的内容要像一篇文档片段」,而不是对话式回答
  2. 「要尽量覆盖相关术语」,因为最终目的是用 Embedding 做检索
代码语言:javascript
复制
请针对以下问题,写一段专业的技术文档片段。
要求:
1. 直接给出内容,不要包含"根据"、"以下是"等引导语
2. 尽量覆盖相关的技术术语和概念
3. 内容格式应该像知识库中的文档,而不是聊天回答
4. 长度控制在 150~200 字

问题:{query}

「关键细节:为什么要强调"像文档而不是聊天回答"?」

因为你的知识库里存的是文档,如果假想文档的语体是"首先我来给你解释一下……",它的 Embedding 会偏向对话风格,反而和文档的距离变远。

关键实现

代码语言:javascript
复制
type HyDEEnhancer struct {
    llmClient LLMClient
    model     string
}

func (h *HyDEEnhancer) EnhanceQuery(
    ctx context.Context, req *Request,
) (*Enhanced, error) {
    hypoDoc, err := h.generateHypotheticalDoc(ctx, req.Query)
    if err != nil {
        return &Enhanced{Enhanced: req.Query, Keywords: []string{req.Query}}, nil
    }
    // 返回假想文档作为增强查询,后续 Retriever 会对它做 Embedding 并检索
    return &Enhanced{Enhanced: hypoDoc, Keywords: extractKeywords(hypoDoc)}, nil
}

func (h *HyDEEnhancer) generateHypotheticalDoc(
    ctx context.Context, query string,
) (string, error) {
    prompt := fmt.Sprintf(`请针对以下问题,写一段专业的技术文档片段。
要求:
1. 直接给出内容,不要包含"根据"、"以下是"等引导语
2. 尽量覆盖相关的技术术语和概念
3. 内容格式应该像知识库中的文档,而不是聊天回答
4. 长度控制在 150~200 字

问题:%s`, query)

    resp, err := h.llmClient.ChatCompletion(ctx, &ChatRequest{
        Model: h.model,
        Messages: []Message{
            {Role: "user", Content: prompt},
        },
        MaxTokens:   300,
        Temperature: 0.7, // 稍高温度,覆盖更多术语
    })
    if err != nil {
        return"", fmt.Errorf("generate hypothetical document: %w", err)
    }
    return strings.TrimSpace(resp.Content), nil
}

「注意 Temperature 设为 0.7」——和查询改写不同,这里希望生成的内容更丰富、覆盖更多术语,所以温度稍高。

适用场景

  • 「简短查询」——用户只输入几个词,但需要匹配长文档
  • 「专业领域」——用户查询和文档之间存在术语鸿沟
  • ❌ 实时性要求高(500ms~2s 延迟)
  • ❌ LLM 幻觉敏感场景(假想文档可能包含错误信息)

一句话总结

❝巧妙地用"假想回答"弥补问题和文档之间的语义鸿沟,对简短查询效果显著。注意提示词要强调"像文档不像聊天"。 ❞


八、方案六:Multi-Query(多查询扩展)

原理

核心思路:「一个问题从不同角度生成多个查询变体」,分别检索后用排序融合算法合并结果。

代码语言:javascript
复制
原始查询: "如何优化 Go 程序的内存使用?"
                    ↓ LLM 生成多个变体
┌─────────────────────────────────────┐
│ 变体1: "Go语言内存优化最佳实践"        │
│ 变体2: "Golang 减少内存分配和GC压力"   │
│ 变体3: "Go内存泄漏排查和pprof使用"     │
│ 变体4: "Go sync.Pool对象池内存复用"    │
└─────────────────┬───────────────────┘
                  ↓
     分别检索 → RRF 融合排序 → 更全面的结果

🔑 提示词设计(核心)

Multi-Query 的提示词有一个核心要求——「变体之间要有差异化」,不能换个措辞说同一件事:

代码语言:javascript
复制
请从不同角度改写以下查询,生成 4 个搜索查询变体。

原始查询:{query}

要求:
1. 每行一个查询变体,不要编号
2. 每个变体必须是独立的、完整的查询
3. 变体之间要有明确的角度差异,例如:
   - 概念解释角度
   - 实现方法角度
   - 最佳实践角度
   - 常见问题/排错角度
4. 不要只是换个措辞说同一件事
5. 只输出查询变体,不要其他内容

「关键细节:"不要只换措辞"这条为什么重要?」

如果 4 个变体只是同义改写("优化内存"、"减少内存"、"内存性能调优"、"内存管理优化"),它们检索到的文档高度重叠,Multi-Query 就退化成了普通查询改写,白白多花了 4 倍检索成本。

RRF 排序融合算法

Multi-Query 的检索结果需要用 「Reciprocal Rank Fusion(RRF)」 合并:

代码语言:javascript
复制
公式:RRF_score(doc) = Σ 1/(k + rank_i)    其中 k 通常取 60

示例(3 个查询变体):
查询1排序: [文档A, 文档B, 文档C]
查询2排序: [文档C, 文档A, 文档E]
查询3排序: [文档A, 文档E, 文档C]

文档A: 1/(60+0) + 1/(60+1) + 1/(60+0) = 0.0498  ← 排第1
文档C: 1/(60+2) + 1/(60+0) + 1/(60+2) = 0.0489  ← 排第2
文档E: 0 + 1/(60+2) + 1/(60+1) = 0.0325          ← 排第3

关键实现

代码语言:javascript
复制
type MultiQueryEnhancer struct {
    llmClient  LLMClient
    model      string
    numQueries int
}

func (m *MultiQueryEnhancer) EnhanceQuery(
    ctx context.Context, req *Request,
) (*Enhanced, error) {
    variants, err := m.generateVariants(ctx, req.Query)
    if err != nil {
        return &Enhanced{Enhanced: req.Query, Keywords: []string{req.Query}}, nil
    }

    // 原始查询 + 变体
    allQueries := append([]string{req.Query}, variants...)

    return &Enhanced{
        Enhanced: strings.Join(allQueries, "\n"),
        Keywords: dedup(extractAllKeywords(allQueries)),
    }, nil
}

// RRF 融合排序(配合 Retriever 使用)
func rrfFusion(results [][]Document, k int) []Document {
    scores := make(map[string]float64)
    docs := make(map[string]Document)

    for _, ranking := range results {
        for rank, doc := range ranking {
            scores[doc.ID] += 1.0 / float64(k+rank)
            docs[doc.ID] = doc
        }
    }

    // 按 RRF 分数降序排列
    type scored struct {
        doc   Document
        score float64
    }
    sorted := make([]scored, 0, len(docs))
    for id, doc := range docs {
        sorted = append(sorted, scored{doc: doc, score: scores[id]})
    }
    sort.Slice(sorted, func(i, j int) bool {
        return sorted[i].score > sorted[j].score
    })

    result := make([]Document, len(sorted))
    for i, s := range sorted {
        result[i] = s.doc
    }
    return result
}

适用场景

  • 「高召回率要求」——不能漏掉任何相关文档
  • 「复杂问题」——涉及多个维度和子主题
  • ❌ 实时对话(总延迟 1~3s)
  • ❌ 成本敏感(多次 LLM 调用 + 多次检索)

一句话总结

❝召回率最高的方案,但延迟和成本也最高。提示词的关键是确保变体之间有真正的角度差异。 ❞


九、方案七:Step-Back Prompting(后退提示)

原理

由 Google DeepMind 在 2023 年提出,核心思路:

「不直接检索具体问题,而是先"后退一步",将问题抽象为更高层次的概念性问题,用抽象问题检索背景知识,再结合原始问题检索具体细节。」

代码语言:javascript
复制
原始查询(具体): "gRPC-Go 中如何实现双向流式 RPC?"
                    ↓ Step-Back 抽象
抽象查询(通用): "gRPC 流式通信的类型和实现模式"
                    ↓
    用抽象查询检索 → 背景知识(流式 RPC 的四种类型、原理)
    用原始查询检索 → 具体细节(Go 代码实现)
                    ↓
    合并两组结果,LLM 同时拿到背景和细节来回答

🔑 提示词设计(核心)

Step-Back 的提示词需要精确控制"后退"的粒度——太粗会检索到不相关的内容,太细等于没后退:

代码语言:javascript
复制
你是一个搜索专家。给定一个具体的技术问题,请"后退一步",
生成一个更高层次的概念性问题,用于检索回答原始问题所需的背景知识。

规则:
1. 抽象问题应该覆盖原始问题所属的知识领域
2. 保留核心主题,不要太笼统("什么是编程?"就太笼统了)
3. 只输出一个问题,不要解释

示例:
具体问题:Python 3.12 中 type 语句的语法是什么?
抽象问题:Python 类型系统和类型注解的语法特性

具体问题:React useEffect 的依赖数组为空时会怎样?
抽象问题:React Hooks 中 useEffect 的生命周期和依赖机制

具体问题:{query}
抽象问题:

「关键细节:为什么要给示例?」

Step-Back 的"后退粒度"很难用规则描述清楚,但 「few-shot 示例可以隐式传达粒度标准」。示例中"Python 3.12 type 语句 → Python 类型系统"就是一个恰到好处的后退——退到了所属知识领域,但没退到"什么是 Python"。

关键实现

代码语言:javascript
复制
type StepBackEnhancer struct {
    llmClient LLMClient
    model     string
}

func (s *StepBackEnhancer) EnhanceQuery(
    ctx context.Context, req *Request,
) (*Enhanced, error) {
    abstractQuery, err := s.generateStepBack(ctx, req.Query)
    if err != nil {
        return &Enhanced{Enhanced: req.Query, Keywords: []string{req.Query}}, nil
    }

    // 同时返回原始查询和抽象查询,后续分别检索再合并
    combined := req.Query + "\n" + abstractQuery
    keywords := dedup(append(
        extractKeywords(req.Query),
        extractKeywords(abstractQuery)...,
    ))

    return &Enhanced{Enhanced: combined, Keywords: keywords}, nil
}

适用场景

  • 「需要背景知识才能回答」的问题
  • 「教程/文档检索」——知识库中有概念文档和实操文档
  • ❌ 简单直接的事实性问题("Go 的 goroutine 上限是多少?"不需要后退)

一句话总结

❝通过"后退一步"检索背景知识,让回答更有深度。提示词的关键是用 few-shot 示例控制后退粒度。 ❞


十、方案对比总结

全维度对比

维度

Passthrough

规则方法

小模型NER

LLM改写

HyDE

Multi-Query

Step-Back

「延迟」

0ms

<1ms

10-50ms

200-500ms

0.5-2s

1-3s

300-600ms

「成本」

免费

免费

中高

「精度提升」

很高

最高

「多轮支持」

⚠️

「实现复杂度」

极低

选型决策树

代码语言:javascript
复制
你的场景是什么?
│
├─ 单轮 + 查询质量高?  → Passthrough
├─ 有多轮对话?        → LLM 查询改写(必须)
│   ├─ 还需要更高召回率?→ + Multi-Query
│   └─ 术语鸿沟大?    → + HyDE
├─ 延迟要求 < 10ms?   → 规则方法
├─ 延迟要求 < 50ms?   → 小模型 NER
└─ 需要背景知识?      → Step-Back + LLM 改写

十一、实战建议

从简单开始,逐步升级

  1. 「MVP 阶段」:Passthrough,跑通全流程
  2. 「优化阶段」:加入 LLM 查询改写
  3. 「精细化阶段」:按需引入 HyDE / Multi-Query

降级策略是必须的

任何使用外部模型的方案,都「必须」有降级处理。LLM 挂了不能让整个 RAG 挂:

代码语言:javascript
复制
func (r *LLMQueryRewriter) EnhanceQuery(
    ctx context.Context, req *Request,
) (*Enhanced, error) {
    result, err := r.callLLM(ctx, req)
    if err != nil {
        log.WarnfContext(ctx, "LLM rewrite failed, falling back: %v", err)
        return r.fallback.EnhanceQuery(ctx, req)  // 降级到规则方法或 Passthrough
    }
    return result, nil
}

缓存可以大幅降低成本

对于重复查询,缓存改写结果可以同时省时间和省钱:

代码语言:javascript
复制
type CachedEnhancer struct {
    inner Enhancer
    cache *lru.Cache
}

func (c *CachedEnhancer) EnhanceQuery(
    ctx context.Context, req *Request,
) (*Enhanced, error) {
    key := buildCacheKey(req)
    if cached, ok := c.cache.Get(key); ok {
        return cached.(*Enhanced), nil
    }
    result, err := c.inner.EnhanceQuery(ctx, req)
    if err != nil {
        returnnil, err
    }
    c.cache.Add(key, result)
    return result, nil
}

提示词设计的通用原则

回顾全文,各方案的提示词有一些共性原则:

原则

说明

反例

「限制输出格式」

"只输出改写结果,不要解释"

LLM 输出"好的,改写如下:…"

「给 few-shot 示例」

示例比规则描述更直观

只写规则不给示例,LLM 理解偏差

「明确边界」

"如果已经足够明确,保持原样"

LLM 过度改写简单查询

「控制温度」

改写用 0,生成用 0.7

改写用高温度导致不稳定

「匹配目标形式」

HyDE 要"像文档",改写要"陈述句"

HyDE 生成对话式回答


十二、写在最后

查询预处理是 RAG 系统中被严重低估的环节。很多团队花大量时间调优 Embedding 模型和 Reranker,却忽略了最前面的查询预处理。

「记住一句话:Garbage In, Garbage Out。」

如果查询本身有问题,后面的检索和排序做得再好也无济于事。

选择适合你场景的方案,从简单开始,逐步优化——这才是 RAG 优化的正确姿势。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-04-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 有文化的技术人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、为什么查询预处理如此重要?
  • 二、查询预处理的整体架构
  • 三、方案一:Passthrough(直接透传)
    • 原理
    • 关键实现
    • 适用场景
    • 一句话总结
  • 四、方案二:规则/统计方法(Rule-Based)
    • 原理
    • 关键实现
    • 适用场景
    • 一句话总结
  • 五、方案三:基于小模型的 NER + 关键词提取
    • 原理
    • 关键实现
    • 适用场景
    • 一句话总结
  • 六、方案四:LLM 查询改写(Query Rewriting)⭐
    • 原理
    • 🔑 提示词设计(核心)
    • 关键实现
    • 模型选择
    • 适用场景
    • 一句话总结
  • 七、方案五:HyDE(假想文档嵌入)
    • 原理
    • 🔑 提示词设计(核心)
    • 关键实现
    • 适用场景
    • 一句话总结
  • 八、方案六:Multi-Query(多查询扩展)
    • 原理
    • 🔑 提示词设计(核心)
    • RRF 排序融合算法
    • 关键实现
    • 适用场景
    • 一句话总结
  • 九、方案七:Step-Back Prompting(后退提示)
    • 原理
    • 🔑 提示词设计(核心)
    • 关键实现
    • 适用场景
    • 一句话总结
  • 十、方案对比总结
    • 全维度对比
    • 选型决策树
  • 十一、实战建议
    • 从简单开始,逐步升级
    • 降级策略是必须的
    • 缓存可以大幅降低成本
    • 提示词设计的通用原则
  • 十二、写在最后
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档