From message types and chat models to tool bindings to reducers and MessagesState.
This lecture is the basic part for building practical AI applications in LangGraph.
In the first lecture (1-1) of Module 1, Node only does string concatenation and has nothing to do with LLM. At the beginning of this lecture, we willReal LLM callIntroducing LangGraph.
To allow LLM to enter the graph, 4 sub-problems need to be solved:
| subproblem | solution |
|---|---|
| What format is used for LLM input/output? | Messages(HumanMessage / AIMessage, etc.) |
| How to call LLM in Node? | Using LangChainChatModel |
| How to make LLM call external functions? | bind_toolsbinding tool |
| Why is the message list in State not overwritten? | Reducer(add_messages) |
Once you understand these four parts, you can install an LLM that "can think and call tools" into the Node of LangGraph, which is the basis for all subsequent Agents.
You have studied LangChain and know that the input of the chat model is a set of "messages". LangGraph completely reuses this system. LangChain defines multiple message types, each corresponding to a role in the conversation:
tool_calls(tool call request).tool_call_idTo correspond to the result of which tool call.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 is stateless - each call requiresComplete conversation historyPass it in so that it can "remember" the context. The message list is the carrier of conversation history. This list is stored in LangGraph's State so that each LLM call can see the complete history.
LangGraph's Node is just an ordinary Python function. In the function body, you can directly call any Chat Model supported by LangChain:
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]}
It's that simple. Node gets the message list from State, calls LLM, and puts the result back into State.LangGraph's philosophy: The simpler the Node, the better, and the complexity is managed by the graph structure.
Modern LLMs (GPT-4, DeepSeek, etc.) support the "tool call" feature: you define some Python functions, tell LLM the names and parameters of these functions, and LLM canRequest to call a function(Instead of giving a direct text answer).
# 定义一个工具:普通 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])
When you ask LLM "What is 2 multiplied by 3?", it will not directly say "6", but will return a string withtool_callsAIMessage:
# LLM 的回复:请求调用工具
tool_call.tool_calls
# 输出:
# [{'name': 'multiply', 'args': {'a': 2, 'b': 3}, 'id': 'call_xxx', 'type': 'tool_call'}]
Returned by LLMtool_callsJust a "request" - it says "I want to call multiply(a=2, b=3)", butactual executionRequires your code (or LangGraph's ToolNode) to complete. This is only the "half" of this lecture - Router will complete the "execution tool" step in the next lecture.
LLM will use your inputautomatic decisionDo you need to call tools:
| user input | LLM behavior |
|---|---|
| "Hello!" | Reply directly with text without calling tools |
| "Multiply 2 and 3" | Return tool_call, request to call multiply |
| "What's the capital of France?" | Reply directly with text (no tools required) |
In the simple diagram of 1-1, State has only one string field, and each Node returns the new value overwriting the old value, which is no problem. But when State is stored inMessage listWhen, something goes wrong:
# State 初始:[HumanMsg]
# Node 返回:{"messages": [AIMsg]}
# State 变成:[AIMsg]
# ← HumanMsg 丢失了!
# State 初始:[HumanMsg]
# Node 返回:{"messages": [AIMsg]}
# State 变成:[HumanMsg, AIMsg]
# ← 追加,历史保留!
reducer is a function that defines "how the old value and the new value in State are merged when Node returns a new value." Via PythonAnnotatedType hints to declare:
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]
verifyadd_messagesBehavior:
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?")]
# ← 追加了,没有覆盖!
If the new messageidand those who have newsidsame,add_messagesmeetingrenewThat message instead of appending. This is useful in streaming - updating partially generated messages.
Because the requirement of "there is a messages list using add_messages in State" is so common, LangGraph has it built-in directly.MessagesState:
from langgraph.graph import MessagesState
# 直接用!等价于手写 TypedDict + Annotated + add_messages
# 还可以继承它来添加自定义字段:
class MyState(MessagesState):
user_id: str # 额外添加的字段
search_results: list
| Way | Amount of code | Recommended scenarios |
|---|---|---|
| Handwriting TypedDict + Annotated | many | Not recommended when there is only a message field |
| Built-in MessagesState | few | In most cases, just use this |
| Inherit MessagesState | Moderate | When additional fields are required |
Now combine all the parts into one picture:
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()
This is a straight line graph (Chain) with no branches. But LLM can decide whether to include tool_calls in the reply.
The same picture, different inputs get different results:
# 场景 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}}]) ← 工具调用请求
# ]
# }
This Chain diagram is simplyRequest returned, the multiply function is not actually executed. The picture stops when it reaches END.
Will be added in the next lecture (Router)ToolNodeandtools_condition, so that the tool is actually executed and the results are returned to the user.
| concept | effect | key code |
|---|---|---|
| HumanMessage / AIMessage | Standard format for conversation messages | from langchain_core.messages import ... |
| ChatModel.invoke(messages) | Call LLM in Node | llm.invoke(state["messages"]) |
| bind_tools | Let LLM know which tools can be called | llm.bind_tools([multiply]) |
| add_messages Reducer | append message instead of overwrite | Annotated[list, add_messages] |
| MessagesState | Built-in conversation status, ready to use out of the box | from langgraph.graph import MessagesState |