- 后端
- API网关
- 模型推理服务
- AI Agent
【免费下载链接】semantic-router
An open, programmable decision layer for models and compute.
本文基于 semantic-router 仓库中的《智能体记忆(Agentic Memory)》概念验证(Proof of Concept)提案展开,梳理其在“跨会话持久、选择性记忆”场景下的设计意图、记忆模型、检索与保存路径、生产化边界,并结合仓库中已实现的
memory插件、global.stores.memory配置与src/semantic-router/pkg/memory源码,说明提案如何落到可运行配置与 API 上。读完本文,你将理解为什么“只检索相关记忆、由用户可检查可删除”是一层正确的产品边界,也能掌握如何在当前项目中配置记忆后端、检索限制、自动存储与反射(Reflection)裁剪,以及对应的管理接口与可观测性指标。
1. 问题背景:为什么对话历史不等于记忆
单次会话中,对话历史足以解释当前交流,但它无法提供跨会话的持久、选择性记忆。若把全部历史消息每次都发给模型:
- 成本高:token 消耗随会话增长持续膨胀;
- 可能超出上下文窗口:长会话会被截断,模型反而丢失关键信息;
- 隐私暴露面增大:历史中的无关内容一并进入模型上下文。
反过来,把每条消息都保存为记忆也有问题:无关或不正确的细节会不断累积,并污染未来的回答。因此提案给出的“有用产品边界”是:一层小的、用户范围的记忆层——只检索相关记录,并且让用户可以检查或删除它们。这正是当前仓库中memory插件与global.stores.memory设计所遵循的出发点。
2. 概念验证:检索→注入→提取→存储的循环
提案描述的拟议循环如下:
两个关键时序约束:
- 检索发生在生成之前:先解析用户身份 → 检索相关记忆 → 注入有界的记忆上下文 → 再交给模型;
- 候选提取发生在成功响应之后:模型返回成功后,从本轮交互中提取候选记忆 → 校验与限定范围 → 持久化或丢弃。
且任一步骤都可以独立禁用——例如某条路由可以只检索不存储,或只存储不检索。这一设计在实现中体现为memory插件的retrieval_limit与auto_store两个相互独立的开关。
3. 记忆模型:三种记录类型与记录字段
提案区分三种有用的记录类型:
| 类型 | 用途 | 示例 |
|---|---|---|
| Semantic | 关于用户的稳定事实或偏好。 | 首选输出语言。 |
| Procedural | 关于工作应如何完成的可复用指令。 | 使用特定审阅格式。 |
| Episodic | 先前交互的有界摘要。 | 一次已完成排障会话的结果。 |
该分类在源码中已落地为枚举类型,见 src/semantic-router/pkg/memory/types.go:
MemoryTypeSemantic MemoryType = "semantic" // 事实、偏好、知识 MemoryTypeProcedural MemoryType = "procedural" // 指令、步骤、how-to MemoryTypeEpisodic MemoryType = "episodic" // 会话摘要、历史事件提案明确把以下内容排除在概念验证范围之外:反思记忆(reflective memories)、自主整合(autonomous consolidation)、知识图谱推断(knowledge-graph inference)——它们需要更强的来源(provenance)和纠正规则。
每条存储记录需要:所有者身份(owner identity)、类型、内容、来源引用、创建时间,以及足以说明为何保存的来源信息。提案特别强调一句实现原则:“嵌入是检索索引,不是权威记录”。对应到源码中,Memory结构体除了ID、Type、Content、Embedding(向量字段带json:"-",不参与序列化)外,还携带了完整的来源与生命周期字段(同上文件):
UserID string `json:"user_id"` // 所有者,用于用户隔离 ProjectID string `json:"project_id,omitempty"` // 可选项目范围 Source string `json:"source,omitempty"` // 来源 CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at,omitempty"` AccessCount int `json:"access_count"` // 检索次数,用于保留分 LastAccessed time.Time `json:"last_accessed,omitempty"` Importance float32 `json:"importance"` // 0.0~1.0 优先级分 // --- 来源(Provenance)字段 --- CreatedByUserID string `json:"created_by_user_id,omitempty"` // 产生该记忆的用户 ConversationID string `json:"conversation_id,omitempty"` // 产生该记忆的会话 CreatedVia string `json:"created_via,omitempty"` // llm_extraction / api / import这些字段正是“每次写入都保留来源,以便运营方调查污染”这一要求的直接实现。
4. 检索:六条原则与身份过滤先行
提案为检索定义了六条要求:
- 要求稳定的用户或租户身份;
- 跳过明显不需要记忆的请求;
- 用当前请求和有界近期上下文形成搜索查询;
- 在返回候选前按所有者过滤;
- 应用 top-k 和相似度限制;
- 将记忆作为不受信任的上下文注入,与系统指令分开。
其中最关键的安全语义是:“相似度分数不是披露记录的许可”——向量检索只负责“找得到”,身份与策略过滤器必须在内容暴露给模型之前运行。否则,一次相似度误命中就可能把另一位用户的记忆送入模型上下文。
这一原则在实现层面对应RetrieveOptions结构(src/semantic-router/pkg/memory/types.go):检索同时约束UserID(所有者过滤)、ProjectID(项目范围)、Types(类型过滤)、Limit(top-k)、Threshold(相似度下限,默认 0.70)等参数;HybridSearch开启 BM25 + n-gram 混合重排,AdaptiveThreshold开启基于聚类间隙的自适应阈值,避免注入弱相关记忆。
5. 保存与纠正:只存持久事实,纠正而非隐藏
保存路径只应提取持久事实、偏好或流程。以下内容不应自动保存:
- 瞬时请求(transient requests);
- 模型推测(model speculation);
- 密钥(secrets);
- 从检索内容复制的指令。
用户需要两类操作:列出记忆与忘记记忆。纠正语义上,新记录应替换或取代较早记录,而不是依赖检索排序来“藏住”旧记录;每次写入保留来源,以便运营方调查污染。
这两类操作在当前项目中已具备管理 API 支撑(见 src/semantic-router/pkg/apiserver/route_memory.go):
| 方法 | 路径 | 作用 |
|---|---|---|
GET | /api/v1/storage/memories?user_id=test | 列出(按用户过滤)记忆 |
GET | /api/v1/storage/memories/{id}?user_id=test | 查看单条记忆 |
DELETE | /api/v1/storage/memories/{id}?user_id=test | 删除单条记忆(并发删除按幂等成功处理,避免 TOCTOU 竞态误报 500) |
DELETE | /api/v1/storage/memories?user_id=test&type=semantic | 按范围/类型批量删除 |
对应处理器为handleListMemories、handleGetMemory、handleDeleteMemory与handleDeleteMemoriesByScope,管理 API 在pkg/apiserver/server.go中通过resolveMemoryStore挂载,记忆存储不可用时记录降级日志而非启动失败。
6. 存储与失败行为:向量存储不构成生产契约
提案中的原始概念验证用向量存储做语义检索,但明确指出该选择并不确立生产存储契约。生产设计仍需要:
- 向量搜索前的索引租户隔离;
- 保留和删除保证;
- 加密和凭证轮换;
- 配额和重复处理;
- 备份与恢复;
- 跨多个路由器副本的行为。
失败行为有两项规定:若检索不可用,在路由允许时应在没有记忆的情况下继续请求(fail-open);失败的写入应可观测,且不得改变模型响应。后者在实现中非常严格:响应路径上的记忆快照准备工作如果出现意外载荷形状,只记录日志而绝不影响已经生成的答案(见 src/semantic-router/pkg/extproc/processor_res_memory.go 中scheduleSemanticResponseMemoryStore的 panic 恢复与recordMemoryPersistenceOutcome上报逻辑,源码注释还引用了问题编号 #1843)。
写入路径还有一道策略阶梯suppressedResponseMemoryStore:缺少提取器 → 拒绝;auto_store关闭 → 拒绝;检测到响应越狱(response jailbreak)→ 按policy_blocked拒绝,且越狱分类器不可用时按jailbreak_unverified处理——也就是说,只有“干净”的请求才会产生记忆,这从源头防止对抗性内容进入记忆库。
7. 从提案到实现:memory 插件的配置实操
提案指出:“当前记忆插件指南是已实现配置和行为的事实来源。本提案不应作为部署配方使用。”以下为落地配置。
7.1 路由局部插件
memory是路由局部插件,在routing.decisions[].plugins下启用:
plugins: - type: memory configuration: enabled: true retrieval_limit: 5 auto_store: trueretrieval_limit:每次请求检索并注入的记忆条数(top-k);auto_store:是否在成功响应后自动提取并存储新记忆;- 未设置的决策阈值继承全局值:应先按所选嵌入模型与检索模式校准全局阈值,再考虑添加覆盖值。
完整可复制示例见 config/fragments/plugin/memory/session-memory.yaml:
plugin: type: memory configuration: enabled: true retrieval_limit: 5 # Inherit the embedding-model-calibrated global threshold. auto_store: true7.2 共享后台存储:global.stores.memory
记忆插件要求共享后台存储在global.stores.memory下配置。路由器支持三种后端:
- Milvus(默认)— 分布式向量数据库,最适合大规模生产;
- Valkey— 使用 Search 模块的轻量单二进制选项,最适合开发/测试或已有 Valkey 基础设施;
- Qdrant— 带 gRPC 的单二进制,运维比 Milvus 更简单,适合小到大规模工作负载。
三种后端的global配置示例(均取自存储与工具教程):
# Milvus 后端(默认) global: stores: memory: enabled: true milvus: address: milvus:19530 collection: agentic_memory dimension: 256# Valkey 后端(需要带 Search 模块的 Valkey) global: stores: memory: enabled: true backend: valkey valkey: host: valkey port: 6379 dimension: 256 collection_prefix: "mem:" index_name: mem_idx metric_type: COSINE# Qdrant 后端 global: stores: memory: enabled: true backend: qdrant qdrant: host: qdrant port: 6334 collection: agentic_memory dimension: 256 embedding_model: mmbert default_retrieval_limit: 5 default_similarity_threshold: 0.30三个示例均使用默认的mmbert(Vela Embedding)生成嵌入,因此dimension取其提供的维度之一:64、128、256、512 或 768。若设为其他值,Router 会记录Failed to create memory store: … Memory will be disabled,并在没有记忆的情况下继续运行。Qdrant 示例中的 0.30 阈值基于原始余弦分数,只是起点而非校准值——上线前还需检查无关查询和已更正的旧事实。
Valkey 后端更完整的参数参考见 config/runtime/memory/valkey.yaml,包含 HNSW 调优(index_m、index_ef_construction)、TLS(tls_enabled、tls_ca_path、tls_insecure_skip_verify)以及“memory_rewrite 改写模型max_response_bytes(省略或非正值使用 1 MiB 默认值)”等说明。对应的部署指南为 Valkey 智能体记忆 与 Qdrant。
7.3 混合检索与反射(Reflection)
hybrid_mode与reflection.algorithm可全局设置于global.stores.memory下,也可在决策的memory插件中覆盖;配置其他任何值(包括 recipe 决策中设置的值)时 Router 将拒绝启动:
| 字段 | 可接受的值 | 默认值 |
|---|---|---|
hybrid_mode | weighted、rrf(完全匹配,小写) | weighted |
reflection.algorithm | heuristic、noop | heuristic |
hybrid_mode仅在hybrid_search: true时生效。旧配置中的hybrid_mode: rerank与algorithm: recency_semantic将无法启动,应分别改为weighted与heuristic(行为与旧值相同);- 当
global.stores.memory.reflection启用(默认的 heuristic 门控)时,检索到的记忆在注入前会被裁剪到max_inject_tokens。
反射门控的源码实现见 src/semantic-router/pkg/memory/reflection.go:ReflectionGate依次执行对抗内容屏蔽(block patterns)→ 新近度衰减(recency decay,默认半衰期 30 天)→ 按调整后分数重排 → 近似去重(dedup,默认阈值 0.90)→ token 预算强制(默认max_inject_tokens2048)。该实现不调用 LLM,为亚毫秒开销,其设计受 RMM(ACL 2025)回顾性反思启发,但按基础设施层“延迟必须最小化”的要求做了适配。
7.4 max_inject_tokens 的脚本感知估算
反射门控的 token 预算采用脚本感知启发式估算,而非上游模型分词器:约每个拉丁词单元 1.3 token、每个汉字/平假名/片假名/谚文字符 1.5 token,合并估算结果向上取整(ceil)。因此,无空格的 CJK 文本会按字符数大量计入预算,而不会塌缩为单个空白分词单元——运营中文、日文、韩文流量时,max_inject_tokens的裁剪会比旧的纯英文估算更紧。泰文等无空格脚本目前未做完整分词;精确分词器核算另有追踪(问题 #3050)。
8. 源码级验证:请求路径、响应路径与身份绑定
8.1 请求路径与响应路径分离
循环的“检索→注入”发生在请求路径:req_filter_memory_context.go/req_filter_memory_rewrite.go在 extproc 处理器中把检索结果注入请求,并有独立的改写(rewrite)过滤器;而“提取→存储”发生在响应路径:processor_res_memory.go的memoryPersistenceJob把当前用户消息、助手响应与有界历史编码后交给MemoryExtractor.ProcessResponseWithHistoryCount异步执行。写入前会先做快照预算校验(validateMemorySnapshotBudget,历史过大则skipped)与容量预留(receipt 队列满则rejected),在 worker 阻塞时被拒绝的快照不会占用容量。
8.2 身份与自动存储的逐级裁决
responseMemoryAutoStoreEnabled体现了“服务端策略先于客户端选择”的顺序:客户端可以选择退出,但不能撤销决策级的显式禁止;裁决链依次为内存配置 → ResponseObjectState → 请求头(request auto-store)→ 决策插件配置 → 全局Memory.AutoStore。同时,缺少user_id的记忆写入在检查或复制会话内容之前就会被拒绝(memory_info_unavailable),这正是“要求稳定用户身份”的实现。
8.3 嵌入模型身份绑定
升级嵌入模型需重启模型运行时。来自模型运行时(包括 Vela Embedding)的嵌入,Router 会将记忆与实际加载的模型、分词器、推理设置及向量维度绑定:变更这些设置会创建独立的物理集合/索引与独立的 Redis 热缓存;相同向量表示重启则复用既有存储,逻辑名称保持不变。早期无身份标记的集合会保留但不会被自动接管——相同向量维度不代表两个模型的嵌入兼容。管理 API 没有记忆导入/批量导出端点,新模型集合从空开始随流量重新积累。远程嵌入端点无法证明向量由哪个模型生成,因此记忆继续使用配置的集合/索引,Router 启动时记录警告;修改endpoint.model后应手动将记忆指向新集合/索引。
9. 可观测性
记忆链路暴露有界的 Prometheus 指标(见 src/semantic-router/pkg/memory/metrics.go):
llm_memory_retrieval_total:检索量计数器,带backend与status标签(hit、miss、error);llm_memory_retrieval_latency_seconds/llm_memory_retrieval_results:检索延迟与结果数直方图(不带 per-user 标签);llm_memory_extraction_total、llm_memory_extraction_latency_seconds、llm_memory_extraction_facts_count:提取环节计数;llm_memory_store_operations_total、llm_memory_store_size、llm_memory_cache_hits_total:存储与热缓存观测。
延迟与结果数直方图刻意不携带用户维度标签,避免通过指标泄露用户级行为。若从带user_id标签的旧版本升级,需参考 Router Memory Prometheus label 发布说明。
10. 评估方法与待决问题
10.1 评估:检索与保存分开衡量
- 检索相关性和跨用户隔离;
- 遗漏、无关和重复记忆的比率;
- 对持久事实和偏好的提取精确率;
- 纠正和删除行为;
- 增加的延迟和上下文 token;
- 身份或存储不可用时的优雅降级。
在使用个人数据测试之前,应使用合成租户和可检查记录(synthetic tenants and inspectable records)。
10.2 待决问题
- 每种协议的权威身份来源是什么?
- 写入是否需要显式用户同意或路由级选择加入?
- 冲突记忆如何解决?
- 哪些记录类型默认需要过期?
- 多租户生产使用前需要哪种隔离原语?
这些开放问题也解释了提案为何刻意保持“概念验证”状态:它验证的是检索 → 注入 → 提取 → 存储这个循环本身,而不主张生产级隔离、可靠的会话结束检测、自主反思或所提取记忆的事实正确性。
11. 范围与结论
该概念验证验证了检索 → 注入 → 提取 → 存储闭环的可行性与产品边界;已实现的配置与行为以记忆插件指南为事实来源,本提案不用于直接部署。与本主题相关的补充材料还包括路由学习中的记忆与回放(讲述热路径学习状态、路由回放与离线配方学习的分层)。实践建议总结如下:
- 先按
global.stores.memory选型并校准后端(Milvus/Valkey/Qdrant)与阈值; - 通过
routing.decisions[].plugins按路由决定retrieval_limit与auto_store; - 为中文/日文/韩文流量重估
max_inject_tokens; - 用
GET/DELETE /api/v1/storage/memories向用户提供检查与删除入口; - 用
llm_memory_*指标持续监控检索命中率、提取率与存储增长,并始终牢记“相似度分数不是披露记录的许可”。
- 后端
- API网关
- 模型推理服务
- AI Agent
【免费下载链接】semantic-router
An open, programmable decision layer for models and compute.
相关推荐
Semantic Router Agentic Memory 提案解读:检索→注入→提取→存储的持久记忆闭环设计与落地对照
Semantic Router Agentic Memory 提案解读:检索→注入→提取→存储的持久记忆闭环设计与落地对照 本篇技术指南围绕 semantic
后端API网关模型推理服务AI AgentSemantic Router 智能体记忆(Agentic Memory)设计全解:跨会话检索、LLM 提取与优雅降级实践
Semantic Router 智能体记忆(Agentic Memory)设计全解:跨会话检索、LLM 提取与优雅降级实践 Semantic Router 的智
后端API网关模型推理服务AI Agentsemantic-router Memory 插件实战:会话记忆的检索、自动存储与注入配置指南
semantic router Memory 插件实战:会话记忆的检索、自动存储与注入配置指南 本指南围绕 semantic router 的 memory 路
后端API网关模型推理服务AI Agent
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考