在前面的文章中,我们已经理解了 Agent 中的 Message、Tool 以及长、短期记忆。

这些内容分别用于解决不同的问题,当 Agent 真正运行起来时,它们最终都会汇到同一个地方:模型的上下文。

模型这一轮应该看到哪些 Message?哪些 Tool Result 还需要保留?长期记忆要不要读取?知识库检索出的内容应该放多少?运行时信息又该以什么方式加入?

这些问题共同构成了上下文工程(Context Engineering)要解决的事情:根据当前任务,从不同的信息来源中选择、整理和补充内容,为每一次模型调用准备合适的上下文。

一、什么是上下文

对于大模型来说,上下文(Context)可以简单理解为:模型在生成当前这次回答之前,能够看到的全部信息。

例如用户发送:

代码片段Text
帮我分析一下这个接口为什么返回 401。

如果模型只看到这一句话,它几乎无法判断问题出在哪里。

但如果同时看到:

代码片段Text
实现该接口的代码
项目的认证配置
请求 Header
错误日志
之前的相关对话

它才有足够的信息进行分析。

这些内容共同构成了模型当前的上下文。

在普通 LLM 调用中,上下文通常比较简单,可能只有:

代码片段Text
System Prompt
+
用户输入

而在 Agent 中,情况就复杂一些。

一次模型调用看到的内容可能包括:

代码片段Text
System Prompt
+
历史 Message
+
当前用户请求
+
Tool 定义
+
Tool Result
+
短期记忆
+
长期记忆
+
RAG 检索结果
+
运行时信息

而且这些内容会随着 Agent 的运行不断变化。

所以,上下文并不是某一段 Prompt,也不是单指聊天记录。

只要某段信息最终进入了当前这次模型调用,它就是模型上下文的一部分。

理解了这一点,我们再来看什么是上下文工程。

二、上下文工程是什么

以前做大模型应用时,经常会讨论 Prompt Engineering。

它主要解决的问题是:

Prompt 应该怎么写,模型才能更准确地理解任务。

例如补充角色、目标、约束条件、Few-shot 示例以及输出格式。

对于简单的模型调用,这往往已经够用了。

但 Agent 不一样。

Agent 会持续对话、调用工具、读取记忆、检索知识库,还可能根据运行状态动态改变自己的行为。

所以真正需要考虑的问题已经从:

代码片段Text
Prompt 应该怎么写?

变成了:

代码片段Text
这一轮调用模型时,
到底应该给它哪些信息?

这就是上下文工程。

可以把它理解成:

根据当前任务,从各种信息来源中挑选、整理和补充内容,组成这一轮模型真正需要的上下文。

对比 Prompt Engineering 和 Context Engineering,它们的区别如下表:

维度Prompt EngineeringContext Engineering
主要对象Prompt整个模型输入
处理内容指令、示例、格式Prompt、Message、Tool、Memory、RAG 等
是否动态通常比较固定经常随运行过程变化
主要目标把任务说清楚把当前需要的信息准备好

需要说明的是,Prompt Engineering 仍然是重要的。

只是到了 Agent 中,它只是上下文工程的一部分。

三、上下文从哪里来

要管理上下文,先要知道 Agent 每一次调用模型时,信息可能从哪里来。

在前面的多篇文章中,我们多次提到 Agent 的运行过程:

代码片段Text
调用模型

判断是否调用 Tool

执行 Tool

返回 Tool Result

再次调用模型

这个过程会重复进行,直到模型生成最终结果。

在这期间,模型每一次拿到的上下文都可能不同。

System Prompt

System Prompt 用来说明 Agent 的基本职责和行为要求。

例如:

代码片段Text
你是一个企业知识库助手。

回答公司制度问题时优先查询内部知识库。
资料不足时不要自行补充事实。

这类内容通常比较稳定。

但在实际项目中,System Prompt 也可能动态变化。

例如管理员和普通员工看到的操作权限不同,那么 Agent 可以根据当前用户身份,在模型调用前临时加入不同的说明。

所以 System Prompt 不一定要在应用启动时全部写死。

当然,如没有特殊目的,不建议动态调整 System Prompt。

Message

Message 是当前 Thread 中已经发生过的对话和执行记录。

常见的消息包括:

代码片段Text
HumanMessage
AIMessage
ToolMessage

例如:

代码片段Text
用户:查询订单 10001
AI:我帮你查一下
Tool:订单状态为已发货
AI:订单已经发货

这些消息会成为后续模型调用的重要上下文。

对话持续得越久,Message History 通常也会越来越长。

Tool Result

Agent 调用工具后,工具返回的数据通常也会进入后续模型调用。

这里需要注意,工具返回的数据会极大影响上下文的长度。

