RAG 实战:手把手做一个差旅智能客服
RAG 实战:手把手做一个差旅智能客服(含 5 个踩坑)
用最短的代码,讲清楚 RAG 是什么、怎么搭,以及新手最容易踩的 5 个坑。
一、什么是 RAG
RAG = Retrieval-Augmented Generation(检索增强生成)。
大模型(LLM)虽然会聊天,但有两个致命问题:
- 知识过时:训练数据有截止时间,答不了最新的内部制度。
- 爱编(幻觉):遇到不知道的,会一本正经地胡说八道。
RAG 的思路很朴素:先查资料,再让模型照着资料回答。
用户提问
│
▼
① 检索(Retrieval):把问题转成向量,去知识库里找最相关的几段
│
▼
② 生成(Generation):把「查到的资料 + 用户问题」一起喂给大模型
│
▼
有依据的回答
因为回答有参考资料兜底,模型就不容易瞎编了。
二、这个 Demo:差旅智能客服
假设我们要做一个差旅知识客服,用户问「住宿标准是多少」「出差补贴怎么算」,系统要从一份差旅制度文档里找答案。
完整流程分 8 步:
| 步骤 | 做什么 |
|---|---|
| 1 | 读取知识库文件(txt) |
| 2 | 把长文本切成小段(chunk) |
| 3 | 准备 embedding 模型(文字 → 向量) |
| 4 | 写入向量库 Chroma(首次写入,之后自动加载) |
| 5 | 准备大模型(LLM) |
| 6 | 写提示词模板(含多轮记忆占位符) |
| 7 | 组装流水线 chain |
| 8 | 问答循环(检索 + 生成) |
三、核心代码
import os
from pathlib import Path
import chromadb
from chromadb.errors import NotFoundError
from dotenv import load_dotenv
from langchain_deepseek import ChatDeepSeek # 换成你用的 LLM
from langchain_community.embeddings import DashScopeEmbeddings # 换成你的 embedding
from langchain_core.documents import Document
from langchain_core.chat_message_histories import ChatMessageHistory
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_chroma import Chroma
from pydantic import SecretStr
load_dotenv(".env", override=True) # API Key 放 .env,不进代码
# 1. 读取知识库
source_file = Path(__file__).with_name("知识库.txt")
content = source_file.read_text(encoding="utf-8")
# 2. 切分(chunk_size 别太小,否则标题和金额会被拆到两段)
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = splitter.split_text(content)
# 3. embedding
embedding = DashScopeEmbeddings(
model="text-embedding-v3",
dashscope_api_key=os.getenv("EMBEDDING_API_KEY"),
)
# 4. 向量库:首次写入,之后加载
chroma_dir = Path(__file__).with_name("chroma_db")
collection_name = "travel_knowledge"
client = chromadb.PersistentClient(path=str(chroma_dir))
try:
client.get_collection(collection_name)
exists = True
except NotFoundError:
exists = False
if exists:
vector_store = Chroma(
persist_directory=str(chroma_dir),
embedding_function=embedding,
collection_name=collection_name,
)
else:
documents = [Document(page_content=c, metadata={"source": source_file.name}) for c in chunks]
vector_store = Chroma.from_documents(
documents=documents,
embedding=embedding,
persist_directory=str(chroma_dir),
collection_name=collection_name,
)
# 5. LLM
llm = ChatDeepSeek(
base_url=os.getenv("LLM_BASE_URL"),
api_key=SecretStr(os.getenv("LLM_API_KEY") or ""),
model="your-llm-model",
temperature=0.2, # 客服场景要稳定,温度调低
)
# 6. 提示词(含多轮记忆占位符)
prompt = ChatPromptTemplate.from_messages([
("system", "你是差旅智能客服。只根据参考资料回答,资料里没有就直接说不知道。"),
MessagesPlaceholder(variable_name="history"),
("human", "参考资料:\n{context}\n\n用户问题: {question}"),
])
# 7. 组装链 + 多轮记忆
chain = prompt | llm | StrOutputParser()
history = ChatMessageHistory()
# 8. 问答循环
while True:
question = input("你:").strip()
if not question or question.lower() in {"q", "quit", "exit"}:
break
# 检索:找最相关的 5 段
rag_results = vector_store.similarity_search(question, k=5)
if not rag_results:
print("没有找到相关资料")
continue
context = "\n\n".join(
f"[来源: {d.metadata.get('source', '未知')}]\n{d.page_content}"
for d in rag_results
)
# 生成
answer = chain.invoke({
"context": context,
"question": question,
"history": history.messages,
})
print(f"客服:{answer}\n")
# 写历史,实现多轮追问
history.add_user_message(question)
history.add_ai_message(answer)
代码里的
EMBEDDING_API_KEY、LLM_API_KEY、模型名都是占位符,请换成你自己的。
四、新手最容易踩的 5 个坑
这部分是最值钱的——都是真实踩过、一个个调出来的。
坑 1:答案说「找不到」,其实是 k 太小 + 关键词没写全
现象:问「L1 职级的酒店标准」,客服回答「资料里没有 L1 标准」。
原因:
k=3太小,真正含金额的那一段没进 top-3。- 知识库里写的是「L1 及以下」,没有字面「L1」;而「交通标准」那段有字面「L1」,结果它排名更高(却没有金额)。
修复:
k调大到 5。- 知识库把「L1」字面写全,让关键词能直接命中。
坑 2:把「4 天」算成「4 晚」
现象:问 9 月 1 日到 9 月 4 日的补贴,客服按 4 晚算(应该 4 天 3 晚)。
原因:检索结果里只有金额标准(「100 元/晚」),没有「按跨夜天数计算」这条规则——规则在文档前面,离金额很远,没被检索到。模型看不到规则,就自作主张把天数当晚数。
修复:把规则直接写进金额那一行,让规则和金额在同一段。
坑 3:chunk 切太小,标题和金额被拆开
现象:问「酒店标准」,客服说「未列出具体金额」。
原因:chunk_size=200 时,「附表一:交通、酒店标准」这个标题(含「酒店」二字)和「交通标准」被切在同一段;真正含金额的「酒店标准:1500 元」被切到了下一段。查询命中标题那段(有「酒店」无金额),金额段反而没进 top-k。
修复:chunk_size 从 200 调到 500,让「标题 + 金额」落在同一段。
坑 4:「定义」和「金额」分离
现象:问「香港酒店标准」,客服说「无法确定香港属于 A 区还是 B 区」。
原因:A 区/B 区的具体城市列表写在「说明」里,和金额(300/400 元)不在同一段,检索没把定义捞回来。
修复:把城市列表直接内联到金额行:
- A 区(300 元/晚):香港、澳门、台湾……
- B 区(400 元/晚):纽约、伦敦……
坑 5:规则措辞有歧义,模型理解偏了
现象:base 香港去上海,客服漏发通讯补贴。
原因:规则写「Base 地(香港、上海、成都)以外的国家和地区」——括号里的城市列表让模型误以为「上海在这个集合里,不算以外」。
修复:去掉有歧义的括号列表,写显式的示例:「Base 香港去大陆适用;Base 大陆去港澳台适用;同一地区内不适用」。
五、总结
RAG 的成败,检索质量往往比生成质量更关键。五个坑全在检索环节。记住三条:
- 知识库要自包含:一段内容要独立成立,别依赖远处的上下文。
- 关键词要字面可匹配:写「L1」,别只写「L1 及以下」。
- k 值、chunk 大小、切分方式都要实测调优。
大模型只照着 context 回答:context 缺信息,它要么瞎编、要么说不知道。所以把力气花在「让检索把该捞的都捞全」上,比调提示词管用得多。

浙公网安备 33010602011771号