跳至正文
来两杯美式
返回

能力目录与协议下发:把接口设计和控制流一起交给 Agent

By 来两杯美式
发布于

上一篇讲了为什么把 Agent Skill 做成 51 行薄壳:业务流程必须留在服务端,客户端只保留「怎么找到流程」。

这个设计要成立,全靠两个 MCP 工具撑起中间的桥:

这一篇拆解这两个工具的接口设计。看起来只是「一个 list 一个 get」,但接口怎么定,直接决定了薄壳能不能保持薄、权限能不能收得住、协议能不能安全下发。

能力目录与协议下发总结:零参数 list 与 protocolTool 控制流下发、目录作为权限投影的两个自动传导、get 的安全细节与 Markdown 协议写作经验

零参数的 list:身份不由客户端声明

先看 yi_list_capabilities 的定义:

server.registerTool(
  "yi_list_capabilities",
  {
    description:
      "列出当前身份可按需加载的领域能力,以及各自适用场景和协议工具。仅返回当前身份有权使用的能力。",
    inputSchema: {},
  },
  handler
);

inputSchema: {}——零参数。这是有意的。

一个容易踩的直觉是把接口设计成 yi_list_capabilities({ userId }) 或者 { role: "admin" }。但只要「我是谁」由客户端传入,就等于把身份声明权交给了最不该拥有它的一方。正确做法是:身份来自 MCP 连接的凭据(API Key 的 scope、OAuth token 的 claim),服务端从认证上下文里取,客户端一个字都不用说。

零参数还有个附带的好处:接口永远不会因为「过滤条件」膨胀。需要什么、能用什么,服务端说了算。

目录条目:把控制流也下发了

看返回结构(节选):

{
  "success": true,
  "capabilities": [
    {
      "id": "work-reporting",
      "name": "工作汇报",
      "description": "记录工作素材,生成日报、周报、月报和工作总结",
      "whenToUse": ["记录工作", "日报", "周报", "月报", "工作总结"],
      "protocolTool": "yi_get_capability_protocol",
      "protocolInput": { "capabilityId": "work-reporting" }
    },
    {
      "id": "user:ck…",
      "name": "",
      "whenToUse": [""],
      "protocolTool": "yi_get_capability_protocol",
      "protocolInput": { "capabilityId": "user:ck…" },
      "isUserDefined": true
    }
  ]
}

逐字段说设计意图:

目录是推导出来的,不是维护出来的

服务端的内置能力注册表长这样:

export const ASSISTANT_CAPABILITIES = [
  {
    id: "work-reporting",
    displayName: "工作汇报",
    description: "记录工作素材,生成日报、周报、月报和工作总结",
    whenToUse: ["记录工作", "日报", "周报", "月报", "工作总结", "生产日报"],
    requiredToolsets: ["work-logs"],
    optionalToolsets: ["automation"],
    protocolKey: "work-logs",
  },
  {
    id: "personal-automation",
    displayName: "个人自动化",
    description: "创建、查询和管理个人提醒、定时任务与自动执行记录",
    whenToUse: ["提醒", "定时", "自动执行", "自动生成", "执行历史"],
    requiredToolsets: ["automation"],
    protocolKey: "automation",
  },
] as const satisfies readonly AssistantCapability[];

注意 requiredToolsets——它既是能力的技术依赖,也是可见性的判定条件

一个能力对当前身份可见,当且仅当它的所有必需工具集都有 read 权限。

权限模型的细节(工具集 × 动作的二维 scope、read < write < admin 的等级比较)我在之前两篇专门写过,这里只讲它和目录的组合效果:

目录不是一份需要维护的清单,而是权限状态在能力维度的投影。

这个「投影」语义带来两个很省心的性质:

  1. 权限收紧自动生效。把某用户的 work-logs read 权限收掉,下次拉目录「工作汇报」能力就消失了——不需要任何「下架」操作。
  2. 依赖下线自动传导。用户自定义能力(user: 前缀)还会校验它引用的平台能力是否全部可用;任何一个依赖被下线或失去授权,这个自定义能力自动从目录里消失。你不需要写「级联下架」的代码,因为目录从来不是存量数据。

用户自定义能力还有一个收窄:下发时只带 workflow 实际引用的平台能力集合。编辑态声明的白名单可以比运行态宽(方便用户增删步骤),但多声明的部分不会下发为运行时依赖——「声明的」和「能用的」是两个集合,后者永远是前者的子集。

get:每次调用都重新校验

yi_get_capability_protocol 的入参就一个 capabilityId,返回结构:

{
  "success": true,
  "capabilityId": "work-reporting",
  "protocolVersion": "4.0.1",
  "updatedAt": "2026-07-21",
  "protocol": "(Markdown 全文)"
}

两个值得说的细节。

第一,鉴权在每次调用时重新做。 不是「会话建立时校验一次、之后默认放行」。权限是会随时间变化的——用户中途被降权、能力被停用,正在进行的会话也必须立刻受到约束。「拉过目录」只代表当时有权,协议下发那一刻服务端再查一次。

第二,错误不区分「不存在」和「无权」。 两种情况返回同一个错误:

{ "success": false, "error": "能力不存在或当前身份无权使用" }

因为一旦区分,攻击者就可以拿错误信息当预言机,枚举探测系统里存在哪些能力 ID。对内部平台这是个过度设计吗?我不这么认为——探测不需要恶意,一个好奇心旺盛的用户让 Agent「把所有 capabilityId 都试一遍」就够了。统一错误是便宜且正确的默认。

一个反直觉的取舍:目录工具不做 scope 限制

按前面「权限投影」的逻辑,一个自然的想法是:这两个目录工具本身也该挂在某个 toolset scope 下面,没授权就连目录都看不到。

我们没有这么做。yi_list_capabilitiesyi_get_capability_protocolget_current_user 一起,对所有已认证会话无条件注册,不受任何业务 toolset scope 约束。代码里的注释写得很直白:避免 scope 配置错误时,Agent 连「我是谁、我有什么」都无法确认。

这是一个可用性与最小权限的取舍:

「身份与能力清单」属于元信息,泄露面小、排障价值高——这类工具放在 scope 体系外面,性价比是划算的。但注意边界:目录不受限,协议内容依然受限——拉取协议时逐能力校验,一层都没少。

协议本体:为什么是 Markdown

协议最终下发的是一份 Markdown 全文。选 Markdown 而不是结构化 JSON,理由只有一个但足够硬:协议的消费者是 LLM,Markdown 就是它的原生指令格式。给人写文档用什么格式,给 LLM 写协议就用什么格式。

看一份真实协议的骨架(工作汇报能力,脱敏节选):

---
protocol_version: 4.0.1
protocol_updated: 2026-07-21
---

# 工作汇报能力协议

本协议由 work-reporting 能力动态下发,覆盖工作素材、报告生成与
工作日志设置。所有定时行为统一由 personal-automation 能力管理;
不要寻找或调用旧的 work_log_schedule_* 工具。

## 工具与用途

| 工具                   | 用途                   |
| ---------------------- | ---------------------- |
| work_logs_record       | 记录一条工作或计划素材 |
| work_logs_list_entries | 查询指定日期素材       |
| work_logs_generate     | 异步生成指定日期日报   |
| work_logs_query        | 查询日报状态与内容     |
| …(共 15 个工具)      |

## 语义检索报告

- 询问历史工作时先调用 work_logs_search……
- 基于命中的片段回答,并标注日期;检索片段不是当前事实,
  不能补写未命中的内容……

## 工作素材规则

- 只记录用户明确提供的事实,不补写或猜测
- 已完成事项 kind="work",后续计划 kind="plan",混合内容拆两次调用
- 修改、删除前先列出素材并让用户确认目标

## 日报与周期报告

- 生成是异步的,随后通过查询工具轮询
- PENDING / POLISHING 表示正在处理,不要重复触发;
  FAILED 时展示原因并询问是否重试;STALE 表示源内容在生成后
  发生变化,须由用户确认后重新生成……

## 设置 / 首次配置

(两段式问询话术、默认 Cron 时间、幂等发布规则……)

写这类协议,我们沉淀了几条经验:

1. 状态机的语义要写给 LLM 看。 PENDINGPOLISHINGFAILEDSTALE 每个状态意味着「该做什么、不该做什么」必须写进协议。工具的返回值能告诉你状态是什么,但只有协议能告诉 Agent 拿到这个状态后的正确行为——比如「POLISHING 时不要重复触发」。这类知识放在工具 description 里太挤,放在协议里正合适。

2. 明确「不做什么」和「做什么」同样重要。 「不要将报告内容写入用户记忆」「不补写或猜测」「不要寻找旧工具」——负面指令是协议的一半。尤其「不要寻找或调用旧的 work_log_schedule_* 工具」这句:工具下线了,但可能存在于 Agent 的记忆或宿主的旧文档里,协议要主动覆盖这些残留。

