一、背景

1.1 从提示工程到上下文工程

这几年我们从“提示工程”一路走到“上下文工程”。前者是在 2022 年底 ChatGPT 爆火后流行起来,大家忙着研究怎么给聊天模型下指令;到了 2024 年中,随着“智能体之年”的到来,大家发现真正棘手的是怎么管理越来越庞杂的对话与工具调用信息,于是“上下文工程”成了新的主角。

1.2 智能体带来的新问题

智能体的工作方式很直白:LLM 绑上一堆工具,在循环里自己决定怎么调,调一次就把结果塞回消息历史;时间一长,历史就像雪球一样越滚越大。现实里,一个看似普通的任务可能就要跑 50 次工具调用,线上智能体动辄上百轮对话,每次还会吐出大段文本,占掉海量 token——上下文很快就被挤满了。

二、核心挑战

2.1 上下文爆炸

所谓“上下文爆炸”,说的就是这些调用和消息按时间不断堆叠:工具输出一条条追加,消息列表一路拉长,模型每次都要背上更重的历史包袱,结果上下文窗口很快被塞满。

2.2 上下文腐烂(Context Rot)

上下文一旦过大,效果就开始“变味”:回答变得重复啰嗦、推理明显变慢、整体质量下滑。Cognition 的报告把这叫做“上下文腐烂”,在实战中我们也经常能直观感受到它正在发生。

2.3 根本悖论

矛盾就在这里:智能体需要靠丰富的上下文来做出靠谱的决策,但上下文越多,模型越容易掉速、走神。上下文工程要做的,就是在窗口里放进“刚刚好”的信息——够用、不浪费,同时给后续的检索与扩展留出空间。

三、解决方案

业界已经形成了五种主要的上下文管理策略:

3.1 上下文卸载(Context Offloading)

简单说,卸载就是把不需要长时间待在消息历史里的大块信息搬到外部去存着,需要时再拿回来用。最常见的做法是把工具输出写到文件系统里,消息里只留个最小必要的引用(比如文件路径);像网页搜索结果、超长工具输出、临时计划这类特别占 token 的内容,都适合这样处理。好处很直接:上下文更干净,占用更小,信息不丢,随取随用,也是生产场景里最常见的实践。

3.2 上下文缩减(Context Reduction)

当卸载还不够时,就轮到“缩减”上场了;它主要有两条路:可逆的“压缩”和不可逆的“摘要”。思路是优先把能外部重建的细节挪走、格式变紧凑,再在必要时对保留内容做更狠的摘要,以在不过度丢信息的前提下把上下文体积降下来。

3.2.1 压缩(Compaction)

压缩的核心思想其实很简单,就是把信息从完整格式转换成紧凑格式,把那些可以从外部状态重建的信息剥离掉。比如说,一个写入文件的工具返回结果可能是 {path: "/file.txt", content: "很长的内容..."},压缩后我们就只保留 {path: "/file.txt"},因为文件已经存在了,需要的时候通过路径重新读取就行。这种方式的优点在于它是完全可逆的,信息并没有真正丢失,只是被外部化了,可以随时恢复,同时还能大幅减少 token 占用,特别适合处理工具调用结果。在实际操作中,我们通常会压缩最旧的工具调用,比如最旧的50%,同时保留最新的工具调用的完整细节,这样模型仍然有最新的、清晰的例子来了解如何正确使用工具,否则模型可能会模仿那些缺少字段的紧凑格式,那就完全错了。

工具结果清除(Tool Result Clearing)是压缩的自动化实现方式,由服务端自动执行,这是生产级智能体系统的标准做法,Claude 4.5 等模型已经内置支持了。它的工作机制是这样的:当上下文超过配置阈值时,会自动按时间顺序清除最旧的工具结果,用占位符文本替换被清除的内容,让模型知道内容已被移除,默认只清除工具结果,保留工具调用(参数),可选同时清除工具调用和结果,整个过程在服务端自动执行,客户端保持完整历史记录,无需同步状态。举个例子,假设一个智能体执行了多次网络搜索和文件操作,当上下文达到阈值(如 30K tokens)时,工具结果清除会自动清除最旧的工具结果,用 [工具结果已清除] 这样的占位符替换,保留最近的工具调用的完整结果,同时保留所有工具调用的参数,这样模型仍然知道执行了什么操作。这种方式的智能之处在于它会保留最近 N 个工具使用/结果对,确保模型有最新的使用示例,同时考虑与提示缓存的交互来优化成本效益,还可以指定某些工具的结果永不清除来保护重要上下文,并且确保清除操作值得缓存失效的成本。

