第七篇:上下文压缩 —— Agent 永续工作的秘密

上下文压缩 ------ Agent 永续工作的秘密

"上下文总会满,要有办法腾地方。" ------ 这个 repo 中文文档里的一句原话。


200K 的上下文窗口也不够用

Claude 有 200K 上下文,GPT-4 有 128K,听起来很多对吧?

来算一笔账:

复制代码
读一个 1000 行的 Python 文件    ≈ 4,000 tokens
读 10 个这样的文件              ≈ 40,000 tokens
跑 3 个测试(输出各 500 行)    ≈ 12,000 tokens
改 5 个文件(diff 输出)        ≈ 8,000 tokens
和 Agent 对话 20 轮             ≈ 15,000 tokens
工具调用的元数据                ≈ 5,000 tokens
─────────────────────────────────────────
总计                            ≈ 84,000 tokens

还没干多少活,80K+ 就没了。

而且最关键的问题是:即使上下文窗口还有空间,模型的推理能力也会随着上下文长度下降。 研究已经反复证明------Transformer 的注意力在长序列上会"稀释",模型会"忘记"早期内容。

所以 s06 的核心命题是:

与其给模型一个无限大的垃圾桶,不如教它定期倒垃圾。


三层压缩:从温和到激进

s06 的压缩策略分为三个层次,逐层加大力度:

复制代码
每一轮对话:

+------------------+
| Tool call result  |  ← 刚执行完一个工具
+------------------+
        |
        v
[Layer 1: micro_compact]         ← 静默执行,每轮都跑
  把超过 3 轮前的 tool_result
  替换成 "[Previous: used {tool_name}]"
        |
        v
[检查: tokens > 50000?]
   |               |
   no              yes
   |               |
   v               v
继续工作       [Layer 2: auto_compact]
                保存完整对话到 .transcripts/
                LLM 摘要整个对话
                用摘要替换所有 messages
                      |
                      v
              [Layer 3: compact tool]
                模型主动调用 compact
                立刻触发摘要(和 auto 一样)
                Agent 手动控制

三层,从"轻量清理"到"归档重开"。


Layer 1: Micro Compact ------ 悄悄的,每轮都做

python 复制代码
KEEP_RECENT = 3
PRESERVE_RESULT_TOOLS = {"read_file"}

def micro_compact(messages: list) -> list:
    # 找出所有 tool_result
    tool_results = []
    for msg_idx, msg in enumerate(messages):
        if msg["role"] == "user" and isinstance(msg.get("content"), list):
            for part_idx, part in enumerate(msg["content"]):
                if isinstance(part, dict) and part.get("type") == "tool_result":
                    tool_results.append((msg_idx, part_idx, part))
  
    # 如果总数不超过保留阈值,不动
    if len(tool_results) <= KEEP_RECENT:
        return messages
  
    # 保留最近 3 条,其余的干掉
    to_clear = tool_results[:-KEEP_RECENT]
    for _, _, result in to_clear:
        if ...:  # 跳过已精简的和 read_file 的结果
            continue
        result["content"] = f"[Previous: used {tool_name}]"
    return messages

最微妙的设计是那行 PRESERVE_RESULT_TOOLS = {"read_file"}

为什么保留 read_file 的结果? 因为读文件是"参考材料"------模型读了一个文件的内容,后面写代码时可能还要引用。你把它压缩了,模型就得重新读一遍。

而 bash 命令的输出、写文件的结果------这些用完了就可以丢了。

这体现了一个深刻的原则:

压缩不是简单地删东西。压缩是要理解什么东西对模型还有用。


Layer 2: Auto Compact ------ 超阈值时的核弹

当 token 估算超过 50,000,auto_compact 启动:

python 复制代码
THRESHOLD = 50000

