上下文与缓存:理解 Agent 对话背后的成本与效率
每次和 AI Agent 对话,都在向大模型发送一段"上下文"。这段上下文如何构成、如何被读取、如何被缓存,直接决定了你的使用成本和响应速度。
一、一次 Agent 调用,上下文里有什么
当你向一个 Agent 提问时,真正发给大模型的并不是只有你那句话。以 HippoBuddy 为例,一次 tool calling 循环中,发送的消息列表大致长这样:
system: "你是 HippoBuddy,一个 AI 编程助手…"
tools:
- ReadFile: path + offset + limit
- WriteFile: path + content
- Grep: pattern + path
- Bash: command
- …(可能有十几个工具)
user: "帮我查一下 src/main 下有哪些 Java 文件"
assistant: "我来帮你搜索一下" (tool_call → Grep)
tool_result: "找到以下文件:Main.java, Service.java…"
assistant: "找到了,目录结构是…"
关键点: 每次调用的消息列表是累加的。第二轮对话时,前一轮的 assistant 回复和 tool_result 也会一起发过去。如果对话持续了 20 轮,每次发的上下文就是"20 轮对话的全部内容 + 新的提问"。
这就引出了两个问题:
- 成本问题: LLM 按 token 计费,上下文越长单次调用越贵
- 效率问题: 上下文越长,模型处理的时间也越长
而这两个问题的核心优化手段,都指向同一个技术——前缀缓存(Prefix Caching)。
二、LLM 怎么"读"你的上下文
注意力不是均匀分配的
大模型使用注意力机制(Attention)来处理输入文本。但对于长上下文,模型的注意力分布很不均匀:
- 开头(System Prompt + Tools 定义): 注意力集中度高
- 中间(历史的工具调用、assistant 回复): 注意力最容易丢失
- 结尾(最新的提问): 注意力集中度高
这就是著名的 Lost in the Middle 现象。2023 年斯坦福和 UC Berkeley 的研究发现,当上下文超过一定长度后,模型对中间位置的信息检索准确率会显著下降。
对 Agent 的影响
这意味着:
- System Prompt 是黄金位置。 写在这里的指令、规则,模型"看到"的可能性最高。
- 工具描述的质量直接影响决策质量。 如果工具描述模糊、参数说明不全,模型可能在注意力高地(工具定义区域)就没理解对,后续的决策也会随之偏离。
- 中间的历史轮次可能被"忽略"。 某轮 tool 调用返回了一个关键信息,但如果它恰好被夹在很长上下文的中间位置,模型在下一次决策时可能"忘记"它。
所以说,一个好的 System Prompt 和工具描述,不是"锦上添花",而是决定 Agent 能不能正确行动的基石。
三、为什么"开头"既是注意力高地,也是缓存高地
前缀缓存的原理
LLM 生成回复时最耗时的步骤之一是计算注意力(Attention),而注意力的核心是维护一个 KV Cache(Key-Value Cache)。KV Cache 的本质是模型在看完输入后留下的"笔记"——记录了对每个 token 的中间计算结果,后续 token 可以复用这些结果,避免重复计算。
前缀缓存(Prefix Caching)正是利用了这个机制:
第 1 次调用:system + tools + "你好"
→ 计算 KV Cache for [system + tools + "你好"]
→ 存在缓存中
第 2 次调用:system + tools + "帮我查文件"
→ 前两个部分 [system + tools] 的 KV Cache 已经算过了
→ 直接从缓存读取结果,只计算新部分 ["帮我查文件"]
→ 显存计算量减半,延迟大幅降低
为什么缓存命中 = 省钱
大部分 LLM API 的定价逻辑是:
- 缓存命中(Cache Hit): 只需支付读取缓存的费用,通常为正常价格的 1/2 甚至 1/10
- 缓存未命中(Cache Miss): 全额计算,按正常价格计费
以当前主流模型的定价来看,缓存命中的成本通常是非命中的 50%~90% 不等,具体比例取决于服务商。
系统 Prompt 和工具定义恰好是前缀缓存的最佳候选——它们在整场对话中几乎不变化,只要不修改,每一轮的 KV Cache 都可以复用。
四、缓存的"脆性"——为什么不能改
前缀缓存有一个关键约束:精确匹配。
也就是说,缓存系统在判断是否命中时,对比的是 token 级别的完整前缀。只要前缀有任何变化,哪怕是一个标点符号、一个空格差异,缓存都会失效,需要从头计算。
对 Agent 的影响
这对 Agent 开发者来说是一个容易被忽略的陷阱:
| 场景 | 后果 |
|---|---|
| 修改了 System Prompt 中的一个标点 | 下一调用缓存全部失效 |
| 调整了某个工具的参数描述 | 整场对话的缓存积累清空 |
| 动态启用/禁用了一个工具 | 工具列表变了 → 前缀变了 → 缓存失效 |
| 工具注册顺序发生了变化 | 同上 |
| 重新生成了 tools 的 JSON Schema | 哪怕内容不变,字符串顺序变了也失效 |
最容易被忽略的情况——同一场会话中途:
假设你正在和 Agent 进行一段长对话,中途通过某个工具调用动态开启了一个新的工具功能。这时,工具列表被修改了,后续调用的前缀(system + tools)变了,前面累积的所有缓存收益一笔勾销,后续调用全部按非缓存计价。
所以,前缀缓存的优化策略不是一个"写了就能自动省钱"的方案,它需要开发者在设计 System Prompt 和工具注册机制时,就有意识地保持前缀的稳定性。
五、对开发者的启示
1. 稳定前缀与动态部分分离
设计消息构建策略时,把"不怎么变"的部分放在前缀头部,把"可能会变"的部分放在靠后的位置:
[稳定前缀]
system prompt(固定不变的核心指令)
[可能变化]
动态 tool 定义(按需启用/禁用的工具)
对话历史
当前提问
但注意:如果 system prompt 本身也需要动态拼接(比如插入用户信息、当前时间),那每次 system prompt 都不同,前缀缓存永远不会命中。解决方法是把变的部分后移。
2. 工具设计的三点建议
- 注册时机提前决策: 所有工具一次性注册,不在会话中途增删。如果确有动态启用的需要,在应用层做过滤,而不是修改 tools 列表
- 描述要精炼且稳定: 工具描述不是越长越好——占据更多前缀 token 意味着更大的缓存空间占用和更高的未命中成本。一个好的工具描述是:用最少的字让模型知道"什么时候该用这个工具"
- 参数名与 Schema 保持一致: 参数名、类型、描述一旦确定,不要随意改动。频繁修改会让前缀缓存形同虚设
3. 不同服务商的缓存策略差异
| 服务商 | 缓存机制 | 触发方式 | 定价优势 |
|---|---|---|---|
| DeepSeek | 自动前缀缓存(硬盘缓存) | 自动,从头匹配 | 缓存命中与未命中差价 50~120 倍;Flash 缓存命中低至 $0.0028/M |
| OpenAI | 自动 Prompt Caching | 自动,新模型无最小 token 要求 | 缓存命中约 90% 折扣(GPT-5.x),老模型约 50%(GPT-4o) |
| Anthropic | 显式缓存(需 cache_control 标记) | 标记后 5min/1hr TTL,写入成本为 1.25x/2x 基础价 | 缓存读取约 90% 折扣(0.10x 基础价) |
| 隐式缓存 + 显式 Context Caching | 隐式自动;显式需 API 创建,另有小时存储费 | 缓存读取约 90% 折扣,但需额外支付 $1.00/M/hr 存储费 |
注意:具体定价和策略会随服务商更新而变化,请以官方文档为准。上述信息供开发者在做技术选型时参考。
4. 注意力层面的建议
结合第二部分(注意力分布),还有几个经验可以分享:
- System Prompt 的核心指令放第一段: 模型读上下文的注意力从开头开始衰减,最重要的规则放在最前面
- 工具描述中优先写"触发条件": 让模型第一时间判断"这个工具的适用场景是什么",比先写参数再写场景更符合注意力的分布规律
- 关键信息在提问中复述: 如果你的工具调用返回了一个后续决策需要的关键结果,在下一轮的 user message 中简要复述它,避免"沉没"在对话中间。
六、小结
这篇文章的核心信息可以总结为三句话:
- 对普通用户: 每个会话专注于一个主线任务,不同的探索方向就开新会话。新会话共享相同的前缀缓存,不会增加额外成本,也避免了对话历史越长注意力越分散的问题。
- 对开发者: 缓存的收益取决于前缀的稳定性。设计 System Prompt 和工具注册机制时,把"不怎么变"和"可能会变"分离,是性价比最高的优化。同时也应通过架构设计(如固定工具列表、避免动态修改前缀)来杜绝会话中途的缓存破坏——在成本视角下,这比提醒用户"别切会话"靠谱得多。
- 对所有人: 理解 LLM 怎么读你的上下文(注意力和缓存),能帮你更好地理解 Agent 的行为,无论是调试还是使用都更有方向感。
上下文的质量 = 注意力 × 缓存效率。好的设计让两者都受益。