参考文档:Claude Context Editing 官方文档

3.2.2 摘要(Summarization)

摘要就是对上下文进行摘要或压缩,生成更短的版本,你可以对工具调用输出进行摘要,也可以对完整消息历史进行摘要。但这里有个关键区别,就是摘要和压缩不一样,压缩是可逆的,它直接移除内容,用占位符替换或保留路径引用,需要的时候可以通过检索恢复,而摘要是不可逆的,它把内容压缩成更短版本,信息可能会丢失。所以在使用摘要的时候要特别谨慎,最佳实践是在摘要前先把关键部分卸载到文件,使用完整版本进行摘要而不是压缩版本,保留最后几次工具调用的完整细节,这样模型知道它上次停在了哪里,能更顺利地继续下去,否则你会发现摘要之后模型有时会改变风格和语气。另外,要识别出"腐烂前阈值"(通常 128K-200K token)作为触发点,当上下文接近这个阈值时才考虑摘要。

3.2.3 压缩和摘要的选择策略

压缩和摘要这两种方式各有特点,压缩是完全可逆的,适合工具结果可从外部重建的场景,实现难度中等,但如果用工具结果清除这样的自动化实现,难度就很低了,而摘要则是不可逆的,信息可能丢失,实现难度较高,只有在需要进一步压缩时才考虑。所以选择策略很明确:优先使用压缩,因为它可逆且安全,特别适合工具结果,包括工具结果清除等自动化实现,只有当压缩还不够的时候,才最后考虑对保留内容进行摘要。在阈值管理上,你需要考虑硬性限制(比如 100万 token)和腐烂前阈值(通常 128K-200K token),当上下文接近阈值时触发缩减,优先压缩,必要时才摘要。实际工作流通常是这样的:上下文增长到达到阈值(比如 30K tokens),第一步先压缩工具结果,保留路径,移除内容,或者使用工具结果清除,如果压缩后上下文仍然接近阈值,那第二步就对保留的内容进行摘要,对历史消息进行摘要。

3.3 上下文检索(Retrieving Context)

检索就是按需把卸载到外部的内容再“拉回视野”。可以走语义搜索那套,用向量索引按相似度找复杂内容;也可以走文件系统这一派,直接用 globgrep 这类简单高效的工具扫文件,尤其适合结构化资料。常见用法像找回研究计划、定位历史工具结果、恢复摘要前的完整上下文等;说到底,检索做得稳,卸载才敢放手,二者配合好,整体管理就高效。

3.4 上下文隔离(Context Isolation)

核心思想:通过多智能体架构,将上下文拆分到不同的子智能体中。

3.4.1 两种模式

上下文隔离有两种截然不同的模式,第一种是"通过通信"(By Communicating),这是更容易理解的一种模式,因为它就是经典的子智能体设置。主智能体编写一个任务指令,然后将该指令发送给子智能体,子智能体的整个上下文仅由该指令组成,子智能体完成任务后返回结果,主智能体并不关心子智能体是如何执行的,只需要结果。这种模式适合任务有清晰简短的指令,并且只有最终输出才重要的场景,比如在代码库中搜索特定片段,它的优势是上下文小、开销低,简单直接,这也是 Cloud Code 通常的做法,它使用任务工具将一个独立、清晰的任务委派给子智能体。

第二种是"通过共享内存"(By Sharing Memory),这意味着子智能体可以看到之前所有的上下文,即所有的工具使用历史,但子智能体有自己的系统提示和自己的动作空间。这种模式适合需要完整历史信息的复杂任务,最终输出依赖大量中间结果的场景,比如深度研究场景,需要大量搜索和笔记,即使你可以把所有笔记和搜索结果保存到文件中,再让子智能体重新读取一切,也只是在浪费延迟和上下文,如果你计算一下 token 的数量,你甚至可能会使用更多的 token 来完成这件事。但要注意,共享上下文是比较昂贵的,因为每个子智能体都有更大的输入需要预填充,这意味着你会在输入 token 上花费更多,而且由于系统提示和动作空间不同,你无法重用 KV 缓存,因此你必须支付全额费用。

3.4.2 选择建议

选择哪种模式其实很简单,对于简单任务,使用通信模式就够了,上下文小、开销低,简单直接,而对于复杂任务,特别是那些需要完整历史信息、最终输出依赖大量中间结果的场景,就应该考虑使用共享内存模式。但要注意,多智能体同步信息可能成为挑战,需要谨慎设计,就像 Cognition 博客中警告的那样,当你有多个智能体时,在它们之间同步信息会变成一场噩梦,但这个问题并不新鲜,在计算机编程的早期,多进程或多线程协调就是一个经典挑战,我们可以借鉴一些其中的智慧。

