芥末
发布于 2025-12-09 / 0 阅读
0
0

AI Agent 上下文清洗:摘要、工作流记忆与工具输出缓存

AI Agent 运行时间一长,真正拖垮它的往往不是模型能力,而是上下文越来越脏。

一个典型的 Agent 对话历史会逐渐变成这样:

system prompt
tool descriptions

user prompt

assistant: 计划调用工具
tool call 1
tool result 1

assistant: 根据结果继续调用工具
tool call 2
tool result 2

assistant: 发现问题,继续搜索
tool call 3
tool result 3

...

如果任务简单,这种结构没有问题。麻烦出现在复杂任务里:Agent 可能连续调用几十次工具,搜索网页、读取文件、分析错误日志、抓取页面内容、修改代码、再运行测试。越往后,前面的很多工具结果已经不再重要,但它们仍然被塞在大语言模型 LLM(Large Language Model)的上下文窗口里。

这会带来三个直接问题:

问题表现后果
上下文窗口被占满历史工具结果、网页内容、日志反复进入请求更早的关键信息被挤掉
推理负担变大模型每轮都要在大量低价值文本里找重点更容易遗漏用户约束或当前目标
token 成本升高每次调用都携带大量历史内容延迟和费用一起上升

上下文工程要解决的核心问题很简单:只把当前决策需要的信息放进 LLM 上下文,把暂时不需要但可能有用的信息放到外部,并保留可恢复路径。

这里有一个关键点:清理上下文不是粗暴删除。粗暴删除会让 Agent 在后续步骤需要某段历史时找不回来,造成信息丢失。更稳妥的做法是把上下文拆成多层:当前上下文、摘要历史、外部工作流记忆、工具输出缓存。

flowchart TD
    A[完整对话与工具调用] --> B{是否还需要逐字保留}
    B -->|当前推理需要| C[保留在 LLM 上下文]
    B -->|历史过程有价值| D[压缩成上下文摘要]
    B -->|可复用任务经验| E[保存为工作流记忆]
    B -->|内容很大但可能要回查| F[工具输出缓存]
    F --> G[用唯一 ID 检索完整结果]

围绕这个目标,可以把 Agent 的上下文清洗拆成三类技术:上下文摘要、工作流记忆、工具输出缓存。


1. 上下文摘要:把支线任务压回背景里

Agent 做复杂任务时,经常会被一个局部问题拖走。

例如,让 Agent 构建一个文本转表情应用,要求包括:

  • 接收用户输入文本;
  • 调用文本生成图像模型;
  • 展示生成的表情;
  • 把结果写入 PostgreSQL 数据库;
  • 加入用户登录能力。

任务进行到一半,图像写入数据库失败。Agent 开始查错误、搜版本差异、看文档、尝试不同写法。这个调试过程可能持续十几轮工具调用。等问题修完,LLM 上下文里已经堆满了数据库错误、搜索结果和调试日志,反而把“要构建完整应用”这个主目标冲淡了。

上下文摘要的作用,就是在这种时候把对话历史压缩成当前仍然有用的信息。

flowchart LR
    A[主任务: 构建应用] --> B[遇到数据库写入 bug]
    B --> C[搜索资料]
    C --> D[读取文档]
    D --> E[尝试修复]
    E --> F[确认根因]

    F --> G[生成摘要]
    G --> H[保留: 主目标、已完成部分、根因、修复方式、剩余任务]
    G --> I[丢弃: 冗余搜索结果、重复报错、无效尝试]

摘要不是简单地“变短”,而是要保留任务继续执行所需的状态。一个好的摘要至少应该包含这些内容:

摘要字段作用
用户目标防止 Agent 忘记最初要完成什么
明确约束例如技术栈、输出格式、不能使用的方案
已完成工作避免重复做已经完成的步骤
当前进展记录任务正处在哪个阶段
未完成事项让 Agent 能继续推进
关键发现例如 bug 根因、接口限制、环境差异
决策记录为什么选择某个方案,为什么放弃某个方案

可以把摘要格式设计成结构化 Markdown:

## 用户目标
构建一个文本转表情应用,支持文本输入、图像生成、结果展示、PostgreSQL 存储和用户登录。

## 已完成工作
- 已完成基础 UI(用户界面)页面。
- 已接入文本生成图像接口。
- 已实现表情预览。

