Semantic Cache(语义缓存)
Aidy 内置了语义缓存能力,通过静态配置 plugins.semantic_cache.* 与 route 级 plugin_config.semantic_cache 配置。
启用条件
Semantic Cache 插件只有在同时满足以下条件时,才会被挂进请求链路:
- 静态配置
plugins.semantic_cache.enable = true - 静态配置中已提供完整运行所需参数:
embedding_service_url、embedding_model、redis - 当前 route 配置了
plugin_config.semantic_cache
如果以上任一条件不满足,Semantic Cache 不会进入当前 route 的请求链路。
另外,Semantic Cache 当前只挂在 chat 的 forward hook 上。因此即使路由启用了语义缓存,embeddings、models、options 与 unknown 请求也不会进入该插件。
链路位置
Semantic Cache 会优先于以下中间件执行:
upstream-ratelimitegress-policyguardconsumerperf
也就是说,命中缓存时,请求会直接在网关内返回,不再继续进入后续上游调用链路。
静态配置
静态配置用于提供 embedding 服务与 Redis 向量存储能力,对应 plugins.semantic_cache.*。完整字段说明见 静态配置。
其中最关键的字段是:
enableembedding_service_urlembedding_modelembedding_api_keyembedding_dimensionsembedding_timeout_msredisverbose_log
路由配置
route 级配置用于指定特定 route 的语义缓存策略,使用以下 proto:
常用字段说明:
cache_ttl_seconds:缓存存活时间,单位秒similarity_threshold:向量距离阈值;越小代表要求越相近distance_metric:距离度量方式,当前仅支持cosinemessage_countback:从末尾回看多少条消息参与 embeddinginclude_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_order 和 create_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。大多数场景保持默认(开启)即可。
运行时行为
插件进入链路后,会按以下顺序工作:
- 根据请求内容构造 embedding 文本
- 调用 embedding 服务生成向量
- 在 Redis 向量索引中按命名空间检索最相近条目
- 命中时直接返回缓存响应
- 未命中时继续请求后续链路;若最终响应为
2xx,则将响应写入缓存
当前缓存命名空间格式为:
sc:{tenant}:{route}:{model}:{tools_hash}
其中:
tenant与route始终参与隔离model是否参与隔离由isolate_by_model决定tools_hash是否参与隔离由include_tools_signature决定
命中 / 未命中 / 绕过
Semantic Cache 会把处理结果写入响应头与请求日志。
hit
- 返回响应头
X-Semantic-Cache-Status: Hit - 同时返回
X-Semantic-Cache-Key与X-Semantic-Cache-Distance - 网关直接返回缓存响应,不再继续调用后续插件和上游
miss
- 返回响应头
X-Semantic-Cache-Status: Miss - 请求继续进入后续链路
- 若最终响应是
2xx且可序列化为内部ResponseIR,则写入缓存
bypass
以下情况会绕过语义缓存逻辑:
- embedding 文本为空
- embedding 服务调用失败
- Redis 向量检索失败
- 插件运行时依赖未正确初始化
绕过时会记录 status=bypass,并按情况附带 bypass_reason 与 error。
请求日志
ext_fields.semantic_cache 使用以下 proto:
常见字段包括:
status:hit/miss/bypassbypass_reason:例如embedding_text_empty、embedding_failed、search_failed、not_configuredcache_key:命中时缓存条目 IDdistance:命中时向量检索距离(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 中;对应索引说明见 日志扩展字段。