一、引言
在过去的一个月里,我从零开始搭建了一个基于 LangGraph 的多 Agent 协作系统。Week 7 我们完成了单个 Agent 的流式推送与部署,而 Week 8 则是从"单兵作战"进化到"团队协作"的关键一周。
本周目标:
- 掌握 LangGraph 的基本图编排能力
- 实现 Supervisor 模式的多 Agent 调度
- 引入长期记忆(SQLite 持久化)
- 解决 DeepSeek 模型与 LangChain 的兼容性问题
- 实现并行多 Agent 执行(Map-Reduce)
如果你也有前端背景,你会发现很多概念似曾相识:
StateGraph≈ Redux 的 reducer + 中间件SendAPI ≈Promise.allthread_id≈ 用户 session ID
二、Day 1:初识 LangGraph ------ 状态机思维
2.1 核心概念
LangGraph 本质上是一个有向图状态机。每个节点(Node)是一个函数,接收当前状态,返回状态更新;边(Edge)定义了执行顺序;条件边(Conditional Edge)根据状态值决定下一步走向。
核心概念(全栈类比版)
| LangGraph 术语 | 前端类比 | 说明 |
|---|---|---|
| State(状态) | Pinia/Vuex 的 store | 一个全局的 TypedDict,所有节点都能读写 |
| Node(节点) | 一个 API 函数或组件 | 接收 State,处理后返回要更新的部分 |
| Edge(边) | 路由守卫 / 条件判断 | 决定下一步走到哪个节点 |
| Conditional Edge | if-else 逻辑 | 根据 State 的值动态选择下一个节点 |
| Compile(编译) | 构建最终的执行图 | 类似 webpack 打包,生成可调用的 app |
2.2 代码示例(week8_day1.py)
python
python
from typing import Literal, TypedDict
from langgraph.graph import END, StateGraph
class MyState(TypedDict):
"""共享状态,类似全局 state。"""
input_text: str
step_count: int
final_output: str
def node_process_input(state: MyState) -> dict:
"""节点1:处理输入,将文本转为大写。"""
print(f"[Node: process_input] 收到: {state['input_text']}")
return {
"input_text": state["input_text"].upper(),
"step_count": state["step_count"] + 1,
}
def node_check_length(state: MyState) -> dict:
"""节点2:判断文本长度,决定下一步。"""
length = len(state["input_text"])
print(f"[Node: check_length] 文本长度: {length}")
return {
"step_count": state["step_count"] + 1,
"final_output": f"文本长度是 {length}",
}
def router_after_process(state: MyState) -> Literal["check_length", "end"]:
"""如果 step_count 小于 3,继续;否则结束。"""
if state["step_count"] < 3:
return "check_length"
return "end"
builder = StateGraph(MyState)
builder.add_node("process_input", node_process_input)
builder.add_node("check_length", node_check_length)
builder.set_entry_point("process_input")
builder.add_conditional_edges(
"process_input",
router_after_process,
{
"check_length": "check_length",
"end": END,
},
)
builder.add_edge("check_length", "process_input")
app = builder.compile()
initial_state: MyState = {
"input_text": "Hello LangGraph!",
"step_count": 0,
"final_output": "",
}
print("=== 开始运行 LangGraph ===")
result = app.invoke(initial_state)
print("\n=== 最终结果 ===")
print(result)

