← 返回AI变现
🌐 其他

上下文工程:LangChain 怎么组织「喂给模型的东西」

来源:掘金 · 发布于 2026-08-19 00:14:29
定义与边界 LangChain 给上下文工程下的定义是: 这个定义本身平淡,但它划出的边界值得注意:它不谈模型能力,只谈「调用模型之前,你准备了什么」。模型是固定的黑盒,你能动的只有输入——系统提示词

上下文工程:LangChain 怎么组织「喂给模型的东西」

liuxiaocheng 2026-08-18 0 阅读11分钟

本文读的是 LangChain v1 官方文档的 Context Engineering 一页。这页文档本身写得比较散——十几个代码片段平铺过去。我想把它重新组织成一个更清楚的结构:两个正交的维度,再把每个维度对齐到具体的 API。文中代码基本沿用官方示例,模型名(gpt-5.5、claude-sonnet-4-6 等)也保持原样。


定义与边界

LangChain 给上下文工程下的定义是:

用正确的格式,把正确的信息和工具提供给 LLM,让它能完成任务。

这个定义本身平淡,但它划出的边界值得注意:它不谈模型能力,只谈「调用模型之前,你准备了什么」。模型是固定的黑盒,你能动的只有输入——系统提示词、消息历史、可用工具、返回格式,以及这些东西背后的数据从哪来。上下文工程就是把这部分工程化。

在 LangChain v1 里,这件事几乎完全落在 middleware 和 tool 的运行时接口上。所以这篇会先讲清楚 Agent 的执行循环、middleware 挂在哪,再展开两个维度。


一、Agent 循环与 middleware 的挂载点

create_agent 构造出来的 Agent,运行时是一个两步循环:

   ┌─────────────────────────────────────────────┐
   │  model call:带 prompt + tools 调一次 LLM     │
   └───────────────────┬─────────────────────────┘
                       │ 模型要求调工具?
              ┌────────┴────────┐
             是                 否 → 结束,返回结果
              │
   ┌──────────▼──────────────────────────────────┐
   │  tool execution:执行工具,结果作为消息回填    │
   └───────────────────┬─────────────────────────┘
                       └──────→ 回到 model call

middleware 就是挂在这个循环各个位置上的钩子。文档主要用到两个,但整套钩子值得先列全,因为它们决定了「你想改的东西该在哪一层改」:

钩子触发时机典型用途
@dynamic_prompt每次 model call 前,计算系统提示词按状态/身份改写 system prompt
@wrap_model_call包裹整个 model call瞬态改 messages / tools / model / response_format
before_model / after_modelmodel call 前后记日志、改状态、条件跳转
@wrap_tool_call包裹单次工具执行拦截工具输入输出、加护栏

wrap_* 是包裹语义:它拿到一个 handler,自己决定改完请求再调用 handler(request),还能对返回值二次加工。这一点后面会反复用到。


二、两个正交的维度

文档把可控的东西分成三类上下文,又分出三个数据来源。这两组东西其实是正交的两个维度,分开看更清楚:

  • 维度 A:你在控制循环的哪个环节? —— Model Context / Tool Context / Life-cycle Context
  • 维度 B:这份数据活多久、谁写? —— Runtime Context / State / Store

任何一个环节,都可以从任意一个数据源取数。比如「动态改系统提示词」是维度 A 里的 Model Context,它的输入既可能来自 State(对话多长了),也可能来自 Store(用户偏好),还可能来自 Runtime Context(用户角色)。文档里那十几个片段,本质就是 A×B 的组合列举。理解了两个维度各自是什么,这些片段就不用一个个背了。

先讲维度 B(数据源),因为它是维度 A 的输入。


三、维度 B:三个数据源

Runtime Context —— 不可变的运行配置

一次 invoke 期间固定不变的配置:用户 ID、API key、数据库连接、角色、部署环境。它由调用方在启动时传入,Agent 运行过程中不会改写它。

用法是三步:dataclass 定 schema → create_agent(context_schema=...) → invoke(context=...)。工具和 middleware 通过 runtime.context 读:

from dataclasses import dataclass
from langchain.tools import tool, ToolRuntime
from langchain.agents import create_agent

@dataclass
class Context:
    user_id: str
    api_key: str
    db_connection: str

@tool
def fetch_user_data(query: str, runtime: ToolRuntime[Context]) -> str:
    """用运行配置去查数据。"""
    user_id = runtime.context.user_id
    api_key = runtime.context.api_key
    db_connection = runtime.context.db_connection
    results = perform_database_query(db_connection, query, api_key)
    return f"Found {len(results)} results for user {user_id}"

agent = create_agent(model="gpt-5.5", tools=[fetch_user_data], context_schema=Context)

result = agent.invoke(
    {"messages": [{"role": "user", "content": "Get my data"}]},
    context=Context(user_id="user_123", api_key="sk-...", db_connection="postgresql://..."),
)

注意 ToolRuntime[Context] 这个泛型参数——它让 runtime.context 带上类型,IDE 能补全、类型检查能报错。这是把「凭证、连接」这类东西从提示词里赶出去的正确姿势:它们不该出现在给模型看的文本里,而应该走 Runtime Context,只有工具能碰到。

State —— 会话级的可变状态

当前这轮会话中会变化的数据:消息历史、上传的文件、认证标志、工具产出的中间结果。它的生命周期是单个会话(在 LangGraph 里对应一个 thread),配了 checkpointer 就能随线程持久化、断点续跑,但不跨会话。

State 本质是一个带 reducer 的字典。最常见的 reducer 就是 messages 那条——新消息是追加而不是覆盖,所以循环里每一轮的消息会累积起来。读用 runtime.state(工具里)或 request.state(middleware 里);写不能直接改字典,而要让工具返回一个 Command,由框架合并进 State:

from langchain.tools import tool, ToolRuntime
from langchain.agents import create_agent
from langgraph.types import Command

@tool
def authenticate_user(password: str, runtime: ToolRuntime) -> Command:
    """认证用户,并把结果写回 State。"""
    if password == "correct":
        return Command(update={"authenticated": True})
    return Command(update={"authenticated": False})

agent = create_agent(model="gpt-5.5", tools=[authenticate_user])

为什么写 State 要绕一层 Command、而不是直接赋值?因为状态更新要走 reducer 合并、要能被 checkpointer 记录、要在并行分支下可预测。Command(update=...) 是把「我想改什么」声明出来交给框架,而不是就地改一个共享字典——这跟 Redux 里 dispatch 一个 action 是同一个道理。

Store —— 跨会话的长期存储

跨会话持久的数据:用户偏好、写作风格、历史洞察、feature flag。它是一个 KV 存储,按 (namespace,) 元组 + key 组织,get / put 读写,通过 store=InMemoryStore()(生产上换成持久实现)挂到 Agent 上:

from langchain.tools import tool, ToolRuntime
from langchain.agents import create_agent
from langgraph.store.memory import InMemoryStore

@tool
def save_preference(preference_key: str, preference_value: str,
                    runtime: ToolRuntime[Context]) -> str:
    """把用户偏好写进 Store。"""
    user_id = runtime.context.user_id
    store = runtime.store
    existing = store.get(("preferences",), user_id)
    prefs = existing.value if existing else {}
    prefs[preference_key] = preference_value
    store.put(("preferences",), user_id, prefs)
    return f"Saved preference: {preference_key} = {preference_value}"

store.get 返回的不是裸值,而是一个带 .value 的条目(还带版本、时间戳等元数据),所以读的时候是 existing.value。namespace 用元组是为了做多租户隔离——("preferences",) 配上 user_id 这个 key,天然按用户分区。

三者对照

Runtime ContextStateStore
生命周期单次 invoke,不变单个会话(thread),可变跨会话,持久
写入方调用方在 invoke(context=) 传入工具返回 Command(update=…)显式 store.put(...)
读取入口runtime.contextruntime.state / request.stateruntime.store
是否类型化是(dataclass schema)弱(dict + reducer)否(KV)
放什么凭证、连接、角色、环境消息、文件、认证标志偏好、历史、feature flag

