LangChain 的上下文装配链路
以工单 Agent 为例,沿着 State、ModelRequest、中间件和模型适配器,追踪一次模型 HTTP 请求的输入如何形成。
工单 Agent 保存了一份完整工单,聊天记录里却只有工具返回的简短摘要。员工接着说「把处理时间改到明天」,模型从哪里拿到当前工单?
答案藏在每次模型调用前的上下文装配里。图状态提供数据,中间件选择和加工内容,模型适配器再把消息与工具定义转换成供应商请求体。沿着这条链路看,动态 Prompt、历史裁剪和工具绑定就能放在同一张图里理解。
下面以 WorkOrderAgent 的工单流程为例,追踪 LangChain 到 DeepSeek OpenAI 兼容 Chat Completions 接口的调用路径。源码版本为 LangChain 1.4.6、LangGraph 1.2.15、langchain-core 1.6.11 和 langchain-deepseek 1.1.1。
从图状态到模型输入
一次模型调用大致经过这些环节:
新输入 + thread_id + 后端运行上下文
↓ 恢复 Checkpoint,合并输入
当前图状态
↓ 模型节点读取消息和注册配置
ModelRequest
↓ 中间件生成动态 Prompt、裁剪历史、筛选工具
加工后的请求
↓ 工具绑定,组合系统消息与历史
模型适配器输入
↓ 序列化消息、工具 Schema 和调用参数
供应商请求体
↓ SDK 发出 HTTP 请求
模型响应
↓ 回填消息,执行工具,更新状态
下一次模型调用,或结束、暂停
这里有三个各司其职的对象:
| 对象 | 保存或组织什么 | 谁使用它 |
|---|---|---|
| 当前图状态 | 消息、当前工单、计数等执行数据 | 图节点、工具和中间件 |
ModelRequest |
本次消息、系统消息、候选工具,以及可读取的状态和运行上下文 | 模型调用中间件 |
| 供应商请求体 | model、messages、tools、采样和输出配置等协议字段 |
模型 API |
Checkpoint 保存图的执行快照。框架恢复快照、合并新输入,再按节点更新当前状态;ModelRequest 则在每次进入模型节点时重新创建。同一轮 Agent 执行调用模型三次,就会经历三次请求装配。
创建阶段定义结构,运行阶段提供数据
先看 Agent 的创建配置:
agent = create_agent(
model=model,
tools=TOOLS,
state_schema=WorkOrderState,
context_schema=AssistantContext,
checkpointer=checkpointer,
middleware=[...],
)
state_schema 描述图状态的形状,context_schema 描述运行上下文的形状。模型、工具、中间件和 Checkpointer 一起确定执行结构。
员工的新消息在调用时传入。下面保留核心参数,省略流事件消费:
agent.astream(
{"messages": [HumanMessage(content="创建一张检查草稿")]},
config={"configurable": {"thread_id": "员工ID:会话ID"}},
context=backend_context,
stream_mode=["messages", "updates"],
version="v2",
)
thread_id 把本次执行关联到已有会话,Checkpointer 与执行框架恢复历史,新消息再按状态合并规则进入 messages。后端通过 context 提供身份、会话标识和业务依赖等运行信息。
工具定义来自创建时的注册配置。后续装配每次模型请求时,框架再把本次可用工具交给适配器。
ModelRequest 把数据交给中间件
LangChain 的异步模型节点 amodel_node 中,核心代码如下,已省略其他配置字段:
messages = _patch_invalid_tool_calls(state["messages"])
request = ModelRequest(
model=model,
tools=default_tools,
system_message=system_message,
messages=messages,
state=state,
runtime=runtime,
)
messages 来自当前图状态,default_tools 来自工具注册。中间件同时拿到 state 和 runtime,就能根据当前工单、员工身份或会话阶段调整本次请求。LangChain 的上下文工程文档也是围绕这些来源组织动态消息、工具和模型选择的。
| 字段 | 中间件可以做什么 | 交给适配器的内容 |
|---|---|---|
system_message |
生成或修改系统消息 | 系统消息内容 |
messages |
选择、裁剪和编辑历史 | 本次消息序列 |
tools |
选择本次可用工具 | 转换后的工具 Schema |
state |
读取图状态,选择业务信息 | 显式写入消息等输入字段的部分 |
runtime |
读取后端运行上下文 | 显式写入消息等输入字段的部分 |
model、model_settings |
选择模型和调用配置 | 模型名称和适配后的调用参数 |
这也给出了判断模型可见内容的方法:沿着字段的使用路径往后追。员工身份存在 runtime 中时,先由程序使用;中间件把其中的姓名写进系统消息后,姓名才进入模型输入。
修改本次请求通常使用 request.override(...)。例如:
return await handler(request.override(messages=trimmed_messages))
这会把裁剪后的历史交给后续处理。Checkpoint 中的历史由图状态更新路径维护,本次请求的裁剪可以和持久化历史分开处理。自定义中间件提供了动态 Prompt、消息加工和工具选择等扩展点。
工单怎样从状态进入系统消息
WorkOrderAgent 的写入工具先调用业务 Service 保存工单,再回填图状态。下面是简化后的结构:
return Command(
update={
"current_order": saved_order,
"messages": [
ToolMessage(
content=json.dumps({"ok": True, "order": summary}),
tool_call_id=runtime.tool_call_id,
)
],
}
)
这里同时保留两份用途不同的信息。current_order 保存完整工单快照;ToolMessage 保存本次调用结果的摘要,包括工单 ID、编号、状态和版本等字段。
下一次进入模型节点后,动态 Prompt 中间件读取完整快照:
@dynamic_prompt
def work_order_prompt(request):
now = datetime.now(ZoneInfo("Asia/Shanghai")).isoformat(timespec="minutes")
saved = request.state.get("current_order")
return (
SYSTEM_PROMPT
+ f"\n当前北京时间:{now}"
+ f"\n当前工单:{json.dumps(saved, ensure_ascii=False)}"
)
固定规则、当前时间和完整工单被组合成系统消息。随后消息窗口中间件处理 request.messages,裁剪本次需要的历史。于是,各类内容的去向就清楚了:
| 内容职责 | 工单 Agent 中的承载位置 |
|---|---|
| 固定业务指令 | system_message 的规则部分 |
| 当前工单与时间 | 同一条 system_message 的动态部分 |
| 历史对话与工具观察 | messages |
| 可调用能力及参数说明 | tools,随后转换成供应商工具定义 |
系统消息回答「当前业务对象是什么」,历史工具消息记录「刚才做了什么」。这样,每次更新工单都只向历史追加摘要,下一次调用再从状态中投影完整对象。即使较早的聊天被裁剪,当前工单仍然可以进入模型输入。
这份工单是最近一次可信读取或写入后的快照。外部系统修改工单后,后续业务操作仍需按版本检查并发冲突。
工具参数为什么能跟着 ainvoke 发出去
中间件加工完成后,factory.py 的 _execute_model_async 会执行:
model_, effective_response_format = _get_bound_model(request)
messages = request.messages
if request.system_message:
messages = [request.system_message, *messages]
output = await model_.ainvoke(messages)
最后一行只传了 messages,工具参数却已经在前面的 _get_bound_model(request) 中绑定好了。
普通工具路径会调用 request.model.bind_tools(final_tools, ...)。ChatDeepSeek 默认沿用父类实现,通过 convert_to_openai_tool 将工具转成协议定义,再绑定到模型调用参数。RunnableBinding.ainvoke 调用底层模型时会合并这些参数,所以工具 Schema 会和消息一起进入适配层。
bind_tools 返回的是带有调用参数的模型包装对象。工具名称、描述和参数 Schema 交给模型,函数实现留在应用侧,等模型返回 Tool Call 后再由工具执行层调用。
系统消息也在这里与历史组合:
request.system_message + request.messages
↓
本次 ainvoke 的消息序列
在这条实现路径中,系统消息按次生成、按次拼接,持久化历史继续由 state.messages 管理。
Provider 适配器生成 HTTP 请求体
沿着 ChatDeepSeek 的 OpenAI 兼容 Chat Completions 路径继续往下,消息对象最终会变成字典。ChatOpenAI 的 _get_request_payload 在这一分支的主要工作,可以缩成下面的逻辑:
payload = {**self._default_params, **kwargs}
payload["messages"] = [
_convert_message_to_dict(message)
for message in messages
]
绑定的工具参数通过 kwargs 进入 tools。ChatDeepSeek 再对部分消息格式进行适配,SDK 根据载荷发送 HTTP 请求。非流式路径可以追踪到 async_client.with_raw_response.create(**payload);流式路径同样先完成载荷装配,再按流式方式获取响应。
下面用占位工单展示请求体的形状,历史消息和工具列表已缩减:
{
"model": "deepseek-flash",
"messages": [
{
"role": "system",
"content": "固定业务规则;当前时间;当前工单 WO-DEMO,版本 4"
},
{
"role": "user",
"content": "把处理时间改到明天"
}
],
"tools": [
{
"type": "function",
"function": {
"name": "find_equipment",
"description": "查询设备",
"parameters": {
"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"]
}
}
}
],
"temperature": 0
}
messages 和 tools 是请求体中的同级字段,共同组成模型完成任务所需的输入。系统提示词描述如何使用工具,tools 则提供可调用能力的名称、描述和参数结构。两部分各有用途。
Provider 适配器负责把 LangChain 对象转成对应接口的字段,SDK 负责将这些字段连同鉴权等传输信息组织成 HTTP 请求。追踪到这里,就能具体回答「本次模型输入里有哪些内容」。
用这条链路排查上下文问题
如果模型遗漏了当前工单,可以先看状态里有没有快照,再看动态 Prompt 是否读取了它,最后检查适配器输入中的系统消息。状态、请求加工和协议转换各有明确位置,定位起来比只看聊天记录更直接。
历史裁剪也可以分两处检查:request.override(messages=...) 控制本次输入;图节点或工具返回的状态更新控制后续保存的历史。排查「消息怎么没了」时,需要先明确丢的是哪一份。
长上下文还要核对预算范围。WorkOrderAgent 当前的窗口策略先扣除系统消息的估算 Token,再裁剪历史,工具 Schema 的开销需要另行计入完整输入预算。工单对象变大、工具数量增加时,这个差别会直接影响实际请求长度。
要复现一次旧请求,还需要保留当时的系统规则、工具定义、模型配置和相关外部数据。Checkpoint 能恢复执行状态,恢复后的模型请求会按当前代码、时间和配置重新装配。
最后还可以在中间件与 Provider 之间观察调用是否继续。中间件能够直接返回 ModelResponse,例如输入超过业务预算时提前结束本次处理;走到模型节点和实际发送 HTTP 请求之间,还隔着这条控制路径。
源码阅读入口
顺着下面的次序读,能把业务投影和框架装配连起来:
| 层次 | 文件或符号 | 重点看什么 |
|---|---|---|
| 应用入口 | 项目的 agent.py、service.py |
create_agent 配置、运行输入、context 与 thread_id |
| 工单回填 | 项目的 tools.py |
Command(update=...) 中完整状态与结果摘要的分工 |
| 请求对象 | langchain/agents/middleware/types.py |
ModelRequest 字段与 override |
| Agent 执行 | langchain/agents/factory.py |
amodel_node、_get_bound_model、_execute_model_async |
| 参数绑定 | langchain_core/runnables/base.py |
RunnableBinding.ainvoke 怎样合并参数 |
| 模型适配 | langchain_deepseek/chat_models.py |
工具绑定和消息格式处理 |
| 请求载荷 | langchain_openai/chat_models/base.py |
工具 Schema 转换、_get_request_payload 与 SDK 调用 |