LangGraph进阶:用子图与人机交互打造生产级Agent

【Agent六层修炼】LangGraph高级特性:子图、人机交互、流式输出、可观测性,生产级Agent的最后一公里

第16期 · 框架工程层·LangGraph高级特性。上一期讲了LangGraph四大核心概念,能跑通一个基础Agent了。但离生产级可用还差最后一公里:复杂流程怎么组织?高风险操作怎么人工确认?怎么流式输出提升体验?怎么追踪调试?这一期讲子图、人机交互、流式输出、可观测性、错误处理、性能优化、部署,最后用子图构建一个完整的Multi-Agent系统。


一、开头:能跑了,但离生产级还差什么?

上一期我们用LangGraph的四大核心概念(State/Node/Edge/Checkpoint),从0到1构建了一个生产级Agent。它能跑、能调用工具、有人工确认、有Checkpoint。

但你把它放到真实生产环境,会发现还有一堆问题:

问题1:流程太复杂,一个图画不下

  • ● 你的Agent有10个节点、20条边,图变成了一团乱麻
  • ● 想复用某一段流程(如工具调用循环),但只能复制粘贴
  • ● Multi-Agent系统,每个Agent自己就是一个复杂的图,怎么组织?

问题2:人机交互太简单

  • ● 上一期只做了"确认/拒绝",但真实场景需要更多:
    • ◇ 人可以修改Agent的输出(如改一下邮件内容再发)
    • ◇ 人可以补充输入(如Agent查不到信息,人手动提供)
    • ◇ 人可以审批(多级审批,主管→总监→CEO)
    • ◇ 人可以随时介入,暂停、修改、继续

问题3:用户体验差,等半天没反应

  • ● Agent调用大模型+工具,可能要等10秒才出结果
  • ● 用户盯着空白页面,不知道Agent在干嘛
  • ● 需要流式输出,让用户看到Agent的每一步

问题4:出了问题不知道在哪

  • ● Agent返回了错误答案,但不知道是哪一步错了
  • ● 不知道大模型调用了几次、每次花了多少Token、耗时多久
  • ● 没有日志、没有追踪、没有监控,调试全靠print

问题5:性能和稳定性

  • ● 节点能不能并行执行?怎么异步?
  • ● 工具调用失败了怎么重试?重试几次?
  • ● 怎么缓存重复的大模型调用?
  • ● 怎么部署?怎么扩缩容?

这些问题,就是"能跑"和"生产级可用"之间的差距。这一期我们就把这些坑全部填上。


二、子图(Subgraph):复杂流程的组织方式

2.1 为什么需要子图?

当你的图只有2-3个节点时,不需要子图。但当图有10+个节点、或者有可复用的流程段、或者是Multi-Agent系统时,就需要子图了。

子图解决3个问题:

问题 子图的解法
图太复杂 把相关节点打包成子图,主图只看子图之间的关系,不看内部细节
流程复用 一段流程(如工具调用循环)可以封装成子图,在多个地方复用
Multi-Agent 每个Agent是一个子图,主图负责编排多个Agent子图之间的协作

类比: 子图就像编程中的函数------把一段逻辑封装起来,对外只暴露输入输出,内部细节隐藏。主图调用子图,就像主函数调用子函数。

2.2 怎么定义子图?

子图的定义和普通图完全一样------定义State、Node、Edge,然后compile。唯一的区别是:子图会被作为一个节点添加到主图中。

复制代码
# ========== 定义子图:工具调用循环 ==========
class ToolLoopState(TypedDict):
    messages: Annotated[list, operator.add]
    loop_count: int

def tool_agent_node(state):
    """子图的Agent节点"""
    response = llm.invoke(state["messages"], tools=tools)
    return {"messages": [response], "loop_count": state.get("loop_count", 0) + 1}

def should_continue(state):
    if state.get("loop_count", 0) >= 10:
        return END
    last = state["messages"][-1]
    if hasattr(last, "tool_calls") and last.tool_calls:
        return "tools"
    return END

# 构建子图
tool_loop = StateGraph(ToolLoopState)
tool_loop.add_node("agent", tool_agent_node)
tool_loop.add_node("tools", ToolNode(tools))
tool_loop.set_entry_point("agent")
tool_loop.add_conditional_edges("agent", should_continue, {"tools": "tools", END: END})
tool_loop.add_edge("tools", "agent")
tool_loop_app = tool_loop.compile()  # 子图compile后就是一个可调用的对象