3.5 缓存上下文(Caching Context)

缓存更像是在给系统安“记忆”,把会反复用到的上下文预先存起来,下次就不用重算、重传,整体效率更高。常见的做法有缓存工具定义、缓存常用查询的结果等;不过要注意,它跟隔离和缩减之间存在权衡:缓存多了能省成本,但也可能牵动上下文的一致性与更新节奏,需要结合场景拿捏分寸。

3.6 分层动作空间(Layered Action Space)

当系统里的工具越堆越多,本身就会吃掉大量上下文,还容易让模型“搞混”到底该用哪一个。分层动作空间的思路是把工具按层级卸载和组织起来:常用且边界清晰的放在函数调用层,更大更重的能力放到沙盒工具层,需要灵活计算与组合的交给包和 API 层;对模型来说入口依然统一,但对系统来说上下文更清爽、误用更少、扩展也更从容。

三层架构

分层动作空间本质上让系统从三个不同级别的抽象中进行选择。第一层是函数调用(Function Calling),这是经典的模式,由于约束解码,它是模式安全的,我们都知道它的缺点,比如会破坏缓存,太多的工具调用可能会引起混淆,所以在 Manis 中,他们目前只使用固定数量的原子函数,例如读写文件、执行 shell 命令、在文件和互联网中搜索,以及一些浏览器操作,这些原子函数有非常清晰的边界,它们可以协同工作以组合成更复杂的工作流。

第二层是沙盒工具(Sandbox Utilities),每个会话都在一个完整的虚拟机沙盒中运行,这意味着可以通过 shell 命令来运行预安装工具,比如格式转换器、语音识别工具,甚至一个特殊的 MCP CLI,通过它来调用 MCP,他们不会将 MCP 工具注入到函数调用空间中,而是在沙盒内通过命令行界面完成所有操作。工具的好处在于,你可以添加新功能而无需触碰模型的函数调用空间,就像你熟悉 Linux 一样,你总是知道如何找到这些新命令,甚至可以运行 --help 来弄清楚如何使用一个新工具,另一个好处是,对于较大的输出,它们可以直接写入文件或分页返回结果,你可以使用所有这些 Linux 工具(如 grepcatlessmore)来即时处理这些结果,这里的权衡是,它非常适合处理大输出,但对于与前端进行低延迟的来回交互来说效果不佳。

第三层是包和 API(Packages and APIs),在这里可以编写 Python 脚本来调用预授权的 API 或自定义包,比如使用一个 3D 设计库进行建模,或调用一个金融 API 来获取市场数据,实际上,很多 API 密钥都预装在系统中,可以直接使用这些密钥访问这些 API。这对于需要大量内存计算但不需要将所有数据推送到模型上下文中的任务是完美的,比如分析一只股票一整年的价格数据,你不会把所有数字都喂给模型,而是应该让脚本进行计算,只把摘要放回上下文中,而且由于代码和 API 具有很强的可组合性,你实际上可以在一步中串联很多事情,比如在一个典型的 API 调用中,你可以在一个 Python 脚本中完成获取城市名称、获取城市 ID、获取天气等所有操作。但要注意,代码不是模式安全的,对代码进行约束解码是非常非常困难的,所以你应该为这些功能找到合适的场景,对于能在编译器或解释器运行时内处理的事情都用代码来完成,否则就使用沙盒工具或函数调用。

好消息是,从模型的角度来看,这三层都通过标准的函数调用进行访问,因此接口保持简单、对缓存友好,并且在函数之间是正交的,因为沙盒工具仍然是通过 shell 函数来访问的,如果你使用第三方应用程序的 API,也只是使用文件函数来读写文件,然后使用 shell 函数来执行它,所以它不会给模型增加额外负担,因为这些都是模型已经训练过并且熟悉的。

四、方案之间的关系

五个维度(卸载、缩减、检索、隔离、缓存)并非独立,而是相互关联:

  • 卸载 + 检索 → 使更高效的缩减成为可能
  • 稳定的检索 → 使隔离变得安全
  • 隔离 → 减慢上下文增长速度,降低缩减频率
  • 隔离 + 缩减 → 影响缓存效率和输出质量

核心原则:上下文工程是一门在多个潜在冲突目标之间取得平衡的艺术与科学。

五、实践建议

5.1 避免过度工程化

