fix(platform): 让"工具用不了"的三种原因可区分,并消掉选端点的隐式顺序依赖
基座层面的两处缺陷,都属于"静默失败"——排查成本高,且本项目已经各踩过一次。
1. tool_executor.py 的拒绝原因原先无法区分:
- "意图压根没发布白名单"与"白名单里没这个工具"共用一句「工具不在当前意图白名单」,
运维不知道该去补发布配置、还是改白名单内容(客服与风控的意图码都要求三处对齐,
两次都因此多花排查时间);
- 权限与角色两处只说「缺少工具权限」,不说是哪一个。
现在四种情况各有独立 message,各自指向不同的处置动作。
同时把**审计与异常分离**:白名单内容、权限码、角色集属于内部配置,只写进审计;
异常 message 会随 API 响应返回给调用方,保持通用、不泄漏配置。
2. model_gateway.py 的 TASK_CAPABILITY 补齐风控的几处 task_type
(risk_agent_chat / risk_analysis / risk_script / risk_summary / daily_report_suggestion)。
它们要的都是文本生成端点;不登记就会落到"未映射 → 返回全部 active 端点"的分支,
而能否选对端点取决于 model_endpoint_config 的**行顺序**——实测风控能跑通,仅仅因为
deepseek-flash(id=3) 恰好排在 qwen-embedding(id=5) 前面。这个隐式依赖现在消掉了。
未登记的 task_type 仍退回全部端点(保持原有保守策略:让故障表现为调用失败而不是
解析为空),但会记 warning,不再静默。
新增 tests/unit/service/test_tool_executor_denials.py(4 条),锁住"四种拒绝可区分"
与"内部细节只进审计、不进 message"。
ruff / mypy(135 文件) / 607 unit+contract 全绿。
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import logging
|
||||
import os
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
@@ -167,11 +168,22 @@ class DatabaseModelGateway:
|
||||
# 任何端点的能力名(deepseek 声明的是 text_generation/json_output/intent_classification),
|
||||
# 它需要的是「能生成结构化文本」的端点。若按同名筛选会得到空集,把记忆抽取打成
|
||||
# 失败关闭——这是修复端点筛选时最容易引入的回归。
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
TASK_CAPABILITY: dict[str, str] = {
|
||||
"embedding": "embedding",
|
||||
"intent_classification": "intent_classification",
|
||||
"memory_extraction": "text_generation",
|
||||
"text_generation": "text_generation",
|
||||
# 风控的几处 task_type:它们要的都是"能生成文本"的端点,与 memory_extraction 同理。
|
||||
# 不登记就会落到下面"未映射 → 返回全部端点"的分支,而能否选到文本端点就取决于
|
||||
# `model_endpoint_config` 的**行顺序**——实测风控能跑通,仅仅因为 deepseek-flash(id=3)
|
||||
# 恰好排在 qwen-embedding(id=5) 前面。这种"靠数据顺序才对"的隐式依赖必须消掉。
|
||||
"risk_agent_chat": "text_generation",
|
||||
"risk_analysis": "text_generation",
|
||||
"risk_script": "text_generation",
|
||||
"risk_summary": "text_generation",
|
||||
"daily_report_suggestion": "text_generation",
|
||||
}
|
||||
|
||||
|
||||
@@ -195,6 +207,14 @@ class DatabaseModelEndpointResolver:
|
||||
)))
|
||||
capability = TASK_CAPABILITY.get(task_type)
|
||||
if capability is None:
|
||||
# 未登记的 task_type 仍退回全部端点(保持原有保守策略:让故障表现为调用失败、
|
||||
# 而不是解析为空),但必须留下痕迹。静默退回会让"选端点靠表行顺序"这类问题
|
||||
# 在下游以"偶发调用失败"的形式冒出来,极难定位。
|
||||
logger.warning(
|
||||
"模型端点筛选:task_type=%r 未登记能力映射,退回全部 active 端点;"
|
||||
"请在 TASK_CAPABILITY 中补上它对应的能力",
|
||||
task_type,
|
||||
)
|
||||
return endpoints
|
||||
matched = [
|
||||
endpoint for endpoint in endpoints
|
||||
|
||||
@@ -65,16 +65,39 @@ class ToolExecutor:
|
||||
configured_tools: dict[str, tuple[str, ...]], context: RequestContext,
|
||||
) -> ToolExecution:
|
||||
definition = self.registry.get(name)
|
||||
allowed = configured_tools.get(intent, ())
|
||||
reason = None
|
||||
if name not in allowed:
|
||||
# 把三种"用不了"分开,并且**审计写详细、异常给通用**:
|
||||
#
|
||||
# 原先前两种情况共用一句"工具不在当前意图白名单",运维无法判断该去补发布配置、
|
||||
# 还是该改白名单内容——本项目已经因此踩坑两次(客服、风控的意图码都要求三处对齐,
|
||||
# 而缺配置时是静默失败关闭)。权限与角色两处也只说"缺少工具权限",不说是哪一个。
|
||||
#
|
||||
# 细节只进审计:异常 message 会随 API 响应返回给调用方,
|
||||
# 白名单内容、权限码、角色集属于内部配置,不该出现在客户可见的响应里。
|
||||
reason = ""
|
||||
detail = ""
|
||||
if intent not in configured_tools:
|
||||
reason = "该意图未配置工具白名单"
|
||||
detail = (
|
||||
f"意图 {intent!r} 在发布版本里没有任何工具白名单"
|
||||
f"(agent_tools / <agent_type>:{intent}),工具失败关闭"
|
||||
)
|
||||
elif name not in configured_tools[intent]:
|
||||
reason = "工具不在当前意图白名单"
|
||||
detail = (
|
||||
f"工具 {name!r} 不在意图 {intent!r} 的白名单 "
|
||||
f"{list(configured_tools[intent])} 内"
|
||||
)
|
||||
elif definition.required_permission not in context.permissions:
|
||||
reason = "缺少工具权限"
|
||||
detail = f"缺少权限 {definition.required_permission!r}"
|
||||
elif not set(definition.allowed_roles).intersection(context.roles):
|
||||
reason = "角色不能使用工具"
|
||||
detail = (
|
||||
f"角色 {list(context.roles)} 与工具允许的角色 "
|
||||
f"{list(definition.allowed_roles)} 无交集"
|
||||
)
|
||||
if reason:
|
||||
await self._audit(name, intent, context, "denied", reason)
|
||||
await self._audit(name, intent, context, "denied", detail)
|
||||
raise ForbiddenAgentError(reason)
|
||||
try:
|
||||
validated = definition.input_model.model_validate(arguments)
|
||||
|
||||
Reference in New Issue
Block a user