【从零构建AI Code终端系统】07 -- 记忆管理:窗口满了怎么办

07 -- 记忆管理:窗口满了怎么办

Agent 做到第 40 轮时

上一章结尾说,不管书架管理得多精细,对话本身一直在涨。

具体场景:agent 正在做一个 20 文件的重构任务。每轮读一个文件、改几行、跑一次测试------文件内容、编辑记录、测试日志不断堆入消息历史。到第 40 轮,上下文逼近窗口上限。第 41 轮的请求发出去,API 返回 context length exceeded。任务中断,前 40 轮白费。

消息历史会无限增长,但窗口有限。需要在不丢关键信息的前提下压缩。


铁律:系统前缀不能动

先说清楚"前缀"是什么。每次发给大模型的请求,token 序列最前面是一段固定内容:系统提示词 + 工具定义。这段内容每轮都一模一样,是前缀缓存命中率最高的部分------API 发现请求开头跟上次一样,就复用已缓存的 KV 向量,不用重新计算。

最直觉的压缩做法是滑动窗口------满了就从头砍掉几条消息。但从头砍意味着系统提示词后面紧跟的消息变了,修改点往后的缓存全部失效,每轮重新计算全部输入 token,成本暴涨近一个数量级。

所以整章的设计都在一条约束下展开:系统提示词和工具定义这个"真前缀"永远不动。 后面的对话消息可以动------微压缩把旧工具输出替换为占位符,整体摘要用一段摘要替换大段历史。这些操作会让修改点之后的缓存失效,但最长、最稳定的系统前缀始终命中,代价可控。

为什么前缀缓存这么重要? 大模型处理一次请求,要对每个输入 token 计算 Key-Value 向量(注意力机制的核心计算)。10 万 token 的请求就要算 10 万组 KV。前缀缓存的原理:如果本次请求的前 N 个 token 跟上次完全一样,这 N 组 KV 直接从缓存取,只需计算新增部分。系统提示词 + 工具定义通常占几千到上万 token,每轮一字不差地重复------天然的缓存热区。保住它,每轮只算新增的对话;破坏它,10 万 token 从头算起。

约束确立后,三个压缩手段按从轻到重的顺序依次上场。


不够时
仍逼近上限
微压缩

每轮自动

旧输出 → 占位符
大输出截断

即时拦截

超大输出 → 存盘 + 预览
整体摘要

窗口快满时

全部对话 → 摘要 + 最近几条


第一道防线------旧输出自动退化

对话里最胖的是工具输出------文件内容、搜索结果、命令日志。但这些输出的价值随时间衰减:10 轮前读过的文件,模型早已提取关键信息,原始文本没用了。

复制代码
beforeModel(state):
    compactable = [msg for msg in messages
                   if msg is ToolMessage
                   and msg.name in {"bash", "read_file", "grep", "glob"}]

    if len(compactable) <= KEEP_RECENT:
        return

    for msg in compactable[:-KEEP_RECENT]:     // 保留最近 3 条不动
        if len(msg.content) > 4000:
            msg.content = "[已压缩 - 需要时可重新获取]"

保留最近几条原样不动,更早的替换为一行占位符。文件读取和搜索都是幂等操作------需要时重新调用就行,信息从"内存"退回"磁盘"。这一层每轮静默执行,用户无感知。


第二道防线------超大输出先存盘

日常卫生只减缓增长,碰到 cat huge-log.txt 这种几十万字符的输出,第一道防线来不及反应------消息还没变"旧",窗口就爆了。所以在工具返回的那一刻就拦截:

复制代码
wrapToolCall(request, handler):
    result = handler(request)
    if len(result.content) <= MAX_OUTPUT_CHARS:
        return result
    path = save_to_disk(result.content)
    preview = result.content[:2000]
    return preview + "\n[完整输出已存盘: " + path + "]"

模型拿到路径后按需读取特定片段。这道防线解决的是单次爆炸------不截断,一条命令就能耗尽整个窗口。但两道防线都只控制增量,一个持续 60 轮的长任务,累积量还是会逼近上限。


最后手段------整体摘要

当上下文逼近窗口上限,系统调一个摘要模型,把全部对话历史压成一段按时间线排列的工作摘要:

复制代码
compactConversation(messages):
    transcript = save_to_disk(messages)      // 归档完整对话
    summary = summarize_model(messages)      // 生成因果链摘要
    tail = messages[-5:]                     // 最近 5 条原样保留

    messages.clear()
    messages.push(summary, ack, ...tail)

摘要的核心要求是保留因果链------"先做了 A,A 因为 Y 失败了,所以改做了 B"。agent 需要知道为什么走到了这一步,不只是当前状态。最近几条原样保留,因为它们是正在进行的工作------连这些也压掉,模型会对"刚刚在做什么"失去感知。

压缩前完整对话先归档到磁盘------信息从窗口中消失,但不永久丢失。这一层平时不触发,触发一次续命数十轮。


压缩的代价

三道防线让对话延续更久,但压缩有一个本质矛盾------压缩就是遗忘,而有些东西不该被遗忘。

假设 agent 列了 20 步重构计划,做到第 12 步时摘要触发。"步骤 7:重构用户服务的数据库查询"变成"之前完成了一些数据库相关的工作"。精确的计划条目被压成模糊记忆。

根源在于消息历史同时承担了两个角色------对话记录状态载体。压缩对话记录合理(旧的调试日志确实不重要),但寄生在消息里的状态会被连带压缩。第 04 章的待办清单正是如此:条目通过消息传递给模型,一旦那条消息被摘要化,状态就断裂了。

出路是把需要持久可见的状态从消息历史中抽离,存到压缩碰不到的地方,每轮重新注入。


相关推荐
Lw老王要学习2 小时前
Windows 下 Miniconda 安装与 conda 命令无法识别问题解决指南
windows·llm·conda·agent
杨天宇ttx16 小时前
Agent 自学指南1 - 别只会"Hi"了:给大模型装上手脚,5分钟变身 Agent
agent
组合缺一18 小时前
赋予 AI Agent “无限续航”:语义保护型上下文压缩技术解析
人工智能·ai·llm·agent·solon·solon-ai
belldeep1 天前
AI agent:介绍 ZeroClaw 安装,使用
人工智能·ai·agent·zeroclaw
XLYcmy1 天前
智能体大赛 实现逻辑 “检索先行”的闭环工作流
数据库·ai·llm·prompt·agent·rag·万方
智慧地球(AI·Earth)1 天前
在Windows上使用Claude Code并集成到PyCharm IDE的完整指南
ide·人工智能·windows·python·pycharm·claude code
26岁的学习随笔1 天前
【Claude Code】我给 Claude Code 做了个桌面启动器 —— 内置道家呼吸引导的悬浮路径工具
c#·开源项目·winforms·桌面工具·claude code
香芋Yu1 天前
【从零构建AI Code终端系统】02 -- Bash 工具:一切能力的基础
开发语言·bash·agent·claude
@atweiwei1 天前
Rust 实现 LangChain
开发语言·算法·rust·langchain·llm·agent·rag