## 关键问题与结论
- 图像无法写入 PostgreSQL 的原因是字段类型不适合直接存储二进制内容。
- 更稳妥的方式是把图像保存到对象存储或文件系统,只在数据库中保存 URL 和元数据。

## 剩余任务
- 调整数据库 schema。
- 修改保存逻辑。
- 增加登录后的用户资源隔离。
- 补充失败重试和错误提示。

什么时候触发摘要

摘要触发得太早,会丢掉还没处理完的细节;触发得太晚,上下文已经被污染。常见触发条件有四类:

触发条件适用场景
token 使用量超过阈值例如上下文窗口使用超过 70% 或 80%
一个子任务结束bug 修复完成、资料搜索完成、文件分析完成
Agent 明显偏离主任务大量轮次集中在支线问题上
需要跨会话继续把当前会话状态传给新的 Agent 或下一次运行

自动摘要可以放在 Agent 运行循环里:

def maybe_summarize_context(agent_state, llm):
    used_tokens = count_tokens(agent_state.messages)
    limit = agent_state.context_limit

    if used_tokens / limit < 0.8:
        return agent_state

    protected_messages = select_protected_messages(agent_state.messages)
    old_messages = select_messages_to_summarize(agent_state.messages)

    summary = llm.generate(
        system="你是上下文压缩器,只保留继续完成任务所必需的信息。",
        user=build_summary_prompt(old_messages)
    )

    agent_state.messages = (
        protected_messages.system_and_tools
        + protected_messages.recent_user_constraints
        + [{"role": "assistant", "content": summary}]
        + protected_messages.recent_messages
    )

    return agent_state

这里有两个细节很重要。

第一,系统提示、工具描述、最近的用户约束不能随便摘要掉。系统提示决定 Agent 的行为边界,工具描述决定它能调用什么,最近的用户消息往往包含最新要求。

第二,不要完全依赖 LLM 摘要。摘要可能遗漏细节,也可能把不确定信息写得过于肯定。更稳妥的做法是保留少量高价值原始消息,例如最近几条用户指令、最新工具结果、关键错误信息,再把更早的大段历史压缩掉。


2. 工作流记忆:把一次任务中的经验变成可复用策略

上下文摘要解决的是“当前会话怎么继续”。工作流记忆解决的是另一个问题:未来遇到类似任务时,Agent 能不能少走弯路。

例如,让 Agent 搜集机器学习数学方向的免费书籍,并整理成 CSV(Comma-Separated Values,逗号分隔值)文件。任务过程中,Agent 发现 archive.org 上部分书籍无法直接读取,于是尝试不同访问路径、搜索镜像、调整解析方式,花了几分钟才找到有效方法。

这段探索过程如果只留在当前上下文里,任务结束后就没了。下次再做类似资料整理,Agent 还会重新踩一遍坑。

工作流记忆要保存的不是完整对话,而是可复用的任务经验:

flowchart TD
    A[执行任务] --> B[遇到失败]
    B --> C[尝试修复]
    C --> D[找到有效策略]
    D --> E[任务完成]
    E --> F[生成工作流记忆]
    F --> G[未来相似任务加载]
    G --> H[直接采用已验证策略]

工作流记忆应该记录什么

工作流记忆不能太细,否则只适用于同一个任务;也不能太粗,否则对未来没有帮助。比较实用的结构如下:

字段说明
任务标题用简短通用的标题描述任务类型
任务描述只写用户想要的结果,不写太多实现细节
解决步骤用有序步骤记录可复用操作
使用工具记录工具名称、用途、为什么有帮助
失败与恢复策略保存错误症状、原因、修复方式、验证方法
备注与观察保存未来执行时必须知道的特殊点
标签用于匹配类似任务,例如 web-researchcsv-generation

一个工作流记忆可以写成这样:

# workflow: collect-free-ml-math-books

## 任务描述
收集机器学习数学方向的免费书籍,整理每本书的描述、主题、前置知识、访问链接,并导出为 CSV 文件。

## 解决步骤
1. 确定书籍筛选标准:免费、可公开访问、与机器学习数学基础相关。
2. 从多个来源搜索候选书籍,包括大学课程页、开放教材站点、电子书归档站点。
3. 验证每个链接是否可访问,优先保留官方页面或稳定镜像。
4. 提取书籍主题、适合人群、前置知识和简介。
5. 统一字段格式并生成 CSV。
6. 对 CSV 做重复项检查和链接可用性检查。

