mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-28 14:10:37 +00:00
docs(harness): scope the memory guides per user and nest DeerMem knobs (#7463)
* test(memory): pin the memory guides to the layout DeerMem resolves
The en and zh harness guides still document the pre-abstraction storage
shape, and nothing in CI reads prose, so the drift stayed invisible.
Derive the per-user manifest and per-agent facts paths from
memory_file_path()/agent_facts_directory() and take the allowed memory:
top-level keys from the public MemoryConfig model.
* docs(harness): scope memory docs per user and nest DeerMem knobs
Both guides claimed a shared {base_dir}/memory.json plus one memory.json
per agent; DeerMem keeps one manifest per user and puts agent state in a
facts directory beside it. The zh Configuration example also still showed
DeerMem-private keys at the memory top level, which config.example.yaml
and the en twin already place under backend_config.
* docs(memory): align per-agent facts overviews in both languages
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
parent
228913b6b7
commit
07a4770e32
102
backend/tests/test_memory_documentation.py
Normal file
102
backend/tests/test_memory_documentation.py
Normal file
@ -0,0 +1,102 @@
|
||||
"""Keep the user-facing memory guides aligned with DeerMem's real storage layout.
|
||||
|
||||
The guides once described the pre-abstraction layout (a shared
|
||||
``{base_dir}/memory.json`` plus one ``agents/{agent_name}/memory.json`` per
|
||||
agent). DeerMem scopes the memory manifest per user and keeps agent state in a
|
||||
``facts`` directory beside it, and the host loader moved every DeerMem-private
|
||||
knob under ``memory.backend_config``. Both guides are prose, so nothing in CI
|
||||
noticed; this test derives the documented shape from the same functions the
|
||||
runtime uses.
|
||||
"""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from deerflow.agents.memory.backends.deermem.deermem.config import DeerMemConfig
|
||||
from deerflow.agents.memory.backends.deermem.deermem.core.paths import agent_facts_directory, memory_file_path
|
||||
from deerflow.config.memory_config import MemoryConfig
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
MEMORY_GUIDES = (
|
||||
Path("frontend/src/content/en/harness/memory.mdx"),
|
||||
Path("frontend/src/content/zh/harness/memory.mdx"),
|
||||
)
|
||||
|
||||
# A root that never collides with the placeholders the guides spell out.
|
||||
PROBE_ROOT = "/deer-flow-test-root"
|
||||
PROBE_USER_ID = "probe-user"
|
||||
PROBE_AGENT_NAME = "Probe-Agent"
|
||||
|
||||
# The pre-abstraction per-agent manifest file. DeerMem writes agent state under
|
||||
# ``agents/{agent_name}/facts/`` instead, so a guide naming this path is stale.
|
||||
LEGACY_PER_AGENT_FILE = "agents/{agent_name}/memory.json"
|
||||
|
||||
|
||||
def _guide_text(path: Path) -> str:
|
||||
return (REPO_ROOT / path).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _configuration_block(path: Path) -> str:
|
||||
"""Return the first yaml example under the guide's Configuration heading."""
|
||||
lines = _guide_text(path).splitlines()
|
||||
starts = [index for index, line in enumerate(lines) if re.fullmatch(r"## (Configuration|配置)\s*", line)]
|
||||
assert len(starts) == 1, f"expected exactly one Configuration heading in {path}, found {len(starts)}"
|
||||
fences = [index for index in range(starts[0], len(lines)) if lines[index].strip().startswith("```yaml")]
|
||||
assert fences, f"no yaml example after the Configuration heading in {path}"
|
||||
end = next(index for index in range(fences[0] + 1, len(lines)) if lines[index].strip() == "```")
|
||||
return "\n".join(lines[fences[0] + 1 : end])
|
||||
|
||||
|
||||
def _top_level_memory_keys(block: str) -> set[str]:
|
||||
"""Keys written directly under ``memory:`` (two-space indent) in a yaml example."""
|
||||
keys = set()
|
||||
inside = False
|
||||
for line in block.splitlines():
|
||||
if line.startswith("memory:"):
|
||||
inside = True
|
||||
continue
|
||||
if not inside:
|
||||
continue
|
||||
if line and not line.startswith((" ", " ", "\t", "#")):
|
||||
break
|
||||
match = re.fullmatch(r" ([A-Za-z_][A-Za-z0-9_]*):.*", line)
|
||||
if match:
|
||||
keys.add(match.group(1))
|
||||
return keys
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def probe_layout() -> tuple[str, str]:
|
||||
"""The per-user manifest and per-agent facts paths, as the runtime resolves them."""
|
||||
memory_path = memory_file_path(DeerMemConfig(storage_path=PROBE_ROOT), user_id=PROBE_USER_ID)
|
||||
facts_path = agent_facts_directory(memory_path, PROBE_AGENT_NAME)
|
||||
return str(memory_path.relative_to(PROBE_ROOT)), str(facts_path.relative_to(PROBE_ROOT))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("path", MEMORY_GUIDES, ids=str)
|
||||
def test_guide_documents_the_scoped_manifest_and_facts_paths(path: Path, probe_layout: tuple[str, str]) -> None:
|
||||
content = _guide_text(path)
|
||||
memory_rel, facts_rel = probe_layout
|
||||
|
||||
# Guides spell the resolved layout with placeholders, so re-spell the real paths
|
||||
# the same way before comparing. ``agent_facts_directory`` already folded the
|
||||
# mixed-case probe name, hence the lowercase replacement.
|
||||
def spelled(rel: str) -> str:
|
||||
return rel.replace(PROBE_USER_ID, "{user_id}").replace(PROBE_AGENT_NAME.lower(), "{agent_name}")
|
||||
|
||||
scoped_manifest = spelled(memory_rel)
|
||||
agent_facts = spelled(facts_rel)
|
||||
assert scoped_manifest in content, f"{path} does not document the per-user manifest at {scoped_manifest}"
|
||||
assert agent_facts in content, f"{path} does not document the per-agent facts directory at {agent_facts}"
|
||||
assert LEGACY_PER_AGENT_FILE not in content, f"{path} still documents the pre-abstraction per-agent file {LEGACY_PER_AGENT_FILE}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("path", MEMORY_GUIDES, ids=str)
|
||||
def test_guide_keeps_backend_private_knobs_out_of_the_memory_top_level(path: Path) -> None:
|
||||
keys = _top_level_memory_keys(_configuration_block(path))
|
||||
assert keys, f"no keys parsed from the Configuration example in {path}"
|
||||
shared = set(MemoryConfig.model_fields)
|
||||
drift = sorted(keys - shared)
|
||||
assert not drift, f"{path} documents DeerMem-private keys at the memory top level: {drift} (they belong under backend_config)"
|
||||
@ -34,7 +34,7 @@ Memory has two operation modes:
|
||||
1. **Injection in both modes**: at the start of each conversation, the agent's current memory is injected into the system prompt at a controlled token budget (`max_injection_tokens`).
|
||||
2. **Middleware mode (default)**: `MemoryMiddleware` runs after each Lead Agent turn. It filters the conversation, queues a background update, and extracts new facts automatically. Updates are debounced by `debounce_seconds` to batch rapid changes.
|
||||
3. **Tool mode (experimental)**: DeerFlow registers `memory_search`, `memory_get`, `memory_add`, `memory_update`, and `memory_delete` as agent tools and skips `MemoryMiddleware`. The model decides when to search, read, or write memory, so effectiveness depends on model tool-use behavior. These explicit CRUD tools are not the passive staleness-review path; operators who enable tool mode are opting into model-directed updates/deletes instead of the middleware staleness guardrails.
|
||||
4. **Per-agent memory**: when a custom agent is active, its memory is stored separately from the global memory. This keeps different agents' knowledge isolated.
|
||||
4. **Per-agent facts**: the default DeerMem backend stores each custom agent's learned facts in its own facts directory. Agents share one memory manifest within the same user.
|
||||
|
||||
In tool mode, the five `memory_*` tool names are reserved. If an MCP server or custom tool already uses `memory_search`, `memory_get`, `memory_add`, `memory_update`, or `memory_delete`, the existing tool keeps that name and the colliding memory tool is skipped with a warning.
|
||||
|
||||
@ -295,12 +295,14 @@ guide.
|
||||
|
||||
## Global vs per-agent memory
|
||||
|
||||
DeerFlow supports two levels of memory:
|
||||
DeerFlow scopes memory files per user and facts per agent:
|
||||
|
||||
- **Global memory**: stored at `{base_dir}/memory.json`. Used when no specific agent is active or when the agent has no per-agent memory file.
|
||||
- **Per-agent memory**: stored at `{base_dir}/agents/{agent_name}/memory.json`. Used when a custom agent is active, keeping that agent's learned knowledge separate.
|
||||
- **Per-user memory**: `{base_dir}/users/{user_id}/memory.json` holds that user's cross-task summary. `agent_name` does not select a different memory file.
|
||||
- **Per-agent facts**: `{base_dir}/users/{user_id}/agents/{agent_name}/facts/` sits next to the same user's memory file, keeping one agent's learned facts separate from another's. The `{agent_name}` segment is lowercased, and names are restricted to `A-Z`, `a-z`, `0-9` and `-`.
|
||||
|
||||
The `MemoryMiddleware` automatically selects the correct memory file based on the active `agent_name` in the request configuration.
|
||||
A caller without a `user_id` falls back to the unscoped legacy root `{base_dir}/memory.json` — see the `Legacy: no user_id` branch in `deermem/core/paths.py`.
|
||||
|
||||
The `MemoryMiddleware` selects which agent's facts directory to read from the active `agent_name` in the request configuration.
|
||||
|
||||
Agent names used for memory storage are validated against `AGENT_NAME_PATTERN` to ensure filesystem safety.
|
||||
|
||||
@ -308,11 +310,11 @@ Agent names used for memory storage are validated against `AGENT_NAME_PATTERN` t
|
||||
|
||||
By default, memory files are stored under the backend base directory:
|
||||
|
||||
- Base directory: `backend/.deer-flow/`
|
||||
- Global memory: `backend/.deer-flow/memory.json`
|
||||
- Per-agent memory: `backend/.deer-flow/agents/{agent_name}/memory.json`
|
||||
- Base directory: `backend/.deer-flow/` (the `runtime_home()` state directory, overridable with `DEER_FLOW_HOME`)
|
||||
- Per-user memory: `backend/.deer-flow/users/{user_id}/memory.json`
|
||||
- Per-agent facts: `backend/.deer-flow/users/{user_id}/agents/{agent_name}/facts/`
|
||||
|
||||
You can change the storage path with the `storage_path` field. Relative paths are resolved against the base directory. Use an absolute path to store memory in a custom location.
|
||||
You can change that root with `memory.backend_config.storage_path`, and the value must be a **directory**: an absolute path is used as the root, a relative path resolves against the state directory, and an empty value keeps the default. A file-style value (the pre-abstraction `storage_path: memory.json`) is dropped with a warning by `deerflow/config/memory_config.py`, and a value pointing at an existing file makes `DeerMemConfig` raise at construction.
|
||||
|
||||
## Custom storage backend
|
||||
|
||||
|
||||
@ -33,7 +33,7 @@ import { Callout } from "nextra/components";
|
||||
1. **两种模式都会注入**:每次对话开始时,Agent 当前记忆以受控的 token 预算(`max_injection_tokens`)注入到系统提示中。
|
||||
2. **中间件模式(默认)**:`MemoryMiddleware` 在每次 Lead Agent 轮次结束后运行,过滤对话、排队后台更新并自动提取新事实。更新通过 `debounce_seconds` 防抖以批量处理快速变化。
|
||||
3. **工具模式(实验性)**:DeerFlow 注册 `memory_search`、`memory_get`、`memory_add`、`memory_update`、`memory_delete` 五个工具,并跳过 `MemoryMiddleware`。模型自行决定何时搜索、读取或写入记忆,因此效果取决于模型的工具调用行为。这些显式 CRUD 工具不是被动的陈旧性复核路径;启用工具模式意味着运营方选择由模型主动更新/删除记忆,而不是依赖中间件的陈旧性保护栏。
|
||||
4. **按 Agent 记忆**:当自定义 Agent 激活时,其记忆独立于全局记忆存储。这保持不同 Agent 的知识隔离。
|
||||
4. **按 Agent 划分事实**:默认的 DeerMem 后端将各自定义 Agent 学到的事实保存在各自的事实目录中。同一用户的各 Agent 共用一个记忆文件。
|
||||
|
||||
在工具模式中,五个 `memory_*` 工具名是保留名。如果 MCP 服务或自定义工具已经使用 `memory_search`、`memory_get`、`memory_add`、`memory_update` 或 `memory_delete`,已有工具会保留该名称,冲突的记忆工具会被跳过并记录告警。
|
||||
|
||||
@ -212,48 +212,61 @@ memory:
|
||||
```yaml
|
||||
memory:
|
||||
enabled: true
|
||||
injection_enabled: true
|
||||
|
||||
# 后端选择:deermem(默认)| noop | openviking | 自定义 MemoryManager 子类的点号路径
|
||||
manager_class: deermem
|
||||
|
||||
# 运行模式:
|
||||
# middleware - 默认;每轮结束后被动后台提取
|
||||
# tool - 实验性;模型直接调用 memory_* 工具
|
||||
mode: middleware
|
||||
|
||||
# 全局记忆文件的存储路径。
|
||||
# 默认:{base_dir}/memory.json(解析为 backend/.deer-flow/memory.json)
|
||||
# 绝对路径按原样使用,相对路径相对于 base_dir 解析。
|
||||
storage_path: memory.json
|
||||
# DeerMem 私有参数必须放在 backend_config 下(而不是顶层),因为它们只属于 DeerMem
|
||||
# ——换一个后端时,由该后端自行解释自己的 backend_config。写在顶层的旧字段会在加载时
|
||||
# 被自动迁移进 backend_config,并合并打一条 WARNING;而以 .json 结尾的旧式文件路径
|
||||
# storage_path 会被直接丢弃(详见 deerflow/config/memory_config.py)。
|
||||
backend_config:
|
||||
# 数据根目录,必须是目录。留空 = deer-flow 的 base_dir;按用户的记忆文件在
|
||||
# {根目录}/users/{user_id}/memory.json。绝对路径即该根目录。
|
||||
storage_path: ""
|
||||
|
||||
# 存储类(留空 = FileMemoryStorage,即默认的基于文件的 JSON 存储,不走 importlib;
|
||||
# 自定义实现填 `module.Class` 点号路径)
|
||||
storage_class: ""
|
||||
# 存储类(留空 = FileMemoryStorage,即默认的可移植文件存储,不走 importlib;
|
||||
# 自定义实现填 `module.Class` 点号路径)
|
||||
storage_class: ""
|
||||
|
||||
# 处理排队记忆更新前等待的秒数(防抖)
|
||||
debounce_seconds: 30
|
||||
# 处理排队记忆更新前等待的秒数(防抖)
|
||||
debounce_seconds: 30
|
||||
|
||||
# 记忆更新提取使用的模型(null = 使用默认模型)
|
||||
model_name: null
|
||||
# 记忆提取使用的模型。整段省略 = 宿主工厂注入应用默认模型(等同于旧写法
|
||||
# model_name: null);显式填写可换用更便宜或不同的模型。
|
||||
model:
|
||||
# provider: openai
|
||||
# model: gpt-4o-mini
|
||||
# api_key: $OPENAI_API_KEY
|
||||
# base_url: # 可选,用于 OpenAI 兼容网关
|
||||
# temperature: # 可选
|
||||
|
||||
# 要存储的最大事实数
|
||||
max_facts: 100
|
||||
# 要存储的最大事实数
|
||||
max_facts: 100
|
||||
|
||||
# 存储事实所需的最低置信度分数(0.0–1.0)
|
||||
fact_confidence_threshold: 0.7
|
||||
# 存储事实所需的最低置信度分数(0.0–1.0)
|
||||
fact_confidence_threshold: 0.7
|
||||
|
||||
# 是否将记忆注入到系统提示中
|
||||
injection_enabled: true
|
||||
|
||||
# 注入到系统提示的最大 token 数
|
||||
max_injection_tokens: 2000
|
||||
# 注入到系统提示的最大 token 数
|
||||
max_injection_tokens: 2000
|
||||
```
|
||||
|
||||
## 全局记忆与按 Agent 记忆
|
||||
|
||||
DeerFlow 支持两级记忆:
|
||||
DeerFlow 按用户划分记忆文件,按 Agent 划分事实目录:
|
||||
|
||||
- **全局记忆**:存储在 `{base_dir}/memory.json`。在没有特定 Agent 激活或 Agent 没有按 Agent 记忆文件时使用。
|
||||
- **按 Agent 记忆**:存储在 `{base_dir}/agents/{agent_name}/memory.json`。当自定义 Agent 激活时使用,使该 Agent 学到的知识保持独立。
|
||||
- **按用户记忆**:`{base_dir}/users/{user_id}/memory.json`,存放该用户跨任务的摘要。`agent_name` 不会切换到另一个记忆文件。
|
||||
- **按 Agent 事实**:`{base_dir}/users/{user_id}/agents/{agent_name}/facts/`,与同一用户的记忆文件并列存放,使各 Agent 学到的事实相互独立。`{agent_name}` 这一段会被转为小写,且仅允许 `A-Z`、`a-z`、`0-9` 和 `-`。
|
||||
|
||||
`MemoryMiddleware` 根据请求配置中的活跃 `agent_name` 自动选择正确的记忆文件。
|
||||
不带 `user_id` 的调用方会退回未分用户的遗留根 `{base_dir}/memory.json`(见 `deermem/core/paths.py` 的 `Legacy: no user_id` 分支)。
|
||||
|
||||
`MemoryMiddleware` 根据请求配置中的活跃 `agent_name` 选择读取哪个 Agent 的 facts 目录。
|
||||
|
||||
用于记忆存储的 Agent 名称通过 `AGENT_NAME_PATTERN` 验证,以确保文件系统安全。
|
||||
|
||||
@ -261,11 +274,11 @@ DeerFlow 支持两级记忆:
|
||||
|
||||
默认情况下,记忆文件存储在后端基础目录下:
|
||||
|
||||
- 基础目录:`backend/.deer-flow/`
|
||||
- 全局记忆:`backend/.deer-flow/memory.json`
|
||||
- 按 Agent 记忆:`backend/.deer-flow/agents/{agent_name}/memory.json`
|
||||
- 基础目录:`backend/.deer-flow/`(即 `runtime_home()`,可用 `DEER_FLOW_HOME` 覆盖)
|
||||
- 按用户记忆:`backend/.deer-flow/users/{user_id}/memory.json`
|
||||
- 按 Agent 事实:`backend/.deer-flow/users/{user_id}/agents/{agent_name}/facts/`
|
||||
|
||||
你可以用 `storage_path` 字段更改存储路径。相对路径相对于基础目录解析;使用绝对路径可以将记忆存储在自定义位置。
|
||||
你可以用 `memory.backend_config.storage_path` 更改这个根目录,且它的值必须是**目录**:绝对路径即该根目录,相对路径相对于宿主状态目录解析,留空则保持默认。以文件形态给出的旧写法(`storage_path: memory.json`)会被 `deerflow/config/memory_config.py` 打一条 WARNING 后丢弃;指向已存在文件的值会让 `DeerMemConfig` 在构造时直接抛错。
|
||||
|
||||
## 自定义存储后端
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user