# ========== 定义主图,把子图作为节点 ==========
class MainState(TypedDict):
    user_input: str
    messages: Annotated[list, operator.add]
    final_answer: str

def input_node(state):
    """主图的输入处理节点"""
    return {"messages": [HumanMessage(content=state["user_input"])]}

def output_node(state):
    """主图的输出整理节点"""
    last = state["messages"][-1]
    return {"final_answer": last.content}

# 构建主图
main_graph = StateGraph(MainState)
main_graph.add_node("input", input_node)
main_graph.add_node("tool_loop", tool_loop_app)  # 子图作为节点!
main_graph.add_node("output", output_node)
main_graph.set_entry_point("input")
main_graph.add_edge("input", "tool_loop")
main_graph.add_edge("tool_loop", "output")
main_graph.add_edge("output", END)

main_app = main_graph.compile()

关键点:

    1. 子图的定义和普通图完全一样
    1. 子图compile后,就可以作为一个节点添加到主图中
    1. 主图调用子图时,会把主图的State传给子图,子图执行完后把更新的State返回给主图

2.3 子图和主图的State怎么通信?

这是子图最容易踩的坑------主图和子图的State可能不一样,怎么传递数据?

方式1:State字段完全一致(最简单)

主图和子图的State字段完全一样,直接传递,不需要转换。

复制代码
# 主图和子图用同一个State定义
class SharedState(TypedDict):
    messages: Annotated[list, operator.add]

方式2:子图State是主图State的子集

子图只需要主图State的一部分字段。LangGraph会自动传递匹配的字段,不匹配的忽略。

复制代码
# 主图State
class MainState(TypedDict):
    messages: Annotated[list, operator.add]
    user_input: str
    user_profile: dict  # 子图不需要这个

# 子图State(是主图的子集)
class SubState(TypedDict):
    messages: Annotated[list, operator.add]  # 只需要messages

方式3:用输入输出转换(最灵活)

如果主图和子图的State差异很大,用input和output参数做转换。

复制代码
# 主图添加子图节点时,指定输入输出转换
main_graph.add_node(
    "sub_agent",
    sub_graph_app,
    input=lambda main_state: {"messages": main_state["messages"]},  # 主图→子图
    output=lambda sub_state: {"messages": sub_state["messages"]}     # 子图→主图
)

实际项目建议:尽量让主图和子图的State字段一致或子集关系,减少转换逻辑。

2.4 子图的Checkpoint

子图默认共享主图的Checkpoint------主图的Checkpoint会包含子图执行的每一步。

也可以给子图单独配置Checkpoint,但通常不需要,共享就好。


三、人机交互(Human-in-the-loop)

3.1 为什么需要人机交互?

Agent不是万能的,有些场景必须有人参与:

场景 说明
高风险操作 发邮件、删数据、转账、发布内容,必须人确认
信息不足 Agent查不到关键信息,需要人手动提供
质量把控 Agent生成的内容需要人审核修改后才能发布
多级审批 重要决策需要多级审批(主管→总监→CEO)
异常处理 Agent遇到无法处理的异常,需要人介入
随时介入 用户想随时暂停、修改、继续Agent的执行

LangGraph的人机交互核心是interrupt()机制------在节点中调用interrupt,图会暂停执行,把控制权交还给人,人处理完后可以继续执行。

3.2 interrupt的基本用法

复制代码
from langgraph.types import interrupt

def review_node(state):
    """审核节点:暂停等人确认"""
    content = state["messages"][-1].content

    # interrupt会暂停图的执行,返回括号里的内容给调用方
    # 人处理完后,用Command(resume=...)继续,resume的值会作为interrupt的返回值
    decision = interrupt({
        "type": "review",
        "content": content,
        "question": "请审核以上内容,确认后继续"
    })

    # decision是人传入的值(如"approve"、"reject"、或修改后的内容)
    if decision == "approve":
        return {"status": "approved"}
    elif decision == "reject":
        return {"status": "rejected"}
    else:
        # 人修改了内容
        return {"modified_content": decision, "status": "modified"}

调用方的代码:

复制代码
config = {"configurable": {"thread_id": "session-1"}}

# 第一次调用:会在review节点中断
result = app.invoke({"messages": [...]}, config=config)

# result中包含interrupt的信息
print(result)  # 会显示 {"type": "review", "content": "...", "question": "..."}

# 人处理完后,用Command(resume=...)继续
from langgraph.types import Command
result = app.invoke(Command(resume="approve"), config=config)

3.3 4种人机交互模式

模式1:确认/拒绝(最简单)

人只需要说"同意"或"拒绝"。

复制代码
def confirm_node(state):
    decision = interrupt({"question": "是否确认发送?"})
    if decision == "yes":
        return {"confirmed": True}
    return {"confirmed": False}

模式2:修改内容(人可以编辑)

人可以修改Agent的输出,然后继续。

复制代码
def edit_node(state):
    draft = state["draft"]
    # 人可以返回修改后的内容
    modified = interrupt({"type": "edit", "draft": draft, "instruction": "请修改后返回"})
    return {"draft": modified}  # 用人修改后的内容替换

模式3:补充输入(人提供信息)

Agent信息不足,让人补充。

复制代码
def human_input_node(state):
    if state.get("missing_info"):
        info = interrupt({"type": "input", "question": f"请提供{state['missing_info']}"})
        return {"human_provided_info": info}
    return {}

模式4:多级审批(多个人依次确认)

重要决策需要多级审批,每一级都是一个interrupt节点。

复制代码
def manager_approve(state):
    decision = interrupt({"level": "manager", "question": "主管审批"})
    return {"manager_decision": decision}

def director_approve(state):
    if state["manager_decision"] != "approve":
        return {"final_decision": "rejected"}
    decision = interrupt({"level": "director", "question": "总监审批"})
    return {"director_decision": decision}

def ceo_approve(state):
    if state.get("director_decision") != "approve":
        return {"final_decision": "rejected"}
    decision = interrupt({"level": "ceo", "question": "CEO审批"})
    return {"final_decision": decision}

3.4 人机交互的最佳实践

    1. interrupt的信息要清晰:告诉人"需要做什么、怎么做、返回什么格式"
    1. 要有超时机制:人长时间不处理,应该超时自动拒绝或升级
    1. 要有记录:谁在什么时候做了什么决定,都要记录到State或日志中
    1. 可以随时介入:不只是在interrupt节点,人可以随时暂停图、修改State、继续
    1. 高风险操作必须人机交互:发邮件、删数据、转账等,不要让Agent自动执行

四、流式输出(Streaming)

4.1 为什么需要流式输出?

Agent执行可能要10秒+(大模型调用+工具调用+多轮循环),如果等全部执行完才输出,用户体验很差------盯着空白页面,不知道在干嘛。

流式输出让用户看到Agent的每一步:

  • ● 大模型正在输出什么字
  • ● 调用了什么工具
  • ● 工具返回了什么
  • ● 当前是第几轮循环

4.2 stream()的基本用法

LangGraph的stream()方法返回一个生成器,逐步输出执行过程。

复制代码
config = {"configurable": {"thread_id": "session-1"}}

# 流式输出
for chunk in app.stream({"messages": [...]}, config=config):
    # chunk是一个字典,key是节点名,value是该节点的输出
    for node_name, output in chunk.items():
        print(f"[{node_name}] {output}")

4.3 3种流式模式

模式1:stream_mode="values"(默认)

每步输出当前完整的State值。

复制代码
for chunk in app.stream({"messages": [...]}, config=config, stream_mode="values"):
    # chunk是当前完整的State
    print("当前State:", chunk)

模式2:stream_mode="updates"

每步只输出该节点更新的字段(增量)。

复制代码
for chunk in app.stream({"messages": [...]}, config=config, stream_mode="updates"):
    # chunk是 {节点名: {更新的字段}}
    for node_name, update in chunk.items():
        print(f"节点 {node_name} 更新了: {update}")

模式3:stream_mode="messages"(大模型Token级流式)

大模型输出的每个Token都流式输出,这是用户体验最好的模式。

复制代码
for chunk in app.stream({"messages": [...]}, config=config, stream_mode="messages"):
    # chunk包含大模型输出的Token
    if isinstance(chunk, tuple):
        msg, metadata = chunk
        if hasattr(msg, "content") and msg.content:
            print(msg.content, end="", flush=True)  # 逐字输出

