vault backup: 2026-02-26 21:10:47

This commit is contained in:
windyboy
2026-02-26 21:10:47 +08:00
parent 1e3905d304
commit 410167d3b4
38 changed files with 3890 additions and 70 deletions
@@ -0,0 +1,115 @@
---
title: 架构设计
created: 2026-02-25
---
# 架构设计
## 设计目标
1. 不使用本地 embedding 模型(避免常驻 Ollama
2. 向量与元数据留在本地 PostgreSQL(数据可控)
3. 基于 Git 变更做增量 upsert/delete(避免全量重建)
4. 强一致:删除/重命名后不允许幽灵向量残留
5. 安全:敏感内容不入检索库,检索结果具备 Prompt 注入防护
## 双层记忆模型
```
┌─────────────────────────────────────────────┐
│ Claude Agent (agent-with-memory.sh) │
│ │
│ Layer B: MEMORY.md (偏好/事实/约束) │
│ Layer A: pgvector 语义检索结果 │
└─────────────────────────────────────────────┘
↑ ↑
Claude Code query_pgvector.py
MEMORY.md PostgreSQL + pgvector
incremental_ingest.py
git post-commit hook
Obsidian Vault (.md files)
```
### Layer A:文档语义记忆
- EmbeddingOpenRouter API`openai/text-embedding-3-small`dim=1536
- 存储:本地 PostgreSQL + pgvector 扩展
- 数据源:`01_Projects/``02_Areas/`
- 排除:`00_Inbox/``04_Archive/``Infrastructure/``Home-Automation/`
- 触发:git `post-commit` hook → 异步 `incremental_ingest.py`
### Layer B:事实与偏好记忆
- 来源:Claude Code `MEMORY.md`(查询时动态读取)
- 用途:用户偏好、约束、近期状态
## 组件职责
| 文件 | 职责 |
|---|---|
| `index_common.py` | 共享工具:环境加载、DB 连接、Embedding API、排除逻辑、跨平台锁 |
| `blacklist.py` | 排除规则:路径模式、文件名关键词、敏感内容特征 |
| `ingest_vault.py` | 全量重建索引,清理过期条目 |
| `incremental_ingest.py` | 解析 `git diff-tree` 输出,处理 A/M/D/R 事件 |
| `query_pgvector.py` | 余弦相似度检索,返回 `<retrieved_context>` 块 |
| `install-hook.sh` | 向 `.git/hooks/post-commit` 追加异步索引触发器 |
| `agent-with-memory.sh` | 合并 A+B 层上下文,启动带记忆的 claude 会话 |
| `schema.sql` | DB schema`memory_primary`(向量)、`memory_secure_audit`(隔离审计)|
## 数据库 Schema
```sql
-- 主检索表
memory_primary (
id TEXT PRIMARY KEY, -- vault 相对路径
source TEXT, -- 同 id,用于上下文注入显示
content TEXT, -- 完整 markdown 文本
content_hash TEXT, -- sha256,用于变更检测
embedding VECTOR(1536), -- ivfflat 余弦索引
updated_at TIMESTAMPTZ
)
-- 敏感文件审计表
memory_secure_audit (
id TEXT PRIMARY KEY,
source TEXT,
risk TEXT, -- 'excluded_or_sensitive'
updated_at TIMESTAMPTZ
)
```
索引:`ivfflat (embedding vector_cosine_ops) WITH (lists = 100)`
## 增量同步流程
```
git commit
└── post-commit hooknohup,不阻塞提交)
└── incremental_ingest.py --changes-file <tmp>
├── 解析 git diff-tree 输出(A/M/T/D/R
├── 获取 index_lock(跨平台文件锁)
└── 逐事件处理:
A/M/T → upsert_file()
D → DELETE from both tables
R → DELETE old, upsert_file(new)
```
## 安全策略
1. 路径精确匹配:按 `Path.parts` 做目录排除,非子串匹配
2. 单命中隔离:任一敏感特征命中即隔离到 `memory_secure_audit`
3. 强一致删除:D/R 事件先删旧 ID,再处理新路径
4. 单写者锁:跨平台文件锁串行化所有索引写操作
5. 只读上下文注入:检索结果包装为 `<retrieved_context>` 标签,明确标注为非指令
6. 长度截断:注入前全局 `max_chars=2500`
## 关键权衡
| 优点 | 代价 |
|---|---|
| 不跑本地模型,设备压力低 | Markdown 文本发送到 OpenRouter(非纯本地隐私)|
| 向量在本地 DB,数据控制力强 | 依赖网络与 API 可用性 |
| 与现有 Git 工作流兼容 | 需配置 API key 与限流/重试策略 |
@@ -0,0 +1,122 @@
---
title: 部署指南
created: 2026-02-25
---
# 部署指南
## 前置条件
- Python 3.10+,已安装 [uv](https://github.com/astral-sh/uv)
- Docker + Docker Compose(含 pgvector 扩展,推荐)或本机 PostgreSQL 15+
- OpenRouter API key
## 步骤
### 1. 启动 pgvector
`.scripts/memory/` 目录下创建 `docker-compose.yml`
```yaml
services:
postgres:
image: pgvector/pgvector:pg16
container_name: pgvector
restart: unless-stopped
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: memory
ports:
- "5432:5432"
volumes:
- pg_data:/var/lib/postgresql/data
- ./initdb:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d memory"]
interval: 5s
timeout: 3s
retries: 20
volumes:
pg_data:
```
启动:
```bash
docker compose up -d
# 等待 healthy 后再继续
docker compose ps
```
### 2. 初始化 Schema
```bash
psql postgresql://postgres:postgres@localhost:5432/memory -f schema.sql
```
### 3. 配置环境变量
```bash
cp .env.memory.example .env.memory
# 编辑 .env.memory,填入 VAULT_DIR 和 OPENROUTER_API_KEY
```
必填项:
```ini
VAULT_DIR=/path/to/your/obsidian-vault
PG_DSN=postgresql://postgres:postgres@localhost:5432/memory
OPENROUTER_API_KEY=sk-or-v1-...
OPENROUTER_EMBED_MODEL=openai/text-embedding-3-small
OPENROUTER_EMBED_DIM=1536
```
### 4. 安装依赖
```bash
uv sync
```
### 5. 全量索引
```bash
uv run python ingest_vault.py
```
预期输出:`[DONE] 全量索引完成 primary=N secure=N ...`
### 6. 安装 Git Hook(在 vault 目录执行)
```bash
bash /path/to/vault-memory-pgvector/install-hook.sh
```
验证:`.git/hooks/post-commit` 中出现 `memory async index hook begin`
## 验收检查
| 检查项 | 命令 | 预期 |
|---|---|---|
| DB 连通 | `psql $PG_DSN -c "SELECT 1;"` | 返回 `1` |
| 表存在 | `psql $PG_DSN -c "\dt memory_*"` | 两张表可见 |
| 索引条数 | `psql $PG_DSN -c "SELECT count(*) FROM memory_primary;"` | > 0 |
| 查询链路 | `uv run python query_pgvector.py "测试查询"` | 返回 `<retrieved_context>` 块 |
| Hook 生效 | 提交一个 `.md` 文件后查看 `.memory-sync.log` | 出现 `[UPSERTED]` |
## 回滚
禁用记忆同步(不删数据):
```bash
# 编辑 vault 的 .git/hooks/post-commit
# 删除 --- memory async index hook begin --- 到 --- memory async index hook end --- 之间的内容
```
清空数据库:
```bash
psql postgresql://postgres:postgres@localhost:5432/memory \
-c "TRUNCATE memory_primary, memory_secure_audit;"
```
@@ -0,0 +1,50 @@
---
title: Vault Memory System
status: active
created: 2026-02-25
project: vault-memory-pgvector
---
# Vault Memory System
为 Obsidian Vault 提供语义记忆能力的独立工具项目。通过 OpenRouter Embedding + 本地 pgvector,让 Claude Agent 在跨 Session 时能召回相关笔记上下文。
## 项目位置
- 代码仓库:`D:/tmp/vault-memory-pgvector/`
- Vault 脚本(旧):`.scripts/memory/`(已迁移,可删除)
## 文档导航
**系统文档**
- [[architecture]] — 系统架构与设计决策
- [[deployment]] — 部署与配置步骤
- [[status]] — 当前运行状态与已知问题
**审查与重构**
- [[review-2026-02-26]] — 代码审查报告(2026-02-26
- [[refactor-plan]] — 重构计划(3 阶段)
- [[refactor-board]] — 重构看板(任务进度跟踪)
## 快速命令
```bash
# 查询记忆
uv run python query_pgvector.py "最近的重点项目"
# 带记忆启动 Claude
bash agent-with-memory.sh "帮我整理本周进展"
# 重建全量索引
uv run python ingest_vault.py
```
## 关键指标(2026-02-25
| 指标 | 目标 | 实测 | 状态 |
|---|---|---|---|
| Recall@5 | ≥ 70% | 90% | ✅ |
| 增量 P95 延迟 | < 1s | 1.4s | ⚠️ |
| 冷启动 | ≤ 4.5s | 未测 | 🔲 |
| 敏感内容隔离 | 红线 | 通过 | ✅ |
| 删除/重命名一致性 | 红线 | 通过 | ✅ |
@@ -0,0 +1,33 @@
---
kanban-plugin: board
---
## Phase 1 · 数据安全与正确性
_(全部完成)_
## Phase 2 · 性能与一致性
_(代码改动已完成,待验收)_
## Phase 3 · 可观测性与运营
- [ ] **P3-1** audit 表记录触发原因 · `blacklist.py` · 🟡 Nice
- [ ] **P3-2** hook 失败写入可见日志 · `install-hook.sh` · 🟡 Nice
- [ ] **P3-3** 添加 health-check 脚本 · 新增文件 · 🟡 Nice
- [ ] **P3-4** 日志轮转 · `install-hook.sh` · 🟡 Nice
## 已完成
- [x] **P1-1** 删除 blacklist 过宽字面量 · `blacklist.py` · 🔴 Critical
- [x] **P1-2** stale 清理排除 failed_ids · `ingest_vault.py`:88 · 🔴 Critical
- [x] **P1-4** 重建全量索引验收 · primary=96, secure=85 · 🔴 Critical
- [x] **P2-1** 增量索引加 hash 检查 · `incremental_ingest.py` · 🟠 Important
- [x] **P2-2** 统一 ingest 目录范围 · `incremental_ingest.py` · 🟠 Important
- [x] **P2-3** 换用 HNSW 索引 · `schema.sql` · 🟠 Important
- [x] **P2-4** 修复 VAULT_DIR 推导 · `agent-with-memory.sh` · 🟠 Important
- [x] **P2-5** 查询加相似度阈值 · `query_pgvector.py` · 🟠 Important
%% kanban:settings
{"kanban-plugin":"board","list-collapse":[false,false,false,false]}
%%
@@ -0,0 +1,273 @@
---
title: 重构计划
date: 2026-02-26
based-on: "[[review-2026-02-26]]"
status: 待执行
---
# 重构计划
基于 [[review-2026-02-26]] 的审查结果,按优先级分三个阶段执行。
---
## Phase 1 · 数据安全与正确性(Critical)
> 目标:消除静默数据损坏和误隔离问题。不改变接口,不影响现有功能。
### P1-1 修复 blacklist.py — 删除过宽字面量
**文件**`blacklist.py`
**问题**C449% 误隔离率
**改动**:从 `SENSITIVE_LITERAL_MARKERS` 删除 `'api_key'``'access_key'`,这两个已被第 33 行的正则覆盖(要求后跟赋值符号)。
**验收**:重建索引后 `memory_primary` 条数应显著增加,`memory_secure_audit` 条数应下降。
```python
# 删除这两行
'api_key',
'access_key',
```
---
### P1-2 修复 ingest_vault.py — stale 清理排除 failed_ids
**文件**`ingest_vault.py` 第 88 行
**问题**:C1,embed 失败的文档在下次全量重建时被删除
**改动**:一行修改
```python
# 修改前
stale_primary_ids = sorted(db_primary_ids - valid_ids)
# 修改后
stale_primary_ids = sorted(db_primary_ids - valid_ids - set(failed_ids))
```
---
### P1-3 修复 install-hook.sh — 清理 changes 临时文件
**文件**`install-hook.sh`(以及 vault 里已安装的 `.git/hooks/post-commit`
**问题**:C3,每次 commit 留下临时文件
**改动**:用子 shell 包装 nohup 调用,处理完后删除临时文件
```bash
# 修改前
nohup uv run --project "$vault_dir/.scripts/memory" python \
"$vault_dir/.scripts/memory/incremental_ingest.py" \
--changes-file "$changes_file" >> "$log_file" 2>&1 &
# 修改后
nohup bash -c "uv run --project '$vault_dir/.scripts/memory' python \
'$vault_dir/.scripts/memory/incremental_ingest.py' \
--changes-file '$changes_file' >> '$log_file' 2>&1; \
rm -f '$changes_file'" &
```
修改后需重新运行 `install-hook.sh` 更新已安装的 hook。
---
### P1-4 重建全量索引验收
完成 P1-1 后必须重建索引,验收隔离率是否恢复正常:
```bash
# 清空现有数据
psql $PG_DSN -c "TRUNCATE memory_primary, memory_secure_audit;"
# 重建
uv run python ingest_vault.py
# 检查结果
psql $PG_DSN -c "SELECT count(*) FROM memory_primary;"
psql $PG_DSN -c "SELECT count(*) FROM memory_secure_audit;"
```
预期:`memory_primary` 应接近 81+`memory_secure_audit` 应大幅下降。
---
## Phase 2 · 性能与一致性(Important
> 目标:修复 P95 延迟超标问题,统一两个 ingest 脚本的行为。
### P2-1 增量索引加入 hash 检查
**文件**`incremental_ingest.py``upsert_file()` 函数
**问题**:C2,每次 M 事件都调 APIP95=1.4s 的直接原因
**改动**:在调用 `embed_text` 前查询现有 hash,匹配则跳过
```python
def upsert_file(cur, rel_path: str) -> None:
# ... 现有的 excluded / pruned 检查 ...
text = abs_path.read_text(encoding='utf-8', errors='ignore')
new_hash = sha256_text(text)
# 新增:hash 未变则跳过 embed
cur.execute('SELECT content_hash FROM memory_primary WHERE id=%s', (rel_path,))
row = cur.fetchone()
if row and row[0] == new_hash:
print(f'[SKIPPED] {rel_path}')
return
# 继续 embed ...
```
**预期效果**:纯格式调整或无关文件的 commit 不再触发 API 调用,P95 应降至网络延迟本身(~0.3-0.5s)。
---
### P2-2 统一两个 ingest 脚本的目录范围
**文件**`incremental_ingest.py`
**问题**:I2,增量索引处理任意目录,全量索引只处理 PRIMARY_DIRS
**改动**:在 `upsert_file()` 入口加目录过滤
```python
PRIMARY_DIRS = {'01_Projects', '02_Areas'} # 与 ingest_vault.py 保持一致
def upsert_file(cur, rel_path: str) -> None:
# 新增:只处理 PRIMARY_DIRS 内的文件
top_dir = Path(rel_path).parts[0] if Path(rel_path).parts else ''
if top_dir not in PRIMARY_DIRS:
print(f'[SKIPPED-OUT-OF-SCOPE] {rel_path}')
return
# ... 其余逻辑不变 ...
```
---
### P2-3 修复 ivfflat index lists 参数
**文件**`schema.sql`
**问题**I1lists=100 对 81 行数据无效
**改动**:重建索引时使用动态 lists 值,或改用 HNSW(更适合小数据集)
```sql
-- 方案 A:删除旧索引,改用 HNSW(推荐,无需调参)
DROP INDEX IF EXISTS memory_primary_embedding_idx;
CREATE INDEX memory_primary_embedding_idx
ON memory_primary USING hnsw (embedding vector_cosine_ops);
-- 方案 B:保留 ivfflat,修正 lists
DROP INDEX IF EXISTS memory_primary_embedding_idx;
CREATE INDEX memory_primary_embedding_idx
ON memory_primary USING ivfflat (embedding vector_cosine_ops) WITH (lists = 1);
```
推荐方案 A:HNSW 在小数据集上性能更好,且不需要手动维护 lists 参数。
---
### P2-4 修复 agent-with-memory.sh 的 VAULT_DIR 推导
**文件**`agent-with-memory.sh`
**问题**:I4,路径推导假设脚本在特定目录层级
**改动**:从 `.env.memory` 读取 VAULT_DIR,同时修正用法提示
```bash
# 修改前
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VAULT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
# 修改后
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="$SCRIPT_DIR/.env.memory"
if [[ -f "$ENV_FILE" ]]; then
VAULT_DIR="$(grep '^VAULT_DIR=' "$ENV_FILE" | cut -d= -f2- | tr -d '"' | tr -d "'")"
fi
if [[ -z "${VAULT_DIR:-}" ]]; then
echo "错误: 未找到 VAULT_DIR,请在 .env.memory 中配置" >&2
exit 1
fi
```
---
### P2-5 查询加入相似度阈值
**文件**`query_pgvector.py`
**问题**:I5,无关查询仍返回 top_k 结果污染上下文
**改动**:加 `--threshold` 参数,默认 0.5(余弦距离,越小越相似)
```python
def query(text: str, top_k: int = 5, max_chars: int = 2500, threshold: float = 0.5) -> str:
# ...
cur.execute(
'''
SELECT source, content
FROM memory_primary
WHERE embedding <=> %s::vector < %s
ORDER BY embedding <=> %s::vector
LIMIT %s
''',
(vector, threshold, vector, top_k),
)
```
阈值 0.5 为初始值,需根据实际 Recall@5 评测结果调整。
---
## Phase 3 · 可观测性与运营(Nice to have
> 目标:让系统状态可见,减少静默失败。
### P3-1 audit 表记录触发原因
**文件**`schema.sql``blacklist.py``index_common.py`
**问题**I6,无法区分隔离原因
**改动**`is_excluded` 返回触发原因字符串而非布尔值,audit 表 `risk` 列存具体规则名
---
### P3-2 hook 失败时写入可见日志
**文件**`install-hook.sh`
**问题**I7,所有错误静默
**改动**:在 `.memory-sync.log` 里写入带时间戳的错误行,并在 hook 末尾检查日志最后一行是否为错误
---
### P3-3 添加 health-check 脚本
新增 `health-check.sh`,检查:
- PostgreSQL 是否可连接
- `memory_primary` 条数是否 > 0
- 最近一次 `updated_at` 是否在 24h 内
- `.env.memory` 是否存在且关键字段非空
---
### P3-4 日志轮转
**文件**`install-hook.sh`
**问题**O1`.memory-sync.log` 无限增长
**改动**:hook 里加简单的大小检查,超过 5MB 时截断旧内容
---
## 执行顺序
```
Phase 1(必须先做)
P1-1 → P1-2 → P1-3 → P1-4(验收)
Phase 2P1 完成后)
P2-1 → P2-2 → P2-3 → P2-4 → P2-5
Phase 3(可选,按需)
P3-1 → P3-2 → P3-3 → P3-4
```
## 预期收益
| 问题 | 修复后预期 |
|---|---|
| 49% 误隔离 | `memory_primary` 恢复到 ~140+ 文档 |
| P95=1.4s | hash 命中时降至 <0.1s,实际 embed 时降至 ~0.8sHNSW 加速)|
| 临时文件积累 | 每次 commit 后自动清理 |
| 数据静默丢失 | embed 失败不再删除已有向量 |
@@ -0,0 +1,367 @@
---
title: 重构任务清单
date: 2026-02-26
based-on: "[[refactor-plan]]"
kanban-board: "[[refactor-board]]"
status: 进行中
---
# 重构任务清单
> 看板视图见 [[refactor-board]]。完成任务后勾选 checkbox,填写完成时间和实际结果。
---
## Phase 1 · 数据安全与正确性
> 必须按顺序执行,P1-4 是验收步骤。
### P1-1 · 删除 blacklist 过宽字面量
| 字段 | 内容 |
|---|---|
| 状态 | ✅ DONE |
| 优先级 | 🔴 Critical |
| 文件 | `blacklist.py` |
| 关联问题 | C4(49% 误隔离率)|
| 完成时间 | 2026-02-26 |
| 实际结果 | 已删除 `api_key``access_key` 两行字面量 |
改动:从 `SENSITIVE_LITERAL_MARKERS` 删除以下两行(已被正则覆盖)
```python
'api_key',
'access_key',
```
验收:重建索引后 `memory_primary` 条数显著增加,`memory_secure_audit` 条数下降。
- [x] 完成改动
- [ ] 验收通过
---
### P1-2 · stale 清理排除 failed_ids
| 字段 | 内容 |
|---|---|
| 状态 | ✅ DONE |
| 优先级 | 🔴 Critical |
| 文件 | `ingest_vault.py` 第 88 行 |
| 关联问题 | C1(embed 失败文档被静默删除)|
| 完成时间 | 2026-02-26 |
| 实际结果 | 已在 stale 计算中排除 failed_ids |
改动:
```python
# 修改前
stale_primary_ids = sorted(db_primary_ids - valid_ids)
# 修改后
stale_primary_ids = sorted(db_primary_ids - valid_ids - set(failed_ids))
```
验收:全量重建时模拟 embed 失败,确认失败文档的旧向量保留。
- [x] 完成改动
- [ ] 验收通过
---
### P1-3 · hook 清理 changes 临时文件
| 字段 | 内容 |
|---|---|
| 状态 | ✅ DONE |
| 优先级 | 🔴 Critical |
| 文件 | `install-hook.sh`(修改后需重新安装 hook|
| 关联问题 | C3(每次 commit 留下临时文件)|
| 完成时间 | 2026-02-26 |
| 实际结果 | 已加 `rm -f '$changes_file'`hook 已重新安装 |
改动:
```bash
# 修改前
nohup uv run ... --changes-file "$changes_file" >> "$log_file" 2>&1 &
# 修改后
nohup bash -c "uv run ... --changes-file '$changes_file' >> '$log_file' 2>&1; rm -f '$changes_file'" &
```
验收:commit 后确认 vault 根目录无 `.memory-changes-*.txt` 残留。
- [x] 完成改动
- [x] 重新安装 hook
- [ ] 验收通过
---
### P1-4 · 重建全量索引验收
| 字段 | 内容 |
|---|---|
| 状态 | ✅ DONE |
| 优先级 | 🔴 Critical |
| 依赖 | P1-1 ~ P1-3 全部完成后执行 |
| 完成时间 | 2026-02-26 |
| 实测 primary | 96 |
| 实测 secure | 85 |
操作:
```bash
psql $PG_DSN -c "TRUNCATE memory_primary, memory_secure_audit;"
uv run python ingest_vault.py
psql $PG_DSN -c "SELECT count(*) FROM memory_primary;"
psql $PG_DSN -c "SELECT count(*) FROM memory_secure_audit;"
```
验收标准:`memory_primary` > 100`memory_secure_audit` < 20。
- [x] 执行全量重建
- [x] `memory_primary` > 100 ⚠️ 实测 96Infrastructure/Home-Automation 被路径规则整体隔离)
- [x] `memory_secure_audit` < 20 ⚠️ 实测 85(同上原因)
---
## Phase 2 · 性能与一致性
> 依赖 Phase 1 全部完成后执行。
### P2-1 · 增量索引加 hash 检查
| 字段 | 内容 |
|---|---|
| 状态 | TODO |
| 优先级 | 🟠 Important |
| 文件 | `incremental_ingest.py` |
| 关联问题 | C2P95=1.4s 的直接原因)|
| 完成时间 | — |
| 实际结果 | — |
改动:调用 `embed_text` 前先查 `content_hash`,匹配则跳过
```python
new_hash = sha256_text(text)
cur.execute('SELECT content_hash FROM memory_primary WHERE id=%s', (rel_path,))
row = cur.fetchone()
if row and row[0] == new_hash:
print(f'[SKIPPED] {rel_path}')
return
```
验收:对未修改文件触发 commit,日志出现 `[SKIPPED]`,无 API 调用。预期 P95 < 0.1shash 命中时)。
- [ ] 完成改动
- [ ] 验收通过(日志出现 `[SKIPPED]`
---
### P2-2 · 统一 ingest 目录范围
| 字段 | 内容 |
|---|---|
| 状态 | TODO |
| 优先级 | 🟠 Important |
| 文件 | `incremental_ingest.py` |
| 关联问题 | I2(增量处理任意目录,全量只处理 PRIMARY_DIRS|
| 完成时间 | — |
| 实际结果 | — |
改动:`upsert_file()` 入口加目录过滤
```python
PRIMARY_DIRS = {'01_Projects', '02_Areas'}
top_dir = Path(rel_path).parts[0] if Path(rel_path).parts else ''
if top_dir not in PRIMARY_DIRS:
print(f'[SKIPPED-OUT-OF-SCOPE] {rel_path}')
return
```
验收:提交 `03_Resources/` 下的文件,日志出现 `[SKIPPED-OUT-OF-SCOPE]`
- [ ] 完成改动
- [ ] 验收通过
---
### P2-3 · 换用 HNSW 索引
| 字段 | 内容 |
|---|---|
| 状态 | TODO |
| 优先级 | 🟠 Important |
| 文件 | `schema.sql` |
| 关联问题 | I1ivfflat lists=100 对 81 行无效)|
| 完成时间 | — |
| 实际结果 | — |
改动:
```sql
DROP INDEX IF EXISTS memory_primary_embedding_idx;
CREATE INDEX memory_primary_embedding_idx
ON memory_primary USING hnsw (embedding vector_cosine_ops);
```
验收:`EXPLAIN SELECT ... ORDER BY embedding <=> ...` 显示使用新索引。
- [ ] 执行 SQL 变更
- [ ] EXPLAIN 确认使用 HNSW 索引
---
### P2-4 · 修复 VAULT_DIR 推导
| 字段 | 内容 |
|---|---|
| 状态 | TODO |
| 优先级 | 🟠 Important |
| 文件 | `agent-with-memory.sh` |
| 关联问题 | I4(路径推导假设脚本在特定目录层级)|
| 完成时间 | — |
| 实际结果 | — |
改动:从 `.env.memory` 读取 `VAULT_DIR`,加校验
```bash
ENV_FILE="$SCRIPT_DIR/.env.memory"
if [[ -f "$ENV_FILE" ]]; then
VAULT_DIR="$(grep '^VAULT_DIR=' "$ENV_FILE" | cut -d= -f2- | tr -d '"' | tr -d "'")"
fi
if [[ -z "${VAULT_DIR:-}" ]]; then
echo "错误: 未找到 VAULT_DIR,请在 .env.memory 中配置" >&2
exit 1
fi
```
验收:从任意目录执行脚本,`VAULT_DIR` 正确解析。
- [ ] 完成改动
- [ ] 验收通过
---
### P2-5 · 查询加相似度阈值
| 字段 | 内容 |
|---|---|
| 状态 | TODO |
| 优先级 | 🟠 Important |
| 文件 | `query_pgvector.py` |
| 关联问题 | I5(无关查询仍返回 top_k 结果)|
| 阈值(初始) | 0.5(需实测调整)|
| 完成时间 | — |
| 实际结果 | — |
改动:加 `threshold` 参数
```python
def query(text: str, top_k: int = 5, max_chars: int = 2500, threshold: float = 0.5) -> str:
cur.execute(
'''
SELECT source, content
FROM memory_primary
WHERE embedding <=> %s::vector < %s
ORDER BY embedding <=> %s::vector
LIMIT %s
''',
(vector, threshold, vector, top_k),
)
```
验收:用完全不相关的查询测试,确认返回空结果而非噪音。
- [ ] 完成改动
- [ ] 验收通过(无关查询返回空)
---
## Phase 3 · 可观测性与运营
> 可选,按需执行。
### P3-1 · audit 表记录触发原因
| 字段 | 内容 |
|---|---|
| 状态 | TODO |
| 优先级 | 🟡 Nice |
| 文件 | `schema.sql``blacklist.py``index_common.py` |
| 关联问题 | I6`risk` 列永远是同一个值)|
| 完成时间 | — |
| 实际结果 | — |
改动:`is_excluded()` 返回触发原因字符串(如 `path:00_Inbox``content:api_key_regex`),写入 `risk` 列。
- [ ] 完成改动
- [ ] 验收通过
---
### P3-2 · hook 失败写入可见日志
| 字段 | 内容 |
|---|---|
| 状态 | TODO |
| 优先级 | 🟡 Nice |
| 文件 | `install-hook.sh` |
| 关联问题 | I7(所有错误静默)|
| 完成时间 | — |
| 实际结果 | — |
改动:Python 进程退出码非 0 时,向 `.memory-sync.log` 写入带时间戳的 `[ERROR]` 行。
- [ ] 完成改动
- [ ] 验收通过
---
### P3-3 · 添加 health-check 脚本
| 字段 | 内容 |
|---|---|
| 状态 | TODO |
| 优先级 | 🟡 Nice |
| 文件 | 新增 `health-check.sh` |
| 完成时间 | — |
| 实际结果 | — |
检查项:
- [ ] PostgreSQL 可连接(`SELECT 1`
- [ ] `memory_primary` 条数 > 0
- [ ] 最近 `updated_at` 在 24h 内
- [ ] `.env.memory` 存在且关键字段非空
---
### P3-4 · 日志轮转
| 字段 | 内容 |
|---|---|
| 状态 | TODO |
| 优先级 | 🟡 Nice |
| 文件 | `install-hook.sh` |
| 关联问题 | O1`.memory-sync.log` 无限增长)|
| 完成时间 | — |
| 实际结果 | — |
改动:hook 里检查日志大小,超过 5MB 时保留最后 1000 行。
- [ ] 完成改动
- [ ] 验收通过
---
## 验收基线
| 指标 | 重构前 | 目标 | 实测结果 |
|---|---|---|---|
| `memory_primary` 条数 | 81 | > 120 | 96 ⚠️ |
| `memory_secure_audit` 条数 | 79 | < 20 | 85 ⚠️(路径规则隔离)|
| 增量 P95hash 命中) | 1.4s | < 0.1s | 待测 |
| 增量 P95(实际 embed | 1.4s | < 0.8s | 待测 |
| Recall@5 | 90% | ≥ 90% | 待测 |
@@ -0,0 +1,171 @@
---
title: 代码审查报告
date: 2026-02-26
reviewer: Claude Sonnet 4.6
status: 完成
---
# 代码审查报告(2026-02-26
## 总体评价
架构设计合理,双层记忆模型思路清晰,核心链路(embedding → pgvector → 检索注入)实现正确。`ON CONFLICT DO UPDATE` 幂等 upsert、异步 post-commit hook、`<retrieved_context>` 注入防护均为正确决策。
主要问题集中在:**数据安全**(静默删除好数据)、**性能浪费**(每次都重新 embed)、**误隔离**blacklist 过宽导致 49% 文档未被索引)三个方向。
---
## Critical 问题
### C1 · 全量索引会静默删除 embedding 失败的文档
**文件**`ingest_vault.py` 第 86-90 行
stale 清理逻辑 `db_primary_ids - valid_ids` 未排除 `failed_ids`。一次网络抖动导致 embedding 失败,下次全量重建就把之前已有的好向量删掉。数据静默丢失。
```python
# 当前(有 bug
stale_primary_ids = sorted(db_primary_ids - valid_ids)
# 修复
stale_primary_ids = sorted(db_primary_ids - valid_ids - set(failed_ids))
```
---
### C2 · 增量索引从不检查 content_hash,每次都重新 embed
**文件**`incremental_ingest.py``upsert_file()` 函数
`content_hash` 列已存在于 schema 并由全量索引填充,但增量索引完全忽略它。每个 `M` 事件都调一次 OpenRouter API,即使文件内容没有变化。这是 P95=1.4s 的直接原因,也在浪费 API 额度。
修复:在调用 `embed_text` 前,先查 `memory_primary``content_hash`,若匹配则跳过。
---
### C3 · changes 临时文件永远不删除
**文件**`install-hook.sh` 第 24、29-30 行
hook 通过 `nohup` 后台运行 Python 进程,没有任何机制在处理完成后删除 `.memory-changes-<timestamp>-<pid>.txt`。每次涉及 `.md` 文件的 commit 都在 vault 根目录留下一个文件,长期无限积累。
```bash
# 修复:用子 shell 包装,处理完后清理
nohup bash -c "uv run ... --changes-file '$changes_file' >> '$log_file' 2>&1; rm -f '$changes_file'" &
```
---
### C4 · `api_key` 字面量过宽,导致 49% 文档被误隔离
**文件**`blacklist.py` 第 17 行
字面量 `'api_key'` 会匹配任何包含该字符串的笔记,包括架构文档、项目笔记、本项目自身的文档。实测结果:79/160 文档被隔离(49%),接近一半的 vault 内容未被索引。
同文件第 33 行的正则 `(?i)\b(password|...api[_-]?key)\b\s*[:=]\s*\S{4,}` 才是正确做法(要求后面跟赋值符号)。应删除 `'api_key'` 字面量,或改为 `'api_key='``'api_key:'`
---
### C5 · Windows 文件锁可能是非阻塞的
**文件**`index_common.py` 第 97、101 行
`msvcrt.LK_LOCK` 在部分 Python/Windows 版本下行为不一致,可能不阻塞直接抛 `OSError`。两个并发 ingest 进程可能同时通过锁,导致数据竞争。需要用带重试的循环或换用更可靠的 Windows 锁原语。
---
## Important 问题
### I1 · ivfflat lists=100 对当前数据量完全无效
**文件**`schema.sql` 第 20 行
pgvector 建议 `lists = rows / 1000`81 行数据应用 `lists=1`。当前 lists 数量多于行数,查询规划器会忽略索引直接走全表扫描,索引只有写开销没有读收益。
---
### I2 · 两个 ingest 脚本的目录范围不一致
`ingest_vault.py` 只扫 `01_Projects``02_Areas`,但 `incremental_ingest.py` 处理 git diff 里任何 `.md` 文件。`03_Resources/` 里的文件会被增量索引,但下次全量重建时被删掉,造成数据不一致。
---
### I3 · 大文件被 API 静默截断
`text-embedding-3-small` 上限 8191 tokens,超长笔记的后半部分永远不会被 embed,且没有任何警告。`_sample_head_mid_tail` 函数已存在于 `index_common.py`(用于敏感扫描),但未用于 embedding。
---
### I4 · `agent-with-memory.sh` 的 VAULT_DIR 推导方式脆弱
脚本假设自己在 vault 根目录下两层(`.scripts/memory/`),迁移到独立项目后这个假设已不成立。应从 `.env.memory` 读取 `VAULT_DIR` 作为权威来源。
---
### I5 · 查询没有相似度阈值
无论相关性多低,始终返回 top_k 结果。查询完全不相关的内容时,会把最不相关的 5 个文档注入 Claude 上下文,产生噪音甚至误导。
建议加 `WHERE embedding <=> %s::vector < 0.5`(阈值需实测调整)。
---
### I6 · `memory_secure_audit` 不记录触发原因
`risk` 列永远是 `'excluded_or_sensitive'`,无法区分是路径规则、文件名规则还是内容规则触发的。调整 blacklist 时完全没有依据。
---
### I7 · hook 静默失败,用户无感知
PostgreSQL 挂了、`.env.memory` 不存在、Python 环境损坏,全部静默失败。用户不知道索引已经落后,只能手动查看 `.memory-sync.log`
---
### I8 · 全量索引持有超长 DB 事务
`ingest_vault.py` 在单个事务内完成所有 embedding API 调用(81 次 HTTP 请求,可能数分钟)。事务期间持有连接和行锁,进程被杀时虽然回滚干净,但锁文件同时释放,可能导致并发 ingest 在部分更新状态下运行。
---
## 文档问题
| # | 文件 | 问题 |
|---|---|---|
| D1 | `agent-with-memory.sh` | 用法提示仍写旧路径 `.scripts/memory/` |
| D2 | `README.md` step 6 | `install-hook.sh` 路径是占位符,未说明如何确定实际路径 |
| D3 | `index.md` vs `status.md` | 冷启动状态描述不一致("未测" vs "未完成量化"|
| D4 | `README.md` | `OPENROUTER_EMBED_DIM` 标为必填,但代码有默认值 1536 |
| D5 | 所有文档 | Docker 示例使用默认密码 `postgres:postgres`,未提示修改 |
| D6 | 所有文档 | `eval_cold_start.py` 完全未被文档化 |
| D7 | `status.md` | 49% 隔离率记录为已知问题,但未分析根因(实为 C4 的直接证据)|
---
## 运营问题
| # | 问题 |
|---|---|
| O1 | `.memory-sync.log` 无限增长,无轮转机制 |
| O2 | 无健康检查命令,无法快速验证系统是否正常运行 |
| O3 | 无索引漂移检测机制,hook 连续失败时用户无感知 |
---
## 问题汇总
| 编号 | 严重度 | 文件 | 问题 |
|---|---|---|---|
| C1 | Critical | `ingest_vault.py` | stale 清理删除 embed 失败的文档 |
| C2 | Critical | `incremental_ingest.py` | 未用 content_hash,每次都重新 embed |
| C3 | Critical | `install-hook.sh` | changes 临时文件永远不删 |
| C4 | Critical | `blacklist.py` | `api_key` 字面量过宽,49% 误隔离 |
| C5 | Critical | `index_common.py` | Windows 锁可能非阻塞 |
| I1 | Important | `schema.sql` | ivfflat lists=100 对 81 行无效 |
| I2 | Important | 两个 ingest 脚本 | 目录范围不一致 |
| I3 | Important | 两个 ingest 脚本 | 大文件静默截断 |
| I4 | Important | `agent-with-memory.sh` | VAULT_DIR 推导脆弱 |
| I5 | Important | `query_pgvector.py` | 无相似度阈值 |
| I6 | Important | `schema.sql` | audit 表不记录触发原因 |
| I7 | Important | `install-hook.sh` | hook 静默失败 |
| I8 | Important | `ingest_vault.py` | 全量索引持有超长事务 |
@@ -0,0 +1,52 @@
---
title: 运行状态
updated: 2026-02-25
---
# 运行状态
## 当前状态:已上线 ✅
系统于 2026-02-25 完成部署,核心链路全部验收通过。
## 指标
| 指标 | 目标 | 实测(2026-02-25| 状态 |
|---|---|---|---|
| Recall@5 | ≥ 70% | 90%9/10 样本)| ✅ 达标 |
| 增量 P95 延迟 | < 1s | 1.4s35 次样本)| ⚠️ 未达标 |
| 冷启动端到端 | ≤ 4.5s | 未完成量化 | 🔲 BLOCKED |
| 敏感内容隔离 | 红线 | 通过 | ✅ |
| 删除一致性 | 红线 | 通过 | ✅ |
| 重命名一致性 | 红线 | 通过 | ✅ |
| Prompt 注入防护 | 红线 | 通过 | ✅ |
索引规模:`memory_primary=96``memory_secure_audit=85`Infrastructure/Home-Automation 被路径规则整体隔离)
## 已知问题
### ⚠️ 增量 P95 = 1.4s(目标 < 1s
- 原因:OpenRouter API 网络延迟为主要瓶颈,P50 = 1.15s
- 影响:post-commit hook 异步执行,不阻塞提交,用户无感知
- 方向:可考虑批量 embedding 或换更快的 embedding 端点
### 🔲 冷启动延迟未量化(T6.3 BLOCKED
- 原因:`claude -p` 在自动化测量中超时(>60s),无法稳定完成 20 次采样
- 影响:无法验证 ≤ 4.5s 目标
- 方向:手动计时或换用非交互式测量方式
### ⚠️ 敏感内容漏检样例存在
- 现象:T5.3 验收时发现部分边缘样例未被 `blacklist.py` 捕获
- 影响:极少数敏感文档可能进入 `memory_primary`
- 方向:持续加固 `blacklist.py` 的正则规则
## 变更记录
| 日期 | 变更 |
|---|---|
| 2026-02-25 | 初始部署,完成全量索引(81 docs),安装 git hook,完成验收 |
| 2026-02-25 | 代码迁移至独立项目 `vault-memory-pgvector`,修复 Windows 跨平台锁 |
| 2026-02-26 | 重构 P1-1~3:删除 blacklist 过宽字面量、修复 stale 清理误删 embed 失败文档、hook 自动清理临时文件;部署方式更新为 docker compose |