10.3 响应缓存:CacheHint 与 CacheMode 本节摘要:本节讲客户端的性能优化——响应缓存。核心是两端协作:服务端在响应上标注 (可缓存性、键),客户端用 决定是否走缓存。这套机制让昂贵的工具调用(如复杂计算、慢查询)结果可复用,避免重复调用。本节讲透缓存的两端、命中与失效规则,以及「客户端缓存」与「服务端缓存」的边界。 一、为什么需要响应缓存 先看一个性能痛点。假设你有个昂贵工具: 如果客户端在短时间内多次调它(同样参数),每次都要等 10 秒——重复计算浪费资源。响应缓存解决这个:第一次调完存结果,后续相同参数直接返回缓存。 二、缓存的两端协作 MCP 的响应缓存是两端协作的——服务端标注、客户端决策: 服务端标注 :告诉客户端「这个响应可缓存、缓存多久、用什么键」。
本节摘要:本节讲客户端的性能优化——响应缓存。核心是两端协作:服务端在响应上标注
CacheHint(可缓存性、键),客户端用CacheMode决定是否走缓存。这套机制让昂贵的工具调用(如复杂计算、慢查询)结果可复用,避免重复调用。本节讲透缓存的两端、命中与失效规则,以及「客户端缓存」与「服务端缓存」的边界。
先看一个性能痛点。假设你有个昂贵工具:
@mcp.tool() async def expensive_analysis(data: str) -> dict: """Run expensive analysis (takes 10 seconds).""" await asyncio.sleep(10) # 模拟昂贵计算 return {"result": "..."}
如果客户端在短时间内多次调它(同样参数),每次都要等 10 秒——重复计算浪费资源。响应缓存解决这个:第一次调完存结果,后续相同参数直接返回缓存。
第一次调用 expensive_analysis("data_x"): 服务端计算 10 秒 → 返回结果 客户端存缓存:["data_x" → 结果] 第二次调用 expensive_analysis("data_x"): 客户端发现缓存命中 → 直接返回(0 秒) (不调服务端)
MCP 的响应缓存是两端协作的——服务端标注、客户端决策:
服务端侧(标注): @mcp.tool(annotations=ToolAnnotations(...)) def expensive(...): return result # SDK 在响应上标注 CacheHint 响应含: { content: [...], structuredContent: {...}, _meta: { cache_hint: {ttl: 300, key: "..."} ← 服务端标注 } } 客户端侧(决策): async with Client(url, cache_mode="enabled") as client: # 客户端看到 cache_hint,决定缓存 result = await client.call_tool(...) # 下次相同参数,客户端走缓存
服务端标注 CacheHint:告诉客户端「这个响应可缓存、缓存多久、用什么键」。客户端用 CacheMode:决定是否启用缓存、如何用缓存。
服务端用 CacheHint 标注响应的可缓存性:
from mcp.server import MCPServer from mcp.server.caching import CacheHint mcp = MCPServer("Demo") @mcp.tool(cache_hint=CacheHint( ttl=300, # 缓存 5 分钟 key="explicit_key", # 可选,显式缓存键 )) async def expensive_analysis(data: str) -> dict: """Run expensive analysis.""" return do_expensive_work(data)
CacheHint 的关键字段:
| 字段 | 作用 | 说明 |
|---|---|---|
ttl |
生存时间(秒) | 多久后缓存失效 |
key |
缓存键(可选) | 默认按工具名+参数自动生成 |
服务端标注后,SDK 在响应的 _meta 里附上 cache_hint,客户端据此决定缓存。
客户端用 CacheMode 控制缓存行为:
from mcp import Client, CacheMode async with Client( url, cache_mode=CacheMode.ENABLED, # 启用缓存 ) as client: result = await client.call_tool("expensive_analysis", {"data": "x"}) # 结果被缓存 # 相同参数再调,走缓存 result2 = await client.call_tool("expensive_analysis", {"data": "x"}) # result2 来自缓存,不调服务端
CacheMode 的常见取值:
| 取值 | 行为 |
|---|---|
ENABLED |
启用缓存(看 CacheHint 决定缓存哪些) |
DISABLED |
不缓存,每次都调服务端 |
FORCE |
强制缓存所有响应(忽略 CacheHint 限制) |
缓存按「键」命中。默认键是「工具名 + 参数」的哈希:
call_tool("expensive_analysis", {"data": "x"}) │ ▼ 生成默认键 键 = hash("expensive_analysis" + "x") │ ▼ 查缓存 缓存里有这个键? 有 → 命中,返回缓存(不调服务端) 无 → 未命中,调服务端,存结果
命中规则:
缓存不会永远有效,几种失效情况:
| 失效原因 | 说明 |
|---|---|
| TTL 过期 | ttl=300 的缓存,5 分钟后失效 |
| 显式失效 | 服务端发失效通知(如数据变了) |
| 客户端重启 | 内存缓存重启后清空(持久化缓存除外) |
| 服务端重启 | 服务端可能发能力变更,客户端清理 |
💡 技巧:TTL 是缓存准确性与性能的权衡。TTL 短(如 30 秒)准确但缓存命中率低;TTL 长(如 1 小时)命中率高但可能返回过时数据。根据数据变化频率选——变化快的(实时数据)用短 TTL,变化慢的(历史数据、计算结果)用长 TTL。
这里要区分两种「缓存」,它们不冲突,层次不同:
| 缓存类型 | 在哪 | 干什么 |
|---|---|---|
| 客户端缓存 | 客户端内存 | 避免重复调用服务端(本节讲的) |
| 服务端缓存 | 服务端内部 | 服务端自己避免重复计算(redis 等) |
客户端调用 expensive_analysis("x"): │ ▼ 客户端缓存检查 命中? → 返回缓存(根本不调服务端) 未命中? │ ▼ 调服务端 服务端内部缓存检查: 命中? → 返回缓存(不重算) 未命中? → 计算,存服务端缓存,返回
客户端缓存避免「客户端→服务端」的往返;服务端缓存避免「服务端的实际计算」。两者配合,最大化性能。本节讲的是客户端缓存,服务端缓存由你自己实现(用 redis 等)。
缓存不是万能,有些场景不适合:
| 适合缓存 | 不适合缓存 |
|---|---|
| 昂贵计算(分析、聚合) | 实时数据(当前股价、库存) |
| 历史数据(过去订单) | 随机结果(随机推荐) |
| 静态资源(配置、文档) | 写操作(创建、删除) |
| 变化慢的数据 | 用户私有数据(易串) |
⚠️ 注意:用户私有数据慎用缓存。如果缓存键不含用户标识,可能「用户 A 的数据被用户 B 命中」——严重安全问题。缓存在用户维度的数据时,缓存键必须含用户 ID,或干脆不缓存。
CacheHint、客户端用 CacheMode 决策。CacheHint(服务端):ttl(生存时间)、key(缓存键,可选)。CacheMode(客户端):ENABLED/DISABLED/FORCE。缓存清楚了,下一节讲订阅——资源变化的即时感知。