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
+25 -4
View File
@@ -34,10 +34,15 @@ ADMIN = "9003"
AGENT_TYPE = "customer_service"
TOOL_NAME = "search_knowledge"
SUITABILITY_TOOL = "check_suitability"
# 画像只读工具:客服的"出口零"(本人风险等级/投资偏好/测评是否过期)走它取权威字段。
# 那个出口复用的是 `faq` 意图 key(见 `customer_service.PROFILE_WHITELIST_INTENT`),
# 所以**必须**把它加进 `faq` 的白名单里;漏了的表现是"问画像一律转人工",
# 而画像出口本身是对的——这是纯配置缺口(实测复现过)。
PROFILE_TOOL = "query_customer_profile"
# 只有会调用工具的意图才需要白名单;chitchat(模型生成)与 transfer_human(引导人工)
# 都不查知识库。给它们配空白名单反而会掩盖"配置漏配",因此不发布这两条。
INTENT_TOOLS: dict[str, tuple[str, ...]] = {
"faq": (TOOL_NAME,),
"faq": (TOOL_NAME, PROFILE_TOOL),
"product_inquiry": (TOOL_NAME,),
"policy_explain": (TOOL_NAME,),
# 适当性裁决要两步:先从知识库拿到产品的风险等级,再由底座按档案里的客户等级裁决
@@ -208,9 +213,25 @@ async def main() -> int:
for intent, tools in INTENT_TOOLS.items()
]
inherited_keys = {(str(i["namespace"]), str(i["item_key"])) for i in inherited}
# **同 key 的继承项必须被本次新定义覆盖**,不能原样搬过去。
# 血泪教训(实测):上一版发布的是 `faq = ["query_knowledge", ...]`,而那个工具
# 已随"客服检索改为 search_knowledge"从代码上限移除;原样继承会让 admin 服务的
# 子集校验(白名单 ⊆ 代码限定的 allowed_tools)直接 422 拒绝整次发布,
# 报错是"配置超出 Agent 工具上限",看不出是继承造成的。
inherited_only = [
item for item in inherited
if (str(item["namespace"]), str(item["item_key"])) not in {
("agent_tools", f"{AGENT_TYPE}:{intent}") for intent in INTENT_TOOLS
}
]
dropped = [item for item in inherited if item not in inherited_only]
for item in dropped:
print(f" [覆盖] {item['namespace']}/{item['item_key']} 将由本次定义替换"
f"(原值 {item['value_json']})")
pending = [
item for item in new_items
if (str(item["namespace"]), str(item["item_key"])) not in inherited_keys
if (str(item["namespace"]), str(item["item_key"])) not in
{(str(i["namespace"]), str(i["item_key"])) for i in inherited_only}
]
if not pending:
print("客服白名单已存在于当前生效版本,无需发布")
@@ -228,9 +249,9 @@ async def main() -> int:
print(f"\n发布版本 id={release_id}")
base = f"/api/v1/admin/config-releases/{release_id}/platform-config-items"
for item in [*inherited, *pending]:
for item in [*inherited_only, *pending]:
response = await post(client, base, auth=auth, payload=item)
mark = "继承" if item in inherited else "新增"
mark = "继承" if item in inherited_only else "新增"
print(f" [{mark}] {item['namespace']}/{item['item_key']} → {response.status_code}")
if response.status_code != 201:
print(f" 失败:{response.text[:200]}")