3. 不把对话状态写进协议,也不让 Agent 存。 协议里有句原则性的话:「只保存投递方式,绝不记录『已询问』『已问过』等对话状态」。状态判断每次从工具查询获得(idempotent read),协议本身保持无状态——这和上一篇「协议不进记忆」是同一个思想的两面。

4. 跨能力引用也走目录。 注意协议里这句:「定时生成……按能力目录加载 personal-automation 协议再操作」。能力 A 的协议需要用到能力 B 时,不把 B 的内容抄进 A,只指个路。这样 B 更新时 A 不需要同步改,能力间保持单向依赖。协议是 Markdown,但能力拓扑不能是随意的网状。

两个实现坑

最后记两个实现层面的坑,都是 Next.js 生态的朋友会撞上的。

坑一:Next.js 生产 bundle 里读不了文件。 协议是 .md 文件,服务端启动时从磁盘读取。开发模式一切正常,生产构建后 import.meta.url 在被 bundle 进 .next 目录的代码里不可靠——按它推导项目根目录会指错地方。解法是从进程工作目录向上逐级探测标志文件(package.jsonprotocols/ 目录)确定真实的项目根。凡是「Next.js + 读磁盘文件」的组合都会遇到,值得单独记一笔。

坑二:协议文件不存在时,宁可不返回。 getCapabilityProtocol(key) 找不到文件时返回 null,上层直接给 Agent 返回失败,绝不回退到 stub、不让 Agent「凭常识继续」。这与上一篇的失败规则呼应:一份缺失的协议,宁可让 Agent 说「暂时不可达」,也不能让它脑补一份。动态下发体系的信任建立在「协议要么是新的、要么没有」之上,没有第三种状态。

小结

这一篇的要点压缩成四句:

  1. list 零参数——身份由凭据决定,不由客户端声明;
  2. 目录条目带 protocolTool + protocolInput——控制流和内容一起下发,薄壳才能保持薄;
  3. 目录是权限的投影,不是维护出来的清单——权限收紧、依赖下线都自动传导;
  4. 协议用 Markdown 写给 LLM,但「不存在」必须就是「不存在」,没有回退。

还有一个贯穿两篇却没正面回答的问题:协议在变、目录在变、工具在变,版本怎么管? 客户端怎么知道该刷新了?下一篇讲这套系统里的四套版本号,以及「把版本管理从客户端挪回服务端」到底意味着什么。

本系列下一篇:《四套版本号:把 Agent Skill 的版本管理挪回服务端之后》


分享这篇文章:
通过邮件分享这篇文章✓ 链接已复制
查看系列全部文章
  1. 01.MCP Server 选型决策树:从语言到审计,六个维度一次说清
  2. 02.当 MCP Server 只是"翻译"现成能力:FastMCP + Mem0 + Redis 的选型逻辑
  3. 03.当 MCP Server 要"嵌进"对外平台:Node.js + 官方 SDK + API Key 的选型逻辑
  4. 04.SSE 已被 MCP 判为 legacy:存量服务怎么迁、新服务怎么选
  5. 05.MCP 权限怎么分级?只读、可操作和高风险动作(附完整源码)
  6. 06.MCP 工具权限码怎么设计?read、write、dangerous 三层模型(附完整源码)
  7. 07.自建 SearXNG + MCP:给 AI 工具接入可控的网页搜索
  8. 08.Agent Skill 是快照,快照会腐烂:我把业务流程全部搬到了服务端
  9. 09.能力目录与协议下发:把接口设计和控制流一起交给 Agent
  10. 10.四套版本号:把 Agent Skill 的版本管理挪回服务端之后
  11. 11.让用户自定义 Agent 能力,而不打开注入的后门
  12. 12.MCP 2026-07-28 改了什么:从有状态会话到无状态核心
  13. 13.Anthropic 发布 Agent Skills:AI Agent 的能力该如何被打包
  14. 14.Agent Skills 设计模式:用文件夹给 Agent 装上专业能力
  15. 15.Agent Skills 最佳实践:从评测、结构拆分到安全审查
  16. 16.MCP Server 升级 2026-07-28:不是换依赖,而是重画协议边界
  17. 17.MCP Client 升级 2026-07-28:服务端能留兼容窗口,我们偏不兼容旧协议
  18. 18.MCP 爆炸之后,Agent 怎么从一堆工具里选对那一个

上一篇
四套版本号:把 Agent Skill 的版本管理挪回服务端之后
下一篇
Agent Skill 是快照,快照会腐烂:我把业务流程全部搬到了服务端