LangGraph 系列 8:AgentState 状态详解与自定义状态


Context 适合保存用户 ID、用户名和权限等调用开始时已经确定的数据,在一次运行中通常按只读方式使用。

但是 Agent 运行起来以后,还会不断产生新的内容。例如用户消息、模型生成的工具调用、工具执行结果,以及工具从外部系统得到的数据。这些数据会随着执行过程变化,因此不能全部放在静态 Context 中。

LangChain 使用 AgentState 管理这类运行时状态。本篇从默认消息状态开始,再为 State 增加自定义字段,并让两个 Tool 通过 State 传递用户名。

1. Agent 中的 State

普通模型调用通常只有一次输入和一次输出:

用户问题 -> 模型回答

具有工具调用能力的 Agent 会经历多个步骤:

用户问题
-> 模型决定调用工具
-> 工具执行
-> 模型读取工具结果
-> 生成最终回答

后面的步骤需要知道前面发生了什么。如果工具执行阶段看不到模型刚刚生成的工具调用,便不知道应该执行哪个 Tool;如果第二次模型调用看不到 ToolMessage,也无法根据工具结果整理答案。

因此,Agent 需要一个贯穿本次执行过程的状态对象。它的主要职责有两个:

  1. 保存 Agent 执行过程中产生的消息。
  2. 保存应用自己定义的可变字段。

AgentState 是状态的结构,不是数据库。State 中有数据,不代表这些数据已经持久化。

2. 默认 State 如何保存消息

一次完整的工具调用通常会生成四条消息:

  1. 用户输入形成 HumanMessage。
  2. 模型生成包含 tool_calls 的 AIMessage。
  3. 工具执行结果形成 ToolMessage。
  4. 模型根据工具结果生成最终 AIMessage。

下图中的每个 State 卡片都是 stream_mode=”values” 返回的完整快照,不是某个节点单独返回的局部更新。后一个快照保留了前面的消息,并通过 add_messages 合并本步骤产生的新消息。

AgentState 消息与完整状态快照

这些消息保存在 AgentState.messages 中。Agent 每执行完一个节点,都会提交一部分状态更新;messages 字段通过 add_messages 合并新消息,不需要业务代码每次重新构造完整消息列表。

2.1 默认AgentState 的导入与字段

AgentState 应从 langchain.agents 导入:

from langchain.agents import AgentState

当前定义的主要字段可以简化理解为:

class AgentState(TypedDict):
    messages: list[AnyMessage]
    jump_to: NotRequired[str | None]
    structured_response: NotRequired[object]
  • messages 是最重要的字段,保存对话和工具调用消息。
  • jump_to 是框架内部控制执行跳转使用的字段,普通 Agent 代码通常不需要操作。
  • structured_response 在 Agent 使用结构化输出时保存解析后的结果。

2.2 默认 AgentState 代码

示例代码放在:

llm-learning/langgraph/p08_agent_state/

本文继续使用 .venv_langgraph,不需要安装新的数据库或向量组件:

cd /path/to/llm-learning
source .venv_langgraph/bin/activate
python -m pip install -r langgraph/p08_agent_state/requirements.txt

使用 LangGraph 系列 6 已经创建的模型环境启动 Qwen3:

source .venv_tool_server/bin/activate

"$VIRTUAL_ENV/bin/python" -m mlx_lm server \
  --model Qwen3-14B-AWQ-4bit-MLX \
  --host 127.0.0.1 \
  --port 18080 \
  --prompt-cache-size 0 \
  --chat-template-args '{"enable_thinking": false}'

先确认模型接口正常:

curl --noproxy '*' http://127.0.0.1:18080/v1/models

代码:

01_inspect_default_agent_state.py:

"""观察默认 AgentState 中的 messages 如何随 Agent 执行逐步增加。

默认 state 结构:{"messages": [HumanMessage, AIMessage, ToolMessage, ...]}
本脚本每执行一步打印一次 messages 列表的长度和最后一条消息。
"""

import httpx
from langchain.agents import create_agent
from langchain.tools import tool
from langchain_openai import ChatOpenAI


@tool
def calculator(a: int, b: int) -> int:
    """计算两个整数的加法。"""
    return a + b


