第 11 章:综合实战 ------ 智能客服 Agent

文章目录
-
- [第 11 章:综合实战 ------ 智能客服 Agent](#第 11 章:综合实战 —— 智能客服 Agent)
-
- [11.1 本章目标](#11.1 本章目标)
- [11.2 核心概念](#11.2 核心概念)
- [11.3 实战:完整智能客服 Agent](#11.3 实战:完整智能客服 Agent)
- [11.4 API 速查](#11.4 API 速查)
- [11.5 错误与避坑指南](#11.5 错误与避坑指南)
-
- [坑 1:意图识别路由覆盖不全](#坑 1:意图识别路由覆盖不全)
- [坑 2:人工审批超时处理](#坑 2:人工审批超时处理)
- [坑 3:知识检索结果为空](#坑 3:知识检索结果为空)
- [11.6 最佳实践总结](#11.6 最佳实践总结)
11.1 本章目标
学完本章你将能够:
- 综合运用前 10 章知识,从零构建完整智能客服 Agent
- 理解生产级 Agent 的系统架构设计
11.2 核心概念
系统架构
#mermaid-svg-GgMInVt8hbTGtyYn{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-GgMInVt8hbTGtyYn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GgMInVt8hbTGtyYn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GgMInVt8hbTGtyYn .error-icon{fill:#552222;}#mermaid-svg-GgMInVt8hbTGtyYn .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GgMInVt8hbTGtyYn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GgMInVt8hbTGtyYn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GgMInVt8hbTGtyYn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GgMInVt8hbTGtyYn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GgMInVt8hbTGtyYn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GgMInVt8hbTGtyYn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GgMInVt8hbTGtyYn .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GgMInVt8hbTGtyYn .marker.cross{stroke:#333333;}#mermaid-svg-GgMInVt8hbTGtyYn svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GgMInVt8hbTGtyYn p{margin:0;}#mermaid-svg-GgMInVt8hbTGtyYn .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-GgMInVt8hbTGtyYn .cluster-label text{fill:#333;}#mermaid-svg-GgMInVt8hbTGtyYn .cluster-label span{color:#333;}#mermaid-svg-GgMInVt8hbTGtyYn .cluster-label span p{background-color:transparent;}#mermaid-svg-GgMInVt8hbTGtyYn .label text,#mermaid-svg-GgMInVt8hbTGtyYn span{fill:#333;color:#333;}#mermaid-svg-GgMInVt8hbTGtyYn .node rect,#mermaid-svg-GgMInVt8hbTGtyYn .node circle,#mermaid-svg-GgMInVt8hbTGtyYn .node ellipse,#mermaid-svg-GgMInVt8hbTGtyYn .node polygon,#mermaid-svg-GgMInVt8hbTGtyYn .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-GgMInVt8hbTGtyYn .rough-node .label text,#mermaid-svg-GgMInVt8hbTGtyYn .node .label text,#mermaid-svg-GgMInVt8hbTGtyYn .image-shape .label,#mermaid-svg-GgMInVt8hbTGtyYn .icon-shape .label{text-anchor:middle;}#mermaid-svg-GgMInVt8hbTGtyYn .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-GgMInVt8hbTGtyYn .rough-node .label,#mermaid-svg-GgMInVt8hbTGtyYn .node .label,#mermaid-svg-GgMInVt8hbTGtyYn .image-shape .label,#mermaid-svg-GgMInVt8hbTGtyYn .icon-shape .label{text-align:center;}#mermaid-svg-GgMInVt8hbTGtyYn .node.clickable{cursor:pointer;}#mermaid-svg-GgMInVt8hbTGtyYn .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-GgMInVt8hbTGtyYn .arrowheadPath{fill:#333333;}#mermaid-svg-GgMInVt8hbTGtyYn .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-GgMInVt8hbTGtyYn .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-GgMInVt8hbTGtyYn .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GgMInVt8hbTGtyYn .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-GgMInVt8hbTGtyYn .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GgMInVt8hbTGtyYn .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-GgMInVt8hbTGtyYn .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-GgMInVt8hbTGtyYn .cluster text{fill:#333;}#mermaid-svg-GgMInVt8hbTGtyYn .cluster span{color:#333;}#mermaid-svg-GgMInVt8hbTGtyYn div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-GgMInVt8hbTGtyYn .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-GgMInVt8hbTGtyYn rect.text{fill:none;stroke-width:0;}#mermaid-svg-GgMInVt8hbTGtyYn .icon-shape,#mermaid-svg-GgMInVt8hbTGtyYn .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GgMInVt8hbTGtyYn .icon-shape p,#mermaid-svg-GgMInVt8hbTGtyYn .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-GgMInVt8hbTGtyYn .icon-shape .label rect,#mermaid-svg-GgMInVt8hbTGtyYn .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GgMInVt8hbTGtyYn .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-GgMInVt8hbTGtyYn .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-GgMInVt8hbTGtyYn :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 产品咨询
售后投诉
闲聊
需要操作
退款操作
普通投诉
用户
意图识别
知识检索
投诉处理
闲聊回复
工具调用
回复生成
需要审批?
人工审批
流式输出
注意 :图中的「工具调用」分支在实际代码中整合到了
order_query和faq节点中(通过调用@tool装饰的函数实现)。如果希望显式分离工具调用,可以在route_by_intent中增加tools分支。
架构说明:整个系统包含 7 种节点类型,覆盖了前面学到的所有核心知识点:
- 意图识别:使用条件边路由(第 3 章)
- 知识检索:使用工具调用(第 4 章)
- 人工审批:使用 interrupt() 中断机制(第 6 章)
- 回复生成:使用 LLM 调用(第 4 章)
- 流式输出:使用 messages 模式(第 10 章)
- 会话持久化:使用 Checkpoint(第 5 章)
- 用户偏好:使用 Store 长期记忆(第 8 章)
11.3 实战:完整智能客服 Agent
python
from typing import TypedDict, Annotated, Literal
import operator
from langgraph.graph import StateGraph, START, END, add_messages
from langgraph.checkpoint.memory import MemorySaver
from langgraph.types import interrupt, Command
from langgraph.config import get_stream_writer
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, SystemMessage, ToolMessage
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
# ============================================
# 1. 定义工具
# ============================================
@tool
def query_order(order_id: str) -> str:
"""查询订单状态。输入订单号,返回订单状态。"""
# 模拟订单查询(实际项目中替换为数据库查询)
orders = {
"123": "订单#123:已发货,预计明天送达",
"456": "订单#456:已签收",
"789": "订单#789:退款处理中",
}
return orders.get(order_id, f"未找到订单#{order_id}")
@tool
def process_refund(order_id: str) -> str:
"""处理退款。输入订单号,执行退款操作。⚠️ 此操作需要审批。"""
return f"订单#{order_id} 退款已提交,将在 3-5 个工作日内退回原支付账户。"
@tool
def search_faq(query: str) -> str:
"""搜索常见问题。输入问题关键词,返回相关答案。"""
faq = {
"退货": "退货流程:1. 在订单页面申请退货 2. 打印退货标签 3. 寄回商品 4. 退款到账",
"发货": "我们通常在 24 小时内发货,物流信息可在订单详情页查看",
"优惠": "最新优惠请查看首页活动页面,新用户首单享 9 折优惠",
}
for key, value in faq.items():
if key in query:
return value
return "请咨询在线客服获取更多帮助,或拨打 400-123-4567。"
# ============================================
# 2. 定义 State
# ============================================
class CustomerServiceState(TypedDict):
"""智能客服的完整 State"""
messages: Annotated[list[BaseMessage], add_messages]
intent: str # 用户意图
knowledge: str # 检索到的知识
needs_approval: bool # 是否需要审批
approved: bool # 是否已批准
# ============================================
# 3. 定义节点
# ============================================
def classify_intent(state: CustomerServiceState) -> dict:
"""
意图识别节点:分析用户输入,判断意图类型。
实际项目中这里应该调用 LLM 做意图分类。
这里用关键词匹配做简单演示。
"""
writer = get_stream_writer()
writer({"status": "正在分析用户意图..."})
last_msg = state["messages"][-1].content
if "退款" in last_msg or "退货" in last_msg:
intent = "refund"
elif "订单" in last_msg or "查询" in last_msg or "物流" in last_msg:
intent = "order_query"
elif "你好" in last_msg or "谢谢" in last_msg or "再见" in last_msg:
intent = "chitchat"
else:
intent = "faq"
print(f" [意图识别] 用户意图: {intent}")
return {"intent": intent}
def route_by_intent(state: CustomerServiceState) -> Literal["order_query", "refund", "faq", "chitchat"]:
"""根据意图路由到不同处理节点"""
return state["intent"]
def handle_order_query(state: CustomerServiceState) -> dict:
"""处理订单查询:调用订单查询工具"""
writer = get_stream_writer()
writer({"status": "正在查询订单..."})
# 从用户消息中提取订单号(简化处理,实际应让 LLM 提取)
result = query_order.invoke({"order_id": "123"})
return {
"knowledge": result,
"messages": [ToolMessage(content=result, tool_call_id="order_query")],
}
def handle_refund(state: CustomerServiceState) -> dict:
"""处理退款请求:标记需要审批"""
writer = get_stream_writer()
writer({"status": "检测到退款请求,需要人工审批..."})
return {"needs_approval": True, "knowledge": "用户请求退款处理"}
def handle_faq(state: CustomerServiceState) -> dict:
"""处理 FAQ 搜索"""
writer = get_stream_writer()
writer({"status": "正在搜索知识库..."})
result = search_faq.invoke({"query": state["messages"][-1].content})
return {
"knowledge": result,
"messages": [ToolMessage(content=result, tool_call_id="faq_search")],
}
def handle_chitchat(state: CustomerServiceState) -> dict:
"""处理闲聊"""
return {"knowledge": "用户在进行闲聊对话"}
def human_approval(state: CustomerServiceState) -> dict:
"""
人工审批节点:使用 interrupt() 暂停执行,等待人类审批。
这是第 6 章 Human-in-the-Loop 的核心应用。
"""
writer = get_stream_writer()
writer({"status": "⏸️ 等待人工审批..."})
# 中断等待审批,展示审批信息
decision = interrupt({
"question": "用户请求退款,是否批准?",
"order_id": "123",
"amount": "¥299.00",
"reason": state["messages"][-1].content,
"options": ["批准", "拒绝"],
})
writer({"status": f"审批结果: {decision}"})
print(f" [人工审批] 审批结果: {decision}")
return {"approved": decision == "批准"}
def generate_response(state: CustomerServiceState) -> dict:
"""
生成最终回复:根据所有前置节点的处理结果,生成最终回复。
这是整个流程的终点,汇总所有信息后回复用户。
"""
writer = get_stream_writer()
writer({"status": "正在生成回复..."})
if state.get("needs_approval") and not state.get("approved"):
reply = "很抱歉,您的退款请求未被批准。如有疑问请联系人工客服:400-123-4567。"
elif state.get("knowledge"):
reply = f"您好!{state['knowledge']}\n\n还有其他可以帮助您的吗?"
else:
reply = "您好!我是智能客服小助手,请问有什么可以帮助您的吗?"
return {"messages": [AIMessage(content=reply)]}
# ============================================
# 4. 构建图
# ============================================
checkpointer = MemorySaver()
builder = StateGraph(CustomerServiceState)
# 注册所有节点
builder.add_node("classify", classify_intent)
builder.add_node("order_query", handle_order_query)
builder.add_node("refund", handle_refund)
builder.add_node("faq", handle_faq)
builder.add_node("chitchat", handle_chitchat)
builder.add_node("approval", human_approval)
builder.add_node("generate", generate_response)
# 添加边
builder.add_edge(START, "classify")
# 条件路由:根据意图分发
builder.add_conditional_edges("classify", route_by_intent, {
"order_query": "order_query",
"refund": "refund",
"faq": "faq",
"chitchat": "chitchat",
})
# 各处理节点 → 审批/生成
builder.add_edge("order_query", "generate")
builder.add_edge("faq", "generate")
builder.add_edge("chitchat", "generate")
# 退款需要审批:refund → approval → generate
builder.add_edge("refund", "approval")
builder.add_edge("approval", "generate")
# 生成回复 → 结束
builder.add_edge("generate", END)
graph = builder.compile(checkpointer=checkpointer)
# ============================================
# 5. 测试完整流程
# ============================================
def run_customer_service(question: str, thread_id: str = "default"):
"""
运行智能客服。
如果遇到退款请求,会触发 interrupt,需要手动恢复。
"""
config = {"configurable": {"thread_id": thread_id}}
print(f"\n{'='*60}")
print(f"用户: {question}")
print(f"{'='*60}")
try:
result = graph.invoke(
{"messages": [HumanMessage(content=question)], "needs_approval": False, "approved": False},
config=config,
)
print(f"AI: {result['messages'][-1].content}")
except Exception:
print("⏸️ 图已暂停,等待人工审批...")
# 模拟审批:批准
print("\n>>> 模拟人工审批:批准 <<<")
result = graph.invoke(
Command(resume="批准"),
config=config,
)
print(f"AI: {result['messages'][-1].content}")
# 测试不同场景
run_customer_service("你好,我想查询订单", thread_id="user-001")
run_customer_service("如何退货?", thread_id="user-001")
run_customer_service("我要退款!", thread_id="user-002")
run_customer_service("今天天气真好", thread_id="user-002")
完整流程解析:
- 用户输入 →
classify_intent节点分析意图 - 根据意图路由到不同的处理节点
- 如果是退款 → 触发
human_approval节点,等待人工审批 - 所有路径最终汇入
generate_response生成回复 - 使用
MemorySaver持久化,支持多会话隔离
11.4 API 速查
本章不新增 API,使用到的 API 来自前 10 章:StateGraph, add_node, add_edge, add_conditional_edges, MemorySaver, interrupt(), Command(resume=), get_stream_writer(), @tool, ToolMessage
11.5 错误与避坑指南
坑 1:意图识别路由覆盖不全
python
# ❌ 错误:只覆盖了部分意图,未匹配的意图没有兜底
def route(state: State) -> Literal["a", "b"]:
return state["intent"] # 如果 intent 是 "c" 会报错
# ✅ 正确:添加兜底分支
def route(state: State) -> Literal["a", "b", "fallback"]:
if state["intent"] in ("a", "b"):
return state["intent"]
return "fallback"
坑 2:人工审批超时处理
python
# ❌ 错误:没有超时处理,用户不审批时图一直挂起
# ✅ 正确:设置超时 + 降级策略
# 在实际应用中,可以通过外部定时器或队列机制实现超时处理
坑 3:知识检索结果为空
python
# ❌ 错误:检索为空时直接返回空字符串
def search(state: State) -> dict:
return {"knowledge": do_search(state["query"])} # 可能是空字符串
# ✅ 正确:检索为空时降级为通用回复
def search(state: State) -> dict:
result = do_search(state["query"])
if not result:
result = "未找到相关信息,建议联系人工客服。"
return {"knowledge": result}
11.6 最佳实践总结
- 每个节点独立可测试:不依赖全局状态,可以单独 invoke 测试
- 使用条件边实现意图路由,Command 实现动态跳转:各司其职
- 关键操作(退款)必须经过人工审批:安全第一
- 流式输出提升用户体验 :用
get_stream_writer()发送进度通知 - 检索为空时优雅降级:不要返回空结果,提供兜底回复