fix(knowledge): 检索服务适配现库集合 schema、发布对齐的工具白名单、免责声明只由治理层注入

合并暴露的三个真机问题(单测全绿但线上必挂):

1. 检索服务字段名与现库集合不符 -> 静默零召回
   架构师那套按 load_knowledge_milvus.py 的 schema 读 doc_id/content/chapter/section/
   visibility,而现库三集合的真实字段是 knowledge_id/title/snippet/tags/version/intent。
   Milvus 对不存在的字段直接报错 -> 三集合全失败 -> degraded -> 客服一律转人工。
   改法:只改读取侧,用 _FIELD_ALIASES 映射;KnowledgeHit 对外形状不变(下游与测试不动)。
   代价已注明:没有 visibility 字段 -> 检索层内部资料硬隔离失效(现库 356 行均为对外知识)。

2. 发布白名单与代码上限不匹配 -> AGENT_PERMISSION_DENIED
   active 版本白名单是 query_knowledge,而合并后代码上限是 search_knowledge/check_suitability/
   query_customer_profile -> 交集为空 -> 所有知识问题 failed。
   改法:publish_customer_service_config.py 补 PROFILE_TOOL 进 faq 白名单,并让同 key 的
   继承项被本次定义覆盖(旧值原样继承会被子集校验 422 拒掉整次发布)。已激活版本 216。

3. 免责声明重复出现
   Agent 自己拼一句 + 治理层追加权威话术 -> 客户看到两条。改为只由治理层注入
   (话术属发布配置,改文案不该改代码)。风控等内部 Agent 不注入(结构化输出不被污染)。

测试:934 passed / 1 failed(test_fund_readonly_contract 既有空集缺陷,与本线无关)
真机:知识问答两问 succeeded 且只带一条声明;画像问答 succeeded;知识库三端点全绿
This commit is contained in:
qyqy
2026-09-11 15:27:16 +08:00
parent 57c4add7d8
commit 395bad25b7
5 changed files with 142 additions and 71 deletions
@@ -13,14 +13,19 @@
from typing import Any
from app.service.agent.governance import FALLBACK_DISCLAIMER
from app.service.agent.implementations.customer_service import (
DISCLAIMER,
FALLBACK_TEMPLATE,
CustomerServiceAgent,
)
PRODUCT = "南方季季盈90天"
#: 固定免责声明由**治理层**注入(`PlatformGovernance.review` → `review_output`),
#: 业务 Agent 不再自己拼——否则客户会看到两条重复声明(合并时实测复现)。
#: 这里取治理层的代码兜底文案,只用于还原"治理后"的正文形状。
DISCLAIMER = FALLBACK_DISCLAIMER
def _decision(**overrides: Any) -> dict[str, Any]:
base: dict[str, Any] = {
@@ -32,8 +37,8 @@ def _decision(**overrides: Any) -> dict[str, Any]:
def _replied(body: str) -> str:
"""出口交给客户的正文形状:`{正文}\\n{DISCLAIMER}`(四个出口都这样组装)。"""
return f"{body}\n{DISCLAIMER}"
"""出口交给客户的正文形状:`{正文}\\n\\n{免责声明}`(治理层统一追加)。"""
return f"{body}\n\n{DISCLAIMER}"
# ---- 出口一:知识直返(正文 = 知识块 content + DISCLAIMER) ----
@@ -29,13 +29,17 @@ async def _embed(text: str) -> list[float]:
def _row(doc_id: str, title: str, score: float, content: str = "正文") -> dict[str, Any]:
"""构造 pymilvus 的 `{distance, entity}` 行(`search()` 的返回形状)。"""
"""构造 pymilvus 的 `{distance, entity}` 行(`search()` 的返回形状)。
字段名按**现库集合的真实 schema**(`knowledge_id` / `snippet`),不是灌库脚本里那套
(`doc_id` / `content`)——后者在本环境的集合从未建起来过,按它写替身会让测试假绿:
业务代码读取 `snippet` 拿到空串,命中被静默丢弃,而断言又恰好只查 `doc_id`。
"""
return {
"distance": score,
"entity": {
"doc_id": doc_id, "title": title, "content": content,
"source_file": "x.md", "visibility": "public", "doc_no": "",
"version": "", "chapter": "",
"knowledge_id": doc_id, "title": title, "snippet": content,
"tags": "", "version": "", "intent": "",
},
}
@@ -48,9 +52,8 @@ def _flat(doc_id: str, title: str, content: str = "正文") -> dict[str, Any]:
测试红了一次——是测试写错,不是业务代码有问题(业务代码"没有正文就不作答"是对的)。
"""
return {
"doc_id": doc_id, "title": title, "content": content,
"source_file": "x.md", "visibility": "public", "doc_no": "",
"version": "", "chapter": "",
"knowledge_id": doc_id, "title": title, "snippet": content,
"tags": "", "version": "", "intent": "",
}
@@ -73,8 +76,11 @@ class FakeClient:
def query(self, **kwargs: Any) -> Any:
self.query_calls += 1
if "content" not in kwargs.get("output_fields", []):
return [{"doc_id": d, "title": t} for d, t in self._product_titles]
# 业务代码请求的字段名取自 `_FIELD_ALIASES`(现库是 `knowledge_id`/`snippet`):
# 这里按"有没有要正文"区分两轮查询,而不是写死某一套字段名。
output_fields = kwargs.get("output_fields", [])
if "snippet" not in output_fields and "content" not in output_fields:
return [{"knowledge_id": d, "title": t} for d, t in self._product_titles]
pattern = str(kwargs.get("filter") or "")
return [row for doc_id, row in self._product_rows.items() if doc_id in pattern]