160.Agent-LangGraph-自定义主管架构智能体

**摘要:**本文围绕 LangGraph 的「主管---子 Agent」多智能体架构展开,讲解如何通过主管节点做任务规划与意图识别,把用户问题拆分为技术、销售、账户等多个子任务,再由条件边路由到对应的子图执行。文中结合完整 Python 代码,说明子图的标准 ReAct 结构、主管与子图之间不共享状态的数据交互方式、任务队列与循环闭环的调度机制,并给出跨领域多任务的测试示例,帮助读者快速搭建可循环协调多任务的多智能体系统。

内容参考于:图灵AI大模型全栈

主管架构说明

核心就是实现一个通过问题进行任务规划的功能

它的核心如下图红框,给主管节点添加一个条件边,通过条件边让主管跳转到子图

主管节点里面主要是返回一个任务,也就是子图的名字

返回什么子图名,是通过提示词向大模型提问获取的

任务规划的内容就是子图的名字列表,如下图红框

主管和子图的数据交互是通过不共享状态来实现的

代码

python 复制代码
# ==================================================
# 导入模块:按功能分层导入,对应架构的不同层级
# ==================================================
# 导入字面量类型,用于严格约束路由函数的返回值只能是指定枚举值
# 【机制】做静态类型校验,避免路由返回非法节点名导致运行时报错
from typing import Literal

# 导入LangChain标准消息类:用户消息、AI回复消息、系统提示消息
# 【机制】所有对话内容都统一封装为消息对象,是MessagesState的基础数据单元
from langchain.messages import HumanMessage, AIMessage, SystemMessage

# 导入OpenAI协议兼容的大模型客户端
# 【作用】统一调用入口,可适配所有支持OpenAI格式的模型服务(此处为阿里云百炼)
from langchain_openai import ChatOpenAI

# 导入LangGraph图构建核心组件
# StateGraph:状态图构建器,所有工作流的基础构造类
# START/END:流程起止标记常量,代表图的入口和出口
# MessagesState:LangGraph内置的对话状态结构
#   【机制】自带messages字段 + add_messages归约器,默认自动追加消息,无需手动管理列表
from langgraph.graph import StateGraph, START, END, MessagesState

# 导入LangGraph预构建的工具调用组件
# ToolNode:通用工具执行节点,自动解析tool_calls并执行对应工具
#   【机制】无需手写工具调用逻辑,自动完成参数解析、函数执行、结果封装全流程
# tools_condition:内置条件路由函数,自动判断消息中是否包含工具调用
#   【机制】读取消息的tool_calls字段,有则返回"tools"分支,无则返回结束分支
from langgraph.prebuilt import ToolNode, tools_condition

# 导入工具装饰器:将普通Python函数包装为大模型可调用的标准工具
# 【机制】自动生成工具的JSON Schema描述、做入参类型校验、封装为LangChain工具对象
from langchain.tools import tool

import os
# 环境变量加载工具,用于读取.env中的敏感配置(如API密钥)
from dotenv import load_dotenv

# 执行环境变量加载,将.env文件中的配置注入系统环境变量
load_dotenv()

# ==============================================================================
# 数据层:模拟业务数据源,替代真实项目中的数据库/后端接口
# 【设计逻辑】分层解耦:工具层只调用数据,不关心数据存在哪里;替换数据源时无需修改工具代码
# ==============================================================================

# 技术知识库:问题关键词 → 标准解决方案
# 【业务作用】技术支持Agent的核心数据源,做关键词匹配的FAQ问答
# 【架构定位】数据层 → 仅技术工具可访问
KNOWLEDGE_BASE = {
    "login": "请清除浏览器缓存并重新登录,或重置密码。",
    "payment": "请检查银行卡余额,确认交易状态,或联系银行。",
    "bug": "我们已记录此问题,技术团队将在24小时内处理。",
    "network": "请检查网络连接,或尝试切换网络环境。",
    "performance": "建议清理缓存、重启应用或检查系统资源使用情况。"
}

# 产品套餐库:套餐ID → 套餐详情(名称、价格、功能列表)
# 【业务作用】销售Agent的数据源,支撑产品介绍、价格查询、升级计算
# 【架构定位】数据层 → 仅销售工具可访问
PRODUCTS = {
    "basic": {"name": "基础版", "price": 99, "features": ["基础功能", "邮件支持"]},
    "pro": {"name": "专业版", "price": 299, "features": ["高级功能", "优先支持", "API访问"]},
    "enterprise": {"name": "企业版", "price": 999, "features": ["企业功能", "专属客服", "定制开发"]}
}