def auto_compact(messages: list) -> list:
    # 1. 保存完整对话到磁盘
    transcript_path = TRANSCRIPT_DIR / f"transcript_{int(time.time())}.jsonl"
    with open(transcript_path, "w") as f:
        for msg in messages:
            f.write(json.dumps(msg, default=str) + "\n")
    print(f"[transcript saved: {transcript_path}]")
  
    # 2. 让模型总结对话
    conversation_text = json.dumps(messages, default=str)[-80000:]
    response = client.messages.create(
        model=MODEL,
        messages=[{"role": "user", "content":
            "Summarize this conversation for continuity. Include: "
            "1) What was accomplished, 2) Current state, 3) Key decisions made."
            "Be concise but preserve critical details.\n\n" + conversation_text}],
        max_tokens=2000,
    )
    summary = extract_text(response)
  
    # 3. 用摘要替换全部消息
    return [
        {"role": "user", "content": f"[Conversation compressed. Transcript: {transcript_path}]\n\n{summary}"},
    ]

这个方法有四个要点:

要点 1:存档到 .transcripts/

压缩前把完整对话写到 .transcripts/transcript_1746000000.jsonl。这是保险------如果需要审计或复盘,数据还在。

要点 2:让模型自己总结自己

这里没有用外部的摘要算法。就是同一个模型,读自己的对话历史,然后写摘要。让他总结三个东西:

  1. 完成了什么
  2. 当前状态
  3. 关键决定

要点 3:一行摘要替换所有

压缩后,messages 只剩下一个 [{user: 摘要}]。上下文从 50K 骤降到几千。

但代价是------模型丢失了所有细节。这就是为什么要有存档。

要点 4:消息里带上存档路径

复制代码
[Conversation compressed. Transcript: .transcripts/transcript_1746000000.jsonl]

完成了数据库接口的抽取。当前正在实现 Repository 类。
决定使用 SQLAlchemy 作为 ORM,放弃手写 SQL。

模型知道刚才压缩了,也知道档案在哪。必要的时候,它可以读档案来恢复细节。


Layer 3: Compact Tool ------ 模型主动说"我内存不够了"

第三层是手动触发的。模型自己决定什么时候需要压缩:

python 复制代码
TOOLS = [
    ...
    {"name": "compact", "description": "Trigger manual conversation compression.",
     "input_schema": {"type": "object", "properties": {
         "focus": {"type": "string", "description": "What to preserve in the summary"}
     }}},
]

当一个模型说"我觉得聊天变得越来越慢了,可能是 token 太多了"------开个玩笑,模型不会这么说。但在进行了一大段探索性工作后,模型可能会意识到自己写了很多中间代码在上下文里,而这些东西已经不需要了。

此时模型可以调 compact,Harness 检测到后立刻执行:

python 复制代码
if block.name == "compact":
    manual_compact = True
# ... 其他工具 ...
if manual_compact:
    print("[manual compact]")
    messages[:] = auto_compact(messages)

注意 messages[:] = ... 这个切片赋值------它直接替换了外层变量的全部内容。 这是 Python 里"修改引用所指的全部内容"的标准做法。

三层压缩一起工作时的生命周期:

复制代码
Round 1-10: Tool results 越来越多
            每轮 micro_compact 把 3 轮前的旧结果替换成占位符
Round 11:   Token 估算 > 50K → auto_compact 触发
            存档 + 摘要 + 替换 messages
Round 12:   从摘要继续工作
            模型如果需要细节,可以读存档
Round 30:   Token 又 > 50K → 再次 auto_compact
            ...
Round 50:   模型主动调 compact → 手动压缩
            ...

理论上,这个过程可以无限重复。 这就是"无限会话"的秘密。


Identity Re-injection:压缩后的自我认知

s06 没有处理这个问题,但 s11 补上了------上下文压缩后,模型可能会忘记自己是谁。

你是一个叫 "coder"、role 是 "backend" 的 Agent,你在团队 "my-team" 里。压缩后:

复制代码
[Conversation compressed. Transcript: ...]
完成了后端 API 开发。当前正在等待 review。

但如果这里缺少了身份信息,模型醒来后可能一脸茫然:"我是谁?我在哪?我在干嘛?"

s11 的解法是 identity re-injection

python 复制代码
def make_identity_block(name: str, role: str, team_name: str) -> dict:
    return {
        "role": "user",
        "content": f"<identity>You are '{name}', role: {role}, team: {team_name}. Continue your work.</identity>",
    }