实际项目中通常组合使用:

  • ● 用messages模式流式输出大模型的回复(用户看到逐字输出)
  • ● 用updates模式展示工具调用状态(用户看到"正在调用搜索工具...")

4.4 前端怎么接流式?

前端用SSE(Server-Sent Events)或WebSocket接收流式数据,逐字渲染。

复制代码
// 前端示例:用fetch接收SSE流式输出
const response = await fetch('/api/agent', {
    method: 'POST',
    body: JSON.stringify({message: userInput})
});

const reader = response.body.getReader();
const decoder = new TextDecoder();

while (true) {
    const {done, value} = await reader.read();
    if (done) break;

    const chunk = decoder.decode(value);
    // 解析chunk,渲染到页面
    renderChunk(chunk);
}

五、可观测性(Observability)

5.1 为什么需要可观测性?

生产级Agent必须能回答这些问题:

  • ● Agent执行了几步?每步做了什么?
  • ● 大模型调用了几次?每次花了多少Token?耗时多久?
  • ● 工具调用了几次?成功了几次?失败了几次?
  • ● 哪一步出错了?错误信息是什么?
  • ● 用户的问题,Agent是怎么一步步解决的?

没有可观测性,Agent就是一个黑盒------出了问题不知道在哪,优化不知道从哪下手。

5.2 LangSmith:官方可观测性平台

LangSmith是LangChain官方的可观测性平台,和LangGraph深度集成,几行代码就能接入。

复制代码
# 配置LangSmith
import os
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_API_KEY"] = "your-langsmith-api-key"
os.environ["LANGCHAIN_PROJECT"] = "my-agent-project"

# 然后正常运行Agent,所有调用都会自动追踪到LangSmith
result = app.invoke({"messages": [...]}, config=config)

接入后,在LangSmith后台可以看到:

  • ● 每次运行的完整Trace(每一步的输入输出、耗时、Token)
  • ● 大模型调用的详细信息(Prompt、Response、Token数、耗时)
  • ● 工具调用的详细信息(参数、返回值、耗时)
  • ● 错误和异常的完整堆栈
  • ● 运行统计(成功率、平均耗时、Token消耗)
  • ● 可以对每次运行打分、标注、做数据集

5.3 自定义回调:不依赖LangSmith

如果不想用LangSmith,可以用LangGraph的回调机制自定义追踪。

复制代码
from langchain_core.callbacks import BaseCallbackHandler

class MyCallbackHandler(BaseCallbackHandler):
    def on_llm_start(self, serialized, prompts, **kwargs):
        print(f"[LLM Start] 模型: {serialized.get('name')}")

    def on_llm_end(self, response, **kwargs):
        token_usage = response.llm_output.get("token_usage", {})
        print(f"[LLM End] Token使用: {token_usage}")

    def on_tool_start(self, serialized, input_str, **kwargs):
        print(f"[Tool Start] 工具: {serialized.get('name')}, 输入: {input_str[:100]}")

    def on_tool_end(self, output, **kwargs):
        print(f"[Tool End] 输出: {output[:100]}")

    def on_chain_error(self, error, **kwargs):
        print(f"[Error] {error}")

# 使用回调
callback = MyCallbackHandler()
result = app.invoke(
    {"messages": [...]},
    config={"callbacks": [callback], "configurable": {"thread_id": "1"}}
)

5.4 可观测性最佳实践

    1. 生产环境必须接入可观测性:LangSmith或自建,不能没有
    1. 记录关键指标:成功率、平均耗时、Token消耗、工具调用成功率
    1. 错误要告警:Agent失败率超过阈值要告警
    1. Trace要可检索:按用户、按时间、按错误类型检索Trace
    1. 敏感信息要脱敏:Trace中可能包含用户隐私,要脱敏后再存储

六、错误处理和重试

6.1 3层错误处理

层级 处理方式 适用
节点级 try-except捕获,返回错误信息给大模型 工具调用失败、大模型调用失败
图级 配置重试策略,临时性错误自动重试 网络超时、API限流
全局 全局兜底,返回友好提示,记录错误 未预期的异常

6.2 节点级错误处理

