【大模型Agent】基于LangGraph搭建 多轮对话客户支持机器人 项目示例

需收集信息
意图分类
问候
一般咨询
订单问题
投诉
转人工
有缺失信息
信息齐全
有缺失信息
信息齐全
触发中断
人工确认后继续
仍有缺失信息
信息已补齐
开始
入口路由
信息收集
意图分类
处理问候
处理咨询
处理订单
处理投诉
转人工请求
结束
升级至人工
⏸ 暂停:等待人工接入
发送转接消息
结束,等待用户回复

基于LangGraph搭建 多轮对话客户支持机器人 项目示例

下面基于 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_intentcollect_info 中的逻辑替换为调用 LLM(如 GPT-4),并返回结构化输出即可。

扩展方向

  • 增加知识库检索节点,连接向量数据库。
  • 增加情绪识别,在用户愤怒时自动升级。
  • 引入子图,把工单创建、日志记录等作为独立的子流程。
  • 部署到 LangGraph Platform,获得监控和可视化能力。

这个项目完整展示了 LangGraph 在多轮对话场景下的核心能力:状态管理、条件路由、信息收集循环、人工介入。你可以把它作为骨架,快速替换内部逻辑为真实 API,搭建生产级客户支持系统。

相关推荐
love在水一方1 小时前
【Voxel-SLAM】Data Structures / 数据结构文档(二)
数据结构·人工智能·机器学习
ConardLi1 小时前
开源我的 GPT-Image2 生图 Skill,附大量玩法指南
前端·人工智能·后端
QYR_111 小时前
2026卷绕式扣式电池产业洞察:智能制造如何重塑微型储能格局?
人工智能·市场调研
白熊1881 小时前
【大模型Agent】LangGraph 深度科普:为智能体而生的“有状态”编排框架
人工智能·langchain·agent·langgraph
数智工坊1 小时前
【SIoU Loss论文阅读】:引入角度感知的框回归损失,让检测收敛更快更准
论文阅读·人工智能·深度学习·机器学习·数据挖掘·回归·cnn
bloglin999991 小时前
向量大模型升级可能改变向量空间(需要回归)
人工智能·数据挖掘·回归
AI技术增长1 小时前
Pytorch图像去噪实战(三):ResUNet图像去噪模型实战,解决UNet深层训练不稳定问题
人工智能·pytorch·深度学习
TDengine (老段)1 小时前
工业软件的未来:构建在工业数据底座之上的 AI Agent
大数据·数据库·人工智能·时序数据库·tdengine
aLTttY1 小时前
Spring Boot集成AI大模型实战:从0到1打造智能应用
人工智能·spring boot·后端