LangGraph 系列 7:智能体运行时上下文与静态配置


前面已经完成本地 Tool、流式工具调用和 Agent API 的学习。调用 Agent 时,我们一直把用户问题放在 messages 中,但实际应用除了问题本身,通常还需要知道是谁发起了请求。

例如,一个用户询问“告诉我当前用户的年龄”,模型只看到这句话,并不知道“当前用户”具体是谁。应用可以要求用户把 ID 写在问题中,但这样既不自然,也可能把不应该由模型决定的身份信息暴露成工具参数。

更合适的方式是在调用 Agent 时额外传入用户 ID、用户名、租户或权限等上下文。动态提示词和 Tool 可以读取这些数据,而模型只需要处理正常的用户问题。

课程把这类数据放在 config[“configurable”] 中。这个写法有助于理解配置如何跟随一次调用传播,但在本文使用的 langchain==1.2.13 中,业务上下文已经有了更明确的入口:context_schema、context= 和 runtime.context。

本篇先理解课程中的 RunnableConfig,再使用类型化 Runtime Context 创建一个能够识别当前用户的本地 Qwen3 Agent。

1. 智能体为什么需要运行时上下文

先看下面这条问题:

告诉我当前用户的年龄和城市。

要正确回答,Agent 至少还需要两个信息:

user_id:用于查询用户资料
user_name:用于生成个性化系统提示词

这些字段不适合直接放进用户消息:

  • 用户不应该每次都手工填写自己的 ID。
  • 用户 ID 应该由登录系统或服务端确认,而不是由模型猜测。
  • Tool 不应该要求模型生成当前用户 ID,否则模型可能传错用户。
  • 同一个 Agent 应该能够被不同用户复用,而不是把用户名写死在代码中。

运行时上下文解决的就是这个问题:调用方在运行开始时提供一组业务数据,Agent 的提示词、Tool 或中间件可以在本次运行中读取它们。

2. Config、Context 和 State 不是同一个对象

在 LangChain 和 LangGraph 中,下面三个对象很容易混淆:

  • config:控制一次执行,例如标签、追踪信息、回调和线程配置。
  • context:提供本次运行需要的业务上下文,例如用户 ID 和用户名。
  • state:保存执行过程中不断变化的数据,例如消息和中间结果。

下图从传入方式、读取入口、可变性和生命周期四个角度比较这三个对象。尤其需要注意:State 默认只存在于单次运行中,配置 Checkpointer 后才可以按线程保存并在后续调用中恢复。

Config、Context 与 State 的职责边界

configurable 并没有被废弃,thread_id 等执行标识仍然需要放在这里。本文只是把用户身份这类业务字段迁移到 Runtime Context,让执行控制与业务依赖各自使用明确的入口。

本篇只实现前两个对象。AgentState 仅用于说明边界,不定义自定义字段,也不讨论 Reducer、Checkpointer 或记忆存储。

2.1 静态上下文的“静态”是什么意思

静态上下文具有下面几个特点:

  1. 在调用 invoke()、stream() 或 Agent API 时传入。
  2. 只服务于本次运行。
  3. 动态提示词和 Tool 可以读取同一份上下文。
  4. 运行过程中应当把它视为只读数据。

这里的“不可变”是应用层面的设计约定。Python 不会自动阻止代码修改一个普通字典中的内容。正确做法是只读取 Context,需要变化的数据留给后续的 State 或 Store,而不是在 Tool 中修改 Context。

3. 先理解课程中的 RunnableConfig

课程采用下面的结构传入用户名:

config = {
    "configurable": {
        "user_name": "小明",
    }
}

最外层对象是 RunnableConfig。configurable 是其中的一个字段,里面可以存放调用方传入的可配置值。

这个对象不是项目配置文件。它与下面这些文件没有关系:

.env
langgraph.json
pyproject.toml

它是一份跟随当前 Runnable 或 Graph 调用传播的运行配置。

3.1 RunnableConfig 中还有什么

除了 configurable,常见字段还包括:

字段 作用
tags 给当前运行增加标签,便于追踪和筛选
metadata 附加运行元数据
callbacks 设置回调处理器
recursion_limit 限制 Graph 的最大递归步数
run_name 设置当前运行名称

使用 Checkpointer 时,thread_id 仍然放在 configurable 中:

config = {
    "configurable": {
        "thread_id": "conversation-001",
    }
}

thread_id 是 LangGraph 持久化机制需要的执行标识,不应该因此把所有业务字段都继续塞进 configurable。

3.2 编写 RunnableConfig 示例