# 用户账户库:用户ID → 账户信息(套餐、状态、权限、余额)
# 【业务作用】账户管理Agent的数据源,支撑查询、退款等账户操作
# 【架构定位】数据层 → 仅账户管理工具可访问
USER_DATABASE = {
    "user123": {"plan": "pro", "status": "active", "support_level": "premium", "balance": 500},
    "user456": {"plan": "basic", "status": "active", "support_level": "standard", "balance": 100}
}

# ==============================================================================
# 大模型初始化:全局模型实例,供所有Agent复用
# ==============================================================================

# 初始化通义千问大模型,使用阿里云百炼的OpenAI兼容接口
llm = ChatOpenAI(
    api_key=os.getenv("DASHSCOPE_API_KEY"),  # 从环境变量读取API密钥,避免硬编码
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",  # 接口地址
    model="qwen3.8-flash",  # 指定模型版本
    temperature=0.1  # 生成温度:值越低回答越稳定、随机性越小
    # 【设计逻辑】客服/工具调用场景使用低温度,保证输出确定性,避免模型胡编乱造
)

# ==============================================================================
# 工具层:按业务领域分组,封装为大模型可调用的标准工具
# 【设计逻辑】职责隔离:每个领域的工具只对应该领域的Agent开放,从权限上避免越权
# ==============================================================================

# ----------------------------------------------------------------------
# 第一组:技术支持类工具 → 仅供技术Agent使用
# ----------------------------------------------------------------------

@tool
def search_knowledge_base(query: str) -> str:
    """
    搜索技术知识库,根据问题关键词匹配解决方案
    Args:
        query: 用户的技术问题描述文本
    Returns:
        匹配到的解决方案字符串,未匹配则返回建单提示
    """
    # 打印调试日志,便于追踪工具调用情况
    print(f"[Tech] 搜索知识库: {query}")

    # 将查询转为小写,实现不区分大小写的关键词匹配
    query_lower = query.lower()
    # 存放所有匹配到的解决方案
    results = []

    # 遍历知识库,逐个匹配关键词
    for issue, solution in KNOWLEDGE_BASE.items():
        # 只要关键词出现在查询文本中,就算匹配成功
        if issue in query_lower:
            results.append(f"{issue}: {solution}")

    # 匹配到结果:拼接所有解决方案返回
    if results:
        return "找到以下解决方案:\n" + "\n".join(results)

    # 未匹配到结果:返回创建工单的引导
    return "未在知识库中找到相关解决方案,建议创建技术工单进行人工处理。"


@tool
def create_support_ticket(issue_description: str, priority: str = "normal") -> str:
    """
    创建技术支持工单,生成唯一工单编号
    Args:
        issue_description: 问题详细描述
        priority: 工单优先级,可选值 normal/high/urgent,默认为普通
    Returns:
        工单创建成功的回执信息
    """
    # 导入uuid模块,生成唯一标识(按需导入,避免顶层不必要的依赖)
    import uuid

    # 生成8位大写的唯一工单ID,格式 TICKET-XXXXXXXX
    ticket_id = f"TICKET-{str(uuid.uuid4())[:8].upper()}"
    print(f"[Tech] 创建工单: {ticket_id}")

    # 返回格式化的工单回执
    return f"已创建支持工单: {ticket_id}\n问题描述: {issue_description}\n优先级: {priority}\n我们的技术团队将在24小时内处理您的问题。"

# ----------------------------------------------------------------------
# 第二组:销售类工具 → 仅供销售Agent使用
# ----------------------------------------------------------------------

@tool
def get_product_info(product_query: str = "") -> str:
    """
    查询产品信息与价格,支持查单个产品或全量产品列表
    Args:
        product_query: 可选,产品ID或名称;不传则返回全部产品
    Returns:
        格式化的产品详情文本
    """
    print(f"[Sales] 查询产品: {product_query or '全部'}")

    # 分支1:没有传入查询词 → 返回所有产品的完整列表
    if not product_query:
        result = "我们的产品线包括:\n\n"
        # 遍历产品字典,拼接每个产品的信息
        for key, product in PRODUCTS.items():
            result += f"**{product['name']}** - ¥{product['price']}/月\n"
            result += f"功能: {', '.join(product['features'])}\n\n"
        return result

    # 分支2:有查询词 → 做关键词匹配,返回单个产品详情
    query_lower = product_query.lower()
    for key, product in PRODUCTS.items():
        # 匹配规则:产品ID或产品名称包含查询词就算命中
        if key in query_lower or product["name"] in query_lower:
            return (
                f"**{product['name']}**\n"
                f"价格: ¥{product['price']}/月\n"
                f"功能: {', '.join(product['features'])}"
            )

    # 未匹配到任何产品:返回提示信息
    return f"未找到关于'{product_query}'的产品信息。请查看我们的完整产品列表。"