复制代码
def robust_tool_node(state):
    """带错误处理的工具节点"""
    last_message = state["messages"][-1]
    results = []

    for tool_call in last_message.tool_calls:
        try:
            result = tool_handlers[tool_call.name](**tool_call.args)
            results.append(ToolMessage(content=str(result), tool_call_id=tool_call.id))
        except TimeoutError:
            # 超时:可以重试或返回超时信息
            results.append(ToolMessage(content="工具调用超时,请重试或换一种方式", tool_call_id=tool_call.id))
        except Exception as e:
            # 其他错误:返回错误信息,让大模型决定下一步
            results.append(ToolMessage(content=f"工具执行出错:{str(e)},请尝试其他方法", tool_call_id=tool_call.id))

    return {"messages": results}

6.3 图级重试

LangGraph支持在编译时配置重试策略。

复制代码
from langgraph.graph import StateGraph

# 配置重试:特定节点最多重试3次
graph = StateGraph(AgentState)
graph.add_node("agent", agent_node, retry=3)  # agent节点失败自动重试3次
graph.add_node("tools", tool_node, retry=2)    # tools节点失败自动重试2次

也可以用tenacity等库做更灵活的重试:

复制代码
from tenacity import retry, stop_after_attempt, wait_exponential

@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10))
def call_llm_with_retry(messages):
    """带指数退避重试的大模型调用"""
    return llm.invoke(messages, tools=tools)

6.4 全局兜底

复制代码
try:
    result = app.invoke({"messages": [...]}, config=config)
except Exception as e:
    # 全局兜底:记录错误,返回友好提示
    logger.error(f"Agent执行失败: {e}", exc_info=True)
    result = {"messages": [AIMessage(content="抱歉,我遇到了一些问题,请稍后再试或换一种方式提问。")]}

七、性能优化

7.1 并行节点

图中没有依赖关系的节点可以并行执行。

复制代码
# 用Send实现并行:从一个节点同时触发多个节点
from langgraph.types import Send

def parallel_dispatch(state):
    """同时派发多个子任务并行执行"""
    tasks = state["sub_tasks"]
    return [Send("worker", {"task": task}) for task in tasks]

graph.add_conditional_edges("dispatcher", parallel_dispatch)

7.2 异步执行

所有节点都可以是异步的,IO密集型操作(大模型调用、API调用)用异步能显著提升性能。

复制代码
async def async_agent_node(state):
    response = await llm.ainvoke(state["messages"], tools=tools)
    return {"messages": [response]}

# 异步运行
result = await app.ainvoke({"messages": [...]}, config=config)

7.3 缓存

重复的大模型调用可以缓存,节省成本和时间。

复制代码
from langchain.cache import InMemoryCache
import langchain

# 启用内存缓存
langchain.llm_cache = InMemoryCache()

# 相同的Prompt会直接返回缓存结果,不再调用大模型

生产环境可以用Redis缓存。


八、完整实战:用子图构建Multi-Agent系统

讲完了所有高级特性,我们用子图构建一个完整的Multi-Agent系统------3个Agent(研究员、写手、审稿员)协作写文章,每个Agent是一个子图,主图负责编排。

8.1 架构图

复制代码
┌─────────────────────────────────────────────┐
│              主图(编排层)                    │
│                                               │
│  用户输入                                      │
│    ↓                                          │
│  ┌──────────┐   ┌──────────┐   ┌──────────┐ │
│  │ 研究员子图 │→│  写手子图  │→│ 审稿员子图 │ │
│  │ (搜素材)  │   │ (写文章)  │   │ (审核修改)│ │
│  └──────────┘   └──────────┘   └────┬─────┘ │
│                                       │       │
│                                  不通过?│       │
│                                       ↓       │
│                                  回到写手子图   │
│                                       │       │
│                                    通过↓       │
│                                  最终文章输出    │
└─────────────────────────────────────────────┘

8.2 完整代码

复制代码
"""
Multi-Agent系统:用子图组织3个Agent协作写文章
研究员(搜素材)→ 写手(写文章)→ 审稿员(审核)→ 不通过回到写手
"""

from typing import TypedDict, Annotated
import operator
from langgraph.graph import StateGraph, END
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4o-mini", api_key="your-key")