在压缩后的消息列表最前面插入这个块:

python 复制代码
if len(messages) <= 3:
    messages.insert(0, make_identity_block(name, role, team_name))
    messages.insert(1, {"role": "assistant", "content": f"I am {name}. Continuing."})

这就像是你午睡醒来后看了一眼自己的工作牌:"哦对,我是张三,我在做后端开发。"然后继续干活。


和 Subagent 的配合

压缩和子代理是天生一对:

  • Subagent 负责把"一次性探索"隔离到独立上下文里,用完即焚 → 减少主上下文的膨胀速度
  • Compression 负责在主上下文不可避免增长时,安全地"瘦身" → 延长会话寿命

一个类比:

复制代码
Subagent  = 用完一次性餐具就扔掉
Compress  = 定期洗碗

两者都不做 → 厨房堆满了脏盘子 → 没法做饭了。


压缩的安全考虑

压缩过程中有一个潜在风险:模型总结时可能丢失关键信息。

s06 用几个机制来缓解:

  1. 完整存档 ------ .transcripts/ 目录保存了原始的 JSONL,任何时候都可以回溯
  2. 三层渐进 ------ 从最轻量的 micro_compact 开始,不给模型造成扰动
  3. 手动控制 ------ compact 工具让模型自己决定何时压缩(如果它知道的话)

但说实话,这些都只是缓解,不是完美解决。信息丢失是压缩的固有代价。这个 repo 没有假装它能完美压缩------它只是承认"上下文总会满,要有办法腾地方"。

这种诚实让人舒服。


Token 估算的"土办法"

最后看一个有趣的细节------s06 怎么估算 token 数的?

python 复制代码
def estimate_tokens(messages: list) -> int:
    """Rough token count: ~4 chars per token."""
    return len(str(messages)) // 4

不是用 tiktoken,不是用 tokenizer API,就是粗暴地 str(messages) 的长度除以 4

这个"土办法"其实很好:

  • 4 chars ≈ 1 token 是对英文 tokenizer 的合理近似
  • 不需要额外依赖
  • 不需要网络请求
  • 够用就行(阈值是 50K,差个几千不致命)

Harness Engineering 的一个原则:不要过度设计。 一个大致差不多的估算就够触发压缩了。精确到个位数的 token 计数没有意义。


下集预告

s01 到 s06 组成了 Agent 的基础设施:循环、工具、规划、子代理、技能、压缩。这基本上就是一个最小可用 Agent 的全部了。

但作者没有停在这里。s07 开始,进入了"企业级"功能:持久化任务系统、后台线程、Agent 团队、状态机协议、自主行动、worktree 隔离。

下一篇,我们看 s08 的后台任务系统------让 Agent 可以"边思考边干活"

下一篇:后台任务 ------ Agent 边推理边等结果

相关推荐
海兰1 小时前
【第32篇】场景示例项目
人工智能·spring boot·状态模式·spring ai
Python私教1 小时前
给 AI 助手装上导航仪:graphify 知识图谱实战,让 Claude Code 秒懂 400 文件项目架构
人工智能
linfengfeiye1 小时前
AI时代的核心技能不是技术,是主动性——Notion产品负责人深度访谈
人工智能·notion
TinTin Land2 小时前
真正的 AI 优先公司:99% 代码由 AI 编写,迭代仅需 1 天
人工智能
icestone20002 小时前
智能客服如何按客户类型切换话术?一套支持“渠道标签 + 用户自选 + 对话推断“的分类架构设计
大数据·人工智能·ai编程
有个人神神叨叨2 小时前
Ontology-Driven Agents(本体驱动智能体)
人工智能
John_ToDebug2 小时前
拆解AI的“五大基础设施”:算力、网络、存储、电力、软件,谁在驱动千亿市值?
网络·人工智能
Pushkin.2 小时前
Symphony:大模型之后的系统范式——从“写代码”到“编排工作”
人工智能
风落无尘2 小时前
我用 LangChain 写了一个带“定速巡航”的向量化工具,发布到 PyPI 了!
人工智能·python·langchain