Version: 0.9.22.dev.260416
后端: 1. 品牌文案与聊天定位统一切到 SmartMate,并放宽非排程问答能力 - 系统人设、路由、排程、查询、交付提示统一从 SmartFlow 改为 SmartMate - 明确普通问答/生活建议/开放讨论可正常回答,deep_answer 不再输出“让我想想”等占位话术 - thinkingMode=auto 时,deep_answer 默认开启 thinking,execute 继续跟随路由决策,其余路由默认关闭 2. Memory 读取链路升级为“结构化强约束 + 语义候选”hybrid 模式,并补齐注入渲染 / Execute 消费 - 新增 read.mode、四类记忆预算、inject.renderMode 等配置及默认值 - 落地 HybridRetrieve,统一 MySQL/RAG 读侧作用域、三级去重(ID/hash/text)、统一重排与按类型预算裁剪 - 新增 FindPinnedByUser、content_hash DTO/兜底补算、legacy/RAG 共用读侧查询口径与 fallback 逻辑 - 记忆注入支持 flat/typed_v2 两种渲染,execute msg3 正式消费 memory_context,主链路注入 MemoryReader 时同步透传 memory 配置 3. Memory 第二步/第三步 handoff 与治理文档补齐 - HANDOFF_Memory向Mem0靠拢三步冲刺计划.md 从 newAgent 迁到 memory 目录,并补充“我的记忆”增删改查与最小留痕口径 - 新增 backend/memory/记忆模块第二步计划.md、backend/memory/第三步治理与观测落地计划.md,分别拆解 hybrid 读取注入闭环与治理/观测/清理路线 - 同步更新 backend/memory/Log.txt 调试日志 前端: 1. 助手输入区新增“智能编排”任务类选择器,并把 task_class_ids 作为请求 extra 透传 - 新建 frontend/src/components/assistant/TaskClassPlanningPicker.vue,支持拉取任务类列表、临时勾选、已选标签回显与清空 - 更新 frontend/src/components/dashboard/AssistantPanel.vue、frontend/src/types/dashboard.ts:Chat extra 正式建模 task_class_ids / retry 字段;当本轮带编排任务类时强制新起会话,避免把现有会话历史误混入新编排 2. 会话上下文窗口统计接入前端展示 - 更新 frontend/src/api/agent.ts、新建 frontend/src/components/assistant/ContextWindowMeter.vue、更新 frontend/src/components/dashboard/AssistantPanel.vue、frontend/src/types/dashboard.ts:接入 /agent/context-stats,兼容 object/string/null 三种返回;在输入工具栏展示 msg0~msg3 占比与预算使用率 3. 助手面板交互细节优化 - 更新 frontend/src/components/dashboard/AssistantPanel.vue:thinking 开关改为 auto/true/false 三态选择;切会话与重试后同步刷新 context stats;历史列表首屏不足时自动继续分页直到形成滚动区 仓库:无
This commit is contained in:
@@ -1,639 +0,0 @@
|
||||
# Memory 向 Mem0 靠拢三步冲刺计划(newAgent)
|
||||
|
||||
## 1. 一句话结论
|
||||
|
||||
当前 `memory` 已经具备了“可异步写入、可基础抽取、可基础检索、可注入 newAgent”的骨架,但距离真正有 Mem0 味道的记忆系统,还差三块核心能力:
|
||||
|
||||
1. 写入侧没有“先召回旧记忆,再做 `ADD/UPDATE/DELETE/NONE` 决策”的治理层。
|
||||
2. 读侧没有把“硬约束优先、语义召回补充、结果去重、注入预算”做成稳定链路。
|
||||
3. 系统层没有形成“可灰度、可解释、可清理、可回滚”的治理闭环。
|
||||
|
||||
因此建议按三步走推进,并严格遵守一个原则:
|
||||
|
||||
1. 每一轮只处理一个能力域。
|
||||
2. 第一步只动写入决策层。
|
||||
3. 第二步只动读链路与注入质量。
|
||||
4. 第三步只动治理、清理、指标与切流收口。
|
||||
|
||||
---
|
||||
|
||||
## 2. 本文档给谁看
|
||||
|
||||
本文档面向三类读者:
|
||||
|
||||
1. 需要继续实现 `memory/newAgent` 的 agent。
|
||||
2. 需要拆任务、排优先级的人。
|
||||
3. 需要快速判断“本轮该改什么、不该改什么”的维护者。
|
||||
|
||||
本文档不是背景介绍文档,而是“可直接拿去拆工单和接力开发”的冲刺说明。
|
||||
|
||||
---
|
||||
|
||||
## 3. 当前现状与目标差距
|
||||
|
||||
### 3.1 当前已完成的部分
|
||||
|
||||
当前已经有的能力:
|
||||
|
||||
1. 聊天消息可通过 `outbox -> memory.extract.requested -> memory_jobs -> worker` 进入异步记忆链路。
|
||||
2. Worker 可调用 LLM 做事实抽取,并通过 `NormalizeFacts` 做批内标准化和批内去重。
|
||||
3. `memory_items / memory_jobs / memory_audit_logs / memory_user_settings` 四张核心表已经建立并接线。
|
||||
4. `ReadService` 已可做基础查询与轻量排序。
|
||||
5. `newAgent` 已通过 `injectMemoryContext` 把记忆写入 pinned block。
|
||||
6. 用户设置、删除、审计已经具备基础治理能力。
|
||||
|
||||
### 3.2 当前离 Mem0 还差什么
|
||||
|
||||
最关键的差距如下:
|
||||
|
||||
| 能力 | 当前状态 | 与 Mem0 的差距 |
|
||||
| --- | --- | --- |
|
||||
| 异步入队 | 已完成 | 基本到位 |
|
||||
| 抽取候选事实 | 已完成 | 缺少更强的抽取后治理 |
|
||||
| 批内去重 | 已完成 | 仅限单批,不处理历史记忆 |
|
||||
| 历史去重 | 未完成 | 需要按旧记忆召回后做决策 |
|
||||
| `ADD/UPDATE/DELETE/NONE` 决策 | 未完成 | 这是最关键差距 |
|
||||
| 语义召回 | 部分完成 | 接口有了,质量与稳定性未形成闭环 |
|
||||
| 读侧去重 | 未完成 | 现在更多是展示层弱去重 |
|
||||
| Prompt 注入 | 基础版已接 | 还没有类型分层与预算控制 |
|
||||
| 管理治理 | 部分完成 | 还缺更新、恢复、历史清理、指标闭环 |
|
||||
| 灰度/回滚 | 较弱 | 需要细粒度 feature flag 与分阶段切流 |
|
||||
|
||||
### 3.3 本次冲刺的目标定义
|
||||
|
||||
本轮不是要把项目做成完整 Mem0,也不是做图记忆或多 Provider 平台,而是要做到一个“Mem0-lite 可自信上线”的状态。满足以下条件,就可以认为基本靠近目标:
|
||||
|
||||
1. 相同或同义记忆不会无脑越写越多。
|
||||
2. 用户纠正一条旧记忆时,系统更倾向于更新旧值,而不是新增一条冲突值。
|
||||
3. 读侧能优先拿到“硬约束 + 偏好 + 当前话题相关事实”,而不是仅按最近更新时间胡乱注入。
|
||||
4. Prompt 注入是稳定、可控、可解释的,而不是纯拼接。
|
||||
5. 出问题时可以快速关掉某一层能力,而不是整条 memory 链路一起陪葬。
|
||||
|
||||
---
|
||||
|
||||
## 4. 设计原则与边界
|
||||
|
||||
### 4.1 每轮只处理一个能力域
|
||||
|
||||
为避免回归问题无法定位,本计划明确规定:
|
||||
|
||||
1. 第一步只处理“写入决策层”。
|
||||
2. 第二步只处理“读取与注入层”。
|
||||
3. 第三步只处理“治理、清理与切流层”。
|
||||
|
||||
禁止在同一轮里同时大改:
|
||||
|
||||
1. `memory` 写入逻辑。
|
||||
2. `newAgent` 图节点结构。
|
||||
3. WebSearch / 其他 RAG 语料。
|
||||
4. 多个 prompt 体系。
|
||||
|
||||
### 4.2 保留旧实现,走并行迁移
|
||||
|
||||
整个冲刺必须遵守并行迁移策略:
|
||||
|
||||
1. 旧的“抽取后直接 `Create`”路径先保留。
|
||||
2. 新的“决策后 ApplyAction”路径并行落地。
|
||||
3. 用 feature flag 灰度切流。
|
||||
4. 验证通过后,再决定是否删除旧路径。
|
||||
|
||||
### 4.3 不新增“memory 工具化”这条支线
|
||||
|
||||
本轮不建议把 `memory` 改成一个显式工具让 `newAgent` 主动调用,原因如下:
|
||||
|
||||
1. 当前 `pinned block` 已经接入主链路,切点稳定。
|
||||
2. 本轮目标是让记忆“更准”,不是让图结构更复杂。
|
||||
3. 若同时引入工具化调用,会把“写入决策层”和“图编排层”耦到一起。
|
||||
|
||||
因此本轮默认继续沿用:
|
||||
|
||||
1. `backend/memory/service/read_service.go`
|
||||
2. `backend/service/agentsvc/agent_memory.go`
|
||||
3. `pinned block` 注入
|
||||
|
||||
---
|
||||
|
||||
## 5. 三步走总览
|
||||
|
||||
| 步骤 | 只处理的能力域 | 核心目标 | 旧实现是否保留 |
|
||||
| --- | --- | --- | --- |
|
||||
| 第一步 | 写入决策层 | 把“抽取即新增”升级为“召回旧记忆 + 决策动作” | 保留 |
|
||||
| 第二步 | 读链路与注入层 | 把“查到就拼”升级为“硬约束优先 + 语义补充 + 注入预算” | 保留 |
|
||||
| 第三步 | 治理与切流层 | 把“能跑”升级为“可灰度、可观测、可清理、可回滚” | 收口 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 第一步:先把写入侧做成 Mem0-lite
|
||||
|
||||
### 6.1 这一步解决什么问题
|
||||
|
||||
当前写入链路本质上还是:
|
||||
|
||||
`抽取 -> 标准化 -> 直接写 memory_items`
|
||||
|
||||
这会带来三个直接问题:
|
||||
|
||||
1. 历史同义记忆不会合并。
|
||||
2. 用户纠正旧记忆时,系统更可能新增一条相反记忆。
|
||||
3. `content_hash` 现在更多只是存了个字段,没有真正承担“历史治理”的职责。
|
||||
|
||||
第一步的目标是把写入链路升级为:
|
||||
|
||||
`抽取 -> 召回旧记忆候选 -> 临时 ID 映射 -> LLM 决策 -> ApplyAction`
|
||||
|
||||
### 6.2 本轮要落的能力
|
||||
|
||||
第一步必须落地以下能力:
|
||||
|
||||
1. 为每条新候选 fact 召回有限个旧记忆候选。
|
||||
2. 用临时整数 ID 或候选序号喂给决策模型,避免模型直接编造真实 `memory_id`。
|
||||
3. 让模型只输出结构化 JSON 决策:`ADD/UPDATE/DELETE/NONE`。
|
||||
4. 后端严格校验决策合法性,再执行数据库动作。
|
||||
5. `UPDATE/DELETE` 也必须补齐审计日志,而不是只有 `create/delete`。
|
||||
|
||||
### 6.3 推荐的文件落点
|
||||
|
||||
建议新增文件:
|
||||
|
||||
1. `backend/memory/model/decision.go`
|
||||
- 定义决策 DTO、候选旧记忆 DTO、ApplyAction DTO。
|
||||
2. `backend/memory/orchestrator/llm_decision_orchestrator.go`
|
||||
- 负责“给定新 fact + 旧候选 -> 输出结构化动作决策”。
|
||||
3. `backend/memory/utils/decision_id_map.go`
|
||||
- 负责“真实 memory_id <-> 临时决策 ID”的映射。
|
||||
4. `backend/memory/utils/decision_validate.go`
|
||||
- 负责校验动作是否合法、目标 ID 是否存在、动作字段是否完整。
|
||||
5. `backend/memory/worker/decision_flow.go`
|
||||
- 负责 worker 内的“候选召回 -> 决策 -> 动作执行编排”。
|
||||
6. `backend/memory/worker/apply_actions.go`
|
||||
- 负责把 `ADD/UPDATE/DELETE/NONE` 落为数据库动作与审计。
|
||||
|
||||
建议修改文件:
|
||||
|
||||
1. `backend/memory/model/config.go`
|
||||
2. `backend/memory/service/config_loader.go`
|
||||
3. `backend/memory/repo/item_repo.go`
|
||||
4. `backend/memory/worker/runner.go`
|
||||
5. `backend/memory/utils/audit.go`
|
||||
|
||||
### 6.4 推荐新增配置
|
||||
|
||||
建议新增配置项,全部走 `memory` 命名空间:
|
||||
|
||||
1. `memory.decision.enabled`
|
||||
- 是否启用决策层。
|
||||
2. `memory.decision.candidateTopK`
|
||||
- 每个新 fact 召回多少个旧记忆候选。
|
||||
3. `memory.decision.fallbackMode`
|
||||
- 建议支持 `legacy_add` / `drop` 两种模式。
|
||||
4. `memory.write.mode`
|
||||
- 建议支持 `legacy` / `decision` 两种模式。
|
||||
|
||||
建议默认值:
|
||||
|
||||
1. `memory.decision.enabled=false`
|
||||
2. `memory.write.mode=legacy`
|
||||
3. `memory.decision.candidateTopK=5`
|
||||
4. `memory.decision.fallbackMode=legacy_add`
|
||||
|
||||
### 6.5 `ItemRepo` 需要补的能力
|
||||
|
||||
当前 `ItemRepo` 只有“查、建、删状态、刷访问时间、刷向量状态”,还不够支撑决策动作。第一步至少要补以下能力:
|
||||
|
||||
1. `FindDecisionCandidates(...)`
|
||||
- 按 `user_id + assistant_id + conversation_id + run_id + memory_type` 查候选。
|
||||
- 当 RAG 可用时,可优先用向量召回补候选。
|
||||
2. `UpdateContentByID(...)`
|
||||
- 用于 `UPDATE`。
|
||||
- 至少要更新:`title/content/normalized_content/content_hash/confidence/importance/sensitivity_level/is_explicit/updated_at`。
|
||||
3. `SoftDeleteByID(...)`
|
||||
- 用于决策型 `DELETE`。
|
||||
4. `FindActiveByHash(...)`
|
||||
- 给兜底幂等或低成本重复检测预留接口。
|
||||
|
||||
注意:
|
||||
|
||||
1. 不要把这些逻辑继续堆进 `UpsertItems`。
|
||||
2. `UpsertItems` 可以暂时保留给 legacy 路径使用。
|
||||
3. 新路径应尽量使用显式动作函数,而不是一个“万能 Upsert”。
|
||||
|
||||
### 6.6 Worker 内推荐的执行顺序
|
||||
|
||||
对每个 job,建议执行以下顺序:
|
||||
|
||||
1. 先抽取新事实。
|
||||
2. 对抽取结果做 `NormalizeFacts`。
|
||||
3. 按用户设置过滤。
|
||||
4. 若 `memory.decision.enabled=false`,直接走旧路径并返回。
|
||||
5. 对每条新 fact 召回旧候选:
|
||||
- 先查强约束域内候选。
|
||||
- 若 `memory.rag.enabled=true`,再用 RAG 补充语义候选。
|
||||
6. 对候选做临时 ID 映射。
|
||||
7. 调 `LLMDecisionOrchestrator` 输出动作。
|
||||
8. 后端校验动作合法性。
|
||||
9. 执行动作:
|
||||
- `ADD`:创建 item + `create` audit
|
||||
- `UPDATE`:更新旧 item + `update` audit
|
||||
- `DELETE`:软删除旧 item + `delete` audit
|
||||
- `NONE`:只记日志,不动表
|
||||
10. 根据动作决定是否做向量同步:
|
||||
- `ADD`:新增向量
|
||||
- `UPDATE`:重写向量
|
||||
- `DELETE`:删向量或打 pending 删除标记
|
||||
|
||||
### 6.7 决策 Prompt 的建议约束
|
||||
|
||||
决策 prompt 需要非常收敛,建议只允许模型做一件事:
|
||||
|
||||
1. 给定一条新 fact。
|
||||
2. 给定少量旧候选。
|
||||
3. 在 `ADD/UPDATE/DELETE/NONE` 中选一个动作。
|
||||
|
||||
不建议第一版就让模型:
|
||||
|
||||
1. 一次同时处理多条新 fact 与多条旧事实的复杂批量决策。
|
||||
2. 自己生成复杂的替代文案策略。
|
||||
3. 自己修改 scope 或元数据。
|
||||
|
||||
推荐第一版输出结构大致为:
|
||||
|
||||
```json
|
||||
{
|
||||
"decisions": [
|
||||
{
|
||||
"candidate_index": 0,
|
||||
"action": "UPDATE",
|
||||
"target_temp_id": 2,
|
||||
"title": "更新后的标题",
|
||||
"content": "更新后的内容",
|
||||
"reason": "新事实是在纠正旧事实"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 6.8 这一步的验收标准
|
||||
|
||||
满足以下条件,可认为第一步完成:
|
||||
|
||||
1. 重复表达同一偏好,不会连续生成多条 `active` 记忆。
|
||||
2. 用户显式纠正旧偏好时,会更倾向触发 `UPDATE`,而不是再新增一条冲突记忆。
|
||||
3. `memory_audit_logs` 能明确区分 `create/update/delete`。
|
||||
4. 决策层失败时,不会阻断原有 legacy 链路。
|
||||
5. 关闭 `memory.decision.enabled` 后,系统行为可完全回到当前实现。
|
||||
|
||||
### 6.9 这一步的回滚点
|
||||
|
||||
第一步必须保留明确回滚点:
|
||||
|
||||
1. 关闭 `memory.decision.enabled`
|
||||
2. `memory.write.mode` 切回 `legacy`
|
||||
|
||||
回滚后仍然使用:
|
||||
|
||||
1. `LLMWriteOrchestrator.ExtractFacts`
|
||||
2. `NormalizeFacts`
|
||||
3. `buildMemoryItems`
|
||||
4. `ItemRepo.UpsertItems`
|
||||
|
||||
### 6.10 这一步明确不做什么
|
||||
|
||||
第一步不要顺手做以下事情:
|
||||
|
||||
1. 不重构 `newAgent` 图节点。
|
||||
2. 不引入 memory 工具调用。
|
||||
3. 不做图记忆。
|
||||
4. 不做用户侧“编辑记忆内容”的管理 API。
|
||||
5. 不同时改 WebSearch 的 RAG 链路。
|
||||
|
||||
---
|
||||
|
||||
## 7. 第二步:把读取与注入做成真正可用的记忆链路
|
||||
|
||||
### 7.1 这一步解决什么问题
|
||||
|
||||
写入侧即使更聪明,如果读出来的还是“按分数凑五条,再平铺给 prompt”,整体体验依然不会像 Mem0。
|
||||
|
||||
第二步要解决的问题是:
|
||||
|
||||
1. 硬约束和偏好不能被普通事实挤掉。
|
||||
2. 历史重复项不能继续在读侧污染 TopK。
|
||||
3. 注入给模型的文本需要可控,而不是简单平铺。
|
||||
4. RAG 可用时要真正成为加分项,不可用时要稳定降级。
|
||||
|
||||
### 7.2 本轮要落的能力
|
||||
|
||||
第二步必须落地以下能力:
|
||||
|
||||
1. 读侧合并“结构化强约束召回”和“语义候选召回”。
|
||||
2. 读侧在服务层做真正的去重,而不是只在渲染字符串时弱去重。
|
||||
3. 注入文本按类型分组,而不是所有内容同一层级平铺。
|
||||
4. 给每一类记忆设置注入预算,避免事实类把 prompt 撑爆。
|
||||
|
||||
### 7.3 推荐的文件落点
|
||||
|
||||
建议优先修改文件:
|
||||
|
||||
1. `backend/memory/service/read_service.go`
|
||||
2. `backend/memory/repo/item_repo.go`
|
||||
3. `backend/service/agentsvc/agent_memory.go`
|
||||
|
||||
如需补辅助文件,建议新增:
|
||||
|
||||
1. `backend/memory/service/retrieve_merge.go`
|
||||
- 负责多路召回的结果合并、去重、预算裁剪。
|
||||
2. `backend/memory/service/retrieve_rank.go`
|
||||
- 负责重排与门控。
|
||||
3. `backend/service/agentsvc/agent_memory_render.go`
|
||||
- 负责把 memory DTO 渲染成稳定的注入 block。
|
||||
|
||||
说明:
|
||||
|
||||
1. 当前 `agent_memory.go` 已经不算小。
|
||||
2. 第二步不要继续往单文件里堆“召回策略 + 去重 + 渲染模板”。
|
||||
3. 这一轮拆开渲染层是合理的职责拆分,不属于跨能力域大重构。
|
||||
|
||||
### 7.4 读取侧推荐的新流程
|
||||
|
||||
建议读侧升级为以下顺序:
|
||||
|
||||
1. 先从 MySQL 拉“必守约束”:
|
||||
- `constraint`
|
||||
- 高置信度 `preference`
|
||||
2. 再按当前 query 做相关召回:
|
||||
- 若 `memory.rag.enabled=true`,优先走 RAG
|
||||
- 否则走 legacy DB 排序
|
||||
3. 合并两路结果。
|
||||
4. 先按 `memory_id` 去重。
|
||||
5. 再按 `content_hash` 去重。
|
||||
6. 最后才按渲染文本兜底去重。
|
||||
7. 对结果做类型预算:
|
||||
- `constraint`:优先保留
|
||||
- `preference`:次优先
|
||||
- `todo_hint`:控制数量
|
||||
- `fact`:最容易膨胀,要严格限额
|
||||
|
||||
### 7.5 注入层推荐的渲染方式
|
||||
|
||||
当前渲染方式更像“扁平清单”。第二步建议升级成“分段注入”,例如:
|
||||
|
||||
1. 必守约束
|
||||
2. 用户偏好
|
||||
3. 当前话题相关事实
|
||||
4. 近期线索
|
||||
|
||||
推荐生成类似文本:
|
||||
|
||||
```text
|
||||
以下是与当前对话相关的用户记忆,仅在确实有帮助时参考,不要机械复述。
|
||||
|
||||
【必守约束】
|
||||
- 用户点外卖不要香菜。
|
||||
|
||||
【用户偏好】
|
||||
- 用户偏爱黑咖啡。
|
||||
|
||||
【当前话题相关事实】
|
||||
- 用户最近在准备周四的程序设计作业。
|
||||
```
|
||||
|
||||
这样做的好处:
|
||||
|
||||
1. 模型更容易区分“必须遵守”和“仅可参考”。
|
||||
2. 日后更容易按类型做 budget。
|
||||
3. 若发生错误注入,也更容易解释是哪一层出错。
|
||||
|
||||
### 7.6 第二步建议新增配置
|
||||
|
||||
建议新增:
|
||||
|
||||
1. `memory.read.mode`
|
||||
- 建议支持 `legacy` / `hybrid`
|
||||
2. `memory.read.factLimit`
|
||||
3. `memory.read.preferenceLimit`
|
||||
4. `memory.read.constraintLimit`
|
||||
5. `memory.inject.renderMode`
|
||||
- 建议支持 `flat` / `typed_v2`
|
||||
|
||||
建议默认值:
|
||||
|
||||
1. `memory.read.mode=legacy`
|
||||
2. `memory.inject.renderMode=flat`
|
||||
|
||||
灰度时再逐步切到:
|
||||
|
||||
1. `memory.read.mode=hybrid`
|
||||
2. `memory.inject.renderMode=typed_v2`
|
||||
|
||||
### 7.7 这一步的验收标准
|
||||
|
||||
满足以下条件,可认为第二步完成:
|
||||
|
||||
1. 同一条重复记忆即使数据库里有多条,最终注入给 prompt 也只保留一条。
|
||||
2. `constraint` 类记忆不会轻易被 `fact` 类挤出注入集合。
|
||||
3. RAG 异常时,系统仍能稳定退回 legacy 读取逻辑。
|
||||
4. 注入文本结构清晰,且总长度稳定,不会一轮长一轮短。
|
||||
5. newAgent 的 `pinned block` 内容更可读、更可解释。
|
||||
|
||||
### 7.8 这一步的回滚点
|
||||
|
||||
第二步必须支持快速回滚:
|
||||
|
||||
1. `memory.read.mode=legacy`
|
||||
2. `memory.inject.renderMode=flat`
|
||||
3. `memory.rag.enabled=false`
|
||||
|
||||
回滚后保留:
|
||||
|
||||
1. 旧的 `ReadService.retrieveByLegacy`
|
||||
2. 当前 `agent_memory.go` 扁平渲染逻辑
|
||||
|
||||
### 7.9 这一步明确不做什么
|
||||
|
||||
第二步不要顺手做以下事情:
|
||||
|
||||
1. 不把 memory 改造成工具调用。
|
||||
2. 不改 `newAgent` 的图路由结构。
|
||||
3. 不把 WebSearch 一起并进统一召回。
|
||||
4. 不在这一轮清理历史重复脏数据。
|
||||
|
||||
---
|
||||
|
||||
## 8. 第三步:做治理、清理、指标与切流收口
|
||||
|
||||
### 8.1 这一步解决什么问题
|
||||
|
||||
前两步做完后,系统可能“效果已经不错”,但仍缺三个上线必须项:
|
||||
|
||||
1. 出问题时怎么知道错在哪一层。
|
||||
2. 历史已经写进去的重复脏数据怎么治理。
|
||||
3. 什么时候能把 legacy 路径关掉。
|
||||
|
||||
第三步就是收口这一层。
|
||||
|
||||
### 8.2 本轮要落的能力
|
||||
|
||||
第三步建议至少做以下能力:
|
||||
|
||||
1. 为写入决策、读取召回、注入渲染补齐结构化日志和指标。
|
||||
2. 增加历史重复清理能力。
|
||||
3. 补齐 `update/restore` 等审计语义。
|
||||
4. 明确 feature flag 切流策略与回滚手册。
|
||||
5. 更新文档,避免后续维护者只看到旧 README。
|
||||
|
||||
### 8.3 推荐的文件落点
|
||||
|
||||
建议修改文件:
|
||||
|
||||
1. `backend/memory/utils/audit.go`
|
||||
2. `backend/memory/service/manage_service.go`
|
||||
3. `backend/memory/repo/item_repo.go`
|
||||
4. `backend/memory/README.md`
|
||||
5. `backend/memory/记忆模块实施计划.md`
|
||||
|
||||
建议新增文件:
|
||||
|
||||
1. `backend/memory/cleanup/dedup_runner.go`
|
||||
- 用于历史重复治理。
|
||||
2. `backend/memory/cleanup/dedup_policy.go`
|
||||
- 负责定义“保留哪条、归档哪条”。
|
||||
3. `backend/memory/observe/log_fields.go`
|
||||
- 统一日志字段,避免不同文件各写各的。
|
||||
|
||||
### 8.4 历史数据清理建议
|
||||
|
||||
建议不要直接写危险 SQL 一把梭清表,而是通过可审计的治理流程清理历史脏数据:
|
||||
|
||||
1. 按 `user_id + memory_type + content_hash + status=active` 扫描重复组。
|
||||
2. 为每组挑一个保留主记录:
|
||||
- 优先保留最近更新
|
||||
- 或优先保留置信度更高
|
||||
3. 其余重复项改为 `archived` 或 `deleted`。
|
||||
4. 对每次治理动作写审计日志。
|
||||
|
||||
建议第一版优先做“离线治理工具”或“手动触发 job”,不要直接绑到主 worker 周期任务里。
|
||||
|
||||
### 8.5 建议补的指标
|
||||
|
||||
第三步建议至少打这些指标:
|
||||
|
||||
1. `memory_job_success_rate`
|
||||
2. `memory_job_retry_rate`
|
||||
3. `memory_decision_distribution`
|
||||
4. `memory_decision_fallback_rate`
|
||||
5. `memory_retrieve_hit_count`
|
||||
6. `memory_retrieve_dedup_drop_count`
|
||||
7. `memory_inject_item_count`
|
||||
8. `memory_rag_fallback_rate`
|
||||
9. `memory_wrong_mention_rate`
|
||||
10. `memory_user_correction_rate`
|
||||
|
||||
其中前八项可以本轮先落,后两项可通过后续用户纠正入口接入。
|
||||
|
||||
### 8.6 建议的切流顺序
|
||||
|
||||
第三步不要“一刀切”。建议按以下顺序灰度:
|
||||
|
||||
1. 阶段 A:决策层 shadow 模式
|
||||
- 真正写库仍走 legacy
|
||||
- 新决策层只做日志,不生效
|
||||
2. 阶段 B:决策层仅对显式记忆生效
|
||||
3. 阶段 C:决策层对全部写入生效
|
||||
4. 阶段 D:读侧切到 hybrid
|
||||
5. 阶段 E:注入切到 typed_v2
|
||||
6. 阶段 F:历史清理跑完,再考虑关闭 legacy 默认路径
|
||||
|
||||
### 8.7 这一步的验收标准
|
||||
|
||||
满足以下条件,可认为第三步完成:
|
||||
|
||||
1. 能从日志看清某条记忆为何被 `ADD/UPDATE/DELETE/NONE`。
|
||||
2. 能从指标看清读侧命中、去重、降级、回滚情况。
|
||||
3. 能对历史重复数据做可审计清理。
|
||||
4. 出现异常时可在分钟级通过开关退回 legacy。
|
||||
5. 文档与代码现状一致,不再依赖口头传递。
|
||||
|
||||
### 8.8 这一步的回滚点
|
||||
|
||||
第三步的回滚不应影响前两步代码保留,只需回切开关:
|
||||
|
||||
1. 决策层回到 `legacy`
|
||||
2. 读侧回到 `legacy`
|
||||
3. 注入渲染回到 `flat`
|
||||
4. 停掉清理任务
|
||||
|
||||
### 8.9 这一步明确不做什么
|
||||
|
||||
第三步仍然不建议同时做以下事情:
|
||||
|
||||
1. 不做图记忆。
|
||||
2. 不做多 Provider 工厂化。
|
||||
3. 不拆独立 memory 服务。
|
||||
4. 不把 WebSearch 与 Memory 强行合并到同一轮上线。
|
||||
|
||||
---
|
||||
|
||||
## 9. 推荐的三轮交付顺序
|
||||
|
||||
如果资源有限,建议严格按下面顺序推进:
|
||||
|
||||
1. 先做第一步。
|
||||
- 原因:写侧如果还是“抽取即新增”,读侧再怎么优化也会越来越脏。
|
||||
2. 再做第二步。
|
||||
- 原因:写侧稳定后,读侧才能真正体现效果。
|
||||
3. 最后做第三步。
|
||||
- 原因:治理、指标、清理要建立在前两步能力已经基本成形的前提下。
|
||||
|
||||
一句话总结:
|
||||
|
||||
1. 先让系统“会整理记忆”。
|
||||
2. 再让系统“会正确读记忆”。
|
||||
3. 最后让系统“可稳定上线和维护”。
|
||||
|
||||
---
|
||||
|
||||
## 10. 建议的任务拆分方式
|
||||
|
||||
如果后续要多人并行,建议按职责边界拆,而不是按文件随意拆:
|
||||
|
||||
### 10.1 第一步可拆为两块
|
||||
|
||||
1. 决策模型与编排
|
||||
- `decision.go`
|
||||
- `llm_decision_orchestrator.go`
|
||||
- `decision_validate.go`
|
||||
2. Repo 与动作执行
|
||||
- `item_repo.go`
|
||||
- `apply_actions.go`
|
||||
- `audit.go`
|
||||
|
||||
### 10.2 第二步可拆为两块
|
||||
|
||||
1. 读侧召回与合并
|
||||
- `read_service.go`
|
||||
- `retrieve_merge.go`
|
||||
- `retrieve_rank.go`
|
||||
2. newAgent 注入渲染
|
||||
- `agent_memory.go`
|
||||
- `agent_memory_render.go`
|
||||
|
||||
### 10.3 第三步可拆为两块
|
||||
|
||||
1. 治理与清理
|
||||
- `dedup_runner.go`
|
||||
- `manage_service.go`
|
||||
2. 观测与文档
|
||||
- 指标日志
|
||||
- README / 计划文档更新
|
||||
|
||||
---
|
||||
|
||||
## 11. 如果只看一个结论,请看这里
|
||||
|
||||
要让当前 memory 真正靠近 Mem0,不是再加一张表,也不是再加一个 prompt,而是要完成以下收敛:
|
||||
|
||||
1. 写入侧从“抽到就加”升级为“先回看旧记忆,再决定加改删不做”。
|
||||
2. 读侧从“查到就拼”升级为“硬约束优先、语义补充、结果去重、预算注入”。
|
||||
3. 系统侧从“能跑”升级为“有灰度、有指标、有清理、有回滚”。
|
||||
|
||||
只要三步按这个顺序推进,最终得到的就不是一个“会不断积灰的记忆表”,而是一套真正能为 `newAgent` 服务的记忆系统。
|
||||
@@ -217,7 +217,7 @@ func streamAndDispatch(
|
||||
)
|
||||
|
||||
flowState.AllowReorder = resolveAllowReorder(input.UserInput, decision.AllowReorder)
|
||||
effectiveThinking := resolveEffectiveThinking(flowState.ThinkingMode, decision.Thinking)
|
||||
effectiveThinking := resolveEffectiveThinking(flowState.ThinkingMode, decision.Route, decision.Thinking)
|
||||
|
||||
switch decision.Route {
|
||||
case newagentmodel.ChatRouteDirectReply:
|
||||
@@ -243,16 +243,22 @@ func streamAndDispatch(
|
||||
// resolveEffectiveThinking 根据前端 ThinkingMode 和路由决策合并出最终 thinking 状态。
|
||||
//
|
||||
// 规则:
|
||||
// - "true" 强制开启;
|
||||
// - "false" 强制关闭;
|
||||
// - "auto"/"" 交给路由决策的 decisionThinking。
|
||||
func resolveEffectiveThinking(mode string, decisionThinking bool) bool {
|
||||
// 1. "true":前端强制开启,所有路由统一开;
|
||||
// 2. "false":前端强制关闭,所有路由统一关;
|
||||
// 3. "auto"/"":按路由语义兜底;
|
||||
// 3.1 deep_answer 的语义本身就是"复杂问答 + 原地深度思考",因此默认开启;
|
||||
// 3.2 execute 继续沿用路由模型给出的 decisionThinking;
|
||||
// 3.3 其余路由默认关闭,避免把轻量闲聊误升成高成本推理。
|
||||
func resolveEffectiveThinking(mode string, route newagentmodel.ChatRoute, decisionThinking bool) bool {
|
||||
switch strings.TrimSpace(strings.ToLower(mode)) {
|
||||
case "true":
|
||||
return true
|
||||
case "false":
|
||||
return false
|
||||
default:
|
||||
if route == newagentmodel.ChatRouteDeepAnswer {
|
||||
return true
|
||||
}
|
||||
return decisionThinking
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,14 +10,21 @@ import (
|
||||
)
|
||||
|
||||
const chatRoutingSystemPrompt = `
|
||||
你是 SmartFlow 的智能路由器。你的回复必须以路由控制码开头,控制码后紧跟用户可见的内容。
|
||||
你是 SmartMate 的聊天路由助手。SmartMate 是时伴(SmartMate)的中文 AI 排程伙伴,面向大学生提供陪伴式日程管理与日常协助;它擅长日程安排、任务管理与学习规划,但不只会做排程。你的回复必须以路由控制码开头,控制码后紧跟用户可见的内容。
|
||||
|
||||
路由规则:
|
||||
- direct_reply:纯闲聊、简单问答、打招呼、感谢等。控制码后直接输出完整回复。
|
||||
- direct_reply:纯闲聊、简单问答、轻量生活建议、打招呼、感谢等不需要工具、也不需要长链路思考的请求。控制码后直接输出完整回复。
|
||||
- execute:需要用工具处理的请求(查询日程、移动课程、排课等),但不需要先制定计划。控制码后输出简短确认。
|
||||
- deep_answer:复杂问题但不需要工具(如分析建议、深度解释等),需要深度思考后回答。控制码后输出过渡语(如"让我想想")。
|
||||
- deep_answer:复杂问题但不需要工具(如分析建议、知识解释、方案比较、深度讨论等),需要深度思考后回答。控制码后不要输出任何占位过渡语,后端会直接进入第二次正式回答。
|
||||
- plan:用户明确要求先制定计划,或涉及多阶段复杂规划。控制码后输出简短确认。
|
||||
|
||||
通用回答约束:
|
||||
- 非日程、非任务类问题,只要不需要工具,也应当正常回答。
|
||||
- 不要因为用户的问题不涉及排程,就说自己“只能处理日程/任务安排”。
|
||||
- 不要把普通问答、生活建议、开放式讨论,硬拐成排程请求。
|
||||
- route=direct_reply 时,控制码后的可见内容应直接回应用户问题,而不是先讲能力边界。
|
||||
- route=deep_answer 时,只输出控制码即可,不要补“让我想想”“这是个好问题”之类的占位话术。
|
||||
|
||||
粗排判断:当用户意图包含"批量安排/排课/把任务类排进日程",且上下文中有任务类 ID 时,设置 rough_build=true。
|
||||
二次粗排约束(强约束):
|
||||
- 若上下文已出现 rough_build_done,且用户未明确要求"重新粗排/从头重排",必须设置 rough_build=false。
|
||||
@@ -50,7 +57,7 @@ const chatRoutingSystemPrompt = `
|
||||
合法示例:
|
||||
|
||||
<SMARTFLOW_ROUTE nonce="给定nonce" route="direct_reply"/>
|
||||
你好!我是 SmartFlow 助手,有什么可以帮你的?
|
||||
当然可以,我先直接回答你这个问题。
|
||||
|
||||
<SMARTFLOW_ROUTE nonce="给定nonce" route="execute"/>
|
||||
好的,我来帮你看看今天的安排。
|
||||
@@ -62,7 +69,6 @@ const chatRoutingSystemPrompt = `
|
||||
好的,我来帮你排课并按你的偏好做微调。
|
||||
|
||||
<SMARTFLOW_ROUTE nonce="给定nonce" route="deep_answer"/>
|
||||
这是个好问题,让我仔细想想。
|
||||
|
||||
<SMARTFLOW_ROUTE nonce="给定nonce" route="plan"/>
|
||||
明白,我来帮你制定一个完整的学习计划。
|
||||
@@ -125,12 +131,13 @@ func BuildChatRoutingUserPrompt(ctx *newagentmodel.ConversationContext, userInpu
|
||||
// --- 深度回答 prompt ---
|
||||
|
||||
const deepAnswerSystemPrompt = `
|
||||
你是 SmartFlow 的深度分析助手。用户提出了一个需要深入思考的问题,请认真分析后给出详细、有价值的回答。
|
||||
你是 SmartMate 的深度分析助手。SmartMate 是时伴(SmartMate)的中文 AI 排程伙伴;即使问题与日程、任务无关,只要不需要工具,你也应当认真分析后给出详细、有价值的回答。
|
||||
|
||||
请遵守以下规则:
|
||||
1. 充分利用上下文中已有的信息(任务类约束、日程数据、历史对话等)。
|
||||
2. 如果缺少关键信息,在回答中说明需要哪些额外信息。
|
||||
3. 直接输出你的回答,不要输出 JSON。
|
||||
1. 优先回答用户真实问题,不要把普通问答硬拐回排程、任务或计划制定。
|
||||
2. 充分利用上下文中已有的信息(历史对话、记忆、任务类约束、日程数据等),但不要无关硬套。
|
||||
3. 如果缺少关键信息,在回答中说明需要哪些额外信息。
|
||||
4. 直接输出你的回答,不要输出 JSON。
|
||||
`
|
||||
|
||||
// BuildDeepAnswerSystemPrompt 返回深度回答阶段的系统提示词。
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
const deliverSystemPrompt = `
|
||||
你是 SmartFlow NewAgent 的交付器。
|
||||
你是 SmartMate 的交付器。
|
||||
你的职责是基于原始计划和执行历史,生成一份简洁、诚实的任务完成总结。
|
||||
|
||||
请遵守以下规则:
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
const executeSystemPromptWithPlan = `
|
||||
你是 SmartFlow NewAgent 的执行器。你需要在"当前 plan 步骤"约束下推进任务。
|
||||
你是 SmartMate 的执行器。你需要在"当前 plan 步骤"约束下推进任务。
|
||||
|
||||
你可以做什么:
|
||||
1. 只围绕当前步骤推进,先读后写,逐步完成当前步骤。
|
||||
@@ -45,7 +45,7 @@ const executeSystemPromptWithPlan = `
|
||||
7. 流程应正式终止时输出 action=abort。`
|
||||
|
||||
const executeSystemPromptReAct = `
|
||||
你是 SmartFlow NewAgent 的执行器,当前处于自由执行模式(无预定义 plan 步骤)。
|
||||
你是 SmartMate 的执行器,当前处于自由执行模式(无预定义 plan 步骤)。
|
||||
|
||||
阶段事实(强约束):
|
||||
1. 若上下文给出"粗排已完成/rough_build_done",表示目标任务类已经进入 suggested/existing,不是待排入状态。
|
||||
|
||||
@@ -48,7 +48,7 @@ const executeMessage1MaxRunes = 1400
|
||||
// 1. message[0] 固定 prompt(规则 + 微调硬引导 + 输出约束 + 工具简表)
|
||||
// 2. message[1] 历史上下文(真实对话流 + 早期 ReAct 摘要)
|
||||
// 3. message[2] 当轮 ReAct Loop 窗口(thought/reason + tool_call + observation 绑定展示)
|
||||
// 4. message[3] 当前执行状态(轮次、模式、plan 步骤、任务类等)
|
||||
// 4. message[3] 当前执行状态(轮次、模式、plan 步骤、任务类、相关记忆等)
|
||||
func buildExecuteStageMessages(
|
||||
stageSystemPrompt string,
|
||||
state *newagentmodel.CommonState,
|
||||
@@ -72,7 +72,7 @@ func buildExecuteStageMessages(
|
||||
func buildExecuteMessage0(stageSystemPrompt string, ctx *newagentmodel.ConversationContext) string {
|
||||
base := strings.TrimSpace(mergeSystemPrompts(ctx, stageSystemPrompt))
|
||||
if base == "" {
|
||||
base = "你是 SmartFlow NewAgent 执行器,请继续 execute 阶段。"
|
||||
base = "你是 SmartMate 执行器,请继续 execute 阶段。"
|
||||
}
|
||||
|
||||
toolCatalog := renderExecuteToolCatalogCompact(ctx)
|
||||
@@ -290,6 +290,10 @@ func buildExecuteMessage3(state *newagentmodel.CommonState, ctx *newagentmodel.C
|
||||
lines = append(lines, "- 顺序策略:默认保持 suggested 相对顺序,禁止调用 min_context_switch。")
|
||||
}
|
||||
}
|
||||
if memoryText := renderExecuteMemoryContext(ctx); memoryText != "" {
|
||||
lines = append(lines, "相关记忆(仅在确有帮助时参考,不要机械复述):")
|
||||
lines = append(lines, memoryText)
|
||||
}
|
||||
|
||||
// 兼容上层传入的执行指令;若为空则使用固定收口指令。
|
||||
instruction := strings.TrimSpace(runtimeUserPrompt)
|
||||
|
||||
31
backend/newAgent/prompt/execute_pinned.go
Normal file
31
backend/newAgent/prompt/execute_pinned.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package newagentprompt
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model"
|
||||
)
|
||||
|
||||
const executeMemoryContextKey = "memory_context"
|
||||
|
||||
// renderExecuteMemoryContext 提取 Execute 阶段需要补充到 msg3 的记忆文本。
|
||||
//
|
||||
// 步骤化说明:
|
||||
// 1. 只白名单消费 memory_context,避免把 execution_context / current_step 等 Execute 自有块再次注入;
|
||||
// 2. 若 block 不存在或正文为空,直接返回空串,不给 msg3 留空段;
|
||||
// 3. 这里不重新渲染记忆,只消费 agentsvc 已经产出的最终文本,保证所有阶段口径一致。
|
||||
func renderExecuteMemoryContext(ctx *newagentmodel.ConversationContext) string {
|
||||
if ctx == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
block, ok := ctx.PinnedBlockByKey(executeMemoryContextKey)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
content := strings.TrimSpace(block.Content)
|
||||
if content == "" {
|
||||
return ""
|
||||
}
|
||||
return content
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
const planSystemPrompt = `
|
||||
你是 SmartFlow NewAgent 的规划器。
|
||||
你是 SmartMate 的规划器。
|
||||
你的职责不是直接执行任务,而是先把用户意图拆成一组清晰、稳定、可逐步执行的自然语言计划,并严格按后端约定的 JSON 协议输出。
|
||||
|
||||
请遵守以下规则:
|
||||
|
||||
Reference in New Issue
Block a user