如果你读过 Anthropic 的 Agent 最佳实践,会看到这样一句话:

上下文工程(Context Engineering)比 Prompt 工程更底层,管的是整个上下文窗口的内容构成与预算分配:放什么、放多少、按什么顺序、预算不够时怎么取舍。
本篇从三个维度拆解它:
一个完整的 Agent 上下文由五类内容组成,每类都有不同的生命周期和成本特征:
┌─────────────────────────────────────────────────────────┐
│ Agent 上下文构成 │
├──────────────────┬──────────────────────────────────────┤
│ ① System Prompt │ 固定载入,定义 Agent 角色和行为准则 │
│ │ 特征:稳定,适合 Prompt Caching │
├──────────────────┼──────────────────────────────────────┤
│ ② 工具定义 │ 按需加载,当前任务相关的工具 Schema │
│ │ 特征:工具越多膨胀越快(每个 ~50-200t) │
├──────────────────┼──────────────────────────────────────┤
│ ③ 对话历史 │ 最近 K 轮,随对话增长 │
│ │ 特征:线性增长,需要截断/摘要控制 │
├──────────────────┼──────────────────────────────────────┤
│ ④ 检索内容 │ 动态注入,当前问题相关的知识库片段 │
│ │ 特征:质量决定生成质量,需相关度过滤 │
├──────────────────┼──────────────────────────────────────┤
│ ⑤ 当前输入 │ 用户当前 Turn 的问题 │
│ │ 特征:永远最后载入,不可省略 │
└──────────────────┴──────────────────────────────────────┘
对一个典型的客服 Agent 做 Token 剖析(128K 窗口,4K 输出预留,可用 124K):
来源 Token数 占预算% 用途
────────────────────────────────────────────────────────────
① System Prompt 155 0.1% 固定载入
② Tool Definitions 174 0.1% 按需加载(当前任务相关工具)
③ Conv. History (8轮) 263 0.2% 最近 N 轮(可截断/摘要)
④ Retrieved Content 200 0.2% 动态(相关度过滤)
⑤ Current Input 22 0.0% 当前 Turn
────────────────────────────────────────────────────────────
合计 814 0.7%
剩余 Buffer 123,186
看起来 8 轮对话只用了 0.7%,问题在哪?两个增长因子:
因子 1:对话轮数增长
因子 2:工具数量增长
Token 计数工具(使用 tiktoken,cl100k_base 编码作为近似):
import tiktoken_enc = tiktoken.get_encoding("cl100k_base")def count_tokens(text: str) -> int:
"""统计文本 Token 数(对中文是近似值,偏低估)"""
return len(_enc.encode(text))def msg_tokens(msg) -> int:
"""计算单条消息 Token 数(含 4 个 overhead tokens)"""
return count_tokens(msg.content) + 4
核心思路:按优先级加载,预算告急时高优先级内容永远完整,低优先级内容弹性裁剪。
P0 System Prompt — 永远完整载入(角色定义不能丢)
P1 Current Input — 永远完整载入(没有问题什么都没有)
P2 Recent History — 从最新轮倒序加入,直到撑不下(可压缩)
P3 Retrieved Docs — 按相关度从高到低加入(可截断)
P4 Tool Defs — 只加载当前任务相关工具(可按需裁剪)
class ContextBudgetManager:
def __init__(self, total_budget: int = 128_000, output_reserve: int = 4_000):
self.available = total_budget - output_reserve
self.used = 0
self.items: list[dict] = [] def add(self, name: str, content: str, priority: int) -> bool:
"""尝试完整载入,成功返回 True,预算不足返回 False"""
t = count_tokens(content)
if t <= self._remaining():
self.used += t
self.items.append({"name": name, "tokens": t, "priority": priority})
return True
return False def add_with_trim(self, name: str, content: str, priority: int) -> int:
"""尽量多载入:超预算时按比例截断,返回实际载入 tokens"""
t = count_tokens(content)
if t <= self._remaining():
self.used += t
self.items.append({"name": name, "tokens": t, "priority": priority})
return t
budget_left = self._remaining()
if budget_left <= 0:
return 0
ratio = budget_left / t
trimmed = content[:int(len(content) * ratio)]
actual_t = count_tokens(trimmed)
self.used += actual_t
self.items.append({"name": name, "tokens": actual_t, "priority": priority,
"trimmed": True})
return actual_t
场景 A:正常对话(12K 预算)
来源 Tokens % 状态
────────────────────────────────────────────────
System Prompt 155 1.6% 完整
Current Input 22 0.2% 完整
History Turn -1 62 0.6% 完整
History Turn -2 47 0.5% 完整
History Turn -3 61 0.6% 完整
History Turn -4 60 0.6% 完整
Retrieved Docs 200 2.0% 完整
Tool Defs 174 1.7% 完整
────────────────────────────────────────────────
已用 781 7.8%
剩余 9,219 92.2%
场景 B:预算紧张(3K 预算,模拟工具密集型 Agent)
来源 Tokens % 状态
────────────────────────────────────────────────
System Prompt 155 7.8% 完整
Current Input 22 1.1% 完整
Recent History (1轮) 40 2.0% 完整
Retrieved Docs 200 10.0% 完整
Tool Defs 174 8.7% 完整
────────────────────────────────────────────────
已用 591 29.5%
剩余 1,409 70.5%
预算紧张时的优先级体现:System Prompt + 当前输入永远完整,对话历史只保留最近 1 轮,其余按剩余预算弹性载入。核心信息不丢,辅助信息随预算弹性调整。
当对话历史超过 Token 上限,有三种应对方式。用同一个场景测试三种策略:
测试设定:
def strategy_truncation(history: list, max_tokens: int) -> tuple[list, int]:
"""从最新消息开始,倒序累积直到超预算"""
kept = []
used = 0
for msg in reversed(history):
t = msg_tokens(msg)
if used + t > max_tokens:
break
kept.insert(0, msg)
used += t
return kept, used
实测结果:保留了 11/20 条消息(294 tokens),最早可见消息是第 5 轮(__enter__ 和 __exit__ 的上下文管理器),第 1 轮的列表推导式内容已被丢弃。
保留消息数:11/20 | 使用 tokens:294/300
最早可见:'实现 __enter__ 和 __exit__ 方法,或用 @contextmanager...'回答(靠 LLM 通用知识):
Python 列表推导式是一种简洁且强大的方式,用于创建列表。
[表达式 for 变量 in 列表 if 条件] ← 正确但没有引用对话历史
summary = llm.invoke([
SystemMessage("将以下 Python 学习对话压缩为简洁摘要,"
"保留所有已讨论的技术主题和关键结论(不超过 150 字):"),
HumanMessage(history_text),
])
实测结果:510 tokens → 99 tokens,压缩比 5.2x。摘要保留了所有 10 个主题:
原始历史:510 tokens → 摘要:99 tokens(压缩比 5.2x)摘要内容:
Python列表推导式简化for循环,字典推导式生成dict,生成器节省内存,
装饰器包装函数,上下文管理器实现with语句,GIL限制线程,
IO密集型用threading,CPU密集型用multiprocessing,
async/await适合IO密集,dataclass减少样板代码,Pydantic用于数据验证。回答(基于摘要):
当然可以。Python 列表推导式是一种简洁且强大的语法结构,
[x**2 for x in range(5)] ← 摘要中有列表推导式,生成了具体例子
# 把每轮对话变成 Document,构建向量索引
history_docs = [
Document(page_content=f"Q: {q}nA: {a}", metadata={"turn": i+1})
for i, (q, a) in enumerate(history_topics)
]
history_store = Chroma.from_documents(history_docs, embeddings)
history_retriever = history_store.as_retriever(search_kwargs={"k": 2})relevant_docs = history_retriever.invoke(test_question)
# → 精准命中 Turn 1(列表推导式)和 Turn 10(Pydantic)
实测结果:从 20 条消息中精准检索出 2 条(118 tokens),直接命中 Turn 1:
检索到 2 条相关历史(118 tokens):
Turn 1: Python 列表推导式是什么? ← 精准命中
Turn 10: Pydantic 和 dataclass...回答(基于检索到的历史):
当然可以。Python 列表推导式是一种简洁的方式来创建列表。
squared_numbers = [x**2 for x in numbers] ← 有具体例子,引用了历史内容
策略 历史Token使用 第1轮是否可见 实现难度
──────────────────────────────────────────────────────
截断 294 已丢弃 极低
摘要 99 在摘要中 中
检索 118 精准命中 高(需向量索引)
截断的第 1 轮列表推导式内容已被丢弃。LLM 靠自身通用知识作答——答案是对的,但那是 LLM 自己知道的,不是"根据我们的对话历史"。
摘要在 510→99 tokens 的压缩下,保留了全部 10 个主题,能体现"我们讨论过列表推导式"。代价是细节被泛化(只有主题名,没有原始问答内容)。
检索用最少的 tokens(118)精准拿到了第 1 轮内容,答案质量最高。代价是需要构建和维护历史向量索引。
Token 预算规划
优先级组装
溢出策略选择
监控与告警
几个核心结论:
下一篇:多 Agent 架构设计模式——什么时候需要多 Agent、Supervisor 模式 vs Pipeline 模式的取舍、以及 LangGraph 的 Subgraph 实现。
欢迎来我的个人主页找到更多有用的知识和有趣的产品