Skip to content

第七阶段:RAG + Agent 智能客服 ​

一、思路 ​

把第五阶段的知识库包装成第三个工具 search_manual:用 index.as_retriever 只检索,把说明书原文交回给模型,由模型自己组织回答。

这样模型可以自主决定:查订单(query_order)、开工单(create_ticket)、还是查说明书(search_manual)。

关键认识:RAG 也可以作为 Agent 的一个工具。 检索只是"取资料",什么时候取、取来怎么用,交给模型决定。

二、07_support_agent.py 完整代码 ​

以下代码与仓库中的文件一致(运行前把 model_name 改成你 LM Studio 中的 Embedding 模型名,并确保 kb_storage 是第五阶段建好的索引):

python
import os, json
from dotenv import load_dotenv
from openai import OpenAI
from llama_index.core import Settings, StorageContext, load_index_from_storage
from llama_index.embeddings.openai_like import OpenAILikeEmbedding

load_dotenv()

client = OpenAI(api_key=<redacted>("LLM_API_KEY"), base_url=os.getenv("LLM_BASE_URL"))
MODEL = os.getenv("LLM_MODEL")

Settings.embed_model = OpenAILikeEmbedding(model_name="这里填你的 Embedding 模型名",
                          api_base="http://localhost:1234/v1", api_key="<redacted>")

index = load_index_from_storage(StorageContext.from_defaults(persist_dir="./kb_storage"))
retriever = index.as_retriever(similarity_top_k=3)  # 只检索,不让 LlamaIndex 自己回答

def search_manual(question):
    """在产品说明书里检索相关段落,原文交回给模型"""
    nodes = retriever.retrieve(question)
    return "\n---\n".join(n.node.get_content() for n in nodes)

# 1. 真正干活的函数(这里用假数据模拟订单系统)
ORDERS = {"1001": "已发货,顺丰 SF123456,预计明天到", "1002": "待付款"}

def query_order(order_id):
    return ORDERS.get(order_id, "查无此订单")

def create_ticket(order_id, problem):
    return f"工单已创建,编号 T{order_id}01,问题:{problem},客服 24 小时内联系您"

# 2. 函数说明书:模型只看得到这个,所以描述要写清楚
tools = [
    {"type": "function", "function": {
        "name": "query_order",
        "description": "根据订单号查询订单状态和物流",
        "parameters": {"type": "object",
                       "properties": {"order_id": {"type": "string", "description": "订单号"}},
                       "required": ["order_id"]}}},
    {"type": "function", "function": {
        "name": "create_ticket",
        "description": "用户反映商品质量问题、要求售后时,创建售后工单",
        "parameters": {"type": "object",
                       "properties": {"order_id": {"type": "string"},
                                      "problem": {"type": "string", "description": "问题描述"}},
                       "required": ["order_id", "problem"]}}},
    {"type": "function", "function": {
        "name": "search_manual",
        "description": "查询折叠键盘的产品说明书,用于回答配对、指示灯、充电、保修等产品使用问题",
        "parameters": {"type": "object",
                       "properties": {"question": {"type": "string", "description": "要查的问题"}},
                       "required": ["question"]}}},
]

FUNCS = {"query_order": query_order, "create_ticket": create_ticket, "search_manual": search_manual}

messages = [{"role": "system", "content": "你是折叠键盘的电商客服。订单问题用 query_order,售后用 create_ticket,产品使用问题必须先用 search_manual 查说明书,只根据查到的内容回答,查不到就如实说不知道,不要编造"}]

while True:
    q = input("\n你:")
    if q == "exit":
        break
    messages.append({"role": "user", "content": q})

    # 3. 循环:模型可能连续调用好几次工具,直到给出最终回答
    while True:
        resp = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
        msg = resp.choices[0].message
        messages.append(msg)  # 模型的回复(包括它的调用请求)也要加进聊天记录
        if not msg.tool_calls:  # 没有要调用的工具,说明这就是最终回答
            print("客服:", msg.content)
            break
        for call in msg.tool_calls:  # 逐个执行模型要求调用的函数
            args = json.loads(call.function.arguments)
            print(f"  [调用 {call.function.name},参数 {args}]")
            result = FUNCS[call.function.name]
            # 把执行结果交回给模型,tool_call_id 用来对应是哪一次调用
            messages.append({"role": "tool", "tool_call_id": call.id, "content": result})

与第六阶段相比,新增了三处:

  • 顶部的检索部分:加载 kb_storage 索引,as_retriever(similarity_top_k=3) 只返回最相关的 3 段原文,用 --- 拼接后交回给模型;
  • tools 里的第三个说明书:search_manual,description 列出了它能回答的问题类型(配对、指示灯、充电、保修……);
  • system 提示:明确告诉模型三个工具分别什么时候用,并要求查不到就如实说。

三、常见错误(实际踩过的坑) ​

(a) tools 里加了 search_manual,但 FUNCS 没登记 ​

模型调用 search_manual 时,FUNCS[call.function.name] 找不到这个键,程序直接 KeyError 崩溃。新增工具要改两处:tools(给模型看的说明书)和 FUNCS(名字到函数的映射)。

(b) system 提示没更新 ​

模型不知道什么时候该查说明书,可能凭自己的知识瞎答。新增工具后,要同步在 system 提示里说明用途和规则。

(c) 只检索时不需要 Settings.llm ​

用 retriever 只做检索,生成回答由 client 负责,所以只需设置 Settings.embed_model,不需要 Settings.llm。并且 Embedding 模型必须与建索引时是同一个——问题向量和文档向量要在同一个"向量空间"里才能比较。

四、实测结果(真实运行数据) ​

你:键盘怎么连手机

text
[调用 search_manual,参数 {'question': '折叠键盘怎么连接手机(蓝牙配对步骤)'}]

客服给出步骤:开机 → Fn+A / S / D 选择 BT1~BT3 并点按 Connect → 手机搜索 BOW 连接;并说明键盘使用 HID 协议,支持 iOS / Android / Windows / MacOS / HarmonyOS。

注意模型把"键盘怎么连手机"改写成了更完整的检索问题,提高了检索命中率。

你:订单 1001 的键盘连不上蓝牙,怎么办?

模型同时调用了 query_order {'order_id': '1001'} 和 search_manual,然后:

  • 发现订单状态是"预计明天到",与"已经连不上"矛盾,主动请用户确认;
  • 给出配对步骤;
  • 说明书里没有专门的故障排查内容,不乱猜;
  • 提出可以帮忙登记售后工单。

你:能连 PS5 吗?

调用 search_manual 后,模型说明:说明书只列出了上述系统,没有提到 PS5,查不到就不下结论。

五、要点 ​

  1. 并行多工具:一句话涉及订单和产品问题时,模型同时调用多个工具。
  2. 发现矛盾主动确认:模型会把不同工具的结果放在一起推理,而不是机械拼接。
  3. 防编造:system 提示 + 只根据检索原文回答,查不到就如实说。

下一节预告 ​

用 LangGraph 重写客服 Agent:用有状态的流程图管理对话,加入"转人工"等分支。

👉 下一阶段:LangGraph 重写客服 Agent