Files
2026Technology-Competition/docs/superpowers/plans/rag-task-4-report.md
T

149 lines
8.4 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 迭代 Task 4ImpactAgent 接入可选 RAG 上下文
## 状态
- 状态:**DONEGREEN**
- 新增/修改文件:
- `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:先写失败测试(RED3 failed)→ 实现 → GREEN3 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 无 missingbranches 无 missing100%**
(文件其余 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
```