System Prompt 与缓存经济
一次错误的 system prompt 修改可能让你的 API 账单暴涨 10 倍。 Hermes 用三层结构 + 几条铁律避免这件事。
Agent 的成本 90% 来自 input tokens。一个 10 轮对话的 turn,每轮把 8K system prompt 重发一次, 就是 80K input tokens。换成钱:以 Claude Opus 计算(input $15/M tokens), 这次对话光 system prompt 就花 $1.20。如果 prompt cache 命中,价格降到 $0.12—— 10 倍差距。Agent 工程里没什么比这条线更值钱了。
5.1Prompt Cache 是什么
Prompt cache 的核心想法:同一段 prompt 前缀被反复使用时,server 端缓存它的 KV-state, 下次相同前缀的请求只算"增量"部分的算力。
主流 provider 都有:
| Provider | 缓存方式 | 费率(cached input vs normal input) | TTL |
|---|---|---|---|
| Anthropic | 显式(cache_control 标记) |
cached: 10% (read) / 125% (write) | 5 min / 1 hour 两档 |
| OpenAI | 自动(≥1024 tokens 前缀) | cached: 50% | 5–10 min |
| DeepSeek | 自动 | cached: 10% | 动态 |
| Google Gemini | 显式(context caching API) | cached: 25% + 小时存储费 | 用户配置 |
无论哪家,cache 命中的条件都是:发送的 messages 前缀和上次逐 token 完全一致。 哪怕中间多一个空格、少一个标点,cache 就破。
5.2System Prompt 的三层结构
Hermes 把 system prompt 切成三层,每层有不同的稳定性。看
agent/system_prompt.py:60-77:
agent/system_prompt.py:60-77
def build_system_prompt_parts(agent, system_message=None) -> Dict[str, str]:
"""Assemble the system prompt as three ordered parts.
Returns a dict with three keys:
* stable — identity, tool guidance, skills prompt,
environment hints, platform hints, model-family
operational guidance.
* context — context files (AGENTS.md, .cursorrules, etc.)
and caller-supplied system_message.
* volatile — memory snapshot, user profile, external
memory provider block, timestamp line.
"""
stable_parts = []
context_parts = []
volatile_parts = []
...
return {
"stable": "\n\n".join(stable_parts),
"context": "\n\n".join(context_parts),
"volatile": "\n\n".join(volatile_parts),
}
三层的稳定性对照:
| 层 | 包含 | 稳定性 | 变化触发条件 |
|---|---|---|---|
| stable | SOUL.md 身份、工具用法、模型家族指导 | 整个 session 不变 | 切 session 或重启 |
| context | AGENTS.md、.cursorrules、调用方 system_message | session 内一般不变 | 切工作目录、用户改文件 |
| volatile | Memory 快照、user profile、时间戳 | 每轮可能变 | 每次 turn / 内存更新 |
拼接顺序是 stable + context + volatile,从最稳定到最易变。
这个顺序极其重要——它直接决定 cache 命中率。
为什么这个顺序
Prompt cache 命中的边界是最长公共前缀。如果你把 volatile 放在最前面, 所有后面的稳定内容也跟着被破坏。所以要让最稳定的在最前。
flowchart TB
subgraph Bad["❌ 坏的顺序:volatile + stable"]
direction TB
B1["Turn 1: [时间戳-T1] [身份] [工具] [AGENTS.md]"]
B2["Turn 2: [时间戳-T2] [身份] [工具] [AGENTS.md]"]
B1 -.->|"T1 != T2 ⇒ 前缀全变"| B2
BAll["⚠️ 全部 cache miss"]:::warn
B2 --> BAll
end
subgraph Good["✅ 好的顺序:stable + context + volatile"]
direction TB
G1["Turn 1: [身份] [工具] [AGENTS.md] [时间戳-T1]"]
G2["Turn 2: [身份] [工具] [AGENTS.md] [时间戳-T2]"]
G1 -.->|"前缀完全相同"| G2
GHit["✓ 前缀 cache 命中
只有末尾时间戳是 miss"]:::good
G2 --> GHit
end
classDef warn fill:#faf0e3,stroke:#b85c00,color:#b85c00
classDef good fill:#ecf3eb,stroke:#2f5d3a,color:#2f5d3a
5.3每层装什么
Stable 层 — 不变的灵魂
看 system_prompt.py 第 84–254 行:
agent/system_prompt.py:84-150 (节选)
# Agent 身份(来自 SOUL.md 或 DEFAULT_AGENT_IDENTITY)
_soul_content = _r.load_soul_md()
stable_parts.append(_soul_content)
# 工具具体使用指导(条件加入)
if "memory" in agent.valid_tool_names:
tool_guidance.append(MEMORY_GUIDANCE)
if "session_search" in agent.valid_tool_names:
tool_guidance.append(SESSION_SEARCH_GUIDANCE)
if "todo" in agent.valid_tool_names:
tool_guidance.append(TODO_GUIDANCE)
# 工具调用纪律(某些模型容易"光说不练")
if agent.valid_tool_names:
_enforce = agent._tool_use_enforcement
if _enforce is True or agent.model in TOOL_USE_ENFORCEMENT_MODELS:
stable_parts.append(TOOL_USE_ENFORCEMENT_GUIDANCE)
# 模型家族特定指导(Gemini / Qwen / GPT 各有不同 quirk)
model_family_block = _get_model_family_guidance(agent.model)
if model_family_block:
stable_parts.append(model_family_block)
设计精神:
- 条件加入指导:没启用某工具就不加它的指导。避免无意义的 token。
- 模型家族区分:Gemini 倾向"过度补全",加一段"answer concisely"; Qwen 在 reasoning 后偶尔忘记 tool_call_id,加一段提醒……每个 block 都是踩坑得来的。
- 所有内容启动时拼好,session 内永不重算。
Context 层 — 工作区相关
agent/system_prompt.py:260-270
if system_message is not None:
context_parts.append(system_message)
if not agent.skip_context_files:
context_files_prompt = _r.build_context_files_prompt(
cwd=_context_cwd,
skip_soul=_soul_loaded,
)
if context_files_prompt:
context_parts.append(context_files_prompt)
build_context_files_prompt() 扫描当前工作目录,自动发现并包括:
AGENTS.md— 项目对 AI 的开发指南(Hermes 本身有 53KB 的).cursorrules— Cursor IDE 用的规则文件CLAUDE.md— Claude Code 用的指南- 用户自定义的其他
.md
Volatile 层 — 每轮可能变
agent/system_prompt.py:272-280
# Memory snapshot — 上次 sync 的内存状态
memory_block = agent.memory_manager.build_system_prompt()
if memory_block:
volatile_parts.append(memory_block)
# User profile — USER.md
user_profile = _r.load_user_profile()
if user_profile:
volatile_parts.append(user_profile)
# 时间戳 + session + model — 总是最后一行
volatile_parts.append(f"Current time: {now_iso()}\nSession: {sid}\nModel: {agent.model}")
注意时间戳——它每轮都变。所以 volatile 层就算第一次也注定 cache miss。 但因为它在最末尾,只影响它自己之后(其实没有了),前面 stable + context 该命中还是命中。
5.4cache_control 标记(Anthropic)
Anthropic 的 cache 是显式——你要主动加 cache_control 字段告诉它
"在这里设个 cache breakpoint"。最多 4 个 breakpoints。Hermes 的标注策略:
# Anthropic 兼容 endpoint 的 messages 示例
[
{
"role": "system",
"content": [
{"type": "text", "text": stable_part},
{"type": "text", "text": context_part,
"cache_control": {"type": "ephemeral"}}, # ← 缓存到这里
{"type": "text", "text": volatile_part} # volatile 不被 cache
]
},
{"role": "user", "content": "..."},
...
]
这个 cache_control 标记的语义:"从消息开头到这里为止,是缓存目标"。
Hermes 通常在 context_part 末尾标记,volatile 部分不进缓存。
tools 的 cache 标记
Tools schemas 数组本身也算 input tokens——可能上千 tokens。Anthropic 允许给 tools 加 cache_control:
tools = [
{
"type": "function",
"function": {...},
},
...
{
"type": "function",
"function": {...},
"cache_control": {"type": "ephemeral"} # 最后一个工具标记
}
]
Hermes 在 tools 列表末尾自动打标记。所以工具 schema 的 cache 命中率非常高—— 只要你不改 toolset,整个 session 都命中。
5.5什么操作会破缓存
Hermes 的 AGENTS.md 里有这样一条铁律:
Hermes-Agent ensures caching remains valid throughout a conversation. Do NOT implement changes that would:
- Alter past context mid-conversation
- Change toolsets mid-conversation
- Reload memories or rebuild system prompts mid-conversation
具体来说,下面这些操作会让 cache 报废:
| 动作 | 破缓存吗 | 原因 |
|---|---|---|
| 追加新消息(user / assistant) | 否 | 纯 append,前缀不变 |
| 修改 system prompt | 是 | 前缀第一段就变了 |
| 修改 tools 列表(即使只是顺序) | 是 | tools 在 prompt cache 范围内 |
| 在历史中间插入消息 | 是 | 前缀变了 |
| 修改历史消息(编辑过去) | 是 | 同上 |
| 把 volatile 内容(时间戳)放进 stable | 是 | 每轮变化的内容在前缀范围 |
| 每轮重算 system prompt(即使内容相同) | 不应该 | 字节级要一致,再算一次有风险 |
正确做法:append-only
所有动态信息只 append 到末尾。Hermes 这条规则被多个机制实现:
- Skills 加载:不改 system prompt,而是追加一条 user message。 包含 SKILL.md 内容。这样 system prompt cache 不破。
- Memory prefetch:每轮把检索结果追加到 volatile 层。 stable 和 context 不变。
- 工具结果:作为新的 tool message append。
- 用户中途调指令(比如 /model):不立即生效,下个 session 才生效。
或者用
--nowflag 显式接受破缓存。
/skills install 默认deferred invalidation——
装完不立即重建 prompt,等下个 session 自动生效。如果你急着要:
/skills install --now,承担一次 cache miss 的成本。
这条原则贯穿所有"会修改 prompt"的命令。
5.6Compaction:当 cache 救不了你
Cache 优化的是"重复前缀"。但当对话越来越长,前缀本身就要塞不下 context window。 这时就要用 compaction:
Anthropic 2025 年的 context engineering 文章里把 compaction 称为 "the first lever in context engineering to drive better long-term coherence by distilling the contents in a high-fidelity manner."
Hermes 的 compaction 机制:
agent/context_compression.py (摘要)
def compress_context(messages, system_prompt, max_tokens, ...):
"""把长 messages 列表压成短消息列表。
1. 留下最后 N 条消息(最近的,仍然 high-fidelity)。
2. 把前面的部分喂给一个 auxiliary LLM(小模型/便宜模型),
生成结构化总结。
3. 用 [system, <总结消息>, ...最近 N 条] 替换原 messages。
"""
keep_recent = messages[-RECENT_KEEP:]
history_to_compress = messages[:-RECENT_KEEP]
summary = aux_llm.summarize(history_to_compress,
goal="preserve facts, decisions, errors")
new_messages = [
{"role": "user", "content": f"[Conversation summary: {summary}]"},
*keep_recent,
]
return new_messages
关键设计选择:
- 用便宜模型做 summary:Hermes 配置一个 auxiliary client, 默认是个便宜小模型(如 Claude Haiku、GPT-4o-mini)。压缩本身不该贵。
- 结构化 prompt:让总结保留事实、决策、错误,丢掉对话客套。
- 留 N 条最近的:最近的内容仍然原样,让模型不丢"上下文动量"。
- 压缩后开新 session 还是同 session?Hermes 默认同 session 内替换 messages, 但有些场景(cron 任务)用 parent_session_id 链接到新 session。
5.7Just-In-Time Context(懒加载)
Anthropic 2025 提的第二招:不要预加载所有数据进 prompt,让 Agent 用工具按需拉取。
反模式
# BAD: 把整个文件预先塞进 prompt
system_prompt = """You are a code reviewer. Here are all the files:
File 1: src/auth.py
[完整内容,2000 lines]
File 2: src/db.py
[完整内容,3000 lines]
...
"""
这会让 system prompt 巨大,cache miss 成本巨大,而且模型可能只用其中一两个文件。
JIT 模式
# GOOD: 只放索引,模型自己决定读哪个
system_prompt = """You are a code reviewer. The repo has these files:
- src/auth.py
- src/db.py
- src/api/users.py
...
Use the read_file tool to view file contents when needed.
"""
模型用 read_file 工具按需读。这正是 Claude Code、Cursor、Hermes 干的事——
它们从不预先 dump 所有代码。
对比:
| 预加载 | JIT | |
|---|---|---|
| System prompt 体积 | 大(含全部数据) | 小(只含索引) |
| 每轮成本 | 所有数据都付钱 | 只为读到的付钱 |
| Cache 命中 | 一旦数据变就全破 | system 不变,永远命中 |
| 模型注意力 | 分散在所有数据上 | 聚焦于当前需要的 |
| 开发负担 | 需要决定塞什么 | 需要写好的工具描述 |
Tool Result Clearing(Anthropic 2025)
JIT 还有个进阶:模型读完文件用了,清掉旧的 tool result,只留 assistant 的总结。 Anthropic 在 2025 年把这做成 Claude API 一级功能:
# Anthropic API 的 tool_choice 现在支持 clear_tool_results
client.messages.create(
...
tool_choice={"type": "auto"},
clear_tool_results_above="some_message_id", # 清掉旧的
)
Hermes 还没完全用上这个新 API(写作时是 2026.05),但代码层面有类似机制: 压缩时优先丢老的 tool result,保留 assistant 的文本总结。
5.8实战:Skills 的注入策略
这一节回到具体场景:用户输入 /skills install fortune。怎么做才不破 cache?
方案 A(坏):把新 skill 加到 system prompt。立刻破整个 cache。
方案 B(好——Hermes 的做法):
- 下载 skill 到
~/.hermes/skills/。 - 不修改 system prompt。
- 下次用户输入
/fortune或者新 session 开始时,自然加载新 skill。
当用户调用 /fortune:
# 不是把 SKILL.md 塞进 system prompt
# 而是作为 user message append 进去
messages.append({
"role": "user",
"content": f"""[Skill activated: fortune]
{SKILL.md 内容}
请按照这个 skill 的指导回答用户接下来的问题。
"""
})
这样 system prompt 和 tools 缓存照样命中。skill 内容作为 user message 也参与缓存—— 下次同一 skill 被启用,从这条 user message 开始的前缀也能命中(如果消息内容字节级一致)。
5.9怎么衡量你的 cache 命中率
不同 provider 在 response 里都返回缓存统计:
# Anthropic 响应中的 usage 字段
{
"usage": {
"input_tokens": 200, # 本次真正"算"的 input
"cache_creation_input_tokens": 0, # 写 cache 的 input
"cache_read_input_tokens": 8000, # 从 cache 读的 input
"output_tokens": 150
}
}
命中率 = cache_read / (cache_read + input_tokens + cache_creation)。 理想是 > 90%。如果你看到 < 50%,肯定有某处反复破 cache。
Hermes 提供 /insights 命令查看本 session 的 cache 统计:
$ hermes
> /insights
─── Cache Performance ──────────────────────────
Read: 12,580 tokens (cached, $0.013 saved)
Write: 1,420 tokens (this session built cache)
Miss: 320 tokens ← 健康
Hit rate: 96%
─── Cost Breakdown ─────────────────────────────
Input: $0.0042
Cached: $0.0095
Output: $0.0231
Total: $0.0368
5.102025–2026 Cache 协议新进展
2025–2026 各家在 cache 上又出了几个新功能,影响 cache 设计:
① Anthropic 1-hour Cache TTL(2025 GA)
原本 Anthropic cache 默认 5 分钟 TTL。2025 年 GA 了1 小时 TTL 选项, 写 cache 时按 2 倍价格收费(vs 5 分钟的 1.25 倍),但适合长期不变的大块前缀 (比如静态指令、整套工具 schema、AGENTS.md 等)。
选 TTL 的简单经济算法:
- 同一前缀未来 1 小时被命中 ≥ 2 次 → 选 1 小时 TTL 划算。
- 突发短任务 → 5 分钟 TTL 即可。
Hermes 默认 5 分钟。生产部署如果你的 Agent 长期固定 + 流量稳定,改 1 小时省 ~30%。
② OpenAI Prompt Caching 自动化(2024.10 GA)
OpenAI 不需要显式标记。它自动检测"前缀重复 ≥ 1024 tokens"就 cache, cache hit 享 50% 折扣。开发者透明——但你仍然要避免"破坏前缀"。 和 Anthropic 哲学相同,机制不同。
③ DeepSeek Auto Caching
DeepSeek 在 2024 年初早早做了 auto caching:缓存 hit 收原价 10%, 比 OpenAI 还便宜。配 V4/R2 用,性价比相当强。
④ Tool Result Clearing(Anthropic 2025)
上一节我们提过 JIT context 和它的关系。具体协议层:
client.messages.create(
...
cache_control={"type": "ephemeral", "ttl": "1h"},
# 2025 新字段:让某个时间点之前的 tool result 清掉
clear_tool_results_above="msg_abc",
)
Hermes 还没用上这个新参数(写作时 2026.05),但代码里已经有等价机制—— 在 Compaction 时优先丢老 tool result、保留 assistant 总结。
⑤ Extended Context Cache(2026 初)
2026 年初 OpenAI 推 GPT-5 的"context cache"——和 prompt cache 类似但 TTL 长达 24 小时、按存储量按时收费。适合"上传一份 10MB 文档让模型反复回答"这种场景。 Hermes 内置 memory 系统某种意义上是这个能力的客户端实现。
⑥ 多模态 cache
2025 起,图片/视频 input 也支持 cache(如 Claude vision 同一张图被多次问)。 这对 GUI Agent / vision 类应用极其关键——一张 screenshot 反复分析时不再每次重算。
② 用 Anthropic 4 个 cache_control breakpoints 优势全占——别只用一个。
③ 跨 session 共用同一份 system prompt + AGENTS.md,让多用户共享 cache。
④ 监控 cache hit rate,目标 > 90%;< 70% 必须查为什么。
5.11本章带走的
- Prompt cache 是 Agent 经济的命脉。命中 vs 未命中差 5–10 倍成本。
- 所有 cache 命中的条件都是:发送前缀逐字节和上次一致。
- Hermes 用三层结构组织 system prompt:stable / context / volatile, 按稳定性从前到后排列,保证 cache 命中边界尽可能靠后。
- 动态内容只append 到末尾,永不 insert/edit 中间。
- Anthropic 用显式
cache_control标记,其他家自动。Hermes 的标记策略:context 末尾 + tools 末尾。 - Skills 通过 user message 注入,不污染 system prompt。这是 Hermes 工程最核心的一招。
- 当 context 接近窗口上限时用 Compaction(让小模型总结)。
- JIT context:放索引、不放全数据,让 Agent 用工具按需拉取。
- 用
/insights或 provider 的 usage 字段监控 cache 命中率,> 90% 才健康。
章末练习
-
Easy
下列哪个动作会破 cache?
- (a) 在历史中间删一条消息
- (b) 在最后追加一条 user message
- (c) 把当前时间放在 system prompt 第一行
- (d) 修改第 5 条消息的 1 个标点
- (e) 添加新工具到 tools 列表
- Easy 为什么 Hermes 三层 prompt 顺序必须是 stable → context → volatile,不能反过来?
-
Medium
读
agent/system_prompt.py,列出 stable 层里有哪些条件分支 (if "X" in agent.valid_tool_names:这种)。 想清楚:每个 if 是怎样在不同 session 之间影响 cache 行为的? - Medium 设计一个场景:用户每天早上 9 点都让 Agent 总结他的 RSS。每天的 RSS 内容不同, 但 prompt 模板不变。怎么组织 prompt 让 cache 命中最大化?把每天变化的部分放到哪里?
- Hard 把"JIT context"应用到一个具体场景:你要 Agent 在 50,000 行代码库上工作。 列出 5 个工具的 schema,让 Agent 不预加载任何代码就能完成任务。
- Hard Anthropic 的 5 分钟 vs 1 小时 cache TTL 选择——什么场景选 5 分钟划算, 什么场景选 1 小时?写一个决策树。提示:考虑 cache_creation_tokens 在两种 TTL 下的价格。