实战二:RAG 知识库助手
很多 Agent 项目到最后都绕不开 RAG。原因很直接:模型再强,也不会天然知道你公司的制度、项目文档、会议纪要和内部流程。
如果你不把这些知识接进来,Agent 很容易表现得“很会说”,但一问到具体业务细节就开始泛化回答,甚至直接编。
这一节我们做一个简化版 HR 助手。它不追求花哨,只追求把最基础的 RAG 闭环看清楚。
[PROMPT_LAB_BANNER]
1. 先理解这件事到底在解决什么
RAG 本质上不是“让模型更聪明”,而是给模型补一个受控的外部知识层。
它解决的是下面这类问题:
- 回答必须基于公司内部文档
- 内容会变,不能只靠预训练知识
- 用户问的是具体政策,不允许随口发挥
一个最小 RAG 流程大概是这样:
sequenceDiagram
participant User
participant Agent (Chain)
participant Retriever
participant VectorDB
participant LLM
User->>Agent: "能报销请客吃饭吗?"
Agent->>Retriever: Invoke
Retriever->>VectorDB: 语义搜索 (Similarity Search)
VectorDB-->>Retriever: 返回文档片段 (Context)
Retriever-->>Agent: Context
Agent->>LLM: Prompt + Context + Question
LLM-->>Agent: "根据规定,上限 $50..."
Agent-->>User: 最终回答
2. 技术栈
- LangChain:用来快速把检索和生成接起来
- ChromaDB:本地向量数据库,适合实验和原型
- 任意支持 embeddings + chat 的模型:用于向量化和最终生成
pip install langchain langchain-chroma langchain-openai
3. 准备知识库(Indexing)
假设我们有一个很小的 handbook.txt:
公司规定:
- 年假:入职满一年有 10 天年假。
- 病假:需提供医生证明,全薪。
- 报销:餐饮报销上限为每人每餐 $50,需发票。
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import CharacterTextSplitter
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings
# 1. 加载文档
loader = TextLoader("./handbook.txt")
documents = loader.load()
# 2. 切分 (Chunking)
text_splitter = CharacterTextSplitter(chunk_size=100, chunk_overlap=0)
docs = text_splitter.split_documents(documents)
# 3. 存入向量数据库
db = Chroma.from_documents(docs, OpenAIEmbeddings())
4. 构建检索链(Retrieval Chain)
入库之后,下一步才是让系统“会查”。
这里有个关键点经常被忽略:RAG 并不是把知识塞给模型就结束了,而是要在回答前先判断“应该查什么、查回来哪些片段、这些片段够不够回答当前问题”。
from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o")
# 定义 Prompt,明确要求基于 Context 回答
prompt = ChatPromptTemplate.from_template("""
你是一个 HR 助手。请基于下面的 Context 回答用户问题。
如果你不知道,就说不知道。
Context:
{context}
Question:
{input}
""")
# 创建 "文档合并链" (把检索到的片段塞进 Prompt)
combine_docs_chain = create_stuff_documents_chain(llm, prompt)
# 创建 "检索链" (连接 DB 和 LLM)
retriever = db.as_retriever()
rag_chain = create_retrieval_chain(retriever, combine_docs_chain)
这段示例能跑,但你在真实项目里最好再多做两件事:
- 明确要求“找不到就说不知道”,不要自由发挥。
- 让模型尽量引用或转述检索结果,而不是脱离 context 自己补完整答案。
5. 运行与测试
response = rag_chain.invoke({"input": "我刚入职,请朋友吃饭花了 80 块,能报销吗?"})
print(response["answer"])
预期回答:
不能全额报销。根据公司规定,餐饮报销上限为每人每餐 $50。
这个例子看起来简单,但已经足够暴露出 RAG 最关键的判断点:
- 检索有没有真的把“餐饮报销上限 $50”这段找出来
- 模型有没有基于这段回答
- 模型会不会看到“80 块”之后又开始自己发挥解释
如果你线上发现回答不稳定,优先拆这三层看,不要一上来就改 prompt。
6. 进阶:把 RAG 变成一个 Tool
上面的 rag_chain 还只是一个知识问答组件。如果你想把它放进更完整的 Agent 里,比如:
- 先查政策
- 再给用户总结
- 必要时帮用户生成申请邮件
那你就不能把 RAG 只当成一个页面功能,而要把它包装成一个可调用的工具。
from langchain.tools.retriever import create_retriever_tool
retriever_tool = create_retriever_tool(
retriever,
"search_company_policy",
"搜素公司关于请假、报销等行政规定的政策文档"
)
# 现在这个 tool 可以和其他 tool 一起给 Agent 使用了
tools = [retriever_tool, send_email_tool, ...]
一旦走到这一步,RAG 就从“一个知识库功能”变成了“Agent 的一项能力”。
7. 真实项目里 RAG 最容易出问题的地方
Chunk 切分不合理
切得太大,召回准确但浪费上下文;切得太碎,模型拿到一堆零散句子,难以理解完整含义。
这通常不是理论问题,而是非常实际的效果问题。很多“RAG 不准”,最后都是 chunk 策略有问题。
Embedding 和文档更新不同步
文档改了,但索引没更新,Agent 就会把旧制度当新制度回答。
这在内部知识库系统里特别危险,因为回答看起来很像真的。
检索到了,但没用对
有些系统明明把正确片段找回来了,结果模型回答时还是带上了自己的常识补充。你最后看到的表象是“RAG 查到了但回答还是不准”。
这时候要看的是:
- prompt 约束够不够
- 上下文是不是太长,正确片段被淹没了
- 模型是否被要求区分“已知”和“推测”
权限没做好
这是最不能忽视的一点。
如果你把所有文档一股脑都向量化,检索时又不做权限过滤,普通员工就可能问到本不该看到的内容。RAG 的权限控制必须做在检索层,而不是只靠最终回答层来“自觉克制”。
8. 一个更像生产环境的升级方向
如果你准备把这个实验继续做深,可以按这个顺序升级:
- 换成真实的多文档知识库,而不是单个
handbook.txt - 增加 metadata,比如文档类型、部门、更新时间、权限级别
- 在检索前做 query rewrite,让用户问题更适合搜索
- 加 rerank,把召回结果重新排序
- 给最终回答加引用来源,方便核对
做到这里,RAG 才会慢慢从 demo 走向可用系统。
小结
RAG 最值得掌握的,不是那几个术语,而是整条链路怎么拆开看:
- Indexing:知识怎么入库
- Retrieval:该找哪段内容
- Generation:回答是否真的基于检索结果
- Toolification:怎么把检索能力接进更大的 Agent 系统
只要你把这四层分清楚,很多“RAG 不准”的问题其实都能定位。