RAG项目实践

文章目录

项目介绍

项目背景

随着美团业务的不断扩展,客服人员需要应对海量的用户咨询,包括订单问题、退款流程、配送异常、优惠政策等。传统的知识库客服系统依赖规则匹配,回答僵硬,难以及时覆盖最新的业务规则。

为提升客户体验和客服效率,本项目基于 RAG(Retrieval-Augmented Generation,检索增强生成) 技术构建智能客服问答系统,将美团内部文档知识与大语言模型结合,实现更智能、更准确的自动化答复。

项目功能

针对智能客服系统本身,我们将采用RAG + LLM来完成这一需求。结合前面所学的知识,这个智能客服系统应该具备以下功能:

  • 支持历史记忆功能,并且能够实现历史记忆持久化。
  • 使用LCEL 表达式来构建链。
  • 支持RAG 检索功能,使大语言模型能够根据知识库文档内容进行作答。
  • 编写完善的提示词模板,内容包括历史对话信息、RAG 检索的上下文信息、用户提问,以及AI 作为客服的系统提示词。

系统架构

RAG 准备阶段

在 RAG 准备阶段,我们需要进行文档收集、文档处理、文档数据向量化操作以及文档相似性检索测试。

文档收集

收集美团客服相关知识文档,例如:

  • 业务手册(退款规则、订单处理流程)
  • 常见问题 FAQ
  • 内部客服知识库
  • 实时更新的运营公告

以美团外卖常见问题为例,文档地址:https://waimai.meituan.com/help/faq,我们通过playwright 工具爬虫获取页面数据并写入本地 txt 文件中。

安装依赖包

python 复制代码
# 安装浏览器插件库
pip install playwright chromium
playwright install
# 安装浏览器中文依赖
sudo apt update && sudo apt install fonts-wqy-zenhei fonts-wqy-microhei -y

代码如下:

python 复制代码
from playwright.sync_api import sync_playwright


def collect_faq(url):
    """
    收集指定URL页面中的FAQ内容

    参数:
        url (str): 目标网页URL地址

    返回:
        str: 提取的FAQ内容html代码
    """
    # 启动Playwright浏览器自动化工具
    with sync_playwright() as p:
        # 启动Chromium浏览器,设置为非无头模式并指定中文语言
        browser = p.chromium.launch(
            headless=False,
            args=['--lang=zh-CN']  # 浏览器语言
        )
        # 创建新页面,配置中文环境
        page = browser.new_page(
            locale='zh-CN',  # 页面 locale
            user_agent=(
                "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                "AppleWebKit/537.36 (KHTML, like Gecko) "
                "Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0"
            ),
            extra_http_headers={
                "Accept-Language": "zh-CN,zh;q=0.9"
            }
        )
        # 访问目标URL并等待页面加载完成
        page.goto(url, timeout=30_000)
        page.wait_for_load_state("networkidle")

        # 提取FAQ列表区域的html代码
        raw_text = page.locator("#faq-list").first.inner_html()
        browser.close()
        return raw_text


def save_faq(cleaned_text: str, output_file: str):
    """
    将FAQ内容保存到指定文件

    参数:
        cleaned_text (str): 要保存的FAQ内容
        output_file (str): 输出文件路径
    """
    # 写入文件
    with open(output_file, "w", encoding="utf-8") as f:
        f.write(cleaned_text)

    print(f"FAQ 已保存到 {output_file}")


if __name__ == "__main__":
    cleaned_text = collect_faq(url="https://waimai.meituan.com/help/faq")
    output_file = "faq.html"
    save_faq(cleaned_text, output_file)

获取到的原始文件内容如下:

python 复制代码
# cat faq.html
  <ul>
        <li class="faq-head head1">
          <h1>在线支付问题</h1>
          <span></span>
        </li>
        <li>
          <dl>
            <dt><a href="javascript:;" class="questions">Q:在线支付取消订单后钱怎么返还?<i class="icon i-triangledown fr"></i></a></dt>
            <dd class="answers hidden ">
              订单取消后,款项会在一个工作日内,直接返还到您的美团账户余额。
            </dd>
          </dl>
        </li>
    ......

