模型不知道你公司的事
问模型"我们公司的报销标准是多少",它只能瞎编——它的训练数据里没有你的内部文档。
两条路:微调模型(贵、慢、资料一变就要重来),或者把相关资料查出来,连同问题一起发给模型。
后者叫 RAG(检索增强生成)。它是目前 95% 的企业 AI 应用的实际做法。
本质就是一句话
先检索,再把检索到的内容塞进提示词。
prompt = f"""根据以下资料回答问题。资料里没有的,就说不知道。
资料:
{retrieved_docs}
问题:{question}"""
RAG 的全部复杂度,都在"怎么把最相关的那几段找出来"上。
为什么不用关键词搜索
用户问"怎么报销打车费",文档里写的是"交通费用凭证提交流程"。关键词匹配一个字都对不上。
向量检索解决的就是这个——它比较的是意思的接近程度,不是字面。
做法是用一个 embedding 模型把每段文字变成一串数字(向量),意思相近的文字,向量在空间里的位置也相近。查询时把问题也变成向量,找最近的几个。
完整的四步
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
# 1. 加载
docs = TextLoader("handbook.md", encoding="utf-8").load()
# 2. 切块
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = splitter.split_documents(docs)
# 3. 向量化 + 存库
vectorstore = FAISS.from_documents(chunks, OpenAIEmbeddings())
# 4. 检索
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
pip install langchain-community faiss-cpu 装依赖。FAISS 是本地向量库,零配置,适合入门和中小数据量。
切块是效果的胜负手
这是 RAG 里最被低估的一步。切得不好,后面怎么调都救不回来。
chunk_size=500:每块约 500 字符。太大→塞进提示词太贵且噪声多;太小→语义被切断,检索到半句话。中文文档 300~800 是常用区间。chunk_overlap=50:相邻块重叠 50 字符。防止一个完整意思正好被切在边界上。RecursiveCharacterTextSplitter会优先在段落、句子这些自然边界切,比按固定长度硬切好得多。
如果文档有明确结构(Markdown 标题、法律条款),用结构感知的切分器(如 MarkdownHeaderTextSplitter)效果更好——按标题切,每块自带上下文。
拼成完整的链
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
template = """只根据下面的资料回答问题。
资料里没有提到的,直接回答"资料里没有相关信息",不要编造。
资料:
{context}
问题:{question}"""
prompt = ChatPromptTemplate.from_template(template)
def format_docs(docs):
return "\n\n".join(d.page_content for d in docs)
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
print(rag_chain.invoke("报销打车费需要什么材料?"))
那个字典的含义是:两条支路并行——问题走 retriever 查资料后填进 context,同时原样填进 question。RunnablePassthrough() 就是"原样传过去"。
效果不好时,按这个顺序查
RAG 出问题时,新手总是先去改提示词,通常是白费力气。正确的排查顺序:
- 先把检索结果打印出来。
retriever.invoke(问题)看看查到了什么。如果查到的内容里根本没有答案,那不是模型的问题,改提示词永远没用。 - 调
chunk_size和切分策略。 - 调
k(取几块)。太少可能漏,太多会引入噪声干扰模型。 - 最后才是改提示词。
这条排查顺序能省你几天时间。
别忘了给出处
生产系统里一定要把来源带出来,让用户能核实:
docs = retriever.invoke(question)
for d in docs:
print(d.metadata) # {'source': 'handbook.md'}
AI 会出错,能溯源的答案才敢给用户看。
10 秒自测
RAG 回答错了,第一步该做什么?
打印检索结果,确认"资料里到底有没有正确答案"。先定位是检索的问题还是生成的问题,再动手。