实战二:RAG 知识库助手
很多 Agent 项目到最后都绕不开 RAG。原因很直接:模型再强,也不会天然知道你公司的制度、项目文档、会议纪要和内部流程。
如果你不把这些知识接进来,Agent 很容易表现得“很会说”,但一问到具体业务细节就开始泛化回答,甚至直接编。
这一节我们做一个简化版 HR 助手。它不追求花哨,只追求把最基础的 RAG 闭环看清楚。
Turn this chapter into hands-on prompt practice
Use the interactive lab to practice real prompt tasks in minutes.
#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 的模型:用于向量化和最终生成
bashpip install langchain langchain-chroma langchain-openai
#3. 准备知识库(Indexing)
假设我们有一个很小的 handbook.txt:
公司规定:
- 年假:入职满一年有 10 天年假。
- 病假:需提供医生证明,全薪。
- 报销:餐饮报销上限为每人每餐 $50,需发票。
pythonfrom 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 并不是把知识塞给模型就结束了,而是要在回答前先判断“应该查什么、查回来哪些片段、这些片段够不够回答当前问题”。
pythonfrom 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. 运行与测试
pythonresponse = rag_chain.invoke({"input": "我刚入职,请朋友吃饭花了 80 块,能报销吗?"}) print(response["answer"])
预期回答:
不能全额报销。根据公司规定,餐饮报销上限为每人每餐 $50。
这个例子看起来简单,但已经足够暴露出 RAG 最关键的判断点:
- 检索有没有真的把“餐饮报销上限 $50”这段找出来
- 模型有没有基于这段回答
- 模型会不会看到“80 块”之后又开始自己发挥解释
如果你线上发现回答不稳定,优先拆这三层看,不要一上来就改 prompt。
#6. 进阶:把 RAG 变成一个 Tool
上面的 rag_chain 还只是一个知识问答组件。如果你想把它放进更完整的 Agent 里,比如:
- 先查政策
- 再给用户总结
- 必要时帮用户生成申请邮件
那你就不能把 RAG 只当成一个页面功能,而要把它包装成一个可调用的工具。
pythonfrom 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 不准”的问题其实都能定位。