// notes / agent
基于 deeplearning.ai 公开课整理的 LangGraph 学习笔记,覆盖 State 管理、Tool Calling、 ReAct 循环、记忆系统、Human-in-the-Loop、Multi-Agent 与 RAG 集成。
注:以下内容在 AI 辅助下整理。核心概念的理解、代码示例和实践总结经过了人工校对。部分 API 行为可能随 LangGraph 版本更新有变化,请以官网文档为准。
State、Node、Edge、Tool Calling、Checkpointer 与 Streaming
在接触 LangGraph 之前,我对 Agent 的理解就是简单的 LLM 调用:扔一个 prompt,拿回结果,完事。这在单轮问答场景下没问题,但一旦涉及多轮对话、工具调用、条件分支,这种简单模式就撑不住了。
接触 LangGraph 后,最大的感受是:它把 Agent 的工作流变得显式可读了。Agent 不只是"调 LLM + 解析输出",而是一个多步骤的工作流——每一步有明确的输入输出,步骤之间有条件跳转,中间状态需要被保存和追踪。
对我来说,LangGraph 解决了一个核心问题:如何让 Agent 的行为可控、可追踪、可中断。这在涉及工具调用和多步推理的场景中尤为关键。
我目前的理解是:LangGraph 是一个把 Agent 应用组织成"带状态的图"的框架。三个概念就能概括:
打个比方:如果 Agent 是一个"自动化办事流程",State 就是那张流转中的表格,Node 是各个办事窗口,Edge 就是连接窗口的箭头。这个结构让复杂流程变得可管理。
State 不是普通的 Python 变量。它最关键的特性是:每个节点返回的部分状态会和全局 State 合并(merge),而不是覆盖。不同节点更新不同字段,互不干扰。
在多轮对话中,State 通常包含:
messages:完整的对话历史(用户消息、AI 回复、工具调用、工具结果)next_step:供条件边判断路由方向context、intermediate_stepsLangGraph 中使用 TypedDict 或 Pydantic 模型定义 State。我目前用 TypedDict + Annotated,因为它可以为每个字段指定 reducer 逻辑(比如消息字段用 add_messages 来累积而不是覆盖)。
add_messages 的具体行为(如同 ID 消息的更新逻辑)在不同 LangGraph 版本间可能有细微差异。在用 LangGraph 之前,我写 Agent 逻辑的方式是一个大函数吃到底:调 LLM → 解析 → 需要就调工具 → 再解析 → 返回。简单场景能跑,但问题很明显:
LangGraph 的 Node + Edge 模式解决了这个问题。每个节点是一个单一职责的函数:
# LLM Node
def call_model(state: AgentState) -> dict:
response = llm.bind_tools(tools).invoke(state["messages"])
return {"messages": [response]}
# Tool Node
def call_tools(state: AgentState) -> dict:
last_message = state["messages"][-1]
tool_results = []
for tool_call in last_message.tool_calls:
result = execute_tool(tool_call)
tool_results.append(result)
return {"messages": tool_results}条件边负责路由决策:
def should_continue(state: AgentState) -> str:
last_message = state["messages"][-1]
if last_message.tool_calls:
return "tools"
return "__end__"
graph.add_conditional_edges("agent", should_continue, {
"tools": "tools",
"__end__": "__end__"
})这是学 LangGraph 最让我开眼界的部分。LLM 只能输出文本,但 Tool Calling 把它从"回答者"变成了"编排者"——它不执行,而是判断需要什么、然后请求执行。
完整的工具调用流程:
关键认知:模型不执行工具——它请求执行。实际的工具执行发生在程序代码中。这把"决策"和"执行"分开了,意味着工具可以是一切:数据库查询、API 调用、脚本执行,甚至启动另一个 Agent。
一开始我很困惑:对话之间的状态怎么持久化?没有持久化,每次新的用户输入都从零开始,之前的工具结果和推理全都丢了。
Checkpointer 解决了这个问题,它像游戏的存档系统:
thread_id 对应一个独立的对话线程thread_id 调用,从上次存档点恢复目前我开发调试只用内存 MemorySaver。如果要上线,需要换成持久化后端(SqliteSaver 或 PostgresSaver)。
graph.stream() 是我觉得最实用的功能之一。除了渐进式输出带来的用户体验提升,它对我更大的价值是调试可见性。
通过流式输出,你能看到:
没有流式输出时,我靠 print 语句追踪流程。有了流式,每一步都清清楚楚,调试效率大幅提升。把这个过程暴露给前端,也能让用户看到 Agent 的"思考过程",增加信任感。
一个最小的 LangGraph 工作流,展示了 ReAct 循环的核心逻辑:
from typing import TypedDict, Annotated, Sequence
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages
from langgraph.checkpoint.memory import MemorySaver
class AgentState(TypedDict):
messages: Annotated[Sequence, add_messages]
def agent_node(state: AgentState):
response = model_with_tools.invoke(state["messages"])
return {"messages": [response]}
def tool_node(state: AgentState):
last_msg = state["messages"][-1]
results = [execute(tc) for tc in last_msg.tool_calls]
return {"messages": results}
def router(state: AgentState):
last_msg = state["messages"][-1]
if getattr(last_msg, "tool_calls", None):
return "tools"
return END
builder = StateGraph(AgentState)
builder.add_node("agent", agent_node)
builder.add_node("tools", tool_node)
builder.set_entry_point("agent")
builder.add_conditional_edges("agent", router)
builder.add_edge("tools", "agent")
graph = builder.compile(checkpointer=MemorySaver())
config = {"configurable": {"thread_id": "1"}}
events = graph.stream(
{"messages": [HumanMessage(content="weather?")]},
config
)
for event in events:
print(event)如果 LLM 判断需要天气工具,流程是:agent → tools → agent → END。如果不需要:agent → END。整个过程逐步流式输出,Agent 的决策链一目了然。