跳到主要内容

Semantic Cache(语义缓存)

Aidy 内置了语义缓存能力,通过静态配置 plugins.semantic_cache.* 与 route 级 plugin_config.semantic_cache 配置。

启用条件

Semantic Cache 插件只有在同时满足以下条件时,才会被挂进请求链路:

  1. 静态配置 plugins.semantic_cache.enable = true
  2. 静态配置中已提供完整运行所需参数:embedding_service_urlembedding_modelredis
  3. 当前 route 配置了 plugin_config.semantic_cache

如果以上任一条件不满足,Semantic Cache 不会进入当前 route 的请求链路。

另外,Semantic Cache 当前只挂在 chat 的 forward hook 上。因此即使路由启用了语义缓存,embeddings、models、options 与 unknown 请求也不会进入该插件。

链路位置

Semantic Cache 会优先于以下中间件执行:

  • upstream-ratelimit
  • egress-policy
  • guard
  • consumer
  • perf

也就是说,命中缓存时,请求会直接在网关内返回,不再继续进入后续上游调用链路。

静态配置

静态配置用于提供 embedding 服务与 Redis 向量存储能力,对应 plugins.semantic_cache.*。完整字段说明见 静态配置

其中最关键的字段是:

  • enable
  • embedding_service_url
  • embedding_model
  • embedding_api_key
  • embedding_dimensions
  • embedding_timeout_ms
  • redis
  • verbose_log

路由配置

route 级配置用于指定特定 route 的语义缓存策略,使用以下 proto:

常用字段说明:

  • cache_ttl_seconds:缓存存活时间,单位秒
  • similarity_threshold:向量距离阈值;越小代表要求越相近
  • distance_metric:距离度量方式,当前仅支持 cosine
  • message_countback:从末尾回看多少条消息参与 embedding
  • include_roles:哪些 role 会参与 embedding 文本构造
  • include_instructions:是否把 instructions 纳入 embedding 文本
  • include_tools_signature:是否把 tools 签名纳入缓存命名空间
  • isolate_by_model:是否按请求 model 隔离缓存命名空间

include_roles 与 include_instructions

include_roles 控制哪些 role 的消息参与构造 embedding 文本,是粗粒度的角色级开关。例如,include_roles 中配置了 system,则 system 角色的整条消息都会进入 embedding。

但 system 角色的消息内部通常包含多个部分:

组成部分说明
instructions(系统指令)核心行为指令,如"你是一个客服助手",定义模型的行为和角色
tools(工具定义)可用的 function/tool 列表及参数 schema
context/documents注入的背景知识、文档片段等
其他 metadata时间戳、用户信息、对话设置等

以下是一个完整的 Chat Completions 请求示例,展示了 system 角色消息中各部分的具体形态:

{
"model": "deepseek-v4",
"messages": [
{
"role": "system",
"content": [
{
"type": "text",
"text": "你是 Acme 公司的客服助手。请用礼貌、专业的语气回答用户问题。如果用户询问退换货政策,请引导用户查看 https://acme.com/returns 页面。"
},
{
"type": "text",
"text": "## 背景知识\n- 公司名称:Acme Inc.\n- 主营产品:智能家居设备\n- 退换货政策:30 天内无理由退换,需保留原包装\n- 客服电话:400-123-4567"
},
{
"type": "text",
"text": "当前时间:2026-05-24 14:30:00 CST\n用户 ID:user_abc123\n会员等级:Gold"
}
]
},
{
"role": "user",
"content": "我买了一个智能灯泡,用了两周不亮了,能退吗?"
}
],
"tools": [
{
"type": "function",
"function": {
"name": "query_order",
"description": "根据用户 ID 查询最近的订单列表",
"parameters": {
"type": "object",
"properties": {
"user_id": { "type": "string" }
}
}
}
},
{
"type": "function",
"function": {
"name": "create_ticket",
"description": "创建退换货工单",
"parameters": {
"type": "object",
"properties": {
"order_id": { "type": "string" },
"reason": { "type": "string" }
}
}
}
}
]
}

在这个请求中,system 消息的三个 text 块分别对应:

text 块对应部分内容
第 1 块instructions角色定义、行为规范、退换货引导规则
第 2 块context/documents公司背景、退换货政策、客服电话等注入知识
第 3 块metadata当前时间、用户 ID、会员等级等运行时信息

tools 数组则是独立于 system 消息的 tools(工具定义) 部分,包含 query_ordercreate_ticket 两个可用函数。

include_roles 包含 system 时,system 消息的所有 text 块都会参与 embedding 文本构造。而 include_instructions 进一步控制:只有系统指令部分(第 1 块)是否影响缓存 key 的计算。

include_instructions 开启时,上述请求中 instructions 内容("你是 Acme 公司的客服助手...")参与缓存 key。如果另一个路由的 instructions 不同(如"你是销售顾问"),即使用户问题完全相同,也会产生不同的缓存条目。

include_instructions 关闭时,instructions 的差异不会影响缓存 key。两个路由共享同一个"智能灯泡退换货"的缓存结果——只要用户问题和 tools 等一致。:当 system 角色已参与 embedding 时,可以单独决定 instructions 部分是否影响缓存 key。

为什么需要这个区分?

include_instructions效果
开启(默认)系统指令不同的请求视为不同缓存,缓存更精确但命中率降低
关闭系统指令不同但用户消息相同的请求共享缓存,命中率高但可能不够精确

实际场景: 假设两个路由配置了不同的 system instructions("你是客服" vs "你是销售"),但 tools 和 context 完全相同。如果希望 tools 变化时区分缓存,但 instructions 变化时不区分(共享热点缓存),就可以将 include_instructions 设为 false,让 instructions 不参与缓存 key 计算。

简而言之:include_roles 决定 system 角色是否参与,include_instructions 决定参与后 instructions 部分是否影响缓存 key。大多数场景保持默认(开启)即可。

运行时行为

插件进入链路后,会按以下顺序工作:

  1. 根据请求内容构造 embedding 文本
  2. 调用 embedding 服务生成向量
  3. 在 Redis 向量索引中按命名空间检索最相近条目
  4. 命中时直接返回缓存响应
  5. 未命中时继续请求后续链路;若最终响应为 2xx,则将响应写入缓存

当前缓存命名空间格式为:

sc:{tenant}:{route}:{model}:{tools_hash}

其中:

  • tenantroute 始终参与隔离
  • model 是否参与隔离由 isolate_by_model 决定
  • tools_hash 是否参与隔离由 include_tools_signature 决定

命中 / 未命中 / 绕过

Semantic Cache 会把处理结果写入响应头与请求日志。

hit

  • 返回响应头 X-Semantic-Cache-Status: Hit
  • 同时返回 X-Semantic-Cache-KeyX-Semantic-Cache-Distance
  • 网关直接返回缓存响应,不再继续调用后续插件和上游

miss

  • 返回响应头 X-Semantic-Cache-Status: Miss
  • 请求继续进入后续链路
  • 若最终响应是 2xx 且可序列化为内部 ResponseIR,则写入缓存

bypass

以下情况会绕过语义缓存逻辑:

  • embedding 文本为空
  • embedding 服务调用失败
  • Redis 向量检索失败
  • 插件运行时依赖未正确初始化

绕过时会记录 status=bypass,并按情况附带 bypass_reasonerror

请求日志

ext_fields.semantic_cache 使用以下 proto:

常见字段包括:

  • statushit / miss / bypass
  • bypass_reason:例如 embedding_text_emptyembedding_failedsearch_failednot_configured
  • cache_key:命中时缓存条目 ID
  • distance:命中时向量检索距离(cosine 距离,0 表示完全一致)
  • similarity:命中时缓存条目与请求向量的 cosine 相似度,等于 1 - distance,越接近 1 越相似
  • cache_age_ms:命中时缓存条目自写入到本次命中的存在时长,单位毫秒
  • namespace:本次检索使用的缓存命名空间
  • embedding_tokens:embedding 服务消耗的 token 数
  • embedding_latency_ms:embedding 调用耗时
  • search_latency_ms:Redis 向量检索耗时
  • error:失败或异常时的错误信息

distance / similarity / cache_age_ms 仅在 status = hit 时写入,其他状态下不存在该字段。

这些字段会汇总到请求日志的 ext_fields.semantic_cache 中;对应索引说明见 日志扩展字段