AI Agent 的上下文窗口管理——如何让 Agent 在有限 token 内做更多事

AI Agent 的上下文窗口管理------如何让 Agent 在有限 token 内做更多事

问题:Agent 跑着跑着就"失忆"了

GPT-4o 有 128K 上下文窗口,Claude 有 200K。看起来很大,但跑一个复杂的 Agent 任务,token 消耗速度远超预期。

我做过一个测试:让一个 ReAct Agent 完成"调研某个开源项目并写一份技术评估报告"的任务。Agent 调用了 12 次工具,每次工具返回的内容平均 2000 token。加上系统提示词 1500 token、每轮的 reasoning 输出约 800 token,跑到第 8 轮时,上下文已经超过 40K token。到第 15 轮,逼近 80K。

问题不是"装不下"------128K 理论上够。问题是:

  1. 成本线性增长 。每轮都把完整历史发给 API,第 15 轮的请求包含了前 14 轮的所有内容。按 GPT-4o 的 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2.5 / 1 M i n p u t t o k e n 计算,一个任务跑下来可能花掉 2.5/1M input token 计算,一个任务跑下来可能花掉 </math>2.5/1Minputtoken计算,一个任务跑下来可能花掉0.3-0.5。批量跑 100 个任务,成本就不可忽视了。
  2. 注意力被稀释。大模型在长上下文中的表现并不均匀。Needle-in-a-Haystack 测试已经证明,信息放在中间位置时,模型的召回率会下降 10%-30%。Agent 的早期工具调用结果恰好就在"中间"位置。
  3. 延迟增加。上下文越长,首 token 延迟(TTFT)越高。128K 上下文的 TTFT 通常是 8K 上下文的 3-5 倍。

所以问题很明确:怎么在不丢失关键信息的前提下,控制上下文的增长速度?

三种实用方案

我在生产环境中试过多种方法,最后沉淀下来三种:滑动窗口 + 摘要、工具输出截断、按需注入。

方案一:滑动窗口 + 摘要

思路很简单:只保留最近 N 轮对话,更早的内容压缩成一段摘要。

python 复制代码
from openai import OpenAI

client = OpenAI()

def summarize_messages(messages: list[dict], max_tokens: int = 300) -> str:
    """把一组消息压缩成摘要"""
    content = "\n".join(
        f"[{m['role']}]: {m['content'][:500]}" for m in messages
    )
    resp = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": "用中文总结以下对话的关键信息,保留所有数据和结论,去掉过程细节。"},
            {"role": "user", "content": content}
        ],
        max_tokens=max_tokens
    )
    return resp.choices[0].message.content

def sliding_window(messages: list[dict], window_size: int = 6) -> list[dict]:
    """保留最近 window_size 轮,其余压缩为摘要"""
    system_msg = messages[0]  # 系统提示词始终保留
    conversation = messages[1:]
    
    if len(conversation) <= window_size * 2:
        return messages  # 还没超,原样返回
    
    old_messages = conversation[:-window_size * 2]
    recent_messages = conversation[-window_size * 2:]
    
    summary = summarize_messages(old_messages)
    
    return [
        system_msg,
        {"role": "user", "content": f"[之前的对话摘要]\n{summary}"},
        *recent_messages
    ]

window_size=6 是我反复调整后的值。太小(比如 3)会丢失刚发生的上下文,Agent 容易重复做已经做过的事。太大(比如 15)起不到压缩效果。6 轮在大多数任务中够用。

摘要用 gpt-4o-mini 生成,成本低,速度快。一次摘要大约 0.001 美元,但能节省后续每轮 5000-10000 token 的输入。

方案二:工具输出截断

Agent 调用搜索 API、读取文件、抓取网页,返回的内容经常有大段无用信息。一个网页抓取结果可能有 8000 token,但 Agent 真正需要的信息只有 500 token。

python 复制代码
import tiktoken

encoder = tiktoken.encoding_for_model("gpt-4o")

def truncate_tool_output(content: str, max_tokens: int = 1500) -> str:
    """截断工具输出,保留前后各一部分"""
    tokens = encoder.encode(content)
    if len(tokens) <= max_tokens:
        return content
    
    # 保留前 60% 和后 20%,中间截断
    head_count = int(max_tokens * 0.6)
    tail_count = int(max_tokens * 0.2)
    
    head = encoder.decode(tokens[:head_count])
    tail = encoder.decode(tokens[-tail_count:])
    removed = len(tokens) - head_count - tail_count
    
    return f"{head}\n\n[...已省略 {removed} tokens...]\n\n{tail}"

def smart_truncate(content: str, query: str, max_tokens: int = 1500) -> str:
    """基于相关性的截断:保留与查询相关的段落"""
    paragraphs = content.split("\n\n")
    query_words = set(query.lower().split())
    
    scored = []
    for p in paragraphs:
        p_words = set(p.lower().split())
        overlap = len(query_words & p_words)
        scored.append((overlap, p))
    
    scored.sort(key=lambda x: x[0], reverse=True)
    
    result = []
    current_tokens = 0
    for score, paragraph in scored:
        p_tokens = len(encoder.encode(paragraph))
        if current_tokens + p_tokens > max_tokens:
            break
        result.append(paragraph)
        current_tokens += p_tokens
    
    return "\n\n".join(result)

smart_truncate 比简单截断更好。它根据段落和当前查询的词汇重叠度排序,优先保留相关段落。在实际使用中,这个方法把工具输出的平均 token 数从 3200 降到了 1100,Agent 的任务完成率没有明显下降。

更精确的做法是用 embedding 计算相似度,但对于大多数场景,词汇重叠已经够用,还省掉了 embedding API 的调用。

方案三:按需注入