2.3 踩坑记录
问题 :add_conditional_edges 的路由映射字典键必须为字符串,不能是列表。
解决 :即使需要并行分发,映射字典中也只能写单个节点名,真正的并行逻辑通过 Send API 实现(Day 7 会讲)。
三、Day 2:ReAct Agent ------ 让 Agent 学会调用工具
3.1 什么是 ReAct?
ReAct = Reasoning + Acting。Agent 先思考(Reasoning),然后决定调用工具(Acting),拿到工具结果后再思考,如此循环直到完成任务。
3.2 代码实现(week8_day2.py)
python
# 1. 定义工具
@tool
def calculator(expression: str) -> str:
"""评估一个数学表达式。只允许数字和 +, -, *, /, (, )."""
try:
# 简单的安全限制,生产环境请用 numexpr 或 sympy
allowed = set("0123456789+-*/(). ")
if any(char not in allowed for char in expression):
return "错误:包含非法字符"
return str(eval(expression))
except Exception as exc:
return f"计算错误: {exc}"
tools = [calculator]
# 2. 定义状态
class AgentState(TypedDict):
# add_messages 是 LangGraph 的 reducer,会自动把新消息追加到列表中
messages: Annotated[list, add_messages]
# 3. 定义节点
def agent_node(state: AgentState) -> AgentState:
"""Agent 节点:调用绑定了工具的 LLM。"""
llm_with_tools = get_llm().bind_tools(tools)
response = llm_with_tools.invoke(state["messages"])
return {"messages": [response]}
def should_continue(state: AgentState) -> str:
"""路由函数:判断 LLM 是否要求调用工具。"""
last_message = state["messages"][-1]
if hasattr(last_message, "tool_calls") and last_message.tool_calls:
return "tools"
return END
# 4. 构建图
builder = StateGraph(AgentState)
builder.add_node("agent", agent_node)
builder.add_node("tools", ToolNode(tools))
builder.add_edge(START, "agent")
builder.add_conditional_edges(
"agent",
should_continue,
{"tools": "tools", END: END},
)
builder.add_edge("tools", "agent")
app = builder.compile()

3.3 代码逻辑与语法解释(前端老手版)
-
@tool装饰器 : 把普通 Python 函数变成 Agent 能用的工具。函数的 Docstring 极其重要!LLM 全靠读这段描述来决定什么时候调用它。 -
AgentState与add_messages:类似前端状态管理。我们定义了一个messages数组。add_messages相当于 Redux 里的 Reducer ,确保每次节点返回新消息时,是追加(Append) 而不是覆盖。 -
bind_tools:告诉 LLM:"你现在手里有这些工具"。LLM 会在需要时返回特定的 JSON 格式(工具调用请求)而不是直接回答。 -
ToolNode: LangGraph 提供的预置组件 。它相当于一个自动化的 API 网关,接收 LLM 的工具调用请求,执行对应的 Python 函数,并把结果包装成ToolMessage返回给 LLM。 -
条件路由
should_continue: 检查最后一条消息有没有tool_calls。有就去tools节点,没有就END。这形成了 ReAct 循环。 -
执行流 :
START->agent(LLM思考,决定调计算器) ->tools(执行计算) ->agent(LLM拿到结果,组织语言回答) ->END
四、Day 3:Supervisor 多 Agent 架构初探
4.1 架构设计
引入一个主管(Supervisor) 节点,它不直接执行任务,而是分析用户意图,将任务分派给不同的专业 Agent(计算专员、搜索专员)。
4.2 代码骨架(week8_day3.py)
python
# ==================== 1. 定义 LLM ====================
llm = ChatOpenAI(
model=os.getenv("MODEL_NAME", "deepseek-chat"),
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url=os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com"),
temperature=0,
)
# ==================== 2. 定义工具 ====================
@tool
def calculator(expression: str) -> str:
"""评估一个数学表达式。只允许数字和 +, -, *, /, (, )."""
try:
allowed = set("0123456789+-*/(). ")
if any(char not in allowed for char in expression):
return "错误:包含非法字符"
return str(eval(expression))
except Exception as exc:
return f"计算错误: {exc}"
@tool
def search_web(query: str) -> str:
"""模拟搜索网页,返回假数据。"""
return f"搜索结果:关于'{query}'的最新信息是:(假数据) 今天气温 25 度。"
# ==================== 2. 绑定工具给子 Agent ====================
calc_tools = [calculator]
search_tools = [search_web]
# ==================== 3. 定义状态(必须放在节点函数之前) ====================
class AgentState(TypedDict):
messages: Annotated[list, add_messages]
next: Annotated[str, lambda x, y: y] # 记录下一步去向
# ==================== 4. 定义节点函数 ====================
def supervisor_node(state: AgentState) -> dict:
"""主管节点:决定下一个步骤"""
last_message = state["messages"][-1]
# 如果最后一条消息是工具返回的结果,说明子 agent 已经干完活,主管直接总结
if last_message.type == "tool":
response = llm.invoke(state["messages"] + [HumanMessage(content="请根据工具结果总结回答用户。")])
return {"messages": [response]}
# 否则,主管思考该派发给谁
response = llm.invoke(
state["messages"] +
[HumanMessage(content="你是主管。请决定下一步:如果需要计算选 'calc',需要搜索选 'search',如果可以直接回答或已完成任务选 'end'。")]
)
# 简单解析主管的回复来决定路由
content = response.content.lower()
if "calc" in content:
return {"next": "calc"}
elif "search" in content:
return {"next": "search"}
else:
return {"next": "end"}
def calc_agent_node(state: AgentState) -> dict:
"""计算专员:只处理计算任务"""
llm_with_tools = llm.bind_tools(calc_tools)
response = llm_with_tools.invoke(state["messages"])
return {"messages": [response]}
def search_agent_node(state: AgentState) -> dict:
"""搜索专员:只处理搜索任务"""
llm_with_tools = llm.bind_tools(search_tools)
response = llm_with_tools.invoke(state["messages"])
return {"messages": [response]}
# ==================== 5. 构建图 ====================
builder = StateGraph(AgentState)
builder.add_node("supervisor", supervisor_node)
builder.add_node("calc", calc_agent_node)
builder.add_node("search", search_agent_node)
builder.add_node("calc_tools", ToolNode(calc_tools))
builder.add_node("search_tools", ToolNode(search_tools))
builder.add_edge(START, "supervisor")
builder.add_conditional_edges(
"supervisor",
lambda state: state["next"],
{"calc": "calc", "search": "search", "end": END}
)
builder.add_edge("calc", "calc_tools")
builder.add_edge("search", "search_tools")
builder.add_edge("calc_tools", "supervisor")
builder.add_edge("search_tools", "supervisor")
app = builder.compile()
核心逻辑解释(前端老手版)
-
状态扩展 (
next) :- 我们在
AgentState里加了一个next字段。 Annotated[str, lambda x, y: y]相当于状态覆盖器(类似前端setState直接替换值),主管节点通过返回{"next": "calc"}来告诉图下一步该干嘛。
- 我们在
-
Supervisor 节点:
- 它是智能路由器。如果看到是工具返回的消息,就负责总结;如果是用户提问,就分析意图(计算还是搜索)。
- 注:为了简化演示,这里用文本匹配
calc/search来路由。生产环境强烈建议给主管模型绑定function calling,强制它输出结构化 JSON 路由指令。
-
子 Agent 与独立工具节点:
- 计算专员和搜索专员各自绑定了自己的工具。
- 流程变成:
Supervisor->子Agent->专属ToolNode-> 回到Supervisor。
-
图的拓扑结构:
- 这是一个星型结构。所有子任务执行完毕后,都必须回到中心节点(Supervisor)进行汇总和结束判定。

