你一定遇到过这样的场景:问大模型一个公司内部的技术问题,它回答得头头是道、引经据典,仔细一看——全是编的。

这就是大模型最让人头疼的问题——幻觉(Hallucination)。模型的知识停留在训练数据的截止日期,对私有数据一无所知,却又不肯承认"我不知道",于是开始一本正经地胡说八道。

怎么解决这个问题?微调成本太高,提示词工程又不够可靠。这篇文章分享一个实用且高效的方案——RAG(Retrieval-Augmented Generation,检索增强生成)

RAG 是什么?

RAG 的核心思想非常简单:让大模型"开卷考试"

与其让模型凭记忆回答,不如先从你的知识库里找到相关资料,然后把资料和问题一起交给模型,让它"参考着回答"。这样做有两个明显的好处:一是回答有据可查,大幅减少幻觉;二是知识可以随时更新,只需更新知识库而不用重新训练模型。

整个流程就三步:

用户提问 → 检索相关文档 → 拼接到 Prompt → LLM 生成回答

举个例子,用户问"我们公司的年假政策是什么?",系统先从公司制度文档中检索出相关段落,再把这些段落连同问题一起发给大模型。模型有了"参考资料",自然不会再编了。

Naive RAG 为什么不够用?

最简单的 RAG 实现看起来很美好:把文档切成小块 → 向量化 → 存入向量数据库 → 用户提问时检索最相似的 K 块 → 塞进 Prompt 生成回答。

但在生产环境中,这种"朴素 RAG"会暴露出不少问题:

切分破坏上下文。按固定 512 token 切分文档,很可能把一个完整的问答对腰斩,问题和答案分到了不同的 chunk 里。

语义相似不等于真相关。"如何重置密码"和"密码重置政策"在向量空间里距离很近,但用户想要的是操作步骤,不是政策条文。

Top-K 检索太粗糙。最相似的 K 个片段不一定是最有用的,可能漏掉真正关键的段落。

生产级 RAG 的三大优化

针对上述问题,2026 年的生产级 RAG 通常会采用以下优化手段。

混合检索(Hybrid Search)

单纯的向量检索擅长捕捉语义相似性,但对精确匹配(比如错误码、方法名)效果很差。混合检索将向量搜索和 BM25 关键词搜索结合,两者互补:

  • 向量搜索:理解"用户想表达什么"
  • BM25 搜索:精确匹配"用户提到的关键词"

两种检索结果通过 RRF(Reciprocal Rank Fusion)算法合并排序,效果远优于单一检索方式。

重排序(Re-ranking)

这是性价比最高的一步优化。先用宽泛的条件粗排召回 50 条文档,再用 Cross-encoder 模型对每条文档与问题的相关性做精细评分,只保留最相关的 Top 5。

Cross-encoder 会同时处理"问题+文档"这对输入,比单独编码的 Bi-encoder 理解更精准。虽然计算成本更高,但只对少量候选文档做重排,整体开销可控。

查询转换(Query Transformation)

用户的原始提问往往不够精确。查询转换技术会在检索前对问题进行改写:

  • HyDE:先让 LLM 生成一个假设性答案,再用这个答案去检索(答案的向量表示通常比问题更接近目标文档)
  • Multi-Query:生成多个不同角度的查询变体,分别检索后合并去重
  • Step-Back:先问一个更宽泛的问题获取背景知识,再结合原始问题精确检索

用 Go 搭建一个 RAG 系统

概念讲完了,让我们看看怎么用 Go 代码实现。这里选用 LangChainGo 框架,它是 LangChain 的 Go 语言实现,提供了 RAG 所需的核心组件。

加载文档并切分

// 创建文本加载器和切分器
f, _ := os.Open("knowledge.md")
loader := documentloaders.NewText(f)
splitter := textsplitter.NewRecursiveCharacter(
    textsplitter.WithChunkSize(500),
    textsplitter.WithChunkOverlap(50),
)
// 加载文档并按语义切分
docs, _ := loader.LoadAndSplit(ctx, splitter)

ChunkOverlap 设置了 50 个 token 的重叠,避免切分时丢失上下文。NewRecursiveCharacter 按递归字符方式切分,比固定长度切分更智能。

向量化并存储

// 创建 LLM 并包装为 Embedder
llm, _ := openai.New()
embedder, _ := embeddings.NewEmbedder(llm)
// 创建 Chroma 向量存储
store, _ := chroma.New(
    chroma.WithEmbedder(embedder),
    chroma.WithNameSpace("my-knowledge"),
)
store.AddDocuments(ctx, docs)

注意 openai.New() 返回的是 LLM,需要通过 embeddings.NewEmbedder() 包装后才能作为向量嵌入器使用。

构建 RAG 链

// 创建 Retriever 和检索问答链
retriever := vectorstores.ToRetriever(store, 5)
chain := chains.NewRetrievalQAFromLLM(llm, retriever)
result, _ := chains.Run(ctx, chain,
    "Go语言的垃圾回收机制是怎样的?")
fmt.Println(result)

vectorstores.ToRetriever(store, 5) 将向量存储转为检索器,检索最相关的 5 个文档片段。chains.NewRetrievalQAFromLLM 内部默认使用 Stuff 模式,将所有检索结果拼接到 Prompt 中。

向量数据库怎么选?

数据库 特点 适用场景
Chroma 轻量、嵌入式 原型开发、小规模
pgvector PostgreSQL 扩展 已用 PG 的团队
Milvus 分布式、十亿级 大规模生产环境
Qdrant 高性能、Rust 编写 对延迟要求高

小团队推荐从 Chroma 或 pgvector 起步,数据量上来了再迁移到 Milvus。

写在最后

RAG 不是什么银弹,但它是目前让大模型落地最务实、最有效的方案。从 Naive RAG 到混合检索、重排序、查询转换,每一步优化都在让系统离"真正有用"更近一步。

Go 语言在 RAG 领域有着天然优势——高并发的检索服务、低延迟的响应、编译成单一二进制的便捷部署。在我看来,虽然 Go 的 AI 生态不如 Python 丰富,但 LangChainGo 已经能覆盖大部分 RAG 场景。