文档处理

我们已经爬取了 FAQ 文档,接下来就需要对收集到的文档进行统一处理,内容包括:

  • 文本清洗(去除 HTML 标签、无关字符)
  • 分段切分(按规则或语义将文档拆分成小片段,便于检索)
  • 元数据标注(来源、时间、业务类别等)。

代码如下:

python 复制代码
import json
from bs4 import BeautifulSoup
from langchain.schema import Document

def parse_faq_html(file_path):
    """
    解析FAQ HTML文件,提取问题和答案信息并封装为Document对象列表。

    参数:
        file_path (str): FAQ HTML文件的路径。

    返回:
        list: 包含Document对象的列表,每个对象的metadata包含分类、问题、答案和来源信息。
    """
    docs = []
    with open(file_path, "r", encoding="utf-8") as f:
        soup = BeautifulSoup(f, "html.parser")

    current_category = None

    # 遍历所有<ul>标签,解析其中的<li>元素
    for ul in soup.find_all("ul"):
        for li in ul.find_all("li", recursive=False):
            h1 = li.find("h1")
            if h1:  # 分类标题
                current_category = h1.get_text(strip=True)
                continue

            dl = li.find("dl")
            if dl:
                # 去掉 Q:
                question_raw = dl.find("dt").get_text(strip=True)
                question = question_raw.lstrip("Q:").strip()
                answer = dl.find("dd").get_text(strip=True)

                docs.append(
                    Document(
                        page_content="",
                        metadata={
                            "source": file_path,
                            "category": current_category,
                            "question": question,
                            "answer": answer
                        }
                    )
                )
    return docs

