Ver Fonte

docs: 补充项目说明和核心流程注释

leon há 1 mês atrás
pai
commit
f62db54db0

+ 158 - 0
MemoryAgents/backend/README.md

@@ -0,0 +1,158 @@
+# Memory Agents API
+
+这是一个用统一 HTTP 接口对比 Agent 记忆方案的后端项目。当前接入了 Text2Mem、Mem0、Letta、ReMe 和 memU。五个系统都通过 `MemoryAgent` 暴露相同的对话、记忆查询、删除、审计和重置能力,前端不需要关心各 SDK 的接口差异。
+
+建议第一次阅读时先运行内置的 Text2Mem。它不依赖第三方记忆 SDK,未配置模型和 PostgreSQL 时也能通过本地规则及 JSON 文件工作。
+
+## 环境要求
+
+- Python 3.11+
+- PostgreSQL(可选,生产存储;需要安装 pgvector 扩展)
+- OpenAI 兼容的 Chat Completions 和 Embeddings 服务(可选)
+
+## 快速启动
+
+在当前目录执行:
+
+```bash
+python3.11 -m venv .venv
+source .venv/bin/activate
+pip install -e .
+pip install pytest pytest-asyncio ruff
+uvicorn app.main:app --reload --port 8000
+```
+
+打开 `http://127.0.0.1:8000/docs` 可以直接调试接口。先检查服务状态:
+
+```bash
+curl http://127.0.0.1:8000/api/health
+curl http://127.0.0.1:8000/api/systems
+```
+
+默认情况下,PostgreSQL 连接失败会自动退回 `local-json`,Text2Mem 仍可使用。可以用下面的请求验证完整流程:
+
+```bash
+curl -X POST http://127.0.0.1:8000/api/chat \
+  -H 'Content-Type: application/json' \
+  -d '{"system":"text2mem","message":"记住:项目数据库使用 PostgreSQL"}'
+
+curl -X POST http://127.0.0.1:8000/api/chat \
+  -H 'Content-Type: application/json' \
+  -d '{"system":"text2mem","message":"项目使用什么数据库?"}'
+```
+
+## 配置
+
+项目通过环境变量配置,常用变量如下:
+
+| 变量 | 默认值 | 说明 |
+| --- | --- | --- |
+| `WORKSPACE_ID` | `local-workspace` | 数据隔离使用的工作区标识 |
+| `DATA_DIR` | `MemoryAgents/data` | 本地 JSON、ReMe、memU 文件目录 |
+| `DATABASE_URL` | `postgresql://memory:memory@localhost:54329/memory_agents` | PostgreSQL 连接串 |
+| `LLM_BASE_URL` | `https://api.openai.com/v1` | 对话模型接口地址 |
+| `LLM_API_KEY` | 空 | 对话模型密钥 |
+| `LLM_MODEL` | `gpt-4o-mini` | 对话模型名 |
+| `EMBEDDING_BASE_URL` | `https://api.openai.com/v1` | 向量接口地址 |
+| `EMBEDDING_API_KEY` | 空 | 向量接口密钥 |
+| `EMBEDDING_MODEL` | `text-embedding-3-small` | 向量模型名 |
+| `EMBEDDING_DIMENSIONS` | `1536` | 向量维度,必须和模型、数据库表一致 |
+| `EMBEDDING_MIN_SCORE` | `0.35` | 语义召回最低相似度 |
+| `REME_ENABLED` 等 | `false` | 是否启用对应第三方适配器 |
+
+开发时可以先在 shell 中 `export`,也可以按 `app/config.py` 的加载路径放置 `.env`。建议显式设置 `DATA_DIR`,避免从不同目录启动时混淆数据位置。
+
+启用第三方实现前,需要安装对应依赖:
+
+```bash
+pip install -e '.[reme]'
+pip install -e '.[mem0]'
+pip install -e '.[letta]'
+pip install -e '.[memu]'
+```
+
+Mem0、Letta 和 memU 目前仍是适配骨架,代码中的 `setup_hint` 列出了尚未闭环的能力,默认不要开启。ReMe 已实现文件记忆、词面检索和 pgvector 语义召回。
+
+## 目录结构
+
+```text
+backend/
+├── app/
+│   ├── main.py             # FastAPI 入口和路由
+│   ├── config.py           # 环境变量与 SDK 兼容配置
+│   ├── schemas.py          # 请求、响应、记忆和审计模型
+│   ├── registry.py         # 记忆适配器注册表
+│   ├── db.py               # PostgreSQL / 本地 JSON 统一存储
+│   ├── llm.py              # OpenAI 兼容对话客户端
+│   ├── embeddings.py       # OpenAI 兼容向量客户端
+│   └── adapters/
+│       ├── base.py         # 所有适配器的公共契约
+│       ├── text2mem.py     # 内置教学实现
+│       ├── reme.py         # ReMe 文件记忆实现
+│       ├── mem0.py         # Mem0 适配器
+│       ├── letta.py        # Letta 适配器
+│       └── memu.py         # memU 适配器
+└── tests/                  # 核心写入、检索和重置测试
+```
+
+## 核心调用链
+
+一次 `/api/chat` 请求的处理过程:
+
+1. `app/main.py:chat()` 根据请求中的 `system` 从注册表取出适配器。
+2. 适配器的 `chat()` 决定是否写入记忆,并执行该系统自己的检索流程。
+3. 公共数据通过 `MemoryRepository` 写入 PostgreSQL;数据库不可用时写入本地 JSON。
+4. 需要语义检索时,`embedding_client` 生成查询向量,存储层计算相似度。
+5. 召回内容交给 `llm` 生成回答;Text2Mem 在模型不可用时返回本地证据。
+6. 写入、检索、删除等动作通过 `_audit()` 留下审计记录。
+
+### Text2Mem
+
+Text2Mem 最适合用来理解项目。`_should_encode()` 先判断输入是陈述还是问题;陈述由 `_memory_type()` 分为语义、情景、流程和任务状态四类。`_encode()` 负责去重和持久化,`_retrieve()` 将词面重合、记忆类型加分和向量相似度合并排序。
+
+这里的规则是教学实现,不是通用中文意图识别器。调整正则时应同时补充 `tests/test_text2mem.py`,避免普通问题被误写入长期记忆。
+
+### ReMe
+
+ReMe 把 Markdown 文件作为真实记忆源。每轮对话按需执行自动记忆和重建索引,再通过规则扩展、可选模型改写、ReMe 文件检索和 pgvector 检索合并证据。后台定时任务被显式关闭,目的是让文件变化和模型费用都由请求触发。
+
+删除 ReMe 记忆时会删除文件、清理公共向量并重建索引;全量重置还会移除 ReMe 工作目录。
+
+### 存储层
+
+`MemoryRepository` 维护三类数据:`memory_items`、`audit_events` 和 `memory_embeddings`。PostgreSQL 使用 HNSW 索引做向量近邻搜索;本地模式使用 JSON 保存数据,并在进程内计算余弦相似度。
+
+本地 JSON 主要用于快速体验和测试,不适合并发写入。部署环境应使用 PostgreSQL,并确保 pgvector 列维度与 `EMBEDDING_DIMENSIONS` 一致。
+
+## API 一览
+
+| 方法 | 路径 | 用途 |
+| --- | --- | --- |
+| `GET` | `/api/health` | 检查存储、模型和向量配置 |
+| `GET` | `/api/systems` | 查看五个记忆系统的可用状态 |
+| `POST` | `/api/chat` | 指定记忆系统进行对话 |
+| `GET` | `/api/memories` | 查询某个系统的记忆 |
+| `DELETE` | `/api/memories/{memory_id}` | 删除指定记忆 |
+| `GET` | `/api/audit` | 查询操作审计记录 |
+| `POST` | `/api/reset` | 重置单个系统或全部系统 |
+
+`POST /api/reset` 不传 `system` 会逐个调用所有适配器。这个行为不能简单替换为清空公共表,因为 ReMe 等适配器还有自己的文件或索引。
+
+## 测试和检查
+
+```bash
+pytest
+ruff check app tests
+```
+
+测试默认使用临时本地存储,不要求 PostgreSQL、模型或第三方记忆 SDK。新增适配器时,至少需要覆盖系统状态、不可用提示、写入/检索、删除和重置行为。
+
+## 新增一个记忆适配器
+
+1. 在 `app/adapters/` 下新增类并继承 `MemoryAgent`。
+2. 设置唯一的 `id` 和 `SystemDescriptor`,实现 `chat()`。
+3. 如果使用公共存储,复用 `self.repo`;外部存储需要重写 `memories()`、`delete_memory()` 和 `reset()`。
+4. 在 `SystemName` 中加入系统 ID,并在 `build_registry()` 注册实例。
+5. 为启用条件、失败降级和数据清理补充测试。
+
+适配器返回的 `ChatResponse` 中,`memory_context` 是本轮使用或可展示的记忆,`memory_events` 描述本轮写入和检索动作,`audit_events` 用于排查实际执行路径。三者含义不要混用。

