274 lines
7.7 KiB
Markdown
274 lines
7.7 KiB
Markdown
---
|
||||
|
|
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`
|
|||
|
|
**问题**:C4,49% 误隔离率
|
|||
|
|
**改动**:从 `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 事件都调 API,P95=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`
|
|||
|
|
**问题**:I1,lists=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 2(P1 完成后)
|
|||
|
|
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.8s(HNSW 加速)|
|
|||
|
|
| 临时文件积累 | 每次 commit 后自动清理 |
|
|||
|
|
| 数据静默丢失 | embed 失败不再删除已有向量 |
|