一句话:别把上下文管理搞得太花。我们的经验是,最大的提升往往来自于“减法”——尽量简化,能不做就不做,能信任模型就别堆技巧;每当把架构瘦身、移除不必要的层,系统就会更快、更稳、更聪明。记住那条底线:上下文工程的目标是让模型更容易工作,而不是更难。

5.2 关键原则

原则其实不复杂:少构建、多理解,先把问题吃透再动手;做缩减时优先用可逆方案,能压缩就不摘要;时刻盯住上下文的“腐烂前阈值”,别等到性能掉下去才补救;隔离上按任务复杂度选择通信还是共享内存;所有决策都要在性能、成本和质量之间找平衡点。

5.3 实施步骤

落地可以按这个顺序走:先评估现状,量一量上下文长度,找出开始变慢变差的那个阈值;接着优先做卸载,把大的工具输出搬到文件系统,并把检索通路打通;然后上压缩策略,设计好紧凑格式和触发阈值;如果任务复杂度需要,再考虑做隔离,决定用通信还是共享内存;最后持续打磨,边监控边简化,别让方案越做越重。

六、代码实现

6.1 上下文缩减(Context Reduction)

上下文缩减提供三种接口:压缩(Compaction)、摘要(Summary)和融合接口。

6.1.1 压缩接口(Compaction)

功能:将工具调用结果从完整格式转换为紧凑格式,将可重建的信息卸载到外部存储。

输入

{
    "messages": List[Message],      # 完整的消息历史
    "compaction_config": {
        "compress_ratio": 0.5,      # 压缩比例:压缩最旧的 50% 工具调用
        "keep_recent": 3,           # 保留最近 N 个工具调用的完整结果
        "storage_path": "/tmp/context",  # 外部存储路径(文件系统或数据库)
        "exclude_tools": ["web_search"]  # 排除的工具列表(这些工具的结果不压缩)
    }
}

输出

{
    "messages": List[Message],      # 压缩后的消息历史(工具结果被替换为路径引用)
    "offloaded_data": {
        "files": [                  # 卸载到文件系统的数据
            {
                "path": "/tmp/context/tool_1_result.json",
                "tool_call_id": "call_123",
                "original_tokens": 5000,
                "compressed_tokens": 50
            }
        ],
        "db_records": [             # 卸载到数据库的数据(可选)
            {
                "record_id": "rec_456",
                "tool_call_id": "call_124",
                "original_tokens": 3000,
                "compressed_tokens": 30
            }
        ]
    },
    "statistics": {
        "original_tokens": 80000,
        "compressed_tokens": 40000,
        "saved_tokens": 40000,
        "compressed_tool_calls": 10,
        "kept_tool_calls": 3
    }
}

参数说明

参数 类型 说明
compress_ratio float (0-1) 压缩比例,如 0.5 表示压缩最旧的 50% 工具调用
keep_recent int 保留最近 N 个工具调用的完整结果,作为使用示例
storage_path string 外部存储路径,可以是文件系统路径或数据库连接字符串
exclude_tools List[string] 排除的工具名称列表,这些工具的结果不会被压缩

6.1.2 摘要接口(Summary)

功能:对上下文进行摘要,在摘要前将关键部分卸载到文件系统。

输入

{
    "messages": List[Message],      # 完整的消息历史
    "summary_config": {
        "summary_ratio": 0.3,        # 摘要比例:摘要最旧的 30% 消息
        "keep_recent": 5,            # 保留最近 N 条消息的完整内容
        "offload_before_summary": True,  # 是否在摘要前卸载关键部分
        "offload_path": "/tmp/context/pre_summary",  # 摘要前卸载路径
        "dump_full_context": True    # 是否将整个摘要前的上下文转储为日志文件
    }
}

输出

{
    "messages": List[Message],      # 摘要后的消息历史
    "offloaded_data": {
        "key_parts": [              # 摘要前卸载的关键部分
            {
                "path": "/tmp/context/pre_summary/key_info_1.json",
                "content_type": "tool_results",
                "original_tokens": 10000
            }
        ],
        "full_context_dump": {      # 完整的摘要前上下文转储(如果启用)
            "path": "/tmp/context/pre_summary/full_context.log",
            "original_tokens": 50000
        }
    },
    "summary_info": {
        "summarized_messages": 15,
        "kept_messages": 5,
        "original_tokens": 50000,
        "summarized_tokens": 15000,
        "saved_tokens": 35000
    }
}

参数说明