五、Day 4:生产级改造 ------ 结构化路由与熔断机制
5.1 核心改进
- 使用
with_structured_output:强制 LLM 输出固定格式的 JSON,避免自然语言歧义。 - 引入迭代计数器:超过 3 次循环强制结束,防止死循环。
- 自定义 ToolNode :给工具结果加上
__agent_result__前缀,防止模型误读。
5.2 关键代码(week8_day4.py)
python
# ==================== 1. 定义工具 ====================
@tool
def calculator(a: int, b: int) -> int:
"""计算两个整数的乘法。"""
return a * b
@tool
def search_web(query: str) -> str:
"""搜索实时信息(如天气)。"""
# 模拟假数据
return f"(假数据) 今天气温 25 度。"
calc_tools = [calculator]
search_tools = [search_web]
# ==================== 2. 定义状态 ====================
class AgentState(TypedDict):
messages: Annotated[list, operator.add]
next: str
iterations: int # 熔断计数器
# ==================== 3. 定义节点 ====================
def supervisor_node(state: AgentState) -> dict:
"""主管节点:决定下一步(宽松匹配版)"""
# 计数器兜底防死循环
if state.get("iterations", 0) > 3:
return {"next": "end", "messages": [AIMessage(content="达到最大迭代次数,强制结束")]}
last_message = state["messages"][-1]
# 如果是工具返回的结果,直接总结
if isinstance(last_message, ToolMessage):
response = llm.invoke(state["messages"] + [HumanMessage(content="请根据工具结果总结回答用户。")])
return {"messages": [response], "next": "end"}
# 不使用工具调用,直接让模型思考并输出文本
prompt = HumanMessage(content="""你是主管。分析用户需求:
- 如果需要计算数学题,你的回复必须包含关键字 calc。
- 如果需要搜索信息(如天气),你的回复必须包含关键字 search。
- 如果可以直接回答或任务已完成,回复 end。
用户当前输入和历史记录如上,请做出决定。""")
response = llm.invoke(state["messages"] + [prompt])
content = response.content.lower() # 转小写匹配
# 宽松匹配逻辑
if "calc" in content:
next_value = "calc"
elif "search" in content:
next_value = "search"
else:
next_value = "end"
return {
"next": next_value,
"messages": [response],
"iterations": state.get("iterations", 0) + 1
}
def calc_agent_node(state: AgentState) -> dict:
"""计算专员:绑定计算工具"""
llm_with_tools = llm.bind_tools(calc_tools)
response = llm_with_tools.invoke(state["messages"])
return {"messages": [response]}
def search_agent_node(state: AgentState) -> dict:
"""搜索专员:绑定搜索工具"""
llm_with_tools = llm.bind_tools(search_tools)
response = llm_with_tools.invoke(state["messages"])
return {"messages": [response]}
# 自定义工具执行节点:确保正确返回 ToolMessage 并打上防污染标记
def custom_tool_node(state: AgentState) -> dict:
"""显式执行工具并封装为 ToolMessage"""
last_message = state["messages"][-1]
if not hasattr(last_message, "tool_calls") or not last_message.tool_calls:
return {}
tool_messages = []
for tc in last_message.tool_calls:
# 简单执行工具
if tc["name"] == "calculator":
res = calculator.invoke(tc["args"])
elif tc["name"] == "search_web":
res = search_web.invoke(tc["args"])
else:
res = "未知工具"
# 封装为标准 ToolMessage,并加入特征标记防止模型误判为新的指令
tool_messages.append(
ToolMessage(
content=f"__agent_result__ {res}",
tool_call_id=tc["id"]
)
)
return {"messages": tool_messages}
# ==================== 4. 构建图 ====================
builder = StateGraph(AgentState)
builder.add_node("supervisor", supervisor_node)
builder.add_node("calc", calc_agent_node)
builder.add_node("search", search_agent_node)
builder.add_node("tools", custom_tool_node) # 使用自定义工具节点
builder.add_edge(START, "supervisor")
# 主管路由
builder.add_conditional_edges(
"supervisor",
lambda state: state["next"],
{"calc": "calc", "search": "search", "end": END}
)
# 子Agent调用工具判断
def check_tool_calls(state: AgentState) -> str:
last_message = state["messages"][-1]
if hasattr(last_message, "tool_calls") and last_message.tool_calls:
return "tools"
return END
builder.add_conditional_edges("calc", check_tool_calls, {"tools": "tools", END: END})
builder.add_conditional_edges("search", check_tool_calls, {"tools": "tools", END: END})
# 工具执行完强制回到主管复盘
builder.add_edge("tools", "supervisor")
app = builder.compile()

