recallweaver 1.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- recallweaver-1.2.0/.env.example +0 -0
- recallweaver-1.2.0/.gitignore +3 -0
- recallweaver-1.2.0/PKG-INFO +205 -0
- recallweaver-1.2.0/README.md +187 -0
- recallweaver-1.2.0/assets/.gitkeep +0 -0
- recallweaver-1.2.0/assets/demo.gif +0 -0
- recallweaver-1.2.0/benchmarks/__init__.py +0 -0
- recallweaver-1.2.0/benchmarks/ablation.py +147 -0
- recallweaver-1.2.0/benchmarks/bench_quality.py +308 -0
- recallweaver-1.2.0/benchmarks/bench_real_world.py +707 -0
- recallweaver-1.2.0/benchmarks/bench_retrieval.py +142 -0
- recallweaver-1.2.0/benchmarks/data_quality.py +222 -0
- recallweaver-1.2.0/benchmarks/dataset_loaders.py +542 -0
- recallweaver-1.2.0/benchmarks/datasets/DATA_QUALITY_REPORT.md +22 -0
- recallweaver-1.2.0/benchmarks/datasets/daily_chatter.json +18 -0
- recallweaver-1.2.0/benchmarks/datasets/fact_conflicts_mock.json +30 -0
- recallweaver-1.2.0/benchmarks/datasets/longmemeval_mock.json +610 -0
- recallweaver-1.2.0/benchmarks/eval_metrics.py +76 -0
- recallweaver-1.2.0/benchmarks/gen_complex_data.py +362 -0
- recallweaver-1.2.0/benchmarks/gen_data.py +36 -0
- recallweaver-1.2.0/benchmarks/gen_eval_dataset.py +184 -0
- recallweaver-1.2.0/benchmarks/soak_test.py +1007 -0
- recallweaver-1.2.0/benchmarks/stress_deadlock.py +401 -0
- recallweaver-1.2.0/docs/adr/ADR-001-tech-stack.md +24 -0
- recallweaver-1.2.0/docs/adr/ADR-001.md +36 -0
- recallweaver-1.2.0/docs/adr/ADR-002-bitemporal.md +26 -0
- recallweaver-1.2.0/docs/adr/ADR-002.md +45 -0
- recallweaver-1.2.0/docs/adr/ADR-003-lazy-decay.md +23 -0
- recallweaver-1.2.0/docs/adr/ADR-003.md +47 -0
- recallweaver-1.2.0/docs/adr/ADR-004.md +0 -0
- recallweaver-1.2.0/docs/adr/ADR-005.md +0 -0
- recallweaver-1.2.0/docs/architecture.md +111 -0
- recallweaver-1.2.0/docs/data_model.md +144 -0
- recallweaver-1.2.0/docs/decay_algorithm.md +75 -0
- recallweaver-1.2.0/examples/context_reset_demo.py +281 -0
- recallweaver-1.2.0/examples/deepseek_agent.py +273 -0
- recallweaver-1.2.0/examples/minimal_demo.py +197 -0
- recallweaver-1.2.0/plugins/dsh_adapter.py +87 -0
- recallweaver-1.2.0/pyproject.toml +34 -0
- recallweaver-1.2.0/schema.sql +151 -0
- recallweaver-1.2.0/scripts/backup.py +126 -0
- recallweaver-1.2.0/scripts/export_json.py +0 -0
- recallweaver-1.2.0/scripts/healthcheck.py +117 -0
- recallweaver-1.2.0/scripts/start_nas.py +78 -0
- recallweaver-1.2.0/src/__init__.py +0 -0
- recallweaver-1.2.0/src/recallweaver/__init__.py +25 -0
- recallweaver-1.2.0/src/recallweaver/api.py +513 -0
- recallweaver-1.2.0/src/recallweaver/cli.py +220 -0
- recallweaver-1.2.0/src/recallweaver/config.py +25 -0
- recallweaver-1.2.0/src/recallweaver/db.py +240 -0
- recallweaver-1.2.0/src/recallweaver/decay_engine.py +98 -0
- recallweaver-1.2.0/src/recallweaver/errors.py +37 -0
- recallweaver-1.2.0/src/recallweaver/extraction/__init__.py +6 -0
- recallweaver-1.2.0/src/recallweaver/extraction/base.py +20 -0
- recallweaver-1.2.0/src/recallweaver/extraction/llm_extractor.py +201 -0
- recallweaver-1.2.0/src/recallweaver/graph.py +201 -0
- recallweaver-1.2.0/src/recallweaver/memory_manager.py +461 -0
- recallweaver-1.2.0/src/recallweaver/migrations/0001_init.sql +150 -0
- recallweaver-1.2.0/src/recallweaver/migrations/__init__.py +0 -0
- recallweaver-1.2.0/src/recallweaver/migrations/runner.py +76 -0
- recallweaver-1.2.0/src/recallweaver/models.py +110 -0
- recallweaver-1.2.0/src/recallweaver/observability/README.md +7 -0
- recallweaver-1.2.0/src/recallweaver/observability/__init__.py +19 -0
- recallweaver-1.2.0/src/recallweaver/observability/logging.py +235 -0
- recallweaver-1.2.0/src/recallweaver/observability/metrics.py +90 -0
- recallweaver-1.2.0/src/recallweaver/plugins/README.md +1 -0
- recallweaver-1.2.0/src/recallweaver/plugins/__init__.py +0 -0
- recallweaver-1.2.0/src/recallweaver/plugins/base.py +0 -0
- recallweaver-1.2.0/src/recallweaver/plugins/registry.py +0 -0
- recallweaver-1.2.0/src/recallweaver/retrieval.py +165 -0
- recallweaver-1.2.0/src/recallweaver/server.py +533 -0
- recallweaver-1.2.0/src/recallweaver.egg-info/PKG-INFO +11 -0
- recallweaver-1.2.0/src/recallweaver.egg-info/SOURCES.txt +30 -0
- recallweaver-1.2.0/src/recallweaver.egg-info/dependency_links.txt +1 -0
- recallweaver-1.2.0/src/recallweaver.egg-info/entry_points.txt +2 -0
- recallweaver-1.2.0/src/recallweaver.egg-info/requires.txt +7 -0
- recallweaver-1.2.0/src/recallweaver.egg-info/top_level.txt +1 -0
- recallweaver-1.2.0/tests/__init__.py +0 -0
- recallweaver-1.2.0/tests/conftest.py +47 -0
- recallweaver-1.2.0/tests/integration/run_test.py +29 -0
- recallweaver-1.2.0/tests/integration/test_api_cli.py +87 -0
- recallweaver-1.2.0/tests/integration/test_chaos_boundaries.py +250 -0
- recallweaver-1.2.0/tests/integration/test_context_reset_demo.py +87 -0
- recallweaver-1.2.0/tests/integration/test_db.py +81 -0
- recallweaver-1.2.0/tests/integration/test_deepseek_demo.py +175 -0
- recallweaver-1.2.0/tests/integration/test_dsh_adapter.py +61 -0
- recallweaver-1.2.0/tests/integration/test_explain_api.py +39 -0
- recallweaver-1.2.0/tests/integration/test_graph.py +53 -0
- recallweaver-1.2.0/tests/integration/test_memory.py +126 -0
- recallweaver-1.2.0/tests/integration/test_memory_conflict.py +240 -0
- recallweaver-1.2.0/tests/integration/test_noise_filtering.py +30 -0
- recallweaver-1.2.0/tests/integration/test_retrieval.py +92 -0
- recallweaver-1.2.0/tests/integration/test_server.py +116 -0
- recallweaver-1.2.0/tests/integration/test_ultimate_suite.py +464 -0
- recallweaver-1.2.0/tests/unit/test_decay.py +79 -0
- recallweaver-1.2.0/tests/unit/test_eval_metrics.py +62 -0
- recallweaver-1.2.0/tests/unit/test_llm_extractor.py +116 -0
- recallweaver-1.2.0/tests/unit/test_logging.py +146 -0
- recallweaver-1.2.0/tests/unit/test_metrics.py +69 -0
- recallweaver-1.2.0/tests/unit/test_observability.py +103 -0
|
File without changes
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: recallweaver
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: A lightweight local memory engine for AI agents
|
|
5
|
+
Requires-Python: ==3.11.9
|
|
6
|
+
Requires-Dist: click>=8.1
|
|
7
|
+
Requires-Dist: fastapi>=0.110
|
|
8
|
+
Requires-Dist: openai>=1.0
|
|
9
|
+
Requires-Dist: pydantic-settings>=2.0
|
|
10
|
+
Requires-Dist: pydantic>=2.0
|
|
11
|
+
Requires-Dist: rich>=13.7
|
|
12
|
+
Requires-Dist: uvicorn>=0.27
|
|
13
|
+
Provides-Extra: test
|
|
14
|
+
Requires-Dist: httpx>=0.27; extra == 'test'
|
|
15
|
+
Requires-Dist: pytest-mock>=3.14; extra == 'test'
|
|
16
|
+
Requires-Dist: pytest>=8.0; extra == 'test'
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# 🧠 织忆 (RecallWeaver)
|
|
20
|
+
|
|
21
|
+
**赋予 LLM Agent 穿越时间的记忆能力。零 Docker、零向量库、纯单文件 SQLite 构筑的工业级 AI 海马体。**
|
|
22
|
+
|
|
23
|
+
<p align="center">
|
|
24
|
+
<a href="https://www.python.org/"><img src="https://img.shields.io/badge/Python-3.11%2B-3776AB?style=for-the-badge&logo=python&logoColor=white" alt="Python 3.11+"></a>
|
|
25
|
+
<a href="#"><img src="https://img.shields.io/badge/Tests-Passing-brightgreen?style=for-the-badge&logo=pytest&logoColor=white" alt="Tests Passing"></a>
|
|
26
|
+
<a href="#"><img src="https://img.shields.io/badge/Latency-10ms-blueviolet?style=for-the-badge&logo=speedtest&logoColor=white" alt="Latency 10ms"></a>
|
|
27
|
+
<a href="#"><img src="https://img.shields.io/badge/Precision-1.0-success?style=for-the-badge" alt="Precision 1.0"></a>
|
|
28
|
+
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow?style=for-the-badge" alt="MIT License"></a>
|
|
29
|
+
</p>
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
33
|
+
> 看!大肥鱼在真实 DeepSeek 环境下,如何利用纯本地记忆识破主人的花生过敏陷阱,并完成北京到杭州的时空转移。
|
|
34
|
+
|
|
35
|
+
**无需 Docker,无需向量数据库,也不必把整段对话塞进上下文。** RecallWeaver 为 Agent 提供可演变、可检索、会遗忘的长期记忆。
|
|
36
|
+
|
|
37
|
+
[核心特性](#-核心高光特性) · [性能与质量](#-性能与质量验证) · [快速开始](#-快速开始) · [架构](#-项目架构)
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 💥 为什么选择 RecallWeaver?
|
|
42
|
+
|
|
43
|
+
传统 RAG 记忆方案经常把“检索到文本”误当成“理解了事实”。当对话变长、事实更新、闲聊增多时,常见问题会逐渐显现:
|
|
44
|
+
|
|
45
|
+
| 传统方案的痛点 | 可能造成的结果 |
|
|
46
|
+
|---|---|
|
|
47
|
+
| 🕰️ **时间逻辑混乱** | “以前住在北京”与“现在住在杭州”同时进入上下文,Agent 前后矛盾,像是“精神分裂”。 |
|
|
48
|
+
| 🔊 **高频噪音挤占上下文** | 股票涨跌、天气闲聊等高频内容挤掉过敏、住址等真正重要的事实。 |
|
|
49
|
+
| 🧱 **部署成本高** | 重型向量库、图数据库和配套服务带来额外的部署、运维与资源成本。 |
|
|
50
|
+
|
|
51
|
+
### RecallWeaver 的破局方式
|
|
52
|
+
|
|
53
|
+
以**单文件 SQLite** 为持久化核心,结合**双时间戳事实版本**与**动态遗忘评分**:事实更新时保留历史并关闭旧版本;检索时综合相关性、时效与权重,并通过分数阈值过滤低价值噪音。轻量部署,也能明确回答“现在是什么”以及“以前是什么”。
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 📊 性能与质量验证
|
|
58
|
+
|
|
59
|
+
以下为项目压测与评测记录中的结果:
|
|
60
|
+
|
|
61
|
+
### 🔥 并发与长稳压测
|
|
62
|
+
|
|
63
|
+
| 持续时间 | 并发线程 | 总请求数 | 成功率 | 死锁 | P99 延迟 |
|
|
64
|
+
|---:|---:|---:|---:|---:|---:|
|
|
65
|
+
| 1,800 秒 | 50 | 180,000 | **100%** | **0** | **10.5 ms** |
|
|
66
|
+
|
|
67
|
+
### 🎯 检索质量评估
|
|
68
|
+
|
|
69
|
+
基于 100 组高仿真测试数据,评估检索结果与核心事实标准答案的一致性:
|
|
70
|
+
|
|
71
|
+
| Precision | Recall | F1-Score | 评测样本 |
|
|
72
|
+
|---:|---:|---:|---:|
|
|
73
|
+
| **1.0000** | **1.0000** | **1.0000** | 100 组 |
|
|
74
|
+
|
|
75
|
+
### 🧪 混沌工程边界验证
|
|
76
|
+
|
|
77
|
+
| 场景 | 验证结果 |
|
|
78
|
+
|---|---|
|
|
79
|
+
| SQL 注入文本 | 参数化查询防护通过 |
|
|
80
|
+
| 高频恶意检索刷分 | 权重加成上限与噪音阈值拦截通过 |
|
|
81
|
+
| 空数据库调用 | 空结果兜底通过 |
|
|
82
|
+
|
|
83
|
+
> 这些数字是项目当前记录的基准结果。实际表现会随硬件、SQLite 版本、数据集和运行配置变化。
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## ✨ 核心高光特性
|
|
88
|
+
|
|
89
|
+
### 🕰️ 双时间戳时空图谱 (Temporal Graph)
|
|
90
|
+
|
|
91
|
+
事实带有 `valid_from` 与 `valid_to`。同一主谓关系发生冲突时,系统在事务中关闭旧事实并写入新事实,历史不被覆盖。基于 SQLite 递归 CTE 的图谱遍历支持多跳关系查询,让 Agent 能区分事实的当前状态与历史状态。
|
|
92
|
+
|
|
93
|
+
### 📉 动态遗忘打分池 (Decay Pool)
|
|
94
|
+
|
|
95
|
+
以指数衰减为基础,综合记忆权重与检索反馈计算候选得分。检索次数的加成受有用性权重和上限约束;最终结果还会经过分数阈值截断,避免无关闲聊仅凭频繁出现就长期霸屏。
|
|
96
|
+
|
|
97
|
+
### ⚡ 混合检索 (Hybrid Search)
|
|
98
|
+
|
|
99
|
+
结合 SQLite FTS5 全文检索与图谱召回,再由衰减引擎统一重排。FTS5 的 `trigram` 分词支持中文连续子串匹配;结构化事实则可通过图谱关系查找,互补关键词召回与事实关联。
|
|
100
|
+
|
|
101
|
+
### 🔎 可观测、可解释
|
|
102
|
+
|
|
103
|
+
通过结构化日志、Trace ID 与线程安全指标记录核心链路状态。召回结果包含来源、分数及有效时间信息,便于排查与理解 Agent 为什么记住或忽略某条信息。
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## 🚀 快速开始
|
|
108
|
+
# 启动 API 服务
|
|
109
|
+
uvicorn src.recallweaver.server:app --reload
|
|
110
|
+
|
|
111
|
+
### 1. 克隆并安装
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
git clone https://github.com/gaoweimax/recallweaver.git
|
|
115
|
+
cd recallweaver
|
|
116
|
+
python -m pip install -e .
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 2. 配置 DeepSeek 密钥
|
|
120
|
+
|
|
121
|
+
示例使用环境变量读取密钥。请替换为你自己的密钥,**不要把真实密钥写入代码或提交到仓库**。
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
export DEEPSEEK_API_KEY="sk-..."
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Windows PowerShell 可使用:
|
|
128
|
+
|
|
129
|
+
```powershell
|
|
130
|
+
$env:DEEPSEEK_API_KEY = "sk-..."
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 3. 写入事实并检索
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from recallweaver.api import RecallWeaverAPI
|
|
137
|
+
|
|
138
|
+
api = RecallWeaverAPI(db_path="recallweaver.db")
|
|
139
|
+
|
|
140
|
+
# 记录结构化事实
|
|
141
|
+
api.remember_triple(
|
|
142
|
+
subject="张三",
|
|
143
|
+
predicate="住在",
|
|
144
|
+
object_="杭州",
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# 检索与问题相关的记忆
|
|
148
|
+
results = api.recall("张三住在哪里?", limit=3)
|
|
149
|
+
for memory in results:
|
|
150
|
+
print(f"[{memory.score:.3f}] {memory.content or memory.object}")
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
运行带真实 DeepSeek API 的 Rich 终端示例:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
python examples/deepseek_agent.py
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## 🏗️ 项目架构
|
|
162
|
+
|
|
163
|
+
```text
|
|
164
|
+
src/recallweaver/
|
|
165
|
+
├── api.py # 统一 API 门面与输入校验
|
|
166
|
+
├── config.py # L1:环境与配置管理
|
|
167
|
+
├── errors.py # 领域异常定义
|
|
168
|
+
├── db.py # SQLite 连接、事务与重试
|
|
169
|
+
├── models.py # Pydantic 输入/输出模型
|
|
170
|
+
├── memory_manager.py # 记忆持久化与双时间戳冲突处理
|
|
171
|
+
├── graph.py # 图谱关系查询与递归 CTE
|
|
172
|
+
├── retrieval.py # FTS5 与图谱混合检索、结果重排
|
|
173
|
+
├── decay_engine.py # L3:衰减评分、有用性权重与阈值过滤
|
|
174
|
+
├── extraction/ # 实体与事实抽取组件
|
|
175
|
+
└── observability/
|
|
176
|
+
├── logging.py # 结构化日志与敏感信息处理
|
|
177
|
+
└── metrics.py # 线程安全的内存指标
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
模块按职责拆分:**L1 配置与校验**提供稳定边界,**L3 检索与衰减算法**专注记忆质量,**L6 可观测性**提供日志和运行指标。架构取舍与演进记录见 [`docs/adr/`](docs/adr/)。
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 🧑💻 开发与测试
|
|
185
|
+
|
|
186
|
+
安装开发依赖后,可运行单元与集成测试:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
python -m pip install -e ".[dev]"
|
|
190
|
+
pytest -v
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
检索基准脚本:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
python benchmarks/bench_retrieval.py
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## 🤝 参与贡献
|
|
200
|
+
|
|
201
|
+
欢迎通过 Issue 讨论问题、提交改进建议或贡献代码。涉及行为变更时,请一并补充相应测试与文档。
|
|
202
|
+
|
|
203
|
+
## 📄 开源协议
|
|
204
|
+
|
|
205
|
+
RecallWeaver 采用 [MIT License](LICENSE)。
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# 🧠 织忆 (RecallWeaver)
|
|
2
|
+
|
|
3
|
+
**赋予 LLM Agent 穿越时间的记忆能力。零 Docker、零向量库、纯单文件 SQLite 构筑的工业级 AI 海马体。**
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://www.python.org/"><img src="https://img.shields.io/badge/Python-3.11%2B-3776AB?style=for-the-badge&logo=python&logoColor=white" alt="Python 3.11+"></a>
|
|
7
|
+
<a href="#"><img src="https://img.shields.io/badge/Tests-Passing-brightgreen?style=for-the-badge&logo=pytest&logoColor=white" alt="Tests Passing"></a>
|
|
8
|
+
<a href="#"><img src="https://img.shields.io/badge/Latency-10ms-blueviolet?style=for-the-badge&logo=speedtest&logoColor=white" alt="Latency 10ms"></a>
|
|
9
|
+
<a href="#"><img src="https://img.shields.io/badge/Precision-1.0-success?style=for-the-badge" alt="Precision 1.0"></a>
|
|
10
|
+
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow?style=for-the-badge" alt="MIT License"></a>
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
> 看!大肥鱼在真实 DeepSeek 环境下,如何利用纯本地记忆识破主人的花生过敏陷阱,并完成北京到杭州的时空转移。
|
|
16
|
+
|
|
17
|
+
**无需 Docker,无需向量数据库,也不必把整段对话塞进上下文。** RecallWeaver 为 Agent 提供可演变、可检索、会遗忘的长期记忆。
|
|
18
|
+
|
|
19
|
+
[核心特性](#-核心高光特性) · [性能与质量](#-性能与质量验证) · [快速开始](#-快速开始) · [架构](#-项目架构)
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 💥 为什么选择 RecallWeaver?
|
|
24
|
+
|
|
25
|
+
传统 RAG 记忆方案经常把“检索到文本”误当成“理解了事实”。当对话变长、事实更新、闲聊增多时,常见问题会逐渐显现:
|
|
26
|
+
|
|
27
|
+
| 传统方案的痛点 | 可能造成的结果 |
|
|
28
|
+
|---|---|
|
|
29
|
+
| 🕰️ **时间逻辑混乱** | “以前住在北京”与“现在住在杭州”同时进入上下文,Agent 前后矛盾,像是“精神分裂”。 |
|
|
30
|
+
| 🔊 **高频噪音挤占上下文** | 股票涨跌、天气闲聊等高频内容挤掉过敏、住址等真正重要的事实。 |
|
|
31
|
+
| 🧱 **部署成本高** | 重型向量库、图数据库和配套服务带来额外的部署、运维与资源成本。 |
|
|
32
|
+
|
|
33
|
+
### RecallWeaver 的破局方式
|
|
34
|
+
|
|
35
|
+
以**单文件 SQLite** 为持久化核心,结合**双时间戳事实版本**与**动态遗忘评分**:事实更新时保留历史并关闭旧版本;检索时综合相关性、时效与权重,并通过分数阈值过滤低价值噪音。轻量部署,也能明确回答“现在是什么”以及“以前是什么”。
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 📊 性能与质量验证
|
|
40
|
+
|
|
41
|
+
以下为项目压测与评测记录中的结果:
|
|
42
|
+
|
|
43
|
+
### 🔥 并发与长稳压测
|
|
44
|
+
|
|
45
|
+
| 持续时间 | 并发线程 | 总请求数 | 成功率 | 死锁 | P99 延迟 |
|
|
46
|
+
|---:|---:|---:|---:|---:|---:|
|
|
47
|
+
| 1,800 秒 | 50 | 180,000 | **100%** | **0** | **10.5 ms** |
|
|
48
|
+
|
|
49
|
+
### 🎯 检索质量评估
|
|
50
|
+
|
|
51
|
+
基于 100 组高仿真测试数据,评估检索结果与核心事实标准答案的一致性:
|
|
52
|
+
|
|
53
|
+
| Precision | Recall | F1-Score | 评测样本 |
|
|
54
|
+
|---:|---:|---:|---:|
|
|
55
|
+
| **1.0000** | **1.0000** | **1.0000** | 100 组 |
|
|
56
|
+
|
|
57
|
+
### 🧪 混沌工程边界验证
|
|
58
|
+
|
|
59
|
+
| 场景 | 验证结果 |
|
|
60
|
+
|---|---|
|
|
61
|
+
| SQL 注入文本 | 参数化查询防护通过 |
|
|
62
|
+
| 高频恶意检索刷分 | 权重加成上限与噪音阈值拦截通过 |
|
|
63
|
+
| 空数据库调用 | 空结果兜底通过 |
|
|
64
|
+
|
|
65
|
+
> 这些数字是项目当前记录的基准结果。实际表现会随硬件、SQLite 版本、数据集和运行配置变化。
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## ✨ 核心高光特性
|
|
70
|
+
|
|
71
|
+
### 🕰️ 双时间戳时空图谱 (Temporal Graph)
|
|
72
|
+
|
|
73
|
+
事实带有 `valid_from` 与 `valid_to`。同一主谓关系发生冲突时,系统在事务中关闭旧事实并写入新事实,历史不被覆盖。基于 SQLite 递归 CTE 的图谱遍历支持多跳关系查询,让 Agent 能区分事实的当前状态与历史状态。
|
|
74
|
+
|
|
75
|
+
### 📉 动态遗忘打分池 (Decay Pool)
|
|
76
|
+
|
|
77
|
+
以指数衰减为基础,综合记忆权重与检索反馈计算候选得分。检索次数的加成受有用性权重和上限约束;最终结果还会经过分数阈值截断,避免无关闲聊仅凭频繁出现就长期霸屏。
|
|
78
|
+
|
|
79
|
+
### ⚡ 混合检索 (Hybrid Search)
|
|
80
|
+
|
|
81
|
+
结合 SQLite FTS5 全文检索与图谱召回,再由衰减引擎统一重排。FTS5 的 `trigram` 分词支持中文连续子串匹配;结构化事实则可通过图谱关系查找,互补关键词召回与事实关联。
|
|
82
|
+
|
|
83
|
+
### 🔎 可观测、可解释
|
|
84
|
+
|
|
85
|
+
通过结构化日志、Trace ID 与线程安全指标记录核心链路状态。召回结果包含来源、分数及有效时间信息,便于排查与理解 Agent 为什么记住或忽略某条信息。
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 🚀 快速开始
|
|
90
|
+
# 启动 API 服务
|
|
91
|
+
uvicorn src.recallweaver.server:app --reload
|
|
92
|
+
|
|
93
|
+
### 1. 克隆并安装
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
git clone https://github.com/gaoweimax/recallweaver.git
|
|
97
|
+
cd recallweaver
|
|
98
|
+
python -m pip install -e .
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### 2. 配置 DeepSeek 密钥
|
|
102
|
+
|
|
103
|
+
示例使用环境变量读取密钥。请替换为你自己的密钥,**不要把真实密钥写入代码或提交到仓库**。
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
export DEEPSEEK_API_KEY="sk-..."
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Windows PowerShell 可使用:
|
|
110
|
+
|
|
111
|
+
```powershell
|
|
112
|
+
$env:DEEPSEEK_API_KEY = "sk-..."
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### 3. 写入事实并检索
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from recallweaver.api import RecallWeaverAPI
|
|
119
|
+
|
|
120
|
+
api = RecallWeaverAPI(db_path="recallweaver.db")
|
|
121
|
+
|
|
122
|
+
# 记录结构化事实
|
|
123
|
+
api.remember_triple(
|
|
124
|
+
subject="张三",
|
|
125
|
+
predicate="住在",
|
|
126
|
+
object_="杭州",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# 检索与问题相关的记忆
|
|
130
|
+
results = api.recall("张三住在哪里?", limit=3)
|
|
131
|
+
for memory in results:
|
|
132
|
+
print(f"[{memory.score:.3f}] {memory.content or memory.object}")
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
运行带真实 DeepSeek API 的 Rich 终端示例:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
python examples/deepseek_agent.py
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## 🏗️ 项目架构
|
|
144
|
+
|
|
145
|
+
```text
|
|
146
|
+
src/recallweaver/
|
|
147
|
+
├── api.py # 统一 API 门面与输入校验
|
|
148
|
+
├── config.py # L1:环境与配置管理
|
|
149
|
+
├── errors.py # 领域异常定义
|
|
150
|
+
├── db.py # SQLite 连接、事务与重试
|
|
151
|
+
├── models.py # Pydantic 输入/输出模型
|
|
152
|
+
├── memory_manager.py # 记忆持久化与双时间戳冲突处理
|
|
153
|
+
├── graph.py # 图谱关系查询与递归 CTE
|
|
154
|
+
├── retrieval.py # FTS5 与图谱混合检索、结果重排
|
|
155
|
+
├── decay_engine.py # L3:衰减评分、有用性权重与阈值过滤
|
|
156
|
+
├── extraction/ # 实体与事实抽取组件
|
|
157
|
+
└── observability/
|
|
158
|
+
├── logging.py # 结构化日志与敏感信息处理
|
|
159
|
+
└── metrics.py # 线程安全的内存指标
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
模块按职责拆分:**L1 配置与校验**提供稳定边界,**L3 检索与衰减算法**专注记忆质量,**L6 可观测性**提供日志和运行指标。架构取舍与演进记录见 [`docs/adr/`](docs/adr/)。
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## 🧑💻 开发与测试
|
|
167
|
+
|
|
168
|
+
安装开发依赖后,可运行单元与集成测试:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
python -m pip install -e ".[dev]"
|
|
172
|
+
pytest -v
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
检索基准脚本:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
python benchmarks/bench_retrieval.py
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## 🤝 参与贡献
|
|
182
|
+
|
|
183
|
+
欢迎通过 Issue 讨论问题、提交改进建议或贡献代码。涉及行为变更时,请一并补充相应测试与文档。
|
|
184
|
+
|
|
185
|
+
## 📄 开源协议
|
|
186
|
+
|
|
187
|
+
RecallWeaver 采用 [MIT License](LICENSE)。
|
|
File without changes
|
|
Binary file
|
|
File without changes
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Run a compact ablation of recency ranking and temporal fact resolution.
|
|
2
|
+
|
|
3
|
+
The script compares normal API recall with a recency-only baseline, then
|
|
4
|
+
contrasts unversioned text history with the graph's current fact timeline.
|
|
5
|
+
All experiments use temporary SQLite databases and leave no project data.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import tempfile
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from recallweaver.api import RecallWeaverAPI
|
|
15
|
+
|
|
16
|
+
QUERY = "张三吃啥"
|
|
17
|
+
CORE_FACT = "朋友问张三吃啥?张三对花生过敏,不能吃花生。"
|
|
18
|
+
NOISE_COUNT_BEFORE = 100
|
|
19
|
+
NOISE_COUNT_AFTER = 50
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _write_noise(api: RecallWeaverAPI, count: int, offset: int) -> None:
|
|
23
|
+
"""Write unrelated chatter records with stable, unique contents.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
api: API instance backed by an experiment-only database.
|
|
27
|
+
count: Number of chatter memories to add.
|
|
28
|
+
offset: Starting sequence number for unique chatter text.
|
|
29
|
+
"""
|
|
30
|
+
for index in range(offset, offset + count):
|
|
31
|
+
api.add_memory(
|
|
32
|
+
f"群聊闲聊{index:03d}:今天天气不错,晚饭吃了米饭和青菜。"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _run_recall_ablation(api: RecallWeaverAPI) -> None:
|
|
37
|
+
"""Compare decay-aware recall with a recency-only ranking baseline.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
api: API instance containing the 151-record recall corpus.
|
|
41
|
+
"""
|
|
42
|
+
_write_noise(api, NOISE_COUNT_BEFORE, 0)
|
|
43
|
+
core_memory_id = api.add_memory(CORE_FACT)
|
|
44
|
+
_write_noise(api, NOISE_COUNT_AFTER, NOISE_COUNT_BEFORE)
|
|
45
|
+
|
|
46
|
+
recall_results = api.recall(QUERY, limit=NOISE_COUNT_BEFORE + 1)
|
|
47
|
+
assert recall_results, "完整流程没有召回任何结果"
|
|
48
|
+
assert recall_results[0].memory_id == core_memory_id, (
|
|
49
|
+
"完整流程中核心事实没有排在第 1 位"
|
|
50
|
+
)
|
|
51
|
+
assert "花生过敏" in recall_results[0].content
|
|
52
|
+
|
|
53
|
+
with api.database_manager.get_connection() as conn:
|
|
54
|
+
recency_rows = conn.execute(
|
|
55
|
+
"""
|
|
56
|
+
SELECT id, content
|
|
57
|
+
FROM memories
|
|
58
|
+
WHERE tenant_id = ?
|
|
59
|
+
ORDER BY rowid DESC
|
|
60
|
+
""",
|
|
61
|
+
("default",),
|
|
62
|
+
).fetchall()
|
|
63
|
+
recency_results: list[dict[str, Any]] = [
|
|
64
|
+
dict(row) for row in recency_rows
|
|
65
|
+
]
|
|
66
|
+
core_positions = [
|
|
67
|
+
index
|
|
68
|
+
for index, row in enumerate(recency_results, start=1)
|
|
69
|
+
if row["id"] == core_memory_id
|
|
70
|
+
]
|
|
71
|
+
assert len(recency_results) == (
|
|
72
|
+
NOISE_COUNT_BEFORE + 1 + NOISE_COUNT_AFTER
|
|
73
|
+
), "消融基线数据条数不符合预期"
|
|
74
|
+
assert core_positions == [NOISE_COUNT_AFTER + 1], (
|
|
75
|
+
"仅按写入顺序排序时,核心事实未落在第 51 位"
|
|
76
|
+
)
|
|
77
|
+
assert all(
|
|
78
|
+
"群聊闲聊" in row["content"]
|
|
79
|
+
for row in recency_results[:NOISE_COUNT_AFTER]
|
|
80
|
+
), "核心事实之前的最近 50 条记录应全部为无关噪音"
|
|
81
|
+
|
|
82
|
+
print("实验组 A(完整检索流程)")
|
|
83
|
+
print(f" 查询:{QUERY}")
|
|
84
|
+
print(f" Top-1:{recall_results[0].content}")
|
|
85
|
+
print(" 结果:核心事实排在第 1 位")
|
|
86
|
+
print("实验组 B(消融:忽略相关性与衰减,仅按最近写入排序)")
|
|
87
|
+
print(f" 最近写入的噪音:{NOISE_COUNT_AFTER} 条")
|
|
88
|
+
print(f" 核心事实排名:第 {core_positions[0]} 位")
|
|
89
|
+
print(" 结果:核心事实被最近写入的闲聊挤到第 51 位")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _run_graph_ablation(db_path: Path) -> None:
|
|
93
|
+
"""Contrast unversioned text retrieval with graph fact history.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
db_path: Temporary database path reserved for this experiment.
|
|
97
|
+
"""
|
|
98
|
+
api = RecallWeaverAPI(str(db_path))
|
|
99
|
+
api.add_memory("张三住在北京。")
|
|
100
|
+
api.remember_triple("张三", "住在", "北京")
|
|
101
|
+
api.add_memory("张三住在上海。")
|
|
102
|
+
api.remember_triple("张三", "住在", "上海")
|
|
103
|
+
|
|
104
|
+
with api.database_manager.get_connection() as conn:
|
|
105
|
+
text_rows = conn.execute(
|
|
106
|
+
"""
|
|
107
|
+
SELECT content
|
|
108
|
+
FROM memories
|
|
109
|
+
WHERE tenant_id = ? AND content LIKE ?
|
|
110
|
+
ORDER BY rowid
|
|
111
|
+
""",
|
|
112
|
+
("default", "%张三住在%"),
|
|
113
|
+
).fetchall()
|
|
114
|
+
text_results = [str(row["content"]) for row in text_rows]
|
|
115
|
+
assert len(text_results) == 2, "文本基线应同时返回两条居住地记忆"
|
|
116
|
+
assert any("北京" in content for content in text_results)
|
|
117
|
+
assert any("上海" in content for content in text_results)
|
|
118
|
+
|
|
119
|
+
history = api.get_history("张三", "住在")
|
|
120
|
+
assert len(history) == 2, "图谱历史应保留两个事实版本"
|
|
121
|
+
current_facts = [fact for fact in history if fact["valid_to"] is None]
|
|
122
|
+
assert len(current_facts) == 1, "图谱必须只保留一个当前有效事实"
|
|
123
|
+
assert current_facts[0]["object"] == "上海"
|
|
124
|
+
|
|
125
|
+
print("实验组 C(消融:仅查文本,不读取图谱有效状态)")
|
|
126
|
+
print(f" 文本结果:{';'.join(text_results)}")
|
|
127
|
+
print(" 结果:北京和上海同时出现,无法判断当前事实")
|
|
128
|
+
print(" 图谱时间线:")
|
|
129
|
+
for fact in history:
|
|
130
|
+
state = "当前有效" if fact["valid_to"] is None else "已失效"
|
|
131
|
+
print(
|
|
132
|
+
f" {fact['object']}:{state} "
|
|
133
|
+
f"(valid_from={fact['valid_from']}, valid_to={fact['valid_to']})"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def main() -> None:
|
|
138
|
+
"""Execute all ablation cases using isolated temporary databases."""
|
|
139
|
+
with tempfile.TemporaryDirectory(prefix="recallweaver-ablation-") as directory:
|
|
140
|
+
root = Path(directory)
|
|
141
|
+
recall_api = RecallWeaverAPI(str(root / "recall-ablation.sqlite3"))
|
|
142
|
+
_run_recall_ablation(recall_api)
|
|
143
|
+
_run_graph_ablation(root / "graph-ablation.sqlite3")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
if __name__ == "__main__":
|
|
147
|
+
main()
|