【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()
关键点:
-
- 子图的定义和普通图完全一样
-
- 子图compile后,就可以作为一个节点添加到主图中
-
- 主图调用子图时,会把主图的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 人机交互的最佳实践
-
- interrupt的信息要清晰:告诉人"需要做什么、怎么做、返回什么格式"
-
- 要有超时机制:人长时间不处理,应该超时自动拒绝或升级
-
- 要有记录:谁在什么时候做了什么决定,都要记录到State或日志中
-
- 可以随时介入:不只是在interrupt节点,人可以随时暂停图、修改State、继续
-
- 高风险操作必须人机交互:发邮件、删数据、转账等,不要让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 可观测性最佳实践
-
- 生产环境必须接入可观测性:LangSmith或自建,不能没有
-
- 记录关键指标:成功率、平均耗时、Token消耗、工具调用成功率
-
- 错误要告警:Agent失败率超过阈值要告警
-
- Trace要可检索:按用户、按时间、按错误类型检索Trace
-
- 敏感信息要脱敏: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系统的亮点
-
- 每个Agent是一个独立子图:研究员、写手、审稿员各自封装,内部细节不暴露给主图
-
- 主图只负责编排:主图看得到的只是3个子图节点之间的流转关系,非常清晰
-
- 审稿循环:审稿不通过就回到写手修改,最多改3次(防死循环)
-
- 可扩展:加一个新角色(如排版师)就是加一个子图节点,改一下边就行
-
- 可复用:研究员子图可以在其他项目中复用
九、本期小结
框架工程层·LangGraph高级特性,核心就7句话:
-
- "能跑"和"生产级可用"之间差了6个问题:复杂流程组织、人机交互、流式输出、可观测性、错误处理、性能优化。 这一期把这些坑全部填上。
-
- 子图是复杂流程的组织方式------把相关节点打包成子图,主图只看子图之间的关系。 子图解决3个问题:图太复杂、流程复用、Multi-Agent组织。子图的定义和普通图完全一样,compile后就可以作为节点添加到主图。主图和子图的State尽量一致或子集关系,减少转换逻辑。
-
- 人机交互的核心是interrupt()机制------在节点中调用interrupt,图暂停,人处理完后用Command(resume=...)继续。 4种模式:确认/拒绝、修改内容、补充输入、多级审批。高风险操作(发邮件、删数据、转账)必须人机交互。
-
- 流式输出用stream()方法,3种模式:values(完整State)、updates(增量更新)、messages(大模型Token级逐字输出)。 实际项目组合使用:messages模式逐字输出大模型回复,updates模式展示工具调用状态。前端用SSE接收流式数据。
-
- 可观测性是生产级Agent的必需品------生产环境必须接入LangSmith或自建追踪。 需要记录:每步的输入输出、大模型Token消耗、工具调用成功率、错误堆栈。关键指标要监控告警,敏感信息要脱敏。
-
- 错误处理分3层:节点级(try-except返回错误信息给大模型)、图级(重试策略,临时性错误自动重试)、全局(兜底返回友好提示)。 性能优化3招:并行节点(无依赖的节点同时执行)、异步执行(IO密集型用异步)、缓存(重复的大模型调用缓存)。
-
- 用子图构建Multi-Agent系统是最佳实践------每个Agent是一个子图,主图负责编排。 我们实现了研究员→写手→审稿员的3-Agent系统,审稿不通过回到写手修改,最多3次。每个子图独立封装、可复用、可扩展。
到这里,LangGraph部分(第15-16期) 就全部讲完了。下一期我们对比其他主流Agent框架,帮你做技术选型。