@tool
def calculate_upgrade_cost(current_plan: str, target_plan: str) -> str:
    """
    计算套餐升级的差价与新增功能
    Args:
        current_plan: 当前套餐的ID
        target_plan: 目标套餐的ID
    Returns:
        升级费用计算结果与新增功能列表
    """
    print(f"[Sales] 计算升级: {current_plan} -> {target_plan}")

    # 第一步:参数合法性校验,避免非法套餐ID导致后续报错
    if current_plan not in PRODUCTS or target_plan not in PRODUCTS:
        return "无效的套餐类型。请检查套餐名称。"

    # 取出两个套餐的价格
    current_price = PRODUCTS[current_plan]["price"]
    target_price = PRODUCTS[target_plan]["price"]

    # 分支:目标套餐价格 ≤ 当前套餐 → 不需要升级费用
    if target_price <= current_price:
        return (
            f"目标套餐 ({PRODUCTS[target_plan]['name']}) "
            f"价格不高于当前套餐 ({PRODUCTS[current_plan]['name']}),无需升级费用。"
        )

    # 计算升级差价
    upgrade_cost = target_price - current_price
    # 计算新增功能:用集合差集,得到目标套餐有但当前套餐没有的功能
    new_features = set(PRODUCTS[target_plan]["features"]) - set(PRODUCTS[current_plan]["features"])

    # 返回格式化的计算结果
    return (
        f"升级费用计算:\n"
        f"当前套餐: {PRODUCTS[current_plan]['name']} (¥{current_price}/月)\n"
        f"目标套餐: {PRODUCTS[target_plan]['name']} (¥{target_price}/月)\n"
        f"升级费用: ¥{upgrade_cost}/月\n\n"
        f"新增功能: {', '.join(new_features)}"
    )

# ----------------------------------------------------------------------
# 第三组:账户管理类工具 → 仅供账户管理Agent使用
# ----------------------------------------------------------------------

@tool
def get_user_account_info(user_id: str) -> str:
    """
    查询用户账户的详细信息
    Args:
        user_id: 用户的唯一标识ID
    Returns:
        格式化的账户信息文本
    """
    print(f"[Admin] 查询账户: {user_id}")

    # 校验:用户ID为空时直接返回提示
    if not user_id:
        return "请提供您的用户ID以查询账户信息。"

    # 命中用户:拼接账户详情返回
    if user_id in USER_DATABASE:
        user_info = USER_DATABASE[user_id]
        return (
            f"账户信息:\n"
            f"用户ID: {user_id}\n"
            f"当前套餐: {user_info['plan']}\n"
            f"账户状态: {user_info['status']}\n"
            f"支持级别: {user_info['support_level']}\n"
            f"账户余额: ¥{user_info['balance']}"
        )

    # 未命中用户:返回提示
    return f"未找到用户ID '{user_id}' 的账户信息。"


@tool
def process_refund_request(user_id: str, reason: str) -> str:
    """
    提交退款申请,校验账户状态并计算退款金额
    Args:
        user_id: 用户ID
        reason: 退款原因
    Returns:
        退款申请受理结果
    """
    print(f"[Admin] 处理退款: {user_id}")

    # 第一步:校验用户ID有效性
    if not user_id or user_id not in USER_DATABASE:
        return "请提供有效的用户ID以处理退款请求。"

    user_info = USER_DATABASE[user_id]

    # 第二步:校验账户状态,仅活跃账户可申请退款
    if user_info["status"] != "active":
        return "只有活跃账户才能申请退款。"

    # 计算退款金额:按月费标准计算(简化逻辑)
    refund_amount = PRODUCTS[user_info["plan"]]["price"]

    # 返回格式化的退款回执
    return (
        f"退款申请已提交:\n"
        f"用户ID: {user_id}\n"
        f"退款原因: {reason}\n"
        f"退款金额: ¥{refund_amount}\n"
        f"处理时间: 3-5个工作日\n"
        f"退款将原路返回到您的支付账户。"
    )