langgraph/p07_agent_runtime_context/01_runnable_configurable_basic.py 的完整代码如下:

"""演示课程中的 RunnableConfig 与 configurable 静态配置写法。"""

from langchain_core.runnables import RunnableConfig, RunnableLambda


def read_config(question: str, config: RunnableConfig) -> dict[str, object]:
    """读取本次 Runnable 调用携带的配置。"""

    # configurable 用于保存调用方传入的可配置字段。
    configurable = config.get("configurable", {})

    return {
        "question": question,
        "user_name": configurable.get("user_name", "匿名用户"),
        "tags": config.get("tags", []),
        "metadata": config.get("metadata", {}),
    }


# RunnableLambda 会把 invoke() 的 config 自动注入 read_config()。
runnable = RunnableLambda(read_config)

result = runnable.invoke(
    "请介绍一下当前用户。",
    config={
        "configurable": {"user_name": "小明"},
        "tags": ["context-demo"],
        "metadata": {"source": "series-24"},
    },
)

print(f"问题:{result['question']}")
print(f"用户名:{result['user_name']}")
print(f"标签:{result['tags']}")
print(f"元数据:{result['metadata']}")

执行:

cd source/_posts/llm_learning

source .venv_langgraph/bin/activate
python langgraph/p07_agent_runtime_context/01_runnable_configurable_basic.py

真实输出如下:

问题:请介绍一下当前用户。
用户名:小明
标签:['context-demo']
元数据:{'source': 'series-24', 'user_name': '小明'}

在 langchain-core==1.2.22 的这次调用中,configurable.user_name 也被合并到了运行元数据中,因此最后一行同时出现了 source 和 user_name。

4. 为什么实战改用 Runtime Context

RunnableConfig 是通用运行配置。课程把 user_name 放进 configurable 可以正常读取,但这会把业务身份和执行控制放在同一个字典中。

本文使用的 create_agent() 提供了单独的 context_schema 参数。编译后的 Graph 也支持:

graph.invoke(input, context={...})

因此可以形成更清晰的分工:

config  -> 怎样执行
context -> 为谁执行、依赖什么业务数据

Context 会被包装在 LangGraph 的 Runtime 中。动态提示词通过 request.runtime.context 读取,Tool 通过 ToolRuntime.context 读取。

下图按照一次完整工具调用的顺序展示 Context 的两条读取路径:动态提示词读取 user_name,本地 Tool 读取 user_id。config 只控制这次运行,不会自动变成模型提示词的一部分。

Agent Runtime Context 注入与工具调用时序图

动态提示词会在每次模型调用前执行。ToolRuntime 则在执行 Tool 时由框架注入,不出现在模型可见的参数 Schema 中。两者读取的是同一次运行的 Context,但使用目的不同。

原始 Context 不会作为一个完整对象直接发送给模型。应用可以选择把用户名整理进系统提示词;用户 ID 则只交给本地 Tool 使用,不出现在模型可见的工具参数中。

5. 准备项目和运行环境

示例继续使用 LangGraph 系列 2 创建的 .venv_langgraph,主要版本如下:

langchain==1.2.13
langchain-core==1.2.22
langchain-openai==1.1.12
langgraph==1.1.3
openai==2.30.0
httpx==0.28.1

安装 p07 本地项目:

cd source/_posts/llm_learning

source .venv_langgraph/bin/activate
python -m pip install -e langgraph/p07_agent_runtime_context
python -m pip check

实际检查结果:

No broken requirements found.

requirements.txt 继续复用 LangGraph 系列 2 的历史锁文件:

-r ../p02_local_agent_project/requirements-lock.txt

5.1 启动本地 Qwen3

模型服务复用 LangGraph 系列 6 创建的 .venv_tool_server:

cd source/_posts/llm_learning

.venv_tool_server/bin/python -m mlx_lm server \
  --model model \
  --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

本文实测接口返回一个可用模型,后续代码均通过 http://127.0.0.1:18080/v1 连接它。

6. 定义类型化的 UserContext

本例只需要两个字段:

class UserContext(TypedDict):
    """定义每次 Agent 运行必须传入的用户上下文。"""

    user_id: str
    user_name: str

TypedDict 可以让编辑器和类型检查工具知道上下文有哪些字段,但它不会自动完成完整的运行时校验。因此示例会在读取字段之前主动检查 Context 是否存在。

6.1 使用动态系统提示词

固定系统提示词通常这样定义:

system_prompt="你是一个中文智能助手。"

