Files

8.4 KiB
Raw Permalink Blame History

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.engineRAG 成员: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_impactself.engine.chat_structured(...) 当作同步调用并直接返回其结果。 而真实 InferenceEngine.chat_structuredsrc/genesis/inference/engine.py:153)是 async def 返回 StructuredResult。原实现运行时会得到一个未 await 的协程,LLM 实际从未执行。 旧测试通过只是因为 FakeEngine 也是同步的(mock 镜像了错误的 sync 形态)——属"测试断言了错误形态"陷阱。

修复要点

  • run_impact 改为 async def,引擎调用加 awaitreturn 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_ragrag 存在且 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