model = ChatOpenAI(
    model="Qwen3-14B-AWQ-4bit-MLX",
    base_url="http://127.0.0.1:18080/v1",
    api_key="not-needed",
    temperature=0,
    max_tokens=128,
    http_client=httpx.Client(trust_env=False),
    http_async_client=httpx.AsyncClient(trust_env=False),
)

agent = create_agent(
    model=model,
    tools=[calculator],
    system_prompt="你是一个计算助手。遇到加法问题时必须调用 calculator 工具。",
)


def describe_latest_message(messages) -> str:
    """用一句话描述 messages 里最后一条消息在干什么。"""
    latest = messages[-1]
    kind = type(latest).__name__

    # 模型决定调工具:AIMessage 里带 tool_calls
    if kind == "AIMessage" and latest.tool_calls:
        call = latest.tool_calls[0]
        return f"模型要调用 {call['name']},参数 {call['args']}"

    # 工具执行完毕:ToolMessage
    if kind == "ToolMessage":
        return f"工具返回 {latest.content}"

    # 模型给出文字回答
    if kind == "AIMessage":
        return f"模型回答 {latest.content}"

    return f"{kind}{latest.content}"

# agent.stream() 返回的是迭代器(不是 list),每迭代一次 Agent 前进一步。
# stream_mode="values" 时,每次迭代得到的是一个 dict,即当时的完整 state 快照:
#   {"messages": [HumanMessage, AIMessage, ...]}
input_state = {"messages": [{"role": "user", "content": "请计算 12 加 8。"}]}
state_snapshots = agent.stream(input_state, stream_mode="values")

step = 1
for state in state_snapshots:
    messages = state["messages"]
    print(f"步骤 {step} | 共 {len(messages)} 条消息 | {describe_latest_message(messages)}")
    step += 1

运行:

python langgraph/p08_agent_state/01_inspect_default_agent_state.py

真实输出如下:

步骤 1 | 共 1 条消息 | HumanMessage:请计算 12 加 8。
步骤 2 | 共 2 条消息 | 模型要调用 calculator,参数 {'a': 12, 'b': 8}
步骤 3 | 共 3 条消息 | 工具返回 20
步骤 4 | 共 4 条消息 | 模型回答 12 加 8 的结果是 20。

stream_mode=”values” 返回每个步骤执行后的完整 State,所以可以直接看到 messages 从 1 条增加到 4 条。这不是四次独立调用,而是同一次 Agent 运行的四个状态快照。

4. 自定义 AgentState

默认 State 已经能够保存消息。如果还要保存用户名、订单号、当前任务阶段或工具生成的中间数据,就需要扩展 AgentState。

本文定义一个 CustomState:

class CustomState(AgentState):
    user_name: NotRequired[str]

这里使用 NotRequired,因为 Agent 刚开始运行时还没有把用户名写入 State。用户名会在第一个 Tool 执行后出现。

创建 Agent 时,需要通过 state_schema 注册它:

agent = create_agent(
    model=model,
    tools=[load_user_name, greet_user],
    context_schema=UserContext,
    state_schema=CustomState,
)

如果只定义 CustomState,却没有传给 state_schema,Agent 不会按这份自定义 Schema 管理字段。

4.1 . Context 与 State 如何配合

上一篇已经区分了静态 Context 与可变 State。本例把两者连接起来:

  1. 调用方通过 Context 传入用户名。
  2. load_user_name 从 runtime.context 读取用户名。
  3. Tool 返回 Command(update=…),把用户名写入 State。
  4. greet_user 从 runtime.state 读取用户名。
  5. Qwen3 根据工具结果生成最终回答。

图中保留了两次 Tool 调用前后的模型决策。Command(update=…) 只把字段和 ToolMessage 提交给 State,并不会直接执行 greet_user;Qwen3 读取第一条工具结果后,才决定发起第二次工具调用。

Runtime Context、Command 与自定义 AgentState 更新流程

这里特意不让第二个 Tool 再次读取 Context。这样可以明确验证:第一个 Tool 写入的 State,确实能够被后续执行步骤读取。

4.2 . 使用 Command 更新 State

普通 Tool 返回字符串或字典时,返回值会转换成工具结果交给模型,但不会自动写入任意自定义 State 字段。

需要修改 State 时,Tool 应返回 Command:

return Command(
    update={
        "user_name": user_name,
        "messages": [
            ToolMessage(
                content=f"已经把用户名 {user_name} 写入 AgentState。",
                tool_call_id=runtime.tool_call_id,
            )
        ],
    }
)

这段代码同时更新两个字段:

  • user_name 保存应用需要的自定义状态。
  • messages 保存模型需要看到的工具执行结果。

4.3 返回 ToolMessage

模型生成工具调用时,会产生一个工具调用 ID。工具执行后的 ToolMessage 必须使用相同 ID,模型才能知道这条结果对应哪一次调用。

当前 API 可以直接通过 runtime.tool_call_id 取得该值,不需要再单独声明 InjectedToolCallId 参数。

如果 Tool 返回 Command 却不补充对应的 ToolMessage,消息历史可能出现只有工具请求、没有工具结果的无效结构,后续模型调用可能失败。

4.4. Tool 如何读取 State

当前代码使用 ToolRuntime:

@tool
def greet_user(runtime: ToolRuntime[UserContext, CustomState]) -> str:
    user_name = runtime.state.get("user_name")
    return f"祝你使用顺利,{user_name}!"

ToolRuntime 由框架自动注入,不会出现在模型可见的 Tool Schema 中。它同时提供 state、context、config 和 tool_call_id 等运行信息。

早期版本也可能使用 InjectedState:

from typing import Annotated
from langgraph.prebuilt import InjectedState


@tool
def greet_user(state: Annotated[CustomState, InjectedState]) -> str:
    return f"祝你使用顺利,{state['user_name']}!"

这种写法仍然可以用于只注入 State 的场景。本文使用 ToolRuntime,因为同一个 Tool 还需要访问 Context 和工具调用 ID,入口更加统一。

4.5. 完整代码

02_custom_state_and_command.py:

"""使用自定义 AgentState、ToolRuntime 和 Command 在工具之间传递用户名。"""

from typing_extensions import NotRequired, TypedDict

import httpx
from langchain.agents import AgentState, create_agent
from langchain.tools import ToolRuntime, tool
from langchain_core.messages import AIMessage, ToolMessage
from langchain_core.utils.function_calling import convert_to_openai_tool
from langchain_openai import ChatOpenAI
from langgraph.types import Command


class UserContext(TypedDict):
    """定义调用开始时传入的静态用户上下文。"""

    user_name: str


class CustomState(AgentState):
    """在默认消息状态之外增加运行期间可变的用户名。"""

    user_name: NotRequired[str]


@tool
def load_user_name(runtime: ToolRuntime[UserContext, CustomState]) -> Command:
    """先从运行时上下文加载用户名,并把用户名写入 AgentState。"""

    if runtime.context is None or "user_name" not in runtime.context:
        raise ValueError("必须通过 context 传入 user_name")

    user_name = runtime.context["user_name"]

    # Command.update 同时更新两部分 State:
    # 1. user_name 是本例新增的自定义字段;
    # 2. messages 保存与本次 Tool Call 对应的工具结果。
    return Command(
        update={
            "user_name": user_name,
            "messages": [
                ToolMessage(
                    content=f"已经把用户名 {user_name} 写入 AgentState。",
                    tool_call_id=runtime.tool_call_id,
                )
            ],
        }
    )


@tool
def greet_user(runtime: ToolRuntime[UserContext, CustomState]) -> str:
    """在用户名已经写入 AgentState 后,为当前用户生成祝福语。"""

    # 第二个工具读取的是可变 State,不再读取静态 Context。
    user_name = runtime.state.get("user_name")
    if not user_name:
        return "AgentState 中还没有用户名,请先调用 load_user_name 工具。"

    return f"祝你使用顺利,{user_name}!"


# 第 1 步:创建模型,再声明 Agent 可以使用的 Context 和 State 结构。
model = ChatOpenAI(
    model="Qwen3-14B-AWQ-4bit-MLX",
    base_url="http://127.0.0.1:18080/v1",
    api_key="not-needed",
    temperature=0,
    max_tokens=256,
    http_client=httpx.Client(trust_env=False),
)