# ========== 共享State ==========
class ArticleState(TypedDict):
    topic: str                    # 文章主题
    research_material: str        # 研究员的素材
    draft: str                    # 写手的初稿
    review_feedback: str          # 审稿意见
    final_article: str            # 最终文章
    review_count: int             # 审稿次数
    messages: Annotated[list, operator.add]

# ========== 子图1:研究员 ==========
def researcher_node(state):
    """研究员:搜索素材、整理观点"""
    prompt = f"""你是一位技术研究员。请围绕主题"{state['topic']}",整理一份结构化的素材清单。
包含:核心观点(3-5条)、关键数据/案例、参考方向。"""
    response = llm.invoke([HumanMessage(content=prompt)])
    return {"research_material": response.content, "messages": [response]}

researcher_graph = StateGraph(ArticleState)
researcher_graph.add_node("research", researcher_node)
researcher_graph.set_entry_point("research")
researcher_graph.add_edge("research", END)
researcher_app = researcher_graph.compile()

# ========== 子图2:写手 ==========
def writer_node(state):
    """写手:基于素材写文章"""
    material = state.get("research_material", "")
    feedback = state.get("review_feedback", "")

    prompt = f"""你是一位技术文章写手。
主题:{state['topic']}
素材:{material}
"""
    if feedback:
        prompt += f"\n上次审稿意见:{feedback}\n请根据审稿意见修改文章。"
    else:
        prompt += "\n请基于素材撰写一篇1500字左右的技术文章。"

    response = llm.invoke([HumanMessage(content=prompt)])
    return {"draft": response.content, "messages": [response]}

writer_graph = StateGraph(ArticleState)
writer_graph.add_node("write", writer_node)
writer_graph.set_entry_point("write")
writer_graph.add_edge("write", END)
writer_app = writer_graph.compile()

# ========== 子图3:审稿员 ==========
def reviewer_node(state):
    """审稿员:审核文章质量"""
    draft = state["draft"]
    count = state.get("review_count", 0) + 1

    prompt = f"""你是一位严格的审稿编辑。请审核以下文章,从事实准确性、逻辑连贯性、表达清晰度、结构完整性四个维度审核。
如果质量合格,输出"通过";如果不合格,输出具体的修改意见。

文章:
{draft}"""
    response = llm.invoke([HumanMessage(content=prompt)])

    return {
        "review_feedback": response.content,
        "review_count": count,
        "messages": [response]
    }

def review_decision(state):
    """审稿决定:通过还是打回"""
    feedback = state["review_feedback"]
    count = state.get("review_count", 0)

    # 审稿超过3次,强制通过(避免无限循环)
    if count >= 3:
        return "approve"

    # 简单判断:反馈中包含"通过"就通过
    if "通过" in feedback and len(feedback) < 100:
        return "approve"
    return "revise"

reviewer_graph = StateGraph(ArticleState)
reviewer_graph.add_node("review", reviewer_node)
reviewer_graph.set_entry_point("review")
reviewer_graph.add_conditional_edges(
    "review",
    review_decision,
    {"approve": END, "revise": END}  # 子图结束,主图决定下一步
)
reviewer_app = reviewer_graph.compile()

# ========== 主图:编排3个子图 ==========
def input_node(state):
    return {"messages": [HumanMessage(content=f"写一篇关于{state['topic']}的文章")]}

def finalize_node(state):
    return {"final_article": state["draft"]}

def main_decision(state):
    """主图决定:审稿通过就结束,不通过就回到写手"""
    feedback = state["review_feedback"]
    count = state.get("review_count", 0)

    if count >= 3 or ("通过" in feedback and len(feedback) < 100):
        return "finalize"
    return "writer"  # 不通过,回到写手修改

main_graph = StateGraph(ArticleState)
main_graph.add_node("input", input_node)
main_graph.add_node("researcher", researcher_app)   # 子图作为节点
main_graph.add_node("writer", writer_app)            # 子图作为节点
main_graph.add_node("reviewer", reviewer_app)        # 子图作为节点
main_graph.add_node("finalize", finalize_node)

main_graph.set_entry_point("input")
main_graph.add_edge("input", "researcher")
main_graph.add_edge("researcher", "writer")
main_graph.add_edge("writer", "reviewer")
main_graph.add_conditional_edges(
    "reviewer",
    main_decision,
    {"writer": "writer", "finalize": "finalize"}
)
main_graph.add_edge("finalize", END)

