跳到主要内容

上下文与缓存:理解 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 轮对话的全部内容 + 新的提问"。

这就引出了两个问题:

  1. 成本问题: LLM 按 token 计费,上下文越长单次调用越贵
  2. 效率问题: 上下文越长,模型处理的时间也越长

而这两个问题的核心优化手段,都指向同一个技术——前缀缓存(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 基础价)
Google隐式缓存 + 显式 Context Caching隐式自动;显式需 API 创建,另有小时存储费缓存读取约 90% 折扣,但需额外支付 $1.00/M/hr 存储费

注意:具体定价和策略会随服务商更新而变化,请以官方文档为准。上述信息供开发者在做技术选型时参考。

4. 注意力层面的建议

结合第二部分(注意力分布),还有几个经验可以分享:

  • System Prompt 的核心指令放第一段: 模型读上下文的注意力从开头开始衰减,最重要的规则放在最前面
  • 工具描述中优先写"触发条件": 让模型第一时间判断"这个工具的适用场景是什么",比先写参数再写场景更符合注意力的分布规律
  • 关键信息在提问中复述: 如果你的工具调用返回了一个后续决策需要的关键结果,在下一轮的 user message 中简要复述它,避免"沉没"在对话中间。

六、小结

这篇文章的核心信息可以总结为三句话:

  1. 对普通用户: 每个会话专注于一个主线任务,不同的探索方向就开新会话。新会话共享相同的前缀缓存,不会增加额外成本,也避免了对话历史越长注意力越分散的问题。
  2. 对开发者: 缓存的收益取决于前缀的稳定性。设计 System Prompt 和工具注册机制时,把"不怎么变"和"可能会变"分离,是性价比最高的优化。同时也应通过架构设计(如固定工具列表、避免动态修改前缀)来杜绝会话中途的缓存破坏——在成本视角下,这比提醒用户"别切会话"靠谱得多。
  3. 对所有人: 理解 LLM 怎么读你的上下文(注意力和缓存),能帮你更好地理解 Agent 的行为,无论是调试还是使用都更有方向感。

上下文的质量 = 注意力 × 缓存效率。好的设计让两者都受益。