参数 类型 说明
summary_ratio float (0-1) 摘要比例,如 0.3 表示摘要最旧的 30% 消息
keep_recent int 保留最近 N 条消息的完整内容
offload_before_summary bool 是否在摘要前将关键部分卸载到文件
offload_path string 摘要前卸载的存储路径
dump_full_context bool 是否将整个摘要前的上下文转储为日志文件

6.1.3 融合接口(Combined)

功能:优先使用压缩,提供 token 计数对比,达到阈值后使用摘要。

输入

{
    "messages": List[Message],      # 完整的消息历史
    "combined_config": {
        "compaction_threshold": 30000,  # 压缩触发阈值(tokens)
        "summary_threshold": 80000,     # 摘要触发阈值(tokens)
        "compaction_config": {...},     # 压缩配置
        "summary_config": {...}         # 摘要配置
    }
}

输出

{
    "messages": List[Message],      # 处理后的消息历史
    "applied_strategy": "compaction" | "summary" | "none",  # 应用的策略
    "token_comparison": {
        "original_tokens": 70000,
        "after_compaction_tokens": 35000,  # 压缩后的 tokens(如果应用)
        "after_summary_tokens": None,      # 摘要后的 tokens(如果应用)
        "saved_tokens": 35000
    },
    "offloaded_data": {...},        # 卸载的数据
    "statistics": {...}             # 统计信息
}

参数说明

参数 类型 说明
compaction_threshold int 压缩触发阈值(tokens),超过此值触发压缩
summary_threshold int 摘要触发阈值(tokens),压缩后仍超过此值则触发摘要
compaction_config object 压缩配置,参考压缩接口参数
summary_config object 摘要配置,参考摘要接口参数

Token 计数接口

输入

{
    "messages": List[Message],      # 消息历史
    "reduction_config": {
        "compaction_config": {...},  # 压缩配置
        "summary_config": {...}      # 摘要配置
    }
}

输出

{
    "original_tokens": int,          # 原始 tokens
    "after_compaction_tokens": int,  # 压缩后的 tokens
    "after_summary_tokens": int,     # 摘要后的 tokens
    "compaction_savings": int,       # 压缩节省的 tokens
    "summary_savings": int,          # 摘要节省的 tokens
    "total_savings": int              # 总共节省的 tokens
}

6.2 上下文检索(Context Retrieving)

上下文检索提供两种方式:基础检索工具和定制化技能。

6.2.1 基础检索工具(Agentic Retrieve)

功能:提供 glob、grep 等文件工具,支持按需检索已卸载的上下文。

输入

{
    "query": str,                   # 检索查询
    "retrieve_config": {
        "tools": ["glob", "grep", "cat", "find"],  # 可用的检索工具
        "search_paths": [           # 搜索路径
            "/tmp/context",
            "/tmp/context/pre_summary"
        ],
        "file_patterns": ["*.json", "*.log", "*.txt"]  # 文件模式
    }
}

输出

{
    "results": [
        {
            "file_path": "/tmp/context/tool_1_result.json",
            "matched_content": "...",  # 匹配的内容片段
            "relevance_score": 0.85,
            "tool_used": "grep"
        }
    ],
    "retrieved_context": str,      # 检索到的完整上下文
    "tools_used": ["grep", "cat"]   # 使用的工具列表
}

参数说明

参数 类型 说明
query string 检索查询字符串
tools List[string] 可用的检索工具列表,如 ["glob", "grep", "cat", "find"]
search_paths List[string] 搜索路径列表
file_patterns List[string] 文件模式列表,如 [".json", ".log", "*.txt"]

6.2.2 定制化技能(Skills)

功能:提供更高级的检索技能,拆分为三个独立接口:meta_inforead_skillread_detail_skill

A. 元信息查询(meta_info)

输入

{
    "target": str  # 目标文件或路径
}

输出

{
    "file_size": int,
    "created_time": str,
    "tool_call_id": str,
    "original_tokens": int
}
B. 概要读取(read_skill)

输入

{
    "target": str  # 目标文件或路径
}

输出

{
    "skill_summary": str,
    "key_points": List[str]
}
C. 详细读取(read_detail_skill)

输入

{
    "target": str  # 目标文件或路径
}

输出

{
    "full_content": str,
    "sections": List[dict]
}

参考文档:Claude Context Editing 官方文档

七、总结

上下文管理是智能体系统的核心挑战。通过合理运用卸载、缩减、检索、隔离、缓存等策略,可以在满足智能体需求的同时,保持系统性能和稳定性。

记住:最好的解决方案往往是最简单的。理解问题本质,选择合适工具,避免过度工程化。