## 使用工具
- search_web:发现候选书籍和官方页面。
- fetch_page:读取网页正文与元信息。
- write_file:生成 CSV 文件。
- validate_url:检查链接可访问性。

## 失败与恢复策略
- 症状:archive.org 页面无法直接读取正文。
  - 可能原因:页面结构复杂或需要不同资源路径。
  - 修复方式:优先查找书籍详情页、PDF 直链或官方镜像。
  - 验证方式:确认链接无需登录即可访问。

## 标签
web-research, open-education, csv-generation, machine-learning, math-books

工作流记忆和 RAG 不是一回事

RAG(Retrieval-Augmented Generation,检索增强生成)通常会把大量文档切块、向量化,再根据相似度检索。工作流记忆不一定需要这么复杂。

如果每个 Agent 只维护少量高质量记忆文件,用文件名、角色名、标题、描述和标签就能完成匹配。这样更可控,也减少了向量检索带来的不确定性。

一个简单加载流程可以这样设计:

flowchart LR
    A[新任务输入] --> B[读取当前 Agent 的工作流记忆索引]
    B --> C[展示标题、描述、标签]
    C --> D[LLM 选择最多 N 个相关工作流]
    D --> E[加载完整工作流内容]
    E --> F[注入当前上下文]
    F --> G[执行任务]

对应的索引可以非常简单:

[
  {
    "id": "collect-free-ml-math-books",
    "title": "收集免费机器学习数学书籍",
    "description": "搜索公开可访问书籍并整理为 CSV",
    "tags": ["web-research", "csv-generation", "machine-learning"]
  },
  {
    "id": "weekly-slack-reminder",
    "title": "创建 Slack 每周会议提醒",
    "description": "根据会议时间配置周期提醒",
    "tags": ["slack", "automation", "calendar"]
  }
]

当工作流记忆越来越多时,应该先考虑清理和合并,而不是立即引入复杂检索。工作流记忆的价值来自“少量、准确、可复用”,不是把所有历史都存下来。


3. 工具输出缓存:用 ID 替代大段过期结果

工具调用是 Agent 的核心能力,但工具输出也是上下文污染的主要来源。

搜索工具可能返回几千字符的搜索结果;文件读取工具可能返回上万字符的 Markdown 文档;浏览器自动化工具可能返回整个网页的 DOM(Document Object Model,文档对象模型)。如果这些结果永久留在上下文里,每一轮 LLM 调用都会重复携带它们。

# 搜索工具返回大量结果
tool_result_1 = search_google("AI agent frameworks")
# 可能包含多个标题、摘要、URL、元数据

# 文件工具读取长文档
tool_result_2 = read_file("documentation.md")
# 可能返回上万字符 Markdown

# 浏览器工具读取页面结构
tool_result_3 = browser_snapshot()
# 可能返回完整 DOM 或复杂可访问性树

很多工具结果只在刚返回时最有用。Agent 看过、提取完关键信息后,完整结果就不需要一直留在上下文里。工具输出缓存的思路是:

  1. 大工具结果保存到上下文外部,例如磁盘、对象存储或数据库;
  2. 为每个结果生成唯一 ID;
  3. 上下文中只保留摘要、预览和 ID;
  4. 如果后续确实需要完整内容,Agent 通过检索工具按 ID 取回。
sequenceDiagram
    participant Agent
    participant Tool as 工具
    participant Store as 外部缓存
    participant LLM

    Agent->>Tool: 调用搜索/读取/浏览器工具
    Tool-->>Agent: 返回大段结果
    Agent->>Store: 保存完整结果并生成 tool_result_id
    Agent->>LLM: 注入预览、摘要、ID、检索说明
    LLM-->>Agent: 判断是否需要完整结果
    Agent->>Store: 必要时按 ID 取回
    Store-->>Agent: 返回完整工具输出

上下文里可以保留这样的引用:

工具结果已缓存。

- id: tool_result_20260607_001
- tool: search_google
- query: AI agent frameworks
- size: 4250 characters
- preview: "Top results include CAMEL, LangGraph, AutoGen..."
- summary: 搜索结果主要包含多个 Agent 框架的项目页、文档页和对比文章。
- retrieve: 如需完整搜索结果,调用 `load_tool_result("tool_result_20260607_001")`。

一个简化版实现如下:

from dataclasses import dataclass
from pathlib import Path
import hashlib
import json
import time

@dataclass
class CachedToolResult:
    result_id: str
    tool_name: str
    preview: str
    summary: str
    path: str
    size: int

class ToolOutputStore:
    def __init__(self, root: str = ".agent_tool_cache"):
        self.root = Path(root)
        self.root.mkdir(exist_ok=True)

    def save(self, tool_name: str, content: str, summary: str) -> CachedToolResult:
        digest = hashlib.sha256(content.encode("utf-8")).hexdigest()[:12]
        result_id = f"{tool_name}_{int(time.time())}_{digest}"
        path = self.root / f"{result_id}.txt"

        path.write_text(content, encoding="utf-8")

        return CachedToolResult(
            result_id=result_id,
            tool_name=tool_name,
            preview=content[:160],
            summary=summary,
            path=str(path),
            size=len(content),
        )

    def load(self, result_id: str) -> str:
        matches = list(self.root.glob(f"{result_id}.txt"))
        if not matches:
            raise FileNotFoundError(result_id)
        return matches[0].read_text(encoding="utf-8")

对消息列表做压缩时,可以只处理较旧且较大的工具结果:

def compact_tool_results(messages, store, max_inline_chars=2000, keep_recent=2):
    tool_message_indexes = [
        i for i, msg in enumerate(messages)
        if msg.get("role") == "tool"
    ]

    recent_tool_indexes = set(tool_message_indexes[-keep_recent:])

    for i in tool_message_indexes:
        if i in recent_tool_indexes:
            continue

        content = messages[i].get("content", "")
        if len(content) <= max_inline_chars:
            continue

        summary = summarize_tool_output(content)
        cached = store.save(
            tool_name=messages[i].get("name", "unknown_tool"),
            content=content,
            summary=summary,
        )

        messages[i]["content"] = {
            "cached": True,
            "result_id": cached.result_id,
            "tool_name": cached.tool_name,
            "size": cached.size,
            "preview": cached.preview,
            "summary": cached.summary,
            "instruction": f"需要完整内容时调用 load_tool_result('{cached.result_id}')"
        }

    return messages

工具输出缓存的风险

工具输出缓存最容易犯的错误,是只看 token 节省,不看准确性。

缓存之后,Agent 看到的是摘要和预览,而不是完整内容。它可能误以为预览已经足够,然后基于不完整信息做决定。更糟的是,Agent 还要额外记住哪些 ID 对应哪些结果、什么时候该取回完整内容,这会增加认知负担。

风险例子缓解方式
信息丢失完整搜索结果里第 8 条才是正确答案,预览只展示前几条缓存时生成结构化摘要,保留关键候选项
错误判断Agent 认为不用读取完整文件,直接根据摘要修改代码当操作依赖细节时强制取回完整内容
ID 混乱多个工具结果相似,Agent 调错缓存 IDID 中包含工具名、时间、hash,并保留 query/path
缓存过度小结果也被替换成引用,反而增加操作复杂度设置长度阈值,只缓存大结果
最新状态丢失最近一次测试输出被缓存,Agent 看不到失败细节保留最近 N 个工具结果原文

比较稳妥的策略是:

  • 最近一两次工具结果保持完整;
  • 超过阈值的大结果才缓存;
  • 缓存引用必须包含摘要、预览、来源、大小、检索方式;
  • 一旦需要基于被省略细节做决策,必须取回完整结果;
  • 对测试日志、错误堆栈、数据库迁移结果等高风险内容要更保守。

在工程实践里,工具输出缓存不能只作为“压缩功能”存在,它还需要配套评估。至少要比较两组 Agent:一组保留完整工具结果,一组启用缓存,观察任务成功率、错误率、调用次数和 token 成本。只有成本下降且成功率不明显受损,缓存策略才算可靠。


4. 三种方法的分工

上下文摘要、工作流记忆、工具输出缓存看起来都在“减少上下文”,但它们处理的是不同类型的信息。

方法处理对象保存形式主要目标典型触发点
上下文摘要当前会话历史结构化摘要保留当前任务状态token 接近上限、子任务结束
工作流记忆已完成任务经验Markdown / JSON 记忆文件复用未来相似任务策略任务完成后、新任务开始前
工具输出缓存大段工具结果外部存储 + ID 引用避免工具结果污染上下文工具结果过大、结果已被处理
原文保留当前关键消息LLM 上下文保证最新信息不丢最近几轮对话、关键用户约束

