首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >智能体窗口里 96% 是工具返回:先把这块清掉,再谈压缩

智能体窗口里 96% 是工具返回:先把这块清掉,再谈压缩

原创
作者头像
Archive
修改于 2026-10-22 11:54:37
修改于 2026-10-22 11:54:37
100
举报
文章被收录于专栏:Agent 架构Agent 架构

我们有个长任务智能体,跑一段时间就会撞到窗口上限,然后开始丢上下文。最先想到的是压缩——把前面摘成一段,重开窗口。做完之后发现效果一般,账还更贵了。后来把窗口里的内容拆开看,问题根本不在压缩。

一、先看一眼窗口里到底装了什么

Anthropic 官方 cookbook 给过一个实测运行,一个研究型智能体的窗口构成是这样:

  • 窗口总量:335,279 Token
  • 文件读取的返回内容:322,946 Token,占 96.3%
  • 其余全部(用户消息、模型推理、调用记录):12,333 Token

我第一次看到这个比例是有点懵的。也就是说,窗口里九成六的空间被工具返回的原文占着,真正的对话和推理只有 3.7%。这部分原文在模型看完那一刻就已经没用了,但之后每一轮都还在按输入计费。

所以改造顺序应该反过来:先清工具结果,再考虑压缩。

二、清理:只改消息列表,不动推理

清理的逻辑只有一句话:把旧的工具返回内容换成一个短占位串,但保留"调用过这个工具、用的是这组参数"这条记录。模型知道自己调过,只是正文没了。真需要那份数据,再调一次工具就行。

我这边落地的版本,参数对齐官方的三个字段:

代码语言:python
复制
def clear_old_tool_results(msgs, keep=3, trigger=100_000, clear_at_least=10_000):
    """把旧的工具返回换成占位串。
    keep           : 保留最近几次工具结果
    trigger        : 输入量达到多少才触发
    clear_at_least : 地板,一次至少清掉多少 token,否则不动
    """
    if count_tokens(msgs) < trigger:
        return msgs

    idx = [i for i, m in enumerate(msgs) if m.get("role") == "tool"]
    victims = idx[:-keep] if keep > 0 else idx
    if not victims:
        return msgs

    freed = 0
    for i in victims:                      # 从最旧的开始
        if msgs[i].get("cleared"):
            continue
        freed += count_tokens(msgs[i])
        msgs[i]["content"] = "[工具结果已清理,如需原始内容请重新调用]"
        msgs[i]["cleared"] = True
        if freed >= clear_at_least:
            break
    return msgs

clear_at_least 这个地板千万别省,原因在第四节。

效果对得上官方给的数:三份各约 4 万 Token 的文本文档,keep 设成 1,上下文从约 128,740 Token 降到约 43,060,少了 67%。而且这一刀不动推理质量,因为模型仍然能看见自己调过什么工具。

代价也得提前想清楚:走到汇总那一步,智能体手上只剩自己的笔记加最近几份读取。我这边笔记写得细,没出问题;如果你的智能体习惯"读一遍就靠记忆",清完可能会开始反复重读。重读本地文件几乎不要钱,但重调一个有限速的第三方接口,就是另一笔账了。

三、压缩:摘要替换历史,以及两个坑

清理之后如果还顶得上限,再上摘要式压缩。做法是把前面一大段总结成一份摘要,用它开新窗口继续。

Claude Code 的取舍可以当参考:留架构决定、没解决的问题、实现细节;丢重复的工具输出;再带上最近访问的五个文件。

如果用的是 Google ADK,它把这件事做成了配置项(下面按官方字段写,具体以文档为准):

代码语言:python
复制
compaction_config = {
    "compaction_interval": 3,   # 每 3 个事件压一次
    "overlap_size": 1,          # 新汇总带上 1 个上一轮的事件
}

间隔 3、重叠 1 的实际走向是:

  • 事件 1–3 完成 → 压 1 到 3
  • 事件 6 完成 → 压 3 到 6(第 3 个重叠)
  • 事件 9 完成 → 压 6 到 9(第 6 个重叠)

留一个重叠是为了两份汇总能接上,代价是后面每份汇总都比第一份大。另外 ADK 里还有一条按 Token 走的策略,文档把它列为主要的那条,会故意留几个最近事件不压,让"它""那个"这类指代有得可指。

调压缩提示词有个固定顺序:先拉满召回,再往回删。反着来必翻车,压狠了丢的都是当时不起眼、后面才要命的那几句。还有一条:在真实的长轨迹上调,短对话根本试不出问题。

四、缓存账:这一节最容易被忽略

压缩和清理默认都在改"前面那一段",而前缀缓存的前提是前面一个字不动。官方文档把账写得很清楚:清理会让已缓存的前缀失效,每次都要付一笔写入费,之后的请求才能复用新前缀。

这就是 clear_at_least 存在的意义:一次只清出几百个 Token,却把整段缓存打掉重写,净亏。官方示例的地板是 1 万个 Token——至少能收回这么多才动手。

所以真正要控制的是频率,不是"压不压"。一次大改动摊得开,每轮一个小改动摊不掉。

有个真实的修法值得抄。某个编程智能体原先从最旧的工具结果开始清,于是每轮都从很靠前的位置把缓存打断。有人把循环反过来,从最新的开始清,前缀就逐字不变了,只有尾巴在变。代价是丢掉的是近期输出而不是旧输出——他们判断这段逻辑只在上下文超过 75% 时当应急触发,每轮全量重算的代价比那点质量损失更大。

五、最麻烦的:它丢了东西不报警

压缩有损是共识,真正难的是丢了什么不会写进摘要。被丢掉的常常是顺带提过的一条约束,或者当时特批的一个例外。丢了之后,智能体照常往下办,没有任何提示。

反复压还有第二种问题:摘要的摘要,偏差会一层层叠上去。两种防法:

  • 每次都从原始记录压,不从上一份摘要压,原始记录存在窗口外面;
  • 摘要后补跑一个探查轮,核一遍关键信息丢没丢,代价是每压一次多一次调用。

验收很土但有效:压完对着原始记录把硬约束点一遍。别看摘要读起来顺不顺——读起来顺,不代表里面的例外还在。

还有一点我踩过:压缩提示词通常是框架自带的默认值,装上就没人再改,但它决定了智能体后半程还记得哪几条约束。这个配置项基本处于"没人验收"的状态。验法不难,找一条跑满窗口的真实轨迹,压一次,逐条核对,少哪个就写进必留项。

六、三件工具怎么挑

压缩不是唯一解,官方把长任务的办法分成三样:

  • 压缩:来回轮次多的任务,快到上限摘一次接着聊。
  • 结构化笔记:有明确里程碑、要一轮轮推进的任务。定期把笔记写到窗口外,用的时候取回来,一个 NOTES.md 就够。
  • 子智能体:并行探索。主智能体派活,子智能体用干净上下文干窄活,烧几万 Token 只带回一两千 Token 的摘要。

判断标准就一句:做最简单管用的那件事。

小结

我这次的改造顺序是:先测窗口构成 → 清工具结果(-67%)→ 再看要不要压缩 → 设缓存地板 → 对原始记录验收。省钱只是副产品,真正救回来的是"模型还记不记得当初那几条约束"。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、先看一眼窗口里到底装了什么
  • 二、清理:只改消息列表,不动推理
  • 三、压缩:摘要替换历史,以及两个坑
  • 四、缓存账:这一节最容易被忽略
  • 五、最麻烦的:它丢了东西不报警
  • 六、三件工具怎么挑
  • 小结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档