[Ai Agent] 09 LangGraph 进阶:构建可控、可协作的多智能体系统

博客配套代码发布于github09 LangGraph 进阶

本系列所有博客均配套Gihub开源代码,开箱即用,仅需配置API_KEY。

如果该Agent教学系列帮到了你,欢迎给我个Star⭐

知识点Human-in-the-Loop(人工干预)| Graph-as-a-Tool(图即工具)| Multi-Agent 多智能体编排


前言:

在08篇中,我们亲手用LangGraph实现了一个透明、可追踪、带记忆的ReAct。它能思考、调用工具并记住历史,看起来已足够强大。

但是在生产级环境,它立马会撞上三大难题:

其一、安全失控:

Agent可以自动删除数据、发送邮件、转账付款------没有任何确认机制,一旦出错,无可挽回。

其二、工作流臃肿:

为了让Agent处理复杂任务,我们在单个节点里不断堆砌逻辑:"先验证输入>再查数据>然后进行业务计算>最后格式化输出..."

节点函数越来越长,流程臃肿。每次修改业务逻辑都需要重写整个函数,测试难度飙升。

其三、职责混乱:

所有功能都挤在一个Agent里,它既是研究员又是专家还是客服,身兼数职,更别提还要加上不断扩大的记忆。短期尚可,长期必炸。

所以,本篇为这三大痛点分别提供了三种对应解决的进阶能力:

Human-in-the-Loop(人工干预) :解决安全失控;

Graph-as-a-tool(图即工具) :解决工作流臃肿;

Multi-Agent(多智能体编排) :解决职责混乱。

接下来,我们用这三个能力,逐一攻克上述的问题。

一、Human-in-the-Loop:让Agent学会"请示"

为什么需要人工干预

全自动执行在demo运行中听起来很好,但在实际运行中非常极其危险

比如用户说,"把项目预算邮件发给财务",Agent自动调用send_email(to="@xx.com",content="xx"),如果内容有误,邮件一旦发出无法撤回。

所以我们需要一种机制:在执行高风险操作前暂停,等待人类确认

Langgraph提供了完美的解决方案:interrupt_before + MemorySaver。

  • MemorySaver会将图的当前状态(包括消息历史、中间变量)持久化;
  • interrupt_before=["node_name"] 表示"在进入该节点前暂停执行";
  • 暂停后,程序可以检查即将执行的操作,并询问用户是否继续;
  • 若用户批准,调用app.stream(None,config) 即可从断点恢复执行

这相当于为Agent加了一道 "安全锁"

人为审批的思路也很简单,我们假设一个发邮件Agent:

当你发邮件时,该Agent会暂停并显示即将执行的操作,等待你输入yes才继续。

大致步骤如下:

1. 提前确定好哪个节点需要"审批",在这里停住:

ini 复制代码
app = workflow.compile(
    # 在内存里做状态持久化
    checkpointer=MemorySaver(),
    interrupt_before=["tools"] # 选择要人工审批的节点 -- 负责在哪里停,之后的代码负责停了之后怎么办
)

2. 触发执行,并自动暂停

注:此处用stream流式,是利用流式API的中断机制,使其能够精准在特定节点前暂停。

ini 复制代码
    while 1:
        # 触发工作流执行,推进到下一个中断点或自然结束
        for _ in app.stream(inputs,config,stream_mode="values"): # 流式执行
        # inputs注入事件;config确定回话id;"values":完整记录每步结果
            pass   # 必须迭代生成器,才能实际执行工作流

3. 获取暂停节点的状态:

ini 复制代码
        # 获取当前状态
        snapshot = app.get_state(config)
        next_tasks = snapshot.next # 返回下一步要执行的节点名列表

4. 如果没有下一步,说明工作流已结束:

python 复制代码
        if not next_tasks:
            final_msg = snapshot.values['messages'][-1]
            print(f'\n最终回复:{final_msg.content}')
            break

5. 如果下一步是需要审批的节点,进行审批:

python 复制代码
        if "tools" in next_tasks:
            last_msg = snapshot.values['messages'][-1]
            tool_call = last_msg.tool_calls[0]
            print(f'\n⚠️ Agent准备执行操作:')
            print(f'    工具名称:{tool_call["name"]}')
            print(f'    参数:{tool_call["args"]}')

            approval = input("\n✅ 是否批准执行?(输入 'yes' 继续,其他取消): ").strip().lower()
            if approval == "yes":
                print('\n 继续执行...')
                inputs = None # 表示从断点继续,无新输入
            else:
                print("\n❌ 操作已取消,流程终止")
                break

完整代码如下:

python 复制代码
import os
from config import OPENAI_API_KEY,LANGCHAIN_API_KEY
from langchain_openai import ChatOpenAI
from langchain.tools import tool
from langchain_core.messages import HumanMessage
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import ToolNode
from langgraph.checkpoint.memory import MemorySaver

# langsmith调试
os.environ["LANGCHAIN_TRACING_V2"] = "true" # 总开关,决定启用追踪功能
os.environ["LANGCHAIN_PROJECT"] = "human_approval" # 自定义项目名
os.environ["LANGCHAIN_API_KEY"] = LANGCHAIN_API_KEY

# llm配置
llm = ChatOpenAI(
    model="deepseek-chat",
    api_key=OPENAI_API_KEY,
    base_url="https://api.deepseek.com"
)

# 定义一个敏感工具:发送邮件(模拟)
@tool
def send_email(to, content):
    """模拟发送邮件"""
    return f'邮件已发送至{to},内容为:{content}'


# 工具绑定到llm
tools = [send_email]
llm_with_tools = llm.bind_tools(tools)

# Node函数与Edge节点
tool_node = ToolNode(tools)


def call_model(state: MessagesState):
    response = llm_with_tools.invoke(state['messages'])
    return {"messages":[response]}


def should_continue(state: MessagesState):
    last_msg = state['messages'][-1]
    if hasattr(last_msg, "tool_calls") and last_msg.tool_calls:
        return "tools"
    return END


# 构建基础ReAct图
workflow = StateGraph(MessagesState)

workflow.add_node("agent", call_model)
workflow.add_node("tools", tool_node)
workflow.add_edge(START, "agent")

workflow.add_conditional_edges(
    "agent",
    should_continue,
    {
        "tools": "tools",
        END: END
    }
)

workflow.add_edge("tools", "agent")

app = workflow.compile(
    checkpointer=MemorySaver(),
    interrupt_before=["tools"] # 选择要人工审批的节点 -- 负责在哪里停,之后的代码负责停了之后怎么办
)


if __name__ == '__main__':
    config = {
        "configurable":{"thread_id":"user123"}
    }
    user_input = "请帮我给 boss@example.com 发一封邮件,内容是:会议推迟到明天下午3点。"

    print("用户输入:",user_input)
    print("\nAgent正在思考...\n")

    # 初识输入
    inputs = {"messages":[HumanMessage(content=user_input)]}

    while 1:
        # 触发工作流执行,推进到下一个中断点或自然结束
        for _ in app.stream(inputs,config,stream_mode="values"): # 流式执行
            pass   # 必须迭代生成器,才能实际执行工作流

        # 获取当前状态
        snapshot = app.get_state(config)
        next_tasks = snapshot.next # 返回下一步要执行的节点名列表

        # 如果没有下一步,说明工作流已结束
        if not next_tasks:
            final_msg = snapshot.values['messages'][-1]
            print(f'\n最终回复:{final_msg.content}')
            break

        # 如果下一步是需要审批的节点
        if "tools" in next_tasks:
            last_msg = snapshot.values['messages'][-1]
            tool_call = last_msg.tool_calls[0]
            print(f'\n⚠️ Agent准备执行操作:')
            print(f'    工具名称:{tool_call["name"]}')
            print(f'    参数:{tool_call["args"]}')

            approval = input("\n✅ 是否批准执行?(输入 'yes' 继续,其他取消): ").strip().lower()
            if approval == "yes":
                print('\n 继续执行...')
                inputs = None # 表示从断点继续,无新输入
            else:
                print("\n❌ 操作已取消,流程终止")
                break

测试:

如果我们把其中的user_input改为boss,不写明邮箱号,再测试一遍:

此时没有调用发邮件工具,自然也不会需要人工确认了。

二、Graph-as-a-Tool:把复杂流程封装成"黑盒工具"

为什么需要这种封装?

回想我们之前的 RAG 实现:一个完整的检索增强生成流程,通常包含多个步骤------文档加载与分块、文本向量化、存入向量数据库、执行检索、上下文增强、最终生成答案。这些操作本身已足够繁琐。

如果直接把这些逻辑塞进一个普通函数,并作为 LangChain Tool 注册给 LangGraph 使用,虽然技术上可行,但会带来明显问题

  • 耦合度高:所有逻辑挤在一起,难以单独测试或替换某一步;
  • 可读性差:主图节点变成"大泥球",失去流程清晰性;
  • 扩展困难:一旦需求变化(比如加入重排、多跳查询、结果校验),代码迅速膨胀,if-else 嵌套失控。

更关键的是------真实世界的智能任务往往不是线性的

例如,RAG 可能需要根据检索质量决定是否重新生成查询、是否引入外部知识、甚至请求人工确认。这类带状态转移、条件分支、循环反馈的逻辑,用传统函数难以优雅表达。

