Skip to content

从零到进阶做 Agent(十一)综合实战:智能客服 Agent

🕒 Published at:

前面十篇的零件到这里全部拼起来。我们来做一个「智扫通机器人智能客服」——一个扫地机器人品牌的客服 Agent:能查产品知识库(RAG)、查天气/用户信息(工具)、记住多轮对话(记忆),还带日志监控(中间件)。这就是一个麻雀虽小五脏俱全的真实 Agent。顶部可切换 Python / Node.js。

整体设计

一句话概括架构:一个 Agent + 三样能力 + 记忆

Agent 的大脑是大模型,它手里握着三类工具:search_kb(把第六、七篇的 RAG 封装成一个工具,用来查产品资料)、get_weather(外部接口示例)、get_user_info(查用户信息示例)。中间件负责日志监控,checkpointer 负责按用户记住对话。用户问什么,Agent 自己决定调哪个工具。

关键思路:把 RAG 变成 Agent 的一个工具。 之前 RAG 是主流程,现在它降格成 Agent 手里的一件「武器」——遇到产品问题才掏出来用。这是 RAG 和 Agent 结合的标准姿势。

Middleware 中间件:日志 / 监控 / 限流(包裹整个运行)用户Agent(LLM)自主决定调哪个工具search_kb(RAG 检索)get_weatherget_user_info记忆 Checkpointer按 thread_id 存取按需调用存/取历史

第一步:把 RAG 封装成工具

复用前几篇的检索链,包成一个函数工具。工具的说明要写清楚「什么时候用它」,模型才会在该用的时候调它。

python
import os
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_core.tools import tool

DASHSCOPE = "https://dashscope.aliyuncs.com/compatible-mode/v1"
KEY = os.getenv("DASHSCOPE_API_KEY")

# 启动时把产品文档建成检索器(真实项目会用持久化的 Chroma)
embeddings = OpenAIEmbeddings(model="text-embedding-v3", api_key=KEY, base_url=DASHSCOPE)
docs = TextLoader("扫地机器人100问.txt", encoding="utf-8").load()
chunks = RecursiveCharacterTextSplitter(chunk_size=300, chunk_overlap=50).split_documents(docs)
_retriever = InMemoryVectorStore.from_documents(chunks, embeddings).as_retriever(search_kwargs={"k": 3})

@tool
def search_kb(query: str) -> str:
    """当用户询问扫地机器人的功能、使用、故障、保养等产品问题时,用它检索知识库。"""
    results = _retriever.invoke(query)
    return "\n\n".join(d.page_content for d in results) or "知识库中没有相关资料。"
javascript
import { ChatOpenAI, OpenAIEmbeddings } from "@langchain/openai";
import { TextLoader } from "@langchain/classic/document_loaders/fs/text";
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
import { MemoryVectorStore } from "@langchain/classic/vectorstores/memory";
import { tool } from "@langchain/core/tools";
import { z } from "zod";

const DASHSCOPE = "https://dashscope.aliyuncs.com/compatible-mode/v1";
const KEY = process.env.DASHSCOPE_API_KEY;

// 启动时把产品文档建成检索器
const embeddings = new OpenAIEmbeddings({ model: "text-embedding-v3", apiKey: KEY, configuration: { baseURL: DASHSCOPE } });
const docs = await new TextLoader("扫地机器人100问.txt").load();
const chunks = await new RecursiveCharacterTextSplitter({ chunkSize: 300, chunkOverlap: 50 }).splitDocuments(docs);
const retriever = (await MemoryVectorStore.fromDocuments(chunks, embeddings)).asRetriever(3);

const searchKb = tool(
  async ({ query }) => {
    const results = await retriever.invoke(query);
    return results.map((d) => d.pageContent).join("\n\n") || "知识库中没有相关资料。";
  },
  {
    name: "search_kb",
    description: "当用户询问扫地机器人的功能、使用、故障、保养等产品问题时,用它检索知识库。",
    schema: z.object({ query: z.string() }),
  }
);

第二步:其它工具

再加两个示例工具,代表「调外部接口」和「查内部数据」。真实项目里换成你的天气 API、用户系统即可。

python
@tool
def get_weather(city: str) -> str:
    """查询某城市天气,用于判断是否适合扫地/拖地。"""
    return f"{city}今天晴,湿度低,适合拖地。"

@tool
def get_user_info(user_id: str) -> str:
    """根据用户 ID 查询其购买的机型和保修状态。"""
    return f"用户 {user_id}:机型 X20 Pro,在保修期内。"
javascript
const getWeather = tool(async ({ city }) => `${city}今天晴,湿度低,适合拖地。`, {
  name: "get_weather",
  description: "查询某城市天气,用于判断是否适合扫地/拖地。",
  schema: z.object({ city: z.string() }),
});

