菜鸟AI - 让提示词生成更简单! 全站导航 全站导航
AI工具安装 新手教程 进阶教程 辅助资源 AI提示词 热点资讯 技术资讯 产业资讯 内容生成 模型技术 AI信息库

已有账号?

首页 > AI创作与模型 > Agent上下文工程:让每个Token高效利用的实战指南
模型技术

Agent上下文工程:让每个Token高效利用的实战指南

2026-05-30
阅读 0
热度 0
作者 菜鸟AI编辑部
摘要

摘要

上下文工程:远不止“写好 Prompt”那么简单 如果你翻阅过 Anthropic 的 Agent 最佳实践指南,

上下文工程:远不止“写好 Prompt”那么简单

如果你翻阅过 Anthropic 的 Agent 最佳实践指南,会发现这样一段核心论述:

Agent系列(八):上下文工程——让每个 Token 都用在刀刃上

上下文工程(Context Engineering)比 Prompt 工程更底层,它直接管理整个上下文窗口的内容构成与预算分配:放什么、放多少、按什么顺序、预算不够时怎么取舍。

本文从三个维度拆解这一工程方法:

  1. 上下文五来源 + Token 成本剖析:窗口里到底塞了哪些东西
  2. 预算约束下的动态组装:Token 告急时如何做优先级决策
  3. 溢出策略三选一:截断、摘要、检索——同一问题下答案质量的真实差异

上下文的五个来源

一个完整的 Agent 上下文由五类内容组成,每类有自己的生命周期和成本特征:

┌─────────────────────────────────────────────────────────┐
│              Agent 上下文构成                             │
├──────────────────┬──────────────────────────────────────┤
│ ① System Prompt  │ 固定载入,定义 Agent 角色和行为准则    │
│                  │ 特征:稳定,适合 Prompt Caching        │
├──────────────────┼──────────────────────────────────────┤
│ ② 工具定义        │ 按需加载,当前任务相关的工具 Schema    │
│                  │ 特征:工具越多膨胀越快(每个 ~50-200t) │
├──────────────────┼──────────────────────────────────────┤
│ ③ 对话历史        │ 最近 K 轮,随对话增长                 │
│                  │ 特征:线性增长,需要截断/摘要控制      │
├──────────────────┼──────────────────────────────────────┤
│ ④ 检索内容        │ 动态注入,当前问题相关的知识库片段     │
│                  │ 特征:质量决定生成质量,需相关度过滤   │
├──────────────────┼──────────────────────────────────────┤
│ ⑤ 当前输入        │ 用户当前 Turn 的问题                  │
│                  │ 特征:永远最后载入,不可省略           │
└──────────────────┴──────────────────────────────────────┘

Token 成本实测

以一个典型客服 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:对话轮数增长

  • 8 轮历史 = 263 tokens;100 轮历史 ≈ 3,200 tokens;1000 轮历史 ≈ 32,000 tokens
  • 长期运行的 Agent(客服/助手)若不加控制,几百轮后必然遭遇溢出

因子 2:工具数量增长

  • 4 个工具 = 174 tokens;20 个工具 ≈ 860 tokens;100 个工具 ≈ 4,300 tokens
  • MCP Agent 接入大量工具时,工具定义本身就会吃掉几 K tokens

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      — 只加载当前任务相关工具(可按需裁剪)

ContextBudgetManager 实现

class ContextBudgetManager:
    def __init__(self, total_budget: int = 128_000, output_reserve: int = 4_000):
        self.a vailable = 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 上限,有三种应对方式。用同一个场景测试三种策略:

测试设定:

  • 10 个 Python 主题的对话历史(20 条消息,510 tokens)
  • 历史 Token 上限设为 300(模拟窗口告急)
  • 测试问题:「我最开始问的 Python 列表推导式是什么?能给我一个实际的使用例子吗?」
  • 目标:答案中能否体现第 1 轮对话中学过列表推导式这个事实

策略一:截断(Truncation)

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 条件]   ← 正确但没有引用对话历史

策略二:摘要(Summarization)

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)] ← 摘要中有列表推导式,生成了具体例子

策略三:检索(Retrieval)

# 把每轮对话变成 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 预算规划

  • 确定模型上下文窗口大小(Claude Sonnet 200K,GPT-4o 128K,GLM-4 128K)
  • 预留足够输出空间(推荐 4K-20K,取决于任务输出长度)
  • 为每个来源估算 Token 上限:System Prompt < 2K,工具定义按需加载,历史上限 20K,检索上限 30K

优先级组装

  • P0/P1(System Prompt + 当前输入)永远完整,不参与裁剪
  • P2(历史)从最新轮倒序加,超预算时停止而不是随机删
  • P3(检索内容)按相关度评分过滤,分数低的不载入
  • P4(工具定义)只加载当前 Turn 可能用到的工具,不全量载入

溢出策略选择

  • 对话轮数 < 20:截断即可,实现简单
  • 对话轮数 20-100 或需保留全局脉络:用摘要(建议 5-10 轮触发一次)
  • 对话轮数 > 100 或有"找早期特定内容"需求:用检索(向量索引历史对话)

监控与告警

  • 记录每次请求的实际 Token 消耗(输入/输出/缓存命中)
  • 设置告警:单次请求超过窗口 80% → 说明压缩策略需要调整
  • 记录截断/摘要触发频率,频繁触发说明阈值设置过低

本篇小结

几个核心结论:

  1. 上下文工程 ≠ Prompt 工程:后者只管 System Prompt 怎么写,前者管整个上下文窗口的内容、预算、优先级
  2. 128K 不是永远够用的:对话历史是最大的增长变量,不加控制的 Agent 在几百轮后会触发溢出
  3. 按优先级组装是核心原则:System Prompt + 当前问题永远优先,历史和检索内容弹性分配
  4. 截断最简单但会丢失早期内容:摘要和检索都能保留第 1 轮信息,检索精准度最高
  5. 摘要 5.2x 的压缩比是真实数据:99 tokens 替代 510 tokens,保留了全部 10 个主题名

下一篇:多 Agent 架构设计模式——什么时候需要多 Agent、Supervisor 模式 vs Pipeline 模式的取舍、以及 LangGraph 的 Subgraph 实现。


参考资料

  • Anthropic: Building Effective Agents
  • LangGraph Context Management
  • tiktoken 文档
  • 本系列完整代码:agent-07-context-engineering

来源:互联网

免责声明

本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。

同类文章推荐

相关文章推荐

更多