这时,Graph-as-a-Tool 的价值就凸显出来了。

它允许我们将某个复杂子任务(如 RAG)自身建模为一个独立的工作流图(sub-graph) ,再将整个图封装成一个对外透明的 Tool。对主 Graph 而言,它只是一个语义明确的黑盒接口 ;而内部,却是一个结构清晰、节点分明、支持中断恢复的完整流程

这正是现代智能体架构的核心理念:分层抽象,嵌套编排

主图负责"决策做什么",子图(作为 Tool)负责"如何做"。

而只有"图"这种结构,才能优雅地表达这种分层、嵌套、带反馈的复杂工作流

假设我们要做一个可以自动重试的API服务调用。该服务调用作为一个工具给主工作流。而它自身则是由一个子图构成,里面涵盖了一个完整的子工作流。

主要代码构建如下:(完整部分github中已提供)

python 复制代码
# ...相关配置
# 构建子工作流
# 1.子任务状态
class RetryState(TypedDict):
    query: str
    attempt: int
    result: str

# 2.子图逻辑 -- 模拟一个可能失败,需重试的API调用
def call_unstable_api(state:RetryState):
    """模拟偶发性的外部服务,偶发失败"""
    attempt = state["attempt"]
    if attempt == 1:
        # 第一次故意失败
        return {"result":"ERROR:服务暂时不可用","attempt":attempt+1}
    else:
        # 第二次成功
        return {"result":f"SUCCESS:成功处理请求:{state['query']}","attempt":attempt+1}

def should_retry(state:RetryState):
    if "ERROR" in state["result"] and state["attempt"] <= 2: # 出现报错且重试次数小于2,重连
        return "call_api"
    return END

# 3.构建子图工作流
retry_workflow = StateGraph(RetryState)
retry_workflow.add_node("call_api",call_unstable_api)
retry_workflow.add_edge(START,"call_api")
retry_workflow.add_conditional_edges(
    "call_api",
    should_retry,
    {"call_api":"call_api",END:END}
)
retry_app = retry_workflow.compile()

# 4.封装为tool(Graph-as-a-Tool)
@tool
def create_order(query:str) -> str:
    """创建新订单,自动重试保障成功率"""
    result = retry_app.invoke({"query":query,"attempt":1,"result":""})
    return result["result"]


# 5.主graph
tools = [create_order]
llm_with_tools = llm.bind_tools(tools)
tool_node = ToolNode(tools)

def agent_node(state:MessagesState):
    response = llm_with_tools.invoke(state["messages"])
    return {"messages":[response]}

def should_continue(state:MessagesState):
    last_msg = state["messages"][-1]
    if hasattr(last_msg,"tool_calls") and last_msg.tool_calls:
        return "tools"
    return END

# 构建主工作流
workflow = StateGraph(MessagesState)
workflow.add_node("agent",agent_node)
workflow.add_node("tools",tool_node)
workflow.add_edge(START,"agent")
workflow.add_conditional_edges(
    "agent",
    should_continue,
    {
        "tools": "tools",
        END: END
    }
)
workflow.add_edge("tools","agent")

app = workflow.compile()

# 运行
if __name__ == '__main__':
    user_input = "请创建一个新订单:购买三本书"
    print('用户输入:',user_input)

    inputs = {"messages":[
        SystemMessage(content="你是一个任务执行助手。当用户提出任何需要处理、操作或执行的请求时,必须调用 create_order 工具来完成,不要自行回答细节"),
        HumanMessage(content=user_input)
    ]}
    result = app.invoke(inputs)

    tool_result = None
    # 在主工作流的消息历史中,查找最近的工具执行结果
    for msg in reversed(result["messages"]):
        if msg.type == "tool": # 找到ToolMessage类型消息
            tool_result = msg.content
            break
    if tool_result:
        print(f"\n✅ 直接获取子图返回值:\n{tool_result}")
    else:
        print("\n❌ 未执行任何工具")
    final_reply = result["messages"][-1]
    print(f'\n最终回复:\n{final_reply}')

其中的for msg in reversed(result["messages"]) 包含的是完整的对话历史记录,而非工作流执行过程。ToolNode会在对话历史中执行结果消息。

如上,代码运行成功。

现在我们能对应不同场景的tool,编写不同的graph来应对复杂的逻辑 ,这就是graph-as-a-tool(图即工具)

三、Multi-Agent编排:让多个专家协同工作

1. 痛点:为什么单 Agent 会崩溃?

