Current section

Files

Jump to
cmdc lib cmdc plugin builtin prompt_cache.ex
Raw

lib/cmdc/plugin/builtin/prompt_cache.ex

defmodule CMDC.Plugin.Builtin.PromptCache do
@moduledoc """
P1 Anthropic Prompt Caching 插件。
`before_request` 事件中检测是否为 Anthropic 模型,若是则选取最多
`max_cache_points`(默认 3)个消息位置打上 `cache_control` 标记,
通过 emit `{:update_message_metadata, list}` 通知 Agent 更新消息 metadata。
Provider 在构建 Anthropic 请求时读取 metadata 中的 `cache_control: true`
将对应消息的 content block 末尾追加 `{"cache_control": {"type": "ephemeral"}}`
非 Anthropic 模型(OpenAI、Gemini 等)自动跳过,不修改任何消息。
## 缓存点选取策略
按以下优先级选取,最多 `max_cache_points` 个:
1. system 消息(通常是最大的固定内容)
2. 最后一条 user 消息(对话的最新上下文)
3. 最早的 assistant + tool_result 消息对(历史工具调用结果)
## 配置
{CMDC.Plugin.Builtin.PromptCache,
max_cache_points: 3, # 最多打多少个缓存标记(Anthropic 限制最多 4 个)
min_tokens: 100 # 跳过估计 token 数低于此值的消息(避免无效缓存)
}
"""
@behaviour CMDC.Plugin
require Logger
@default_max_cache_points 3
@default_min_tokens 100
# ==========================================================================
# Plugin Callbacks
# ==========================================================================
@impl true
def init(opts) do
state = %{
max_cache_points: Keyword.get(opts, :max_cache_points, @default_max_cache_points),
min_tokens: Keyword.get(opts, :min_tokens, @default_min_tokens)
}
{:ok, state}
end
@impl true
def priority, do: 130
@impl true
def describe, do: "P1 Anthropic Prompt Caching:给关键消息追加 cache_control 标记"
@impl true
def handle_event({:before_request, messages}, state, ctx) do
if anthropic_model?(ctx.model) do
apply_cache_control(messages, state, ctx)
else
{:continue, state}
end
end
def handle_event(_event, state, _ctx), do: {:continue, state}
# ==========================================================================
# 私有 — 缓存控制应用
# ==========================================================================
defp apply_cache_control(messages, state, ctx) do
candidates = select_cache_candidates(messages, state)
if candidates == [] do
{:continue, state}
else
Logger.debug(
"[PromptCache] 为 #{length(candidates)} 条消息添加 cache_control session=#{ctx.session_id}"
)
updates = Enum.map(candidates, fn msg -> {msg.id, %{cache_control: true}} end)
{:emit, {:update_message_metadata, updates}, state}
end
end
defp select_cache_candidates(messages, state) do
system_msgs =
Enum.filter(messages, &(&1.role == :system and long_enough?(&1, state.min_tokens)))
user_msgs =
Enum.filter(messages, &(&1.role == :user and long_enough?(&1, state.min_tokens)))
last_user = List.last(user_msgs)
tool_result_msgs =
messages
|> Enum.filter(&(&1.role == :tool_result and long_enough?(&1, state.min_tokens)))
|> Enum.take(state.max_cache_points)
candidates =
[system_msgs, if(last_user, do: [last_user], else: []), tool_result_msgs]
|> List.flatten()
|> Enum.uniq_by(& &1.id)
|> Enum.take(state.max_cache_points)
candidates
end
defp long_enough?(%{content: content}, min_tokens) when is_binary(content) do
div(byte_size(content), 4) >= min_tokens
end
defp long_enough?(_, _), do: false
# ==========================================================================
# 私有 — Anthropic 模型检测
# ==========================================================================
defp anthropic_model?(model) when is_binary(model) do
String.contains?(model, "anthropic:") or String.contains?(model, "claude")
end
defp anthropic_model?(%{provider: :anthropic}), do: true
defp anthropic_model?(_), do: false
end