agent = create_agent(
    model=model,
    tools=[load_user_name, greet_user],
    system_prompt=(
        "你是一个祝福助手。"
    ),
    # context_schema:声明 invoke(..., context={...}) 可以传入哪些字段。
    # Context 是「本次调用的固定输入」,整次运行不变;由 ToolRuntime.context 读取。
    context_schema=UserContext,
    # state_schema:声明 Agent 运行过程中可读写哪些 state 字段(在默认 messages 之外扩展)。
    # State 会随工具执行变化;load_user_name 通过 Command.update 写入 user_name,
    # greet_user 再从 ToolRuntime.state 读取。
    state_schema=CustomState,
)

def run_for_user(user_name: str) -> None:
    """使用指定用户名独立运行一次 Agent。"""

    # 第 2 步:Context 是本次调用的固定输入,State 会在工具执行时变化。
    result = agent.invoke(
        {"messages": [{"role": "user", "content": "请给当前用户一句祝福语。"}]},
        context={"user_name": user_name},
    )

    # 第 3 步:遍历消息,查看两个工具的实际调用顺序。
    tool_names = []
    for message in result["messages"]:
        if isinstance(message, AIMessage) and message.tool_calls:
            # 一条 AIMessage 可能包含多个 Tool Call,所以再遍历一次。
            for call in message.tool_calls:
                tool_names.append(call["name"])
        elif isinstance(message, ToolMessage):
            print(f"工具结果:{message.content}")

    print(f"当前调用:{user_name}")
    print(f"工具调用顺序:{' -> '.join(tool_names)}")
    print(f"最终 State 用户名:{result.get('user_name')}")
    print(f"最终回答:{result['messages'][-1].content}\n")

# runtime 由框架注入,因此两个工具都没有模型可见参数。
for current_tool in [load_user_name, greet_user]:
    tool_schema = convert_to_openai_tool(current_tool)
    properties = tool_schema["function"]["parameters"]["properties"]
    print(f"{current_tool.name} 的模型可见参数:{properties}")

print()
run_for_user("小明")
run_for_user("小红")

运行:

python langgraph/p08_agent_state/02_custom_state_and_command.py

真实输出如下:

load_user_name 的模型可见参数:{}
greet_user 的模型可见参数:{}

工具结果:已经把用户名 小明 写入 AgentState。
工具结果:祝你使用顺利,小明!
当前调用:小明
工具调用顺序:load_user_name -> greet_user
最终 State 用户名:小明
最终回答:祝你使用顺利,小明!

工具结果:已经把用户名 小红 写入 AgentState。
工具结果:祝你使用顺利,小红!
当前调用:小红
工具调用顺序:load_user_name -> greet_user
最终 State 用户名:小红
最终回答:祝你使用顺利,小红!

这次运行验证了三件事:

  1. Qwen3 按顺序调用了两个 Tool。
  2. 第二个 Tool 能读取第一个 Tool 写入的 State。
  3. 小明和小红的两次 invoke() 没有互相覆盖。

工具调用 ID 是动态值,示例没有打印它,但 runtime.tool_call_id 已经用于创建匹配的 ToolMessage。

5. 状态的追加,修改与删除

普通状态字段如果没有 Reducer,新值通常会覆盖旧值。但 AgentState.messages 已经使用 add_messages 作为内置 Reducer。

它至少解决三个问题:

  1. 新消息拥有新 ID 时追加到列表。
  2. 新消息与已有消息 ID 相同时替换原消息。
  3. 收到 RemoveMessage 时删除对应 ID 的消息。

这里只介绍默认 messages 字段必须理解的行为。自定义 Reducer、并行节点冲突和工作流状态合并留到后续 Workflow 文章。

完整代码:

03_message_update_and_remove.py:

"""结合 LLM 与自定义 State,演示 messages / state 的追加、修改、删除。

流程:
  1. 与 02 相同:invoke Agent,LLM + 工具自然追加 messages 和 user_name
  2. 在真实对话结果上,用 add_messages 手动「追加 / 同 ID 替换 / RemoveMessage 删除」
  3. 用修剪后的 messages 再次 invoke,观察 LLM 只能看到保留下来的历史

messages 的 reducer 规则(add_messages):
  - 新 ID  → 追加到列表末尾
  - 相同 ID → 替换原消息(修改)
  - RemoveMessage(id=...) → 删除该条消息
"""

from typing_extensions import NotRequired, TypedDict