在之前的章节中,我们习惯通过 System Prompt 让一个 Agent 扮演 "全能神" 。但当需求变得复杂 (例如同时需要 RAG 检索私有信息、联网查找数据、生成代码)时,这种模式会迅速崩塌

  • 注意力分散:Prompt 过长,LLM 经常忽略指令,表现为"幻觉"。
  • 记忆爆炸:所有任务共用一个 Message History,上下文飞速扩张,不仅烧钱,还容易让模型"记混"信息。
  • 工具混杂:几十个工具堆在一起,模型极易误调(例如该查文档时去联网)。
  • 脆弱性:一步错,全盘输。无法针对特定任务(如写代码)单独优化 Prompt。

解法:微服务化(Microservices) 将一个试图全能的单智能体,拆解为多个专注、自治、可组合的子智能体,并通过编排协调完成复杂任务。

  • RAG 专家只负责查私有知识。
  • Web 搜索员只负责查公开信息。
  • 代码生成器只写 Python。

而这一切,都需要一个**总控(Supervisor)**来调度。

2. 初始化:工具配置与核心状态定义

首先,我们需要定义工具和状态。这里的核心技术点在于状态(State)的设计

代码实现:

python 复制代码
# 模拟工具
@tool
def search_internal_docs(query:str):
    """搜索公司内部文档获取政策信息"""
    return "根据公司手册,年假为15天"

@tool
def search_web(query:str):
    """通过搜索引擎获取最新的公开信息"""
    return "据TechCrunch报道,LangGraph 0.6已支持持久化记忆"

@tool
def generate_code(requirement:str):
    """根据需求生成可运行的Python代码"""
    return "python\nprint('Hello from Code writer!')"


# 共享状态定义
class AgentState(TypedDict):
    messages: Annotated[list,add_messages] # 自动累积对话历史
    next_speaker: str

深度解读:为什么不用预置的 MessagesState?

LangGraph 提供了一个预置类MessagesState ,但它只有一个 messages 字段。在多智能体编排中,我们需要额外的字段 next_speaker 来存储 Supervisor 的决策结果,因此必须自定义 TypedDict

关于Annotated[list, add_messages]

  • Annotated 的作用:在这里它不仅仅是类型提示,它会被 LangGraph 框架读取。
  • add_messages 的机制 :它的作用是告诉框架,当节点返回新的 messages 时,不要直接覆盖旧列表,而是追加(Append) 。这是实现自动累积对话历史的关键。

3. 定义节点:专家与总控

这一步我们需要明确 "大脑""手脚" 的分工。

代码实现:

ini 复制代码
# 专家节点
def rag_expert(state:AgentState):
    prompt = "你是公司知识库专家,只基于内部文档回答问题。回答应简洁明了,直接给出最终结论。请在回答的最后一行加上:(任务已完成)"
    messages = [SystemMessage(content=prompt)]+state['messages']
    tools = [search_internal_docs]
    response = llm.bind_tools(tools).invoke(messages)
    return {'messages':[response]}

def web_research(state:AgentState):
    prompt = "你是互联网研究员,擅长用搜索引擎获取最新公开信息。回答应简洁明了,直接给出最终结论。请在回答的最后一行加上:(任务已完成)"
    messages = [SystemMessage(content=prompt)]+state['messages']
    tools = [search_web]
    response = llm.bind_tools(tools).invoke(messages)
    return {'messages':[response]}

def code_writer(state:AgentState):
    prompt = "你是python工程师,只生成可运行代码,不解释。回答应简洁明了,直接给出最终结论。请在回答的最后一行加上:(任务已完成)"
    messages = [SystemMessage(content=prompt)]+state['messages']
    tools = [generate_code]
    response = llm.bind_tools(tools).invoke(messages)
    return {'messages':[response]}

# 总控节点
def supervisor(state:AgentState):
    supervisor_prompt = """
        你是一个任务协调员。你的目标是管理专家来解决用户的问题。

        当前对话需要以下专家参与:
        - rag_expert:涉及公司政策、内部流程
        - web_research:涉及外部新闻、公开数据
        - code_writer:需要生成代码

        【决策逻辑】
        1. **检查历史记录**:先看上一个回复是否已经完整回答了用户的初始问题。
        2. **如果已经回答完毕**:必须输出 'FINISH'。
        3. **如果尚未回答或需要补充**:根据当前缺少的步骤,选择下一个最合适的专家。

        请只输出专家名字或 'FINISH',不要输出任何其他解释。
        """
    messages = [SystemMessage(content=supervisor_prompt)]+state['messages']
    response = llm.invoke(messages)
    next_speaker = response.content.strip()
    return {"next_speaker":next_speaker}

