news 2026/8/20 9:48:17

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
上下文工程:LangChain 怎么组织「喂给模型的东西」

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


定义与边界

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

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

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

在 LangChain v1 里,这件事几乎完全落在middlewaretool 的运行时接口上。所以这篇会先讲清楚 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读:

fromdataclassesimportdataclassfromlangchain.toolsimporttool,ToolRuntimefromlangchain.agentsimportcreate_agent@dataclassclassContext:user_id:strapi_key:strdb_connection:str@tooldeffetch_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)returnf"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:

fromlangchain.toolsimporttool,ToolRuntimefromlangchain.agentsimportcreate_agentfromlanggraph.typesimportCommand@tooldefauthenticate_user(password:str,runtime:ToolRuntime)->Command:"""认证用户,并把结果写回 State。"""ifpassword=="correct":returnCommand(update={"authenticated":True})returnCommand(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 上:

fromlangchain.toolsimporttool,ToolRuntimefromlangchain.agentsimportcreate_agentfromlanggraph.store.memoryimportInMemoryStore@tooldefsave_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.valueifexistingelse{}prefs[preference_key]=preference_value store.put(("preferences",),user_id,prefs)returnf"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,返回一个字符串:

fromlangchain.agents.middlewareimportdynamic_prompt,ModelRequest@dynamic_promptdefcontext_aware_prompt(request:ModelRequest)->str:role=request.runtime.context.user_role env=request.runtime.context.deployment_env base="You are a helpful assistant."ifrole=="admin":base+="\nYou have admin access. You can perform all operations."elifrole=="viewer":base+="\nYou have read-only access."ifenv=="production":base+="\nBe extra careful with any data modifications."returnbase

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

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

fromlangchain.agents.middlewareimportwrap_model_call,ModelRequest,ModelResponsefromlangchain.chat_modelsimportinit_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_calldefstate_based_model(request:ModelRequest,handler)->ModelResponse:n=len(request.messages)model=large_modelifn>20elsestandard_modelifn>10elseefficient_modelreturnhandler(request.override(model=model))

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

@wrap_model_calldefcontext_based_tools(request:ModelRequest,handler)->ModelResponse:role=request.runtime.context.user_roleifrole=="editor":tools=[tfortinrequest.toolsift.name!="delete_data"]request=request.override(tools=tools)elifrolenotin("admin","editor"):tools=[tfortinrequest.toolsift.name.startswith("read_")]request=request.override(tools=tools)returnhandler(request)

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

frompydanticimportBaseModel,FieldclassSimpleResponse(BaseModel):answer:str=Field(description="A brief answer")classDetailedResponse(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_calldefstate_based_output(request:ModelRequest,handler)->ModelResponse:fmt=SimpleResponseiflen(request.messages)<3elseDetailedResponsereturnhandler(request.override(response_format=fmt))

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

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

@wrap_model_calldefinject_file_context(request:ModelRequest,handler)->ModelResponse:"""把本会话上传过的文件信息,临时拼进这一次调用。"""uploaded=request.state.get("uploaded_files",[])ifuploaded:desc="\n".join(f"-{f['name']}({f['type']}):{f['summary']}"forfinuploaded)messages=[*request.messages,{"role":"user","content":f"可引用的文件:\n{desc}"}]request=request.override(messages=messages)returnhandler(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

fromlangchain.agents.middlewareimportSummarizationMiddleware 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_callrequest.overrideToolRuntimeCommandstore.put,各自都能填进这张表里的某一格。看懂了格子,API 只是查一下的事。

标签上下文工程Context EngineeringLangChainAI AgentLLM 工程

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/20 9:47:02

Unity独立开发艺术展馆漫游:从零到一的高效实践指南

1. 先搞清楚“独立开发艺术展馆漫游”到底要做什么 如果你在考虑用Unity独立开发一个艺术展馆漫游项目&#xff0c;最该关心的不是Unity本身有多少功能&#xff0c;而是 如何用最低的成本和最快的速度&#xff0c;把一个能跑起来、能看、能交互的虚拟展馆做出来 。这通常意味…

作者头像 李华
网站建设 2026/8/20 9:45:49

主流 GEO 优化服务商测评报告,2026 国内 B2B 企业项目落地实操参考

B2B 采购决策越来越多发生在 AI 搜索场景,决策者借助大模型筛选供应商,GEO 优化对于 B2B 企业的获客价值持续放大。但大量 B2B 企业对 GEO 认知不足,缺少选型评判标准。这份测评报告依托六大评测维度完成主流头部 GEO 服务商能力拆解,结合 B2B 行业业务特征,为国内 B2B 企业项目…

作者头像 李华
网站建设 2026/8/20 9:45:23

天气丹化妆品礼盒OEM代工,别光盯着瓶子像不像,料体耐不耐打才是真章

拿着韩妆红瓶礼盒的图来问代工价的老板&#xff0c;十个里有八个上来就问能不能做“同款”。说实话&#xff0c;现在正规厂里没人跟你聊“同款”这两个字&#xff0c;那玩意儿容易踩线。我们聊的是同风格、同配方架构、自牌名定制——你把商标注册好&#xff0c;我把这套韩系高…

作者头像 李华
网站建设 2026/8/20 9:44:58

testtt

#test##test标签&#xff1a;#test 111

作者头像 李华
网站建设 2026/8/20 9:44:31

Python有哪些好用且实用的Web框架?

是一种具备无所不能特性的编程语言, 在爬虫、人工智能、游戏开发、自动化测试等领域有着出色应用, 还是Web开发常用的编程语言, 有着众多可直接拿来使用的Web框架, 能助力我们提升工作效率、节省不必要时间。那么其中有哪些好用的Web框架? 我们一同来瞧瞧。第一个&#xff1a;…

作者头像 李华
网站建设 2026/8/20 9:41:42

工程测量投影变形计算工具与专业投影方案

温馨提示&#xff1a;文末有联系方式一、轻量高效&#xff1a;工程测量投影变形计算软件 本款专用计算工具可快速完成测区内投影改正量与归化改正量的联合求解&#xff0c;自动输出总变形值&#xff0c;操作简洁、结果精准。 软件仅8元&#xff0c;后请务必在订单备注中留下有效…

作者头像 李华