def save_docs_to_json(docs, output_file):
    """
    将Document对象列表保存为JSON格式文件。

    参数:
        docs (list): 包含Document对象的列表。
        output_file (str): 输出JSON文件的路径。

    返回:
        None
    """
    data = [
        {
            "question": doc.metadata["question"],
            "answer": doc.metadata["answer"],
            "category": doc.metadata["category"],
            "source": doc.metadata["source"]
        }
        for doc in docs
    ]
    with open(output_file, "w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=2)
    print(f"FAQ 已保存到 {output_file}")

if __name__ == "__main__":
    faq_docs = parse_faq_html("faq.html")
    for d in faq_docs:
        print(d.metadata)
    save_docs_to_json(faq_docs, "faq.json")

执行后的结果如下:

python 复制代码
# cat faq.json
[
  {
    "question": "在线支付取消订单后钱怎么返还?",
    "answer": "订单取消后,款项会在一个工作日内,直接返还到您的美团账户余额。",
    "category": "在线支付问题",
    "source": "faq.html"
  },
  ......

文档数据向量化

我们将 FAQ 数据格式化成 json 数据后,接下来就要转成向量数据并存储到向量数据库中,此处以 redis 为例,操作内容包括:

使用 向量化模型(Embedding Model,如 BGE、OpenAI Embedding) 将文档片段转换为向量表示。

存储至向量数据库(如 Milvus、Weaviate、Redis Vector、Faiss),支持高效的相似度搜索。

代码如下

python 复制代码
import json
from langchain_ollama import OllamaEmbeddings
from langchain_redis import RedisConfig, RedisVectorStore


def insert_faq(texts, meta_data):
    """
    将FAQ文本数据插入到Redis向量存储中
    
    Args:
        texts (list): 包含问题文本的列表
        meta_data (list): 包含每个问题对应元数据的列表,每个元素为字典格式
        
    Returns:
        None
    """
    # 配置Redis连接参数和索引名称
    config = RedisConfig(
        index_name="faq",
        redis_url="redis://localhost:6379",
    )
    # 初始化 Embedding 模型
    embedding = OllamaEmbeddings(model="deepseek-r1:14b")
    # 创建Redis向量存储实例
    vector_store = RedisVectorStore(embedding, config=config)
    vector_store.add_texts(texts=texts, metadatas=meta_data)


def insert_from_file(file_path):
    """
    从JSON文件中读取FAQ数据并插入到向量存储中
    
    Args:
        file_path (str): 包含FAQ数据的JSON文件路径
        
    Returns:
        None
    """
    with open(file_path, "r", encoding="utf-8") as f:
        docs = json.load(f)
    texts = []
    meta_data = []
    # 解析文档数据,提取问题文本和元数据
    for doc in docs:
        texts.append(doc["question"])
        meta_data.append({
            "answer": doc["answer"],
            "category": doc["category"],
            "source": doc["source"]
        })
    insert_faq(texts, meta_data)


if __name__ == "__main__":
    # 程序入口:先创建索引再批量插入数据
    insert_from_file("faq.json")

查看 redis 数据内容

文档数据相似性检索

文档向量数据写入数据库后,接下来就是测试验证召回数据准确性,主要内容包括:

  • 用户提问后,将问题转换为向量,与向量数据库中的文档进行相似性匹配。
  • 召回与问题最相关的文档片段(如退款流程、配送延误规则),并返回给上层系统。

代码如下:

python 复制代码
from langchain_ollama import OllamaEmbeddings
from langchain_redis import RedisConfig, RedisVectorStore

def search_question(question):
    # 初始化 Embedding 模型
    embedding = OllamaEmbeddings(model="deepseek-r1:14b")

    # 配置Redis连接参数和索引名称
    config = RedisConfig(
        index_name="faq",
        redis_url="redis://localhost:6379",
    )

    # 创建Redis向量存储实例
    vector_store = RedisVectorStore(embedding, config=config)


    # 创建检索器,进行数据检索
    retriever = vector_store.as_retriever()
    documents = retriever.invoke(question)

    for document in documents:
        print(document.page_content)
        print(document.metadata)
        print("=================================")

if __name__ == "__main__":
    # 程序入口:先创建索引再批量插入数据
    search_question("在线支付取消订单后钱怎么返还给我呢")

执行结果如下

python 复制代码
20:10:56 httpx INFO   HTTP Request: POST http://127.0.0.1:11434/api/embed "HTTP/1.1 200 OK"
20:10:56 redisvl.index.index INFO   Index already exists, not overwriting.
20:10:56 httpx INFO   HTTP Request: POST http://127.0.0.1:11434/api/embed "HTTP/1.1 200 OK"
在线支付取消订单后钱怎么返还?
{'answer': '订单取消后,款项会在一个工作日内,直接返还到您的美团账户余额。', 'category': '在线支付问题', 'source': 'faq.html'}
=================================
在线支付的过程中,订单显示未支付成功,款项却被扣了,怎么办?
{'answer': '出现此问题,可能是银行/支付宝的数据没有即时传输至美团,请您不要担心,稍后刷新页面查看。 如半小时后仍显示"未付款",请先联系银行/支付宝客服,获取您扣款的交易号,然后致电美团外卖客服4008507777,我们会协助您解决。', 'category': '在线支付问题', 'source': 'faq.html'}
=================================
在线支付订单如何退款?
{'answer': '商家接单前,您可以直接取消订单,订单金额会自动退款到美团余额;商家接单后,您在点击"申请退款",在线申请。提交退款申请之后,商家有24小时处理您的退款申请。商家同意退款,或24小时内没有处理您的退款申请,您的支付金额会退款至您的美团余额。', 'category': '在线支付问题', 'source': 'faq.html'}
=================================
美团账户里的余额怎么提现?
{'answer': '余额可到美团网(meituan.com)------"我的美团→美团余额"里提取到您的银行卡或者支付宝账号,另外,余额也可直接用于支付外卖订单(限支持在线支付的商家)。', 'category': '在线支付问题', 'source': 'faq.html'}
=================================

构建提示词

把 用户问题 + 检索召回的上下文 拼接成一个高质量的 Prompt 送给大模型。

提示词示例:

python 复制代码
你是一个外卖公司的智能客服,接下来你将扮演一个专业客服的角色,对用户提出来的商品问题进行回答,一定要礼貌热情,如果用户提问与客服和商品无关的问题,礼貌委婉的表示拒绝或无法回答,只回答外卖服务相关的问题。

用户问题:
取消订单后多久能收到退款?

可用文档片段:
【文档片段1】
Q: 在线支付取消订单后钱怎么返还?
A: 订单取消后,款项会在一个工作日内,直接返还到您的美团账户余额。

【文档片段2】
Q: 怎么查看退款是否成功?
A: 退款会在一个工作日之内到美团账户余额,可在"账号管理------我的账号"中查看是否到账。

请基于以上信息,生成简洁明了的回答:

提示词代码如下:

python 复制代码
from langchain_ollama import OllamaEmbeddings
from langchain_redis import RedisConfig, RedisVectorStore
from langchain_core.prompts import PromptTemplate


def build_prompt(question: str):
    """
    使用向量检索技术查找相关文档,并通过 LangChain PromptTemplate 构造提示词。

    参数:
        question (str): 用户提出的问题。

    返回:
        str: 构造完成的提示词字符串。
    """
    # 初始化 Embedding 模型
    embedding = OllamaEmbeddings(model="deepseek-r1:14b")

    # Redis 配置
    config = RedisConfig(
        index_name="faq",
        redis_url="redis://localhost:6379",
    )

    # 创建 Redis 向量存储实例
    vector_store = RedisVectorStore(embedding, config=config)

    # 创建检索器,取 2 个最相关文档
    retriever = vector_store.as_retriever(search_kwargs={"k": 2})
    documents = retriever.invoke(question)

    # 组装 context
    context = "\n\n".join(
        f"【文档片段{i + 1}】\nQ: {doc.page_content}\nA: {doc.metadata.get('answer', '')}"
        for i, doc in enumerate(documents)
    )

    # 定义 Prompt 模板
    template = """
    你是一个外卖公司的智能客服,接下来你将扮演一个专业客服的角色,
    对用户提出来的商品问题进行回答,一定要礼貌热情,如果用户提问与客服和商品无关的问题,
    礼貌委婉的表示拒绝或无法回答,只回答外卖服务相关的问题。
    
    用户问题:
    {question}
    
    可用文档片段:
    {context}
    
    请基于以上信息,生成简洁明了的回答:
    """
    prompt_template = PromptTemplate(
        input_variables=["question", "context"], template=template
    )

    # 渲染提示词
    prompt = prompt_template.format(question=question, context=context)

    print("=== 提示词 ===")
    print(prompt)
    print("=================================")
    return prompt


if __name__ == "__main__":
    build_prompt("在线支付取消订单后钱怎么返还给我呢")

执行结果如下

python 复制代码
=== 提示词 ===

你是一个外卖公司的智能客服,接下来你将扮演一个专业客服的角色,
对用户提出来的商品问题进行回答,一定要礼貌热情,如果用户提问与客服和商品无关的问题,
礼貌委婉的表示拒绝或无法回答,只回答外卖服务相关的问题。

用户问题:
在线支付取消订单后钱怎么返还给我呢

可用文档片段:
【文档片段1】
Q: 在线支付取消订单后钱怎么返还?
A: 订单取消后,款项会在一个工作日内,直接返还到您的美团账户余额。

【文档片段2】
Q: 在线支付取消订单后钱怎么返还?
A: 订单取消后,款项会在一个工作日内,直接返还到您的美团账户余额。

请基于以上信息,生成简洁明了的回答:

=================================

RAG 系统实现

主要步骤

接下来,开始实现智能客服系统,主要包含以下8 个步骤:

  1. 创建提示词模板:模板包括 系统消息、消息占位符、人类消息。其中,系统消息用于设置 AI 的身份和当前业务场景;消息占位符用于传递聊天历史;人类消息则用来传递用户提问以及通过RAG检索到的上下文信息。
  2. 构建模型:使用 deepseek-r1:14b 模型。
  3. 创建输出解析器:创建一个 字符串输出解析器,用于结果输出。
  4. 构建检索器:连接 Weaviate 数据库,创建 WeaviateVectorStore 对象,并传入 文本嵌入对象、Weaviate 客户端对象、存储文本信息 key、集合名称。然后调用 WeaviateVectorStore.as_retriever() 方法生成检索器,并指定只返回一条最相关的文档数据。
  5. 创建记忆组件:构建记忆组件,并将历史对话信息保存在 customer_service_history.txt 中。
  6. 构建链:构建LCEL 链。链的后半部分较为直观,这里重点介绍前半部分。由于检索器需要接收一个字符串参数,我们使用字典进行构建:将检索器的输出信息通过 format_documents() 方法拼接成一个字符串,作为 context 参数,同时添加 query 参数,供下一个可运行组件使用。 这里利用了 RunnableParallel 的参数传递功能。之前介绍过,在LCEL 表达式中,使用字典结构包裹并通过管道符连接时,会自动被包装成 RunnableParallel。
  7. 调用链:使用 stream() 方法调用链,传入用户提问。stream() 可以实现流式输出,相比一次性返回结果,用户体验更好。
  8. 记忆保存:调用 save_context(),将对话记忆进行持久化。

代码编写

python 复制代码
from langchain_core.output_parsers import StrOutputParser
from langchain_ollama import OllamaEmbeddings, ChatOllama
from langchain_redis import RedisConfig, RedisVectorStore, RedisChatMessageHistory
from langchain_core.prompts import PromptTemplate
from langchain_core.runnables import RunnableWithMessageHistory

# ---------- 工具 ----------

def format_docs(docs):
    """
    把检索到的文档格式化成上下文字符串,用于提供给语言模型作为参考信息。

    参数:
        docs (list): 文档对象列表,每个对象应包含 page_content 和 metadata 属性。

    返回:
        str: 格式化后的字符串,包含多个文档片段及其问答内容。
    """
    return "\n\n".join(
        f"【文档片段{i + 1}】\n"
        f"Q: {doc.page_content}\n"
        f"A: {doc.metadata.get('answer', '')}"
        for i, doc in enumerate(docs)
    )


def extract_question(x: str | list) -> str:
    """
    从 RunnableWithMessageHistory 的输入中提取用户的纯文本问题。

    参数:
        x (str | list): 输入可以是字符串或消息对象列表。

    返回:
        str: 提取到的用户问题文本。
    """
    if isinstance(x, str):
        return x
    # x 是 list[HumanMessage]
    return x[-1].content


# ---------- 构建链 ----------

def build_chain():
    """
    构建一个基于检索增强生成(RAG)的对话链,用于智能客服问答。

    返回:
        Chain: 一个可调用的 LangChain 链对象,用于处理用户问题并生成回答。
    """
    # 初始化嵌入模型
    embedding = OllamaEmbeddings(model="deepseek-r1:14b")

    # 配置 Redis 向量存储
    config = RedisConfig(index_name="faq", redis_url="redis://localhost:6379")
    vector_store = RedisVectorStore(embedding, config=config)

    # 创建文档检索器,最多返回2个相关文档
    retriever = vector_store.as_retriever(search_kwargs={"k": 2})

    # 定义提示模板
    template = """
    你是一个外卖公司的智能客服,接下来你将扮演一个专业客服的角色,
    对用户提出来的商品问题进行回答,一定要礼貌热情,如果用户提问与客服和商品无关的问题,
    礼貌委婉的表示拒绝或无法回答,只回答外卖服务相关的问题。
    
    用户问题:
    {question}
    
    可用文档片段:
    {context}
    
    请基于以上信息,生成简洁明了的回答:
    """
    prompt = PromptTemplate.from_template(template)

    # 初始化语言模型和输出解析器
    llm = ChatOllama(model="deepseek-r1:14b", reasoning=False)
    parser = StrOutputParser()

    # 构建处理链:提取问题 -> 检索文档 -> 格式化上下文 -> 拼接提示 -> 调用模型 -> 解析输出
    chain = (
        {
            "context": extract_question | retriever | format_docs,
            "question": extract_question,
        }
        | prompt
        | llm
        | parser
    )
    return chain


# ---------- 交互 ----------

def main():
    """
    主函数,启动智能客服交互系统。
    """
    # 构建对话链
    chain = build_chain()

    # 初始化 Redis 聊天历史记录
    history = RedisChatMessageHistory(session_id='rag', redis_url='redis://localhost:6379/0')

    # 将对话链包装为带历史记录的可运行对象
    runnable = RunnableWithMessageHistory(
        chain,
        get_session_history=lambda: history
    )

    # 启动交互循环
    print(">>> 欢迎使用外卖智能客服系统,输入 quit/exit 退出 <<<")
    while True:
        try:
            user = input("\n您:").strip()
        except (KeyboardInterrupt, EOFError):
            print("\nbye~")
            break
        if user.lower() in {"quit", "exit", "q"}:
            print("客服:祝您生活愉快,再见!")
            break
        answer = runnable.invoke(user)      # 自动把 user 包装成 HumanMessage
        print("客服:", answer)


if __name__ == "__main__":
    main()

执行效果如下:

python 复制代码
>>> 欢迎使用外卖智能客服系统,输入 quit/exit 退出 <<<
您:今天天气怎么样
客服: 您好,我是外卖公司的智能客服。关于天气问题,我无法提供相关信息。如需查询天气,请您开启天气应用查看实时情况哦!如果有任何外卖服务相关的问题,我会很乐意为您提供帮助。

您:在线支付取消订单后钱怎么返还给我呢
客服: 您好,关于在线支付取消订单后的退款问题,请您放心,订单取消后,款项会在一个工作日内直接返还到您的美团账户余额。如有任何疑问或需要进一步帮助,请随时联系我们。感谢您的理解与支持!

您:重复回答
客服: 您好,关于在线支付取消订单后的退款问题,请您放心,订单取消后,款项会在一个工作日内直接返还到您的美团账户余额。如有任何疑问或需要进一步帮助,请随时联系我们。感谢您的理解与支持!
相关推荐
我送炭你添花14 小时前
Pelco KBD300A 模拟器:06+6.键盘按键扩展、LCD 优化与指示灯集成(二次迭代)
python·自动化·计算机外设·运维开发
猫头虎14 小时前
如何解决pip报错 import pandas as pd ModuleNotFoundError: No module named ‘pandas‘问题
java·python·scrapy·beautifulsoup·pandas·pip·scipy
飞天小蜈蚣14 小时前
python-django_ORM的基本操作
android·python·django
大数据小禅14 小时前
【AI大模型】大模型预训练从零到一:深入理解大语言模型的训练之路
人工智能·语言模型·自然语言处理
七分辣度14 小时前
Python给PDF添加水印(极速版)
python·pdf
DP+GISer14 小时前
02基于pytorch的深度学习遥感地物分类全流程实战教程(包含遥感深度学习数据集制作与大图预测)-实践篇-python基础与遥感深度学习境配置
人工智能·pytorch·python·深度学习·图像分割·遥感·地物分类
万俟淋曦14 小时前
【论文速递】2025年第49周(Nov-30-Dec-06)(Robotics/Embodied AI/LLM)
人工智能·深度学习·机器学习·机器人·大模型·论文·具身智能
倔强的小石头_14 小时前
Python 从入门到实战(十八):学生成绩系统高级功能实战(实时通知与数据看板)
开发语言·python
weixin_4624462314 小时前
Python 使用 pypdf 按指定页码范围批量拆分 PDF(分章节)
python·pdf·pdf分割