例如搜索工具返回了十几 KB 的网页内容,数据库工具一次查回几百条记录,这些内容都可能继续留在上下文里。

所以很多 Agent 后期 Token 增长很快,并不只是因为聊天记录太长,还有一个常见原因:

Tool Result 太多、太大。

Runtime Context

运行时上下文(Runtime Context)保存的是程序本次运行需要使用的信息,例如:

代码片段Text
user_id
用户角色
租户 ID
权限
数据库连接
API Client
环境配置

这类信息不一定需要直接给模型看。

例如数据库连接对象只是 Tool 执行数据库查询时需要,模型根本不需要知道它的具体内容。

所以 Runtime Context 更像程序运行时的依赖。

需要时,Middleware 或 Tool 可以读取它,再决定哪些信息需要转换成模型能够使用的上下文。

State 与 Store

State 保存当前 Thread 的状态,也是短期记忆的重要载体。

例如:

代码片段Text
当前 Message
当前任务状态
已经执行过哪些步骤
中间结果

Store 更适合保存跨 Thread 的长期信息,例如:

代码片段Text
用户偏好
历史事实
长期配置
用户画像

数据放在哪里是一回事。

这一轮要不要给模型看,是另一回事。

上下文工程的核心,就是决定上述这些数据中,哪些应该在当前任务中提供给模型。

四、为什么不能什么都给

理论上来说,我们刚才提到的那些数据都有用,因此,很容易形成一种思路:

既然这些信息以后可能有用,那就都给模型。

刚开始这样做确实很省事。

但系统稍微复杂以后,问题就会越来越明显。

最直接的是 Context Window 有长度限制。

Message、检索文档和 Tool Result 一直增加,最终可能超过模型能够接受的最大输入长度。

但即使没有达到这个上限,上下文过多也不一定有帮助。

例如用户问:

代码片段Text
帮我看看这个接口为什么返回 401。

模型真正需要的可能只有:

代码片段Text
接口代码
认证方式
请求 Header
最近的错误日志
相关配置

如果同时再给它:

代码片段Text
三天前讨论过的数据库设计
完整项目 README
几十个 Tool 定义
完整用户画像
以前的所有搜索结果

这些东西虽然都属于同一个项目,但对当前问题帮助不大。

它们反而会增加:

代码片段Text
输入 Token
响应时间
无关信息干扰
工具选择难度

所以,上下文工程不是简单地解决“窗口不够大”。

它要解决的是:

模型这一轮判断时,到底需要知道多少。

即使模型支持很大的 Context Window,也没有必要把所有能找到的信息都塞进去。

窗口更大,只代表可以放更多东西,并不代表应该放更多东西。

五、上下文怎么控制

实际开发中,上下文管理常见的做法主要有选择、裁剪、摘要和动态注入。

选择

最理想的方式,不是先把所有内容放进去再删除,而是一开始就只取当前需要的信息。

例如一个企业 Agent 有 40 个 Tool。

如果用户现在只是查询订单,那么这一轮可能只需要:

代码片段Text
订单查询
物流查询
退款查询

财务报表、代码仓库、服务器运维等 Tool,没有必要一起暴露给模型。

Tool 本身也会占用上下文。

而且 Tool 越多,模型需要做的选择越复杂,选错工具的可能性也会增加。

Memory 和 RAG 也是同样的道理:

代码片段Text
不是读取全部记忆
而是读取相关记忆

不是加载整个知识库
而是检索相关文档

不是保留全部历史
而是保留当前任务需要的历史

裁剪

对于 Message History,最简单的方法是直接裁剪。

例如:

代码片段Text
只保留最近 20 条 Message

或者:

代码片段Text
只保留最近 8000 Token 的历史

这种方式实现简单,也不会多产生一次模型调用。

问题也很明显:被删掉的信息,当前这一轮就看不到了。

所以它更适合那些主要依赖近期对话的场景。

摘要

如果旧消息不能直接删除,就可以把较早的内容压缩成摘要。

例如:

代码片段Text
前 40 条 Message

一段会话摘要
+
最近 10 条原始 Message

LangChain 提供了 SummarizationMiddleware 来处理这类长对话。

代码片段Python
from langchain.agents import create_agent
from langchain.agents.middleware import SummarizationMiddleware
 
agent = create_agent(
    model="gpt-5.5",
    tools=[],
    middleware=[
        SummarizationMiddleware(
            model="gpt-5.4-mini",
            trigger=("tokens", 4000),
            keep=("messages", 20),
        )
    ],
)

运行结果

代码片段Text
历史消息超过设定阈值后:

较早消息 → 压缩成摘要
最近消息 → 保留原文
后续调用 → 使用“摘要 + 最近消息”