一条判断规则:这份数据在一次调用里会变吗?跨会话还要吗? 不变且单次用完 → Runtime Context;会变、但会话结束就没意义 → State;要跨会话记住 → Store。


四、维度 A:控制循环的哪个环节

Model Context:动态构造这一次调用的输入

这是最主要的一类,控制的是每次 model call 喂进去的五样东西:system prompt、messages、tools、model、response_format。它们都可以在 middleware 里按数据源动态决定。

系统提示词用 @dynamic_prompt,返回一个字符串:

from langchain.agents.middleware import dynamic_prompt, ModelRequest

@dynamic_prompt
def context_aware_prompt(request: ModelRequest) -> str:
    role = request.runtime.context.user_role
    env = request.runtime.context.deployment_env
    base = "You are a helpful assistant."
    if role == "admin":
        base += "\nYou have admin access. You can perform all operations."
    elif role == "viewer":
        base += "\nYou have read-only access."
    if env == "production":
        base += "\nBe extra careful with any data modifications."
    return base

其余四样都走 @wrap_model_call + request.override(...)。override 返回一个改过的请求副本,只对这一次 handler(request) 生效。下面是三个有代表性的例子。

按对话长度换模型(成本/质量权衡下沉到运行时):

from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
from langchain.chat_models import init_chat_model

large_model = init_chat_model("claude-sonnet-4-6")
standard_model = init_chat_model("gpt-5.5")
efficient_model = init_chat_model("gpt-5.4-mini")

@wrap_model_call
def state_based_model(request: ModelRequest, handler) -> ModelResponse:
    n = len(request.messages)
    model = large_model if n > 20 else standard_model if n > 10 else efficient_model
    return handler(request.override(model=model))

按角色裁剪工具面(权限收敛在这里,而不是靠提示词求模型别乱调):

@wrap_model_call
def context_based_tools(request: ModelRequest, handler) -> ModelResponse:
    role = request.runtime.context.user_role
    if role == "editor":
        tools = [t for t in request.tools if t.name != "delete_data"]
        request = request.override(tools=tools)
    elif role not in ("admin", "editor"):
        tools = [t for t in request.tools if t.name.startswith("read_")]
        request = request.override(tools=tools)
    return handler(request)

按会话阶段切换返回格式(前几轮要简,后面要带推理和置信度):

from pydantic import BaseModel, Field

class SimpleResponse(BaseModel):
    answer: str = Field(description="A brief answer")

class DetailedResponse(BaseModel):
    answer: str = Field(description="A detailed answer")
    reasoning: str = Field(description="Explanation of reasoning")
    confidence: float = Field(description="Confidence score 0-1")

@wrap_model_call
def state_based_output(request: ModelRequest, handler) -> ModelResponse:
    fmt = SimpleResponse if len(request.messages) < 3 else DetailedResponse
    return handler(request.override(response_format=fmt))

这里有一个必须分清的机制点:request.override(...) 是瞬态的,Command(update=...) 是持久的。 前者只改「这一次递给模型的请求」,不落进 State,下一轮循环从原始状态重新计算;后者是真的把 State 改了,之后每一轮都看得到。用 override 往 messages 里塞一段临时上下文,和把它 append 进 State,行为完全不同——前者不会污染历史,后者会。这两者混淆是很难查的一类 bug。

顺带看一个 override 改 messages 的例子,它同时展示了「从 State 取数据」:

@wrap_model_call
def inject_file_context(request: ModelRequest, handler) -> ModelResponse:
    """把本会话上传过的文件信息,临时拼进这一次调用。"""
    uploaded = request.state.get("uploaded_files", [])
    if uploaded:
        desc = "\n".join(f"- {f['name']} ({f['type']}): {f['summary']}" for f in uploaded)
        messages = [*request.messages, {"role": "user", "content": f"可引用的文件:\n{desc}"}]
        request = request.override(messages=messages)
    return handler(request)

