Manus 与 LangChain 团队进行了一场深度对话,话题聚焦在一个被长期忽视的技术命题:上下文工程。
为什么说被忽视?因为大多数智能体开发者把精力放在提示词调优、模型选型、工具设计上,却很少系统性地思考「上下文本身」该如何管理。而 Manus 的实践表明,这恰恰是生产环境中最影响成本和性能的因素。

一、核心问题:上下文腐烂
1.1 什么是上下文腐烂?
智能体与普通聊天机器人的关键区别在于:它需要频繁调用工具,而每次工具调用的结果都会被追加到上下文中。
想象一个典型的智能体任务:用户要求「分析这个 CSV 文件并生成报告」。智能体可能需要:
- 读取 CSV 文件(工具调用 1,返回 5000 tokens 的文件内容)
- 分析数据结构(工具调用 2,返回 1000 tokens 的分析结果)
- 执行 Python 代码进行统计(工具调用 3-10,每次返回数百 tokens)
- 搜索相关背景知识(工具调用 11-15,每次返回 2000 tokens)
- 生成图表(工具调用 16-20)
- 撰写报告(工具调用 21-25)
每一次工具调用的完整输入和输出都会被追加到上下文中。到第 25 次调用时,上下文可能已经膨胀到 50k-100k tokens。
Manus 披露的数据:
- 平均每个任务涉及 50+ 次工具调用
- 任务长度大约每 7 个月翻一番
- 复杂任务可能涉及 上千次工具调用
1.2 上下文腐烂的三重后果
第一,成本急剧增加
以 Claude 3 Sonnet 为例,输入价格为 $3/百万 tokens。一个 50 次工具调用的任务,假设每轮平均上下文为 30k tokens:
ini
总输入成本 = 50 × 30,000 × $3/1,000,000 = $4.5
如果任务需要重试或涉及多轮迭代,成本会快速累积到数十美元。
第二,延迟累积
处理更长的上下文需要更多时间。首 token 延迟与上下文长度正相关:
- 10k tokens: ~500ms
- 50k tokens: ~2s
- 100k tokens: ~5s
对于需要实时交互的场景,这种延迟是不可接受的。
第三,性能下降
研究表明,模型性能会随着上下文长度而衰减。大多数模型在远低于上下文上限时就开始出现问题:
- 重复生成:模型开始重复之前说过的内容
- 推理变慢:复杂推理能力下降
- 遗忘早期内容:中间迷失现象
- 指令遵循能力下降:更容易偏离用户的原始意图
这种现象被称为「上下文腐烂」,通常在 200k tokens 左右开始显现。
二、最重要的单一指标:KV 缓存命中率
2.1 为什么是 KV 缓存?
Manus 团队的判断:
「如果只能选择一个指标,键值缓存命中率是生产阶段智能体最重要的单一指标。」
这个判断基于一个关键事实:智能体的输入输出比例约为 100:1。
普通聊天场景,用户输入 100 个字,模型输出 500 个字,输入输出比例约 1:5。但智能体场景完全不同------每一轮对话都要把所有历史(系统提示词 + 工具定义 + 之前所有的对话和工具调用结果)传给模型,而模型输出可能只是一个工具调用请求。
makefile
典型智能体的一轮对话:
输入:50,000 tokens(历史上下文)
输出:500 tokens(一个工具调用)
比例:100:1
这意味着,优化输入成本的效果是优化输出成本的 100 倍。
2.2 什么是提示词缓存?
提示词缓存是模型提供商提供的优化机制:
如果连续两次 API 调用的提示词前缀相同,第二次调用可以复用第一次的计算结果。
大模型的推理过程分为两个阶段:
- 预填充阶段:处理输入的所有 tokens,生成 KV 缓存
- 解码阶段:逐个生成输出 tokens
预填充阶段的计算量与输入长度成正比,是主要的延迟来源。如果输入的前缀与之前的请求相同,就可以直接复用之前计算好的 KV 缓存,跳过预填充阶段的大部分计算。
2.3 缓存命中的收益
以各厂商的定价为例:
| 厂商 | 原价($/百万 tokens) | 缓存命中价 | 折扣率 |
|---|---|---|---|
| Anthropic (Claude) | $3.00 | $0.30 | 90% |
| DeepSeek | $0.14 | $0.014 | 90% |
| OpenAI | $2.50 | $1.25 | 50% |
缓存命中意味着:
- 成本降低 50-90%
- 延迟大幅减少(跳过预填充计算)
- 吞吐量提升(相同算力可处理更多请求)
对于一个日均百万次调用的智能体服务,缓存命中率从 50% 提升到 90%,每月可节省数十万美元。
2.4 前缀匹配的工作原理
缓存基于字节级前缀匹配,这是理解所有优化策略的基础:
ini
第一次调用:
[System Prompt][Tool Definitions][User Message 1][Tool Output 1]
↑
缓存写入到这里
第二次调用:
[System Prompt][Tool Definitions][User Message 1][Tool Output 1][User Message 2]
└──────────────────────── 完全相同的前缀 ────────────────────────┘
↓
可以复用缓存!
第三次调用(前缀被破坏):
[System Prompt v2][Tool Definitions][User Message 1][Tool Output 1][User Message 2]
↑
这里变了,后面的缓存全部失效!
关键约束:
- 缓存基于字节级前缀匹配,哪怕差一个空格也会失效
- 任何位置的修改都会破坏从该位置开始的缓存
- 前面的内容改变,后面的缓存全部失效
这就解释了为什么「前缀稳定性」如此重要------系统提示词和工具定义位于上下文的最前面,一旦变化,整个缓存链条都会断裂。
三、五个维度的平衡艺术
Manus 将上下文工程分为五个相互关联的维度:
| 维度 | 范畴 | 核心含义 |
|---|---|---|
| 卸载 | 长期持久化 | 把信息从上下文搬到外部存储,跨任务复用 |
| 缩减 | 会话级上下文 | 过滤 + 压缩 + 摘要,减少当前会话的上下文体积 |
| 检索 | 信息恢复 | 从外部状态(文件系统、数据库)恢复被卸载或缩减的信息 |
| 隔离 | 架构设计 | 子智能体独立上下文,过程噪音不污染主上下文 |
| 缓存 | 推理优化 | 优化 KV 缓存命中率,降低推理成本 |
3.1 五维度的相互作用
scss
┌─────────────┐
│ 缓存层 │
│ (推理优化) │
└──────┬──────┘
│ 影响
▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 卸载 │◄──►│ 缩减 │◄──►│ 隔离 │
│ (长期存储) │ │ (会话管理) │ │ (架构设计) │
└──────┬──────┘ └──────┬──────┘ └─────────────┘
│ │
└────────┬─────────┘
▼
┌─────────────┐
│ 检索 │
│ (信息恢复) │
└─────────────┘
这五个维度并非相互独立,而是存在复杂的相互作用:
「卸载和检索使得缩减更为高效,而稳定的检索则使隔离变得安全。但隔离又会减慢上下文的传递速度,降低缩减的频率。然而,更多的隔离和缩减又会影响缓存效率和输出质量。所以,归根结底,上下文工程是一门艺术与科学,它要求在多个可能相互冲突的目标之间找到完美的平衡。这真的很难。」
下面逐一展开。
四、上下文卸载:长期持久化
卸载面向长期,目的是把信息从上下文搬到外部存储,实现跨任务复用。
4.1 为什么需要卸载?
上下文窗口是「短期记忆」,有容量限制,且会话结束后消失。卸载的核心思想是:
「信息被外部化了,没有真正丢失。只要信息能从外部状态重建,就应该从上下文中剔除。」
卸载解决两类问题:
- 长期知识的持久化:用户偏好、项目背景、过往发现
- 工具定义的外部化:减少工具占用的上下文空间
4.2 长期记忆系统
智能体可以主动将重要信息存储到外部,以便在未来的任务中复用。
适合卸载的内容:
- 用户偏好:代码风格、语言习惯、输出格式偏好
- 项目背景:技术栈、架构设计、团队规范
- 任务历史:过往任务的关键发现、解决方案
- 领域知识:专业术语、业务规则
记忆的存储形式:
json
{
"user_id": "user_123",
"memories": [
{
"content": "用户偏好使用 TypeScript 而非 JavaScript",
"source": "对话记录",
"created_at": "2024-12-20",
"importance": "high"
},
{
"content": "项目使用 Next.js 14 + Tailwind CSS",
"source": "代码分析",
"created_at": "2024-12-18",
"importance": "high"
}
]
}
记忆的检索时机:
- 新对话开始时,检索用户相关的长期记忆
- 遇到特定领域问题时,检索领域知识
- 任务规划时,检索过往类似任务的经验
4.3 技能系统:工具定义的卸载
随着系统变得越来越复杂,工具本身也会占用大量上下文:
「上下文中工具过多会导致混乱,我们称之为'上下文混淆',模型可能会调用错误的工具,甚至是不存在的工具。所以我们必须找到一种方法,也能卸载工具。」
问题分析:
假设一个智能体有 50 个工具,每个工具定义平均 500 tokens:
ini
工具定义总占用 = 50 × 500 = 25,000 tokens
这 25k tokens 在每一轮对话都要传递给模型,即使大多数工具在当前任务中根本用不到。
动态检索增强的问题:
一个常见的解决方案是对工具描述进行动态检索增强------根据当前任务按需加载工具。但这也会带来问题:
- 破坏缓存:工具定义位于上下文的前端,每次变动都会导致 KV 缓存重置
- 历史混乱:模型过去对那些已被移除的工具的调用记录仍然存在于上下文中,这可能会误导模型
Manus 的解决方案:分层式行为空间
Manus 试验了一种三层设计:
| 层级 | 模型可见性 | 示例 | 上下文占用 |
|---|---|---|---|
| 第一层:函数调用 | 可见 | read_file, shell_exec, web_search | ~5,000 tokens |
| 第二层:沙盒工具 | 不可见 | grep, ffmpeg, awk, curl | 0 |
| 第三层:软件包与 API | 不可见 | Pandas, Numpy, requests, 外部 API | 0 |
工作原理:
- 模型只看到约 10 个核心「元工具」(如
shell_exec、python_execute) - 通过这些元工具,模型可以调用第二、三层的任意工具
- 第二、三层的工具定义不进入上下文,而是存储在文件系统中
示例:
css
用户请求:「分析这个视频的音频质量」
传统方式(工具都在上下文中):
[ffmpeg_tool] [audio_analyzer_tool] [spectral_tool] ...
→ 50 个工具定义,25k tokens
Manus 方式(分层行为空间):
[shell_exec_tool] [python_execute_tool]
→ 模型通过 shell_exec 调用 ffmpeg
→ 2 个工具定义,1k tokens
效果:
- 最大化缓存命中率:模型可见的工具定义极度稳定(约 10 个核心工具)
- 零额外占用:第二、三层的海量工具不占用上下文
- 无限扩展:熟悉 Linux/Python 就拥有无限工具箱
4.4 Anthropic 的解决方案:技能系统
Anthropic 在 Claude for Enterprise 中采用了另一种思路------技能系统(Skill System)。
核心思想:
- 把复杂工具的使用方法封装为「技能文档」
- 技能文档存储在文件系统中,不进入上下文
- 模型需要使用某个技能时,按需加载对应的技能文档
技能文档的结构:
markdown
# 技能名称:数据可视化
## 适用场景
- 需要生成图表、数据可视化
- 支持的图表类型:折线图、柱状图、饼图、散点图
## 使用方法
1. 准备数据:确保数据为 CSV 或 JSON 格式
2. 调用 python_execute,使用 matplotlib 或 plotly
3. 保存图表到 /workspace/charts/
## 示例代码
```python
import matplotlib.pyplot as plt
import pandas as pd
df = pd.read_csv('data.csv')
plt.figure(figsize=(10, 6))
plt.bar(df['category'], df['value'])
plt.savefig('/workspace/charts/output.png')
```
## 注意事项
- 中文需要设置字体:plt.rcParams['font.sans-serif'] = ['SimHei']
- 大数据集建议使用 plotly,支持交互式图表
与 Manus 方案的对比:
| 维度 | Manus 分层行为空间 | Anthropic 技能系统 |
|---|---|---|
| 抽象层次 | 底层工具(shell、python) | 高层能力(数据可视化、文档处理) |
| 学习曲线 | 需要模型熟悉 Linux/Python | 技能文档自包含,降低认知负担 |
| 可维护性 | 依赖模型的通用能力 | 技能文档可独立更新、版本管理 |
| 缓存友好性 | 工具定义极度稳定 | 技能按需加载,可能影响缓存 |
实际应用中的选择:
- Manus 方案适合技术能力强的场景,模型自由度高
- 技能系统适合标准化流程,可控性强,便于团队协作
4.5 Scratchpad 模式:主动外部化
除了系统自动的卸载,智能体还可以主动将关键发现写入文件(如 /workspace/notes/),这是独立于系统压缩/摘要的第一道防线。
| 特性 | 主动外部化 | 系统压缩 |
|---|---|---|
| 触发者 | 智能体自己(提示词引导) | 系统自动(> 阈值 tokens) |
| 存储内容 | 提炼后的关键发现(50-200 tokens) | 原始工具输出(5000+ tokens) |
| 恢复成本 | 低(读取简短笔记) | 高(读取原始大文件) |
| 存储位置 | /workspace/notes/(用户可见) |
.context/(系统目录) |
价值:在长对话中,主动外部化可避免恢复时重读大量原始数据。
五、上下文缩减:会话级管理
缩减面向短期,目的是管理当前会话的上下文体积。包含三种策略:过滤、压缩、摘要。
5.1 过滤:入口拦截
当工具返回超大结果时,系统自动拦截并转移到文件系统,同时生成智能预览传给模型。
触发条件:
yaml
单个工具结果 > 5000 tokens → 触发过滤
任务感知的混合过滤策略:
关键设计是根据内容类型选择不同的过滤策略:
| 内容类型 | 过滤器 | 策略详情 | 需要模型 |
|---|---|---|---|
| JSON | 结构化过滤 | 提取键名、数组长度、嵌套结构 | ❌ |
| XML | 结构化过滤 | 提取标签层级、属性摘要 | ❌ |
| 代码 | 结构化过滤 | 提取函数/类签名、导入语句 | ❌ |
| HTML | 语义过滤 | 模型生成任务相关摘要 | ✅ |
| Markdown | 语义过滤 | 模型生成任务相关摘要 | ✅ |
| 纯文本 | 语义过滤 | 模型生成任务相关摘要 | ✅ |
为什么要区分?
结构化内容有明确的语法结构,可以用代码快速提取关键信息,无需调用大模型,成本为零。
非结构化内容需要理解语义才能有效摘要,必须借助大模型。
任务感知:
过滤器不只是机械地提取结构信息,而是根据用户的查询意图提取相关信息。
arduino
用户查询:「这个网页上有哪些产品的价格?」
工具返回:一个 50,000 tokens 的网页 HTML
任务无关的过滤:提取所有 <div> 标签结构
任务感知的过滤:提取所有 class="price" 和 class="product-name" 的内容
过滤后的格式:
makefile
FILTERED: web_fetch_tool
URL: https://example.com/products
FILE: /workspace/.context/web_fetch_xxx.html
PREVIEW: [任务相关的摘要,约 500 tokens]
RECOVER: 需要完整内容时,读取上述文件
5.2 压缩:可逆的紧凑格式
当总上下文达到阈值时,系统将历史中的旧工具调用结果转换为紧凑格式。
触发条件:
总上下文 > 60,000 tokens(可配置)
且预计节省 > 3,000 tokens(避免小清理破坏缓存)
信息外部化
Manus 的解释:
「假设你有一个向文件写入的工具,它可能有两个字段:路径和内容。一旦工具执行完毕,你可以确保该文件已经存在于环境中。因此,在紧凑格式中,我们可以安全地去掉超长的内容字段,只保留路径。如果你的智能体足够聪明,当它需要再次读取该文件时,只需通过路径即可检索。」
「这样一来,没有任何信息真正丢失,只是被外部化了。我们认为这种可逆性至关重要,因为智能体需要基于之前的行为和观察进行链式预测,你永远不知道哪个过去的行为会在十步之后突然变得极其重要。这是无法预测的。」
完整格式 vs 紧凑格式:
vbnet
【完整格式 - 5,000 tokens】
{
"tool": "web_search_tool",
"arguments": {
"query": "Claude API pricing 2024"
},
"result": {
"sources": [
{
"title": "Claude API Pricing | Anthropic",
"url": "https://www.anthropic.com/pricing",
"snippet": "Claude 3.5 Sonnet: $3/M input tokens, $15/M output tokens..."
},
// ... 20 个搜索结果,每个 200 tokens
],
"summary": "Claude API 定价如下:..."
}
}
【紧凑格式 - 150 tokens】
COMPACTED: web_search_tool
QUERY: Claude API pricing 2024
FILE: /workspace/.context/web_search_20241220_153000.txt
RECOVER: cat <FILE> with bash_tool
META: tokens_saved=4850 time=2024-12-20T15:30:00
压缩策略:
不同工具类型有不同的压缩规则:
| 工具类型 | 保留字段 | 移除字段 |
|---|---|---|
web_search_tool |
query, source_count | 完整搜索结果 |
file_write_tool |
path | content(文件已存在) |
file_read_tool |
path | content(可重新读取) |
code_execute_tool |
code_summary, exit_code | 完整输出 |
web_fetch_tool |
url, status_code | 网页内容 |
部分压缩策略:
压缩不意味着压缩整个历史记录。Manus 的做法是:
- 只压缩最旧的 50% 的工具调用
- 保留最近 5 次调用的完整格式
- 这样模型仍然有新鲜的示例来学习如何正确使用工具
yaml
对话历史(20 次工具调用):
┌──────────────────────────────────────────────────────────┐
│ 调用 1-10: 转为紧凑格式(节省约 40,000 tokens) │
│ 调用 11-15: 转为紧凑格式(节省约 20,000 tokens) │
│ 调用 16-20: 保留完整格式(作为少样本示例) │
└──────────────────────────────────────────────────────────┘
5.3 摘要:不可逆的最后手段
压缩也有其极限。最终,上下文仍然会增长并触及上限。这时,Manus 会将压缩与更传统的摘要结合起来,但做得非常谨慎。
触发条件:
总上下文 >= 150,000 tokens(可配置)
为什么摘要是最后手段?
摘要是不可逆操作,一旦执行,原始消息结构被替换,无法恢复。这与压缩的本质区别在于:
- 压缩:信息外部化到文件,可以随时读回
- 摘要:信息被大模型重新生成,原始细节丢失
有损但可追溯策略:
Manus 的做法:
「在进行摘要之前,我们可能会将上下文的关键部分卸载到文件中。有时,我们甚至会更激进,将整个摘要前的上下文转储为一个文本文件或日志文件,以便日后随时恢复。如果模型足够聪明,它甚至知道如何检索那些被摘要前的上下文。」
工作流程:
- 备份 :将完整上下文转储到
/workspace/.context/summary_dump_xxx.txt - 摘要:基于完整数据生成结构化摘要
- 替换:上下文中只保留摘要 + 备份文件路径
- 恢复:模型可用 grep 命令检索原始细节
结构化摘要格式:
关于摘要格式,建议使用固定模式而非自由形式:
「不要使用自由形式的提示让 AI 生成所有内容,而是定义一个模式,就像一个有很多字段的表单,让 AI 去填写。如果你使用这种更结构化的模式,至少输出会比较稳定。」
json
{
"user_goal": "分析订单数据并生成销售报告",
"completed_actions": [
"读取 orders.csv(10,000 条订单记录)",
"按月份统计销售额",
"分析退货原因分布",
"生成可视化图表 4 张"
],
"key_findings": [
"2024 年总销售额 $1.2M,同比增长 15%",
"退货率 5.2%,主要原因是尺寸问题(占 65%)",
"Q4 销售额占全年 40%"
],
"files_modified": [
"/workspace/reports/sales_2024.md",
"/workspace/charts/monthly_sales.png",
"/workspace/charts/return_reasons.png"
],
"pending_tasks": [
"生成 PDF 版本报告"
],
"last_action": "保存 Markdown 报告"
}
保留最近调用的重要性:
Manus 的经验:
「保留几个完整的工具调用和结果示例非常有帮助。因为这能让模型知道它上次停在了哪里,从而更平滑地继续工作。否则,你会发现摘要之后,模型有时会改变其风格和语气。」
5.4 一个关键洞察:紧凑格式也占空间
摘要不会「永远不触发」。关键在于紧凑格式本身也占空间(约 150 tokens/个):
| 工具调用数 | 紧凑格式累积 | 其他消息 | 总计 | 触发摘要? |
|---|---|---|---|---|
| 100 | ~15,000 | 20,000 | 35,000 | ❌ |
| 500 | ~75,000 | 30,000 | 105,000 | ❌ |
| 1000 | ~150,000 | 40,000 | 190,000 | ✅ |
当对话非常长(上千次工具调用)时,紧凑格式累积最终会超过摘要阈值,此时必须触发摘要。
5.5 三者的执行顺序
markdown
工具调用返回时
│
└─ 结果是否超大(> 5k tokens)?
│
├─ 是 → 【过滤】
│ ├─ 完整结果存入文件
│ ├─ 生成任务相关预览
│ └─ 预览进入上下文
│
└─ 否 → 完整结果进入上下文
│
↓
每轮对话结束后检查总上下文
│
├─ 总上下文 < 60k tokens
│ └─ 正常运行,无需处理
│
├─ 60k ~ 150k tokens
│ └─ 【压缩】
│ ├─ 检查压缩收益是否 > 3k tokens
│ ├─ 压缩最旧的 50% 工具调用
│ ├─ 保留最近 5 次完整格式
│ └─ 原始结果存入文件系统
│
└─ > 150k tokens
└─ 【摘要】
├─ 备份完整上下文到文件
├─ 生成结构化摘要
├─ 保留最近 5 次完整工具调用
└─ 用摘要替换历史消息(不可逆)
5.6 阈值管理的经验值
关于阈值的经验值:
「通过大量的评估,确定那个'腐烂前'的阈值非常重要,通常在 128k 到 200k 之间。我们将它作为触发上下文缩减的信号。」
根据场景调整阈值:
| 场景 | 压缩阈值 | 摘要阈值 | 最小节省阈值 | 原因 |
|---|---|---|---|---|
| 短对话(<10 轮) | 80k | 180k | 10k+ | 剩余轮数少,缓存价值高 |
| 长对话(>30 轮) | 60k | 150k | 2-3k | 剩余轮数多,节省效果累积 |
| 研究任务 | 60k | 150k | 5k | 大量工具调用,上下文增长快 |
| 实时交互 | 80k | 180k | 8k+ | 低延迟优先,避免频繁压缩 |
六、上下文隔离:子智能体的独立上下文
这一策略来自 Anthropic 的多智能体架构设计。
6.1 为什么需要隔离?
核心思想是:子智能体拥有独立的上下文窗口,其探索过程中的「噪音」不会污染主智能体的上下文。
场景示例:
用户请求:「调研一下 2024 年最新的 AI Agent 框架,给出对比分析」
如果不使用隔离,主智能体需要:
- 搜索「AI Agent 框架 2024」
- 访问 10+ 个网页,每个 5000 tokens
- 阅读多篇技术博客和论文
- 整理对比表格
整个过程产生的上下文:10 × 5000 = 50,000+ tokens,全部进入主智能体上下文。
使用隔离后:
- 主智能体调用
researcher_tool,传入研究主题 - 子智能体在独立上下文中完成调研
- 子智能体返回精炼的摘要(~1000 tokens)+ 完整报告文件路径
- 主智能体上下文只增加 1000 tokens
6.2 隔离的工作原理
yaml
主智能体上下文窗口
┌─────────────────────────────────────────────────────────────┐
│ [System Prompt] [Tools] [User: 调研 AI Agent 框架] │
│ [Tool Call: researcher_tool(topic="AI Agent 框架 2024")] │
│ [Tool Result: {summary: "...", file: "/reports/xxx.json"}] │
│ │
│ 上下文增量:~1000 tokens │
└─────────────────────────────────────────────────────────────┘
子智能体上下文窗口(独立)
┌─────────────────────────────────────────────────────────────┐
│ [System Prompt: 你是研究员...] │
│ [web_search: AI Agent 框架 2024] → 结果 5000 tokens │
│ [web_fetch: langchain.com] → 结果 8000 tokens │
│ [web_fetch: crewai.com] → 结果 6000 tokens │
│ [web_fetch: autogen.com] → 结果 7000 tokens │
│ ... 更多搜索和访问 ... │
│ │
│ 上下文累积:~80,000 tokens(全部隔离,不影响主智能体) │
└─────────────────────────────────────────────────────────────┘
6.3 隔离后的返回格式
json
{
"topic": "AI Agent 框架 2024 对比分析",
"summary": "经过调研,2024 年主流的 AI Agent 框架包括 LangChain、CrewAI、AutoGen、Semantic Kernel 等。LangChain 生态最完善但学习曲线较陡;CrewAI 专注多智能体协作...",
"key_points": [
"LangChain: 生态最完善,社区活跃,但抽象层次多",
"CrewAI: 多智能体协作首选,API 简洁",
"AutoGen: 微软出品,与 Azure 深度集成",
"Semantic Kernel: 企业级首选,支持 .NET/Python/Java"
],
"comparison_table": "详见完整报告",
"_isolated": true,
"_full_result_file": "/workspace/research_results/ai_agent_frameworks_2024.json",
"_full_result_tokens": 85000,
"_read_instruction": "完整研究报告(85000 tokens)已保存,如需细节请读取上述文件"
}
6.4 隔离的类比
「子智能体可能累积数万 token 的过程噪音,但这些 token 被隔离在其独立上下文中------主智能体只接收精炼后的结论。这类似于组织中的层级汇报:基层员工处理大量细节,向上级仅提交摘要和结论。」
七、缓存优化的核心原理
7.1 Manus 的三条铁律
铁律一:保持提示前缀稳定
「由于大模型的自回归特性,即使是单个 token 的差异也会使该 token 之后的缓存失效。一个常见的错误是在系统提示的开头包含时间戳。」
❌ 错误做法:
python
system_prompt = f"""
当前时间:{datetime.now()}
你是一个专业的 AI 助手...
"""
每次调用,时间戳都不同,导致整个系统提示词的缓存失效。
✅ 正确做法:
python
system_prompt = """
你是一个专业的 AI 助手。
动态上下文信息会在用户消息中提供。
"""
user_message = f"""
[当前上下文]
时间:{datetime.now()}
用户偏好:{preferences}
[用户问题]
{user_query}
"""
铁律二:使上下文只追加
「避免修改之前的操作或观察。确保你的序列化是确定性的。许多编程语言和库在序列化 JSON 对象时不保证键顺序的稳定性。」
常见陷阱:
- Python dict 序列化顺序不稳定(Python 3.7+ 已按插入顺序,但不同版本可能有差异)
- 浮点数精度问题 :
0.1 + 0.2可能产生0.30000000000000004 - 随机 ID:每次生成不同的 UUID 或时间戳
解决方案:
python
import json
# 确保键顺序稳定
def stable_serialize(obj):
return json.dumps(obj, sort_keys=True, ensure_ascii=False)
铁律三:明确标记缓存断点
「某些模型提供商或推理框架不支持自动增量前缀缓存,而是需要在上下文中手动插入缓存断点。」
Anthropic 的 cache_control 示例:
python
messages = [
{
"role": "system",
"content": [
{
"type": "text",
"text": "你是一个专业的 AI 助手...",
"cache_control": {"type": "ephemeral"} # 标记缓存断点
}
]
},
# ... 后续消息
]
7.2 工具定义的分层排序
工具列表动态变化会破坏缓存前缀,因为工具定义位于上下文前部,任何更改都会使后续所有内容的缓存失效。
解决方案:按稳定性分层排序
| 层级 | 稳定性 | 变化频率 | 缓存价值 | 工具示例 |
|---|---|---|---|---|
| CORE | 极高 | 几乎不变 | 最高 | web_search, file_editor, shell |
| COMMON | 高 | 很少变 | 高 | web_fetch, memory_tool |
| EXTENDED | 中 | 偶尔变 | 中 | 临时加载的技能、answer_user |
分层结构:
scss
[System Prompt] ← 完全稳定,永远缓存
[Layer 1: 核心工具定义] ← 高度稳定,几乎不变 (web_search, file_editor)
[Layer 2: 常用工具定义] ← 相对稳定
[Layer 3: 动态/扩展工具] ← 易变区 (临时加载的技能)
[对话历史] ← 增量增长
效果:
ini
第 1 轮: [System][CORE: search,file,shell][EXTENDED: planner][History 1]
第 2 轮: [System][CORE: search,file,shell][EXTENDED: memory][History 1-2]
|<----- 这部分可缓存 ----->|
虽然 EXTENDED 工具变了,但:
- System Prompt + CORE 工具部分仍然命中缓存
- 只有 EXTENDED 及之后需要重新计算
7.3 批量清理优于渐进清理
问题:渐进式清理(每次超过阈值就清理)会频繁破坏缓存。
yaml
渐进式:
轮 10: 上下文 62k → 清理到 55k → 缓存失效
轮 15: 上下文 63k → 清理到 58k → 缓存失效
轮 20: 上下文 65k → 清理到 60k → 缓存失效
(3 次缓存失效,3 次重建缓存的成本)
批量式:
轮 10-19: 上下文 62k → 80k,积累,不清理
轮 20: 上下文 85k → 一次性清理到 50k → 缓存失效
(1 次缓存失效,1 次重建缓存的成本)
实现方式:
tokens < 60k → 正常运行,不处理
60k < tokens < 120k → 开始积累,等待 N 轮后批量清理
tokens > 120k → 强制立即清理(紧急情况)
八、成本分析:压缩与缓存的博弈
8.1 核心公式
上下文工程与提示词缓存本质上是「数量减少」与「单价打折」之间的博弈。
定义两个核心变量:
R_comp(压缩率):上下文工程把 token 数量减少了多少。例如压缩 50%,R_comp = 0.5D_cache(缓存折扣率):命中缓存后的价格是原价的多少。例如 Claude 命中缓存后价格是原价的 10%,则D_cache = 0.1
scss
上下文工程的成本 = Input_orig × (1 - R_comp) × Price_base
缓存命中的成本 = Input_orig × D_cache × Price_base
盈亏平衡点:当 (1 - R_comp) = D_cache 时两者相等
8.2 结论:缓存通常完胜
目前主流厂商(DeepSeek、Anthropic)的缓存折扣率约为 10%。这意味着:
- 除非上下文工程能达到 90% 压缩率,否则单纯从成本角度看,不如直接用缓存
- 而 90% 的压缩率通常会严重丢失信息,导致模型变笨
示例计算:
假设原始输入 100k tokens,Claude 原价 $3/百万:
ini
不压缩 + 缓存命中:100k × $0.3/M = $0.03
压缩 50% + 无缓存:50k × $3/M = $0.15
压缩 50% + 缓存命中:50k × $0.3/M = $0.015(最优)
结论:压缩应该服务于缓存,而不是替代缓存。
8.3 场景化决策
| 场景 | 特点 | 策略 | 原因 |
|---|---|---|---|
| 静态长文本 | 系统设定、知识库、少样本示例 | 死磕缓存,不压缩 | 缓存打 1 折,压缩反而破坏缓存 |
| 动态长历史 | 50 轮对话,接近窗口上限 | 必须用上下文工程 | 物理塞不进去是首要矛盾 |
| 检索增强 | 每次检索内容不同 | 混合策略 + 前缀增厚 | 系统提示词缓存 + 检索内容按需处理 |
前缀增厚策略:
对于检索增强场景,一个进阶技巧是让系统提示词变「厚」:
- 加入 10-20 个高质量的少样本示例
- 把前缀从 500 tokens 扩展到 3000-5000 tokens
- 缓存收益从 5% 提升到 30-50%
九、各模型提供商对比
9.1 机制对比
| 提供商 | 触发方式 | 折扣率 | 写入成本 | TTL | 最小长度 |
|---|---|---|---|---|---|
| Anthropic | 自动 + 显式 | 90% | 原价 25% | 5分钟/1小时 | 1024-4096 |
| OpenAI | 纯自动 | 50% | 无 | 5-10 分钟 | 1024 |
| DeepSeek | 纯自动 | ~90% | 无 | 动态 | 64 |
| 显式 + 隐式 | 75% | $1/M/小时 | 1小时-1天 | 1k / 32k | |
| 阿里云 | 隐式 + 显式 | 80%/90% | 隐式无/显式125% | 不确定/5分钟 | 256 / 1024 |
9.2 Anthropic (Claude) 的注意事项
Claude 的缓存并非 100% 保证命中,存在以下限制:
-
精确匹配要求 :缓存命中需要 100% 相同 的提示词段,包括所有文本和图像的字节级一致
-
20 块回溯窗口 :系统仅检查每个显式
cache_control断点之前的最多 20 个块 。如果修改发生在 20 个块之外,将无法命中缓存 -
并发请求限制 :缓存条目仅在第一个响应开始后才可用。并行请求可能无法命中刚创建的缓存
-
组织隔离:缓存在组织之间隔离,不同组织永远不会共享缓存
最小缓存长度要求:
| 模型 | 最小缓存长度 |
|---|---|
| Claude Opus 4.5 | 4096 tokens |
| Claude Sonnet 4.x | 1024 tokens |
| Claude Haiku 4.5 | 4096 tokens |
| Claude Haiku 3.x | 2048 tokens |
9.3 阿里云的特殊机制
阿里云的隐式缓存与显式缓存互斥,单个请求只能应用其中一种模式。
隐式缓存(自动模式):
- 命中概率:不确定,由系统判定
- 即使请求上下文完全一致,仍可能未命中
- 优势:无额外写入成本
显式缓存(主动模式):
- 命中概率:确定性命中
- 需要在 messages 中加入
cache_control标记 - 写入成本:输入价格的 125%
- 命中价格:输入价格的 10%
9.4 统一优化策略
好消息 :所有提供商都基于相同的核心机制------前缀匹配。
这意味着优化策略具有通用性:
- ✅ 保持前缀稳定
- ✅ 工具定义分层
- ✅ 只追加不删除
- ✅ 批量清理
十、实践建议
10.1 系统提示词设计
python
# ❌ 不推荐:每次调用都变化
system = f"""
当前时间:{datetime.now()}
你是 AI 助手,帮助用户 {user_name} 完成任务。
"""
# ✅ 推荐:系统提示词稳定,动态信息放在用户消息中
system = """
你是一个专业的 AI 助手。
动态上下文信息会在每次对话中提供。
请根据上下文信息个性化地回应用户。
"""
messages = [
SystemMessage(content=system), # 稳定的前缀
HumanMessage(content=f"""
[当前上下文]
时间:{datetime.now()}
用户:{user_name}
偏好:{user_preferences}
[用户问题]
{user_query}
"""),
]
10.2 工具定义顺序
python
# 按稳定性排序
tools = [
# CORE 层:最稳定,放最前面
web_search_tool,
file_editor_tool,
shell_execute_tool,
# COMMON 层:相对稳定
web_fetch_tool,
memory_tool,
# EXTENDED 层:可能变化,放最后
*dynamically_loaded_skills,
answer_user_tool,
]
10.3 序列化的确定性
python
import json
from collections import OrderedDict
def stable_serialize(data: dict) -> str:
"""确保序列化结果的字节级一致性"""
return json.dumps(
data,
sort_keys=True, # 键排序
ensure_ascii=False, # 保留中文
separators=(',', ':'), # 紧凑格式,无多余空格
)
# 测试
data = {"b": 2, "a": 1}
assert stable_serialize(data) == '{"a":1,"b":2}'
10.4 监控指标
python
# 关键监控指标
metrics = {
"cache_hit_rate": "缓存命中率,目标 > 80%",
"context_length_p95": "上下文长度 P95,监控增长趋势",
"compression_frequency": "压缩触发频率,过高说明阈值设置不合理",
"summarization_count": "摘要触发次数,应该很少",
"ttft_p95": "首 token 延迟 P95,反映缓存效果",
}
十一、写在最后
上下文工程在智能体开发中越来越重要。
Manus 的实践数据:
- 推理次数:从平均 4.3 次降至 1.2 次(-72%)
- 总成本:降低 72%
这个效果来自两个层面的叠加:
- 缓存层:提高命中率,降低单次推理成本
- 准确率层:减少失败重试,降低调用次数
用 Manus 的话来说:
「上下文工程是一门艺术与科学,它要求在多个可能相互冲突的目标之间找到完美的平衡。这真的很难。」
如果只能做一件事,从监控缓存命中率开始。