5.3 踩坑:DeepSeek 不支持 with_structured_output 的默认方法
LangChain 默认使用 JSON schema 模式,但 DeepSeek 只支持 Function Calling。解决方法是指定 method="function_calling"。
六、Day 5:长期记忆 ------ 让 Agent 记住你是谁
6.1 为什么需要记忆?
没有记忆的 Agent 每次对话都是"失忆症患者"。我们需要把对话历史持久化到数据库,并在下次对话时恢复上下文。
6.2 两种记忆后端
| 后端 | 特点 | 适用场景 |
|---|---|---|
MemorySaver |
内存存储,速度快 | 开发调试、单次会话 |
SqliteSaver |
磁盘持久化,跨会话 | 生产环境、多用户 |
6.3 代码实现(week8_day5.py)
less
db_path = os.path.abspath("chat_history.db")
conn = sqlite3.connect(db_path, check_same_thread=False) # [5,7](@ref)
memory = SqliteSaver(conn)
app = builder.compile(checkpointer=memory)

6.4 踩坑:导入路径变更
LangGraph 版本更新后,SqliteSaver 的导入路径从 langgraph.checkpoint.sqlite 变成了 langgraph.checkpoint.sqlite(注意大小写)。需要安装 langgraph-checkpoint-sqlite 包。
七、Day 6:多 Agent 协作稳定版 ------ 结构化路由 + 防死循环
7.1 最终架构
scss
纯文本
纯文本
用户输入 → Supervisor (结构化路由) → math_agent / search_agent → 工具执行 → 回到 Supervisor
↓
FINISH (结束)
7.2 关键改进点
- Pydantic Route :
Literal["math_agent", "search_agent", "FINISH"] - Worker 职责隔离 :每个 Worker 只返回带前缀的纯文本结论(如
[数学专家]: 408) - Supervisor 只看最近 5 条消息:防止历史过长导致决策混乱
- 递归限制 :
recursion_limit: 10作为最后防线
7.3 代码亮点(week8_day6.py)
python
def supervisor_node(state: AgentState):
"""主管:看上下文,决定派发任务或结束"""
# 安全机制:超过 5 次迭代强制结束
if state.get("iterations", 0) >= 5:
return {"next": "FINISH", "iterations": state.get("iterations", 0) + 1}
system_prompt = """你是主管。下面有专家:
- math_agent: 仅处理数学计算。
- search_agent: 仅处理实时搜索(如天气)。
以最近一条用户消息为当前任务,不要重新派发历史任务。
如果当前任务已有专家给出答案,或属于闲聊及回顾历史,输出 FINISH。"""
llm = get_llm()
# DeepSeek 不支持默认的 json_schema,改用工具调用并保留 Route 校验。
supervisor_llm = llm.with_structured_output(Route, method="function_calling")
decision = supervisor_llm.invoke(
[SystemMessage(content=system_prompt)] + state["messages"]
)
update = {
"next": decision.next,
"iterations": state.get("iterations", 0) + 1
}
if decision.next == "FINISH" and isinstance(state["messages"][-1], HumanMessage):
response = llm.invoke(
[SystemMessage(content="请根据对话历史回答用户当前的问题。")] + state["messages"]
)
update["messages"] = [response]
return update