不要一开始就把所有背景信息塞进系统提示词。把信息拆成模块,Agent 需要时再注入。

python 复制代码
CONTEXT_MODULES = {
    "code_style": "代码规范:使用 black 格式化,类型注解必须完整,docstring 用 Google 风格...",
    "project_arch": "项目结构:src/ 下按领域划分模块,每个模块有 models/、services/、api/ 三层...",
    "api_docs": "API 文档:POST /users 创建用户,需要 name 和 email 字段...",
    "db_schema": "数据库:users 表有 id, name, email, created_at 四个字段...",
}

def build_system_prompt(base_prompt: str, active_modules: list[str]) -> str:
    """根据当前任务阶段,拼装系统提示词"""
    parts = [base_prompt]
    for mod in active_modules:
        if mod in CONTEXT_MODULES:
            parts.append(f"\n---\n{CONTEXT_MODULES[mod]}")
    return "\n".join(parts)

# Agent 在写代码阶段,只注入 code_style 和 project_arch
prompt = build_system_prompt(
    "你是一个 Python 开发助手。",
    ["code_style", "project_arch"]
)

# Agent 在调试 API 阶段,换成 api_docs 和 db_schema
prompt = build_system_prompt(
    "你是一个 Python 开发助手。",
    ["api_docs", "db_schema"]
)

这个方法在编码类 Agent 中效果最好。一个典型的项目可能有 5000 token 的背景信息,但 Agent 在某个具体步骤中只需要其中 1000-1500 token。动态注入可以把系统提示词的平均大小降低 60%。

把三种方案组合起来

单独用一种方案效果有限。组合使用才能发挥最大价值:

python 复制代码
class ContextManager:
    def __init__(self, window_size=6, max_tool_tokens=1500):
        self.window_size = window_size
        self.max_tool_tokens = max_tool_tokens
        self.messages = []
        self.active_modules = []
    
    def add_message(self, role: str, content: str):
        if role == "tool":
            content = truncate_tool_output(content, self.max_tool_tokens)
        self.messages.append({"role": role, "content": content})
    
    def set_modules(self, modules: list[str]):
        self.active_modules = modules
    
    def get_messages(self, base_prompt: str) -> list[dict]:
        system = build_system_prompt(base_prompt, self.active_modules)
        all_msgs = [{"role": "system", "content": system}] + self.messages
        return sliding_window(all_msgs, self.window_size)
    
    def token_count(self) -> int:
        text = "".join(m["content"] for m in self.messages)
        return len(encoder.encode(text))

踩坑经验

1. 摘要丢信息是不可避免的,关键是选择丢什么。

最初我直接让 LLM "总结对话",结果它总是丢掉具体的数字和 URL。后来改成在摘要提示词里加一句"保留所有数据、数字、URL、文件路径",情况好了很多。但仍然会偶尔丢东西。我的解决办法是维护一个独立的 key_facts 列表,把每轮提取的关键数据单独存储,不参与摘要压缩。

2. 截断位置比截断长度更重要。

最开始我用简单的前 N 个 token 截断。问题是很多网页的前 1000 token 是导航栏和版权声明。改成"前 60% + 后 20%"之后好了一些,但最佳方案还是基于相关性的 smart_truncate

3. token 计算别用 len(text) / 4 估算。

中文文本的 token 比例和英文差异很大。一个中文字符通常是 1-2 个 token,不是 0.25 个。用 tiktoken 精确计算,别估算。我因为用估算值设了截断阈值,导致实际发送的 token 比预期多了 40%,白白多花了钱。

4. 滑动窗口的 window_size 要根据任务类型调。

代码生成任务,window_size 可以小一点(4-5 轮),因为每轮相对独立。调研类任务需要更大的窗口(8-10 轮),因为前后轮之间的依赖关系更强。我现在的做法是根据任务类型动态设置,而不是用一个固定值。

5. 别在每轮都做摘要。

摘要本身也有成本------gpt-4o-mini 的一次调用大约 50-100ms 延迟加上 token 费用。我设了一个阈值:只有当历史消息超过 window_size * 3 时才触发摘要。频繁摘要反而拖慢了 Agent 的响应速度。


上下文管理没有银弹。不同任务需要不同策略,代码里的参数也需要根据实际效果反复调整。但把这三种方法组合起来用,在我的场景中把平均 token 消耗降低了 55%,任务完成率只下降了 3%。对于大多数 Agent 应用来说,这个 trade-off 是值得的。

相关推荐
元境3 小时前
2026科幻大会|元境朱国政:AIGC正在创造一个“开源”的科幻宇宙
开源·aigc
摄影图4 小时前
AI神经网络数据可视化图片素材 多格式多场景助力设计高效开展
人工智能·aigc·插画
墨风如雪13 小时前
别让 AI 写得像 AI:用自己的 83 篇博客训练专属写作助手,顺手做成了一个 Skill
aigc
小和尚同志15 小时前
A社 npm 包事故导致 Claude Code 源码泄漏?
人工智能·aigc·claude
Code_LT15 小时前
【AIGC】多 Agent 架构 还是 单Agent?Agent Teams vs SubAgent
架构·aigc
程序员陆业聪16 小时前
字节跳动开源 DeerFlow 2.0 源码拆解:14层Middleware、Sub-Agent并发编排和结构化记忆是怎么做的
aigc
AI先驱体验官16 小时前
智能体变现:从技术实现到产品化的实践路径
大数据·人工智能·深度学习·重构·aigc
小程故事多_8019 小时前
破解Agent“半途摆烂”困局,OpenDev凭Harness架构,撕开Code Agents的工程化真相
人工智能·架构·aigc·harness
用户7546955211121 小时前
从 XML 到叙事稿:我是如何用 AI Agent 自动编辑 PPT 演讲备注的
aigc