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/