8.4 KiB
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 的初始假设存在重大出入,据此调整落地方式:
-
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等)保持原样未改动,向后兼容。
-
src/genesis/rag/impact_rag.py(真实 RAG 接口)ImpactRAG.__init__(self, store: RagStore, embedder: Embedder)index(self, scope: str, sources: List[Tuple[str, str]]) -> Noneretrieve(self, scope: str, query: str, k: int = 5) -> List[str]- 命中片段格式为
[{name}]\n{piece},因此片段文本天然含文件名(如TradeApplication.java)。
-
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)。
-
src/genesis/rag/embeddings.pyFakeEmbedder().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}"。
- LLM 调用方法名:
- 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)