逻辑要点:

  • 专家节点:遵循 ReAct 模式。必须使用 bind_tools 将工具绑定给 LLM,否则模型无法触发工具调用。我们还在 Prompt 中加入了"任务已完成"的标记,辅助总控判断。
  • 总控节点 :不执行具体任务,只负责 "看" 。它的输出直接决定了 next_speaker 的值,从而控制整个图的流向。

4. 构建协作图:核心架构逻辑

这是本章最复杂的部分。我们需要通过 StateGraph 将节点织成一张网。

代码实现:

python 复制代码
# 路由函数定义
def route_supervisor(state:AgentState):
    if state["next_speaker"]=="FINISH":
        return END
    return state["next_speaker"]

def should_continue(state:AgentState):
    last_msg = state["messages"][-1]
    if hasattr(last_msg,"tool_calls") and last_msg.tool_calls:
        return "tools"
    return "supervisor"

def route_after_tool(state:AgentState):
    # 工具执行完后,通过next_speaker知道是谁调用的,路由回去
    return state["next_speaker"]


# 添加工具节点
tools = [search_internal_docs,search_web,generate_code]
tool_node = ToolNode(tools)


# 构建协作图
workflow = StateGraph(AgentState)

# 1. 添加节点
workflow.add_node("supervisor",supervisor)
workflow.add_node("rag_expert",rag_expert)
workflow.add_node("web_research",web_research)
workflow.add_node("code_writer",code_writer)
workflow.add_node("tools",tool_node)


# 2. 总控回路
workflow.add_edge(START,"supervisor")
workflow.add_conditional_edges("supervisor",route_supervisor)

# 3. 专家节点的ReAct循环
for member in ["rag_expert","web_research","code_writer"]: # 为每个专家添加条件边:决定是去执行工具还是回总控
    workflow.add_conditional_edges(
        member,
        should_continue,
        {"tools":"tools","supervisor":"supervisor"}
    )

# 4.工具节点闭环
workflow.add_conditional_edges( # 工具执行完,根据next_speaker路由回原来的专家
    "tools",
    route_after_tool
)

app = workflow.compile()

技术深挖:理解图构建中的三个关键点

第一点:条件边的两种写法(动态 vs 静态)

  • 直接返回(动态路由) :如 route_supervisor 。函数直接返回节点名称字符串 。适用于目标节点不确定的情况(Supervisor 可能返回任何专家的名字)。

    perl 复制代码
    def route_supervisor(state:AgentState):
        if state["next_speaker"]=="FINISH":
            return END
        return state["next_speaker"]
    
    # ...
    
    workflow.add_conditional_edges("supervisor",route_supervisor)
  • 字典映射(静态结构) :如 should_continue 。虽然函数返回的是 "tools",但我们在 add_conditional_edges 中显式定义 了 {"tools": "tools"} 的映射 。这适用于结构固定的场景(三选一或二选一)。

    python 复制代码
    def should_continue(state:AgentState):
        last_msg = state["messages"][-1]
        if hasattr(last_msg,"tool_calls") and last_msg.tool_calls:
            return "tools"
        return "supervisor"
    
    # ...
    
    for member in ["rag_expert","web_research","code_writer"]: # 为每个专家添加条件边:决定是去执行工具还是回总控
        workflow.add_conditional_edges(
            member,
            should_continue,
            {"tools":"tools","supervisor":"supervisor"}
        )

第二点:For 循环的本质(构建时 vs 运行时) 代码中的 for member in [...] 并不是让程序运行时轮流跑一遍专家。 这是构建阶段(Build Time) 的代码。它的作用是 "批量注册" :我们告诉图,这三个专家节点,它们"出门"后的规则是一模一样的(要么去修工具,要么回总控)。这避免了重复写三遍相同的 add_conditional_edges 代码。

第三点:add_conditional_edges 的回环特性 这一行代码相当于给节点安装了一个永久生效的 "单向任意门"

  • 回环 :一旦定义了 Expert -> check -> Tools ,以后无论流程第几次回到 Expert 节点,离开时都会触发这个检查。

  • 单向性:虽然我们在逻辑上实现了 Expert -> check -> Tools 的闭环,但这个闭环是由两条单向边拼凑的:

    1. Expert -> Tools(通过 should_continue 定义)
    2. Tools -> Expert(通过 route_after_tool 定义,必须显式写出来,否则程序会卡死在 Tools 节点)。

5. 测试运行

最后,通过打印日志来验证多智能体的协作流。

代码实现:

python 复制代码
# 测试运行
if __name__ == '__main__':
    user_input = "公司年假多少天"
    print("用户提问:",user_input)
    print('\n开始多智能体协作...\n')

    inputs = {"messages":[HumanMessage(content=user_input)]}
    # app是编译好的图,stream()会让图开始运转,并返回一个生成器
    # 图每执行完一个节点,就会产出一个step字典
    for step in app.stream(inputs):
        # 因为step是个字典,所以需要拆包拿到 节点名(Node) 与 输出内容(output)
        for node,output in step.items():
            # 有工具/专家回复
            if "messages" in output:
                msg = output["messages"][-1]
                if hasattr(msg,"tool_calls") and msg.tool_calls:
                    call = msg.tool_calls[0]
                    print(f"【{node}】调用工具 {call['name']}({call['args']})")
                else:
                    print(f"【{node}】回复:{msg.content}")
            # supervisor刚做完决策,确定下个发言人
            elif "next_speaker" in output:
                speaker = output["next_speaker"]
                print(f"【Supervisor】指定下一位发言人:{speaker}")

通过这段日志,我们可以清晰地观测到系统的运行脉络

Supervisor 识别意图 -> 指派 RAG 专家 -> RAG 专家发现需要查库 -> 调用 Tool ->

Tool 返回结果 -> RAG 专家生成最终回复 -> Supervisor 确认任务完成 (FINISH)。

其工作流大致如下图所示:

scss 复制代码
           (开始)
             |
             v
   +--> [ 🧠 总控 Supervisor ] --(完成)--> (( END ))
   |         |
   |         | (1. 指派)
   |         v
   |    [ 👷 专家 (Workers) ] <====> [ 🛠️ 工具 (Tools) ]
   |         |        ^         (2. 干活循环)
   |         |        |
   +---------+--------+
      (3. 汇报结果)

总结:三种能力如何组合?

能力 解决的问题 典型场景
Human-in-the-Loop 不可逆操作的安全风险 发邮件、删数据、审批流
Graph-as-a-Tool 复杂内部逻辑的维护难题 带重试的 API 调用、多步验证
Multi-Agent 编排 单 Agent 职责爆炸 会议纪要、故障排查、需求分析

四、综合实战

在前三章中,我们分别掌握了 安全锁、 黑盒工具、 团队管理 。现在,我们不再纸上谈兵,而是将这三者融为一体,构建一个生产级架构的IT运维智能体

业务场景设定

我们模拟一个服务器故障排查场景:

总控(Supervisor) :负责接收报警,调度专家

日志专家(Log Expert) :负责分析服务器日志。它手中的工具 analyze_logs 是一个子图,包含SSH连接重试、日志提取等复杂逻辑(模拟网络不稳定的真实环境)。

运维专家(Ops Expert) :负责执行修复。它手中的工具 restart_services 是敏感操作,必须经过人工审批(Human-in-the-Loop)。

代码实现

python 复制代码
import os
import time
from config import OPENAI_API_KEY,LANGCHAIN_API_KEY
from langchain_openai import ChatOpenAI
from langchain.tools import tool
from langchain_core.messages import HumanMessage,SystemMessage
from langgraph.graph import StateGraph,MessagesState,START,END
from langgraph.prebuilt import ToolNode
from langgraph.checkpoint.memory import MemorySaver
from typing import TypedDict,Annotated
from langgraph.graph.message import add_messages

# LangSmith调试
os.environ["LANGCHAIN_TRACING_V2"] = "true" # 总开关,决定启用追踪功能
os.environ["LANGCHAIN_PROJECT"] = "supervisor_agent_ops_system" # 自定义项目名
os.environ["LANGCHAIN_API_KEY"] = LANGCHAIN_API_KEY

llm = ChatOpenAI(
    model="deepseek-chat",
    api_key=OPENAI_API_KEY,
    base_url="https://api.deepseek.com"
)

# === 一、Graph-as-a-Tool ===
# === 模拟一个不稳定的SSH日志查询过程 ===

class SSHState(TypedDict):
    target_ip: str
    attempt: int
    logs: str

def connect_ssh(state:SSHState):
    """模拟SSH连接,第一次连接必定超时"""
    print(f'    [子图]正在尝试连接服务器{state["target_ip"]}(第{state["attempt"]}次)')
    if state['attempt'] == 1:
        return {'logs':'ERROR:Connection Timed Out','attempt':state['attempt']+1}
    return {'logs':'CONNECTED','attempt':state['attempt']+1}

def grep_system_logs(state:SSHState):
    """连接成功后读取日志"""
    if state["logs"] == "CONNECTED":
        # 打印查到的结果
        return {'logs':f'SUCCESS: Retrieved logs from {state['target_ip']}:[ERROR: OutOfMemory at line 4032]'}
    return {'logs':state['logs']} # 保持错误状态

def ssh_routing(state:SSHState):
    """路由逻辑:如果连接失败且尝试次数少于3,重试"""
    if "ERROR" in state['logs'] and state["attempt"] <= 2:
        return "connect"
    return "grep"