+ 3 - 0
MemoryAgents/backend/app/adapters/base.py

@@ -14,6 +14,7 @@ class AdapterUnavailable(RuntimeError):
 
 
 class MemoryAgent(ABC):
+    """所有记忆系统必须遵守的统一接口。"""
     id: str
     descriptor: SystemDescriptor
 
@@ -35,6 +36,7 @@ class MemoryAgent(ABC):
         await self.repo.reset(self.id)
 
     async def delete_memory(self, memory_id: str) -> bool:
+        """删除记忆时同步清理向量和记录审计。"""
         deleted = await self.repo.delete_memory(self.id, memory_id)
         if deleted:
             await self.repo.delete_embedding(self.id, memory_id)
@@ -48,6 +50,7 @@ class MemoryAgent(ABC):
         status: str = "ok",
         details: dict[str, Any] | None = None,
     ) -> AuditEvent:
+        """写入一条适配器操作记录。"""
         event = AuditEvent(
             id=self.repo.memory_id("audit"),
             system=self.id,

+ 35 - 19
MemoryAgents/backend/app/adapters/reme.py

@@ -12,20 +12,18 @@ from .base import AdapterUnavailable, MemoryAgent
 
 
 class ReMeAgent(MemoryAgent):
+    """用 Markdown 文件保存记忆,并提供文件与向量搜索。"""
+
     id = "reme"
     _PASSIVE_JOB_OVERRIDES = {
-        # ReMe starts these as background/cron jobs by default. This local lab
-        # keeps memory work request-driven to avoid silent file changes or LLM
-        # cost; chat() performs an explicit reindex before every retrieval.
+        # 关闭后台任务,改为每次对话显式更新索引,避免隐式改文件和模型调用。
         "index_update_loop": {"backend": "base", "enable_serve": False},
         "resource_watch_loop": {"backend": "base", "enable_serve": False},
         "digest_watch_loop": {"backend": "base", "enable_serve": False},
         "dream_cron": {"backend": "base", "enable_serve": False},
     }
 
-    # These lightweight expansions strengthen ReMe's BM25 branch. The project
-    # intentionally leaves ReMe's internal embedding_store disabled and uses
-    # pgvector as the separate Embedding retrieval signal when configured.
+    # 扩展常见工程词,补强 ReMe 的 BM25 召回;语义召回单独使用 pgvector。
     _QUERY_EXPANSIONS = {
         "部署": ("deploy", "发布", "上线", "测试环境", "索引构建", "批次"),
         "上线": ("deploy", "发布", "测试环境", "索引构建"),
@@ -77,6 +75,7 @@ class ReMeAgent(MemoryAgent):
     )
 
     def __init__(self) -> None:
+        """加载 ReMe 依赖并记录当前是否可用。"""
         super().__init__()
         self._service: Any | None = None
         try:
@@ -102,11 +101,12 @@ class ReMeAgent(MemoryAgent):
         )
 
     async def memories(self, query: str | None = None) -> list[MemoryItem]:
