从消息类型、聊天模型到工具绑定,再到 Reducer 和 MessagesState。
这一讲是 LangGraph 中构建实际 AI 应用的基础零件。
Module 1 的第一讲(1-1)里,Node 只是做字符串拼接,和 LLM 没有任何关系。这一讲开始,我们把真正的 LLM 调用引入 LangGraph。
要让 LLM 进图,需要解决 4 个子问题:
| 子问题 | 解决方案 |
|---|---|
| LLM 输入/输出用什么格式? | Messages(HumanMessage / AIMessage 等) |
| 怎么在 Node 里调用 LLM? | 使用 LangChain 的 ChatModel |
| 怎么让 LLM 调用外部函数? | bind_tools 绑定工具 |
| State 里的消息列表怎么不被覆盖? | Reducer(add_messages) |
搞清楚这 4 个零件,你就能把一个"会思考、会调用工具"的 LLM 装进 LangGraph 的 Node 里,这是后续所有 Agent 的基础。
你学过 LangChain,知道聊天模型的输入是一组"消息"。LangGraph 完全复用了这个体系。LangChain 定义了多种消息类型,每种对应对话中的一个角色:
tool_calls(工具调用请求)。tool_call_id 来对应是哪次工具调用的结果。from langchain_core.messages import AIMessage, HumanMessage
# 构建一段对话历史
messages = [
AIMessage(content="So you said you were researching ocean mammals?", name="Model"),
HumanMessage(content="Yes, that's right.", name="Lance"),
AIMessage(content="Great, what would you like to learn about.", name="Model"),
HumanMessage(content="I want to learn about the best place to see Orcas.", name="Lance"),
]
# 把消息列表传给 LLM
result = llm.invoke(messages)
# result 是一个 AIMessage 对象
LLM 是无状态的——每次调用都需要把完整的对话历史传进去,它才能"记得"上下文。消息列表就是对话历史的载体。LangGraph 的 State 里存这个列表,就是为了让每次 LLM 调用都能看到完整历史。
LangGraph 的 Node 就是普通 Python 函数。在函数体里,你可以直接调用任何 LangChain 支持的 Chat Model:
from langchain_openai import ChatOpenAI
# 或者用 DeepSeek、Anthropic、Google 等任意 LangChain 支持的模型
llm = ChatOpenAI(model="gpt-4o-mini")
# 这就是一个调用 LLM 的 Node
def tool_calling_llm(state: MessagesState):
# state["messages"] 是当前对话历史(列表)
response = llm_with_tools.invoke(state["messages"])
# 把 LLM 的回复作为新消息返回
return {"messages": [response]}
就这么简单。Node 从 State 里取消息列表,调用 LLM,把结果放回 State。LangGraph 的哲学:Node 越简单越好,复杂性交给图结构来管理。
现代 LLM(GPT-4、DeepSeek 等)支持"工具调用"特性:你定义一些 Python 函数,告诉 LLM 这些函数的名称和参数,LLM 在回复时可以请求调用某个函数(而不是直接给出文字答案)。
# 定义一个工具:普通 Python 函数,有类型注解和 docstring
def multiply(a: int, b: int) -> int:
"""Multiply a and b.
Args:
a: first int
b: second int
"""
return a * b
# bind_tools:把函数"绑定"给 LLM,让它知道有这个工具可用
llm_with_tools = llm.bind_tools([multiply])
当你问 LLM "What is 2 multiplied by 3?",它不会直接说 "6",而是返回一个带 tool_calls 的 AIMessage:
# LLM 的回复:请求调用工具
tool_call.tool_calls
# 输出:
# [{'name': 'multiply', 'args': {'a': 2, 'b': 3}, 'id': 'call_xxx', 'type': 'tool_call'}]
LLM 返回的 tool_calls 只是一个"请求"——它说"我想调用 multiply(a=2, b=3)",但实际执行需要你的代码(或者 LangGraph 的 ToolNode)来完成。这是本讲只看了"一半"——下一讲 Router 会补全"执行工具"这一步。
LLM 会根据你的输入自动决定是否需要调用工具:
| 用户输入 | LLM 的行为 |
|---|---|
| "Hello!" | 直接用文字回复,不调用工具 |
| "Multiply 2 and 3" | 返回 tool_call,请求调用 multiply |
| "What's the capital of France?" | 直接用文字回复(不需要工具) |
在 1-1 的简单图里,State 只有一个字符串字段,每个 Node 返回新值覆盖旧值,这没问题。但当 State 里存的是消息列表时,就出问题了:
# State 初始:[HumanMsg]
# Node 返回:{"messages": [AIMsg]}
# State 变成:[AIMsg]
# ← HumanMsg 丢失了!
# State 初始:[HumanMsg]
# Node 返回:{"messages": [AIMsg]}
# State 变成:[HumanMsg, AIMsg]
# ← 追加,历史保留!
Reducer 是一个函数,它定义了"当 Node 返回新值时,State 里的旧值和新值怎么合并"。通过 Python 的 Annotated 类型提示来声明:
from typing import Annotated
from langchain_core.messages import AnyMessage
from langgraph.graph.message import add_messages
class MessagesState(TypedDict):
# Annotated[字段类型, Reducer函数]
# add_messages:把新消息追加到列表,而不是覆盖
messages: Annotated[list[AnyMessage], add_messages]
验证 add_messages 的行为:
from langgraph.graph.message import add_messages
initial = [AIMessage(content="Hello!"), HumanMessage(content="Hi!")]
new_msg = AIMessage(content="How can I help?")
result = add_messages(initial, new_msg)
# result = [AIMessage("Hello!"), HumanMessage("Hi!"), AIMessage("How can I help?")]
# ← 追加了,没有覆盖!
如果新消息的 id 和已有消息的 id 相同,add_messages 会更新那条消息而不是追加。这在流式输出(streaming)中很有用——可以用来更新部分生成的消息。
因为"State 里有一个用 add_messages 的 messages 列表"这个需求太普遍了,LangGraph 直接内置了 MessagesState:
from langgraph.graph import MessagesState
# 直接用!等价于手写 TypedDict + Annotated + add_messages
# 还可以继承它来添加自定义字段:
class MyState(MessagesState):
user_id: str # 额外添加的字段
search_results: list
| 方式 | 代码量 | 推荐场景 |
|---|---|---|
| 手写 TypedDict + Annotated | 多 | 只有消息字段时不推荐 |
| 内置 MessagesState | 少 | 大多数情况,直接用这个 |
| 继承 MessagesState | 适中 | 需要额外字段时 |
现在把所有零件组合成一张图:
from langgraph.graph import StateGraph, MessagesState, START, END
# Node:一个调用带工具绑定的 LLM 的节点
def tool_calling_llm(state: MessagesState):
return {"messages": [llm_with_tools.invoke(state["messages"])]}
# 构建图:START → tool_calling_llm → END
builder = StateGraph(MessagesState)
builder.add_node("tool_calling_llm", tool_calling_llm)
builder.add_edge(START, "tool_calling_llm")
builder.add_edge("tool_calling_llm", END)
graph = builder.compile()
这是一条直线图(Chain),没有分支。但 LLM 可以决定是否在回复里带 tool_calls。
同样的图,不同输入得到不同结果:
# 场景 1:普通问候,LLM 直接回复
messages = graph.invoke({"messages": HumanMessage(content="Hello!")})
# messages = {
# "messages": [
# HumanMessage("Hello!"),
# AIMessage("Hi there! How can I assist you today?") ← 纯文字回复
# ]
# }
# 场景 2:数学计算,LLM 请求调用工具
messages = graph.invoke({"messages": HumanMessage(content="Multiply 2 and 3")})
# messages = {
# "messages": [
# HumanMessage("Multiply 2 and 3"),
# AIMessage(tool_calls=[{'name':'multiply','args':{'a':2,'b':3}}]) ← 工具调用请求
# ]
# }
这张 Chain 图在 LLM 请求调用工具时只是返回了请求,并没有真正执行 multiply 函数。图走到 END 就停了。
下一讲(Router)会加入 ToolNode 和 tools_condition,让工具真正被执行,并把结果返回给用户。
| 概念 | 作用 | 关键代码 |
|---|---|---|
| HumanMessage / AIMessage | 对话消息的标准格式 | from langchain_core.messages import ... |
| ChatModel.invoke(messages) | 在 Node 里调用 LLM | llm.invoke(state["messages"]) |
| bind_tools | 让 LLM 知道可以调用哪些工具 | llm.bind_tools([multiply]) |
| add_messages Reducer | 追加消息而不是覆盖 | Annotated[list, add_messages] |
| MessagesState | 内置的对话状态,开箱即用 | from langgraph.graph import MessagesState |