Files
xuhdandClaude Opus 4.7 f9d7610679 feat(memory): Memory V2 重写 - 可插拔 provider + Hindsight 集成
以 Hermes 的可插拔记忆架构为骨架重写 deerflow 记忆模块,兼容原有
结构化数据,并新增 Hindsight 作为外部记忆后端。

P1 - 抽象层 + builtin 本地记忆:
- MemoryProvider 抽象基类 + MemoryManager 编排层(builtin + ≤1 外部)
- BuiltinFileProvider:USER.md/MEMORY.md 二元存储、字符配额、文件锁、
  原子写、frozen snapshot
- memory 工具(LangChain @tool):LLM 主动写入用户画像/工作笔记
- 记忆内容安全扫描(注入/外渗模式 + 不可见 unicode)
- memory_config 改为嵌套结构 + 旧扁平字段兼容 + per-user 三层覆盖
- 迁移脚本 migrate_memory_to_v2.py:旧 memory.json -> USER.md/MEMORY.md
- Gateway 新增 /api/memory/v2 与 v2/config 端点(用户可配置白名单字段)

P2 - Hindsight 外部 provider:
- HindsightProvider:双 bank 隔离(user bank 跨 agent 共享 + work bank
  agent 私有)、并发召回合并、auto-retain、on_memory_write 镜像
- hindsight_recall/reflect/retain 三个 LangChain 工具
- MemoryMiddleware.before_model 注入 Hindsight 动态召回
- context/tools/hybrid 三种 memory_mode
- hindsight-client 作为可选依赖,未装时自动退回纯 builtin

注入走系统提示词(builtin frozen snapshot)+ before_model(Hindsight
动态召回)。后台抽取保留但默认关闭,P3 期重接入。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 12:15:55 +08:00

156 lines
5.9 KiB
Python

