AI Agent Context 工程
大家好,我是程序员小策。
AI Agent 的 Context 是什么?
它有什么作用?
在项目中怎么去使用?
如果你有上述问题,不妨接着往下看。
一、大窗口为什么不是银弹
先说一个反直觉的事实:上下文窗口变大 ≠ 模型能更聪明地使用信息。
2023 年 Stanford 和 Meta 的论文「Lost in the Middle」发现了一个 U 型曲线——相关信息出现在开头或结尾时模型表现很好,埋在中间准确率就掉。Chroma 在 2025 年「Context Rot」研究里测了 18 个模型,只是改变信息在上下文里的排列方式,GPT-4o 的准确率就能从 98.1% 掉到 64.1%。
换句话说,你给模型一个 1M token 的窗口,它能"用上"的 token 可能就前 32K + 后 32K,中间那一片基本是噪声。
再看一个生产数据:在完整的 Agent 任务执行链路里,工具调用返回的结果占据整体 token 的 60%+,而系统提示词只占 30% 左右。大量珍贵的上下文空间被杂乱冗长的外部返回数据占据,真正用于思考决策的有效空间被严重挤压。
那 Context Engineering 到底是什么?用 Andrej Karpathy 的话说:“用下一步的正确信息填充上下文窗口的微妙艺术和科学。”
Cognition(Devin 的开发团队)说过一句我很认同的话:“Context engineering is effectively the #1 job of engineers building AI agents.” 翻译过来就是:写 Agent 的人,其实大部分时间不是在调 prompt,而是在管理上下文。
二、4 大 Context 分类 + 引用块定义
业内对 Context 的分类有 3 套主流说法(LangChain 官方、Deep Agents 四大支柱、Galileo 四象限),本质上互相重叠。我把它们归一化,得到 4 大类。
Context(上下文):Agent 在推理前能看到的所有信息生态,由 4 层组成。
- 输入上下文(Input):用户指令、System Prompt、RAG 检索片段、当前会话历史
- 运行时上下文(Runtime):Agent 自身行动、工具调用结果、中间推理步骤
- 压缩上下文(Compressed):对冗长历史做摘要、提取、嵌入后的精炼信息
- 长期记忆(Long-term):跨会话持久化的用户偏好、历史交互、学到的知识
类比时间。把这 4 层映射到"自来水管网",我跟你讲,绝绝子:
- 长期记忆 = 自来水公司的水库,容量大、调用慢、永远在
- 压缩上下文 = 区域加压站,把支管的水汇总加压,按需供给
- 运行时上下文 = 入户主水管,这一户用得最多的水都从这里走
- 输入上下文 = 水龙头,你每次开水龙头就调一次温度(重新组装)
主水管(运行时)不能太粗,否则水压撑不住——这就是为什么 200K 窗口也会爆。输入层(指令)必须稳,否则水龙头出水忽冷忽热——System Prompt 不能随上下文变化被污染。
三、主流项目怎么存 Context:从 LangGraph 到 Claude Code
理论说完了,看实战。下面是 5 个主流项目的存储方案对比:
| 项目 | 短期存储 | 长期存储 | 核心创新 |
|---|---|---|---|
| LangGraph | Checkpointer(InMemory / Postgres) |
Store + 向量索引 |
命名空间隔离 (user_id, “profile”) |
| CrewAI | 统一 Memory 类 |
同一类按 scope tree 分层 | LLM 自动推断 scope/category/importance |
| AutoGen | 对话 buffer | 配合 OpenMemory 持久化 | 框架无关 |
| Claude Code | 五层压缩 + Session Memory | 后台 fork 进程维护笔记 | 笔记复用 prompt cache,零额外成本 |
| Cognee | — | PostgreSQL + 知识图谱 | 图结构 + 向量双索引 |
两个值得抄的作业:
-
LangGraph Store:单线程用
Checkpointer(InMemorySaver / PostgresSaver),跨线程用Store(InMemoryStore / PostgresStore)。命名空间隔离是事实标准。 -
Claude Code 的 Session Memory:后台 fork 进程持续维护一份结构化笔记,主线程压缩时直接读笔记,避免重复 LLM 调用。这等于"压缩的输入成本被分摊到了所有模型调用"上。
四、代码实现:用 LangGraph 搭 4 层 Context
# 完整可运行 Demo:4 层 context 管理
# 依赖:pip install langgraph langchain-openai langchain-core
import os
from typing import Annotated, TypedDict
from operator import add
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.store.memory import InMemoryStore
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage, RemoveMessage
# ====== 1. 4 层 context 的状态 schema ======
class AgentContext(TypedDict):
# 输入上下文(用户消息 + System Prompt)
messages: Annotated[list, add]
system_prompt: str
# 运行时上下文(Agent 内部状态)
current_step: str
tool_call_count: int
# 压缩上下文(历史摘要)
history_summary: str
# 长期记忆(从 Store 取,本地缓存)
user_preferences: dict
# ====== 2. 长期记忆:跨会话 Store ======
long_term_store = InMemoryStore(
index={"embed": lambda texts: [[0.0] * 1536 for _ in texts]} # 生产换 OpenAI embedding
)
def load_long_term_memory(user_id: str) -> dict:
"""从 Store 读取用户偏好"""
namespace = (user_id, "profile")
prefs = {}
for key, value in long_term_store.search(namespace):
prefs[key] = value.value
return prefs
def save_long_term_memory(user_id: str, key: str, value: dict):
"""写入用户偏好到 Store"""
long_term_store.put((user_id, "profile"), key, value)
# ====== 3. 压缩节点:超过 8 条消息就触发摘要 ======
def compress_history_node(state: AgentContext) -> dict:
messages = state["messages"]
if len(messages) <= 8:
return {} # 未触发压缩
# 保留 System + 最近 2 条,其余送 LLM 摘要
to_summarize = messages[:-2]
recent = messages[-2:]
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
summary_prompt = (
"请将以下对话历史压缩为 200 字内的关键信息摘要,"
"保留:用户偏好、关键决策、未完成任务。\n\n"
f"{chr(10).join(f'{m.type}: {m.content[:200]}' for m in to_summarize)}"
)
summary = llm.invoke(summary_prompt).content
# 显式删除被压缩的消息(不靠截断最后 N 条那种暴力做法)
return {
"messages": [RemoveMessage(id=m.id) for m in to_summarize if hasattr(m, "id")],
"history_summary": summary,
}
# ====== 4. 主 Agent 节点:组装 4 层 context 后调用 LLM ======
def agent_node(state: AgentContext, config: dict) -> dict:
user_id = config.get("configurable", {}).get("user_id", "default")
prefs = load_long_term_memory(user_id)
# 4 层组装:System + 长期偏好注入 + 压缩摘要 + 当前历史
system_content = state["system_prompt"]
if prefs:
system_content += f"\n\n用户偏好:{prefs}"
if state.get("history_summary"):
system_content += f"\n\n历史摘要:{state['history_summary']}"
full_messages = [SystemMessage(content=system_content)] + state["messages"]
llm = ChatOpenAI(model="gpt-4o", temperature=0)
response = llm.invoke(full_messages)
return {
"messages": [response],
"current_step": "responded",
"tool_call_count": state.get("tool_call_count", 0),
}
# ====== 5. 搭图:compress → agent ======
workflow = StateGraph(AgentContext)
workflow.add_node("compress", compress_history_node)
workflow.add_node("agent", agent_node)
workflow.add_edge(START, "compress")
workflow.add_edge("compress", "agent")
workflow.add_edge("agent", END)
# Checkpointer 用于短期记忆(单线程)
checkpointer = InMemorySaver()
graph = workflow.compile(checkpointer=checkpointer)
# ====== 6. 运行测试 ======
if __name__ == "__main__":
config = {"configurable": {"thread_id": "demo-1", "user_id": "alice"}}
# 写入一条长期记忆
save_long_term_memory("alice", "writing_style",
{"style": "简洁有力,避免废话"})
# 模拟多轮对话触发压缩
result = None
for i in range(12):
payload = {
"messages": [HumanMessage(content=f"第 {i+1} 个问题:帮我写一段开场白")],
"system_prompt": "你是一个小说写作助手",
"current_step": "init",
"tool_call_count": 0,
"history_summary": "",
"user_preferences": {},
} if i == 0 else None
result = graph.invoke(payload, config)
print("最终消息数:", len(result["messages"]))
print("历史摘要:", result.get("history_summary", "(未压缩)")[:100])
关键设计点:
Checkpointer管单线程(thread_id 隔离)Store管跨线程(namespace = (user_id, “profile”))RemoveMessage显式删除被压缩消息- 压缩触发阈值:8 条(生产环境建议 60-80% 窗口使用率)
五、看起来很完美,但这 4 个坑你可能踩过
坑 1:压缩时机太晚
现象:等到 prompt_too_long 报错才压缩
根因:触发后状态已污染,回滚困难
解法:60% 主动压缩,70% 强制压缩。Claude Code 团队的内部建议。
坑 2:摘要质量不稳定
现象:同样一段对话,不同次摘要差异巨大
根因:摘要 prompt 没固定结构
解法:用「Anchored Iterative Summarization」,固定 section(intent / decisions / files / next steps),强制结构化。
坑 3:原子消息组被拆散
现象:
tool_call消息被保留,但对应的tool_result被截断
根因:按条数截断,没考虑消息组完整性
解法:必须以 MessageGroup 为单位(Microsoft Agent Framework 的做法),保证 tool_call + tool_result 同时保留或删除。
坑 4:长期记忆没分类
现象:所有偏好混在一起,召回时一堆噪声
根因:直接put(key, value),没分类
解法:用 scope tree 分层(CrewAI 做法),/user/alice/preferences和/user/alice/projects隔离。
六、生产考量:从 Demo 到上线还差 4 件事
1. 持久化:InMemorySaver 只适合 demo。生产换 PostgresSaver,Store 换 PostgresStore。命名空间隔离用 (user_id, "profile") 这种元组。
2. 监控:token 消耗分桶统计——输入 prompt、工具返回、压缩摘要调用、LLM 输出。LangSmith / Langfuse / Helicone 三选一。只监控总 token 等于没监控,因为压垮你的是某一桶。
3. 容错:压缩失败怎么办?保留原消息 + 标记 compression_failed=True,下次再试。绝对不能因为压缩失败导致会话中断。
4. 资源:摘要本身要 LLM 调用,建议用 gpt-4o-mini 或 claude-haiku-4-5 而不是主力模型,省 80% 成本。
七、5 种压缩策略对比一览表
| 策略 | 核心思路 | 成本 | 保真度 | 适用场景 |
|---|---|---|---|---|
| 截断 (Truncation) | 直接丢弃最早 N 条 | 零 | 低 | 闲聊、问候、已解决的支持问题 |
| 滑动窗口 (Sliding Window) | 保留最近 N + 重叠 | 零 | 中 | 固定长度任务流 |
| 摘要 (Summarization) | LLM 生成摘要替换原文 | 中(有 LLM 调用) | 高 | 长对话、多任务连续 |
| 选择性保留 (Selective Retention) | 按类型分级(System 永久、Tool 可压) | 低 | 中-高 | 多类型混合内容 |
| 外部化 (Offloading) | 大块内容写盘,只回注引用 | 低 | 高 | 工具返回超长、文档处理 |
一句话总结:单独用哪种都不对,业界共识是"分层组合"——截断/滑窗打头阵(便宜确定),摘要兜底(保真),必要时外部化或子代理隔离。Claude Code 的五层压缩(microcompact → autocompact → context collapse)就是这个套路。
八、面试官还会追问什么?
追问 1:Context 压缩和 RAG 检索本质区别是什么?
→ 压缩是把"已有信息"瘦身,RAG 是从"外部知识库"补血。一个是减法,一个是加法。生产环境里两者配合用,先 RAG 补全缺什么,再压缩减重。
追问 2:为什么不让 LLM 自己决定"该压缩哪部分"?
→ LLM 决策失败时会把关键决策一起压掉,反而更糟。正确做法是规则优先(按时间、长度、类型),LLM 只负责生成摘要内容,不决定"压不压"。
追问 3:长期记忆和 RAG 看起来很像,怎么选?
→ 长期记忆是"用户画像 + 历史决策",强个性化、低更新频率。RAG 是"领域知识 + 文档片段",强客观性、高更新频率。前者用 Store 配 embedding,后者用向量数据库。
追问 4:如果模型支持 1M token,是不是就不用压缩了?
→ 大窗口有"Context Rot"问题:信息越多,关键信息被淹没的概率越高。而且成本线性增长、延迟线性增加。1M token 不是不用压缩,而是更该分层管理。
九、总结:Context 工程的核心是「分层 + 预算」
Context Engineering 不是"把 prompt 写长一点"也不是"换个大窗口模型",它的本质是用分层 + 预算管理把 200K token 运营成可控资源。
读完这篇你应该能:
- 区分 4 大 context 类型,知道它们各自的生命周期
- 选型主流存储方案(LangGraph Checkpointer + Store 是事实标准)
- 选型压缩策略(截断优先 + 摘要兜底 + 外部化兜底)
- 避开 4 个常见坑(时机、结构、原子组、分类)
如果这篇文章让你对 context 工程少一些敬畏,欢迎点赞 + 在看。
最后一个问题留给你:你的项目里,"运行时上下文"现在有多重?有没有量过工具返回占多少比例?
更多推荐


所有评论(0)