implemented(除 4 篇超长文档随后补)、proposed、rejected 全树配对; 同一流水线 + 二遍校验(paraphrase-back + 仓库上下文一致性)产出。 docs/rfc/implemented/AGENTS.md 与其 CLAUDE.md 符号链接列入排除 (agent 指令文件,与根 AGENTS.md 同策略)。
7.4 KiB
RFC:Skill 系统——面向 agent 的渐进式指令披露
Status: implemented
English | 中文
问题
各 agent 产品已趋同于一种 skill 模式:保持请求提示词精简,仅列出可用的指令包,待模型判定任务匹配时再加载完整正文。Codex、Claude Code、OpenCode 和 Kimi Code 在细节上各有不同,但都将发现元数据与完整指令分离,使工作区能承载可复用行为而无需在每个轮次支付全量提示词成本。
DeepSeek Harness 使用同一原语,让项目级的评审指导、插件编写指导和工具使用指导存放在工作区或用户的 agent 配置旁,而非硬编码进 agent loop(智能体循环)。
决策
@deepseek-ai/dsh-skill 是纯提供方注册表(ctx.skills),@deepseek-ai/dsh-skill-local 是随附的本地文件系统提供方,@deepseek-ai/dsh-tool-skill 负责会话前缀目录和面向模型的 loader 工具。dsh-agent-spine-demo 默认加载注册表、本地提供方和消费方,使 stdio 与 ACP 应用获得相同行为,同时嵌入式或远程提供方可在不改动注册表或消费方的前提下贡献 skill。其 skills 配置将 registry、local 和 tool 分支分别转发给对应的负责方。
提供方插件在 apply() 期间同步注册。提供方成员关系是直接由 effect 持有的状态:注册与 dispose(资源释放)同步地使已完成的目录失效,发现操作按需读取当前提供方映射,而非监听注册表变更事件。提供方目录从 awaited list() 调用返回排序后的候选项,远程提供方在此期间执行初始化、认证和发现,同时遵守查找的 abort signal。注册表校验每个候选项,对同名 skill 按 rank、提供方注册顺序和提供方内部顺序执行 first-wins 解析,然后按 skill 名称排序摘要以保证消费方获得确定性结果。注册表仅缓存已完成的目录快照,当提供方/运行时修订版本在发现过程中发生变化时重试,因此 unload 不会将一个陈旧、不可解析的 skill 冻结进会话前缀。运行时 ctx.skills.register(...) 仍作为嵌入式进程内 skill 的便捷方式保留,使用 project-over-user 优先级;runtime 作为注册表持有的提供方名称被保留。
本地提供方按 first-wins 的 rank 顺序扫描对 cwd 敏感的项目根目录、自定义根目录和用户根目录:项目 .dsh、项目 .agents、customSkillDirs、用户 .dsh,然后是用户 .agents。用户 .dsh/skills 扫描跳过 .system,使系统持有的目录不被当作普通用户内容。DeepSeek Harness 不随附内置系统 skill;嵌入式或远程提供方在配置后提供额外 skill。
每个 skill 是 <name>/SKILL.md 或带 YAML frontmatter 的 <name>.md。name 和 description 为必填;whenToUse、disableModelInvocation 和 metadata 为可选。名称使用 kebab-case。YAML frontmatter 使用 yaml 包解析,而非 js-yaml 或手写解析器:yaml 是本包有限 frontmatter 需求所声明的现代解析器,手写窄解析器要么拒绝用户期望能正常工作的合法 YAML,要么膨胀为一个未经评审的 YAML 子集。
本地 skill 的文件系统 I/O 在加载了文件系统服务时通过 ctx.fs 进行:项目根目录查找使用 resolve 和 stat 探测 .git,根目录发现使用 listDir,skill 读取使用 readText。对于未挂载 fs seam 的最小上下文,Node 文件系统仍作为回退。缺失的根目录、不可读或格式错误的 skill 文件,以及提供方 list() 的瞬态失败均降级为 warn-and-skip,使单个坏源不会导致每个 agent 请求失败;格式错误的候选项仍然快速失败,因为它们违反了提供方契约。
dsh-tool-skill 通过 agent/session-prefix 贡献一条 user-role <system-reminder> 目录。目录仅包含排序后的 skill 名称和描述;不包含正文、路径、来源、提供方和路由提示。描述经过空白规范化、XML 转义,并受 catalogDescriptionMaxLength 限制,其默认值为 500,最小值为 3。会话前缀 seam 将仅用于请求的目录按 loop 实例冻结,并记录在请求头中,在不将其加入持久化历史的前提下保持可重建性。完整 skill 正文从不包含在目录中。
skill({ name }) 工具为当前 agent cwd 加载一个完整 skill,返回包含 <skill_content name="...">、<skill_resources> 和 <skill_instructions> 的工具结果。resourceBase 提供一个目录、URL 或不透明的提供方管理的基路径,用于显式引用的脚本、参考资料和资产;资源仅按需加载,不进行目录枚举。无法解析的名称报告该 skill 未知或不再可用;无效名称和标记了 disableModelInvocation 的 skill 保留不同的工具错误。工具结果是面向模型的披露路径。
数据结构与目录/工具契约记录在 skills.md,服务签名见生成的服务目录。
曾考虑的替代方案
将完整 skill 正文注入每条系统提示词。 否决,因为这破坏了渐进式披露,使每个请求都为可能不适用的指令付出代价。
仅将 skill 暴露为斜杠命令。 否决,因为模型主动加载才是核心能力;斜杠/ACP 命令广播不改变发现机制。
将本地文件系统扫描直接放在 ctx.skills 内。 否决,因为编码 agent、Web agent 和未来的插件生态需要不同的 skill 来源。提供方注册表与 subagent seam 同构:注册表负责冲突解析和消费方,实现负责加载。
使用系统提示词段落。 否决,因为渲染后的系统提示词是单一字符串,而目录是一条具有仅请求生命周期要求的 user-role <system-reminder> 消息。agent/session-prefix 是选定的机制:它将目录置于派生历史之前,并将组合后的消息记录在请求头中。
将内置 DSH 编写 skill 物化到 ~/.dsh/skills/.system。 否决,因为打包的 skill 不应在启动时写入用户主目录,嵌入式或远程提供方在配置后提供 skill。
递归发现嵌套的 **/SKILL.md。 否决。扁平文件和一级目录包已覆盖配置的根目录,同时保持重复处理和目录顺序易于推理。
手写 frontmatter 解析器。 否决,因为已接受的 schema 包含一个开放的 metadata 对象。窄解析器要么拒绝用户期望能正常工作的合法 YAML,要么膨胀为一个未经评审的 YAML 子集。
后果
agent-core 主干包含一个会话前缀贡献者、一个本地提供方和一个面向模型的工具。skill 发现对 cwd 敏感,因此以不同会话 cwd 值创建 agent 的调用方可以按设计观察到不同的项目 skill 覆盖。
目录在固定的根目录集和运行时注册修订版本下是确定性的,但不监听磁盘变化;发现结果被缓存,直到运行时注册使缓存失效或进程重启。
延后
fork 式 skill 上下文(context: fork)、直接用户/斜杠调用(user-invocable)、参数声明与提示(arguments 和 argument-hint),以及逐 skill 的工具约束(allowed-tools 和 disallowed-tools)不在已交付的契约范围内。注册表、本地提供方和面向模型的工具不解析、不广播、不强制执行这些字段。