如果提示词需要使用当前用户名,就不能把名字写死。@dynamic_prompt 会在每次模型调用前执行,并通过 ModelRequest 提供本次运行的 Runtime:

@dynamic_prompt
def personalized_prompt(request: ModelRequest) -> str:
    context = request.runtime.context
    if context is None or "user_name" not in context:
        raise ValueError("必须通过 context 传入 user_name")

    user_name = context["user_name"]
    return (
        "你是一个中文智能助手。"
        f"当前用户的名字是{user_name}。"
        "用户询问自己的资料时,必须调用 get_current_user_info 工具。"
    )

这里的函数是中间件,不是 Tool。每次模型准备回答时都会执行,不需要模型先判断是否调用它。

6.2 在 Tool 中读取当前用户

用户 ID 不应该作为普通 Tool 参数:

# 不推荐:user_id 需要由模型生成
def get_user_info(user_id: str):
    ...

使用 ToolRuntime 后,框架会自动注入 Context:

@tool
def get_current_user_info(runtime: ToolRuntime[UserContext]) -> dict[str, object]:
    """查询当前用户的姓名、年龄和所在城市。"""

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

    user_id = context["user_id"]
    return USER_DATABASE.get(user_id, {"error": "没有找到当前用户"})

模型只需要决定是否调用 get_current_user_info,不需要知道当前用户 ID。

6.3 完整的 Agent Graph

langgraph/p07_agent_runtime_context/src/context_agent/graph.py 的完整代码如下:

"""创建使用类型化运行时上下文的本地 Qwen3 Agent。"""

from typing_extensions import TypedDict

import httpx
from langchain.agents import create_agent
from langchain.agents.middleware import ModelRequest, dynamic_prompt
from langchain.tools import ToolRuntime, tool
from langchain_openai import ChatOpenAI


class UserContext(TypedDict):
    """定义每次 Agent 运行必须传入的用户上下文。"""

    user_id: str
    user_name: str


# 使用固定数据模拟用户数据库,避免引入数据库和后续记忆知识。
USER_DATABASE = {
    "user-001": {"name": "小明", "age": 18, "city": "杭州"},
    "user-002": {"name": "小红", "age": 20, "city": "成都"},
}


model = ChatOpenAI(
    model="default_model",
    base_url="http://127.0.0.1:18080/v1",
    api_key="not-needed",
    temperature=0,
    max_tokens=256,
    # 禁止 httpx 读取系统代理,确保本机请求直连 127.0.0.1。
    http_client=httpx.Client(trust_env=False),
)


@dynamic_prompt
def personalized_prompt(request: ModelRequest) -> str:
    """根据本次运行的用户名生成动态系统提示词。"""

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

    user_name = context["user_name"]
    return (
        "你是一个中文智能助手。"
        f"当前用户的名字是{user_name}。"
        "用户询问自己的资料时,必须调用 get_current_user_info 工具。"
    )


@tool
def get_current_user_info(runtime: ToolRuntime[UserContext]) -> dict[str, object]:
    """查询当前用户的姓名、年龄和所在城市。"""

    # runtime 由 LangChain 自动注入,模型不需要生成 user_id 参数。
    context = runtime.context
    if context is None or "user_id" not in context:
        raise ValueError("必须通过 context 传入 user_id")

    user_id = context["user_id"]
    return USER_DATABASE.get(user_id, {"error": "没有找到当前用户"})


# context_schema 声明本 Graph 接收的运行时上下文结构。
graph = create_agent(
    model=model,
    tools=[get_current_user_info],
    middleware=[personalized_prompt],
    context_schema=UserContext,
)

这里没有定义 state_schema,也没有导入 AgentState。本例只关心运行开始时传入的静态上下文。

7. 直接调用 Context Agent

调用 Graph 时,用户问题仍然放在 messages 中,身份信息通过单独的 context 参数传入:

result = graph.invoke(
    {"messages": [{"role": "user", "content": "告诉我当前用户的年龄和城市。"}]},
    context={"user_id": user_id, "user_name": user_name},
)

langgraph/p07_agent_runtime_context/02_invoke_context_agent.py 的完整代码如下:

"""直接调用 Context Agent,验证动态提示词、工具上下文和用户隔离。"""

from langchain_core.messages import AIMessage, ToolMessage
from langchain_core.utils.function_calling import convert_to_openai_tool

from context_agent.graph import get_current_user_info, graph