八、Day 7:并行执行 ------ Map-Reduce 模式
8.1 为什么需要并行?
用户可能同时提出多个独立任务:"帮我算一下 12×34,同时查一下上海天气"。串行执行会浪费一半时间。
8.2 核心 API:Send
Send 允许在条件路由中返回多个目标,LangGraph 会并发执行这些节点。
python
python
python
from langgraph.types import Send
def parallel_router(state: AgentState):
"""并行路由:如果决策是 both,同时发送给两个专家"""
if state["next"] == "both":
# 使用 Send 实现动态分发(并发执行)
return [Send("math_agent", state), Send("search_agent", state)]
return state["next"]

8.3 汇总节点(Synthesizer)
所有并行分支完成后,触发一个汇总节点,将结果合并成最终回复。
sql
python
python
builder.add_edge(["math_agent", "search_agent"], "synthesizer")
builder.add_edge("synthesizer", END)
8.4 踩坑:路由映射字典不能包含列表
add_conditional_edges 的第三个参数(映射字典)的键必须为字符串,值可以是节点名或 END。并行逻辑完全由 Send 控制,映射字典中只需为 "both" 随便指定一个占位节点(如 "math_agent")。
九、总结与展望
9.1 本周收获
| 天数 | 核心技能 | 关键踩坑 |
|---|---|---|
| Day 1 | LangGraph 基本图编排 | 路由映射键不可为列表 |
| Day 2 | ReAct Agent + ToolNode | 网络超时(需加 retry) |
| Day 3 | Supervisor 多 Agent | 文本匹配不稳定 |
| Day 4 | 结构化路由 + 熔断 | DeepSeek 不支持默认 json_schema |
| Day 5 | SQLite 持久化记忆 | 导入路径变更 |
| Day 6 | 稳定版多 Agent 协作 | Worker 上下文污染 |
| Day 7 | 并行执行 (Map-Reduce) | Send 与映射字典的配合 |
9.2 下一步计划
- Swarm 模式:Agent 之间直接传递上下文,无需主管持续监控。
- 接入真实 RAG 知识库:让 Agent 能检索私有文档。
- 流式输出:将 Day 7 的并行结果也改为 SSE 流式推送。
9.3 给前端同学的忠告
- 状态管理思维迁移:LangGraph 的 StateGraph 和 Redux 极其相似,理解 reducer 和 action 就能快速上手。
- 异步并发 :
SendAPI 就是后端的Promise.all,只是写法不同。 - 错误处理:Agent 系统比前端更脆弱,一定要加熔断、重试、日志。