import httpx
from langchain.agents import AgentState, create_agent
from langchain.tools import ToolRuntime, tool
from langchain_core.messages import (
    AIMessage,
    HumanMessage,
    RemoveMessage,
    ToolMessage,
)
from langgraph.graph.message import add_messages
from langgraph.types import Command
from langchain_openai import ChatOpenAI


class UserContext(TypedDict):
    user_name: str


class CustomState(AgentState):
    user_name: NotRequired[str]
    # 记录对消息历史做过的操作,演示自定义字段也可随 Command 更新。
    edit_log: NotRequired[list[str]]


def _message_preview(message) -> str:
    content = str(message.content or "").strip()
    if content:
        return content[:50].replace("\n", " ")
    if isinstance(message, AIMessage) and message.tool_calls:
        names = ", ".join(tc["name"] for tc in message.tool_calls)
        return f"[tool_calls: {names}]"
    return ""


def print_state(title: str, state: dict) -> None:
    """打印 state 中的自定义字段和 messages 摘要。"""
    print(f"\n{'=' * 60}")
    print(title)
    print(f"user_name: {state.get('user_name')}")
    print(f"edit_log:  {state.get('edit_log', [])}")
    messages = state["messages"]
    print("messages:")
    if not messages:
        print("  (empty)")
        return
    id_width = max(len(str(m.id or "")) for m in messages)
    type_width = max(len(type(m).__name__) for m in messages)
    for message in messages:
        msg_id = str(message.id or "")
        msg_type = type(message).__name__
        preview = _message_preview(message)
        print(f"  {msg_id:<{id_width}} | {msg_type:<{type_width}} | {preview}")


@tool
def load_user_name(runtime: ToolRuntime[UserContext, CustomState]) -> Command:
    """从 context 读取用户名,写入 AgentState。"""
    user_name = runtime.context["user_name"]
    return Command(
        update={
            "user_name": user_name,
            "messages": [
                ToolMessage(
                    content=f"已加载用户 {user_name}。[后面测试删除]",
                    tool_call_id=runtime.tool_call_id,
                )
            ],
        }
    )


@tool
def greet_user(runtime: ToolRuntime[UserContext, CustomState]) -> str:
    """根据 state 中的用户名生成祝福语。"""
    user_name = runtime.state.get("user_name")
    if not user_name:
        return "请先调用 load_user_name。"
    return f"祝你使用顺利,{user_name}!"


model = ChatOpenAI(
    model="Qwen3-14B-AWQ-4bit-MLX",
    base_url="http://127.0.0.1:18080/v1",
    api_key="not-needed",
    temperature=0,
    max_tokens=256,
    http_client=httpx.Client(trust_env=False),
    http_async_client=httpx.AsyncClient(trust_env=False),
)

agent = create_agent(
    model=model,
    tools=[load_user_name, greet_user],
    system_prompt="你是祝福助手。",
    context_schema=UserContext,
    state_schema=CustomState,
)


# ---------------------------------------------------------------------------
# 第 1 步:正常 LLM 交互 —— Agent 自动追加 messages(与 02 相同)
# ---------------------------------------------------------------------------
state = agent.invoke(
    {"messages": [{"role": "user", "content": "请给当前用户送上一句祝福语。"}]},
    context={"user_name": "小明"},
)
print_state("1. 首次 invoke 后(LLM + 工具自动追加 messages)", state)

# 找到第一条 ToolMessage 和最后一条 AIMessage,供后续修改 / 删除演示。
tool_message = next(m for m in state["messages"] if isinstance(m, ToolMessage))
final_ai_message = next(
    m for m in reversed(state["messages"]) if isinstance(m, AIMessage)
)

messages = state["messages"]
edit_log = list(state.get("edit_log") or [])

# ---------------------------------------------------------------------------
# 第 2 步:追加 —— 新 ID 的消息会 append 到末尾
# ---------------------------------------------------------------------------
messages = add_messages(
    messages,
    [HumanMessage(content="[系统备注] 已记录本次祝福。", id="note-append")],
)
edit_log.append("追加 note-append")
print_state("2. 追加一条 HumanMessage(新 ID)", {**state, "messages": messages, "edit_log": edit_log})