# ==============================================================================
# 执行层:子Agent子图(共3个,均为标准ReAct工具调用结构)
# 【架构设计】每个子Agent是一个独立编译的子图,职责单一、可独立测试、可复用
# 【核心机制】标准ReAct循环:模型思考 → 调用工具 → 拿到结果 → 模型再思考 → 输出答案
# ==============================================================================

# ----------------------------------------------------------------------
# 子图1:技术支持Agent子图
# ----------------------------------------------------------------------
def create_tech_agent_subgraph():
    """
    创建技术支持子Agent,负责处理所有技术类问题
    内部结构:标准ReAct闭环(模型决策 ↔ 工具执行)
    Returns:
        编译完成的子图实例
    """
    # 定义该Agent专属的工具列表 → 从权限上限制Agent只能调用技术类工具
    tech_tools = [search_knowledge_base, create_support_ticket]

    def tech_model_node(state: MessagesState):
        """
        技术Agent的模型决策节点:调用大模型,自主决定是否调用工具
        【核心作用】ReAct循环的"思考"环节,是Agent的大脑
        """
        print("[Tech] LLM 决策...")

        # 系统提示词:明确角色、工作流程、职责边界、回答规范
        # 【设计逻辑】通过提示词约束Agent行为,配合工具权限,实现双重职责隔离
        system_prompt = """你是技术支持助手,负责解决技术问题。

        **工作流程**:
        1. 先用 search_knowledge_base 查知识库
        2. 查到答案 → 直接告诉用户
        3. 查不到 → 用 create_support_ticket 创建工单

        **职责边界**:
        处理:报错、bug、登录失败、网络问题、性能问题
        不处理:价格咨询、账户余额、退款申请

        **回答要求**:
        - 简洁明了,直接给出解决方案
        - 知识库有答案就快速回复
        - 需要创建工单时,告知用户工单ID和预计处理时间
        """

        # 拼接完整消息列表:系统提示词放在最前面,后面跟上历史对话消息
        messages = [SystemMessage(content=system_prompt)] + state["messages"]

        # 先给模型绑定工具定义,再调用模型
        # 【机制】bind_tools会把工具的JSON Schema注入模型,让模型知道有哪些工具可用、参数是什么
        ai_message = llm.bind_tools(tech_tools).invoke(messages)

        # 返回AI消息,会自动追加到状态的messages列表中
        # 【机制】MessagesState的messages字段使用add_messages归约器,默认是追加而非覆盖
        return {"messages": [ai_message]}

    # 创建子图的构建器,使用内置对话状态
    builder = StateGraph(MessagesState)

    # 添加两个核心节点
    # 1. 模型决策节点:负责思考和决策
    builder.add_node("tech_model", tech_model_node)
    # 2. 工具执行节点:LangGraph内置通用节点,负责自动执行工具调用
    # 【机制】ToolNode会自动读取消息中的tool_calls,找到对应函数执行,生成ToolMessage
    builder.add_node("tech_tools", ToolNode(tech_tools))

    # 添加起始边:流程启动后,先进入模型决策节点
    builder.add_edge(START, "tech_model")

    # 添加条件边:模型决策后,动态决定下一步走向
    builder.add_conditional_edges(
        "tech_model",  # 源节点:模型决策节点
        tools_condition,  # 路由判断函数:内置工具调用检测器
        # 分支映射:路由函数的返回值 → 目标节点
        {"tools": "tech_tools", END: END}
        # 【机制】有tool_calls → 走tools分支到工具节点;没有 → 走END结束
    )

    # 添加回边:工具执行完成后,回到模型决策节点
    # 【核心机制】形成 ReAct 闭环:工具结果返回给模型,模型继续推理下一步
    builder.add_edge("tech_tools", "tech_model")

    # 编译子图,返回可执行实例
    return builder.compile()