const getUserInfo = tool(async ({ userId }) => `用户 ${userId}:机型 X20 Pro,在保修期内。`, {
  name: "get_user_info",
  description: "根据用户 ID 查询其购买的机型和保修状态。",
  schema: z.object({ userId: z.string() }),
});

第三步:中间件(日志监控)

沿用上一篇的做法,加一个记录模型调用和工具调用的中间件。

python
from langchain.agents.middleware import before_model, wrap_tool_call

@before_model
def log_model(state, runtime):
    print(f"[LOG] 调用模型,消息数={len(state['messages'])}")
    return None

@wrap_tool_call
def monitor_tool(request, handler):
    print(f"[LOG] 调用工具:{request.tool_call['name']} 参数={request.tool_call['args']}")
    return handler(request)
javascript
import { createMiddleware } from "langchain";

const logging = createMiddleware({
  name: "Logging",
  beforeModel: (state) => {
    console.log(`[LOG] 调用模型,消息数=${state.messages.length}`);
    return undefined;
  },
  wrapToolCall: async (request, handler) => {
    console.log(`[LOG] 调用工具:${request.toolCall.name} 参数=${JSON.stringify(request.toolCall.args)}`);
    return handler(request);
  },
});

第四步:组装 Agent 并加记忆

把模型、三个工具、中间件、系统提示词、checkpointer 全部交给 create_agent。checkpointer 让它按 thread_id 记住每个用户的对话。

python
from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver

model = ChatOpenAI(model="qwen-plus", api_key=KEY, base_url=DASHSCOPE, temperature=0)

SYSTEM = (
    "你是「智扫通」扫地机器人的智能客服。"
    "产品相关问题必须调用 search_kb 查证后再回答,不要凭空作答;"
    "涉及用户机型/保修用 get_user_info;涉及清洁建议可用 get_weather。"
    "回答简洁、亲切。"
)

agent = create_agent(
    model=model,
    tools=[search_kb, get_weather, get_user_info],
    system_prompt=SYSTEM,
    middleware=[log_model, monitor_tool],
    checkpointer=InMemorySaver(),
)

def chat(text: str, user: str):
    cfg = {"configurable": {"thread_id": user}}
    result = agent.invoke({"messages": [{"role": "user", "content": text}]}, cfg)
    return result["messages"][-1].content

if __name__ == "__main__":
    print(chat("我的滤网多久洗一次?", user="u1"))
    print(chat("我是用户 U8899,我的机器还在保修吗?", user="u1"))
    print(chat("那我刚才问的滤网,你还记得吗?", user="u1"))   # 记忆生效
javascript
import { createAgent } from "langchain";
import { MemorySaver } from "@langchain/langgraph";

const model = new ChatOpenAI({ model: "qwen-plus", apiKey: KEY, temperature: 0, configuration: { baseURL: DASHSCOPE } });

const SYSTEM =
  "你是「智扫通」扫地机器人的智能客服。" +
  "产品相关问题必须调用 search_kb 查证后再回答,不要凭空作答;" +
  "涉及用户机型/保修用 get_user_info;涉及清洁建议可用 get_weather。" +
  "回答简洁、亲切。";

const agent = createAgent({
  model,
  tools: [searchKb, getWeather, getUserInfo],
  prompt: SYSTEM,
  middleware: [logging],
  checkpointer: new MemorySaver(),
});

async function chat(text, user) {
  const cfg = { configurable: { thread_id: user } };
  const result = await agent.invoke({ messages: [{ role: "user", content: text }] }, cfg);
  return result.messages[result.messages.length - 1].content;
}

console.log(await chat("我的滤网多久洗一次?", "u1"));
console.log(await chat("我是用户 U8899,我的机器还在保修吗?", "u1"));
console.log(await chat("那我刚才问的滤网,你还记得吗?", "u1"));   // 记忆生效

跑起来观察日志,你会看到 Agent 的决策过程:问滤网 → 调 search_kb;问保修 → 调 get_user_info;第三句考验记忆 → 它记得前面聊过滤网。三样能力 + 记忆,在一个 Agent 里协同工作。

接上界面

要变成能用的产品,把第八篇的界面接上即可:Streamlit 里把 st.session_state.chain 换成这个 agent、用 write_stream(agent.stream(...)) 流式展示;Node 那套 Express + HTML 里,把 /api/askragChain.stream 换成 agent.stream。核心逻辑一行不用改——这就是分层的好处。

小结与预告

你完整搭出了一个生产雏形级的客服 Agent:RAG 作工具 + 多工具编排 + 中间件 + 记忆,并理解了「RAG 降格为 Agent 的一件工具」这个关键设计。到这里,从「调用一次模型」到「一个会查资料、会调工具、有记忆的智能体」,整条路你都亲手走通了。

最后一篇《结语与进阶方向》,我们回顾整个系列的知识地图,聊聊从 Demo 到生产还差哪些东西(评估、可观测、成本、安全),以及往下深入可以看什么。