149 lines
8.4 KiB
Markdown
149 lines
8.4 KiB
Markdown
# 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)
|
||
```
|