一个完整的 Agent 上下文管理流程可以设计成这样:

flowchart TD
    A[用户输入任务] --> B[加载相关工作流记忆]
    B --> C[LLM 规划下一步]
    C --> D[调用工具]
    D --> E[获得工具结果]
    E --> F{结果是否过大}
    F -->|是| G[保存完整结果到外部缓存]
    G --> H[上下文保留摘要、预览、ID]
    F -->|否| I[直接保留结果]
    H --> J{上下文是否接近阈值}
    I --> J
    J -->|是| K[生成上下文摘要]
    J -->|否| L[继续执行]
    K --> L
    L --> M{任务是否完成}
    M -->|否| C
    M -->|是| N[生成或更新工作流记忆]

这套流程的重点不是把上下文压到越短越好,而是让不同信息进入正确的位置:

  • 当前必须参与推理的信息,留在上下文;
  • 当前不需要逐字阅读、但影响任务状态的信息,变成摘要;
  • 未来可复用的经验,沉淀为工作流记忆;
  • 可能需要回查的大块原始资料,放进工具输出缓存。

5. 实现时容易踩的坑

摘要提示太模糊

如果只告诉模型“总结以上内容”,得到的结果通常不稳定。摘要提示应该明确字段、保留标准和删除标准。

可以要求模型按固定结构输出:

请把对话历史压缩为任务继续执行所需的状态,不要写无关过程。

必须保留:
1. 用户目标和明确约束
2. 已完成工作
3. 当前正在处理的问题
4. 关键决策及原因
5. 未完成事项
6. 会影响后续执行的错误、限制和外部依赖

必须删除:
1. 重复搜索结果
2. 已验证无效的尝试细节
3. 与任务目标无关的闲聊
4. 不影响后续执行的中间输出

工作流记忆太像流水账

工作流记忆不是运行日志。不要保存“第 3 轮搜索了什么、第 4 轮点了哪个页面”这种细节,而要保存“遇到类似站点读取失败时,优先查找 PDF 直链或官方镜像”这种可迁移策略。

判断一条记忆是否值得保存,可以问两个问题:

  1. 未来相似任务会不会再次遇到这个问题?
  2. 这条经验能不能减少未来探索成本?

如果答案都是否定的,就不应该写入工作流记忆。

工具缓存没有恢复机制

只把工具结果替换成“已省略”是危险的。缓存必须提供可调用的恢复工具,例如:

def load_tool_result(result_id: str) -> str:
    """根据工具结果 ID 读取完整输出。"""
    return tool_output_store.load(result_id)

还要在 Agent 的系统提示中明确规则:

当回答、修改代码、生成结论依赖某个已缓存工具结果的完整细节时,
必须先调用 load_tool_result(result_id) 读取完整内容,
不能只根据 preview 或 summary 做最终判断。

没有评估就上线压缩策略

上下文清洗会改变 Agent 看到的信息,因此它不是纯粹的性能优化。每一种压缩策略都应该评估:

指标关注点
任务成功率清洗后是否更容易失败
平均 token 消耗是否真的降低上下文成本
平均工具调用次数是否因为信息丢失导致反复检索
任务完成时间是否因频繁摘要或回查变慢
错误类型是否出现“根据不完整信息决策”的新错误

如果 token 降了但成功率也降了,就需要调整摘要字段、缓存阈值或恢复策略。


6. 推荐的上下文分层设计

把 Agent 记忆设计成四层,会比只维护一个巨大消息列表更稳。

层级内容生命周期是否进入 LLM 上下文
活跃上下文最近用户指令、最新工具结果、当前计划当前几轮
压缩摘要主目标、已完成事项、关键约束、剩余任务当前任务
工具缓存搜索结果、长文件、网页 DOM、大日志当前任务或短期默认否,需要时按 ID 取回
工作流记忆可复用步骤、失败恢复策略、工具使用经验长期新任务匹配后进入

这也是上下文工程最实用的原则:不要让所有信息都挤在同一个窗口里。

LLM 上下文适合放“马上要用来推理的信息”;外部存储适合放“可能要回查的原始资料”;长期记忆适合放“以后还能复用的方法”。当这三类信息被混在一起,Agent 就会在无关细节里消耗注意力;当它们被拆开管理,Agent 更容易围绕当前目标行动。


评论