наблюдаемость: измерять бюджет каталога MCP
This commit is contained in:
@@ -8,7 +8,9 @@ use crank_core::{CacheScope, CoordinationStateStore, CoordinationStateValue};
|
|||||||
use crank_registry::{PostgresRegistry, PublishedAgentTool, RegistryError};
|
use crank_registry::{PostgresRegistry, PublishedAgentTool, RegistryError};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::sync::{Mutex, RwLock};
|
use tokio::sync::{Mutex, RwLock};
|
||||||
use tracing::info;
|
use tracing::{info, warn};
|
||||||
|
|
||||||
|
use crate::manifest::analyze_published_tool_catalog;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct PublishedToolCatalog {
|
pub struct PublishedToolCatalog {
|
||||||
@@ -105,6 +107,7 @@ impl PublishedToolCatalog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some((tools, age)) = self.load_shared_snapshot(workspace_slug, agent_slug).await {
|
if let Some((tools, age)) = self.load_shared_snapshot(workspace_slug, agent_slug).await {
|
||||||
|
log_catalog_analysis(workspace_slug, agent_slug, "shared_cache", &tools);
|
||||||
let mut guard = self.cached.write().await;
|
let mut guard = self.cached.write().await;
|
||||||
guard.insert(
|
guard.insert(
|
||||||
key,
|
key,
|
||||||
@@ -125,6 +128,7 @@ impl PublishedToolCatalog {
|
|||||||
Err(RegistryError::PublishedAgentNotFound { .. }) => Vec::new(),
|
Err(RegistryError::PublishedAgentNotFound { .. }) => Vec::new(),
|
||||||
Err(error) => return Err(error),
|
Err(error) => return Err(error),
|
||||||
};
|
};
|
||||||
|
log_catalog_analysis(workspace_slug, agent_slug, "postgres", &tools);
|
||||||
self.store_shared_snapshot(workspace_slug, agent_slug, &tools)
|
self.store_shared_snapshot(workspace_slug, agent_slug, &tools)
|
||||||
.await;
|
.await;
|
||||||
let mut guard = self.cached.write().await;
|
let mut guard = self.cached.write().await;
|
||||||
@@ -208,6 +212,42 @@ impl PublishedToolCatalog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn log_catalog_analysis(
|
||||||
|
workspace_slug: &str,
|
||||||
|
agent_slug: &str,
|
||||||
|
source: &str,
|
||||||
|
tools: &[PublishedAgentTool],
|
||||||
|
) {
|
||||||
|
let analysis = match analyze_published_tool_catalog(tools) {
|
||||||
|
Ok(analysis) => analysis,
|
||||||
|
Err(error) => {
|
||||||
|
warn!(workspace_slug, agent_slug, source, %error, "published catalog analysis failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let warning_count = analysis
|
||||||
|
.quality
|
||||||
|
.findings
|
||||||
|
.iter()
|
||||||
|
.filter(|finding| finding.severity == crank_core::ToolQualitySeverity::Warning)
|
||||||
|
.count();
|
||||||
|
|
||||||
|
info!(
|
||||||
|
workspace_slug,
|
||||||
|
agent_slug,
|
||||||
|
source,
|
||||||
|
tool_count = analysis.budget.tool_count,
|
||||||
|
serialized_bytes = analysis.budget.serialized_bytes,
|
||||||
|
estimated_context_tokens = analysis.budget.estimated_context_tokens,
|
||||||
|
largest_tool_estimated_context_tokens =
|
||||||
|
analysis.budget.largest_tool_estimated_context_tokens,
|
||||||
|
recommended_context_tokens = analysis.budget.recommended_context_tokens,
|
||||||
|
exceeds_recommended_budget = analysis.budget.exceeds_recommended_budget,
|
||||||
|
catalog_quality_warning_count = warning_count,
|
||||||
|
"published agent catalog analyzed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn now_unix_ms() -> u64 {
|
fn now_unix_ms() -> u64 {
|
||||||
SystemTime::now()
|
SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
use crank_core::{HttpMethod, OperationSafetyClass, OperationSafetyPolicy, Target};
|
use crank_core::{
|
||||||
|
HttpMethod, OperationSafetyClass, OperationSafetyPolicy, Target, ToolCatalogAnalysis,
|
||||||
|
ToolCatalogAnalysisError, ToolQualityCatalogTool, analyze_tool_catalog,
|
||||||
|
};
|
||||||
use crank_registry::PublishedAgentTool;
|
use crank_registry::PublishedAgentTool;
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
@@ -27,6 +30,37 @@ pub fn tool_definitions(tool: &PublishedAgentTool) -> Vec<Value> {
|
|||||||
)]
|
)]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn analyze_published_tool_catalog(
|
||||||
|
tools: &[PublishedAgentTool],
|
||||||
|
) -> Result<ToolCatalogAnalysis, ToolCatalogAnalysisError> {
|
||||||
|
let mut quality_tools = Vec::new();
|
||||||
|
let mut definitions = Vec::new();
|
||||||
|
|
||||||
|
for tool in tools {
|
||||||
|
for definition in tool_definitions(tool) {
|
||||||
|
quality_tools.push(ToolQualityCatalogTool {
|
||||||
|
name: definition_string(&definition, "name")?,
|
||||||
|
display_name: definition_string(&definition, "title")?,
|
||||||
|
description: definition_string(&definition, "description")?,
|
||||||
|
});
|
||||||
|
definitions.push(definition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
analyze_tool_catalog(&quality_tools, &definitions)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn definition_string(
|
||||||
|
definition: &Value,
|
||||||
|
field: &'static str,
|
||||||
|
) -> Result<String, ToolCatalogAnalysisError> {
|
||||||
|
definition
|
||||||
|
.get(field)
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
.ok_or(ToolCatalogAnalysisError::DefinitionFieldMissing(field))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn tool_definition(name: &str, title: &str, description: &str, input_schema: Value) -> Value {
|
pub fn tool_definition(name: &str, title: &str, description: &str, input_schema: Value) -> Value {
|
||||||
json!({
|
json!({
|
||||||
"name": name,
|
"name": name,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use crank_community_mcp::manifest::tool_definitions;
|
use crank_community_mcp::manifest::{analyze_published_tool_catalog, tool_definitions};
|
||||||
use crank_core::{
|
use crank_core::{
|
||||||
ExecutionConfig, HttpMethod, Operation, OperationId, OperationSecurityLevel, OperationStatus,
|
ExecutionConfig, HttpMethod, Operation, OperationId, OperationSecurityLevel, OperationStatus,
|
||||||
Protocol, RestTarget, Target, ToolDescription, WorkspaceId,
|
Protocol, RestTarget, Target, ToolDescription, WorkspaceId,
|
||||||
@@ -57,6 +57,20 @@ fn marks_destructive_tools_as_two_step_confirmation_calls() {
|
|||||||
assert_eq!(definition["inputSchema"]["required"], json!(["base"]));
|
assert_eq!(definition["inputSchema"]["required"], json!(["base"]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn catalog_budget_uses_the_same_definitions_as_tools_list() {
|
||||||
|
let tool = published_tool();
|
||||||
|
let definitions = tool_definitions(&tool);
|
||||||
|
let analysis = analyze_published_tool_catalog(&[tool]).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(analysis.budget.tool_count, definitions.len());
|
||||||
|
assert_eq!(
|
||||||
|
analysis.budget.serialized_bytes,
|
||||||
|
serde_json::to_vec(&definitions[0]).unwrap().len()
|
||||||
|
);
|
||||||
|
assert!(!analysis.budget.exceeds_recommended_budget);
|
||||||
|
}
|
||||||
|
|
||||||
fn published_tool() -> PublishedAgentTool {
|
fn published_tool() -> PublishedAgentTool {
|
||||||
PublishedAgentTool {
|
PublishedAgentTool {
|
||||||
workspace_id: WorkspaceId::new("ws_01"),
|
workspace_id: WorkspaceId::new("ws_01"),
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ pub mod observability;
|
|||||||
pub mod operation;
|
pub mod operation;
|
||||||
pub mod protocol;
|
pub mod protocol;
|
||||||
pub mod secret;
|
pub mod secret;
|
||||||
|
pub mod tool_catalog;
|
||||||
pub mod tool_quality;
|
pub mod tool_quality;
|
||||||
pub mod workspace;
|
pub mod workspace;
|
||||||
|
|
||||||
@@ -50,6 +51,7 @@ pub mod domain {
|
|||||||
};
|
};
|
||||||
pub use crate::protocol::{AuthKind, ExportMode, HttpMethod, Protocol};
|
pub use crate::protocol::{AuthKind, ExportMode, HttpMethod, Protocol};
|
||||||
pub use crate::secret::{Secret, SecretKind, SecretStatus, SecretVersion};
|
pub use crate::secret::{Secret, SecretKind, SecretStatus, SecretVersion};
|
||||||
|
pub use crate::tool_catalog::{ToolCatalogAnalysis, ToolCatalogBudget};
|
||||||
pub use crate::tool_quality::{
|
pub use crate::tool_quality::{
|
||||||
ToolQualityCatalogTool, ToolQualityFinding, ToolQualityMappingRule, ToolQualityMappingSet,
|
ToolQualityCatalogTool, ToolQualityFinding, ToolQualityMappingRule, ToolQualityMappingSet,
|
||||||
ToolQualityReport, ToolQualitySchemaKind, ToolQualitySchemaNode, ToolQualitySeverity,
|
ToolQualityReport, ToolQualitySchemaKind, ToolQualitySchemaNode, ToolQualitySeverity,
|
||||||
@@ -141,6 +143,10 @@ pub use operation::{
|
|||||||
};
|
};
|
||||||
pub use protocol::{AuthKind, ExportMode, HttpMethod, Protocol};
|
pub use protocol::{AuthKind, ExportMode, HttpMethod, Protocol};
|
||||||
pub use secret::{Secret, SecretKind, SecretStatus, SecretVersion};
|
pub use secret::{Secret, SecretKind, SecretStatus, SecretVersion};
|
||||||
|
pub use tool_catalog::{
|
||||||
|
RECOMMENDED_TOOL_CATALOG_CONTEXT_TOKENS, ToolCatalogAnalysis, ToolCatalogAnalysisError,
|
||||||
|
ToolCatalogBudget, analyze_tool_catalog,
|
||||||
|
};
|
||||||
pub use tool_quality::{
|
pub use tool_quality::{
|
||||||
ToolQualityCatalogTool, ToolQualityFinding, ToolQualityMappingRule, ToolQualityMappingSet,
|
ToolQualityCatalogTool, ToolQualityFinding, ToolQualityMappingRule, ToolQualityMappingSet,
|
||||||
ToolQualityReport, ToolQualitySchemaKind, ToolQualitySchemaNode, ToolQualitySeverity,
|
ToolQualityReport, ToolQualitySchemaKind, ToolQualitySchemaNode, ToolQualitySeverity,
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::tool_quality::{
|
||||||
|
ToolQualityCatalogTool, ToolQualityFinding, ToolQualityReport, ToolQualitySeverity,
|
||||||
|
analyze_agent_tool_catalog_quality,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const RECOMMENDED_TOOL_CATALOG_CONTEXT_TOKENS: usize = 4_096;
|
||||||
|
const ESTIMATED_TOKEN_UTF8_BYTES: usize = 3;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct ToolCatalogBudget {
|
||||||
|
pub tool_count: usize,
|
||||||
|
pub serialized_bytes: usize,
|
||||||
|
pub estimated_context_tokens: usize,
|
||||||
|
pub largest_tool_estimated_context_tokens: usize,
|
||||||
|
pub recommended_context_tokens: usize,
|
||||||
|
pub exceeds_recommended_budget: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct ToolCatalogAnalysis {
|
||||||
|
pub budget: ToolCatalogBudget,
|
||||||
|
pub quality: ToolQualityReport,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ToolCatalogAnalysisError {
|
||||||
|
#[error("tool catalog and definitions length mismatch")]
|
||||||
|
DefinitionCountMismatch,
|
||||||
|
#[error("tool catalog definition is missing string field: {0}")]
|
||||||
|
DefinitionFieldMissing(&'static str),
|
||||||
|
#[error("tool catalog definition serialization failed: {0}")]
|
||||||
|
Serialization(#[from] serde_json::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn analyze_tool_catalog(
|
||||||
|
tools: &[ToolQualityCatalogTool],
|
||||||
|
definitions: &[Value],
|
||||||
|
) -> Result<ToolCatalogAnalysis, ToolCatalogAnalysisError> {
|
||||||
|
if tools.len() != definitions.len() {
|
||||||
|
return Err(ToolCatalogAnalysisError::DefinitionCountMismatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut serialized_bytes = 0usize;
|
||||||
|
let mut estimated_context_tokens = 0usize;
|
||||||
|
let mut largest_tool_estimated_context_tokens = 0usize;
|
||||||
|
for definition in definitions {
|
||||||
|
let definition_bytes = serde_json::to_vec(definition)?.len();
|
||||||
|
let definition_tokens = estimate_context_tokens(definition_bytes);
|
||||||
|
serialized_bytes = serialized_bytes.saturating_add(definition_bytes);
|
||||||
|
estimated_context_tokens = estimated_context_tokens.saturating_add(definition_tokens);
|
||||||
|
largest_tool_estimated_context_tokens =
|
||||||
|
largest_tool_estimated_context_tokens.max(definition_tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
let exceeds_recommended_budget =
|
||||||
|
estimated_context_tokens > RECOMMENDED_TOOL_CATALOG_CONTEXT_TOKENS;
|
||||||
|
let budget = ToolCatalogBudget {
|
||||||
|
tool_count: tools.len(),
|
||||||
|
serialized_bytes,
|
||||||
|
estimated_context_tokens,
|
||||||
|
largest_tool_estimated_context_tokens,
|
||||||
|
recommended_context_tokens: RECOMMENDED_TOOL_CATALOG_CONTEXT_TOKENS,
|
||||||
|
exceeds_recommended_budget,
|
||||||
|
};
|
||||||
|
let mut quality = analyze_agent_tool_catalog_quality(tools);
|
||||||
|
if exceeds_recommended_budget {
|
||||||
|
quality.findings.push(ToolQualityFinding {
|
||||||
|
severity: ToolQualitySeverity::Warning,
|
||||||
|
code: "agent_catalog_context_budget_high".to_owned(),
|
||||||
|
message: "Каталог инструментов занимает слишком много контекста модели.".to_owned(),
|
||||||
|
suggested_action: Some(
|
||||||
|
"Сократите описания и схемы либо разделите инструменты между специализированными агентами."
|
||||||
|
.to_owned(),
|
||||||
|
),
|
||||||
|
field_path: Some("agent.operations".to_owned()),
|
||||||
|
});
|
||||||
|
quality = ToolQualityReport::new(quality.findings);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ToolCatalogAnalysis { budget, quality })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn estimate_context_tokens(serialized_bytes: usize) -> usize {
|
||||||
|
serialized_bytes.div_ceil(ESTIMATED_TOKEN_UTF8_BYTES)
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
mod unit {
|
mod unit {
|
||||||
|
mod tool_catalog;
|
||||||
mod tool_quality;
|
mod tool_quality;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
use crank_core::{
|
||||||
|
ToolCatalogAnalysis, ToolQualityCatalogTool, ToolQualitySeverity, analyze_tool_catalog,
|
||||||
|
};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn measures_the_actual_serialized_catalog_definition() {
|
||||||
|
let definitions = vec![json!({
|
||||||
|
"name": "get_exchange_rate",
|
||||||
|
"title": "Получить курс",
|
||||||
|
"description": "Возвращает актуальный курс выбранной валютной пары.",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"base": {"type": "string"},
|
||||||
|
"quote": {"type": "string"}
|
||||||
|
},
|
||||||
|
"required": ["base", "quote"]
|
||||||
|
}
|
||||||
|
})];
|
||||||
|
|
||||||
|
let analysis = analyze_tool_catalog(&[tool("get_exchange_rate")], &definitions).unwrap();
|
||||||
|
let expected_bytes = serde_json::to_vec(&definitions[0]).unwrap().len();
|
||||||
|
|
||||||
|
assert_eq!(analysis.budget.tool_count, 1);
|
||||||
|
assert_eq!(analysis.budget.serialized_bytes, expected_bytes);
|
||||||
|
assert!(analysis.budget.estimated_context_tokens > 0);
|
||||||
|
assert_eq!(
|
||||||
|
analysis.budget.largest_tool_estimated_context_tokens,
|
||||||
|
analysis.budget.estimated_context_tokens
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn warns_when_actual_catalog_exceeds_context_budget() {
|
||||||
|
let tools = (0..9)
|
||||||
|
.map(|index| tool(&format!("large_tool_{index}")))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let definitions = (0..9)
|
||||||
|
.map(|index| {
|
||||||
|
json!({
|
||||||
|
"name": format!("large_tool_{index}"),
|
||||||
|
"description": "x".repeat(1_500),
|
||||||
|
"inputSchema": {"type": "object", "properties": {}}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let analysis = analyze_tool_catalog(&tools, &definitions).unwrap();
|
||||||
|
|
||||||
|
assert!(analysis.budget.exceeds_recommended_budget);
|
||||||
|
assert!(has_warning(&analysis, "agent_catalog_context_budget_high"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_mismatch_between_catalog_tools_and_definitions() {
|
||||||
|
let error = analyze_tool_catalog(&[tool("one")], &[]).unwrap_err();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
error.to_string(),
|
||||||
|
"tool catalog and definitions length mismatch"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tool(name: &str) -> ToolQualityCatalogTool {
|
||||||
|
ToolQualityCatalogTool {
|
||||||
|
name: name.to_owned(),
|
||||||
|
display_name: name.to_owned(),
|
||||||
|
description: format!("Инструмент {name} выполняет одну конкретную операцию."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_warning(analysis: &ToolCatalogAnalysis, code: &str) -> bool {
|
||||||
|
analysis
|
||||||
|
.quality
|
||||||
|
.findings
|
||||||
|
.iter()
|
||||||
|
.any(|finding| finding.code == code && finding.severity == ToolQualitySeverity::Warning)
|
||||||
|
}
|
||||||
@@ -228,6 +228,17 @@ MCP-клиент видит только опубликованные опера
|
|||||||
|
|
||||||
Черновики операций не попадают в MCP-каталог. Если два пользователя работают в одном workspace, один может редактировать черновик, а второй публиковать агента. В опубликованный каталог попадут только опубликованные версии операций.
|
Черновики операций не попадают в MCP-каталог. Если два пользователя работают в одном workspace, один может редактировать черновик, а второй публиковать агента. В опубликованный каталог попадут только опубликованные версии операций.
|
||||||
|
|
||||||
|
При каждом обновлении каталога Crank анализирует фактические определения `tools/list` и записывает в структурированный журнал:
|
||||||
|
|
||||||
|
- число инструментов;
|
||||||
|
- размер компактного JSON в байтах;
|
||||||
|
- оценочный объём контекста в токенах;
|
||||||
|
- размер крупнейшего инструмента;
|
||||||
|
- рекомендуемый предел и признак его превышения;
|
||||||
|
- число предупреждений качества каталога.
|
||||||
|
|
||||||
|
Оценка токенов равна округлённому вверх отношению размера UTF-8 к трём. Она нужна для стабильного сравнения ревизий каталога и не заменяет точный токенизатор конкретной модели.
|
||||||
|
|
||||||
## Обновление каталога
|
## Обновление каталога
|
||||||
|
|
||||||
`mcp-server` периодически обновляет опубликованный каталог. Интервал задается:
|
`mcp-server` периодически обновляет опубликованный каталог. Интервал задается:
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ Crank сохраняет данные о тестовых запусках и в
|
|||||||
- краткий preview запроса и ответа;
|
- краткий preview запроса и ответа;
|
||||||
- категория ошибки, если вызов завершился ошибкой.
|
- категория ошибки, если вызов завершился ошибкой.
|
||||||
|
|
||||||
|
При обновлении опубликованного MCP-каталога отдельное событие `published agent catalog analyzed` содержит `tool_count`, `serialized_bytes`, `estimated_context_tokens`, `largest_tool_estimated_context_tokens`, `recommended_context_tokens`, `exceeds_recommended_budget` и число предупреждений качества. По этим полям можно заметить рост цены `tools/list` до того, как он ухудшит выбор инструментов моделью.
|
||||||
|
|
||||||
## Использование
|
## Использование
|
||||||
|
|
||||||
Раздел использования агрегирует:
|
Раздел использования агрегирует:
|
||||||
@@ -31,4 +33,3 @@ Crank сохраняет данные о тестовых запусках и в
|
|||||||
- увидеть ошибки маппинга или внешнего API;
|
- увидеть ошибки маппинга или внешнего API;
|
||||||
- найти медленные endpoint-ы;
|
- найти медленные endpoint-ы;
|
||||||
- понять, какие инструменты реально используются.
|
- понять, какие инструменты реально используются.
|
||||||
|
|
||||||
|
|||||||
@@ -154,6 +154,10 @@ Crank возвращает структурированные ошибки. MCP-
|
|||||||
|
|
||||||
Если у агента слишком много похожих инструментов, модель чаще ошибается при выборе.
|
Если у агента слишком много похожих инструментов, модель чаще ошибается при выборе.
|
||||||
|
|
||||||
|
Crank измеряет опубликованный каталог по тому же компактному JSON, который возвращается в `tools/list`. В расчёт входят имя, заголовок, описание, входная JSON-схема и добавляемое Crank описание подтверждения опасной операции. Для сравнения используется независимая от конкретной модели консервативная оценка: один токен на три байта UTF-8. Это не счётчик токенов конкретного поставщика, а стабильная инженерная метрика для поиска регрессий.
|
||||||
|
|
||||||
|
Рекомендуемый бюджет одного агентского каталога — не более 4096 оценочных токенов. Превышение не блокирует публикацию, потому что допустимый объём зависит от модели, но создаёт предупреждение. Сначала сокращайте лишние описания и схемы. Если инструменты решают разные задачи, разделяйте их между специализированными агентами. Выбор нужного агента и постепенное раскрытие каталогов выполняет оркестратор Drivetrain, а не Crank.
|
||||||
|
|
||||||
## Проверочный список
|
## Проверочный список
|
||||||
|
|
||||||
- Имя инструмента конкретное и не похоже на `call_api`.
|
- Имя инструмента конкретное и не похоже на `call_api`.
|
||||||
@@ -164,3 +168,4 @@ Crank возвращает структурированные ошибки. MCP-
|
|||||||
- Для POST/PATCH задан idempotency key, если повторный вызов может создать дубль.
|
- Для POST/PATCH задан idempotency key, если повторный вызов может создать дубль.
|
||||||
- Для DELETE пользователь видит двухшаговое подтверждение.
|
- Для DELETE пользователь видит двухшаговое подтверждение.
|
||||||
- Агенту привязаны только инструменты, нужные для его задачи.
|
- Агенту привязаны только инструменты, нужные для его задачи.
|
||||||
|
- Опубликованный каталог укладывается в выбранный бюджет контекста модели.
|
||||||
|
|||||||
Reference in New Issue
Block a user