def run_for_user(user_id: str, user_name: str) -> None:
    """使用指定用户上下文运行一次 Agent。"""

    result = graph.invoke(
        {"messages": [{"role": "user", "content": "告诉我当前用户的年龄和城市。"}]},
        context={"user_id": user_id, "user_name": user_name},
    )

    print(f"\n当前上下文:{user_id} / {user_name}")
    for message in result["messages"]:
        if isinstance(message, AIMessage) and message.tool_calls:
            print(f"工具调用:{message.tool_calls[0]['name']}")
        elif isinstance(message, ToolMessage):
            print(f"工具结果:{message.content}")

    print(f"最终回答:{result['messages'][-1].content}")


# runtime 参数由框架注入,因此工具 Schema 中没有 user_id 和 user_name。
tool_schema = convert_to_openai_tool(get_current_user_info)
print(f"模型可见的工具参数:{tool_schema['function']['parameters']['properties']}")

run_for_user("user-001", "小明")
run_for_user("user-002", "小红")

执行:

python langgraph/p07_agent_runtime_context/02_invoke_context_agent.py

本次真实输出如下:

模型可见的工具参数:{}

当前上下文:user-001 / 小明
工具调用:get_current_user_info
工具结果:{"name": "小明", "age": 18, "city": "杭州"}
最终回答:小明今年18岁,来自杭州。

当前上下文:user-002 / 小红
工具调用:get_current_user_info
工具结果:{"name": "小红", "age": 20, "city": "成都"}
最终回答:当前用户的年龄是20岁,所在城市是成都。

这段结果验证了三件事:

  1. 模型看到的工具参数是空字典,runtime 和用户身份没有暴露给模型。
  2. 两次运行都调用了同一个 get_current_user_info 工具。
  3. 两位用户分别得到自己的资料,没有发生上下文串用。

模型回答的具体措辞可能变化,但工具名称、工具返回的数据和用户隔离结果应该保持一致。

8. 通过 Agent API 传入 Context

本地直接调用使用 graph.invoke(…, context=…)。Agent 发布为服务后,LangGraph SDK 同样提供独立的 context 参数。

8.1 注册 context_agent

langgraph.json 内容如下:

{
  "dependencies": ["."],
  "graphs": {
    "context_agent": "./src/context_agent/graph.py:graph"
  },
  "python_version": "3.12"
}

进入 p07 项目并启动 Agent Server:

cd source/_posts/llm_learning/langgraph/p07_agent_runtime_context

../../.venv_langgraph/bin/langgraph dev \
  --host 127.0.0.1 \
  --port 2024 \
  --no-browser

检查健康接口:

curl --noproxy '*' http://127.0.0.1:2024/ok

真实结果:

{"ok":true}

8.2 使用 SDK 调用

langgraph/p07_agent_runtime_context/03_call_context_agent_api.py 的完整代码如下:

"""使用 LangGraph Python SDK 向 Agent Server 传入运行时上下文。"""

from langgraph_sdk import get_sync_client


def call_agent(user_id: str, user_name: str) -> None:
    """通过 Agent API 查询指定上下文对应的用户资料。"""

    client = get_sync_client(url="http://127.0.0.1:2024")
    tool_result = ""
    final_answer = ""

    for chunk in client.runs.stream(
        None,  # 使用 Threadless Run,本例不保存聊天历史。
        "context_agent",  # 对应 langgraph.json 中注册的 Graph 名称。
        input={
            "messages": [
                {"role": "user", "content": "告诉我当前用户的年龄和城市。"},
            ]
        },
        context={"user_id": user_id, "user_name": user_name},
        stream_mode="messages-tuple",
    ):
        if chunk.event != "messages":
            continue

        # messages-tuple 第一项是消息块,第二项是执行元数据。
        message, metadata = chunk.data
        if message.get("type") == "tool":
            tool_result = message.get("content", "")
        elif message.get("type") == "AIMessageChunk" and tool_result:
            final_answer += message.get("content", "")

    print(f"上下文:{user_id} / {user_name}")
    print(f"工具结果:{tool_result}")
    print(f"最终回答:{final_answer}")


call_agent("user-001", "小明")
call_agent("user-002", "小红")

从 llm_learning 根目录执行:

.venv_langgraph/bin/python \
  langgraph/p07_agent_runtime_context/03_call_context_agent_api.py

真实结果如下:

上下文:user-001 / 小明
工具结果:{"name": "小明", "age": 18, "city": "杭州"}
最终回答:小明今年18岁,来自杭州。
上下文:user-002 / 小红
工具结果:{"name": "小红", "age": 20, "city": "成都"}
最终回答:当前用户的年龄是20岁,所在城市是成都。

SDK 中的 context 与 config 是两个独立参数:

client.runs.stream(
    thread_id,
    assistant_id,
    input={...},
    config={...},
    context={...},
)

如果后面同时使用线程持久化,可以把 thread_id 交给 Agent Server,同时继续通过 context 传递本次运行的业务身份。不要把 Context 当作聊天记录保存方式。

9. Context 的生命周期和隔离范围

本例连续执行了两次相同问题:

第一次:user-001 / 小明
第二次:user-002 / 小红

Graph 和 Tool 没有变化,只有调用时传入的 Context 不同。每次运行都会创建自己的 Runtime,因此第二次运行不会继续使用第一次的 user_id。

Context 也不会因为 Agent Server 使用相同 Graph 就自动保存。下一次运行仍然必须重新传入。需要保存聊天历史时应使用 Thread 和 Checkpointer;需要保存跨会话资料时应使用 Store 或业务数据库。这些属于后续章节。

10. 常见问题

10.1 把 context 传成了 config

下面两种调用并不等价:

graph.invoke(input, config={"configurable": {"user_id": "user-001"}})
graph.invoke(input, context={"user_id": "user-001", "user_name": "小明"})

本文的 Graph 使用 context_schema=UserContext,动态提示词读取的是 runtime.context,因此应该使用第二种方式。

10.2 忘记传入 Context

示例主动检查了必要字段。缺少 Context 时,真实错误为:

ValueError
必须通过 context 传入 user_name

相比直接出现 ‘NoneType’ object is not subscriptable,这种错误更容易定位。

10.3 TypedDict 为什么没有自动拦截字段缺失

TypedDict 主要用于静态类型提示。它不会像 Pydantic 模型一样自动完成所有运行时校验,所以关键字段仍然需要在应用入口检查。

10.4 Tool Schema 中看不到 runtime

这是正常现象。ToolRuntime 属于框架注入参数,不应该由模型生成。本例打印出的模型可见参数为:

{}

10.5 动态提示词没有生效

使用 @dynamic_prompt 后,需要把它加入 Agent 的 middleware:

graph = create_agent(
    model=model,
    tools=[get_current_user_info],
    middleware=[personalized_prompt],
    context_schema=UserContext,
)

只定义函数但不加入 middleware,Agent 不会执行它。

10.6 本地 langchain 目录遮蔽了安装包

学习项目根目录中本身存在一个名为 langchain/ 的代码目录。在根目录使用下面这种标准输入方式时:

python - <<'PY'
import langchain
PY

Python 可能优先把本地目录识别为 langchain 命名空间,导致:

ModuleNotFoundError: No module named 'langchain.agents'

本文提供的脚本可以直接按文中的路径执行。需要临时运行 Python 片段时,可以进入 p07_agent_runtime_context 目录,或者避免让包含同名目录的路径位于 sys.path 首位。

10.7 连接本地服务失败

先分别检查两个接口:

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

18080 是 Qwen3 模型服务,2024 是 LangGraph Agent Server。代码中的 httpx.Client(trust_env=False) 用于避免系统代理或 VPN 转发本机请求。

11. 小结

课程中的 config[“configurable”] 展示了配置如何跟随一次 Runnable 或 Agent 调用传播。它仍然是有效的运行配置入口,尤其是 thread_id、标签、元数据和回调等执行信息。

对于用户身份、租户、权限和业务依赖,langchain==1.2.13 提供的 Runtime Context 边界更加清楚。使用 context_schema 声明结构,通过 context= 传入,再从 request.runtime.context 或 ToolRuntime.context 读取。

本次实测中,同一个 Agent 分别读取了小明和小红的 Context,正确调用本地工具并返回各自资料。用户 ID 没有出现在模型可见的 Tool Schema 中,两次运行也没有互相覆盖。

对象 传入方式 主要用途 读取方式 是否在运行中变化
用户输入 input={“messages”: […]} 用户问题和对话消息 state[“messages”] 会随着对话和工具调用增加
RunnableConfig config={…} 线程、标签、元数据、回调和执行限制 函数的 config 或 runtime.config 应由框架和调用方管理
Runtime Context context={…} 用户 ID、用户名、租户、权限和依赖 runtime.context 本次运行期间按只读方式使用
AgentState Graph 内部维护 消息和执行过程中产生的动态数据 State 参数或 runtime.state 可以被节点和 Tool 更新,后续专门介绍

参考文档:LangChain RuntimeContext overviewLangChain ToolsLangGraph Graph API

下一篇将进入 AgentState,观察消息状态如何随 Agent 执行过程变化,并使用 Command(update=…) 在 Tool 中更新自定义状态。


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