文件清单存在 State(会话级),但每次调用是瞬态注入给模型的——用完即弃,不会把这段说明永久钉进对话历史。这正是「State 存数据」和「override 用数据」的分工。

Tool Context:工具的读与写

工具是 Agent 真正对外产生副作用的地方。它两头都接着数据源:入参里声明一个 ToolRuntime,就能读 runtime.state / runtime.store / runtime.context;要写,就返回 Command(改 State)或调 store.put(改 Store)。上面 Runtime Context、State、Store 三节的代码其实已经把这些都演示过了,这里不重复。

要点是:工具能读能写持久状态,是 Agent 从「会对话」变成「会办事」的关键。一个只读提示词、不碰 State/Store 的工具,本质还是个函数调用;能读认证标志、能把偏好写回 Store 的工具,才让 Agent 具备了跨轮次、跨会话的记忆和状态机行为。

Life-cycle Context:步骤之间的动作

有些逻辑不属于某一次 model call,而是发生在循环的步骤之间——最典型的是上下文压缩。LangChain 内置了 SummarizationMiddleware:

from langchain.agents.middleware import SummarizationMiddleware

agent = create_agent(
    model="gpt-5.5",
    tools=[...],
    middleware=[
        SummarizationMiddleware(
            model="gpt-5.4-mini",       # 用便宜模型做摘要
            trigger={"tokens": 4000},   # 超过 4000 token 触发
            keep=("messages", 20),      # 保留最近 20 条,其余压成摘要
        ),
    ],
)

它做的事:监控 State 里的消息,一旦 token 超过阈值,就用一个(通常更便宜的)模型把较早的消息总结掉、替换进 State,把上下文窗口腾出来。这是「正确的信息也包括别塞太多」在框架层的自动化,而且它改的是 State(持久),所以压缩效果对后续每一轮都生效——这跟前面 override 那种瞬态修改是两回事。


五、把两个维度合起来

回到最初那张 A×B 的表。文档里每个片段,都能定位成「在某个环节,用某个数据源」:

Model ContextTool ContextLife-cycle
Runtime Context按角色改 prompt / 裁工具工具拿 api_key 查库——
State按对话长度换模型 / 换格式工具读认证标志超长时触发摘要
Store按偏好定 prompt / 模型工具存取用户偏好——

真正写代码时,你做的永远是同一件事:在循环的某个环节(Model / Tool / Life-cycle),从某个数据源(Context / State / Store)取出需要的数据,构造出这一次要喂给模型的输入。 剩下的都是这个句式的具体填空。


六、几个实践判断

文档结尾给的建议不多,结合上面的机制,有几条值得单独强调:

  • 先静态,再动态。 能写死的 prompt 和工具就先写死,确有分支需求了再加 middleware。动态逻辑越多,越难判断某一次调用到底喂了什么进去。
  • 凭证走 Runtime Context,不进提示词。 只有工具该碰 api_key、连接串,把它们放进给模型看的文本里既浪费 token 又有泄漏面。
  • 分清瞬态与持久。 request.override 只影响当次调用,Command(update=) 和 store.put 是持久写入。想清楚你改的东西该活多久,是避免一类隐蔽 bug 的前提。
  • 盯 token 与延迟。 动态注入越多,上下文越长、越贵、越慢;SummarizationMiddleware 是现成的止损手段,但它本身也要额外调一次模型,别无脑开。
  • 一次加一个 middleware 再测。 多个 wrap_model_call 是层层包裹的,叠在一起时执行顺序和相互覆盖不直观,逐个加进去好定位。

小结

LangChain 这页文档真正有价值的,不是那些 API 名字,而是它背后的组织方式:把「喂给模型的东西」拆成「控制哪个环节」和「数据活多久」两个正交维度。API 会变、会加,但这两个维度是稳定的思考框架——@dynamic_prompt、@wrap_model_call、request.override、ToolRuntime、Command、store.put,各自都能填进这张表里的某一格。看懂了格子,API 只是查一下的事。

标签:上下文工程 Context Engineering LangChain AI Agent LLM 工程