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

发布时间:2026/8/20 9:48:49
上下文工程:LangChain 怎么组织「喂给模型的东西」 本文读的是 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 callmiddleware 就是挂在这个循环各个位置上的钩子。文档主要用到两个但整套钩子值得先列全因为它们决定了「你想改的东西该在哪一层改」钩子触发时机典型用途dynamic_prompt每次 model call 前计算系统提示词按状态/身份改写 system promptwrap_model_call包裹整个 model call瞬态改 messages / tools / model / response_formatbefore_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_agentdataclassclassContext:user_id:strapi_key:strdb_connection:strtooldeffetch_user_data(query:str,runtime:ToolRuntime[Context])-str:用运行配置去查数据。user_idruntime.context.user_id api_keyruntime.context.api_key db_connectionruntime.context.db_connection resultsperform_database_query(db_connection,query,api_key)returnfFound{len(results)}results for user{user_id}agentcreate_agent(modelgpt-5.5,tools[fetch_user_data],context_schemaContext)resultagent.invoke({messages:[{role:user,content:Get my data}]},contextContext(user_iduser_123,api_keysk-...,db_connectionpostgresql://...),)注意ToolRuntime[Context]这个泛型参数——它让runtime.context带上类型IDE 能补全、类型检查能报错。这是把「凭证、连接」这类东西从提示词里赶出去的正确姿势它们不该出现在给模型看的文本里而应该走 Runtime Context只有工具能碰到。State —— 会话级的可变状态当前这轮会话中会变化的数据消息历史、上传的文件、认证标志、工具产出的中间结果。它的生命周期是单个会话在 LangGraph 里对应一个 thread配了 checkpointer 就能随线程持久化、断点续跑但不跨会话。State 本质是一个带reducer的字典。最常见的 reducer 就是messages那条——新消息是追加而不是覆盖所以循环里每一轮的消息会累积起来。读用runtime.state工具里或request.statemiddleware 里写不能直接改字典而要让工具返回一个Command由框架合并进 Statefromlangchain.toolsimporttool,ToolRuntimefromlangchain.agentsimportcreate_agentfromlanggraph.typesimportCommandtooldefauthenticate_user(password:str,runtime:ToolRuntime)-Command:认证用户并把结果写回 State。ifpasswordcorrect:returnCommand(update{authenticated:True})returnCommand(update{authenticated:False})agentcreate_agent(modelgpt-5.5,tools[authenticate_user])为什么写 State 要绕一层Command、而不是直接赋值因为状态更新要走 reducer 合并、要能被 checkpointer 记录、要在并行分支下可预测。Command(update...)是把「我想改什么」声明出来交给框架而不是就地改一个共享字典——这跟 Redux 里 dispatch 一个 action 是同一个道理。Store —— 跨会话的长期存储跨会话持久的数据用户偏好、写作风格、历史洞察、feature flag。它是一个 KV 存储按(namespace,)元组 key 组织get/put读写通过storeInMemoryStore()生产上换成持久实现挂到 Agent 上fromlangchain.toolsimporttool,ToolRuntimefromlangchain.agentsimportcreate_agentfromlanggraph.store.memoryimportInMemoryStoretooldefsave_preference(preference_key:str,preference_value:str,runtime:ToolRuntime[Context])-str:把用户偏好写进 Store。user_idruntime.context.user_id storeruntime.store existingstore.get((preferences,),user_id)prefsexisting.valueifexistingelse{}prefs[preference_key]preference_value store.put((preferences,),user_id,prefs)returnfSaved 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,ModelRequestdynamic_promptdefcontext_aware_prompt(request:ModelRequest)-str:rolerequest.runtime.context.user_role envrequest.runtime.context.deployment_env baseYou are a helpful assistant.ifroleadmin:base\nYou have admin access. You can perform all operations.elifroleviewer:base\nYou have read-only access.ifenvproduction:base\nBe extra careful with any data modifications.returnbase其余四样都走wrap_model_callrequest.override(...)。override返回一个改过的请求副本只对这一次handler(request)生效。下面是三个有代表性的例子。按对话长度换模型成本/质量权衡下沉到运行时fromlangchain.agents.middlewareimportwrap_model_call,ModelRequest,ModelResponsefromlangchain.chat_modelsimportinit_chat_model large_modelinit_chat_model(claude-sonnet-4-6)standard_modelinit_chat_model(gpt-5.5)efficient_modelinit_chat_model(gpt-5.4-mini)wrap_model_calldefstate_based_model(request:ModelRequest,handler)-ModelResponse:nlen(request.messages)modellarge_modelifn20elsestandard_modelifn10elseefficient_modelreturnhandler(request.override(modelmodel))按角色裁剪工具面权限收敛在这里而不是靠提示词求模型别乱调wrap_model_calldefcontext_based_tools(request:ModelRequest,handler)-ModelResponse:rolerequest.runtime.context.user_roleifroleeditor:tools[tfortinrequest.toolsift.name!delete_data]requestrequest.override(toolstools)elifrolenotin(admin,editor):tools[tfortinrequest.toolsift.name.startswith(read_)]requestrequest.override(toolstools)returnhandler(request)按会话阶段切换返回格式前几轮要简后面要带推理和置信度frompydanticimportBaseModel,FieldclassSimpleResponse(BaseModel):answer:strField(descriptionA brief answer)classDetailedResponse(BaseModel):answer:strField(descriptionA detailed answer)reasoning:strField(descriptionExplanation of reasoning)confidence:floatField(descriptionConfidence score 0-1)wrap_model_calldefstate_based_output(request:ModelRequest,handler)-ModelResponse:fmtSimpleResponseiflen(request.messages)3elseDetailedResponsereturnhandler(request.override(response_formatfmt))这里有一个必须分清的机制点request.override(...)是瞬态的Command(update...)是持久的。前者只改「这一次递给模型的请求」不落进 State下一轮循环从原始状态重新计算后者是真的把 State 改了之后每一轮都看得到。用 override 往 messages 里塞一段临时上下文和把它 append 进 State行为完全不同——前者不会污染历史后者会。这两者混淆是很难查的一类 bug。顺带看一个 override 改 messages 的例子它同时展示了「从 State 取数据」wrap_model_calldefinject_file_context(request:ModelRequest,handler)-ModelResponse:把本会话上传过的文件信息临时拼进这一次调用。uploadedrequest.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}}]requestrequest.override(messagesmessages)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 内置了SummarizationMiddlewarefromlangchain.agents.middlewareimportSummarizationMiddleware agentcreate_agent(modelgpt-5.5,tools[...],middleware[SummarizationMiddleware(modelgpt-5.4-mini,# 用便宜模型做摘要trigger{tokens:4000},# 超过 4000 token 触发keep(messages,20),# 保留最近 20 条其余压成摘要),],)它做的事监控 State 里的消息一旦 token 超过阈值就用一个通常更便宜的模型把较早的消息总结掉、替换进 State把上下文窗口腾出来。这是「正确的信息也包括别塞太多」在框架层的自动化而且它改的是 State持久所以压缩效果对后续每一轮都生效——这跟前面 override 那种瞬态修改是两回事。五、把两个维度合起来回到最初那张 A×B 的表。文档里每个片段都能定位成「在某个环节用某个数据源」Model ContextTool ContextLife-cycleRuntime 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 EngineeringLangChainAI AgentLLM 工程