# ---------------------------------------------------------------------------
# 第 3 步:修改 —— 相同 ID 会替换原消息,而不是再追加一条
# ---------------------------------------------------------------------------
messages = add_messages(
    messages,
    [
        AIMessage(
            content=final_ai_message.content + "(测试更新)",
            id=final_ai_message.id,
        )
    ],
)
edit_log.append(f"修改 AIMessage id={final_ai_message.id}")
print_state("3. 用相同 ID 替换最终 AIMessage", {**state, "messages": messages, "edit_log": edit_log})

# ---------------------------------------------------------------------------
# 第 4 步:删除 —— RemoveMessage 按 ID 移除(不能 del messages[i])
# ---------------------------------------------------------------------------
messages = add_messages(messages, [RemoveMessage(id=tool_message.id)])
edit_log.append(f"删除 ToolMessage id={tool_message.id}")
print_state("4. 删除中间 ToolMessage", {**state, "messages": messages, "edit_log": edit_log})

# ---------------------------------------------------------------------------
# 第 5 步:用修剪后的 state 再次 invoke,LLM 只能看到剩余 messages
# ---------------------------------------------------------------------------
state_after_edit = {
    "messages": messages,
    "user_name": state.get("user_name"),
    "edit_log": edit_log,
}

follow_up = agent.invoke(
    {
        **state_after_edit,
        "messages": add_messages(
            messages,
            [HumanMessage(content="刚才祝福的是哪个用户?", id="user-followup")],
        ),
    },
    context={"user_name": "小明"},
)
print_state("5. 携带修剪后的历史再次 invoke", follow_up)
print(f"\n最终回答:{follow_up['messages'][-1].content}")

运行:

python langgraph/p08_agent_state/03_message_update_and_remove.py

真实输出如下:

============================================================
1. 首次 invoke 后(LLM + 工具自动追加 messages)
user_name: 小明
edit_log:  []
messages:
  3fe5b0cd-bc1c-473a-bfd7-364f1b580194           | HumanMessage | 请给当前用户送上一句祝福语。
  lc_run--019f9f22-ed2f-7f82-8033-7bc6c3234046-0 | AIMessage    | [tool_calls: load_user_name, greet_user]
  abad69f4-27b1-4133-9205-1b05f4644a19           | ToolMessage  | 已加载用户 小明。[后面测试删除]
  c57959ab-1feb-4f64-866f-36981281a5d9           | ToolMessage  | 请先调用 load_user_name。
  lc_run--019f9f22-f6b7-7bd1-9ed9-d6f6f781e001-0 | AIMessage    | 已加载用户 小明。祝你每天都有好心情!

============================================================
2. 追加一条 HumanMessage(新 ID)
user_name: 小明
edit_log:  ['追加 note-append']
messages:
  3fe5b0cd-bc1c-473a-bfd7-364f1b580194           | HumanMessage | 请给当前用户送上一句祝福语。
  lc_run--019f9f22-ed2f-7f82-8033-7bc6c3234046-0 | AIMessage    | [tool_calls: load_user_name, greet_user]
  abad69f4-27b1-4133-9205-1b05f4644a19           | ToolMessage  | 已加载用户 小明。[后面测试删除]
  c57959ab-1feb-4f64-866f-36981281a5d9           | ToolMessage  | 请先调用 load_user_name。
  lc_run--019f9f22-f6b7-7bd1-9ed9-d6f6f781e001-0 | AIMessage    | 已加载用户 小明。祝你每天都有好心情!
  note-append                                    | HumanMessage | [系统备注] 已记录本次祝福。

============================================================
3. 用相同 ID 替换最终 AIMessage
user_name: 小明
edit_log:  ['追加 note-append', '修改 AIMessage id=lc_run--019f9f22-f6b7-7bd1-9ed9-d6f6f781e001-0']
messages:
  3fe5b0cd-bc1c-473a-bfd7-364f1b580194           | HumanMessage | 请给当前用户送上一句祝福语。
  lc_run--019f9f22-ed2f-7f82-8033-7bc6c3234046-0 | AIMessage    | [tool_calls: load_user_name, greet_user]
  abad69f4-27b1-4133-9205-1b05f4644a19           | ToolMessage  | 已加载用户 小明。[后面测试删除]
  c57959ab-1feb-4f64-866f-36981281a5d9           | ToolMessage  | 请先调用 load_user_name。
  lc_run--019f9f22-f6b7-7bd1-9ed9-d6f6f781e001-0 | AIMessage    | 已加载用户 小明。祝你每天都有好心情!(测试更新)
  note-append                                    | HumanMessage | [系统备注] 已记录本次祝福。