# 构建子图
ssh_workflow = StateGraph(SSHState)
ssh_workflow.add_node("connect",connect_ssh)
ssh_workflow.add_node("grep",grep_system_logs)

ssh_workflow.add_edge(START,"connect")
ssh_workflow.add_conditional_edges("connect",ssh_routing,{"connect":"connect","grep":"grep"})
ssh_workflow.add_edge("grep",END)

ssh_app = ssh_workflow.compile()

# 将子图封装为工具
@tool
def analyze_server_logs(ip_address:str):
    """使用SSH连接服务器并分析最近的错误日志(内含自动重连机制)"""
    result = ssh_app.invoke({"target_ip":ip_address,"attempt":1,"log":""})
    return result['logs']



# === 二、Human-in-the-Loop ===
# === 重启服务,高危操作,需要审批 ===

@tool
def restart_service(service_name:str):
    """重启指定的服务器服务"""
    return f"服务[{service_name}]已成功重启,系统负载已恢复正常"



# === 三、Multi-Agent 编排 ===
# === 总控调度 + 专家分工 ===

# 1. 共享状态
class AgentState(TypedDict):
    messages:Annotated[list,add_messages]
    next_speaker:str

# 2. 专家节点
def log_expert(state:AgentState):
    prompt = "你是日志分析专家,使用工具分析服务器日志,找出报错原因。回答需简洁。"
    messages = [SystemMessage(content=prompt)] + state['messages']
    # 绑定子图工具
    tools = [analyze_server_logs]
    response = llm.bind_tools(tools).invoke(messages)
    return {"messages":[response]}

def ops_expert(state:AgentState):
    prompt = "你是运维专家。当收到修复指令时,请立即调用 'restart_service' 工具进行修复,不要输出任何额外的解释文本。"
    messages = [SystemMessage(content=prompt)] + state['messages']
    tools = [restart_service]
    # 绑定敏感工具
    response = llm.bind_tools(tools).invoke(messages)
    return {"messages":[response]}

# 3. 总控节点(supervisor)
def supervisor(state:AgentState):
    prompt = """
    你是 IT 运维总指挥。
    专家列表:
    - log_expert
    - ops_expert

    决策逻辑:
    1. 未知原因 -> log_expert
    2. 已知原因(如OOM、报错) -> ops_expert
    3. 修复完成 -> FINISH

    【输出约束】
    仅输出下一个专家的名字(如 log_expert),不要包含任何其他字符或标点。
    """
    messages = [SystemMessage(content=prompt)] + state['messages']
    response = llm.invoke(messages)
    return {"next_speaker":response.content.strip()}

# 4. 路由逻辑
def route_supervisor(state:AgentState):
    if state['next_speaker'] == "FINISH":
        return END
    return state['next_speaker']

def should_continue(state:AgentState):
    last_msg = state['messages'][-1]
    if hasattr(last_msg,"tool_calls") and last_msg.tool_calls:
        return "tools"
    return "supervisor"

def route_after_tool(state:AgentState):
    return state["next_speaker"]


# === 四、构建主图与集成 ===

# 工具集合
all_tools = [analyze_server_logs,restart_service]
tool_node = ToolNode(all_tools)

# 构建主图
workflow = StateGraph(AgentState)

workflow.add_node("supervisor",supervisor)
workflow.add_node("log_expert",log_expert)
workflow.add_node("ops_expert",ops_expert)
workflow.add_node("tools",tool_node)

workflow.add_edge(START,"supervisor")
workflow.add_conditional_edges("supervisor",route_supervisor)
for member in ["log_expert","ops_expert"]:
    workflow.add_conditional_edges(
        member,
        should_continue,
        {"tools":"tools","supervisor":"supervisor"}
    )
workflow.add_conditional_edges("tools",route_after_tool)

# 编译图:加入记忆与中断机制(Human-in-the-Loop)
# 注: 我们在所有工具执行前都暂停,但在运行时进行逻辑判断
app = workflow.compile(
    checkpointer=MemorySaver(),
    interrupt_before=["tools"]
)

# === 五、运行时逻辑(模拟生产环境的交互) ===

