博客配套代码发布于github:09 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 可能返回任何专家的名字)。
perldef 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"} 的映射 。这适用于结构固定的场景(三选一或二选一)。
pythondef 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 的闭环,但这个闭环是由两条单向边拼凑的:
- Expert -> Tools(通过 should_continue 定义)
- 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
运行效果
当你运行这段代码时,你会看到一个非常精彩的交互过程:
-
总控调度:Supervisor收到报警,首先指派log_expert。
-
子图自动重试:
- log_expert 调用 analyze_server_logs;
- 系统识别这是安全工具,自动批准;
- 控制台打印出子图内部逻辑:第一次连接超时 -> 自动重试 -> 连接成功 -> 获取到 OutOfMemory 错误
- 二次调度:
- log_expert 汇报:"发现了 OOM 错误"
- Supervisor 决策:"这是内存溢出,需要重启,指派ops_expert。"
- 人工拦截
- ops_expert 试图调用 restart_services。
- 系统识别这是高危工具,强制暂停。
- 控制台提示:⚠️ 警告: 这是一个高危操作!
- 最终修复:
- 输入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!