============================================================
4. 删除中间 ToolMessage
user_name: 小明
edit_log:  ['追加 note-append', '修改 AIMessage id=lc_run--019f9f22-f6b7-7bd1-9ed9-d6f6f781e001-0', '删除 ToolMessage id=abad69f4-27b1-4133-9205-1b05f4644a19']
messages:
  3fe5b0cd-bc1c-473a-bfd7-364f1b580194           | HumanMessage | 请给当前用户送上一句祝福语。
  lc_run--019f9f22-ed2f-7f82-8033-7bc6c3234046-0 | AIMessage    | [tool_calls: load_user_name, greet_user]
  c57959ab-1feb-4f64-866f-36981281a5d9           | ToolMessage  | 请先调用 load_user_name。
  lc_run--019f9f22-f6b7-7bd1-9ed9-d6f6f781e001-0 | AIMessage    | 已加载用户 小明。祝你每天都有好心情!(测试更新)
  note-append                                    | HumanMessage | [系统备注] 已记录本次祝福。

============================================================
5. 携带修剪后的历史再次 invoke
user_name: 小明
edit_log:  ['追加 note-append', '修改 AIMessage id=lc_run--019f9f22-f6b7-7bd1-9ed9-d6f6f781e001-0', '删除 ToolMessage id=abad69f4-27b1-4133-9205-1b05f4644a19']
messages:
  3fe5b0cd-bc1c-473a-bfd7-364f1b580194           | HumanMessage | 请给当前用户送上一句祝福语。
  lc_run--019f9f22-ed2f-7f82-8033-7bc6c3234046-0 | AIMessage    | [tool_calls: load_user_name, greet_user]
  c57959ab-1feb-4f64-866f-36981281a5d9           | ToolMessage  | 请先调用 load_user_name。
  lc_run--019f9f22-f6b7-7bd1-9ed9-d6f6f781e001-0 | AIMessage    | 已加载用户 小明。祝你每天都有好心情!(测试更新)
  note-append                                    | HumanMessage | [系统备注] 已记录本次祝福。
  user-followup                                  | HumanMessage | 刚才祝福的是哪个用户?
  lc_run--019f9f22-fa5f-7db0-b9d0-6ef457a9c7bb-0 | AIMessage    | 刚才祝福的用户是小明。

最终回答:刚才祝福的用户是小明。

需要注意,当前 Command 构造函数没有 delete 参数。删除消息不是把 update 改成 delete,而是向 messages 提交一个带目标 ID 的 RemoveMessage,再由 add_messages 完成删除。

6. AgentState 与记忆存储的边界

State 描述 Agent 当前拥有哪些数据,以及节点更新数据时应该怎样合并。它解决的是运行过程中的数据传递问题。

但是下面这段代码连续执行两次时:

agent.invoke(input, context={"user_name": "小明"})
agent.invoke(input, context={"user_name": "小红"})

如果没有配置 Checkpointer,两次调用会创建相互独立的 State。第二次调用不会自动得到第一次调用的消息或 user_name。

后续记忆文章会继续介绍:

  • Checkpointer 如何按 Thread 保存短期状态。
  • 开发环境和 PostgreSQL 如何持久化 Checkpoint。
  • Store 如何保存跨 Thread 使用的长期数据。

这些能力建立在 State 之上,但不属于 AgentState Schema 本身。

7. 小结

默认 AgentState 已经负责管理 Agent 的消息历史。用户消息、工具调用、工具结果和最终回答都会依次进入 messages,add_messages 负责合并这些更新。

业务需要额外的运行状态时,可以继承 AgentState 增加字段,再通过 state_schema 注册。Tool 使用 ToolRuntime.state 读取状态,使用 Command(update=…) 修改状态。

本次实测中,Qwen3 依次调用两个 Tool,将 Context 中的用户名写入 CustomState,再从 State 读取用户名生成祝福。小明和小红两次调用的状态完全隔离。


文章作者: hnbian
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hnbian !
评论
  目录