if __name__ == '__main__':
    # 模拟一次完整的故障处理流程
    user_input = "服务器 192.168.1.100 报警,响应极慢,请处理。"
    config = {
        "configurable": {"thread_id": "incident_001"}
    }

    print(f'收到报警 : {user_input}')
    inputs = {"messages":[HumanMessage(content=user_input)]}

    # 循环执行,直到任务结束
    while 1:
        # 1. 执行图直到中断或结束
        for _ in app.stream(inputs,config,stream_mode="values"):
            pass

        # 2. 检查当前状态
        snapshot = app.get_state(config)
        next_tasks = snapshot.next

        # 没有下一步,任务结束
        if not next_tasks:
            print(f"    最终报告:{snapshot.values['messages'][-1].content}")
            break

        # 3. 处理中断:判断是哪个工具被调用
        if "tools" in next_tasks:
            last_msg = snapshot.values['messages'][-1]
            tool_call = last_msg.tool_calls[0]
            tool_name = tool_call["name"]

            print(f'\n[系统暂停] 请求调用工具:{tool_name}')

            # 策略A: 自动放行安全工具(Graph-as-a-Tool)
            if tool_name == "analyze_server_logs":
                print('     -> 这是一个查询类工具,系统自行批准。')
                inputs = None # 继续执行
                continue

            # 策略B: 拦截高危工具(Human-in-the-Loop)
            elif tool_name == "restart_service":
                print("     -> ⚠️ 警告: 这是一个高危操作!")
                user_approval = input("     -> 请人工审批 (输入 'yes' 允许重启):")

                if user_approval == "yes":
                    print('    -> ✅ 审批通过,正在执行...')
                    inputs = None # 继续执行
                else:
                    print('    -> ❌️ 审批拒绝,任务终止!')
                    # 实际系统中,应通过 ToolMessage 反馈人工拒绝,使 LLM 能继续响应;
                    # 当前demo为简化,直接退出
                    break

运行效果

当你运行这段代码时,你会看到一个非常精彩的交互过程

  1. 总控调度:Supervisor收到报警,首先指派log_expert。

  2. 子图自动重试:

  • log_expert 调用 analyze_server_logs;
  • 系统识别这是安全工具,自动批准;
  • 控制台打印出子图内部逻辑:第一次连接超时 -> 自动重试 -> 连接成功 -> 获取到 OutOfMemory 错误
  1. 二次调度:
  • log_expert 汇报:"发现了 OOM 错误"
  • Supervisor 决策:"这是内存溢出,需要重启,指派ops_expert。"
  1. 人工拦截
  • ops_expert 试图调用 restart_services。
  • 系统识别这是高危工具,强制暂停。
  • 控制台提示:⚠️ 警告: 这是一个高危操作!
  1. 最终修复:
  • 输入yes
  • 服务重启成功,Supervisor 输出 FINISH。

通过LangSmith看一下详细输出:

流程运行完美。

总结、

知识点概括Human-in-the-Loop(人工干预)| Graph-as-a-Tool(图即工具)| Multi-Agent 多智能体编排

通过这个实战案例,我们证明了 LangGraph 不仅仅是一个简单的 DAG (有向无环图------有方向但不能形成循环),它是一套构筑复杂系统的骨架

Graph-as-a-Tool: 让我们能把脏活累活藏起来,主流程保持干净。

Human-in-the-Loop: 让我们敢于把Agent放入生产环境,让人工确认作为最终防线。

Multi-Agent: 让系统具备了可扩展性,加功能只是加个专家节点而已。

三者结合,即可构建企业级可信 Agent 系统。


预告:10 篇 《 MCP 基础篇》

09 篇的所有专家都运行在同一Python进程中。但如果:

  • RAG 服务部署在公司内网?
  • 代码生成器是 Java 写的 REST API?
  • 你想直接调用高德地图、12306的官方服务?

10篇,我们将引入MCP (Model Context Protocol)协议,

让任何符合标准的远程服务,都能成为你的工具------无需写一行@tool!

相关推荐
烟袅3 小时前
用 llm + SQLite 实现自然语言到 SQL 的智能转换:一个实战案例
sqlite·llm·agent
samroom6 小时前
langchain+ollama+Next.js实现AI对话聊天框
javascript·人工智能·langchain
Q180809519 小时前
FPGA实现CAN通信:基于SJA1000 FPGA的收发设计
langchain
AI大模型10 小时前
全面掌握 AI Agent 30 个高频面试的问题与解答相关的核心知识点!
程序员·llm·agent
liliangcsdn12 小时前
langgraph基于ReACT集成langchain定义的功能
langchain
大数据追光猿1 天前
LangChain / LangGraph / AutoGPT / CrewAI / AutoGen 五大框架对比
经验分享·笔记·python·langchain·agent
川Princess1 天前
【面试经验】百度Agent架构研发工程师一面
面试·职场和发展·架构·agent
大模型教程1 天前
AI智能体(Agent)保姆级入门指南,零基础小白也能轻松上手
程序员·llm·agent
烟袅1 天前
使用 OpenAI SDK 调用 Tools 实现外部工具集成
python·openai·agent