diff --git a/services/hermes_analyst_service.py b/services/hermes_analyst_service.py index 8977401..b4be5a7 100644 --- a/services/hermes_analyst_service.py +++ b/services/hermes_analyst_service.py @@ -124,6 +124,7 @@ class HermesAnalystService: caller='hermes_analyst', threshold=0.85, request_id=rid, + mark_saved_call=True, ) if rag.has_high_confidence: logger.info( diff --git a/services/openclaw_strategist_service.py b/services/openclaw_strategist_service.py index 847c24c..f76a669 100644 --- a/services/openclaw_strategist_service.py +++ b/services/openclaw_strategist_service.py @@ -180,6 +180,7 @@ def generate_strategy_response(query: str, context: Optional[Dict[str, Any]] = N rag = rag_service.query( text=q, caller='openclaw_qa', threshold=0.85, request_id=request_id, + mark_saved_call=True, ) if rag.has_high_confidence: logger.info( diff --git a/services/rag_service.py b/services/rag_service.py index cb702a1..be1a608 100644 --- a/services/rag_service.py +++ b/services/rag_service.py @@ -301,6 +301,7 @@ class RAGService: threshold: float = RAG_DEFAULT_THRESHOLD, request_id: Optional[str] = None, insight_type: Optional[str] = None, + mark_saved_call: bool = False, ) -> RAGResult: """執行 RAG 召回。 @@ -311,6 +312,10 @@ class RAGService: threshold: cosine similarity 門檻(0-1,預設 0.85) request_id: 與 ai_calls.request_id 串鏈 insight_type: 限制 ai_insights.insight_type(None = 全類型) + mark_saved_call: + True 時,若本次結果 has_high_confidence,rag_query_log.saved_call + 會寫 True。只給「命中後直接跳過 LLM」的 RAG-first caller 使用, + 觀測台相似案例查詢等輔助用途應保留 False。 Returns: RAGResult。失敗時 hits=[] + duration_ms 仍記錄。 @@ -381,9 +386,9 @@ class RAGService: embedding_signature=signature, hits=hits, threshold=threshold, - saved_call=False, # caller 確認後再設(_call_after_decision API 預留) duration_ms=duration_ms, ) + result.saved_call = bool(mark_saved_call and result.has_high_confidence) # ── 路徑 5:fire-and-forget rag_query_log ── self._async_log( @@ -393,6 +398,7 @@ class RAGService: top_k=top_k, threshold=threshold, hits=hits, + saved_call=result.saved_call, request_id=request_id, ) @@ -548,7 +554,8 @@ class RAGService: top_k: int, threshold: float, hits: List[Dict[str, Any]], - request_id: Optional[str], + request_id: Optional[str] = None, + saved_call: bool = False, ) -> None: """放到 daemon thread 寫入 rag_query_log,主流程不阻塞。 @@ -563,7 +570,7 @@ class RAGService: threading.Thread( target=self._write_log, - args=(caller, text, query_vec, top_k, threshold, hits, request_id), + args=(caller, text, query_vec, top_k, threshold, hits, request_id, saved_call), name=f"rag-query-log-{caller}", daemon=True, ).start() @@ -576,7 +583,8 @@ class RAGService: top_k: int, threshold: float, hits: List[Dict[str, Any]], - request_id: Optional[str], + request_id: Optional[str] = None, + saved_call: bool = False, ) -> None: """try/except 全包;DB 掛了只 log warning 不爆炸。""" try: @@ -625,7 +633,7 @@ class RAGService: 'threshold': round(float(threshold), 3), 'hit_count': len(hits), 'used_results': used_results if used_results else None, - 'saved_call': False, # caller 確認後另寫;INSERT 階段固定 False + 'saved_call': bool(saved_call), 'request_id': (request_id or None), }, ) diff --git a/tests/test_rag_service.py b/tests/test_rag_service.py index dc70381..182af32 100644 --- a/tests/test_rag_service.py +++ b/tests/test_rag_service.py @@ -126,6 +126,44 @@ class TestRagEnabledHits: assert result.has_high_confidence is True assert '本週業績漲' in result.synthesize() + def test_mark_saved_call_only_when_requested_and_confident(self, rag_enabled, monkeypatch): + """RAG-first caller 明確標記時,高信心命中才計入 saved_call。""" + from services import rag_service as rs + + monkeypatch.setattr( + 'services.ollama_service.ollama_service.generate_embedding', + lambda text, model="bge-m3:latest": _fake_embedding(), + ) + monkeypatch.setattr( + rs.rag_service, + '_select_hits', + lambda **_kw: [{'id': 101, 'content': '命中內容', 'score': 0.95}], + ) + captured = {} + + def _capture_async_log(**kwargs): + captured.update(kwargs) + + monkeypatch.setattr(rs.rag_service, '_async_log', _capture_async_log) + + result = rs.rag_service.query( + "本週業績趨勢", + caller='openclaw_qa', + mark_saved_call=True, + ) + + assert result.saved_call is True + assert captured['saved_call'] is True + + result = rs.rag_service.query( + "本週業績趨勢", + caller='admin_quality_trend', + mark_saved_call=False, + ) + + assert result.saved_call is False + assert captured['saved_call'] is False + def test_low_confidence_no_hit(self, rag_enabled, monkeypatch): """所有結果 distance>0.15 → similarity<0.85 → SQL 已過濾,回空 → has_high_confidence False。""" from services import rag_service as rs @@ -292,10 +330,12 @@ class TestFireAndForgetLog: threshold=0.85, hits=[{'id': 101}], request_id='req-1', + saved_call=True, ) assert "embedding_signature" in captured["sql"] assert captured["params"]["embedding_signature"] == rs.get_embedding_signature() + assert captured["params"]["saved_call"] is True fake_session.commit.assert_called_once() def test_embedding_failure_falls_back_to_empty(self, rag_enabled, monkeypatch):