feat: 客服对话支持寒暄回复与跑题拒答

问候不再写入槽位;明显无关问题直接拒绝,知识库未命中时明确说明无法回答,并继续合理追问。

Co-authored-by: haijd <haijd@163.com>
This commit is contained in:
Cursor Agent
2026-09-20 05:52:22 +00:00
co-authored by haijd
parent cd67211043
commit 625a6a3a9d
4 changed files with 249 additions and 14 deletions
+62
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
import re
from typing import Any
from .config_loader import business_config
@@ -24,6 +25,67 @@ def retrieve_for_scheme(slots: dict[str, str], bundle: list[str]) -> RetrievalRe
return RetrievalResult([], "ragflow", warning=str(exc))
def retrieve_question(question: str, *, page_size: int = 3, min_score: float = 0.22) -> RetrievalResult:
"""自由问答检索;分数过低视为知识库未命中。"""
backend = (settings.retrieval_backend or "ragflow").lower()
q = (question or "").strip()
if not q:
return RetrievalResult([], backend or "none", warning="empty_query")
if backend == "mock":
return _retrieve_mock_question(q, min_score=min_score)
try:
client = RagflowClient()
dataset_ids = [item.strip() for item in settings.ragflow_dataset_ids.split(",") if item.strip()]
raw = client.retrieve(q, dataset_ids, page_size=page_size)
chunks = []
for index, item in enumerate(raw[:page_size]):
if not isinstance(item, dict):
continue
score = item.get("similarity")
try:
score_f = float(score) if score is not None else 0.0
except (TypeError, ValueError):
score_f = 0.0
if score_f < min_score:
continue
chunks.append(
{
"id": item.get("id") or f"rf-q-{index}",
"title": item.get("document_keyword")
or item.get("document_name")
or item.get("docnm_kwd")
or "知识片段",
"content": item.get("content") or item.get("content_with_weight") or "",
"score": score_f,
}
)
warning = "" if chunks else "知识库未召回到足够相关的内容"
return RetrievalResult(chunks, "ragflow", warning=warning)
except RagflowError as exc:
return RetrievalResult([], "ragflow", warning=str(exc))
def _retrieve_mock_question(question: str, *, min_score: float = 0.22) -> RetrievalResult:
picked: list[dict[str, Any]] = []
for chunk in business_config.chunks:
blob = f"{chunk.get('title', '')} {chunk.get('content', '')} {' '.join(chunk.get('tags') or [])}"
hits = sum(1 for token in re_split_tokens(question) if token and token in blob)
if hits:
score = min(0.9, 0.2 + 0.15 * hits)
if score >= min_score:
item = dict(chunk)
item["score"] = score
picked.append(item)
if len(picked) >= 3:
break
warning = "" if picked else "知识库未召回到足够相关的内容"
return RetrievalResult(picked, "mock", warning=warning)
def re_split_tokens(text: str) -> list[str]:
return [t for t in re.split(r"[\s,,。;;??!!/|]+", text) if len(t) >= 2]
def _retrieve_mock(slots: dict[str, str], bundle: list[str]) -> RetrievalResult:
chunks = business_config.chunks
picked: list[dict[str, Any]] = []