LLM 有两个核心局限:知识有截止日期、无法访问私有数据。RAG(Retrieval-Augmented Generation)是目前解决这两个问题最主流的方案。

RAG 的基本流程

文档 → 切块 → Embedding → 存入向量库
                                ↓
用户问题 → Embedding → 向量检索 → 召回相关块 → 组合 Prompt → LLM → 回答

四个核心步骤:文档处理向量化检索生成

文档切块

文档太长无法直接塞给模型,需要切成小块:

from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50,  # 相邻块有重叠,避免信息割裂
    separators=["\n\n", "\n", "。", ",", " "]
)

chunks = splitter.split_documents(docs)

chunk_overlap 是个容易忽略的参数——它让相邻的块有内容重叠,避免一句话被硬切断导致语义丢失。

Embedding 与向量存储

from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma

embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

# 将文档块向量化并存入 Chroma
vectorstore = Chroma.from_documents(
    documents=chunks,
    embedding=embeddings,
    persist_directory="./chroma_db"
)

向量数据库本质是做高维空间的近邻搜索。Chroma 适合本地开发,生产环境可以考虑 Pinecone、Weaviate 或自建 Milvus。

检索与生成

from langchain.chains import RetrievalQA

retriever = vectorstore.as_retriever(
    search_type="similarity",
    search_kwargs={"k": 4}  # 召回最相关的 4 个块
)

qa_chain = RetrievalQA.from_chain_type(
    llm=ChatOpenAI(model="gpt-3.5-turbo"),
    retriever=retriever,
    return_source_documents=True
)

result = qa_chain.invoke({"query": "公司的请假政策是什么?"})
print(result["result"])

几个影响效果的关键点

切块策略chunk_size 太小,单块缺乏上下文;太大,引入噪声。通常 300-600 tokens 是个比较合适的范围。

Embedding 模型选择text-embedding-3-small 性价比高,中文场景也可以考虑 BGE 系列开源模型。

召回数量 k:k 越大召回越全但噪声越多。可以先召回多一点,再用 reranker 重排序。

Prompt 设计:明确告诉模型"只根据以下材料回答,材料中没有的不要编造",能显著减少幻觉。

小结

RAG 的上限在于检索质量。生成的问题好解决,检索的问题才是核心挑战。后续文章会深入讲检索优化的各种技巧。