# ----------------------------------------------------------------------
# 子图2:销售顾问Agent子图
# 【结构说明】与技术Agent完全一致的标准ReAct结构,仅工具集和系统提示词不同
# ----------------------------------------------------------------------
def create_sales_agent_subgraph():
    """创建销售子Agent,负责产品介绍、价格查询、升级费用计算"""
    # 销售专属工具集
    sales_tools = [get_product_info, calculate_upgrade_cost]

    def sales_model_node(state: MessagesState):
        """销售Agent的模型决策节点"""
        print("[Sales] LLM 决策...")

        # 销售专属系统提示词:明确销售职责边界
        system_message = SystemMessage(content="""
            你是销售顾问,**只负责**产品销售相关咨询。

            你负责:
            - 介绍产品功能和价格(使用 get_product_info)
            - 计算套餐升级费用(使用 calculate_upgrade_cost)
            - 推荐合适产品
            - 告知促销活动

            你**不负责**:
            - 查询用户余额、账户信息
            - 处理退款申请
            - 技术支持问题

            **重要**:如果用户询问职责范围外的问题,请礼貌地告知:
            "关于【账户/余额/退款/技术】问题,我会转交给相关同事处理。"
            然后继续处理销售相关问题。

            原则:先了解需求,再推荐方案;诚实专业,不夸大产品。
        """)

        # 拼接系统消息 + 历史对话
        messages = [system_message] + state["messages"]

        # 绑定销售工具后调用模型
        ai_message = llm.bind_tools(sales_tools).invoke(messages)
        return {"messages": [ai_message]}

    # 构建子图,结构同技术Agent
    builder = StateGraph(MessagesState)
    builder.add_node("sales_model", sales_model_node)
    builder.add_node("sales_tools", ToolNode(sales_tools))

    builder.add_edge(START, "sales_model")
    builder.add_conditional_edges(
        "sales_model",
        tools_condition,
        {"tools": "sales_tools", END: END}
    )
    builder.add_edge("sales_tools", "sales_model")

    return builder.compile()

# ----------------------------------------------------------------------
# 子图3:账户管理Agent子图
# 【结构说明】标准ReAct结构,负责账户查询、退款等客户管理业务
# ----------------------------------------------------------------------
def create_admin_agent_subgraph():
    """创建客户管理子Agent,负责账户查询、退款申请等业务"""
    # 账户管理专属工具集
    admin_tools = [get_user_account_info, process_refund_request]

    def admin_model_node(state: MessagesState):
        """账户管理Agent的模型决策节点"""
        print("[Admin] LLM 决策...")

        # 账户管理专属系统提示词
        system_message = SystemMessage(content="""
            你是客户管理助手,**只负责**账户和支付相关咨询。

            你负责:
            - 查询账户信息、余额(使用 get_user_account_info)
            - 处理退款申请(使用 process_refund_request)

            你**不负责**:
            - 产品介绍、价格咨询
            - 技术支持问题
            - 升级费用计算

            **重要**:如果用户询问职责范围外的问题,请礼貌地告知:
            "关于【产品/技术】问题,我会转交给相关同事处理。"
            然后继续处理账户相关问题。

            请根据用户的问题,选择合适的工具来提供帮助。
        """)

        # 拼接消息并调用模型
        messages = [system_message] + state["messages"]
        ai_message = llm.bind_tools(admin_tools).invoke(messages)
        return {"messages": [ai_message]}

    # 构建子图
    builder = StateGraph(MessagesState)
    builder.add_node("admin_model", admin_model_node)
    builder.add_node("admin_tools", ToolNode(admin_tools))

    builder.add_edge(START, "admin_model")
    builder.add_conditional_edges(
        "admin_model",
        tools_condition,
        {"tools": "admin_tools", END: END}
    )
    builder.add_edge("admin_tools", "admin_model")

    return builder.compile()

# ==============================================================================
# 调度层:主管调度图(整个系统的核心大脑)
# 【核心架构】主管-工人模式(Supervisor-Worker)
# 【核心机制】任务队列 + 循环闭环 + 手动调用子图
#   1. 任务队列:识别所有任务后放入队列,逐个取出执行
#   2. 循环闭环:子Agent执行完回到主管,继续取下一个任务,直到队列为空
#   3. 手动调用子图:在节点内主动invoke子图,灵活控制传参和状态合并
# ==============================================================================

