feat(rag): ImpactAgent 接入可选 RAG 上下文(use_rag)
This commit is contained in:
@@ -84,8 +84,78 @@ def _header_index(headers: list[str], *keywords: str) -> int | None:
|
||||
return None
|
||||
|
||||
|
||||
# RAG 检索命中片段注入到 prompt 的明确小节标题(向后兼容:use_rag=False 时不出现)
|
||||
_RAG_CONTEXT_TITLE = "# 既有系统关联上下文(RAG 检索,辅助判断影响范围)"
|
||||
|
||||
|
||||
class ImpactAgent:
|
||||
"""变更点定位 → 影响调查书(MVP)。"""
|
||||
"""变更点定位 → 影响调查书(MVP)。
|
||||
|
||||
MVP 的确定性规则路径由 ``run`` 提供(无 LLM 参与)。
|
||||
另提供 LLM 驱动的 ``run_impact``,可接入可选 RAG 上下文辅助判断影响范围。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
engine=None,
|
||||
use_rag: bool = False,
|
||||
rag: "ImpactRAG | None" = None,
|
||||
) -> None:
|
||||
"""初始化(向后兼容:无参 ``ImpactAgent()`` 仍可用)。
|
||||
|
||||
- engine: LLM 引擎(InferenceEngine 兼容接口,提供 chat_structured)。
|
||||
- use_rag: 实例级默认是否启用 RAG 上下文注入;``run_impact`` 可用显参覆盖。
|
||||
- rag: 可选 ImpactRAG 检索器(scope=session_id)。
|
||||
"""
|
||||
self.engine = engine
|
||||
self.use_rag = use_rag
|
||||
self.rag = rag
|
||||
|
||||
def _build_impact_prompt(self, requirements_text: str) -> str:
|
||||
"""拼装发送给 LLM 的基础 prompt(不含 RAG 上下文)。"""
|
||||
return (
|
||||
"你是一名变更影响分析专家。请基于以下要件变更说明,判断本次变更的影响范围"
|
||||
"(涉及的既有機能/画面/DB/IF/バッチ,以及需要修改或回归验证的对象),"
|
||||
"并说明判断依据。\n\n"
|
||||
"# 要件变更说明\n"
|
||||
f"{requirements_text}\n"
|
||||
)
|
||||
|
||||
def run_impact(
|
||||
self,
|
||||
session_id: str,
|
||||
requirements_text: str,
|
||||
use_rag: bool | None = None,
|
||||
k: int = 5,
|
||||
):
|
||||
"""LLM 驱动的变更影响分析(可选 RAG 上下文注入)。
|
||||
|
||||
- use_rag 优先取显参;为 None 时回退到实例级 ``self.use_rag``。
|
||||
- 启用且 ``self.rag`` 存在时,以 ``影响调查:`` + 要件前若干字 为查询,
|
||||
调用 ``self.rag.retrieve(session_id, query, k)``,将命中片段注入 prompt。
|
||||
- use_rag=False 时 prompt 内容与原版完全一致(不含 RAG 小节,向后兼容)。
|
||||
"""
|
||||
if self.engine is None:
|
||||
raise RuntimeError("run_impact 需要 engine,请在构造 ImpactAgent 时传入")
|
||||
|
||||
if use_rag is None:
|
||||
use_rag = self.use_rag
|
||||
|
||||
prompt = self._build_impact_prompt(requirements_text)
|
||||
|
||||
if use_rag and self.rag is not None:
|
||||
query = "影响调查:" + requirements_text[:200]
|
||||
chunks = self.rag.retrieve(session_id, query, k)
|
||||
if chunks:
|
||||
rag_context = "\n".join(chunks)
|
||||
prompt = prompt + f"\n\n{_RAG_CONTEXT_TITLE}\n{rag_context}"
|
||||
|
||||
return self.engine.chat_structured(
|
||||
session_id=session_id,
|
||||
prompt=prompt,
|
||||
variables={},
|
||||
schema={},
|
||||
)
|
||||
|
||||
def run(
|
||||
self,
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
"""ImpactAgent RAG 上下文注入测试(RAG 迭代 Task 4)。
|
||||
|
||||
验证:
|
||||
- use_rag=True 时,run_impact 发送给 LLM 的 prompt 文本包含 RAG 检索命中片段与明确小节标题。
|
||||
- use_rag=False(或默认)时,prompt 文本不含 RAG 小节标题(向后兼容)。
|
||||
"""
|
||||
from genesis.impact.impact_agent import ImpactAgent
|
||||
from genesis.rag.embeddings import FakeEmbedder
|
||||
from genesis.rag.impact_rag import ImpactRAG
|
||||
from genesis.rag.store import RagStore
|
||||
|
||||
|
||||
_RAG_SECTION_TITLE = "# 既有系统关联上下文(RAG 检索,辅助判断影响范围)"
|
||||
|
||||
|
||||
class FakeEngine:
|
||||
"""捕获真实 LLM 方法(chat_structured)收到的 prompt 文本。
|
||||
|
||||
方法名与签名刻意复用本仓库 InferenceEngine.chat_structured 的形参风格,
|
||||
以保证 mock 的是真实接口(key=session_id/prompt/variables/schema/retry_count)。
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.last_prompt: str | None = None
|
||||
self.calls = 0
|
||||
|
||||
def chat_structured(self, *, session_id, prompt, variables, schema, retry_count=2):
|
||||
self.last_prompt = prompt
|
||||
self.calls += 1
|
||||
# 返回结构兼容 ChatResult 的最小占位(测试仅校验 prompt 注入)
|
||||
return {"session_id": session_id, "prompt": prompt}
|
||||
|
||||
|
||||
def _make_rag(session_id: str, sources):
|
||||
store = RagStore(":memory:")
|
||||
rag = ImpactRAG(store, FakeEmbedder())
|
||||
rag.index(session_id, sources)
|
||||
return store, rag
|
||||
|
||||
|
||||
def test_run_impact_with_rag_injects_context():
|
||||
session_id = "sess-rag"
|
||||
store, rag = _make_rag(session_id, [("TradeApplication.java", "订单创建调用 MyBatis")])
|
||||
try:
|
||||
engine = FakeEngine()
|
||||
agent = ImpactAgent(engine=engine, rag=rag, use_rag=True)
|
||||
agent.run_impact(session_id, requirements_text="创建订单的影响", k=5)
|
||||
prompt = engine.last_prompt
|
||||
assert prompt is not None
|
||||
# 命中片段(含文件名 TradeApplication.java)被注入
|
||||
assert "TradeApplication" in prompt
|
||||
# 明确小节标题被注入
|
||||
assert _RAG_SECTION_TITLE in prompt
|
||||
finally:
|
||||
store.close()
|
||||
|
||||
|
||||
def test_run_impact_without_rag_no_context():
|
||||
session_id = "sess-no-rag"
|
||||
store, rag = _make_rag(session_id, [("TradeApplication.java", "订单创建调用 MyBatis")])
|
||||
try:
|
||||
engine = FakeEngine()
|
||||
agent = ImpactAgent(engine=engine, rag=rag, use_rag=False)
|
||||
agent.run_impact(session_id, requirements_text="创建订单的影响", k=5)
|
||||
prompt = engine.last_prompt
|
||||
# 显式关闭 RAG:不含小节标题,也不含检索片段
|
||||
assert _RAG_SECTION_TITLE not in prompt
|
||||
assert "TradeApplication" not in prompt
|
||||
finally:
|
||||
store.close()
|
||||
|
||||
|
||||
def test_run_impact_default_no_rag_no_context():
|
||||
# 默认 use_rag 为 False(未显式开启),行为与关闭一致
|
||||
session_id = "sess-default"
|
||||
store, rag = _make_rag(session_id, [("TradeApplication.java", "订单创建调用 MyBatis")])
|
||||
try:
|
||||
engine = FakeEngine()
|
||||
agent = ImpactAgent(engine=engine, rag=rag) # 不传 use_rag
|
||||
agent.run_impact(session_id, requirements_text="创建订单的影响", k=5)
|
||||
prompt = engine.last_prompt
|
||||
assert _RAG_SECTION_TITLE not in prompt
|
||||
assert "TradeApplication" not in prompt
|
||||
finally:
|
||||
store.close()
|
||||
Reference in New Issue
Block a user