RAG召回常见问题


一、RAG召回最常遇到的6类问题

1. 语义不匹配(最常见)

用户提问是自然语言,库中文档是关键词/专业表述,字面不匹配、语义匹配,导致召回不到正确文档。
例:

  • 用户问:“电脑连不上网怎么办”
  • 库中是:“以太网连接故障排查”
    → 传统关键词检索完全搜不到。

2. 召回噪声大(无关内容多)

召回了一堆不相关的片段,大模型被干扰,回答跑偏、 hallucination(幻觉)。

3. 召回不完整(漏关键信息)

长文档被切分后,上下文断裂,只召回碎片,缺关键前提/结论。

4. 向量漂移/过时

文档更新后向量没重算,旧向量和新内容不匹配;高频更新场景尤其严重。

5. 检索速度慢

数据量大(10w+文档)时,暴力检索/全量相似度计算耗时高。

6. 多路召回融合差

同时用关键词+向量+规则召回,结果权重混乱,最终质量不稳定。


二、Go 语言如何处理这些问题

Go 适合做 RAG 召回层:高性能、并发强、内存占用低,非常适合企业级服务。


问题1:语义不匹配

Go 解决方案
使用嵌入模型(Embedding)生成向量,用向量数据库做相似度检索。

Go 常用栈

  • 嵌入:github.com/liukanshan/go-embedding / 调用本地 Ollama / 通义千问/文心一言API
  • 向量库:Milvus(Go SDK)、ChromaPinecone
  • 相似度:余弦相似度

Go 极简示例(向量召回)

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package main

import (
"context"
"fmt"
"github.com/milvus-io/milvus-sdk-go/v2/client"
"github.com/milvus-io/milvus-sdk-go/v2/entity"
)

// 向量检索:解决语义不匹配
func vectorSearch(queryEmbedding []float32) ([]string, error) {
// 1. 连接 Milvus 向量库
c, err := client.NewGrpcClient(context.Background(), "localhost:19530")
if err != nil {
return nil, err
}
defer c.Close()

// 2. 构建检索参数
searchParam := entity.NewIndexFlatSearchParam(10)
vectors := []entity.Vector{entity.FloatVector(queryEmbedding)}

// 3. 执行语义检索
result, err := c.Search(
context.Background(),
"rag_collection", // 集合名
"", // 分区
[]string{"content"},// 要返回的字段
"", // 过滤条件
vectors, // 用户问题向量
"vector", // 向量字段
entity.COSINE, // 余弦相似度
5, // 召回 top5
searchParam,
)
if err != nil {
return nil, err
}

// 4. 解析结果
var docs []string
for _, rs := range result {
for _, field := range rs.Fields {
if field.Name == "content" {
docs = append(docs, field.Values.([]string)...)
}
}
}
return docs, nil
}

问题2:召回噪声大(无关内容)

Go 解决方案

  1. 多路召回 + 重排序(Rerank)
  2. 相似度阈值过滤
  3. 元数据过滤(Metadata Filtering)

Go 实现:阈值过滤 + Rerank

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 对召回结果做重排序 & 过滤噪声
func rerankAndFilter(query string, docs []string, threshold float64) []string {
var filtered []string

for _, doc := range docs {
// 计算 query 与 doc 的相似度(可调用 embedding 模型)
score := computeSimilarity(query, doc)

// 低于阈值直接丢弃(去噪)
if score >= threshold {
filtered = append(filtered, doc)
}
}

// 按分数排序
sort.Slice(filtered, func(i, j int) bool {
return computeSimilarity(query, filtered[i]) > computeSimilarity(query, filtered[j])
})

return filtered
}

企业级方案
使用 bge-rerank / Cohere Rerank 模型,Go 直接调用 API 做二次精排。


问题3:召回不完整(上下文断裂)

Go 解决方案

  1. 智能分块(Smart Chunking):按段落/标题/语义切分,不硬切固定长度
  2. 窗口召回:召回目标块的上一块 + 下一块,补全上下文

