10.3 响应缓存:CacheHint 与 CacheMode


文档摘要

10.3 响应缓存:CacheHint 与 CacheMode 本节摘要:本节讲客户端的性能优化——响应缓存。核心是两端协作:服务端在响应上标注 (可缓存性、键),客户端用 决定是否走缓存。这套机制让昂贵的工具调用(如复杂计算、慢查询)结果可复用,避免重复调用。本节讲透缓存的两端、命中与失效规则,以及「客户端缓存」与「服务端缓存」的边界。 一、为什么需要响应缓存 先看一个性能痛点。假设你有个昂贵工具: 如果客户端在短时间内多次调它(同样参数),每次都要等 10 秒——重复计算浪费资源。响应缓存解决这个:第一次调完存结果,后续相同参数直接返回缓存。 二、缓存的两端协作 MCP 的响应缓存是两端协作的——服务端标注、客户端决策: 服务端标注 :告诉客户端「这个响应可缓存、缓存多久、用什么键」。

10.3 响应缓存:CacheHint 与 CacheMode

本节摘要:本节讲客户端的性能优化——响应缓存。核心是两端协作:服务端在响应上标注 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:服务端标注

服务端用 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:客户端决策

客户端用 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 过期 ttl=300 的缓存,5 分钟后失效
显式失效 服务端发失效通知(如数据变了)
客户端重启 内存缓存重启后清空(持久化缓存除外)
服务端重启 服务端可能发能力变更,客户端清理

💡 技巧:TTL 是缓存准确性与性能的权衡。TTL 短(如 30 秒)准确但缓存命中率低;TTL 长(如 1 小时)命中率高但可能返回过时数据。根据数据变化频率选——变化快的(实时数据)用短 TTL,变化慢的(历史数据、计算结果)用长 TTL。

七、客户端缓存 vs 服务端缓存

这里要区分两种「缓存」,它们不冲突,层次不同:

缓存类型 在哪 干什么
客户端缓存 客户端内存 避免重复调用服务端(本节讲的)
服务端缓存 服务端内部 服务端自己避免重复计算(redis 等)
客户端调用 expensive_analysis("x"): │ ▼ 客户端缓存检查 命中? → 返回缓存(根本不调服务端) 未命中? │ ▼ 调服务端 服务端内部缓存检查: 命中? → 返回缓存(不重算) 未命中? → 计算,存服务端缓存,返回

客户端缓存避免「客户端→服务端」的往返;服务端缓存避免「服务端的实际计算」。两者配合,最大化性能。本节讲的是客户端缓存,服务端缓存由你自己实现(用 redis 等)。

八、缓存的适用与不适用

缓存不是万能,有些场景不适合:

适合缓存 不适合缓存
昂贵计算(分析、聚合) 实时数据(当前股价、库存)
历史数据(过去订单) 随机结果(随机推荐)
静态资源(配置、文档) 写操作(创建、删除)
变化慢的数据 用户私有数据(易串)

⚠️ 注意:用户私有数据慎用缓存。如果缓存键不含用户标识,可能「用户 A 的数据被用户 B 命中」——严重安全问题。缓存在用户维度的数据时,缓存键必须含用户 ID,或干脆不缓存。

本节要点回顾

  1. 响应缓存两端协作:服务端标注 CacheHint、客户端用 CacheMode 决策。
  2. CacheHint(服务端):ttl(生存时间)、key(缓存键,可选)。
  3. CacheMode(客户端):ENABLED/DISABLED/FORCE
  4. 命中规则:工具名+参数相同、TTL 未过期 → 命中。
  5. 失效原因:TTL 过期、显式失效、重启、能力变更。
  6. TTL 是准确性与性能的权衡:变化快用短,变化慢用长。
  7. 客户端缓存 vs 服务端缓存:前者避免往返,后者避免重算,配合用。
  8. 用户私有数据慎用缓存,键要含用户 ID 或不缓存,避免串数据。

缓存清楚了,下一节讲订阅——资源变化的即时感知。


发布者: 作者: 灏天文库 转发
评论区 (0)
U