def create_supervisor_graph():
    """
    创建主管调度总图,负责任务识别、分配、循环协调
    支持单个用户问题包含多个跨领域任务的串行处理
    Returns:
        编译完成的主管总图实例
    """
    # 第一步:实例化三个子Agent子图,作为主管可调度的执行单元
    tech_subgraph = create_tech_agent_subgraph()
    sales_subgraph = create_sales_agent_subgraph()
    admin_subgraph = create_admin_agent_subgraph()

    # --------------------------------------------------------------------------
    # 定义主管状态:在对话状态基础上,扩展调度专用字段
    # 【设计逻辑】继承MessagesState获得对话能力,新增4个字段用于任务调度管理
    # --------------------------------------------------------------------------
    class SupervisorState(MessagesState):
        """
        主管全局状态结构
        Attributes:
            current_agent: 当前正在执行的子Agent名称,用于日志追踪
            pending_tasks: 待处理任务队列,按顺序存放待执行的Agent名称
            completed_tasks: 已完成任务列表,记录已执行过的Agent,避免重复
            next_agent: 下一个要执行的Agent,由主管节点设置,供路由函数读取
        """
        current_agent: str
        pending_tasks: list[str]
        completed_tasks: list[str]
        next_agent: str | None = None  # 默认值为None,表示无下一个任务

    # --------------------------------------------------------------------------
    # 主管核心节点:任务识别、队列管理、调度分发
    # 【三分支逻辑】
    #   1. 有待办任务 → 取队首执行
    #   2. 首次执行无待办 → LLM意图识别,生成任务队列
    #   3. 无任务 → 结束流程
    # --------------------------------------------------------------------------
    def supervisor_node(state: SupervisorState) -> dict:
        """
        主管调度节点:整个系统的调度中枢
        每次进入都会根据当前队列状态,决定下一步动作
        """
        # 从状态中取出调度相关字段,缺省值为空列表
        pending = state.get("pending_tasks", [])
        completed = state.get("completed_tasks", [])
        messages = state["messages"]
        # 取最后一条消息,用于判断是否是新的用户输入
        last_msg = messages[-1] if messages else None

        # ============== 分支1:队列中有待处理任务 → 直接取出第一个执行 ==============
        # 【触发场景】子Agent执行完回到主管,队列里还有剩余任务
        if pending:
            next_task = pending[0]  # 取出队首第一个任务
            remaining = pending[1:]  # 剩下的任务保留在队列中
            print(f"\n[Supervisor] 取出待处理任务: {next_task}, 剩余: {remaining}")
            return {
                "pending_tasks": remaining,  # 更新剩余任务队列
                "next_agent": next_task      # 标记下一个要执行的Agent
            }

        # ============== 分支2:队列为空,且是新用户消息 → LLM做意图识别 ==============
        # 【触发场景】首次进入主管,用户刚发了问题,还没做过任务拆分
        if isinstance(last_msg, HumanMessage):
            print(f"\n[Supervisor] 使用 LLM 进行意图识别...")

            # 意图识别提示词:明确Agent分类标准,要求模型输出任务列表
            routing_prompt = f"""你是意图识别专家。根据用户消息,决定调用哪个 Agent 来处理。

            Agent 类型:
            - tech_agent: 技术支持(报错、bug、故障、登录问题、网络问题、性能问题、技术咨询等)
            - sales_agent: 销售服务(价格咨询、套餐升级、产品信息、购买咨询、升级费用等)
            - admin_agent: 账户管理(查询余额、账户信息、退款申请、账户状态等)

            注意:一个问题可能需要多个 Agent 处理。

            用户消息: {last_msg.content}

            请分析用户消息,返回需要调用的 Agent 列表(最多3个,按处理顺序排列)。
            只返回 Agent 名称,用逗号分隔。例如:tech_agent,sales_agent"""

            # 调用大模型进行意图分类
            response = llm.invoke(routing_prompt)
            # 兼容处理:取出模型返回的文本内容
            response_text = response.content if hasattr(response, 'content') else str(response)
            print(f"response_text========={response_text}")

            # 解析模型输出,提取有效的Agent名称
            new_tasks = []
            # 遍历三个标准Agent名,检查是否出现在模型返回文本中
            for agent in ["tech_agent", "sales_agent", "admin_agent"]:
                # 两个条件:模型返回包含该Agent 且 该任务未完成过
                if agent in response_text and agent not in completed:
                    new_tasks.append(agent)

            # 识别到有效任务:初始化任务队列,取出第一个执行
            if new_tasks:
                print(f"[Supervisor] LLM 识别到任务: {new_tasks}")
                next_task = new_tasks[0]
                remaining = new_tasks[1:]
                return {
                    "pending_tasks": remaining,  # 剩余任务放入待办队列
                    "next_agent": next_task      # 第一个任务作为下一步执行
                }

        # ============== 分支3:无任务可执行 → 结束流程 ==============
        # 【触发场景】队列为空,也没有新的用户输入,所有任务都处理完了
        print("\n[Supervisor] 所有任务已完成,结束对话")
        return {"pending_tasks": [], "next_agent": None}

    # --------------------------------------------------------------------------
    # 路由函数:根据主管设置的next_agent,跳转到对应子Agent节点
    # 【设计逻辑】将"决策"和"跳转"分离,主管节点只做决策写状态,路由函数只读状态做跳转
    # --------------------------------------------------------------------------
    def route_by_pending(state: SupervisorState) -> Literal["tech_agent", "sales_agent", "admin_agent", END]:
        """
        条件路由函数:读取next_agent字段,返回对应的目标节点名称
        """
        next_agent = state.get("next_agent")
        if next_agent == "tech_agent":
            return "tech_agent"
        elif next_agent == "sales_agent":
            return "sales_agent"
        elif next_agent == "admin_agent":
            return "admin_agent"
        else:
            # next_agent为空时,返回结束标记
            return END

    # --------------------------------------------------------------------------
    # 子Agent调用节点:手动执行子图,合并结果到主管状态
    # 【设计选择】手动调用子图,而非直接嵌套子图为节点
    #   优势1:可以自定义传参,灵活控制给子图传递哪些状态
    #   优势2:可以自定义状态合并逻辑,同时更新调度字段
    #   优势3:父子图状态结构不同时,可手动做字段映射
    # --------------------------------------------------------------------------

    def call_tech_agent(state: SupervisorState) -> SupervisorState:
        """调用技术支持子Agent,执行完成后合并结果"""
        print("[Supervisor] 调用 Tech 子图...")

        # 保留原有的调度状态,子图执行不修改待办队列
        pending = state.get("pending_tasks", [])
        completed = state.get("completed_tasks", [])

        # 手动调用子图:传入当前对话历史,获取子图执行后的完整状态
        # 【机制】子图独立执行,内部走完完整的ReAct循环后返回结果
        result = tech_subgraph.invoke({"messages": state["messages"]})

        # 更新主管状态:合并消息、标记任务已完成、记录当前Agent
        return {
            "messages": result["messages"],  # 合并子图产生的新消息到全局对话
            "pending_tasks": pending,        # 待办队列保持不变,回到主管后再取
            "completed_tasks": completed + ["tech"],  # 标记技术任务已完成
            "current_agent": "tech"          # 记录当前执行的Agent
        }

    def call_sales_agent(state: SupervisorState) -> SupervisorState:
        """调用销售顾问子Agent"""
        print("[Supervisor] 调用 Sales 子图...")

        pending = state.get("pending_tasks", [])
        completed = state.get("completed_tasks", [])

        result = sales_subgraph.invoke({"messages": state["messages"]})

        return {
            "messages": result["messages"],
            "pending_tasks": pending,
            "completed_tasks": completed + ["sales"],
            "current_agent": "sales"
        }

    def call_admin_agent(state: SupervisorState) -> SupervisorState:
        """调用账户管理子Agent"""
        print("[Supervisor] 调用 Admin 子图...")

        pending = state.get("pending_tasks", [])
        completed = state.get("completed_tasks", [])

        result = admin_subgraph.invoke({"messages": state["messages"]})

        return {
            "messages": result["messages"],
            "pending_tasks": pending,
            "completed_tasks": completed + ["admin"],
            "current_agent": "admin"
        }

    # --------------------------------------------------------------------------
    # 构建主管总图
    # --------------------------------------------------------------------------
    builder = StateGraph(SupervisorState)

    # 添加所有节点:1个主管调度节点 + 3个子Agent调用节点
    builder.add_node("supervisor", supervisor_node)
    builder.add_node("tech_agent", call_tech_agent)
    builder.add_node("sales_agent", call_sales_agent)
    builder.add_node("admin_agent", call_admin_agent)

    # 起始边:流程启动后,首先进入主管节点做任务识别
    builder.add_edge(START, "supervisor")

    # 条件边:主管决策后,根据路由函数跳转到对应子Agent或结束
    builder.add_conditional_edges(
        "supervisor",
        route_by_pending,
        {
            "tech_agent": "tech_agent",
            "sales_agent": "sales_agent",
            "admin_agent": "admin_agent",
            END: END
        }
    )

    # 三条回边:所有子Agent执行完成后,都回到主管节点
    # 【核心机制】形成 主管→子Agent→主管 的闭环,实现循环调度
    # 每次回到主管,都会重新检查任务队列,还有任务就继续分发,没有就结束
    builder.add_edge("tech_agent", "supervisor")
    builder.add_edge("sales_agent", "supervisor")
    builder.add_edge("admin_agent", "supervisor")

    # 编译主管总图,返回可执行实例
    return builder.compile()