"""Memory V2 - Hindsight 的 3 个 LangChain 工具。
仅当 ``config.memory.hindsight.memory_mode`` 为 ``tools`` 或 ``hybrid`` 时,
这些工具才会被 ``get_available_tools`` 加入工具集 (``context`` 模式不暴露)。
- hindsight_recall 多策略检索 (语义 + 实体图谱),查双 bank
- hindsight_reflect 跨记忆综合 (LLM 推理)
- hindsight_retain 主动存储一条记忆到全局 user bank
与 builtin 的 ``memory`` 工具一样,这些工具从 :class:`ToolRuntime` 与请求上下文
解析 user_id / agent_id,按生效配置懒建一个 :class:`HindsightProvider` 后调用。
"""
from __future__ import annotations
import json
import logging
from langchain.tools import ToolRuntime, tool
from langgraph.config import get_config
from langgraph.typing import ContextT
from deerflow.agents.thread_state import ThreadState
from deerflow.config.memory_config import get_effective_memory_config
from deerflow.runtime.user_context import get_effective_user_id
logger = logging.getLogger(__name__)
def _resolve_agent_id(runtime: ToolRuntime[ContextT, ThreadState]) -> str:
"""从 runtime context / RunnableConfig 解析 agent_id,缺省 ``"default"``。"""
agent_id = runtime.context.get("agent_id") if runtime.context else None
if agent_id:
return str(agent_id)
runtime_config = getattr(runtime, "config", None) or {}
agent_id = runtime_config.get("configurable", {}).get("agent_id")
if agent_id:
return str(agent_id)
try:
agent_id = get_config().get("configurable", {}).get("agent_id")
except RuntimeError:
agent_id = None
return str(agent_id) if agent_id else "default"
def _build_provider(runtime: ToolRuntime[ContextT, ThreadState]):
"""按生效配置懒建并初始化一个 HindsightProvider;不可用时返回 ``None``。"""
from deerflow.agents.memory.providers.hindsight import HindsightProvider
user_id = get_effective_user_id()
config = get_effective_memory_config(user_id)
if not config.enabled or config.provider != "hindsight":
return None
provider = HindsightProvider(config.hindsight)
if not provider.is_available():
return None
thread_id = runtime.context.get("thread_id") if runtime.context else None
provider.initialize(
user_id=user_id,
agent_id=_resolve_agent_id(runtime),
thread_id=thread_id or "",
base_dir="",
)
return provider
@tool("hindsight_recall", parse_docstring=True)
def hindsight_recall_tool(
runtime: ToolRuntime[ContextT, ThreadState],
query: str,
) -> str:
"""检索 Hindsight 长期记忆,查找与 query 相关的历史信息。
用于回答关于用户、过往会话、之前讨论过的内容的问题。会同时检索全局用户
记忆和当前 agent 的工作记忆。
Args:
query: 检索查询,描述你想找回什么信息。
"""
provider = _build_provider(runtime)
if provider is None:
return json.dumps({"success": False, "error": "Hindsight 记忆不可用。"}, ensure_ascii=False)
try:
results = provider.recall(query)
except Exception as exc: # noqa: BLE001
logger.warning("hindsight_recall 失败:%s", exc)
return json.dumps({"success": False, "error": f"检索失败:{exc}"}, ensure_ascii=False)
if not results:
return json.dumps({"success": True, "result": "没有找到相关记忆。"}, ensure_ascii=False)
lines = "\n".join(f"{i}. {text}" for i, text in enumerate(results, 1))
return json.dumps({"success": True, "result": lines}, ensure_ascii=False)
@tool("hindsight_reflect", parse_docstring=True)
def hindsight_reflect_tool(
runtime: ToolRuntime[ContextT, ThreadState],
query: str,
) -> str:
"""对 Hindsight 记忆做跨记忆综合推理,得到一段综合性回答。
与 hindsight_recall 返回原始记忆片段不同,reflect 会让 LLM 综合多条记忆
给出推理结论。适合需要归纳、总结、跨记忆关联的问题。
Args:
query: 你希望综合分析的问题。
"""
provider = _build_provider(runtime)
if provider is None:
return json.dumps({"success": False, "error": "Hindsight 记忆不可用。"}, ensure_ascii=False)
try:
text = provider.reflect(query)
except Exception as exc: # noqa: BLE001
logger.warning("hindsight_reflect 失败:%s", exc)
return json.dumps({"success": False, "error": f"综合失败:{exc}"}, ensure_ascii=False)
return json.dumps(
{"success": True, "result": text or "没有找到相关记忆。"}, ensure_ascii=False
)
@tool("hindsight_retain", parse_docstring=True)
def hindsight_retain_tool(
runtime: ToolRuntime[ContextT, ThreadState],
content: str,
) -> str:
"""主动把一条信息存入 Hindsight 的全局用户记忆库。
用于记录跨会话、跨 agent 仍有价值的用户事实。Hindsight 会自动做实体抽取
和知识图谱关联。
Args:
content: 要存储的记忆内容。
"""
if not content or not content.strip():
return json.dumps({"success": False, "error": "content 不能为空。"}, ensure_ascii=False)
provider = _build_provider(runtime)
if provider is None:
return json.dumps({"success": False, "error": "Hindsight 记忆不可用。"}, ensure_ascii=False)
try:
provider.retain_to_user_bank(content.strip())
except Exception as exc: # noqa: BLE001
logger.warning("hindsight_retain 失败:%s", exc)
return json.dumps({"success": False, "error": f"存储失败:{exc}"}, ensure_ascii=False)
return json.dumps({"success": True, "result": "记忆已存储。"}, ensure_ascii=False)
#: 三个 Hindsight 工具,供 get_available_tools 在 tools/hybrid 模式下加入。
HINDSIGHT_TOOLS = [hindsight_recall_tool, hindsight_reflect_tool, hindsight_retain_tool]
__all__ = [
"hindsight_recall_tool",
"hindsight_reflect_tool",
"hindsight_retain_tool",
"HINDSIGHT_TOOLS",
]