LangChain LangGraph
Module 1 Module 2 Module 3 Module 4 Module 5 Module 6
Module 1 · 基础入门
1 1-1 Simple Graph
2 1-2 Chain
3 1-3 Router
4 1-4 Agent
5 1-5 Agent Memory
6 1-6 Deployment
SUM Module Summary
HomeLangGraphModule 1 · 基础入门1-2 Chain
📓 Notebook 📖 Explained
LANGGRAPH MODULE 1 · LESSON 2

Chain:LLM + Messages + Tools
一切的起点

从消息类型、聊天模型到工具绑定,再到 Reducer 和 MessagesState。
这一讲是 LangGraph 中构建实际 AI 应用的基础零件。

1 这一讲要解决什么问题?

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 的基础。

2 Messages:LLM 对话的基本单位

你学过 LangChain,知道聊天模型的输入是一组"消息"。LangGraph 完全复用了这个体系。LangChain 定义了多种消息类型,每种对应对话中的一个角色:

System
SystemMessage — 给 LLM 的"人设"指令。在对话开始之前告诉 LLM 它应该是谁、应该做什么。例如:"你是一个数学助手。"
Human
HumanMessage — 用户发出的消息。这是你(或你的应用)传给 LLM 的问题或指令。
AI
AIMessage — LLM 生成的回复。可以是纯文本,也可以包含 tool_calls(工具调用请求)。
Tool
ToolMessage — 工具执行完毕后,把结果告诉 LLM 的消息。包含 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 调用都能看到完整历史。

3 Chat Models:在 Node 里调用 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 越简单越好,复杂性交给图结构来管理。

4 Tools:让 LLM 调用函数

什么是 Tool Calling?

现代 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 只是"请求"调用,不会真的执行

LLM 返回的 tool_calls 只是一个"请求"——它说"我想调用 multiply(a=2, b=3)",但实际执行需要你的代码(或者 LangGraph 的 ToolNode)来完成。这是本讲只看了"一半"——下一讲 Router 会补全"执行工具"这一步。

LLM 如何知道何时调用工具?

LLM 会根据你的输入自动决定是否需要调用工具:

用户输入LLM 的行为
"Hello!"直接用文字回复,不调用工具
"Multiply 2 and 3"返回 tool_call,请求调用 multiply
"What's the capital of France?"直接用文字回复(不需要工具)

5 Reducer:解决状态"被覆盖"的问题

问题:默认行为会破坏对话历史

在 1-1 的简单图里,State 只有一个字符串字段,每个 Node 返回新值覆盖旧值,这没问题。但当 State 里存的是消息列表时,就出问题了:

❌ 默认覆盖行为(错误)

# State 初始:[HumanMsg]
# Node 返回:{"messages": [AIMsg]}
# State 变成:[AIMsg]
# ← HumanMsg 丢失了!

✅ 使用 add_messages Reducer

# State 初始:[HumanMsg]
# Node 返回:{"messages": [AIMsg]}
# State 变成:[HumanMsg, AIMsg]
# ← 追加,历史保留!

Reducer 是什么?

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?")]
# ← 追加了,没有覆盖!
add_messages 还有一个隐藏功能

如果新消息的 id 和已有消息的 id 相同,add_messages更新那条消息而不是追加。这在流式输出(streaming)中很有用——可以用来更新部分生成的消息。

6 MessagesState:官方内置的对话状态

因为"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适中需要额外字段时

7 完整 Chain 图:组装起来

现在把所有零件组合成一张图:

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 图的结构

__start__
tool_calling_llm
读取 messages → 调用 LLM → 返回 AIMessage
__end__

这是一条直线图(Chain),没有分支。但 LLM 可以决定是否在回复里带 tool_calls。

8 运行结果解读

同样的图,不同输入得到不同结果:

# 场景 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)会加入 ToolNodetools_condition,让工具真正被执行,并把结果返回给用户。

小结:这一讲的知识点地图

概念作用关键代码
HumanMessage / AIMessage对话消息的标准格式from langchain_core.messages import ...
ChatModel.invoke(messages)在 Node 里调用 LLMllm.invoke(state["messages"])
bind_tools让 LLM 知道可以调用哪些工具llm.bind_tools([multiply])
add_messages Reducer追加消息而不是覆盖Annotated[list, add_messages]
MessagesState内置的对话状态,开箱即用from langgraph.graph import MessagesState