学习时间 :2026年6月(5天) | 核心主题:LangGraph 图编排 → 复杂工作流 → 多智能体协作 → 生产部署 → 性能优化
一、本周学习路线总览
本周以"从单 Agent 的 ReAct 循环到构建生产级、有状态、多智能体协作系统"为主线,按以下路径递进:
LangGraph基础图模型 → 复杂工作流与状态管理 → 多智能体协作 → 生产级部署 → 性能优化与监控
Day1 Day2 Day3 Day4 Day5
如果说第 1 周是学会"如何用 LCEL 搭积木",第 2 周是学会"如何让模型接入外部世界(RAG + Agent)",那么第 3 周的核心命题是------如何让 Agent 从玩具进化为可部署、可观测、可协作的生产级系统。LangGraph 提供显式的图编排能力让你精确控制每一步,状态持久化让 Agent 拥有跨会话记忆,多智能体架构让复杂任务被拆解为多个专业子 Agent 并行协作,MCP 协议让工具跨框架复用,而性能优化与成本监控则是上线的最后一道关卡。
二、Day 1:LangGraph 入门------从黑盒到透明
2.1 为什么需要 LangGraph:create_agent() 的边界
第 2 周我们使用 create_agent() 一行代码就构建出了能调用工具的智能助手。这种简洁性是高层 API 的优势------它隐藏了 ReAct 循环、工具调用解析、消息管理等复杂细节。但当你需要构建的不是"提问→思考→回答"的线性流程,而是多步骤、有条件分支、需要人工审批、甚至要在失败后从断点恢复的复杂工作流时,这种黑盒抽象就成了桎梏。
LangGraph 不是另一个更高层的 Agent 封装,而是一个低层的、透明的编排框架。它把 Agent 工作流建模为一个有向图:节点负责执行具体逻辑(调用 LLM、执行工具、进行判断),边负责决定下一步走向哪里。这种设计让你可以用 Python 函数定义节点行为,再用简单的图构建 API 把它们串联起来,最终得到一个完全可控、可观测、可持久化的状态机。
重要信号 :在 2026 年的 LangChain 生态中,LangGraph 已经从可选扩展成长为框架的核心基础设施,官方推荐的
create_agent()底层正是基于 LangGraph 构建的。理解底层之后,高层 API 的种种行为你将了然于胸。
2.2 四大核心概念:StateGraph、State、Node、Edge
LangGraph 的世界由四个核心概念构成:
StateGraph 是主要的图类,接收一个用户自定义的状态 Schema 作为类型参数。所有节点和边的读写都围绕这个共享状态进行。状态 Schema 通常使用 TypedDict 定义,也可用 Pydantic BaseModel 获得递归校验能力(性能略低于 TypedDict)。
State(状态) 的每个字段都有独立的 reducer 函数,决定节点返回的更新如何与现有状态合并。默认行为是覆盖,但通过 Annotated 类型可以指定自定义 reducer。由于消息列表是最高频的状态形式,LangGraph 提供了预置的 MessagesState,内部使用 add_messages 作为 reducer------新消息追加到列表,已存在消息(通过 ID 匹配)原地更新,同时自动将字典格式消息反序列化为 LangChain 消息对象:
python
from langgraph.graph import MessagesState
class State(MessagesState):
documents: list[str] # 追加自定义字段,自动获得覆盖式 reducer
llm_calls: int # 而 messages 字段继承父类的 add_messages reducer
Node(节点) 是图中执行实际工作的 Python 函数。接收当前状态作为第一个参数,返回一个状态更新字典(只需返回需要更新的字段,不需返回完整状态)。在底层节点函数被转换为 RunnableLambda,自动获得批处理和异步支持。
Edge(边) 定义了路由关系,分三种:
- 普通边
add_edge("A", "B"):固定路径,A 执行完无条件进入 B - 条件边
add_conditional_edges("A", routing_fn):动态路由,routing_fn接收状态返回目标节点名 - 特殊节点
START和END:分别标记图的入口和终止
关键约束 :每个节点只能选择一种出边机制(普通边、条件边或
Command),不要混用,否则多条路径可能同时被激活。
2.3 手写 ReAct 循环:LangGraph 版的 Agent
这是 Day 1 的核心实践------亲手用图 API 构建一个 ReAct Agent,理解 create_agent() 底层到底做了什么。
第一步:定义工具和模型
python
from langchain.tools import tool
from langchain.chat_models import init_chat_model
model = init_chat_model("Qwen/Qwen2.5-7B-Instruct", model_provider="openai")
@tool
def multiply(a: int, b: int) -> int:
"""Multiply a and b."""
return a * b
@tool
def add(a: int, b: int) -> int:
"""Add a and b."""
return a + b
tools = [add, multiply]
tools_by_name = {tool.name: tool for tool in tools} # 工具注册表
model_with_tools = model.bind_tools(tools) # 绑定工具到模型
tools_by_name 字典是工具执行节点中按名称查找工具的桥梁------模型返回的 tool_call 只包含名称和参数,不包含工具对象本身。
第二步:定义两个核心节点
python
from langchain.messages import SystemMessage, ToolMessage
def llm_call(state: dict):
"""LLM 决定是否调用工具"""
return {
"messages": [
model_with_tools.invoke(
[SystemMessage(content="You are a helpful assistant.")]
+ state["messages"]
)
],
"llm_calls": state.get("llm_calls", 0) + 1
}
def tool_node(state: dict):
"""执行工具调用"""
result = []
for tool_call in state["messages"][-1].tool_calls:
tool = tools_by_name[tool_call["name"]]
observation = tool.invoke(tool_call["args"])
result.append(ToolMessage(content=observation, tool_call_id=tool_call["id"]))
return {"messages": result}
关键细节:
- 系统提示放在消息列表最前面,确保模型每次推理都看到系统指令
tool_node用for循环处理------模型可以一次请求并行调用多个工具ToolMessage的tool_call_id必须与原始 tool_call 的 id 对应,否则模型无法匹配
第三步:条件路由------ReAct 的心脏
python
from typing import Literal
from langgraph.graph import StateGraph, START, END
def should_continue(state: MessagesState) -> Literal["tool_node", END]:
"""检查最后一条消息是否包含工具调用"""
last_message = state["messages"][-1]
if last_message.tool_calls:
return "tool_node"
return END
这个函数虽短,但承载了 ReAct 循环的核心决策:tool_calls 非空 → 继续执行工具;为空 → 模型已给出最终答案,终止循环。
第四步:构建和编译图
python
agent_builder = StateGraph(MessagesState)
agent_builder.add_node("llm_call", llm_call)
agent_builder.add_node("tool_node", tool_node)
agent_builder.add_edge(START, "llm_call")
agent_builder.add_conditional_edges("llm_call", should_continue, {"tool_node": "tool_node", END: END})
agent_builder.add_edge("tool_node", "llm_call") # 循环回路
agent = agent_builder.compile()
图的结构:START → llm_call → [tool_node → llm_call → ...] → END。compile() 校验图完整性并绑定运行时配置,未编译的图无法执行。
2.4 流式输出:实时观测 Agent 的每一步
stream() 方法支持多种流模式,v2 格式下统一为 StreamPart 字典:
| 模式 | 输出内容 | 适用场景 |
|---|---|---|
updates |
每个节点返回的状态更新 | 调试、查看中间结果 |
values |
每一步后的完整状态快照 | 追踪状态演变 |
messages |
LLM 的 token 级别生成文本 | 前端逐字展示 |
custom |
节点内通过 get_stream_writer() 推送的自定义事件 |
进度通知、日志 |
python
for chunk in graph.stream(inputs, stream_mode=["updates", "custom"], version="v2"):
if chunk["type"] == "updates":
for node_name, state_update in chunk["data"].items():
print(f"Node {node_name} updated: {state_update}")
elif chunk["type"] == "custom":
print(f"Custom: {chunk['data']}")
2.5 状态持久化:让 Agent 拥有记忆
在 compile() 时传入 checkpointer,LangGraph 会在每个 super-step 边界自动保存状态快照。同一 thread_id 的多次调用沿同一条时间线累积状态:
python
from langgraph.checkpoint.memory import InMemorySaver
checkpointer = InMemorySaver()
graph = agent_builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "conversation-1"}}
graph.invoke({"messages": [HumanMessage(content="北京天气怎么样?")]}, config)
# 第二轮自动继承之前的消息历史
graph.invoke({"messages": [HumanMessage(content="我刚才问了什么?")]}, config)
三种 checkpointer 实现:InMemorySaver(开发用,内存存储)→ SqliteSaver(本地持久化)→ PostgresSaver(生产环境,支持异步和加密)。
2.6 AgentExecutor vs LangGraph 对比
| 维度 | AgentExecutor | LangGraph |
|---|---|---|
| 架构模式 | 黑盒 while 循环 | 显式有向图,每步可寻址 |
| 状态管理 | 隐式闭包,外部不可见 | 显式 TypedDict/dataclass,可读写 |
| 流程控制 | 固定的思考-工具循环 | 自定义节点+条件边,任意拓扑 |
| 流式输出 | 基础 token 流 | 多模式流(状态/更新/token/自定义) |
| 持久化 | 不支持 | 内置检查点机制,支持断点恢复 |
| 人工介入 | 不支持 | 原生 interrupt() 暂停-审批-恢复 |
| 扩展性 | 修改源码或包装 Runnable | 插入节点/嵌套子图/多 Agent 协作 |
三、Day 2:LangGraph 进阶------从二元循环到复杂工作流
3.1 多节点顺序流水线
Day 1 的图只有两个节点(LLM + 工具),是 Agent 的最简骨架。真实场景需要更复杂的拓扑。以"研究→分析→写作→审核"的内容生产流水线为例:
python
class PipelineState(MessagesState):
research_notes: str # 研究阶段产出
analysis_result: str # 分析阶段产出
draft: str # 写作阶段产出
review_feedback: str # 审核阶段产出
final_output: str # 最终产出
# 四个节点依次串联
pipeline.add_edge(START, "research")
pipeline.add_edge("research", "analysis")
pipeline.add_edge("analysis", "writing")
pipeline.add_edge("writing", "review")
pipeline.add_edge("review", END)
状态设计的精髓在于 reducer 的混合使用:messages 字段继承 MessagesState 的 add_messages 追加语义(积累型数据),而 research_notes、draft 等自定义字段使用默认的覆盖式 reducer(阶段产出型数据),各取所需、互不干扰。
3.2 条件路由:让图拥有决策能力
线性流水线的致命缺陷是"审核不通过怎么办"------不能让不合格的内容直接输出。条件边让图获得"审核→返工"循环:
python
def route_after_review(state: PipelineState) -> Literal["writing", END]:
if "PASS" in state.get("review_feedback", ""):
return END
else:
return "writing"
pipeline.add_conditional_edges("review", route_after_review, {"writing": "writing", END: END})
路由映射表的左侧是函数返回值 ,右侧是图中实际节点名 。两者可以不同,比如函数返回 "pass" / "fail",映射表写成 {"pass": END, "fail": "writing"},实现返回值与节点名的完全解耦。映射表还提供编译时校验------右侧节点名如果未注册,编译阶段就报错。
3.3 Command:合并状态更新与路由
条件边的路由决策住在节点外部 的独立函数中,节点只负责执行并更新状态。Command 把路由权收回节点内部,让节点一次返回同时完成"更新状态"和"指定路由":
python
from langgraph.types import Command
def review_node(state: PipelineState) -> Command[Literal["writing", END]]:
response = model.invoke(f"审查以下文章:\n{state['draft']}")
if "PASS" in response.content:
return Command(update={"review_feedback": response.content, "final_output": state["draft"]}, goto=END)
else:
return Command(update={"review_feedback": response.content}, goto="writing")
Command 的核心优势:路由决策可以直接使用节点内部的中间变量和 LLM 即时响应,不需要先把信息写回状态再让外部函数读取。Command 还能做条件边做不到的两件事------通过 resume 配合 interrupt() 实现人工审批恢复,通过 graph 实现跨图跳转。
选择原则 :路由逻辑简单、完全基于状态字段 → 用条件边更清晰;路由决策需要节点内部运行时信息、或需要和状态更新打包 → 用
Command更合适。
3.4 Send 与并行分发
当一个节点需要将不同数据片段分发给多个下游实例并行处理时(如研究节点检索到 5 篇文章,需要 5 个分析实例各分析一篇),使用 Send:
python
from langgraph.types import Send
def continue_to_analysis(state: PipelineState):
articles = state.get("articles", [])
return [Send("analysis", {"current_article": article}) for article in articles]
pipeline.add_conditional_edges("research", continue_to_analysis)
所有 Send 目标在同一个 super-step 中并行执行,总耗时由最慢的那个决定。这是实现搜索聚合、多文档分析、批量评估等 map-reduce 模式的基础原语。
3.5 检查点深入:不仅仅是"记住对话"
检查点的四大价值:跨轮记忆 (同一 thread 多轮对话自动累积上下文)、人工介入 (暂停后从断点恢复)、时间旅行 (回放到任意历史 checkpoint 并分叉新路径)、容错恢复(节点失败后从上一个成功 checkpoint 重试)。
python
# 查看最新状态
snapshot = app.get_state(config)
print(snapshot.values) # 所有状态字段的当前值
print(snapshot.next) # 下一步要执行的节点
# 查看完整历史
for s in app.get_state_history(config):
print(f"Step {s.metadata['step']}: next={s.next}")
graph.update_state(config, values, as_node="某个节点名") 可以手动编辑任意 checkpoint 的状态,在人工介入场景中非常实用------审核者可以直接修改初稿字段,让图从修改后的状态继续执行。
3.6 人工参与:interrupt() 的艺术
LangGraph 提供两种中断方式:
| 方式 | 触发位置 | 适用场景 |
|---|---|---|
静态断点 interrupt_before/interrupt_after |
编译时固定 | 调试、步进观察 |
动态中断 interrupt() |
节点内部任意位置 | 生产环境的人工审批 |
动态中断的核心代码模式:
python
from langgraph.types import interrupt, Command
def approval_node(state: PipelineState) -> dict:
decision = interrupt({
"action": "review_draft",
"content": state.get("draft", ""),
"message": "请审核以上内容。输入 'approve' 通过,或输入修改意见。"
})
if decision == "approve":
return {"final_output": state["draft"]}
else:
return {"review_feedback": f"人工修改意见:{decision}"}
# 恢复执行时 Command(resume=value) 的值会成为 interrupt() 的返回值
app.stream_events(Command(resume=user_decision), config=config, version="v3")
四条关键规则 :① 不能用
try/except包裹interrupt()------它通过抛特殊内部异常实现暂停;② 节点内有多个interrupt()时,每次恢复都从头执行整个节点,所以调用顺序和数量必须一致,不能有条件跳过;③ payload 必须是 JSON 可序列化的简单值;④interrupt()之前的副作用代码必须是幂等的(恢复时节点从头重跑导致这些代码再次执行)。
四、Day 3:多智能体协作系统------拆解复杂性的艺术
4.1 单 Agent 的天花板
当把越来越多工具塞进同一个 Agent,或让同一个 Agent 同时处理代码审查、数据分析、客户沟通这些性质迥异的任务时,单 Agent 架构的三个瓶颈会逐渐暴露:
- 上下文膨胀:每次工具调用(网页搜索、数据库查询等)都在消息历史中堆积数据,逼近上下文窗口上限
- 工具选择退化:当可选工具超过十几个,模型在决定调用哪个工具时的准确率显著下降,混淆相似工具、忽略冷门工具
- 指令冲突:不同业务领域需要不同的系统提示、工具集、甚至模型,全塞进一个提示等于让模型每次推理都消化庞杂指令
解决方案:把复杂的单一 Agent 拆分成多个专门的子 Agent,每个负责一个明确领域,由主 Agent 担任协调者 。官方文档强调,多智能体设计的本质是上下文工程------决定每个 Agent 看到什么信息、不知道什么信息。
4.2 四种正式架构模式
| 模式 | 核心特征 | 控制权 | 适用场景 |
|---|---|---|---|
| Subagents | 子 Agent 以工具形式暴露给主 Agent 调用,子 Agent 无状态、上下文隔离 | 集中(主 Agent 全权调度) | 多领域并行任务、层级调度 |
| Handoffs | 通过状态变量驱动路由切换,控制权在 Agent 之间直接转移 | 分散(Agent 之间交接) | 多跳协作(搜索→分析→写作) |
| Skills | 单 Agent 按需动态加载专业知识文件,渐进式信息披露 | 集中(Agent 自主加载) | 需要深入领域知识但不需多 Agent |
| Router | 入口处一次分类,将请求路由到不同专门 Agent | 分散(一次分发) | 简单分类、无跨轮记忆需求 |
决策框架:子 Agent 不需要直接与用户对话 → Subagents;需要 Agent 间传递控制权、多跳协作 → Handoffs;只需让单 Agent "变专业"而不增加架构复杂度 → Skills;只是把不同类型请求分发出去 → Router。这些模式可以混合使用。
4.3 原生子 Agent 委派:as_tool() 方式
LangChain 原生方式的核心是"子 Agent 作为工具"------用 create_agent() 创建子 Agent,用 as_tool() 包装为工具,主 Agent 通过调用工具来委派任务:
python
research_agent = create_agent(
model=model,
tools=[web_search, file_reader],
system_prompt="你是一个研究员,负责收集和整理信息。"
)
analysis_agent = create_agent(
model=model,
tools=[calculator, chart_generator],
system_prompt="你是一个数据分析师,基于研究笔记提炼核心观点。"
)
main_agent = create_agent(
model=model,
tools=[
research_agent.as_tool(
name="research",
description="当需要搜索和收集某个主题的详细资料时使用此工具"
),
analysis_agent.as_tool(
name="analysis",
description="当需要对研究数据进行计算和分析时使用此工具"
),
],
system_prompt="你是一个项目经理,通过调用研究和分析工具来完成复杂任务。"
)
关键设计点:
- 子 Agent 每个有独立的系统提示和工具集,彼此完全隔离
as_tool()自动处理消息构造和结果提取,隐藏子 Agent 内部的思考-行动循环- 工具描述是主 Agent 决定何时委派的唯一依据------必须精确、具体、行动导向
4.4 Deep Agents:声明式多智能体
Deep Agents SDK 将子 Agent 配置简化为声明式字典,内部自动完成编译、注册、上下文管理等:
python
from deepagents import create_deep_agent
research_subagent = {
"name": "research-agent",
"description": "收集某个主题的详细资料并返回结构化的研究笔记。",
"system_prompt": "你是一个资深研究员...",
"tools": [web_search]
}
analysis_subagent = {
"name": "analysis-agent",
"description": "对研究笔记和数据进行定量分析和逻辑推理。",
"system_prompt": "你是一个数据分析师...",
"tools": [calculator],
"model": analysis_model, # 不同的子Agent可以用不同的模型!
"response_format": AnalysisResult, # 结构化 JSON 输出
}
main_agent = create_deep_agent(
model=main_model,
system_prompt="你是一个项目经理,协调研究Agent和分析Agent完成任务。",
subagents=[research_subagent, analysis_subagent]
)
Deep Agents 的核心增值能力:
- 任务规划 :内置
write_todos工具,自动分解复杂任务为步骤列表并追踪进度 - 上下文压缩:offloading(大型工具结果卸载到虚拟文件系统)+ summarization(对话历史达到 85% token 上限时自动摘要替换),双轨机制
- 虚拟文件系统 :提供
ls/read_file/write_file/edit_file四个基础操作,Agent 可以像操作真实文件系统一样管理数据 - 子 Agent 生成 :通过内置
task工具创建和调用子 Agent,子 Agent 在隔离上下文中运行
最佳实践:让每个子 Agent 返回简洁摘要(在 system_prompt 中明确"控制在 X 字以内")→ 保护上下文隔离优势;只给子 Agent 确实需要的工具 → 减少工具选择退化;不同任务用不同模型 → 利用各模型优势;主 Agent 提示中明确引导委派 → 避免主 Agent "自信"地自己回答问题。
4.5 并行协作与架构选型
Send 机制是实现并行分发的核心原语。将它与 Subagents 模式结合,可以构建主 Agent 在单轮中并行调用多个子 Agent 的高效系统------搜索 Agent 同时查多个数据源、分析 Agent 同时审阅多份文档,总延迟由最慢的那个决定。
架构选型四维度评估:
- 控制权归属:集中(Subagents)vs 分散(Handoffs/Router)
- 上下文隔离需求:子 Agent 的中间过程是否需要对主 Agent 可见
- 并行化需求 :是否需要
Send实现多路并发 - 开发复杂度:声明式配置(Deep Agents)vs 手写包装函数(原生方式)
五、Day 4:生产级部署------从 python main.py 到线上服务
5.1 错误处理与重试:多层容错体系
生产环境中,一个任务的中间步骤可能数十步,仅因一次临时网络超时就丢弃所有上下文重启,这在经济和体验上都不可接受。LangChain/LangGraph 提供从底层 HTTP 重试到顶层 Agent 自愈的完整容错链:
第一层:RunnableConfig 基础防护
python
from langchain_core.runnables import RunnableConfig
config = RunnableConfig(max_retries=3, timeout=30)
result = agent.invoke({"messages": [...]}, config=config)
max_retries 主要针对可重试错误(网络超时、429 限流、5xx),业务逻辑错误不会自动重试。
第二层:节点级 RetryPolicy
python
from langgraph.types import RetryPolicy
graph.add_node(
"call_model", call_model,
retry=RetryPolicy(
max_attempts=3,
initial_interval=1.0, # 首次等待 1 秒
backoff_factor=2, # 指数退避:1→2→4 秒
retry_on=(TimeoutError, ConnectionError) # 仅在特定异常时重试
)
)
第三层:with_fallbacks() 模型降级
python
robust_model = primary_model.with_fallbacks([fallback_model])
agent = create_agent(robust_model, tools=[...])
多个后备方案形成降级链:主模型 → 备用模型 → 本地缓存。
第四层:中间件级错误回调
python
@wrap_tool_call
def error_handling_middleware(request, handler):
try:
return handler(request)
except RateLimitError:
time.sleep(60)
return handler(request) # 限流→等待后重试
except TimeoutError:
return "服务响应超时,请稍后重试。" # 超时→降级响应
except Exception as e:
return f"工具执行遇到错误:{e}" # 未知→日志+降级
第五层:MCP 工具级自愈 (v0.3.0+):工具执行错误默认包装为 status="error" 的 ToolMessage 返回给模型,模型看到错误后可自行换参数、换工具或请求人工介入,而非直接崩溃。
5.2 MCP 协议:一次开发,多平台复用
Model Context Protocol(MCP)是 Anthropic 提出的标准化协议,让工具的定义与消费端彻底解耦。编写一个 MCP 服务器后,所有支持 MCP 的客户端(LangChain、Cursor、Claude Desktop)都可以直接使用。
创建 MCP 服务器:
python
# math_server.py
from fastmcp import FastMCP
mcp = FastMCP("Math")
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers"""
return a + b
if __name__ == "__main__":
mcp.run(transport="stdio") # 或 transport="http" 部署为远程服务
消费 MCP 工具------协议透明,Agent 不感知工具来自本地子进程还是远程服务器:
python
from langchain_mcp_adapters.client import MultiServerMCPClient
client = MultiServerMCPClient({
"math": {
"transport": "stdio",
"command": "python",
"args": ["/path/to/math_server.py"],
},
"weather": {
"transport": "http",
"url": "http://localhost:8000/mcp",
"headers": {"Authorization": "Bearer YOUR_TOKEN"},
}
})
tools = await client.get_tools()
agent = create_agent(model=model, tools=tools)
拦截器系统 (v0.3.0+):让 LangGraph 运行时信息穿透到 MCP 工具调用中------在调用前注入用户凭证、在调用后更新图状态或通过 Command(goto=...) 驱动路由:
python
async def inject_user_context(request, handler):
runtime = request.runtime
modified_request = request.override(
args={**request.args, "user_id": runtime.context.user_id}
)
return await handler(modified_request)
client = MultiServerMCPClient({...}, tool_interceptors=[inject_user_context])
MCP 的三种能力(工具、资源、提示)合在一起,让 MCP 服务器成为名副其实的"上下文协议"------同时向 Agent 提供可调用的函数、可读取的数据和可参考的提示模板。
5.3 LangSmith Fleet:无代码 Agent 平台(了解)
Fleet 是运行在 LangSmith 上的无代码 Agent 构建和管理平台,代表 Agent 开发从纯代码走向"低代码 + 代码"混合模式的行业趋势。五个核心能力:内置数十种即用型工具 + MCP 协议接入、多频道调度(被动响应 + 定时任务 + 事件驱动)、两层记忆系统(线程级短期 + 文件持久化长期)、高风险操作人工审批、子 Agent 与技能系统。
六、Day 5:性能优化与监控------速度、成本、稳定性的三角平衡
6.1 LCEL 并行处理:用并发换时间
单模型批量并行 ------batch() 替代逐个 invoke():
python
# 串行:逐条等待,总耗时 = 所有请求耗时累加
# for q in questions:
# answer = model.invoke(q)
# 并行:同时发出,总耗时 ≈ 单个请求耗时
responses = model.batch(questions)
通过 RunnableConfig(max_concurrency=N) 精确控制并发窗口,防止触发 API 限流。batch_as_completed() 支持"完成一个处理一个"的流式结果消费,适合前端实时展示。
多任务并行编排 ------RunnableParallel:
python
from langchain_core.runnables import RunnableParallel
parallel_chains = RunnableParallel(
summary=summary_chain,
keywords=keywords_chain,
sentiment=sentiment_chain,
)
result = parallel_chains.invoke({"text": article_text})
# result["summary"], result["keywords"], result["sentiment"] 同时就绪
RunnableParallel 和 batch() 可以组合使用实现二维并行------输入维度批量并发 + 任务维度子任务并发。
图级节点并发------LangGraph 的 super-step 机制:
从节点 A 同时画边到 B 和 C,B 和 C 在同一 super-step 中并发执行。整个 super-step 是事务性的------如果 B 或 C 任一抛出异常,两个分支对状态的所有更新都不会被应用,状态回滚到 super-step 开始之前。
6.2 LangSmith 成本监控:让每一分钱算得清楚
Agent 的动态特性意味着无法静态预测成本------简单查询可能只触发 2 次模型调用,复杂分析可能触发 20 次。LangSmith 通过自动 trace 机制记录每一次 LLM 调用、每一次工具执行的结构化数据。
接入方式 :安装 langsmith,设置 LANGSMITH_API_KEY 和 LANGSMITH_TRACING=true 环境变量,LangChain 框架内的调用自动追踪,零代码改动。
@traceable 装饰器------让任意 Python 函数成为 trace 树节点:
python
from langsmith import traceable
@traceable(run_type="llm", name="Chat Completion")
def call_model(messages: list[dict]):
response = client.chat.completions.create(model="gpt-4.1", messages=messages)
return response.choices[0].message
@traceable(run_type="tool", name="Search Knowledge Base")
def search_knowledge_base(query: str) -> str:
return f"搜索结果: {query}"
@traceable(name="Research Pipeline")
def research_pipeline(topic: str):
context = search_knowledge_base(topic) # 子 trace
return call_model([...]) # 子 trace
嵌套调用自动形成父子关系,构建完整调用层级图。run_type 决定 UI 渲染方式:"llm" 自动展示 token 计数和成本估算,"tool" 结构化展示输入输出,"retriever" 渲染检索文档列表。
对于自部署模型,通过 get_current_run_tree().set(usage_metadata={...}) 手动上报 token 用量和成本(单位美元),让所有模型在 LangSmith 中拥有一致的成本视图。
6.3 上下文压缩:不让 Agent 被历史淹没
过长的上下文带来三个问题:模型对中间信息的注意力稀释("迷失在中间"效应)、推理延迟线性增长、token 成本线性增长。
| 策略 | 触发条件 | 优点 | 适用场景 |
|---|---|---|---|
| 消息裁剪 | Token 数 > 阈值 | 实现简单,零额外 LLM 调用 | 对话主题频繁切换,历史信息价值低 |
| 摘要压缩 | 子任务完成 / 阶段节点 | 保留语义信息,不打断当前思考 | 多步推理、长对话中需要跨轮次记忆 |
| 混合策略 | 阈值触发 + 保留最近 N 条 | 兼顾效率与信息保真 | 绝大多数生产环境对话 Agent |
SummarizationMiddleware 是 LangChain v1 内置的摘要压缩中间件,持续监控 token 数,超阈值时自动用便宜模型将早期消息压缩为精炼摘要:
python
from langchain.agents.middleware import SummarizationMiddleware
SummarizationMiddleware(
model=summary_model,
trigger=("tokens", 4000), # 超过 4000 token 触发
keep=("messages", 20), # 始终保留最近 20 条原文
)
重要踩坑 :使用
trigger=("fraction", 0.8)(上下文占比模式)时,中间件需要从model.profile["max_input_tokens"]获取上下文上限。通过硅基流动等第三方 API 调用 Qwen/DeepSeek 等模型时,init_chat_model不会自动携带 profile 信息,必须手动传入profile={"max_input_tokens": 131072},否则fraction模式静默失效------摘要永远不会被触发。
原生支持 OR 触发(trigger 传入列表,任一条件满足即触发),需要 AND 触发时可子类化覆写 _should_summarize 方法。
七、核心概念的个性化理解
7.1 对"图即流程"的感悟
LangGraph 的核心洞见是:Agent 的执行流程本质上就是一个有向图 。create_agent() 隐藏了这个图结构,而 LangGraph 把它显式化。显式化的好处不仅仅是"能看见",更重要的是"能修改"------你可以在任意两个节点之间插入新节点,可以用条件边替换固定边,可以把一个节点替换为整个子图。这种可组合性让 Agent 从"黑盒产品"变成了"可定制的乐高"。回顾第 1 周学的 LCEL Runnable 管道,LangGraph 的图模型是更高维度的"管道"------不仅在时间维度上串联步骤,还在空间维度上支持分支和并行。
7.2 对"状态即记忆"的理解
第 2 周我们用 checkpointer + thread_id 实现多轮对话记忆,当时只觉得这是一个"保存消息历史"的便利功能。学完 LangGraph 的检查点机制后才理解,状态持久化远不止记忆------它是人工介入的基础(暂停后能从断点恢复),是容错的基础(失败后从上一个成功 checkpoint 重试),是时间旅行的基础(回放历史状态探索"如果当时做了不同选择")。状态不是 Agent 的附属品,状态就是 Agent 本身------Agent 的行为由当前状态和图的拓扑共同决定。
7.3 对"多智能体 = 上下文工程"的理解
一开始我以为多智能体是让多个 AI 像人类团队一样"讨论协作"。但实际上它的本质更朴素------控制每个 Agent 看到什么信息。子 Agent 只获得完成任务所需的最小工具集和最小上下文,主 Agent 只看到子 Agent 返回的最终结果而非中间过程。这不是多 Agent 在"对话",而是通过信息隔离来降低每个 Agent 的认知负担。这也解释了为什么 Subagents 是最常用模式------子 Agent 不需要"人格",它只是一个被严格限定输入输出的函数。
7.4 对 MCP 协议与 @tool 的关系理解
第 2 周学 @tool 装饰器时,我以为工具定义就是写个 Python 函数加个装饰器。MCP 协议让我意识到这只是一个"本地实现"。在工程上,工具应该与消费端解耦------工具服务器独立部署、独立扩缩容、独立更新,所有客户端通过标准协议消费。MCP 之于工具,就像 REST API 之于 Web 服务:定义了一套标准化的通信契约。这也是为什么 LangChain 官方在 v0.3.0 中大力投入 MCP 适配器------它代表了工具从"框架绑定"到"协议驱动"的演进方向。
7.5 性能优化的元原则
并行处理、成本监控、上下文压缩看似是三个独立主题,但它们共同遵循一个元原则:在 LLM 应用中,资源(时间、金钱、token)的消耗是不可见且容易被忽略的。本地开发时 3 秒和 0.5 秒的延迟差异不明显,但乘上每天数千次请求就是巨大的体验鸿沟。单次调用几美分的成本不在意,但月底账单会告诉你真相。这个元原则提醒我们:从开发第一天起就应该建立观测意识------没有度量就没有优化。
八、踩坑记录与经验
-
fraction触发模式需要手动传profile:使用SummarizationMiddleware的trigger=("fraction", 0.8)时,通过硅基流动等第三方 API 调用的模型不会自动携带max_input_tokens。必须在init_chat_model()中手动传入profile={"max_input_tokens": 131072},否则中间件无法计算阈值,摘要永不触发------不会报错,属于静默失效,排查极其耗时。 -
interrupt()不能被try/except包裹 :interrupt()通过抛出特殊内部异常(GraphInterrupt)来实现暂停。如果你在它外面包了except Exception,这个异常会被捕获,导致中断不被传递到运行时,图不会暂停而是继续执行。调试时发现图"跳过了"中断点,十有八九是这个原因。 -
一个节点不要同时用普通边和条件边 :LangGraph 允许你为一个节点同时添加
add_edge和add_conditional_edges,但结果是两条路径都会被激活------节点执行完后既走固定边又走条件边,导致不可预测的行为(通常是下游节点被执行两次)。正确做法是每个节点只选一种路由机制。 -
interrupt()恢复时节点从头重跑,副作用必须幂等 :如果节点在interrupt()之前做了数据库插入、文件写入等操作,恢复时这些操作会再次执行,造成重复数据。应该将副作用移到interrupt()之后,或者使用 upsert 等幂等操作。这个坑在实际项目中非常常见。 -
RunnableParallel中所有分支共享同一个输入 :如果各子任务需要不同的输入字段,需要在每个分支前加RunnablePassthrough或 lambda 做字段筛选,否则会把完整输入字典传给每个分支。好消息是 LangChain 的RunnableParallel会自动将输出按字段名组装,无需手动合并。 -
MCP 的
handle_tool_errors默认值已变为True:自langchain-mcp-adaptersv0.3.0 起,工具执行错误默认被包装为ToolMessage(status="error")返回而非直接抛异常。这意味着 Agent 获得了"自愈"能力------它能读取错误信息并自行调整策略。但如果你需要严格的 fail-fast 行为(如金融交易场景),必须显式设置handle_tool_errors=False。 -
max_concurrency是客户端限流,不是服务端 :RunnableConfig(max_concurrency=N)控制的是 LangChain 向 API 发送请求的节奏,不会改变模型 API 本身的行为。如果 API 提供商有自己的速率限制,光设置max_concurrency不够,还需要配合RetryPolicy处理 429 响应。 -
Send分发时,目标节点的状态更新是独立合并的 :如果多个Send目标并行写同一个状态字段,需要该字段有合适的 reducer(如operator.add),否则后完成的节点会覆盖先完成的节点的写入。这与 LangGraph super-step 的事务性不矛盾------事务性保证的是"要么全部生效要么全部回滚",不是"保证写入顺序"。
九、下周展望
第 4 周的主题是项目实战与前沿探索。从本周的认知出发:
- LangGraph 的图编排能力将在实际项目中被完整验证------一个真实业务场景可能需要数十个节点和复杂的条件分支
- Deep Agents 的子 Agent 委派 + 上下文压缩 + 虚拟文件系统将在长期运行的分析任务中发挥关键作用
- MCP 协议的工具复用能力使得"构建一次工具服务器,多项目共用"成为现实
- 通过 LangSmith 的性能数据,可以识别出系统中的瓶颈节点(哪个节点耗时最长、哪个工具失败率最高)并针对性优化
- 第 4 周的实战项目将把这些零散的技术点串联为端到端的可部署应用
十、本周学习成果自检
- 理解 LangGraph 的四大核心概念(StateGraph / State / Node / Edge),能解释
add_messagesreducer 的工作原理 - 能手写完整 ReAct 循环的 LangGraph 版本(LLM 节点 + 工具节点 + 条件边 + 循环回路)
- 理解三种流模式(updates / values / messages)的输出差异,能使用
get_stream_writer()推送自定义事件 - 掌握状态持久化机制,能用
InMemorySaver+thread_id实现多轮对话记忆 - 能构建 4 节点以上的多节点流水线(如研究→分析→写作→审核),理解混合 reducer 的设计
- 理解条件边、
Command、Send三种路由机制的区别与适用场景 - 掌握
interrupt()动态中断机制,能实现人工审批工作流,理解四条关键规则 - 理解多智能体的四种正式模式(Subagents / Handoffs / Skills / Router)及其适用场景
- 能使用
as_tool()和 Deep Agents 的声明式配置两种方式实现主 Agent + 子 Agent 调度 - 理解 Deep Agents 的内置能力(任务规划 / 上下文压缩 / 虚拟文件系统)及其设计意图
- 掌握从
RunnableConfig→RetryPolicy→with_fallbacks()→@wrap_tool_call的多层容错体系 - 能创建 MCP 服务器并将工具接入 LangChain Agent,理解协议透明的设计哲学
- 能使用
batch()/RunnableParallel/ LangGraph super-step 三种并行机制提升吞吐量 - 能使用
@traceable装饰器和 LangSmith 平台监控 Agent 的调用链路与成本 - 掌握
SummarizationMiddleware的配置方法,理解fraction模式的 profile 依赖问题 - 能对比 AgentExecutor 与 LangGraph 在架构/状态管理/扩展性三个维度的差异