Go 智能分块示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 按段落切分,避免句子被截断
func chunkByParagraph(text string) []string {
// 按换行/空行分段
paragraphs := regexp.MustCompile(`\n\s*\n`).Split(text, -1)

// 过滤空段
var chunks []string
for _, p := range paragraphs {
trimmed := strings.TrimSpace(p)
if len(trimmed) > 0 {
chunks = append(chunks, trimmed)
}
}
return chunks
}

问题4:向量漂移、数据过时

Go 解决方案

  1. 增量更新向量
  2. 文档哈希校验:内容变了才重新生成向量
  3. 定时任务更新:Go 原生 time/ticker 做定时刷新

Go 增量更新向量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 只有文档内容变化时,才重新生成向量
func updateEmbeddingIfNeeded(docID string, content string) error {
// 1. 计算内容哈希
hash := sha256.Sum256([]byte(content))
hashStr := fmt.Sprintf("%x", hash)

// 2. 查询数据库中旧哈希
oldHash, err := getDocHashFromDB(docID)
if err != nil {
return err
}

// 3. 没变就跳过
if hashStr == oldHash {
return nil
}

// 4. 变了 → 重新生成向量并更新
emb, err := generateEmbedding(content)
if err != nil {
return err
}
return saveEmbeddingToMilvus(docID, emb, hashStr)
}

问题5:检索速度慢

Go 解决方案

  1. 向量库建索引(IVF_FLAT / HNSW)
  2. Go 并发预处理:嵌入、检索并发执行
  3. 内存缓存:热点 Query 缓存召回结果

Go 并发生成嵌入(提速)

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
// 并发生成文档向量,大幅提速
func batchGenerateEmbeddings(docs []string) ([][]float32, error) {
ch := make(chan []float32, len(docs))
errCh := make(chan error, len(docs))

// 并发请求
for _, doc := range docs {
go func(d string) {
emb, err := callEmbeddingAPI(d)
if err != nil {
errCh <- err
return
}
ch <- emb
}(doc)
}

// 收集结果
var embeddings [][]float32
for i := 0; i < len(docs); i++ {
select {
case emb := <-ch:
embeddings = append(embeddings, emb)
case err := <-errCh:
return nil, err
}
}
return embeddings, nil
}

问题6:多路召回融合差

Go 解决方案

  • 向量召回(语义)
  • 关键词召回(BM25)
  • 元数据过滤(权限、分类、时间)
  • 加权融合(RRF 算法)

Go 非常适合做多路召回的调度层

RRF 融合公式(Go 实现)

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
// RRF 召回结果融合(最稳定的多路融合算法)
func rrfFuse(ranks ...[]string) []string {
score := make(map[string]float64)
const k = 60

for _, rank := range ranks {
for i, doc := range rank {
score[doc] += 1.0 / (float64(i+1) + k)
}
}

// 按总分排序
type pair struct {
doc string
score float64
}
var pairs []pair
for d, s := range score {
pairs = append(pairs, pair{d, s})
}
sort.Slice(pairs, func(i, j int) bool {
return pairs[i].score > pairs[j].score
})

var res []string
for _, p := range pairs {
res = append(res, p.doc)
}
return res
}

三、总结

1
2
3
4
5
6
嵌入模型:Ollama (bge-small) / 云厂商API
向量数据库:Milvus(Go SDK 最完善)
关键词检索:Bleve(Go 原生 BM25)
重排序:bge-rerank-large
缓存:Redis / 内存LRU
调度:Go 原生并发 + 协程

纯向量的问题?

纯向量:容易漏招、错招(比如用户搜 “TCP 三次握手”,向量可能召回 “UDP 连接”)
工业级 RAG 标准做法:向量(Dense)+ 关键词(Sparse/BM25)多路召回 + RRF 融合
【RRF = Reciprocal Rank Fusion,倒数排名融合,是 RAG 里最常用、实现最简单、效果稳定的多路召回结果合并算法。】
MySQL 现在自带 BM25 + 全文检索,功能足够覆盖 RAG 召回。