Tool 越接越多以后,Agent 最先坏掉的不是模型,而是“到底谁能用什么”:我用 Spring Boot 做了一个 Capability Registry
Agent 项目刚开始时,工具通常不多。
search_docs
query_order
send_email
三五个 Tool,直接写在代码里也没什么。
半年以后很容易变成:
40 个 REST Tool
12 个 MCP Server
8 个 Skill
5 个内部 Agent
3 套模型
然后配置开始散在:
application.yml- Prompt
- MCP 客户端配置
- 数据库
- Git 仓库
- 各个 Agent 的 Java Bean
- 运维文档
这时候最常见的问题不是“模型不知道怎么选工具”,而是平台自己已经不知道:
这个能力到底还在不在?
谁负责?
谁有权限?
风险多高?
当前版本是什么?
哪个 Agent 正在用?
我越来越倾向在 Agent 平台里加一个单独的:
Capability Registry
它不是 Tool List 的另一个名字。
它是 Tool、Skill、MCP Server、Sub-Agent 和 Workflow 的统一目录。
为什么不用 tools/list 直接当目录
MCP 的 tools/list 很方便。
但它回答的是:
这个 MCP Server 现在暴露哪些 Tool?
企业真正需要回答的是:
整个组织有哪些能力?
哪些 Agent 可以发现?
哪些主体可以调用?
需要哪些 Scope?
有多危险?
谁是 Owner?
能不能进生产?
这是两个完全不同的问题。
比如:
MCP Tool:
delete_service
Registry 中应该变成更完整的能力对象:
{
"capability_id": "cloudrun.service.delete",
"type": "MCP_TOOL",
"provider": "cloudrun-mcp",
"tool_name": "delete_service",
"risk": "CRITICAL",
"approval": "REQUIRED",
"owner": "platform-team",
"status": "PRODUCTION",
"version": "3"
}
模型看到的是 Tool。
平台管理的是 Capability。
我先定义一个最小模型
Spring Boot 里:
public enum CapabilityType {
REST_TOOL,
MCP_TOOL,
SKILL,
SUB_AGENT,
WORKFLOW
}
状态:
public enum CapabilityStatus {
DRAFT,
REVIEW,
PRODUCTION,
DEPRECATED,
DISABLED
}
风险:
public enum RiskLevel {
LOW,
MEDIUM,
HIGH,
CRITICAL
}
核心实体:
@Entity
@Table(name = "agent_capability")
public class CapabilityEntity {
@Id
private String id;
private String name;
@Enumerated(EnumType.STRING)
private CapabilityType type;
@Enumerated(EnumType.STRING)
private CapabilityStatus status;
@Enumerated(EnumType.STRING)
private RiskLevel riskLevel;
private String ownerTeam;
private String version;
private String endpointRef;
private boolean approvalRequired;
private String schemaHash;
private Instant updatedAt;
@Version
private long rowVersion;
}
这里我故意不把 Secret 放进 Registry。
Registry 只存:
能力元数据
Credential 交给独立的 Secret/Credential Broker。
Scope 单独建表
一个 Capability 可能需要多个授权。
@Entity
@Table(name = "capability_scope")
public class CapabilityScopeEntity {
@Id
@GeneratedValue
private Long id;
private String capabilityId;
private String scope;
private boolean required;
}
例如:
cloudrun.service.read
对应:
run.readonly
而:
cloudrun.service.deploy
对应更高权限。
不要让两个 Capability 共用一个过宽 Scope,只因为它们都来自同一个 MCP Server。
Discovery 不是全量返回
最简单的接口:
GET /api/capabilities
如果什么都返回,会很快遇到两个问题:
权限泄露
上下文膨胀
我更愿意设计成:
POST /api/capabilities/discover
请求:
{
"agent_id": "release-agent",
"tenant_id": "team-a",
"purpose": "deploy",
"query": "发布 Cloud Run 服务"
}
返回:
{
"capabilities": [
{
"id": "cloudrun.service.read",
"risk": "LOW"
},
{
"id": "cloudrun.service.deploy",
"risk": "HIGH",
"approval_required": true
}
]
}
Discovery 本身也要经过权限。
Resolver 不应该只靠 Embedding
很多人会做:
Capability Description
→ Embedding
→ TopK
语义检索有用,但不能单独决定“可调用”。
正确顺序:
先做 Policy Filter
↓
再做 Capability Search
↓
再做 Risk Filter
↓
最后返回模型
而不是:
全公司所有 Tool
先向量搜索
因为搜索本身可能把用户没权知道的能力名称暴露出来。
一个 Resolver
@Service
public class CapabilityResolver {
private final CapabilityRepository repository;
private final CapabilityPolicy policy;
private final CapabilitySearch search;
public List discover(
DiscoveryRequest request,
AccessContext access) {
List candidates =
repository.findAllByStatus(
CapabilityStatus.PRODUCTION);
List allowed =
candidates.stream()
.filter(c ->
policy.canDiscover(
access,
request,
c))
.toList();
return search.rank(
request.query(),
allowed)
.stream()
.limit(8)
.map(CapabilityView::from)
.toList();
}
}
这里最重要的是:
policy.canDiscover()
在 Rank 之前。
Discover 和 Invoke 权限要分开
一个用户可以知道:
“公司有生产部署能力”
但不一定能调用。
所以:
public interface CapabilityPolicy {
boolean canDiscover(
AccessContext access,
DiscoveryRequest request,
CapabilityEntity capability);
InvocationDecision canInvoke(
AccessContext access,
InvocationRequest request,
CapabilityEntity capability);
}
这是很重要的两个动作。
Invocation Decision 不只返回 true/false
高风险能力经常是:
可以调用
但需要审批
所以:
public record InvocationDecision(
boolean allowed,
boolean approvalRequired,
Set requiredScopes,
Duration credentialTtl,
int maximumCalls,
List reasons) {
}
例如:
{
"allowed": true,
"approval_required": true,
"required_scopes": [
"cloudrun.write"
],
"credential_ttl": "PT5M",
"maximum_calls": 1
}
Capability Adapter
Registry 不应该关心具体协议细节。
定义统一接口:
public interface CapabilityAdapter {
boolean supports(
CapabilityType type);
InvocationResult invoke(
CapabilityEntity capability,
InvocationContext context,
JsonNode arguments);
}
实现:
RestToolAdapter
McpToolAdapter
SkillAdapter
SubAgentAdapter
WorkflowAdapter
这样上层 Agent 不需要知道:
这个能力来自 MCP 还是 REST
MCP Adapter 只拿短期身份
@Component
public class McpToolAdapter
implements CapabilityAdapter {
private final CredentialBroker credentialBroker;
private final McpClientFactory clientFactory;
@Override
public InvocationResult invoke(
CapabilityEntity capability,
InvocationContext context,
JsonNode arguments) {
TemporaryCredential credential =
credentialBroker.issue(
context.subject(),
capability.id(),
context.requiredScopes(),
context.credentialTtl());
McpClient client =
clientFactory.create(
capability.endpointRef(),
credential);
return client.callTool(
capability.name(),
arguments);
}
}
Registry 从来不返回长期 Key。
版本不能只写一个字符串
Tool Schema 改了以后,旧 Agent 可能立即坏。
至少保存:
Capability Version
Schema Hash
Compatibility
例如:
public record CapabilityVersion(
String capabilityId,
String version,
String schemaHash,
CompatibilityLevel compatibility,
Instant effectiveFrom) {
}
兼容类型:
BACKWARD_COMPATIBLE
BREAKING
Breaking Change 不能直接覆盖生产。
Deprecation 也要进入 Registry
否则 Tool 下线时只能全仓库搜索。
public record DeprecationPolicy(
Instant announcedAt,
Instant disableAt,
String replacementCapabilityId,
String migrationNote) {
}
Agent 在 Discover 阶段就可以得到:
deprecated=true
replacement=...
Registry 最有价值的地方其实是 Usage Graph
如果有 Capability Registry,就可以回答:
谁在用这个 Tool?
建立:
Agent
→ Capability
→ MCP Server
→ Backend
Usage Graph。
例如:
cloudrun.service.deploy
被 7 个 Agent 使用
其中:
release-agent 68%
ops-agent 21%
incident-agent 11%
当要升级 Schema 时,就知道影响范围。
调用 Ledger
每次 Invoke 记录:
public record CapabilityInvocationRecord(
String invocationId,
String runId,
String agentId,
String subjectId,
String tenantId,
String capabilityId,
String capabilityVersion,
RiskLevel risk,
String approvalId,
String resultStatus,
Duration latency,
BigDecimal cost,
Instant startedAt) {
}
这张表后面能做很多事:
- 成本;
- 审计;
- 使用率;
- 故障;
- 回滚;
- 推荐;
- 下线。
不要把描述写成营销文案
Capability Description 是给模型检索和选择的。
差的:
一个强大、智能、灵活的企业云资源管理工具。
这种没有信息。
好的:
读取 Cloud Run 服务当前 Revision、
流量分配和镜像信息。
只读,不修改资源。
模型更容易选对。
Capability Contract
我会强制每个生产能力至少有:
id: cloudrun.service.read
owner: platform-team
type: MCP_TOOL
status: PRODUCTION
risk: LOW
description: >
Read Cloud Run service configuration,
revisions and traffic allocation.
Does not modify resources.
inputs:
schema: cloudrun-read-v2.json
outputs:
schema: cloudrun-service-v3.json
auth:
scopes:
- run.readonly
limits:
timeout: 10s
max_calls_per_run: 5
observability:
audit: true
trace: true
这已经很接近 API Product。
Tool 数量大以后,模型不应该看到 Registry 的全部内容
比如公司有:
800 个 Capability
一股脑塞 Tool Schema 肯定不行。
流程应该是:
Intent
↓
Capability Discovery
↓
Top 5—10
↓
Tool Schema Materialize
↓
LLM Tool Selection
把“发现能力”和“选择具体 Tool”拆两层。
Search 也可以混合
标签
+ BM25
+ Embedding
+ 使用历史
例如评分:
score = (
semantic * 0.45
+ keyword * 0.20
+ agent_affinity * 0.15
+ reliability * 0.10
+ cost_score * 0.10
)
但 Risk/Permission 是过滤条件,不进入加权平均。
Registry 还能解决“同一能力多实现”
例如:
search.customer
有三个 Provider:
crm-primary
warehouse
legacy-crm
Agent 不需要知道全部。
Registry 可以根据:
- 租户;
- 地域;
- 健康;
- 成本;
- 数据新鲜度;
选择实现。
Capability
→ Provider Binding
这比 Prompt 写:
优先调用 crm_primary,失败再……
更容易运维。
Health 也进入 Capability
public record CapabilityHealth(
String capabilityId,
double successRate,
Duration p95,
double errorRate,
Instant measuredAt) {
}
Discover 时:
UNHEALTHY
的能力可以隐藏或降权。
最后,把 Registry 做成 Marketplace 前要先克制
很多平台一说 Registry,马上想做:
Agent Marketplace
先别急。
第一阶段先解决:
可发现
可授权
可版本
可审计
第二阶段再做:
评分
推荐
模板
共享
市场
否则很容易得到一个漂亮首页,背后 Tool 还是没有 Owner 和权限。
我现在判断一个 Agent 平台是否开始成熟,会问一个很简单的问题:
如果生产里某个 Tool 明天必须紧急下线,你能不能在 5 分钟内知道哪些 Agent 受影响,并阻止它继续被发现和调用?
如果答案是:
要全仓库 grep 一下
那其实还没有真正的 Agent 平台。
Capability Registry 就是为了解决这件事。
更多企业级 AI 应用、Agent、RAG 与模型工程化内容,我会继续整理在 智元界:
https://www.zyentor.com/