这里要区分两种处理方式。

如果只是模型调用前临时删除一些 Message,影响的只是这一次调用。

State 中原来的消息仍然可以保留。

SummarizationMiddleware 会更新 State 中的消息历史,所以后续模型调用看到的也是压缩后的结果。

虽然都能减少上下文,但两者对后续运行的影响并不一样。

六、上下文可以按需生成

真实系统里,还有很多信息并不适合长期写在 System Prompt 中。

例如:

代码片段Text
当前用户角色
用户回答偏好
当前上传的文件
任务执行阶段
业务权限
当前环境

这些信息每一次运行都可能不同。

更合适的方式,是在模型调用之前,根据当前状态临时生成。

LangChain 可以通过 Middleware 或 dynamic_prompt 读取 State、Store 和 Runtime Context,再构造这一轮需要的 System Prompt。

假设 Store 中保存了用户的回答偏好,而 Runtime Context 中提供当前 user_id

代码片段Python
from dataclasses import dataclass
 
from langchain.agents import create_agent
from langchain.agents.middleware import dynamic_prompt, ModelRequest
from langgraph.store.memory import InMemoryStore
 
 
@dataclass
class UserContext:
    user_id: str
 
 
store = InMemoryStore()
 
store.put(
    ("preferences",),
    "user-001",
    {
        "communication_style": "concise",
    },
)
 
 
@dynamic_prompt
def build_system_prompt(request: ModelRequest) -> str:
    user_id = request.runtime.context.user_id
 
    preference = request.runtime.store.get(
        ("preferences",),
        user_id,
    )
 
    prompt = "You are a technical assistant."
 
    if preference:
        style = preference.value.get(
            "communication_style",
            "balanced",
        )
 
        if style == "concise":
            prompt += "\nKeep answers concise and avoid unnecessary background."
 
    return prompt
 
 
agent = create_agent(
    model="gpt-5.5",
    tools=[],
    middleware=[build_system_prompt],
    context_schema=UserContext,
    store=store,
)
 
result = agent.invoke(
    {
        "messages": [
            {
                "role": "user",
                "content": "解释一下 HTTP 401 和 403 的区别。",
            }
        ]
    },
    context=UserContext(user_id="user-001"),
)
 
print(result["messages"][-1].content)

运行结果大致如下:

代码片段Text
401 表示身份认证失败或尚未完成认证,
例如 Token 缺失、无效或已经过期。

403 表示服务器已经识别用户身份,
但当前用户没有访问这个资源的权限。

这里没有把整个用户画像放进模型。

Middleware 先通过 user_id 找到用户偏好,再把:

代码片段Text
回答尽量简洁

这一条真正有用的信息加入当前 System Prompt。

如果下一个用户偏好详细解释,那么下一次调用生成的 Prompt 又可以不同。

这样一来,上下文不再是一份长期不变的模板,而是根据当前任务临时组装。

七、Memory 和 RAG 怎么参与

从上下文工程的角度看,它们其实都是信息来源,只是解决的问题不同。

短期记忆关心的是:

代码片段Text
这个 Thread 前面发生过什么?

例如:

代码片段Text
用户刚才说了什么
Agent 调用了哪些工具
任务已经做到哪一步

LangChain 中,这些内容通常保存在 State,并可以通过 Checkpointer 持久化。

长期记忆关心的是:

代码片段Text
换一个 Thread 以后,还应该记住什么?

例如:

代码片段Text
用户喜欢简洁回答
用户所在部门
以前确认过的偏好
长期保存的用户信息

这类数据可以保存在 Store 中,需要时再读取。

RAG 解决的是:

代码片段Text
回答当前问题,需要查哪些外部资料?

例如:

代码片段Text
公司制度
产品文档
技术手册
数据库记录
网页内容

于是,一个 Agent 完全可以同时使用三种来源:

代码片段Text
State

当前会话的信息

Store

跨会话保存的信息

Retriever

当前问题需要的外部资料



筛选和整理



LLM

重要的原则,依然是:不能因为这些信息都有用,就全部取出来。

实际开发中,更常见的做法是按需分批获取。例如,只取最近几轮聊天记录,而不是全部历史;用户记忆按活跃度或相关性筛选;RAG 文档限制在 Top 5;工具结果仅保留最近一次调用。也可以通过设置上下文窗口预算,为不同类型信息分配额度,超出的部分丢弃或压缩。

这样做能避免上下文失控。

八、Middleware 怎么管理

理解到这里,再看 Middleware 就比较容易了。

create_agent 构建在 LangGraph 之上,Middleware 可以插入 Agent 的运行过程,在模型调用和 Tool 调用前后处理数据。

模型调用之前,可以做:

代码片段Text
修改 System Prompt
筛选 Message
读取长期记忆
选择 Tool
选择 Model
设置 Response Format

Tool 执行之后,也可以做:

代码片段Text
整理 Tool Result
更新 State
写入 Store
提取需要长期保存的信息

对话越来越长时,还可以:

代码片段Text
摘要旧消息
清理历史 Tool Result
限制上下文大小

因此,一个稍完整的 Agent 可以形成这样的结构:

代码片段Text
State / Store / Runtime Context / Retriever

              Middleware

       Prompt / Message / Tools

                 LLM

Middleware 更适合处理的是“模型调用前后怎么整理信息”,而不是负责保存所有信息。

例如 Store 中有几十条用户记忆。

Middleware 可以先找到与当前问题有关的两三条,再放进 Prompt。

一个 Agent 有几十个 Tool,也可以根据当前任务,只把真正需要的工具交给模型。

如果这些逻辑散落在每一个业务调用中,很快就会变得难以维护。

放到 Middleware 中统一处理,会清晰很多。

九、多 Agent 中的上下文

当一个 Agent 要做的事情越来越多,上下文往往也会越来越复杂。

例如一个研究任务可能需要:

代码片段Text
搜索网页
读取十几个页面
分析内容
比较多个来源
整理结论

如果这些步骤全部发生在主 Agent 中,那么搜索结果、网页正文、工具调用记录都会不断进入主 Agent 的 Message History。

等真正开始写答案时,上下文中已经堆满了大量中间过程。

这时 Multi-Agent 有一个很实用的价值:

把不同任务的上下文分开。

例如:

代码片段Text
Main Agent

   ├── Research Agent
   │      ├── Search
   │      ├── Read
   │      ├── Analyze
   │      └── Compare

   ←── 研究结果

Research Agent 可以在自己的上下文里完成搜索和分析。

主 Agent 不需要看到几十次 Tool 调用,只需要拿到最后整理好的研究结果。

LangChain 的 Subagents 模式就是这种思路。

不过,上下文隔离不等于什么都不传。

真正需要考虑的是:

代码片段Text
主 Agent 给 Subagent 什么
Subagent 自己保留什么
最后返回给主 Agent 什么

如果主 Agent 把完整 Message History 原样传给所有 Subagent,隔离的意义就小了。

反过来,如果只给一句:

代码片段Text
帮我研究这个问题

Subagent 又可能缺少完成任务所需的背景。

所以 Multi-Agent 设计中,一个很重要的问题就是:

上下文应该在哪里断开,又应该在哪里传递。

十、实际开发中的选择

上下文工程没有固定模板。

不同 Agent 需要的信息完全不同,但有一些做法比较实用。

第一,先从简单上下文开始。

不要一上来就同时加入:

代码片段Text
长期记忆
自动摘要
动态 Prompt
十几个 Middleware
Multi-Agent
复杂 RAG

先把最基本的 Agent 跑通。

模型缺什么,再补什么;上下文真的变长了,再做裁剪和摘要。

第二,区分“系统里有什么”和“模型看到什么”。

数据库可以保存十万条历史记录,但当前模型调用可能只需要三条。

Store 负责保存。

Retriever 或业务代码负责查找。

Middleware 再决定怎样交给模型。

第三,Tool Result 尽量只保留后续推理真正需要的数据。

例如数据库查出了 500 行记录,但模型只需要:

代码片段Text
总数
最近 5 条
几个统计值

那就应该在 Tool 内先整理,而不是直接返回完整 JSON。

第四,长期信息尽量按需加载。

姓名、语言、固定回答偏好这类经常使用的信息,可以动态加入 Prompt。

历史订单、以前讨论的问题、大量用户事实,则更适合需要时再查。

第五,不要只看 Token。

实际运行中还应该一起观察:

代码片段Text
输入 Token
响应时间
Tool 调用次数
任务成功率
工具误选情况
回答准确性

把上下文压得太短,也可能导致模型缺少必要信息。

总结

上下文工程不是独立于 Agent 的模块,而是设计 Agent 输入信息的方式。

对模型而言,上下文就是这一轮调用中它能看到的全部内容,包括 System Prompt、Message、Tool Result、短期与长期记忆、RAG 检索结果及部分运行时信息。

核心问题不是“让模型看到更多”,而是“这一轮它应该看到什么”。

State 记录当前会话,Store 保存跨会话信息,Retriever 查找外部知识,Middleware 在模型调用前进行筛选、裁剪、摘要和注入。多 Agent 同理,拆开任务可避免中间信息堆满 Context Window。

开发时可先检查每次调用带了哪些 Message、Tool Result 返回了多少内容。上下文工程是让 Agent 在需要时刚好拿到所需信息。