main_app = main_graph.compile()

# ========== 运行 ==========
if __name__ == "__main__":
    result = main_app.invoke({
        "topic": "AI Agent的核心组件和工作原理",
        "review_count": 0
    })
    print("最终文章:")
    print(result["final_article"])

8.3 这个Multi-Agent系统的亮点

    1. 每个Agent是一个独立子图:研究员、写手、审稿员各自封装,内部细节不暴露给主图
    1. 主图只负责编排:主图看得到的只是3个子图节点之间的流转关系,非常清晰
    1. 审稿循环:审稿不通过就回到写手修改,最多改3次(防死循环)
    1. 可扩展:加一个新角色(如排版师)就是加一个子图节点,改一下边就行
    1. 可复用:研究员子图可以在其他项目中复用

九、本期小结

框架工程层·LangGraph高级特性,核心就7句话:

    1. "能跑"和"生产级可用"之间差了6个问题:复杂流程组织、人机交互、流式输出、可观测性、错误处理、性能优化。 这一期把这些坑全部填上。
    1. 子图是复杂流程的组织方式------把相关节点打包成子图,主图只看子图之间的关系。 子图解决3个问题:图太复杂、流程复用、Multi-Agent组织。子图的定义和普通图完全一样,compile后就可以作为节点添加到主图。主图和子图的State尽量一致或子集关系,减少转换逻辑。
    1. 人机交互的核心是interrupt()机制------在节点中调用interrupt,图暂停,人处理完后用Command(resume=...)继续。 4种模式:确认/拒绝、修改内容、补充输入、多级审批。高风险操作(发邮件、删数据、转账)必须人机交互。
    1. 流式输出用stream()方法,3种模式:values(完整State)、updates(增量更新)、messages(大模型Token级逐字输出)。 实际项目组合使用:messages模式逐字输出大模型回复,updates模式展示工具调用状态。前端用SSE接收流式数据。
    1. 可观测性是生产级Agent的必需品------生产环境必须接入LangSmith或自建追踪。 需要记录:每步的输入输出、大模型Token消耗、工具调用成功率、错误堆栈。关键指标要监控告警,敏感信息要脱敏。
    1. 错误处理分3层:节点级(try-except返回错误信息给大模型)、图级(重试策略,临时性错误自动重试)、全局(兜底返回友好提示)。 性能优化3招:并行节点(无依赖的节点同时执行)、异步执行(IO密集型用异步)、缓存(重复的大模型调用缓存)。
    1. 用子图构建Multi-Agent系统是最佳实践------每个Agent是一个子图,主图负责编排。 我们实现了研究员→写手→审稿员的3-Agent系统,审稿不通过回到写手修改,最多3次。每个子图独立封装、可复用、可扩展。

到这里,LangGraph部分(第15-16期) 就全部讲完了。下一期我们对比其他主流Agent框架,帮你做技术选型。

相关推荐
Leo.yuan1 小时前
从“看全局“到“评成效“:央国企穿透式监管六步链路,哪些厂商能真正闭环
java·大数据·人工智能
狂奔solar1 小时前
眨眼检测——OCEC 112KB 模型重新定义实时眼部状态分类
大数据·人工智能·分类
shxjnpl1 小时前
高保密单位选AI会议助手,真正要看的不是功能多少
人工智能·语音识别
秦先生在广东1 小时前
OpenRig:将离散 AI Agent 编织为持久化协作系统的多智能体编排实践
人工智能
秦先生在广东1 小时前
Skills Manager:统一54+ AI编程工具Agent技能的跨平台桌面中枢
人工智能
科研online1 小时前
用可解释机器学习XGBoost+SHAP发SCI期刊的优势?
人工智能·机器学习·学习方法
秦先生在广东1 小时前
Hindsight:突破RAG瓶颈的仿生记忆系统,如何在Agent长期记忆中实现SOTA性能
人工智能
沐言Agent1 小时前
太惊艳了!2 个让你的 Codex 狠狠省 Token 的开源项目,必须收藏!
人工智能·ai·开源
moxiaoran57531 小时前
Codex控制与引导
人工智能