# ==============================================================================
# 测试运行模块
# ==============================================================================

def run_examples():
    """
    运行测试用例,验证多智能体系统的效果
    覆盖单领域任务、跨领域多任务等典型场景
    """
    print("=" * 70)
    print("主管架构多智能体系统 (支持循环协调多任务)")
    print("=" * 70)

    # 创建并编译主管总图
    graph = create_supervisor_graph()

    # 测试用例列表,可按需注释开启
    test_cases = [
        # {"message": "我的应用登录有问题,显示error 500"},       # 单领域:技术
        # {"message": "我想了解专业版的价格"},                     # 单领域:销售
        # {"message": "查询一下我的余额,用户ID是user123"},        # 单领域:账户
        # 跨领域多任务:同时包含销售+账户两个领域的需求
        {"message": "我想了解专业版的价格。我的用户ID是user123帮我查询一下我的余额"},
    ]

    # 遍历执行每个测试用例
    for i, test_case in enumerate(test_cases, 1):
        print(f"\n{'=' * 70}")
        print(f"测试案例 {i}")
        print(f"{'=' * 70}")
        print(f"用户: {test_case['message']}")
        print("-" * 50)

        # 构造初始状态:包含用户消息 + 调度字段初始值
        initial_state = {
            "messages": [HumanMessage(content=test_case["message"])],
            "current_agent": "supervisor",  # 初始当前角色为主管
            "pending_tasks": [],            # 待办队列初始为空
            "completed_tasks": [],          # 已完成列表初始为空
            "next_agent": None              # 无下一步执行目标
        }

        try:
            # 调用工作流执行,同步等待完整结果
            result = graph.invoke(initial_state)

            # 打印AI最终回复内容(只打印AI消息,过滤掉工具消息等)
            print("\nAI 回复:")
            for msg in result["messages"]:
                if isinstance(msg, AIMessage) and msg.content:
                    print(f"  {msg.content}")

            # 打印任务执行统计
            print(f"\n已完成任务: {result.get('completed_tasks', [])}")

        except Exception as e:
            # 异常捕获:打印错误信息和堆栈,便于调试
            print(f"处理失败: {e}")
            import traceback
            traceback.print_exc()

