推测式 Prompt 缓存
预测性缓存策略进一步提升效率
🌐 查看英文原文 | 源码 Notebook
推测式 Prompt 缓存
本教程演示”推测式 Prompt 缓存”——一种通过在用户还在构思问题时就预热缓存来减少首 token 时间(TTFT)的模式。
不使用推测式缓存: 1. 用户输入问题(3 秒) 2. 用户提交问题 3. API 加载上下文到缓存并生成回复
使用推测式缓存: 1. 用户开始输入(缓存预热立即开始) 2. 用户继续输入(缓存在后台完成预热) 3. 用户提交问题 4. API 使用已预热的缓存生成回复
环境配置
首先,让我们安装所需的包:
%pip install anthropic httpx --quietNote: you may need to restart the kernel to use updated packages.
import asyncio
import copy
import datetime
import time
import httpx
from anthropic import AsyncAnthropic
# Configuration constants
MODEL = "claude-sonnet-4-6"
SQLITE_SOURCES = {
"btree.h": "https://sqlite.org/src/raw/18e5e7b2124c23426a283523e5f31a4bff029131b795bb82391f9d2f3136fc50?at=btree.h",
"btree.c": "https://sqlite.org/src/raw/63ca6b647342e8cef643863cd0962a542f133e1069460725ba4461dcda92b03c?at=btree.c",
}
DEFAULT_CLIENT_ARGS = {
"system": "You are an expert systems programmer helping analyze database internals.",
"max_tokens": 4096,
"temperature": 0,
}辅助函数
让我们设置用于下载大量上下文并准备消息的函数:
async def get_sqlite_sources() -> dict[str, str]:
print("Downloading SQLite source files...")
source_files = {}
start_time = time.time()
async with httpx.AsyncClient(timeout=30.0) as client:
tasks = []
async def download_file(filename: str, url: str) -> tuple[str, str]:
response = await client.get(url, follow_redirects=True)
response.raise_for_status()
print(f"Successfully downloaded {filename}")
return filename, response.text
for filename, url in SQLITE_SOURCES.items():
tasks.append(download_file(filename, url))
results = await asyncio.gather(*tasks)
source_files = dict(results)
duration = time.time() - start_time
print(f"Downloaded {len(source_files)} files in {duration:.2f} seconds")
return source_files
async def create_initial_message():
sources = await get_sqlite_sources()
# Prepare the initial message with the source code as context.
# A Timestamp is included to prevent cache sharing across different runs.
initial_message = {
"role": "user",
"content": [
{
"type": "text",
"text": f"""
Current time: {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
Source to Analyze:
btree.h:
```c
{sources["btree.h"]}btree.c:
{sources["btree.c"]}
```""",
"cache_control": {"type": "ephemeral"},
}
],
}
return initial_message
async def sample_one_token(client: AsyncAnthropic, messages: list):
"""Send a single-token request to warm up the cache"""
args = copy.deepcopy(DEFAULT_CLIENT_ARGS)
args["max_tokens"] = 1
await client.messages.create(
messages=messages,
model=MODEL,
**args,
)
def print_query_statistics(response, query_type: str) -> None:
print(f"\n{query_type} query statistics:")
print(f"\tInput tokens: {response.usage.input_tokens}")
print(f"\tOutput tokens: {response.usage.output_tokens}")
print(f"\tCache read input tokens: {getattr(response.usage, 'cache_read_input_tokens', '---')}")
print(
f"\tCache creation input tokens: {getattr(response.usage, 'cache_creation_input_tokens', '---')}"
)示例 1:标准 Prompt 缓存(不使用推测式缓存)
首先,让我们看看标准 prompt 缓存是如何工作的。用户输入问题后,我们将整个上下文 + 问题发送给 API:
async def standard_prompt_caching_demo():
client = AsyncAnthropic()
# Prepare the large context
initial_message = await create_initial_message()
# Simulate user typing time (in real app, this would be actual user input)
print("User is typing their question...")
await asyncio.sleep(3) # Simulate 3 seconds of typing
user_question = "What is the purpose of the BtShared structure?"
print(f"User submitted: {user_question}")
# Now send the full request (context + question)
full_message = copy.deepcopy(initial_message)
full_message["content"].append(
{"type": "text", "text": f"Answer the user's question: {user_question}"}
)
print("\nSending request to API...")
start_time = time.time()
# Measure time to first token
first_token_time = None
async with client.messages.stream(
messages=[full_message],
model=MODEL,
**DEFAULT_CLIENT_ARGS,
) as stream:
async for text in stream.text_stream:
if first_token_time is None and text.strip():
first_token_time = time.time() - start_time
print(f"\n🕐 Time to first token: {first_token_time:.2f} seconds")
break
# Get the full response
response = await stream.get_final_message()
total_time = time.time() - start_time
print(f"Total response time: {total_time:.2f} seconds")
print_query_statistics(response, "Standard Caching")
return first_token_time, total_time# Run the standard demo
standard_ttft, standard_total = await standard_prompt_caching_demo()Downloading SQLite source files...
Successfully downloaded btree.h
Successfully downloaded btree.c
Downloaded 2 files in 0.30 seconds
User is typing their question...
User submitted: What is the purpose of the BtShared structure?
Sending request to API...
🕐 Time to first token: 20.87 seconds
Total response time: 28.32 seconds
Standard Caching query statistics:
Input tokens: 22
Output tokens: 362
Cache read input tokens: 0
Cache creation input tokens: 151629
示例 2:推测式 Prompt 缓存
现在让我们看看推测式 prompt 缓存如何通过在用户输入时预热缓存来改善 TTFT:
async def speculative_prompt_caching_demo():
client = AsyncAnthropic()
# The user has a large amount of context they want to interact with,
# in this case it's the sqlite b-tree implementation (~150k tokens).
initial_message = await create_initial_message()
# Start speculative caching while user is typing
print("User is typing their question...")
print("🔥 Starting cache warming in background...")
# While the user is typing out their question, we sample a single token
# from the context the user is going to be interacting with with explicit
# prompt caching turned on to warm up the cache.
cache_task = asyncio.create_task(sample_one_token(client, [initial_message]))
# Simulate user typing time
await asyncio.sleep(3) # Simulate 3 seconds of typing
user_question = "What is the purpose of the BtShared structure?"
print(f"User submitted: {user_question}")
# Ensure cache warming is complete
await cache_task
print("✅ Cache warming completed!")
# Prepare messages for cached query. We make sure we
# reuse the same initial message as was cached to ensure we have a cache hit.
cached_message = copy.deepcopy(initial_message)
cached_message["content"].append(
{"type": "text", "text": f"Answer the user's question: {user_question}"}
)
print("\nSending request to API (with warm cache)...")
start_time = time.time()
# Measure time to first token
first_token_time = None
async with client.messages.stream(
messages=[cached_message],
model=MODEL,
**DEFAULT_CLIENT_ARGS,
) as stream:
async for text in stream.text_stream:
if first_token_time is None and text.strip():
first_token_time = time.time() - start_time
print(f"\n🚀 Time to first token: {first_token_time:.2f} seconds")
break
# Get the full response
response = await stream.get_final_message()
total_time = time.time() - start_time
print(f"Total response time: {total_time:.2f} seconds")
print_query_statistics(response, "Speculative Caching")
return first_token_time, total_time# Run the speculative caching demo
speculative_ttft, speculative_total = await speculative_prompt_caching_demo()Downloading SQLite source files...
Successfully downloaded btree.h
Successfully downloaded btree.c
Downloaded 2 files in 0.36 seconds
User is typing their question...
🔥 Starting cache warming in background...
User submitted: What is the purpose of the BtShared structure?
✅ Cache warming completed!
Sending request to API (with warm cache)...
🚀 Time to first token: 1.94 seconds
Total response time: 8.40 seconds
Speculative Caching query statistics:
Input tokens: 22
Output tokens: 330
Cache read input tokens: 151629
Cache creation input tokens: 0
性能对比
让我们比较结果,看看推测式缓存带来的好处:
print("=" * 60)
print("PERFORMANCE COMPARISON")
print("=" * 60)
print("\nStandard Prompt Caching:")
print(f" Time to First Token: {standard_ttft:.2f} seconds")
print(f" Total Response Time: {standard_total:.2f} seconds")
print("\nSpeculative Prompt Caching:")
print(f" Time to First Token: {speculative_ttft:.2f} seconds")
print(f" Total Response Time: {speculative_total:.2f} seconds")
ttft_improvement = (standard_ttft - speculative_ttft) / standard_ttft * 100
total_improvement = (standard_total - speculative_total) / standard_total * 100
print("\n🎯 IMPROVEMENTS:")
print(
f" TTFT Improvement: {ttft_improvement:.1f}% ({standard_ttft - speculative_ttft:.2f}s faster)"
)
print(
f" Total Time Improvement: {total_improvement:.1f}% ({standard_total - speculative_total:.2f}s faster)"
)============================================================
PERFORMANCE COMPARISON
============================================================
Standard Prompt Caching:
Time to First Token: 20.87 seconds
Total Response Time: 28.32 seconds
Speculative Prompt Caching:
Time to First Token: 1.94 seconds
Total Response Time: 8.40 seconds
🎯 IMPROVEMENTS:
TTFT Improvement: 90.7% (18.93s faster)
Total Time Improvement: 70.4% (19.92s faster)
关键要点
- 推测式缓存显著降低了 TTFT——通过在用户输入时预热缓存
- 该模式在跨查询复用的大上下文(>1000 tokens)时效果最佳
- 实现很简单——只需在用户输入时发送一个 1-token 的请求
- 缓存预热与用户输入并行进行,有效地”隐藏”了缓存创建时间
最佳实践
- 尽早开始缓存预热(例如,当用户聚焦到输入框时)
- 预热和实际请求使用完全相同的上下文,以确保缓存命中
- 监控
cache_read_input_tokens以验证缓存命中 - 添加时间戳以防止不同会话之间不必要的缓存共享
Tip
中文读者提示:本章节的代码和输出为英文原文。如需理解具体实现细节,可参考上方中文导读和代码注释。如有疑问,欢迎在 GitHub Issues 讨论。