你一定遇到过这样的场景:问大模型一个公司内部的技术问题,它回答得头头是道、引经据典,仔细一看——全是编的。
这就是大模型最让人头疼的问题——幻觉(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 场景。