-        """Expose ReMe's Markdown files in the common inspector contract."""
+        """把 ReMe 的 Markdown 文件转换为统一记忆模型。"""
         root = settings.data_dir / "reme"
         if not root.exists():
             return []
         items: list[MemoryItem] = []
+        # 对外接口只认识 MemoryItem,因此需要把文件逐个转换。
         for path in sorted(root.glob("daily/**/*.md")):
             content = path.read_text(encoding="utf-8", errors="replace").strip()
             if not content:
@@ -130,6 +130,7 @@ class ReMeAgent(MemoryAgent):
         return items
 
     async def delete_memory(self, memory_id: str) -> bool:
+        """按记忆 ID 删除文件,并同步清理索引和操作记录。"""
         root = settings.data_dir / "reme"
         deleted = False
         for path in root.glob("daily/**/*.md"):
@@ -139,6 +140,7 @@ class ReMeAgent(MemoryAgent):
             path.unlink(missing_ok=True)
             deleted = True
             break
+        # 文件删掉后,对应向量和文件索引也要一起更新。
         if deleted:
             await self.repo.delete_embedding(self.id, memory_id)
         if deleted and self._class and settings.reme_enabled:
@@ -149,6 +151,7 @@ class ReMeAgent(MemoryAgent):
         return deleted
 
     async def _ensure(self) -> Any:
+        """按需启动 ReMe 服务,同一进程内重复使用。"""
         if not self._class or not settings.reme_enabled:
             raise AdapterUnavailable(self.descriptor.setup_hint or "ReMe 不可用")
         if self._service is None:
@@ -167,6 +170,7 @@ class ReMeAgent(MemoryAgent):
 
     @classmethod
     def _should_memorize(cls, message: str) -> bool:
+        """判断当前输入是否值得写入长期文件。"""
         normalized = " ".join(message.strip().split())
         if not normalized:
             return False
@@ -178,20 +182,19 @@ class ReMeAgent(MemoryAgent):
 
     @classmethod
     def _heuristic_queries(cls, message: str) -> list[str]:
-        """Build short queries for ReMe's file search."""
+        """为文件检索生成短查询和工程词扩展。"""
         normalized = message.strip()
         queries: list[str] = [normalized] if normalized else []
 
-        # Preserve explicit Latin/domain tokens such as RAG, FastAPI and
-        # PostgreSQL; ReMe's tokenizer can match these reliably.
+        # 保留 RAG、FastAPI、PostgreSQL 等可直接匹配的领域词。
         queries.extend(re.findall(r"[A-Za-z][A-Za-z0-9_.:/-]{1,}", normalized))
 
         for trigger, expansions in cls._QUERY_EXPANSIONS.items():
+            # 用户用词较少时,补上文件里可能出现的近义说法。
             if trigger in normalized:
                 queries.extend((trigger, *expansions))
 
-        # Add short Chinese chunks as a fallback, while excluding question
-        # words that add noise to a lexical search.
+        # 中文短词作为兜底,过滤会干扰词面检索的疑问词。
         for chunk in re.findall(r"[\u4e00-\u9fff]{2,}", normalized):
             if chunk not in cls._QUERY_STOPWORDS:
                 queries.append(chunk)
@@ -206,6 +209,7 @@ class ReMeAgent(MemoryAgent):
 
     @staticmethod
     def _dedupe_queries(queries: list[str]) -> list[str]:
+        """清理重复或空查询,并限制单次搜索数量。"""
         unique: list[str] = []
         seen: set[str] = set()
         for query in queries:
@@ -220,7 +224,7 @@ class ReMeAgent(MemoryAgent):
         return unique[:12]
 
     async def _rewrite_query(self, message: str) -> list[str]:
-        """Combine deterministic expansions with optional LLM query rewrite."""
+        """合并规则扩展和可选的模型查询改写。"""
         queries = self._heuristic_queries(message)
         try:
             rewritten = await self.llm.json(
@@ -236,14 +240,13 @@ class ReMeAgent(MemoryAgent):
             if isinstance(generated, list):
                 queries.extend(str(item) for item in generated if str(item).strip())
         except Exception:
-            # ReMe remains usable when the optional rewrite call fails. The
-            # deterministic expansions above cover the common engineering terms.
+            # 改写失败时继续使用规则查询,不影响基础检索。
             pass
         return self._dedupe_queries(queries)
 
     @staticmethod
     def _merge_search_results(results: list[str]) -> str:
-        """Merge duplicate snippets returned by multiple ReMe search queries."""
+        """合并多个查询结果并按正文去重。"""
         merged: list[str] = []
         seen: set[str] = set()
         for result in results:
@@ -263,6 +266,7 @@ class ReMeAgent(MemoryAgent):
         return "\n\n".join(merged)
 
     async def _search_memory(self, service: Any, queries: list[str]) -> tuple[str, list[str]]:
+        """逐个查询 ReMe 文件,并返回有结果的查询词。"""
         results: list[str] = []
         used_queries: list[str] = []
         for query in queries:
@@ -274,17 +278,19 @@ class ReMeAgent(MemoryAgent):
         return self._merge_search_results(results), used_queries
 
     async def _sync_embeddings(self) -> dict[str, Any]:
-        """Embed changed ReMe files and remove vectors for deleted files."""
+        """更新已变化文件的向量,并清理已删除文件。"""
         if not embedding_client.configured:
             return {"enabled": False, "embedded": 0, "removed": 0}
 
         items = await self.memories()
         current_ids = {item.id for item in items}
         previous_hashes = await self.repo.embedding_hashes(self.id)
+        # 文件不存在后,旧向量也要删除。
         stale_ids = set(previous_hashes) - current_ids
         for memory_id in stale_ids:
             await self.repo.delete_embedding(self.id, memory_id)
 
+        # 只处理新增或内容有变化的文件。
         pending = [
             item
             for item in items
@@ -306,12 +312,14 @@ class ReMeAgent(MemoryAgent):
         return {"enabled": True, "embedded": len(pending), "removed": len(stale_ids)}
 
     async def _semantic_search(self, message: str) -> tuple[str, int]:
+        """查找内容相近的文件,并过滤分数过低的结果。"""
         if not embedding_client.configured:
             return "", 0
         try:
             query_vector = (await embedding_client.embed([message]))[0]
             matches = await self.repo.search_embeddings(self.id, query_vector, limit=5)
         except Exception:
+            # 这一路失败时,主流程仍可使用 ReMe 文件搜索。
             return "", 0
         matches = [
             match
@@ -328,6 +336,7 @@ class ReMeAgent(MemoryAgent):
         return self._merge_search_results(blocks), len(matches)
 
     async def _answer_from_memory(self, message: str, evidence: str) -> str:
+        """根据搜索结果生成回答,模型不可用时直接返回原文。"""
         if not evidence:
             return "没有检索到与这个问题相关的历史记忆。"
         try:
@@ -340,12 +349,15 @@ class ReMeAgent(MemoryAgent):
                 f"用户问题:{message}\n\n记忆证据:\n{evidence}",
             )
         except Exception:
+            # 保留原始内容比吞掉已经找到的结果更方便排查。
             return f"根据检索到的记忆:\n\n{evidence}"
 
     async def chat(self, message: str) -> ChatResponse:
+        """完成文件写入、索引更新、搜索、回答和操作记录。"""
         service = await self._ensure()
         write_candidate = self._should_memorize(message)
         memory_result: Any | None = None
+        # 问题和临时请求不写文件,只处理值得长期保存的陈述。
         if write_candidate:
             memory_result = await service.run_job(
                 "auto_memory",
@@ -356,18 +368,20 @@ class ReMeAgent(MemoryAgent):
                     "问题、寒暄和临时请求不应写入长期记忆。"
                 ),
             )
+        # 写入后立即更新索引,保证本轮搜索能看到刚保存的内容。
         reindex_result = await service.run_job("reindex")
         try:
             embedding_sync = await self._sync_embeddings()
         except Exception as exc:
-            # A temporary Embedding outage should not take down file-memory
-            # retrieval; ReMe's built-in file search remains available.
+            # 向量服务异常时仍保留 ReMe 自带的文件检索。
             embedding_sync = {"enabled": True, "embedded": 0, "removed": 0, "error": str(exc)}
         queries = await self._rewrite_query(message)
+        # 文件搜索和相近内容搜索各跑一次,最后合并并去重。
         lexical_search, used_queries = await self._search_memory(service, queries)
         semantic_search, semantic_count = await self._semantic_search(message)
         search = self._merge_search_results([lexical_search, semantic_search])
         answer = await self._answer_from_memory(message, search)
+        # 记录查询词和搜索摘要,出现误匹配时可回看整个过程。
         await self._audit(
             "REME/auto_memory+search",
             details={
@@ -400,7 +414,9 @@ class ReMeAgent(MemoryAgent):
         )
 
     async def reset(self) -> None:
+        """清空公共数据,关闭服务并删除 ReMe 工作目录。"""
         await super().reset()
+        # 先关闭服务,避免删除目录后仍有后台句柄占用文件。
         if self._service is not None:
             await self._service.close()
             self._service = None

+ 23 - 0
MemoryAgents/backend/app/adapters/text2mem.py

@@ -12,6 +12,7 @@ from .base import MemoryAgent
 
 
 class Text2MemAgent(MemoryAgent):
+    """内置记忆实现,负责显式写入、混合检索和审计。"""
     id = "text2mem"
 
     _EXPLICIT_WRITE_RE = re.compile(r"记住|记下|请记录|请保存")
@@ -48,6 +49,7 @@ class Text2MemAgent(MemoryAgent):
     )
 
     def __init__(self) -> None:
+        """初始化内置实现及其系统说明。"""
         super().__init__()
         self.descriptor = SystemDescriptor(
             id="text2mem",
@@ -65,6 +67,7 @@ class Text2MemAgent(MemoryAgent):
 
     @classmethod
     def _should_encode(cls, message: str) -> bool:
+        """区分需要写入的陈述和只需要检索的问题。"""
         normalized = " ".join(message.strip().split())
         if not normalized:
             return False
@@ -76,6 +79,7 @@ class Text2MemAgent(MemoryAgent):
 
     @staticmethod
     def _memory_type(content: str) -> str:
+        """用规则把内容归入四类教学记忆。"""
         if re.search(r"上次|之前|曾经|遇到|发生|故障|失败", content):
             return "episodic"
         if re.search(r"流程|步骤|先.+再|以后.+按", content):
@@ -86,6 +90,7 @@ class Text2MemAgent(MemoryAgent):
 
     @classmethod
     def _query_type(cls, message: str) -> str | None:
+        """根据问题内容判断优先查找哪类记忆。"""
         if re.search(r"流程|步骤|怎么修复|如何修复|操作方法", message):
             return "procedural"
         if re.search(r"上次|之前|历史|曾经|遇到|发生|故障", message):
@@ -98,6 +103,7 @@ class Text2MemAgent(MemoryAgent):
 
     @classmethod
     def _tokens(cls, text: str) -> set[str]:
+        """提取英文单词和中文短词,供关键词匹配使用。"""
         tokens = {item.casefold() for item in re.findall(r"[A-Za-z][A-Za-z0-9_.+-]{1,}", text)}
         for chunk in re.findall(r"[\u4e00-\u9fff]{2,}", text):
             if chunk not in cls._STOPWORDS:
@@ -111,7 +117,9 @@ class Text2MemAgent(MemoryAgent):
         return tokens
 
     async def _encode(self, content: str) -> tuple[MemoryItem, bool]:
+        """去重后写入记忆,返回记忆及是否新建。"""
         normalized = " ".join(content.strip().split()).casefold()
+        # 相同内容只保留一份,重复输入只记审计记录。
         for existing in await self.repo.list_memories(self.id):
             if " ".join(existing.content.strip().split()).casefold() == normalized:
                 await self._audit(
@@ -123,6 +131,7 @@ class Text2MemAgent(MemoryAgent):
                 return existing, False
 
         now = utcnow()
+        # 新记忆在这里补齐分类、来源和时间信息。
         item = MemoryItem(
             id=self.repo.memory_id("t2m"),
             system=self.id,
@@ -148,15 +157,18 @@ class Text2MemAgent(MemoryAgent):
         return item, True
 
     async def _sync_embeddings(self, items: list[MemoryItem]) -> dict[str, Any]:
+        """只更新内容指纹发生变化的向量。"""
         if not embedding_client.configured:
             return {"enabled": False, "embedded": 0, "removed": 0}
 
         current_ids = {item.id for item in items}
         previous_hashes = await self.repo.embedding_hashes(self.id)
+        # 记忆已经删除时,旧向量不能继续留在搜索结果里。
         stale_ids = set(previous_hashes) - current_ids
         for memory_id in stale_ids:
             await self.repo.delete_embedding(self.id, memory_id)
 
+        # 指纹没变的内容无需重复请求向量服务。
         pending = [
             item
             for item in items
@@ -176,6 +188,7 @@ class Text2MemAgent(MemoryAgent):
         return {"enabled": True, "embedded": len(pending), "removed": len(stale_ids)}
 
     async def _retrieve(self, message: str, limit: int = 5) -> tuple[list[MemoryItem], dict[str, Any]]:
+        """融合词面、记忆类型和向量分数进行召回。"""
         items = await self.repo.list_memories(self.id)
         if not items:
             return [], {"strategy": "empty", "embedding": {"enabled": False}}
@@ -184,6 +197,7 @@ class Text2MemAgent(MemoryAgent):
         expected_type = self._query_type(message)
         token_overlap: dict[str, float] = {}
         lexical_scores: dict[str, float] = {}
+        # 先看关键词是否对得上,同一类记忆再加一点分。
         for item in items:
             item_tokens = self._tokens(item.content)
             overlap = len(query_tokens & item_tokens) / max(1, len(query_tokens))
@@ -193,6 +207,7 @@ class Text2MemAgent(MemoryAgent):
 
         semantic_scores: dict[str, float] = {}
         try:
+            # 配置了向量服务时,再补一轮相近内容搜索。
             embedding_status = await self._sync_embeddings(items)
             if embedding_status["enabled"]:
                 query_vector = (await embedding_client.embed([message]))[0]
@@ -206,12 +221,14 @@ class Text2MemAgent(MemoryAgent):
                     for match in semantic_matches
                 }
         except Exception as exc:
+            # 向量搜索失败后继续使用关键词结果。
             embedding_status = {"enabled": True, "error": str(exc), "embedded": 0, "removed": 0}
 
         ranked: list[tuple[float, MemoryItem]] = []
         for item in items:
             lexical = lexical_scores[item.id]
             semantic = semantic_scores.get(item.id, 0.0)
+            # 类型、关键词和内容都对不上时,不把这条记忆交给模型。
             if (
                 expected_type
                 and item.memory_type != expected_type
@@ -224,6 +241,7 @@ class Text2MemAgent(MemoryAgent):
             ranked.append((lexical + max(semantic, 0.0), item))
 
         if not ranked and expected_type:
+            # 没有直接命中时,至少返回问题所对应的同类记忆。
             ranked = [
                 (0.1, item)
                 for item in items
@@ -247,7 +265,9 @@ class Text2MemAgent(MemoryAgent):
         }
 
     async def chat(self, message: str) -> ChatResponse:
+        """执行写入判断、记忆检索和回答生成。"""
         events: list[dict[str, Any]] = []
+        # 先处理可能的写入,再查询本轮需要的上下文。
         if self._should_encode(message):
             item, created = await self._encode(message)
             events.append(
@@ -274,6 +294,7 @@ class Text2MemAgent(MemoryAgent):
             answer = await self.llm.chat(system_prompt, f"记忆上下文:\n{context_text}\n\n用户:{message}")
             mode = "llm"
         except LLMUnavailable:
+            # 没配模型也能查看本轮写入和检索到的原始内容。
             mode = "local-fallback"
             answer = (
                 "Text2Mem 本地模式:我已按显式 IR 规则处理本轮输入。\n\n"
@@ -281,12 +302,14 @@ class Text2MemAgent(MemoryAgent):
                 "配置 LLM_API_KEY 后,可让模型基于这些记忆生成自然语言回答。"
             )
         except Exception as exc:
+            # 模型临时出错不影响已经完成的记忆写入和查询。
             mode = "local-fallback"
             retrieval["llm_error"] = str(exc)[:500]
             answer = (
                 "模型调用失败,Text2Mem 已退回本地证据模式;记忆写入和检索结果不受影响。\n\n"
                 f"当前命中记忆:{context_text}"
             )
+        # 保存实际用到的记忆,方便之后检查回答来源。
         await self._audit(
             "RET/Retrieve",
             details={

+ 3 - 3
MemoryAgents/backend/app/config.py

@@ -13,6 +13,7 @@ load_dotenv(ROOT_DIR / ".env")
 
 
 def _bool(name: str, default: bool = False) -> bool:
+    """读取常见布尔环境变量写法。"""
     value = os.getenv(name)
     if value is None:
         return default
@@ -21,6 +22,7 @@ def _bool(name: str, default: bool = False) -> bool:
 
 @dataclass(frozen=True)
 class Settings:
+    """集中管理服务、模型、存储和可选适配器配置。"""
     app_env: str = os.getenv("APP_ENV", "development")
     workspace_id: str = os.getenv("WORKSPACE_ID", "local-workspace")
     data_dir: Path = Path(os.getenv("DATA_DIR", str(ROOT_DIR / "data")))
@@ -51,9 +53,7 @@ if not 0.0 <= settings.embedding_min_score <= 1.0:
     raise ValueError("EMBEDDING_MIN_SCORE 必须在 0 到 1 之间")
 settings.data_dir.mkdir(parents=True, exist_ok=True)
 
-# Some optional SDKs, especially Mem0's default local client, look for the
-# conventional OpenAI environment names. Keep the project-facing configuration
-# unified while making those SDKs inherit the same OpenAI-compatible endpoint.
+# 可选 SDK 通常只识别 OpenAI 标准变量,这里把项目配置同步给它们。
 if settings.llm_api_key:
     os.environ.setdefault("OPENAI_API_KEY", settings.llm_api_key)
     os.environ.setdefault("DEEPSEEK_API_KEY", settings.llm_api_key)

+ 6 - 6
MemoryAgents/backend/app/db.py

@@ -16,12 +16,7 @@ def utcnow() -> datetime:
 
 
 class MemoryRepository:
-    """PostgreSQL-first repository with a transparent local JSON fallback.
-
-    The fallback is intentionally exposed in the API status as `local-json`.
-    It is for bootstrapping the Web UI only; Docker PostgreSQL is the intended
-    persistence layer for the project.
-    """
+    """统一存储层:优先使用 PostgreSQL,连接失败时退回本地 JSON。"""
 
     def __init__(self) -> None:
         self.pool: Any | None = None
@@ -33,6 +28,7 @@ class MemoryRepository:
         self.embeddings: dict[tuple[str, str], dict[str, Any]] = {}
 
     async def initialize(self) -> None:
+        """创建数据库表和索引;初始化失败不阻塞服务启动。"""
         try:
             import asyncpg  # type: ignore
 
@@ -115,6 +111,7 @@ class MemoryRepository:
             self._load_local()
 
     def _load_local(self) -> None:
+        """从本地 JSON 恢复记忆、审计和向量数据。"""
         if not self.path.exists():
             return
         try:
@@ -170,6 +167,7 @@ class MemoryRepository:
         return value or {}
 
     async def add_memory(self, item: MemoryItem) -> MemoryItem:
+        """新增或覆盖一条记忆。"""
         if self.pool:
             async with self.pool.acquire() as conn:
                 await conn.execute(
@@ -333,6 +331,7 @@ class MemoryRepository:
         query_embedding: list[float],
         limit: int = 5,
     ) -> list[dict[str, Any]]:
+        """按余弦相似度返回向量检索结果。"""
         if self.pool:
             async with self.pool.acquire() as conn:
                 rows = await conn.fetch(
@@ -426,6 +425,7 @@ class MemoryRepository:
         return sorted(events, key=lambda event: event.created_at, reverse=True)[:200]
 
     async def reset(self, system: str | None = None) -> None:
+        """清空指定系统或当前工作区的全部数据。"""
         if self.pool:
             async with self.pool.acquire() as conn:
                 if system:

+ 3 - 2
MemoryAgents/backend/app/embeddings.py

@@ -13,7 +13,7 @@ class EmbeddingUnavailable(RuntimeError):
 
 
 def embedding_fingerprint(content: str) -> str:
-    """Invalidate stored vectors when either the text or model config changes."""
+    """文本或模型配置变化时生成新的向量指纹。"""
     payload = (
         f"{settings.embedding_base_url}\0{settings.embedding_model}\0"
         f"{settings.embedding_dimensions}\0{content}"
@@ -22,7 +22,7 @@ def embedding_fingerprint(content: str) -> str:
 
 
 class OpenAICompatibleEmbeddingClient:
-    """Embedding client for DashScope and other OpenAI-compatible endpoints."""
+    """调用 OpenAI 兼容的 Embedding 接口。"""
 
     @property
     def configured(self) -> bool:
@@ -33,6 +33,7 @@ class OpenAICompatibleEmbeddingClient:
         )
 
     async def embed(self, inputs: list[str]) -> list[list[float]]:
+        """批量生成向量,并校验返回数量和维度。"""
         if not self.configured:
             raise EmbeddingUnavailable(
                 "未配置 EMBEDDING_API_KEY / EMBEDDING_BASE_URL / EMBEDDING_MODEL"

+ 2 - 1
MemoryAgents/backend/app/llm.py

@@ -13,6 +13,7 @@ class LLMUnavailable(RuntimeError):
 
 
 class OpenAICompatibleClient:
+    """项目使用的最小 OpenAI 兼容对话客户端。"""
     @property
     def configured(self) -> bool:
         return bool(settings.llm_api_key and settings.llm_base_url and settings.llm_model)
@@ -38,6 +39,7 @@ class OpenAICompatibleClient:
             return data["choices"][0]["message"]["content"]
 
     async def json(self, system: str, user: str) -> dict[str, Any]:
+        """调用模型并从回复中提取 JSON 对象。"""
         raw = await self.chat(system, user, temperature=0.1)
         cleaned = raw.strip()
         if "```" in cleaned:
@@ -49,4 +51,3 @@ class OpenAICompatibleClient:
 
 
 llm = OpenAICompatibleClient()
-

+ 3 - 3
MemoryAgents/backend/app/main.py

@@ -15,6 +15,7 @@ from .adapters.base import AdapterUnavailable
 
 @asynccontextmanager
 async def lifespan(_: FastAPI):
+    """启动时初始化存储连接,退出时释放连接池。"""
     await repository.initialize()
     yield
     if repository.pool:
@@ -58,6 +59,7 @@ async def systems():
 
 @app.post("/api/chat", response_model=ChatResponse)
 async def chat(request: ChatRequest):
+    """把对话请求交给指定记忆适配器处理。"""
     agent = agents[request.system]
     try:
         return await agent.chat(request.message)
@@ -98,9 +100,7 @@ async def audit(system: str | None = None):
 @app.post("/api/reset")
 async def reset(request: ResetRequest):
     if request.system is None:
-        # Some adapters own storage outside the common repository. Calling each
-        # adapter keeps framework files/indexes (notably ReMe) in sync with the
-        # PostgreSQL projection when the user requests a global reset.
+        # 全量重置必须逐个调用适配器,ReMe 等实现还维护自己的文件和索引。
         for agent in agents.values():
             await agent.reset()
     elif request.system in agents:

+ 1 - 1
MemoryAgents/backend/app/registry.py

@@ -7,9 +7,9 @@ from .adapters.text2mem import Text2MemAgent
 
 
 def build_registry() -> dict[str, MemoryAgent]:
+    """实例化所有适配器,并按系统 ID 建立索引。"""
     agents = [Text2MemAgent(), Mem0Agent(), LettaAgent(), ReMeAgent(), MemUAgent()]
     return {agent.id: agent for agent in agents}
 
 
 agents = build_registry()
-

+ 4 - 1
MemoryAgents/backend/app/schemas.py

@@ -10,6 +10,7 @@ SystemName = Literal["text2mem", "mem0", "letta", "reme", "memu"]
 
 
 class MemoryItem(BaseModel):
+    """不同记忆系统对外展示的统一数据结构。"""
     id: str
     system: str
     workspace_id: str
@@ -26,6 +27,7 @@ class MemoryItem(BaseModel):
 
 
 class AuditEvent(BaseModel):
+    """记录一次记忆操作及其执行结果。"""
     id: str
     system: str
     workspace_id: str
@@ -37,6 +39,7 @@ class AuditEvent(BaseModel):
 
 
 class SystemDescriptor(BaseModel):
+    """描述适配器能力、依赖和当前可用状态。"""
     id: SystemName
     name: str
     paradigm: str
@@ -54,6 +57,7 @@ class ChatRequest(BaseModel):
 
 
 class ChatResponse(BaseModel):
+    """统一对话响应,包含回答、召回记忆和操作轨迹。"""
     system: SystemName
     answer: str
     mode: str
@@ -64,4 +68,3 @@ class ChatResponse(BaseModel):
 
 class ResetRequest(BaseModel):
     system: SystemName | None = None
-