需收集信息
意图分类
问候
一般咨询
订单问题
投诉
转人工
有缺失信息
信息齐全
有缺失信息
信息齐全
触发中断
人工确认后继续
仍有缺失信息
信息已补齐
开始
入口路由
信息收集
意图分类
处理问候
处理咨询
处理订单
处理投诉
转人工请求
结束
升级至人工
⏸ 暂停:等待人工接入
发送转接消息
结束,等待用户回复
基于LangGraph搭建 多轮对话客户支持机器人 项目示例
-
- 项目结构
- [1. 状态设计](#1. 状态设计)
- [2. 节点函数(模拟真实业务)](#2. 节点函数(模拟真实业务))
- [3. 构建图](#3. 构建图)
- [4. 运行与多轮对话演示](#4. 运行与多轮对话演示)
- [5. 项目说明](#5. 项目说明)
-
- 跨轮对话的持久化
- 人在回路(Human-in-the-Loop)
- [如何与真实 LLM 集成](#如何与真实 LLM 集成)
- 扩展方向
下面基于 LangGraph 实现一个完整的多轮对话客户支持机器人。这个项目会展示信息收集、意图路由、条件循环、工单创建、人工升级等多步骤流程。
项目结构
customer_support_bot/
├── bot.py # 主程序,包含图定义和执行逻辑
└── requirements.txt # 依赖
你可以直接复制代码运行,所有核心逻辑都在 bot.py 中。
1. 状态设计
python
from typing import TypedDict, Annotated, List
import operator
class CustomerState(TypedDict):
messages: Annotated[List[dict], operator.add] # 完整对话历史,自动追加
user_email: str
user_order_id: str
intent: str
missing_fields: List[str] # 当前还缺少的信息字段,如 ['email','order_id']
ticket_created: bool
messages 使用 operator.add 作为 reducer,意味着每次节点返回的新消息会自动拼接到历史里,非常适合多轮对话。
2. 节点函数(模拟真实业务)
python
import re
# ---------- 工具函数 ----------
def extract_email(text: str):
match = re.search(r'[\w\.-]+@[\w\.-]+\.\w+', text)
return match.group(0) if match else None
def extract_order_id(text: str):
match = re.search(r'[A-Z]{2,4}-\d{4,6}', text) # 形如 ABC-12345
return match.group(0) if match else None
# ---------- 节点实现 ----------
def entry_router(state: CustomerState):
"""入口路由:如果之前有缺失信息未补齐,则继续信息收集,否则进入意图分类"""
if state.get("missing_fields"):
return {"next_node": "collect_info"}
return {"next_node": "classify_intent"}
def classify_intent(state: CustomerState):
"""分析用户意图(简化:关键词匹配)"""
last_msg = state["messages"][-1]["content"].lower()
if any(w in last_msg for w in ["你好", "嗨", "hello"]):
intent = "greeting"
missing = []
elif any(w in last_msg for w in ["订单", "order", "物流"]):
intent = "order_issue"
# 检查是否已有信息
missing = []
if not state.get("user_email"):
missing.append("email")
if not state.get("user_order_id"):
missing.append("order_id")
elif any(w in last_msg for w in ["投诉", "complain"]):
intent = "complaint"
missing = []
elif any(w in last_msg for w in ["人工", "客服"]):
intent = "human"
missing = []
else:
intent = "question"
missing = []
if not state.get("user_email"): # 普通咨询也建议留邮箱以便跟进
missing.append("email")
return {"intent": intent, "missing_fields": missing}
def collect_info(state: CustomerState):
"""从用户最新消息中提取信息,并判断是否仍缺失字段"""
last_msg = state["messages"][-1]["content"]
# 尝试提取
email = extract_email(last_msg)
order_id = extract_order_id(last_msg)
updates = {}
if email:
updates["user_email"] = email
if order_id:
updates["user_order_id"] = order_id
# 更新 missing_fields
current_missing = state.get("missing_fields", [])
new_missing = [f for f in current_missing if (
(f == "email" and not (email or state.get("user_email"))) or
(f == "order_id" and not (order_id or state.get("user_order_id")))
)]
updates["missing_fields"] = new_missing
# 如果还有缺失,反问用户
if new_missing:
prompt_map = {
"email": "请提供您的邮箱地址,方便我们联系您。",
"order_id": "请提供您的订单号(格式如 ABC-12345)。"
}
questions = [prompt_map[f] for f in new_missing]
assistant_msg = {"role": "assistant", "content": " ".join(questions)}
updates["messages"] = [assistant_msg]
else:
updates["messages"] = [{"role": "assistant", "content": "感谢您的配合,正在为您处理..."}]
return updates
def handle_greeting(state: CustomerState):
return {"messages": [{"role": "assistant", "content": "您好!我是智能助手,有什么可以帮您?"}]}
def handle_question(state: CustomerState):
# 模拟查询 FAQ
return {"messages": [{"role": "assistant", "content": "根据您的问题,建议您查看我们的帮助中心:https://help.example.com/faq"}]}
def handle_order_issue(state: CustomerState):
# 创建工单
ticket_id = f"TK-{hash(state['user_email'] + state['user_order_id']) % 10000:04d}"
return {
"ticket_created": True,
"messages": [{"role": "assistant", "content": f"已为您创建工单 {ticket_id},我们的客服会在24小时内联系您。"}]
}
def handle_complaint(state: CustomerState):
# 直接转人工(中断点将在 escalate_to_human 前触发)
return {"next_node": "escalate_to_human"}
def handle_human(state: CustomerState):
return {"next_node": "escalate_to_human"}
def escalate_to_human(state: CustomerState):
"""转人工节点(会在此前中断,等待外部提供人工回复)"""
# 如果没有中断,会执行到这里。但通常会被 interrupt_before 拦截
return {"messages": [{"role": "assistant", "content": "正在为您转接人工客服,请稍候..."}]}
3. 构建图
python
from langgraph.graph import StateGraph, END
def build_graph():
graph = StateGraph(CustomerState)
# 添加节点
graph.add_node("entry_router", entry_router)
graph.add_node("classify_intent", classify_intent)
graph.add_node("collect_info", collect_info)
graph.add_node("handle_greeting", handle_greeting)
graph.add_node("handle_question", handle_question)
graph.add_node("handle_order_issue", handle_order_issue)
graph.add_node("handle_complaint", handle_complaint)
graph.add_node("handle_human", handle_human)
graph.add_node("escalate_to_human", escalate_to_human)
# 设置入口
graph.set_entry_point("entry_router")
# entry_router 条件边
graph.add_conditional_edges(
"entry_router",
lambda s: s.get("next_node", "classify_intent"),
{
"collect_info": "collect_info",
"classify_intent": "classify_intent"
}
)
# classify_intent 条件边:按意图分派
graph.add_conditional_edges(
"classify_intent",
lambda s: s["intent"],
{
"greeting": "handle_greeting",
"question": "handle_question",
"order_issue": "handle_order_issue",
"complaint": "handle_complaint",
"human": "handle_human"
}
)
# 订单处理:先检查信息是否齐全(条件边)
graph.add_conditional_edges(
"handle_order_issue",
lambda s: "collect_info" if s.get("missing_fields") else "end",
{"collect_info": "collect_info", "end": END}
)
# 普通咨询也需信息检查
graph.add_conditional_edges(
"handle_question",
lambda s: "collect_info" if s.get("missing_fields") else "end",
{"collect_info": "collect_info", "end": END}
)
# 投诉和人工请求直接去转人工节点(但在此之前会被中断)
graph.add_edge("handle_complaint", "escalate_to_human")
graph.add_edge("handle_human", "escalate_to_human")
# 信息收集后:若仍缺失,结束并等待用户回复;否则回到入口路由
graph.add_conditional_edges(
"collect_info",
lambda s: "end" if s.get("missing_fields") else "entry_router",
{"end": END, "entry_router": "entry_router"}
)
# 其他节点直接结束
graph.add_edge("handle_greeting", END)
graph.add_edge("escalate_to_human", END)
# 编译,并在 escalate_to_human 前插入中断点
from langgraph.checkpoint.memory import MemorySaver
memory = MemorySaver()
app = graph.compile(checkpointer=memory, interrupt_before=["escalate_to_human"])
return app
关键设计:
- 每次用户输入后运行图,图从
entry_router开始。如果有缺失信息(missing_fields非空),直接进入信息收集。 - 信息收集节点提取内容:如果仍有缺失,反问用户并结束;如果补齐,则回到
entry_router重新分类意图。 - 转人工前设置
interrupt_before,图会暂停,外部可以检查并注入人工回复后继续。
4. 运行与多轮对话演示
python
if __name__ == "__main__":
app = build_graph()
# 线程 ID 用于持久化状态(同一对话使用同一个 thread_id)
config = {"configurable": {"thread_id": "user-123"}}
# 轮次1:用户发起订单咨询
inputs = {"messages": [{"role": "user", "content": "我的订单怎么还没到?"}]}
for event in app.stream(inputs, config):
for k, v in event.items():
if "messages" in v:
print(f"[{k}] {v['messages'][-1]['content']}")
print("--- 第1轮结束,等待用户提供信息 ---\n")
# 轮次2:用户提供邮箱
inputs = {"messages": [{"role": "user", "content": "我的邮箱是 user@example.com"}]}
for event in app.stream(inputs, config):
for k, v in event.items():
if "messages" in v:
print(f"[{k}] {v['messages'][-1]['content']}")
print("--- 第2轮结束,还缺订单号 ---\n")
# 轮次3:用户提供订单号
inputs = {"messages": [{"role": "user", "content": "订单号是 ORD-98765"}]}
for event in app.stream(inputs, config):
for k, v in event.items():
if "messages" in v:
print(f"[{k}] {v['messages'][-1]['content']}")
print("--- 第3轮结束,工单已创建 ---\n")
# 轮次4:用户要求转人工(将触发中断)
inputs = {"messages": [{"role": "user", "content": "我要投诉!"}]}
# 第一次 stream 会停在 escalate_to_human 前
for event in app.stream(inputs, config):
for k, v in event.items():
if "messages" in v:
print(f"[{k}] {v['messages'][-1]['content']}")
# 检查状态
snapshot = app.get_state(config)
print(f"状态: next=({snapshot.next})") # 会输出 ('escalate_to_human',) 表示中断在此
# 模拟人工客服接管,更新状态继续执行
app.update_state(config, {"messages": [{"role": "assistant", "content": "人工客服已接入:您好,我是客服小张,了解您的不便..."}]})
# 继续执行
for event in app.stream(None, config):
for k, v in event.items():
if "messages" in v:
print(f"[{k}] {v['messages'][-1]['content']}")
运行输出示例:
[collect_info] 请提供您的邮箱地址,方便我们联系您。
--- 第1轮结束,等待用户提供信息 ---
[collect_info] 请提供您的订单号(格式如 ABC-12345)。
--- 第2轮结束,还缺订单号 ---
[handle_order_issue] 已为您创建工单 TK-7623,我们的客服会在24小时内联系您。
--- 第3轮结束,工单已创建 ---
[escalate_to_human] 正在为您转接人工客服,请稍候...
状态: next=('escalate_to_human',)
[escalate_to_human] 人工客服已接入:您好,我是客服小张,了解您的不便...
5. 项目说明
跨轮对话的持久化
我们使用了 MemorySaver 作为检查点存储器,同一个 thread_id 的多次 stream 调用会共享状态。这意味着即使程序重启,只要换用持久化存储器(如 SQLite),对话也可以无缝恢复。
人在回路(Human-in-the-Loop)
interrupt_before=["escalate_to_human"] 让图在进入人工节点前自动暂停。开发者可以检查状态,展示给人工专员,通过 app.update_state 注入人工回复后继续执行。这个机制也非常适合审批流、高风险操作确认等场景。
如何与真实 LLM 集成
示例中的意图分类和信息提取都使用了简化的正则与关键词匹配。在实际项目中,只需要将 classify_intent 和 collect_info 中的逻辑替换为调用 LLM(如 GPT-4),并返回结构化输出即可。
扩展方向
- 增加知识库检索节点,连接向量数据库。
- 增加情绪识别,在用户愤怒时自动升级。
- 引入子图,把工单创建、日志记录等作为独立的子流程。
- 部署到 LangGraph Platform,获得监控和可视化能力。
这个项目完整展示了 LangGraph 在多轮对话场景下的核心能力:状态管理、条件路由、信息收集循环、人工介入。你可以把它作为骨架,快速替换内部逻辑为真实 API,搭建生产级客户支持系统。