AIGC标识 RAG 实战:手把手做一个差旅智能客服

RAG 实战:手把手做一个差旅智能客服(含 5 个踩坑)

用最短的代码,讲清楚 RAG 是什么、怎么搭,以及新手最容易踩的 5 个坑。

一、什么是 RAG

RAG = Retrieval-Augmented Generation(检索增强生成)。

大模型(LLM)虽然会聊天,但有两个致命问题:

  1. 知识过时:训练数据有截止时间,答不了最新的内部制度。
  2. 爱编(幻觉):遇到不知道的,会一本正经地胡说八道。

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_KEYLLM_API_KEY、模型名都是占位符,请换成你自己的。

四、新手最容易踩的 5 个坑

这部分是最值钱的——都是真实踩过、一个个调出来的。

坑 1:答案说「找不到」,其实是 k 太小 + 关键词没写全

现象:问「L1 职级的酒店标准」,客服回答「资料里没有 L1 标准」。

原因

  1. k=3 太小,真正含金额的那一段没进 top-3。
  2. 知识库里写的是「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 的成败,检索质量往往比生成质量更关键。五个坑全在检索环节。记住三条:

  1. 知识库要自包含:一段内容要独立成立,别依赖远处的上下文。
  2. 关键词要字面可匹配:写「L1」,别只写「L1 及以下」。
  3. k 值、chunk 大小、切分方式都要实测调优

大模型只照着 context 回答:context 缺信息,它要么瞎编、要么说不知道。所以把力气花在「让检索把该捞的都捞全」上,比调提示词管用得多。

posted @ 2026-09-01 20:20  灯无焰  阅读(14)  评论(0)    收藏  举报