一、方案目标
这套方案要解决 4 个核心问题:
- 当前会话不丢上下文
- 当前任务能够连续推进
- 跨会话能够找回长期经验和稳定偏好
- 不同层记忆不打架、不重复、不污染 prompt
这不是一套“把所有内容都塞进一个存储”的方案,而是一套分层记忆体系。每一层只负责一种时间尺度、一种检索问题、一种注入目的。
核心原则:
- 原始历史和可注入记忆分开
- 当前状态和长期知识分开
- 权威结构化信息和语义召回信息分开
- 存储决策由系统层控制,不由 agent 自由落库
- 检索与注入分开,召回到的内容必须经过去重、排序和压缩后才能进上下文
二、整体分层
说明
下面的“第 0 层、第 1 层、第 2 层、第 3 层”是逻辑分层编号,不是运行时的固定访问顺序。
运行时是否访问某一层,由路由规则决定。默认逻辑优先级通常可以理解为:
PG -> Redis -> LTM
但这表示的是默认判断顺序和职责优先级,不表示所有实现都必须严格串行执行。某些场景可以跳过某层,某些场景可以并行查。
第 0 层:会话原始历史层
存储介质
- 本地文件
- 对象存储
- append-only log
存什么
- user 原始输入
- assistant 原始输出
- tool 原始结果
- 系统事件
作用
- 回放
- 审计
- debug
- 必要时重新抽取记忆
- 作为低频 fallback 回源材料
注意 这一层不是短期记忆层。它是 raw transcript,不直接作为主检索源,也不应该直接整段塞进 prompt。
什么时候会用到它
- Redis / PG / 长期记忆结果之间出现冲突时
- 某条短期记忆的来源需要核验时
- 怀疑某条长期记忆晋升错误,需要重建时
- 需要重新抽取结构化事实时
- 会话压缩后,想回看更早原始过程时
第 1 层:PG 结构化权威层
存储介质
- PostgreSQL
存什么
- 用户画像
- 用户长期偏好
- 权限、角色
- tenant / project / workspace 元数据
- agent 配置
- 显式保存的稳定结构化信息
- 从短期记忆中晋升出的稳定结构化事实
作用
- 权威结构化事实源
- 精确查询
- 给其他检索层提供 filter 条件
- 初始化运行时元数据
- 作为 profile / constraints / config 类信息的最终准据
不负责什么
- 不负责当前任务的临时状态
- 不负责语义相似案例召回
- 不负责高频工作记忆
- 不负责保存原始聊天历史
典型表
usersuser_profilesuser_preferencesprojectsworkspacesuser_project_rolesagent_settingsstable_memory_facts
第 2 层:Redis 短期工作记忆层
存储介质
- Redis
存什么
- 当前任务的关键事实
- 当前会话的稳定事实
- 已确认结论
- 已排除项
- 当前决策
- 用户当前约束
- 已验证 observation
- 短期偏好
作用
- 连续几轮会话续接
- debug / repair / verify 场景的上下文补全
- 快速、低延迟、高精度的工作记忆召回
- 作为当前任务状态的主要检索层
本质 它是 working memory / short-term memory,不是聊天记录库。
它解决的问题
- 当前这件事做到哪了
- 哪些原因已经排除
- 当前重点是什么
- 当前约束是什么
- 下一步该做什么
第 3 层:长期记忆检索层
存储介质
- Elasticsearch
- Milvus
存什么
- 跨会话仍有价值的长期记忆卡片
- 类似案例
- 通用经验
- 长期用户偏好
- 文档型知识摘要
- 从 STM 晋升后的长期知识
作用
- 语义相似召回
- 历史案例召回
- 关键词/BM25 检索
- 混合检索
- 远距离知识联想
本质 它是 long-term semantic memory / archival memory,不是当前状态层。
注意 这里建议把 ES + Milvus 看成一个统一的 LTM 子系统:
- ES 负责关键词、过滤、正文返回
- Milvus 负责语义相似召回
- 二者共同产出长期记忆候选
三、每层职责边界
PG 回答的问题
- 这个用户是谁
- 有什么权限
- 默认配置是什么
- 当前 workspace / project 是什么
- 用户长期偏好是什么
- 当前租户或项目有哪些稳定约束
Redis 回答的问题
- 当前这件事进行到哪了
- 哪些原因已经排除
- 当前约束是什么
- 当前下一步做什么
- 这个会话中已经确认过什么
LTM 回答的问题
- 以前有没有类似问题
- 有没有相似案例
- 有没有可复用经验
- 这个模糊表达可能关联什么历史知识
- 跨会话是否有相关长期信息
原始历史层回答的问题
- 当时原始输入和工具结果到底是什么
- 某条记忆最初是怎么来的
- 当前结构化结论是否可追溯
四、Redis 里到底怎么存
1. 内容本体
每条短期记忆存成一个独立对象,例如:
memory_item_{id}
可以用 Redis Hash 或 RedisJSON。
建议在 v1 里优先选择一种统一格式,不要混用。若内容结构经常变化,优先 RedisJSON;若追求极简和性能,优先 Redis Hash。
推荐字段
{
"id": "m_1001",
"user_id": "u_42",
"session_id": "s_123",
"task_id": "bug_456",
"memory_type": "fact",
"subtype": "diagnosis",
"summary": "createOrder 在 prod-k8s 超时,数据库已排除",
"detail": {
"symptom": "timeout",
"service": "payment-service",
"api": "createOrder",
"env": "prod-k8s",
"excluded_root_causes": ["database"],
"next_focus": ["thread_pool", "downstream_latency"]
},
"entities": ["createOrder", "payment-service", "database", "prod-k8s"],
"tags": ["timeout", "diagnosis", "prod"],
"source": {
"kind": "conversation+tool",
"message_ids": ["msg_91", "msg_94"],
"tool_call_ids": ["tool_18"]
},
"confidence": 0.92,
"importance": 0.84,
"created_at": 1713861000,
"updated_at": 1713861100,
"last_access_at": 1713861200,
"expires_at": 1713947400,
"fingerprint": "hash(user+task+subtype+normalized_fact)",
"canonical_fact_key": "bug456:createOrder:db_excluded",
"version": 3,
"status": "active"
}
2. 字段解释
id:记忆条目 IDuser_id:用户 IDsession_id:会话 IDtask_id:当前任务 ID,没有任务时可为空memory_type:主类别subtype:更细的子类别summary:给模型看的短摘要detail:结构化内容,给程序和压缩器用entities:实体列表,用于索引和打分tags:主题标签,用于筛选和统计source:来源追踪,便于追溯confidence:这条记忆是否可靠importance:这条记忆在当前任务中的重要性created_at/updated_at/last_access_at:时间信息expires_at:条目级生命周期fingerprint:近重复去重键canonical_fact_key:同一事实的规范键version:upsert 更新版本status:活动状态,例如active/superseded/deleted
3. memory_type 建议
至少分这几类:
fact:稳定事实decision:当前决策hypothesis:当前假设observation:工具或日志观测constraint:用户或环境约束preference:短期用户偏好
4. 可选字段
如果是多 agent 协作系统,可以额外带:
agent_idowner_agentshared_scope
这些不是 v1 的必要字段,单 agent 系统可以不存。
5. Redis 里不该存什么
- 原始聊天全文
- 大段日志全文
- 模型中间推理
- 未验证猜测
- 明显重复内容
- 与当前 task / session 无关的一次性碎片
- 暂时没有结构化价值的原始文本搬运
五、zset 里存什么
原则:zset 只存 ID,不存内容本体。
1. 最近更新索引
memory:user:{userId}:recent
- member =
memory_id - score =
updated_at
用途
- 最近记忆兜底召回
- 用户续接刚才任务时快速回捞
2. session 索引
memory:user:{userId}:session:{sessionId}
- member =
memory_id - score =
updated_at
用途
- 当前会话续接
- 代词和上下文依赖输入的快速召回
3. task 索引
memory:user:{userId}:task:{taskId}
- member =
memory_id - score =
updated_at
用途
- 多步任务
- debug / repair / verify 场景
- 当前任务上下文召回的主索引
4. 过期索引
memory:user:{userId}:expire
- member =
memory_id - score =
expires_at
用途
- 定时扫描和逐条清理
5. entity 索引
memory:user:{userId}:entity:{entity}
- member =
memory_id - score =
updated_at或importance
用途
- 按接口名、服务名、环境名、工单号等实体快速召回
6. type 索引(可选增强)
memory:user:{userId}:type:{memory_type}
- member =
memory_id - score =
updated_at
用途
- 只查特定类型记忆,例如 constraint / decision
注意 这不是 v1 必需项,而是可选增强索引。v1 核心索引是 recent / session / task / expire / entity。
六、PG 扮演什么角色
PG 不是又一个记忆库,而是权威结构化事实层。
1. Source of Truth
存稳定、明确 schema 的信息:
- 用户画像
- 用户长期偏好
- 角色、权限
- project / tenant / workspace 元数据
- agent 配置
- 显式长期规则
- 稳定结构化约束
2. 检索过滤器来源
PG 经常不是直接给模型看,而是给检索器提供 filter:
- tenant_id
- project_id
- workspace_id
- role
- permission scope
- language
- 当前环境标签
3. 稳定信息归档层
有些信息一开始来自 Redis STM,后来被证明是稳定事实,就晋升到 PG:
- 用户长期偏好
- 稳定配置
- 长期适用的结构化约束
- 明确的项目级静态事实
4. PG 查出来后怎么用
PG 的结果通常不进入普通 candidate pool,而是组装成:
profile_blockconstraints_blockruntime_metadata_block
例如:
{
"language": "zh-CN",
"answer_style": "brief_first",
"permission": "read_only",
"project_default_env": "prod-cn"
}
5. PG 与其他层冲突时的规则
PG 通常不参与 Redis / LTM 的普通排序式去重,但如果出现冲突:
profile.*permission.*config.*project_metadata.*
这些域默认以 PG 为权威来源。
七、ES + Milvus 混合检索里各存什么
1. 总体原则
ES 和 Milvus 不是两套相互独立的长期记忆,而是同一条长期记忆卡片的双索引系统。
同一条长期记忆卡片:
- 在 ES 中用于关键词检索、过滤和正文返回
- 在 Milvus 中用于向量相似召回
- 通过同一个
memory_id汇合
2. ES 存什么
ES 存长期记忆卡片的正文和结构化过滤信息:
{
"memory_id": "ltm_9001",
"user_id": "u_42",
"tenant_id": "t_1",
"project_id": "p_payment",
"memory_type": "semantic_fact",
"title": "createOrder timeout 常见排查经验",
"summary": "历史上多次出现 createOrder timeout,数据库正常时常见根因是线程池饱和",
"content": "完整的长期记忆正文或经验卡片内容",
"entities": ["createOrder", "database", "thread_pool", "payment-service"],
"tags": ["timeout", "case", "diagnosis"],
"source_type": "promoted_from_stm",
"bm25_text": "给 BM25/关键词检索用的归一化文本",
"created_at": 1713861000,
"updated_at": 1713861100,
"importance": 0.88,
"confidence": 0.91,
"access_count": 7,
"status": "active"
}
ES 的职责
- 关键词/BM25 检索
- 结构化过滤
- 返回可读正文
- 作为混合检索中的 lexical 分支
3. Milvus 存什么
Milvus 存同一条长期记忆卡片的向量表示和少量 metadata:
{
"id": "ltm_9001",
"vector": [0.12, -0.08, 0.44],
"user_id": "u_42",
"tenant_id": "t_1",
"project_id": "p_payment",
"memory_type": "semantic_fact",
"importance": 0.88
}
Milvus 的职责
- 语义相似召回
- 向量近邻搜索
- 产出同一
memory_id的候选集合
4. LTM 子系统内部怎么工作
当路由层决定“需要查长期记忆”时,不一定等于只查向量。
更准确的做法是:
- 先判断是否需要长期记忆子系统
need_ltm - 然后在 LTM 内部再决定:
- 只查 ES
- 只查 Milvus
- ES 和 Milvus 都查后做融合
典型情况
- 用户明确提到实体、错误码、接口名 -> ES 更重要
- 用户表达模糊,想找相似案例 -> Milvus 更重要
- 又要关键词准确,又要语义泛化 -> ES + Milvus 混合查
八、什么时候查哪一层
不是每次都查三层,而是先做 query-profile,再按规则路由。
1. 什么是 query-profile
query-profile 是把用户输入转成程序可判定的结构化查询画像。它不是必须由 LLM 生成,v1 完全可以通过规则、词典和运行时状态抽取。
例如:
{
"intent": "debug_continue",
"has_reference": true,
"has_profile_request": false,
"has_similarity_request": false,
"entities": ["createOrder", "database"],
"active_task_id": "bug456",
"session_recent": true,
"topic_shift": false,
"needs_exact_metadata": false,
"recent_entity_overlap": 2,
"profile_cache_missing": false,
"profile_cache_stale": false,
"redis_result_insufficient": false
}
2. 它怎么来
通过三类特征抽取:
文本规则
- 指代词:
这个/那个/刚才/继续/还是/第二个 - 画像词:
偏好/习惯/权限/角色/默认/配置 - 相似词:
类似/以前/历史/案例/经验 - 调试词:
报错/超时/排查/修复/验证
实体抽取
- 词典匹配
- 接口名 / 服务名 / 工单号 / 环境名正则
- 最近 session 实体表匹配
运行时状态
- 是否有
active_task_id - 当前 session 是否刚活跃过
- profile cache 是否缺失或过期
- 最近 session 实体有哪些
- 上一轮 Redis 召回是否不足
- 当前是否明显发生 topic shift
3. 为什么先用规则而不是 LLM
v1 用纯规则更容易落地,也更稳定:
- 可解释
- 易调试
- 延迟低
- 成本低
- 行为稳定
只有规则误判很多、业务规模变大后,才考虑引入轻量分类器。默认不需要先上 LLM。
九、怎么判断查 PG / Redis / LTM
1. PG
适合查 PG 的场景
- 问画像 / 偏好 / 权限 / 配置
- session 初始化或恢复
- 检索前需要 filter 条件
- PG 缓存缺失或过期
- 需要精确结构化值,而不是语义近似
规则示例
need_pg = (
profile_cache_missing
or profile_cache_stale
or intent in {"profile_query", "permission_query", "config_query"}
or needs_exact_metadata
)
2. Redis
适合查 Redis 的场景
- 正在续当前任务
- 输入依赖当前上下文
- 当前正在 debug / verify / fix
- 命中最近 session / task 实体
规则示例
need_redis = (
has_reference
or (active_task_id is not None and intent in {"continue_task", "debug_continue", "verify_current_state"})
or recent_entity_overlap > 0
)
为什么 Redis 优先于 LTM
- 更贴合当前任务
- 延迟更低
- 结果噪音更小
- 召回语义更偏“当前状态”而不是“历史相似”
3. LTM
适合查 LTM 的场景
- 用户问类似历史 / 相似案例
- 表达模糊但明显在找旧经验
- Redis 结果不足
- 新会话但可能和过去任务相关
规则示例
need_ltm = (
has_similarity_request
or fuzzy_history_reference
or redis_result_insufficient
)
4. 是否必须串行
不是。
更准确地说:
- 逻辑上通常先判断 PG、再判断 Redis、再判断 LTM
- 但实现上不一定严格串行
- 例如 PG profile 已缓存时,可以直接查 Redis
- 某些明确需要类似案例的场景,也可以在 Redis 的同时触发 LTM
十、查询优先级和合并方式
1. 运行时总体原则
- 先路由,决定查不查
- 查的话,PG 产出 metadata block
- Redis 和 LTM 产出 memory candidates
- memory candidates 再统一去重、排序、压缩
2. PG 的处理方式
PG 结果通常不进普通 candidate pool,而是直接组装为:
profile_blockconstraints_blockruntime_metadata_block
3. Redis + LTM 的处理方式
Redis 和 LTM 结果进入统一候选池:
- 合并
- 去重
- 重打分
- 取 topK
- 压缩成 memory summary
- 注入 prompt
4. 最终注入结构建议
用户/环境画像:
- 语言:中文
- 回答风格:先结论后细节
- 权限:只读
- 默认环境:prod-cn
当前任务短期记忆:
- 当前任务:bug456
- 问题:createOrder 在 prod-k8s 超时
- 已排除:数据库
- 当前重点:线程池、下游延迟
相关长期经验:
- 历史上有一次类似超时案例,根因是应用线程池饱和
十一、什么时候写入 Redis 短期记忆
1. 适合写入的时机
适合写入 Redis STM 的时刻:
- 工具结果出来后
- 当前任务状态发生变化时
- 用户给出稳定约束时
- 回合结束时做一次轻量候选抽取
2. 适合写入的内容
- 已验证 observation / fact
- 当前任务的重要 decision
- 当前任务的 constraint
- 当前会话的稳定结论
- 高置信的状态更新
- 与已有内容相比有明显新信息的事实
3. 不该写入 Redis 的内容
- 闲聊
- 原始长日志全文
- 模型中间推理
- 一次性碎片信息
- 没验证的猜测
- 重复事实
- 低置信、低价值、短时即失效的原始噪音
十二、谁来判断存不存
不是 agent 自己决定,也不是异步子 agent 最终裁决。
最稳的分工是:
- 异步子 agent / 抽取器:从对话和 tool result 里提取候选事实
- system memory manager / policy engine:判断值不值得写、写哪层、给什么 TTL
- 规则去重 / upsert 层:按 fingerprint 和 canonical_fact_key 合并
- 存储层:写 Redis,必要时异步晋升到 PG 或 LTM
一句话:
- 子 agent 负责提议
- 规则引擎负责裁决
- 去重器负责落库前合并
十三、写入规则怎么判
推荐为每条候选事实构建一个 write_profile:
{
"is_verified": true,
"is_stable_for_current_task": true,
"is_user_constraint": false,
"is_decision": true,
"is_preference": false,
"novelty": 0.8,
"confidence": 0.92,
"estimated_ttl_minutes": 180,
"duplicate": false,
"transient": false,
"raw_unstructured_blob": false
}
1. 适合写 Redis 的条件
- 已验证 observation / fact
- 当前任务的重要 decision
- 当前任务的 constraint
- 高置信的稳定状态
- 相比已有内容有新信息
2. 不适合写 Redis 的条件
- 低置信
- 重复
- 太短命
- 只是原始文本搬运
- 无法结构化
- 与当前 task / session 无关
3. 一个简单打分
write_score =
3 * is_verified
+ 3 * is_user_constraint
+ 2 * is_decision
+ 2 * is_stable_for_current_task
+ 2 * novelty_high
+ 1 * confidence_high
- 3 * duplicate
- 2 * transient
- 2 * raw_unstructured_blob
write_score >= 3 就写 Redis。
十四、什么时候从 Redis 晋升到 PG 或 LTM
不要自动把所有 Redis 都同步到长期层。晋升必须分为两条不同路径:
路径 A:晋升为稳定结构化事实 -> PG
适合进入 PG 的内容:
- 用户长期偏好
- 稳定角色和权限信息
- 稳定项目配置
- 长期适用的结构化约束
- 稳定画像信息
路径 B:晋升为长期检索知识卡片 -> LTM
适合进入 ES + Milvus 的内容:
- 可复用经验
- 相似案例总结
- 通用排障模式
- 长期有价值的知识卡片
- 跨会话仍值得召回的经验摘要
晋升的通用触发条件
只有满足下面条件才考虑晋升:
- 被多次命中 / 复用
- 跨多个 session 仍然出现
- 用户显式说“记住这个”
- 属于长期偏好 / 稳定画像
- 是通用经验,不只是当前任务临时状态
- 存活超过阈值
适合晋升的例子
- 用户偏好先结论后细节
- 某类 timeout 问题常见根因是线程池
- 某项目默认环境是 prod-cn
- 某用户长期只有只读权限
不适合晋升的例子
- 当前 bug456 下一步先看线程池
- 刚才那次日志输出
- 当前会话的临时排查路径
特别注意
STM 和长期记忆之间最大的风险,不是漏召回,而是错误晋升导致长期记忆污染。一旦长期记忆被写脏,后续召回会长期带噪音,因此晋升条件应比短期记忆写入条件更严格。
十五、怎么去重和合并
1. Redis 和 LTM 都查到时怎么办
会重复,所以不能把两边结果直接都塞进 prompt。
正确流程是:
- 各路召回候选
- 进入统一 candidate pool
- 做归一化和去重
- 重打分
- topK
- 压缩再注入
2. 去重键建议
每条记忆维护:
fingerprintcanonical_fact_keysource_typesource_priority
例如:
{
"canonical_fact_key": "bug456:createOrder:db_excluded",
"fingerprint": "sha1(normalized_fact)",
"source_type": "redis_stm",
"source_priority": 100
}
3. 权威优先级原则
不是简单全局 PG > Redis > LTM,而是按数据域决定:
profile.* / permission.* / config.* / project_metadata.*-> PG 权威task_state.* / session_state.* / current_decision.*-> Redis 权威similar_case.* / semantic_hint.*-> LTM 只做补充
4. 默认来源优先级
在 Redis 和 LTM 的普通候选去重时,可以先用:
Redis STM > LTM > raw history fallback
5. 为什么默认保留 Redis 候选
当 Redis 和 LTM 命中同一事实时,优先保留 Redis 的原因是:
- 更近
- 更贴合当前任务
- 更新鲜
- 噪音更小
十六、会话级记忆和压缩机制
1. 为什么要压缩
会话原始历史会越来越长,但长期有价值的通常不是原始输出,而是:
- 结论
- 状态
- 约束
- 决策
- 下一步动作
因此压缩不是简单删除旧消息,而是把原始过程逐渐转换为更稳的工作语义。
2. 两层压缩
第一层:轻量压缩
优先处理:
- 较早的大块工具输出
- 搜索原文
- 文件读取原文
- 长命令输出
这些内容体积大,但持续价值通常较低。
第二层:正式压缩
如果还不够,就把更早的过程折叠成结构化摘要,通常保留:
- 当前任务目标
- 用户最新要求
- 已完成工作
- 关键决定和约束
- 已知问题和风险
- 当前状态
- 下一步动作
3. 压缩优先级原则
压缩不是单纯按时间,而是综合看:
- 当前执行依赖度
- 内容类型
- 体积大小
- 新旧程度
- 持续价值
更准确的关系是:
当前执行依赖度 > 内容类型 > 体积 > 时间先后
4. 近场上下文
近场上下文不是最后一句,而是当前任务还能连续推进所必须保留的最近工作现场,通常包括:
- 用户当前最新问题
- 紧邻之前的一轮用户问题
- 对那一轮的模型回复
- 若最近几轮中有仍直接相关的工具结果,也一并保留
5. 工具结果依赖判断
判断一条回复是否依赖某个工具结果,重点不是时间,而是依赖关系:
- 回复结论是否直接来自工具结果
- 去掉工具结果后,回复是否还成立
- 回复是在解释工具结果,还是独立推进任务
- 工具结果是否已被消化为稳定语义
6. 工具结果依赖的三分类
- 强依赖:回复直接消费工具结果,没有它回复就不成立
- 弱依赖:工具结果提供背景,但回复主要在继续推进任务
- 已消化:工具结果的核心信息已经沉淀成结论、状态、约束或计划
只有已被消化的原始工具结果,才更适合进入压缩候选。
十七、整体推荐流程
1. 查询流程
用户输入
↓
构建 query-profile
↓
规则路由 decide_pg / decide_redis / decide_ltm
↓
PG 产出 profile / constraints / runtime metadata block
↓
Redis / LTM 各自召回候选
↓
统一 candidate pool
↓
去重 / 重打分 / topK
↓
压缩成 memory summary
↓
注入模型
2. 写入流程
对话 / tool result
↓
候选事实抽取器(可异步)
↓
write_profile
↓
policy engine 判断存不存
↓
去重 / upsert / TTL 计算
↓
写 Redis STM
↓
异步判断是否晋升到 PG 或 LTM
3. 清理流程
定时任务扫描 expire zset
↓
找到已过期 memory_id
↓
删除 memory_item
↓
从 recent / session / task / entity / type / expire 各索引中删除对应 id
十八、最小可落地版本
建议先做 v1,不要一开始做复杂智能路由。
v1 建议
- 会话历史:本地文件存 raw transcript
- PG:存用户画像、偏好、权限、project / workspace 元数据
- Redis:存当前任务短期记忆
- ES + Milvus:存晋升后的长期记忆
- 路由:纯规则 + 轻量打分
- 写入:候选抽取 + 规则判断 + upsert
- 压缩:先轻量删大块原始结果,不够再摘要压缩
v2 再考虑
- 路由分类器
- 更细粒度 rerank
- 更复杂的长期记忆晋升策略
- 召回质量评估闭环
- LTM 子系统内部更细的混合检索策略
十九、一句话总结
最合理的 agent 记忆方案,不是把所有内容都塞进一个库,而是:
- 用本地原始历史保存完整会话
- 用 PG 保存权威结构化事实
- 用 Redis 保存当前任务短期工作记忆
- 用 ES + Milvus 保存长期语义记忆
- 用 query-profile 和规则路由决定查哪层
- 用 policy engine 和去重规则决定存哪层
- 用统一候选池和压缩流程,避免重复和上下文污染