# ==============================================================================
# 程序入口
# ==============================================================================

if __name__ == "__main__":
    # 主程序启动,运行测试示例
    run_examples()

相关推荐
梦想出海-Phoebe2 小时前
ChatGPT 2026年10月更新汇总:GPT-6.1 Terra、AI短信助手与新功能解析
人工智能·chatgpt
乌恩大侠2 小时前
从纳秒级同步到厘米级定位:基于 NVIDIA Aerial、SRS 与多 O-RU 的 5G/6G UL-TDOA 实现原理与工程实践
人工智能·5g·o-ru·ai-ran
霸道流氓气质2 小时前
智能表单生成器:结构化输出契约、JSON Schema 驱动渲染与 EAV/JSONB 混合存储的 Vue3 + Spring AI 生产级实战
人工智能·spring·json
情 九2 小时前
MCP 开发环境搭建:Mac 与 Windows 跑通 Python 服务和 stdio 客户端
windows·python·macos·mcp
lpfasd1232 小时前
Mac mini M6(24GB)本地大模型部署实战:选型考量、踩坑记录与全链路搭建
人工智能
云上工程笔记2 小时前
2026年如何保障Token传输过程中的数据安全与隐私?加密、脱敏、审计与权限指南
人工智能
2601_970703832 小时前
工业除湿机选型与安全使用研究报告:把铭牌数字放回工况里
大数据·人工智能
日日大王学编程2 小时前
LangGraph 基本用法详解:从入门到实战
python·语言模型·langchain
奔跑的熊猫3 小时前
VSCode + FastAPI +AI Agent+langchain第一个程序
langchain·fastapi
进击的横打3 小时前
【人工智能】AI将推翻了传统软件开发吗
人工智能