Files
2026Technology-Competition/docs/superpowers/plans/2026-08-29-iteration-impact-rag.md
T

514 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 影响调查 RAG 化 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 在影响调查(impact investigation)阶段,对项目的既有系统源码/设计文档/规则做切块与向量检索(RAG),把与当前需求/表最相关的片段作为上下文喂给 LLM,提升受影响模块/字段的召回率;在无法检索或无嵌入能力时回退到现有确定性交叉引用(DesignReference),保证行为不退化。
**Architecture:** 新增轻量本地 RAG 栈——`Embedder`(优先复用推理引擎的 embed 能力,缺省用确定性的词袋 FakeEmbedder 以保证测试与离线运行)、`RagStore`SQLite 持久化 chunks + 余弦检索,复用项目既有 SQLite 习惯,不引入 chromadb/faiss 等新重依赖)、`ImpactRAG`index/retrieve)。`ImpactAgent` 增加 `use_rag` 开关:开启时把检索到的 top-k 片段拼入影响调查 prompt;关闭时走原路径。`GenesisService` 透传 `use_rag`,默认 `False`(向后兼容)。
**Tech Stack:** Python `sqlite3` + `json`(向量存储,无新依赖);现有 `inference` 引擎抽象(取 embed);`node:test` 仅用于前端(本迭代无前端改动,故无 Node 测试)。
## Global Constraints
- 中文优先:所有 UI 文案、注释、日志使用中文(专有名词/代码关键字除外)。
- 不新增重依赖:RAG 存储用标准库 `sqlite3` + `json`;Embedder 优先复用推理引擎,缺省 `FakeEmbedder`(纯标准库)。禁止引入 chromadb / faiss / sentence-transformers 等重依赖。
- TDD:每个任务先写失败测试再实现;全绿方可 commit。
- 覆盖率门禁:`fail_under=99`pyproject 现有),新增 Python 代码须有测试覆盖。
- 向后兼容:`use_rag` 默认 `False`;未开启时影响调查输出与现状完全一致(DesignReference 路径不变)。
- 增量索引:同一 scope 重复 index 先清后写,避免重复累积。
---
### Task 1: Embedder 抽象(含离线 FakeEmbedder
**Files:**
- Create: `src/genesis/rag/embeddings.py`
- Test: `tests/test_rag_embeddings.py`
**Interfaces:**
- Consumes: `genesis.inference`(可选,用于真实 embed
- Produces:
- `genesis.rag.embeddings.Embedder` 协议:`embed(texts: list[str]) -> list[list[float]]`
- `genesis.rag.embeddings.FakeEmbedder`(词袋确定性,dim=64,归一化)
- `genesis.rag.embeddings.get_embedder(engine) -> Embedder`(有 embed 能力用真实,否则 FakeEmbedder
- [ ] **Step 1: 写失败测试**
```python
# tests/test_rag_embeddings.py
from genesis.rag.embeddings import FakeEmbedder, get_embedder
def test_fake_embedder_deterministic():
e = FakeEmbedder()
a = e.embed(["OrderController 创建订单"])[0]
b = e.embed(["OrderController 创建订单"])[0]
assert a == b
def test_fake_embedder_similar_closer_than_unrelated():
e = FakeEmbedder()
base = e.embed(["OrderController 处理创建订单请求"])[0]
sim = e.embed(["OrderController 保存订单到数据库"])[0]
dif = e.embed(["用户登录认证模块"])[0]
import math
def cos(x, y):
dot = sum(p * q for p, q in zip(x, y))
nx = math.sqrt(sum(p * p for p in x)); ny = math.sqrt(sum(q * q for q in y))
return dot / (nx * ny or 1.0)
assert cos(base, sim) > cos(base, dif)
def test_get_embedder_fake_engine_returns_fake():
e = get_embedder("fake")
assert e.__class__.__name__ == "FakeEmbedder"
```
- [ ] **Step 2: 运行测试确认失败**
Run: `python -m pytest tests/test_rag_embeddings.py -q`
Expected: FAIL`ModuleNotFoundError`
- [ ] **Step 3: 实现最小版本**
```python
# src/genesis/rag/embeddings.py
from __future__ import annotations
import hashlib
import math
from typing import List, Protocol
class Embedder(Protocol):
def embed(self, texts: List[str]) -> List[List[float]]: ...
_DIM = 64
def _tokenize(text: str) -> List[str]:
# 简易分词:去标点、按空白与驼峰边界切分,转小写
toks = []
cur = ""
for ch in text.lower():
if ch.isalnum():
cur += ch
else:
if cur:
toks.append(cur); cur = ""
if cur:
toks.append(cur)
# 驼峰拆分(OrderController -> order, controller
out = []
for t in toks:
idx = 0
for i, c in enumerate(t):
if i > 0 and c.isupper():
out.append(t[idx:i]); idx = i
out.append(t[idx:])
return [x for x in out if x]
class FakeEmbedder:
def embed(self, texts: List[str]) -> List[List[float]]:
vecs = []
for t in texts:
v = [0.0] * _DIM
for tok in _tokenize(t):
h = hashlib.md5(tok.encode("utf-8")).digest()
idx = h[0] % _DIM
v[idx] += 1.0
norm = math.sqrt(sum(x * x for x in v)) or 1.0
vecs.append([x / norm for x in v])
return vecs
def get_embedder(engine) -> Embedder:
# 真实引擎若提供 embed API 则在此接入;当前统一回退 FakeEmbedder
return FakeEmbedder()
```
- [ ] **Step 4: 运行测试确认通过**
Run: `python -m pytest tests/test_rag_embeddings.py -q`
Expected: PASS3 passed
- [ ] **Step 5: 提交**
```bash
git add src/genesis/rag/embeddings.py tests/test_rag_embeddings.py
git commit -m "feat(rag): 新增 Embedder 抽象与离线 FakeEmbedder"
```
---
### Task 2: RagStoreSQLite 向量存储 + 余弦检索)
**Files:**
- Create: `src/genesis/rag/store.py`
- Test: `tests/test_rag_store.py`
**Interfaces:**
- Consumes: `embeddings.Embedder`(仅用于维度约定,实际检索用传入向量)
- Produces:
- `genesis.rag.store.RagStore(db_path)`:方法 `reset_scope(scope)``add(scope, chunks, embeddings)``search(scope, query_vec, k) -> list[str]`
- 向量以 JSON 文本列存储(无新依赖)
- [ ] **Step 1: 写失败测试**
```python
# tests/test_rag_store.py
import math
from genesis.rag.store import RagStore
def _vec(*ones):
v = [0.0] * 8
for i in ones:
v[i] = 1.0
n = math.sqrt(sum(x * x for x in v))
return [x / n for x in v]
def test_search_returns_most_similar_chunk():
s = RagStore(":memory:")
s.reset_scope("p1")
s.add("p1", ["订单模块处理创建", "用户认证登录"], [_vec(0, 1), _vec(4, 5)])
res = s.search("p1", _vec(0, 1), k=1)
assert res == ["订单模块处理创建"]
def test_reset_scope_clears():
s = RagStore(":memory:")
s.reset_scope("p1")
s.add("p1", ["a"], [_vec(0)])
s.reset_scope("p1")
assert s.search("p1", _vec(0), k=3) == []
```
- [ ] **Step 2: 运行测试确认失败**
Run: `python -m pytest tests/test_rag_store.py -q`
Expected: FAIL
- [ ] **Step 3: 实现最小版本**
```python
# src/genesis/rag/store.py
from __future__ import annotations
import json
import math
import sqlite3
from typing import List
def _cosine(a, b):
dot = sum(x * y for x, y in zip(a, b))
na = math.sqrt(sum(x * x for x in a)); nb = math.sqrt(sum(y * y for y in b))
return dot / (na * nb or 1.0)
class RagStore:
def __init__(self, db_path: str):
self.conn = sqlite3.connect(db_path)
self.conn.execute(
"CREATE TABLE IF NOT EXISTS rag_chunks ("
"id INTEGER PRIMARY KEY, scope TEXT, chunk TEXT, embedding TEXT)"
)
self.conn.commit()
def reset_scope(self, scope: str) -> None:
self.conn.execute("DELETE FROM rag_chunks WHERE scope=?", (scope,))
self.conn.commit()
def add(self, scope: str, chunks: List[str], embeddings: List[List[float]]) -> None:
self.reset_scope(scope)
for c, e in zip(chunks, embeddings):
self.conn.execute(
"INSERT INTO rag_chunks(scope, chunk, embedding) VALUES(?,?,?)",
(scope, c, json.dumps(e)),
)
self.conn.commit()
def search(self, scope: str, query_vec: List[float], k: int = 5) -> List[str]:
rows = self.conn.execute(
"SELECT chunk, embedding FROM rag_chunks WHERE scope=?", (scope,)
).fetchall()
scored = []
for chunk, emb in rows:
scored.append((_cosine(query_vec, json.loads(emb)), chunk))
scored.sort(key=lambda x: x[0], reverse=True)
return [c for _, c in scored[:k]]
```
- [ ] **Step 4: 运行测试确认通过**
Run: `python -m pytest tests/test_rag_store.py -q`
Expected: PASS2 passed
- [ ] **Step 5: 提交**
```bash
git add src/genesis/rag/store.py tests/test_rag_store.py
git commit -m "feat(rag): 新增 RagStoreSQLite 向量存储 + 余弦检索)"
```
---
### Task 3: ImpactRAG 索引/检索服务
**Files:**
- Create: `src/genesis/rag/impact_rag.py`
- Test: `tests/test_impact_rag.py`
**Interfaces:**
- Consumes: `RagStore``Embedder`
- Produces:
- `genesis.rag.impact_rag.ImpactRAG(store, embedder)`
- `index(scope, sources: list[tuple[str, str]])`sources = [(文件名, 文本), ...]
- `retrieve(scope, query: str, k: int = 5) -> list[str]`
- [ ] **Step 1: 写失败测试**
```python
# tests/test_impact_rag.py
from genesis.rag.embeddings import FakeEmbedder
from genesis.rag.store import RagStore
from genesis.rag.impact_rag import ImpactRAG
def test_retrieve_returns_relevant_chunk():
store = RagStore(":memory:")
rag = ImpactRAG(store, FakeEmbedder())
sources = [
("OrderController.java", "public class OrderController { 创建订单 }"),
("UserAuth.java", "public class UserAuth { 用户登录认证 }"),
]
rag.index("p1", sources)
res = rag.retrieve("p1", "OrderController 的影响范围", k=1)
assert res and "OrderController" in res[0]
```
- [ ] **Step 2: 运行测试确认失败**
Run: `python -m pytest tests/test_impact_rag.py -q`
Expected: FAIL
- [ ] **Step 3: 实现最小版本**
```python
# src/genesis/rag/impact_rag.py
from __future__ import annotations
from typing import List, Tuple
from genesis.rag.embeddings import Embedder
from genesis.rag.store import RagStore
_CHUNK = 800
def _split(text: str, size: int = _CHUNK) -> List[str]:
paras = [p.strip() for p in text.split("\n") if p.strip()]
out, buf = [], ""
for p in paras:
if len(buf) + len(p) > size and buf:
out.append(buf); buf = p
else:
buf = (buf + "\n" + p).strip()
if buf:
out.append(buf)
return out or [""]
class ImpactRAG:
def __init__(self, store: RagStore, embedder: Embedder):
self.store = store
self.embedder = embedder
def index(self, scope: str, sources: List[Tuple[str, str]]) -> None:
chunks = []
for name, text in sources:
for piece in _split(text):
chunks.append(f"[{name}]\n{piece}")
embs = self.embedder.embed(chunks)
self.store.add(scope, chunks, embs)
def retrieve(self, scope: str, query: str, k: int = 5) -> List[str]:
if not query.strip():
return []
qv = self.embedder.embed([query])[0]
return self.store.search(scope, qv, k)
```
- [ ] **Step 4: 运行测试确认通过**
Run: `python -m pytest tests/test_impact_rag.py -q`
Expected: PASS
- [ ] **Step 5: 提交**
```bash
git add src/genesis/rag/impact_rag.py tests/test_impact_rag.py
git commit -m "feat(rag): 新增 ImpactRAG 索引/检索服务"
```
---
### Task 4: ImpactAgent 接入 use_rag
**Files:**
- Modify: `src/genesis/impact/impact_agent.py`
- Test: `tests/test_impact_agent_rag.py`
**Interfaces:**
- Consumes: `ImpactRAG`(可选)、`FakeEmbedder`/`RagStore`(测试)
- Produces: `ImpactAgent.__init__` 新增 `rag: ImpactRAG | None = None``run_impact(..., use_rag: bool = False)` 开启时把检索片段并入 prompt 上下文
- [ ] **Step 1: 写失败测试**
```python
# tests/test_impact_agent_rag.py
from genesis.rag.embeddings import FakeEmbedder
from genesis.rag.store import RagStore
from genesis.rag.impact_rag import ImpactRAG
from genesis.impact.impact_agent import ImpactAgent
def _make():
store = RagStore(":memory:")
rag = ImpactRAG(store, FakeEmbedder())
rag.index("p1", [("OrderController.java", "class OrderController { 创建订单 }")])
# ImpactAgent 构造参数按现有签名;rag 以 kw 注入
agent = ImpactAgent(rag=rag)
return agent
def test_run_impact_includes_rag_context(monkeypatch):
agent = _make()
captured = {}
def fake_engine_invoke(prompt):
captured["prompt"] = prompt
return '{"impacts":[{"target":"OrderController","type":"修改","detail":"受订单表影响"}]}'
monkeypatch.setattr(agent, "_engine_invoke", fake_engine_invoke)
out = agent.run_impact("订单表新增字段", scope="p1", use_rag=True)
assert "OrderController" in captured["prompt"]
assert "OrderController" in str(out)
```
(注:`_engine_invoke` 为 ImpactAgent 内部调用 LLM 的既有方法名占位;若实际方法名不同,实现任务中替换为真实方法名,并在测试中同步。)
- [ ] **Step 2: 运行测试确认失败**
Run: `python -m pytest tests/test_impact_agent_rag.py -q`
Expected: FAIL`rag` 参数 / `use_rag` 不存在)
- [ ] **Step 3: 实现最小版本**
`impact_agent.py` 顶部 `from genesis.rag.impact_rag import ImpactRAG`(仅在类型标注用,避免循环导入:若 `rag` 包尚未在 `genesis/rag/__init__` 暴露,使用 `TYPE_CHECKING` 方式或字符串标注)。
`ImpactAgent.__init__` 增加:
```python
def __init__(self, ..., rag: "ImpactRAG | None" = None):
...
self.rag = rag
```
`run_impact` 在拼装影响调查 prompt 前,若 `use_rag and self.rag`
```python
context_chunks = self.rag.retrieve(scope, requirement_text, k=5)
if context_chunks:
rag_block = "\n".join(f"# 相关代码/文档片段\n{c}" for c in context_chunks)
prompt = prompt + "\n\n" + rag_block
```
未开启或 `self.rag is None` 时,`prompt` 与现状一致(DesignReference 路径不变)。返回结构不变。
- [ ] **Step 4: 运行测试确认通过**
Run: `python -m pytest tests/test_impact_agent_rag.py -q`
Expected: PASS
- [ ] **Step 5: 提交**
```bash
git add src/genesis/impact/impact_agent.py tests/test_impact_agent_rag.py
git commit -m "feat(impact): ImpactAgent 接入 use_rag,检索片段并入 prompt(默认关闭)"
```
---
### Task 5: 服务层接线 + 端到端 + 文档
**Files:**
- Modify: `src/genesis/server/service.py``src/genesis/server/app.py`(如需要透传 `use_rag`
- Test: `tests/test_impact_rag_e2e.py`
- Modify: `docs/design.md``_AI_USAGE_LOG.md`
**Interfaces:**
- Consumes: `ImpactRAG``RagStore``FakeEmbedder``ImpactAgent(rag=...)`
- Produces: `GenesisService(use_rag: bool = False)`;会话级影响调查在 `use_rag` 为真时走 RAG
- [ ] **Step 1: 写失败测试**
```python
# tests/test_impact_rag_e2e.py
from genesis.rag.embeddings import FakeEmbedder
from genesis.rag.store import RagStore
from genesis.rag.impact_rag import ImpactRAG
from genesis.server.service import GenesisService
from genesis.server.store import ProjectsStore, SessionStore
def test_service_impact_uses_rag_when_enabled(tmp_path):
store = SessionStore(db_path=str(tmp_path / "s.db"))
projects = ProjectsStore(db_path=str(tmp_path / "s.db"))
rag_store = RagStore(str(tmp_path / "rag.db"))
rag = ImpactRAG(rag_store, FakeEmbedder())
rag.index("p1", [("OrderController.java", "class OrderController { 创建订单 }")])
svc = GenesisService(store=store, data_root=str(tmp_path / "data"),
engine="fake", projects=projects, use_rag=True, rag=rag)
# 仅验证接线:service 持有 rag 且 use_rag 为真
assert svc.use_rag is True and svc.rag is rag
```
(完整端到端需真实引擎与样本源码;以接线断言 + 既有 impact 测试不退化为下限。)
- [ ] **Step 2: 运行测试确认失败**
Run: `python -m pytest tests/test_impact_rag_e2e.py -q`
Expected: FAIL`use_rag` / `rag` 参数不存在)
- [ ] **Step 3: 实现最小版本**
`service.py``GenesisService.__init__` 增加 `use_rag: bool = False, rag: ImpactRAG | None = None`;保存 `self.use_rag / self.rag`。在 `run_impact`(或 `_build_impact_agent`)构造 `ImpactAgent(rag=self.rag)` 并将 `use_rag=self.use_rag` 透传给 `run_impact`
`app.py``create_app` 增加 `use_rag: bool = False` 参数并透传 `GenesisService(..., use_rag=use_rag)``scripts/serve.py` 增加 `--use-rag` 开关(默认关)。
- [ ] **Step 4: 运行全量测试确认无回归**
Run: `python -m pytest -q -o addopts=""`
Expected: 全部通过(在现有基线基础上新增用例;既有 impact/design 测试不受默认关闭影响)
- [ ] **Step 5: 更新文档 + 提交**
`docs/design.md` §12 追加:「影响调查 RAG 化(2026-08-29):新增 `genesis/rag`embeddings/store/impact_rag),`ImpactAgent.use_rag` 开启时检索 top-k 片段并入 prompt;默认关闭,回退 DesignReference;存储用 SQLite,无新重依赖。」
```bash
git add src/genesis/server/service.py src/genesis/server/app.py scripts/serve.py tests/test_impact_rag_e2e.py docs/design.md _AI_USAGE_LOG.md
git commit -m "feat(rag): 服务层接线 use_rag + design.md 记录(默认关闭)"
```
---
## Self-Review
1. **Spec coverage:** 离线 Embedder(T1)、向量存储(T2)、索引/检索(T3)、Agent 接入与开关(T4)、服务层接线与文档(T5)均覆盖;回退路径在 T4/T5 明确保留。
2. **Placeholder scan:** 无 TBD`_engine_invoke` 已在测试中标注为真实方法名占位,实现任务需对齐真实方法名(已在 Step 3 注明)。
3. **Type consistency:** `ImpactRAG.index(scope, sources)` / `retrieve(scope, query, k)` 在 T3/T4 一致;`RagStore.add(scope, chunks, embeddings)` 维度与 `Embedder.embed` 输出一致;`use_rag` 在 Service/Agent 间透传一致。
4. **限制:** 默认 `use_rag=False`,不影响现有行为;真实 embed 接入点在 `get_embedder` 预留,待推理引擎提供 embed API 时扩展(不在本迭代)。