From cc8850cffe3ae4b29be049bf8ce754ea75ec0629 Mon Sep 17 00:00:00 2001 From: lhl Date: Sun, 30 Aug 2026 00:57:36 +0800 Subject: [PATCH] =?UTF-8?q?docs(rag):=20=E8=A1=A5=E5=BD=95=20RAG=20?= =?UTF-8?q?=E8=BF=AD=E4=BB=A3=E5=90=84=E4=BB=BB=E5=8A=A1=E8=AF=84=E5=AE=A1?= =?UTF-8?q?=E6=8A=A5=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/superpowers/plans/rag-task-3-report.md | 63 +++++++++ docs/superpowers/plans/rag-task-4-report.md | 148 ++++++++++++++++++++ docs/superpowers/plans/rag-task-5-report.md | 52 +++++++ 3 files changed, 263 insertions(+) create mode 100644 docs/superpowers/plans/rag-task-3-report.md create mode 100644 docs/superpowers/plans/rag-task-4-report.md create mode 100644 docs/superpowers/plans/rag-task-5-report.md diff --git a/docs/superpowers/plans/rag-task-3-report.md b/docs/superpowers/plans/rag-task-3-report.md new file mode 100644 index 0000000..2800770 --- /dev/null +++ b/docs/superpowers/plans/rag-task-3-report.md @@ -0,0 +1,63 @@ +# RAG 迭代 Task 3 实现报告(含评审修订 D1 的 index_dir) + +## 状态 +**GREEN — 已完成并通过测试** + +- 先写失败测试(RED:因 `impact_rag.py` 缺失触发 `ModuleNotFoundError`) +- 实现 `ImpactRAG` 及 D1 修订项 `index_dir` +- 重新运行测试:`2 passed` + +## Commit +`feat(rag): 新增 ImpactRAG 索引/检索服务(含 index_dir)` + +涉及文件: +- `src/genesis/rag/impact_rag.py`(新增) +- `tests/test_impact_rag.py`(新增) + +## 测试输出摘要 +``` +python -m pytest tests/test_impact_rag.py -q +2 passed in 2.86s +``` + +测试用例: +1. `test_retrieve_returns_relevant_chunk`:验证 `index` + `retrieve` 能做到 chunk 级语义召回(FakeEmbedder 词袋向量 + cosine),返回结果含 `OrderController`。 +2. `test_index_dir_reads_text_files`:验证 `index_dir` 仅索引文本源文件(白名单扩展名),跳过 `binary.bin` 与非文本/空文件,返回索引片段数为 1,且可检索到内容。 + +## 自我审查 +- `index`:将 `(文件名, 文本)` 切分为 chunk(默认 800 字,按段落聚合),前缀 `[文件名]`,批量 embed 后 `store.add`。 +- `index_dir`(D1):`Path(root).rglob("*")` 遍历,按扩展名白名单过滤,UTF-8 `errors="ignore"` 读取,跳过空文本与读取异常;返回实际索引文件数(即 sources 数,非 chunk 数 —— 与 brief 文档及测试断言 `n == 1` 一致)。 +- `retrieve`:空查询直接返回 `[]`;否则 embed 查询向量后调用 `store.search`。 +- 测试中对 `RagStore(":memory:")` 采用 `try/finally` 包裹 `close()`,避免 ResourceWarning。 + +## 一句话摘要 +实现 `ImpactRAG` 的 `index`/`retrieve` 与 D1 修订项 `index_dir`,2 项测试全绿,可为上传即索引(Task 5 接线)提供目录级源码索引能力。 + +## 顾虑 +- 运行指定测试文件时,全局 pytest 覆盖率配置(要求 99%)会报 `Required test coverage of 99.0% not reached`,但本任务验证仅需 `2 passed`,不影响功能正确性;若 CI 以全量套件口径执行,需单独豁免或补充本模块覆盖率。 +- `index_dir` 返回的是"索引文件数"而非"片段数";brief 文档两处措辞略不一致(接口签名注释写"返回索引的片段数",测试与示例暗示为文件数),当前实现以测试断言为准(文件数)。 + +## 修复后验证 + +### 状态 +**GREEN — 分支测试补全,覆盖率 100%** + +针对评审发现(空 query 行为未测 + fail_under=99 硬要求),补全 `index`/`index_dir`/`retrieve` 全部分支测试与防御性过滤,并更新 `index_dir` docstring 为"返回索引的文件数"。 + +### 修改摘要 +涉及文件: +- `src/genesis/rag/impact_rag.py` + - `index`:对 `_split` 结果过滤空片段(`if piece.strip():`),避免空向量噪声 chunk。 + - `index_dir`:docstring 由"片段数"更正为"文件数";读取文本后叠加内容级跳过(空文本 `if not text.strip(): continue`、含 NUL 字节二进制误带扩展名 `\x00` 跳过)。 +- `tests/test_impact_rag.py`:新增 10 个用例,覆盖空 query、空文本源、非目录 root、空文件、读取异常、大写扩展名、空目录、NUL 二进制、单段超长与多段拆行。 + +### 测试输出摘要 +``` +python -m pytest tests/test_impact_rag.py -q --cov=genesis.rag.impact_rag --cov-report=term-missing +src\genesis\rag\impact_rag.py 53 0 26 0 100% +12 passed in 0.28s +``` +(注:若沿用全局 `addopts` 的 `--cov=genesis`,整体覆盖率会因只跑单文件而低于 99% 告警,但 `impact_rag.py` 本文件覆盖率为 100%、12 例全绿,符合本次验收口径。) + +### 一句话摘要 +补全 `ImpactRAG` 分支测试与防御后,`impact_rag.py` 达到语句/分支 100% 覆盖,原 2 例与新 10 例共 12 例全绿。 diff --git a/docs/superpowers/plans/rag-task-4-report.md b/docs/superpowers/plans/rag-task-4-report.md new file mode 100644 index 0000000..4279f71 --- /dev/null +++ b/docs/superpowers/plans/rag-task-4-report.md @@ -0,0 +1,148 @@ +# RAG 迭代 Task 4:ImpactAgent 接入可选 RAG 上下文 + +## 状态 + +- 状态:**DONE(GREEN)** +- 新增/修改文件: + - `src/genesis/impact/impact_agent.py`(新增 `__init__`、`run_impact`、`_build_impact_prompt`、模块常量 `_RAG_CONTEXT_TITLE`) + - `tests/test_impact_agent_rag.py`(新增,3 个测试) +- 提交:见文末 commit + +## 真实代码要点(先读真实代码后实现) + +实现前已实际读取以下文件,确认与 Task Brief 的初始假设**存在重大出入**,据此调整落地方式: + +1. **`src/genesis/impact/impact_agent.py`(原文件)** + - `ImpactAgent` 原**没有 `__init__` 方法**(无位置参数、无 engine 成员)。 + - 原**没有 `run_impact` 方法、没有 `engine` 成员、没有任何 LLM 调用**;它是**确定性规则实现**,唯一对外方法是 + `run(self, structured_source: StructuredSource, session_id: str = "impact", scope: dict | None = None) -> ImpactReport`, + 内部通过 `_classify_table` / `_build_lookup` / `_match_tokens` 等纯规则产出影响调查书。 + - prompt 文本在原文件中**不存在**(无 LLM),故不存在"既有 prompt 拼装位置"。 + - 其他被调用方(`impact_report_to_dict`、`_element_to_dict`、`_header_index` 等)保持原样未改动,向后兼容。 + +2. **`src/genesis/rag/impact_rag.py`(真实 RAG 接口)** + - `ImpactRAG.__init__(self, store: RagStore, embedder: Embedder)` + - `index(self, scope: str, sources: List[Tuple[str, str]]) -> None` + - `retrieve(self, scope: str, query: str, k: int = 5) -> List[str]` + - 命中片段格式为 `[{name}]\n{piece}`,因此片段文本天然含文件名(如 `TradeApplication.java`)。 + +3. **`src/genesis/inference/engine.py`(真实 LLM engine 接口)** + - `InferenceEngine.chat_structured(self, *, session_id, prompt, variables, dict, schema, retry_count=2)`。 + - 注意:真实方法是 **async** 协程;但本仓库各测试中的 `FakeEngine.chat_structured` 普遍为**同步**签名 + `def chat_structured(self, *, session_id, prompt, variables, schema, retry_count=2)`。 + - 为与测试契约(同步调用、捕获 `prompt`)一致,`run_impact` 以**同步**方式调用 + `self.engine.chat_structured(...)`,形参名与真实接口完全一致(`session_id/prompt/variables/schema`)。 + +4. **`src/genesis/rag/embeddings.py`** + - `FakeEmbedder().embed(texts) -> List[List[float]]`,测试可直接用。 + +### 据此落地的真实签名(实现后) + +- `ImpactAgent.__init__(self, engine=None, use_rag: bool = False, rag: "ImpactRAG | None" = None)` + - 新增末尾参数,保留无参 `ImpactAgent()` 兼容(既有 18 个 `run` 测试仍通过)。 + - engine 成员名:`self.engine`;RAG 成员:`self.rag`;默认开关:`self.use_rag`。 +- `ImpactAgent.run_impact(self, session_id: str, requirements_text: str, use_rag: bool | None = None, k: int = 5)` + - LLM 调用方法名:`self.engine.chat_structured(...)`(与真实 engine 形参对齐)。 + - prompt 拼装位置:`_build_impact_prompt(requirements_text)` 产出基础 prompt; + RAG 注入在 `run_impact` 内:启用时 `prompt = prompt + f"\n\n{_RAG_CONTEXT_TITLE}\n{rag_context}"`。 +- RAG 检索查询:`"影响调查:" + requirements_text[:200]`;`self.rag.retrieve(session_id, query, k)`。 + +## 行为说明 + +- `use_rag=True`(显参或 `self.use_rag`)且 `self.rag` 存在:检索命中片段以明确小节标题 + `# 既有系统关联上下文(RAG 检索,辅助判断影响范围)` 追加进实际发送给 LLM 的 prompt。 +- `use_rag=False`(默认)或 `self.rag` 为 None:prompt 与原版完全一致,**不含**该小节(向后兼容已验证)。 +- `run_impact` 在无 `engine` 时抛出 `RuntimeError`,避免静默空跑。 +- 既有确定性 `run` 路径未改动,影响调查书生成逻辑不受影响。 + +## 测试输出摘要 + +新增 `tests/test_impact_agent_rag.py`: + +``` +python -m pytest tests/test_impact_agent_rag.py -q --no-cov +... [100%] +3 passed in 0.10s +``` + +- `test_run_impact_with_rag_injects_context`:构造 `ImpactAgent(engine=FakeEngine(), rag=rag, use_rag=True)`, + `rag.index(session_id, [("TradeApplication.java","订单创建调用 MyBatis")])`,调用 + `run_impact(session_id, requirements_text="创建订单的影响", k=5)`;断言 FakeEngine 收到的 prompt + **包含** `"TradeApplication"` 与 RAG 小节标题。PASS。 +- `test_run_impact_without_rag_no_context`:同上但 `use_rag=False`;断言 prompt **不含** RAG 小节标题与片段。PASS。 +- `test_run_impact_default_no_rag_no_context`:不传 `use_rag`(默认 False);同向后兼容断言。PASS。 + +回归(既有 impact 测试,确认无破坏): + +``` +python -m pytest tests/ -q -k impact --no-cov +71 passed, 533 deselected, 1 warning in 32.40s +``` + +> 说明:仓库 `pytest` 配置含 99% 覆盖率门禁;单独跑子集会因覆盖率不足而返回非 0, +> 故验证阶段加 `--no-cov` 仅校验测试本身。全量 `pytest tests/` 不受影响(本次未改动既有逻辑)。 +> 新增 `run_impact` 经由 `tests/test_impact_agent_rag.py` 覆盖,确定性 `run` 路径由既有 18 个用例覆盖。 + +## 自我审查 + +- ✅ 先读真实代码:确认原 `ImpactAgent` 无 LLM、无 `run_impact`,已据实调整而非照搬 Brief 假设。 +- ✅ TDD:先写失败测试(RED:3 failed)→ 实现 → GREEN(3 passed)。 +- ✅ 向后兼容:默认 `use_rag=False` 时 prompt 内容不变;既有 `run` 与 18 个旧测试全部通过。 +- ✅ RAG 注入正确:命中片段含文件名 `TradeApplication.java` 被拼入 prompt 并加明确小节标题。 +- ✅ 不新增重依赖:`ImpactRAG`/`RagStore`/`FakeEmbedder` 均为既有模块;类型注解用字符串前向引用避免新增 import。 +- ⚠ 顾虑:`run_impact` 为同步调用 `chat_structured`,而真实 `InferenceEngine.chat_structured` 是 async。 + Task 5 服务层接线时需注入**同步包装**或使 `run_impact` 支持 await(当前按 Brief 测试契约保持同步)。 +- ⚠ 顾虑:新增 `engine`/`rag` 成员在确定性 `run` 路径中未被使用,仅为 LLM 路径 `run_impact` 服务; + 若评审期望保持 `ImpactAgent` 纯确定性,可考虑将 `run_impact` 拆分为独立子类,但 Brief 明确要求加在 `ImpactAgent` 上,故未拆分。 + +## Commit + +``` +<待提交> feat(rag): ImpactAgent 接入可选 RAG 上下文(use_rag) +``` + +## 修复后验证(RAG 迭代 Task 4 严重正确性缺陷修复) + +### 缺陷说明 + +原 `run_impact` 把 `self.engine.chat_structured(...)` 当作**同步**调用并直接返回其结果。 +而真实 `InferenceEngine.chat_structured`(src/genesis/inference/engine.py:153)是 **`async def`**, +返回 **`StructuredResult`**。原实现运行时会得到一个**未 await 的协程**,LLM 实际从未执行。 +旧测试通过只是因为 `FakeEngine` 也是同步的(mock 镜像了错误的 sync 形态)——属"测试断言了错误形态"陷阱。 + +### 修复要点 + +- `run_impact` 改为 **`async def`**,引擎调用加 **`await`**:`return await self.engine.chat_structured(...)`, + 返回底层引擎的 `StructuredResult`(含 `data`/`raw_text`)。 +- `tests/test_impact_agent_rag.py` 改写为**异步**形态:`FakeEngine.chat_structured` 改为 + `async def`,测试内用 `asyncio.run(agent.run_impact(...))` 驱动(不依赖 pytest-asyncio 配置)。 +- 扩展为 6 例,覆盖全部分支:`engine is None` / `use_rag is None` 回退实例默认 / + `use_rag` 且 `rag` 存在且 `chunks` 非空 / `chunks` 空(无命中)/ 显式 `use_rag=False`。 + +### 验证输出 + +`python -m pytest tests/test_impact_agent_rag.py -q`(新增 6 例): + +``` +6 passed in 4.92s +``` + +`run_impact` 分支覆盖率(仅统计 124-155 行,独立 `--cov-branch` JSON 复核): +**lines 124-155 无 missing,branches 无 missing(100%)**。 +(文件其余 missing 行/分支均在确定性 `run()` 路径,不在本次修复范围内。) + +回归(既有 impact 测试,确认无破坏): + +``` +python -m pytest tests/ -q -k impact +74 passed, 533 deselected, 1 warning in 50.06s +``` + +> 说明:仓库 `pytest` 配置含 99% 覆盖率门禁,单独跑子集会因覆盖率不足而返回非 0, +> 故修复验证加 `--no-cov`/独立 JSON 仅校验测试本身。 + +### Commit + +``` +fix(rag): run_impact 改为 async 并 await 引擎(StructuredResult) +``` diff --git a/docs/superpowers/plans/rag-task-5-report.md b/docs/superpowers/plans/rag-task-5-report.md new file mode 100644 index 0000000..f2d400f --- /dev/null +++ b/docs/superpowers/plans/rag-task-5-report.md @@ -0,0 +1,52 @@ +# RAG 影响调查接入(Task 5)实现报告 + +日期:2026-08-29 +范式步骤:Agent 实现(RAG 迭代 Task 5,含评审修订 D1/D3) + +## 真实代码要点(先读后改) + +- `src/genesis/server/service.py` + - `GenesisService.__init__`:新增 `rag: ImpactRAG | None = None`、`use_rag: bool = False`、`rag_db_path: str | None = None`、`embedder=None`(默认 `get_embedder("fake")`)。保存 `self.rag`/`self.use_rag`;`rag=None` 为不启用(向后兼容默认关闭)。 + - `upload_file`:当 `file_type == "existing_system"` 且 `self.rag is not None`,解压后 `self.rag.index_dir(session_id, path)`;异常 `try/except` + `_LOGGER.warning` 不阻断上传(D1 上传即索引,scope=session_id)。 + - `run_impact` 改为 `async def run_impact(self, session_id, use_rag=None)`:`eff = self.use_rag if use_rag is None else use_rag`。`eff 且 self.rag is not None` 时走 RAG 路径——`engine` 为空则 `build_inference_engine()`;取 `requirements_text = rec.files["requirements"]["name"] or "要件定義"`;`await ImpactAgent(engine=..., rag=..., use_rag=True).run_impact(...)`;写 `status=awaiting_impact_confirm` + `impact_summary={"rag_enabled": True, "impact_llm": ...}`,并尽量保留 `impact_report_path`。否则走原确定性 `ImpactAgent().run(...)` 路径(行为不变)。 +- `src/genesis/server/app.py` + - `create_app` 新增 `rag=None`;为 None 时内部构造 `RagStore(data_root/rag.db) + ImpactRAG(..., get_embedder("fake"))`(先 `mkdir(parents=True)` 防止 `unable to open database file`),以 `rag=rag, use_rag=False` 注入 `GenesisService`。 + - **Critical 修复**:`_FakeEngine.chat_structured` 改为 `async def`(与真实 `InferenceEngine` 一致)。 + - `start_impact` 改为 `async def start_impact(sid, use_rag=False)`,`await service.run_impact(sid, use_rag=use_rag)`。 +- `src/genesis/chat/agent.py`:`_parse_and_confirm` 与 `_run_impact` 中两处 `self.service.run_impact(...)` 以 `asyncio.run(...)` 包裹(兼容同步消息处理,避免协程未执行)。 +- `tests/test_server_service.py`:两处 `svc.run_impact(...)` 改为 `asyncio.run(svc.run_impact(...))`,保持全绿。 +- `tests/test_impact_rag_e2e.py`(新增,D3 强验证):真实 `GenesisService` + 临时 `SessionStore` + 临时 `data_root` + 异步 `FakeEngine`(捕获 prompt)+ `ImpactRAG(RagStore(tmp/rag.db), FakeEmbedder())`,`use_rag=True`。 + - 辅助:`zipfile` 构造含 `src/KnownOrder.java`(内容含「订单创建调用 MyBatis」)的 existing_system zip;requirements/template 用 `glob` 定位 `sample/` 下真实文件。 + - 流程:create_session → upload requirements → upload template → upload existing_system → run_parse → confirm_parse(→ impact_running)→ `asyncio.run(svc.run_impact(sid, use_rag=True))`。 + - 核心断言:`FakeEngine.captured` 含「KnownOrder」(上传源码被 RAG 检索并注入 LLM prompt);上传后 `rag.retrieve(sid, "订单创建", k=1)` 命中非空(D1 验证);另起会话 `rag=None` 且默认 `use_rag` 时走确定性路径、`captured is None` 且不含 RAG 小节标题(向后兼容)。 + +## 验证状态 + +- `python -m pytest tests/test_impact_rag_e2e.py -q`:**2 passed** +- `python -m pytest tests/test_server_service.py -q`:**20 passed** +- `python -m pytest tests/ -k "server or impact or chat" -q`:**228 passed**(无回归;最初 `test_server_api` 因 `rag.db` 目录不存在报 `OperationalError`,已通过 `Path(rag_db).parent.mkdir(...)` 修复) + +## 自我审查(顾虑) + +- `create_app` 现在总会创建 `data_root/rag.db`(即使 `use_rag=False`);默认 `app = create_app()` 会在工作区生成 `data/server/rag.db` 文件,属轻微副作用,可接受(向后兼容且关闭时不参与影响调查)。 +- `chat/agent.py` 用 `asyncio.run` 包裹 `run_impact` 是最小侵入方案;若未来聊天层整体异步化,可改为直接 `await`。 +- 覆盖配置 `fail_under=99%`,单文件运行 e2e 会因采样不足触发覆盖率告警(非测试失败);以 `-o addopts=""` 运行可获干净结果。 +- RAG 路径不依赖 `_rebuild_source`(仅用 requirements 文件名 + RAG 检索),与 Brief 一致;确定性路径仍保留 `_rebuild_source`。 + +## 修复后验证(复审 Minor 3 项最小化修复) + +日期:2026-08-30 +范式步骤:反馈迭代(RAG 迭代 Task 5 复审 Minor 修复) + +### 修复项 +1. **死存储(service.py)**:移除 `GenesisService.__init__` 的 `rag_db_path` 与 `embedder` 两个未使用参数及其赋值(`rag`/`use_rag` 保留)。已确认全仓无调用方传入这两个参数(仅 `app.create_app` 传 `rag=`)。 +2. **引擎构建竞态(service.py)**:`run_impact` 中引擎懒构建改为双重检查锁:`__init__` 新增 `self._engine_lock = threading.Lock()`,构建处 `with self._engine_lock: if self.engine is None: self.engine = build_inference_engine()`(新增 `import threading`)。 +3. **文档措辞(docs/design.md)**:将"端点同步改为 `async def`"改为"端点改为 `async def`"(原端点本就是同步 def,去掉"同步"歧义)。 + +### 验证命令 +`python -m pytest tests/test_impact_rag_e2e.py tests/test_server_service.py -q` + +### 结果摘要 +- **22 passed**(test_impact_rag_e2e 2 + test_server_service 20),无回归。 +- 注:覆盖率门禁 `fail_under=99%` 在仅运行这两个文件时会触发(项目级全量门禁,与本次修复无关,非测试失败)。按报告既有建议以 `-o addopts=""` 运行可获干净结果。 +- 默认路径语义与 `use_rag=False` 行为保持不变。