Warn about confusing agent tool catalogs
Deploy / deploy (push) Successful in 1m34s
CI / Rust Checks (push) Successful in 5m3s
CI / UI Checks (push) Successful in 5s
CI / Deployment Manifests (push) Successful in 3s
CI / Frontend E2E (push) Successful in 4m44s

This commit is contained in:
github-ops
2026-06-20 21:09:45 +00:00
parent ce773e6196
commit 7099faad2b
6 changed files with 218 additions and 6 deletions
+70
View File
@@ -8,6 +8,7 @@ const GENERIC_TOOL_NAMES: &[&str] = &["call_api", "manage", "execute", "request"
const MULTI_ACTION_FIELD_NAMES: &[&str] = &["action", "mode", "operation", "type"];
const ENUM_LIKE_FIELD_NAMES: &[&str] = &["mode", "type", "status", "category"];
const MANY_PROJECTED_FIELDS_THRESHOLD: usize = 12;
const LARGE_AGENT_TOOL_CATALOG_THRESHOLD: usize = 8;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
@@ -70,6 +71,13 @@ pub struct ToolQualityMappingSet {
pub rules: Vec<ToolQualityMappingRule>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolQualityCatalogTool {
pub name: String,
pub display_name: String,
pub description: String,
}
impl ToolQualityReport {
pub fn new(findings: Vec<ToolQualityFinding>) -> Self {
let blocking = findings
@@ -218,6 +226,37 @@ pub fn analyze_tool_response_projection_quality(
ToolQualityReport::new(findings)
}
pub fn analyze_agent_tool_catalog_quality(tools: &[ToolQualityCatalogTool]) -> ToolQualityReport {
let mut findings = Vec::new();
if tools.len() > LARGE_AGENT_TOOL_CATALOG_THRESHOLD {
findings.push(finding(
ToolQualitySeverity::Warning,
"agent_catalog_too_large",
"В каталоге агента много инструментов.",
Some("Оставьте только инструменты, которые нужны этому агенту для конкретной задачи."),
Some("agent.operations"),
));
}
for (left_index, left) in tools.iter().enumerate() {
for (right_offset, right) in tools[left_index + 1..].iter().enumerate() {
let right_index = left_index + right_offset + 1;
if tools_are_similar(left, right) {
findings.push(finding(
ToolQualitySeverity::Warning,
"agent_catalog_similar_tools",
"У агента есть похожие инструменты.",
Some("Переименуйте инструменты точнее или оставьте один вариант, чтобы модель не выбирала наугад."),
Some(&format!("agent.operations.{left_index},{right_index}")),
));
}
}
}
ToolQualityReport::new(findings)
}
fn is_safe_tool_name(value: &str) -> bool {
let mut chars = value.chars();
let Some(first) = chars.next() else {
@@ -230,6 +269,37 @@ fn is_safe_tool_name(value: &str) -> bool {
chars.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '_')
}
fn tools_are_similar(left: &ToolQualityCatalogTool, right: &ToolQualityCatalogTool) -> bool {
let left_tokens = catalog_tool_tokens(left);
let right_tokens = catalog_tool_tokens(right);
if left_tokens.is_empty() || right_tokens.is_empty() {
return false;
}
let shared = left_tokens
.iter()
.filter(|token| right_tokens.contains(token))
.count();
let smaller = left_tokens.len().min(right_tokens.len());
shared >= 2 && shared * 100 / smaller >= 60
}
fn catalog_tool_tokens(tool: &ToolQualityCatalogTool) -> Vec<String> {
let text = format!("{} {} {}", tool.name, tool.display_name, tool.description);
let mut tokens = Vec::new();
for token in text
.to_lowercase()
.split(|ch: char| !(ch.is_ascii_alphanumeric() || ('а'..='я').contains(&ch) || ch == 'ё'))
{
if token.len() < 3 || tokens.iter().any(|existing| existing == token) {
continue;
}
tokens.push(token.to_owned());
}
tokens
}
fn analyze_schema_node(
path: &str,
schema: &ToolQualitySchemaNode,