应用系统体系架构-05:Skill 设计模式
原子工具解决一个明确操作,复杂任务则需要决定调用哪些能力、怎样传递状态、失败后如何恢复。Skill 设计模式 把这些协作方式组织成可复用结构,避免每个工具都重复实现路由、校验、重试和审批。
本章先区分任务规范与执行接口,再讨论路由、组合、降级、交互、缓存、校验和恢复,最后把它们放入可观测、可评估的控制器。
规范与能力
SKILL.md 描述某类任务的触发条件、工作方法和所需资源,Tool 实现实际操作,编排器把多个操作连接成流程。三者可以协作,但承担不同职责。
| 对象 | 描述什么 | 如何生效 |
|---|---|---|
| Skill 文件 | 什么时候使用、按什么方法工作 | 宿主发现并读取任务规范 |
| Tool / Function | 能执行什么、参数与结果是什么 | 程序登记并调用实现 |
| 编排器 | 多步依赖、分支、状态与恢复 | 控制器或状态图执行 |
| MCP Server | 如何跨应用发现和访问能力 | 协议 Client 建立连接 |
文字要求“必须回滚”不会自动创建事务,文件写了“只读”也不能证明脚本无副作用。实际限制需要执行层、数据库或运行环境支持。
一组普通函数可以被注册成工具并参与编排,但不等于宿主已经发现一个规范的 Skill 包。反过来,某些 Skill 只提供工作规则或模板,也可以有价值,不要求每个 Skill 都自带可执行代码。
入口和元数据应遵循所用标准,正文的自然语言 Workflow 可以按任务组织。工具名单、是否需要审批等扩展字段需要宿主理解并落实,不能把一份写有这些字段的 Markdown 直接当成权限系统。
Skill 文件
Skill 通常以一个目录组织,入口文件使用 YAML frontmatter 保存名称与描述,正文说明工作流程,辅助材料按需要读取。
1 | refactor-legacy-sql/ |
入口可以写成以下形式:
1 | --- |
描述字段用于发现与选择,正文用于实际工作。宿主特有的权限或工具字段需要按其支持方式配置,不能解释成跨产品通用的强制权限机制。
模板与流程
可重复工作适合提炼为 Skill。例如生成课程 HTML 页面时,样式、页眉页脚、代码配色和公式呈现规则较稳定,当前页面主题与正文则每次变化。
| 部分 | 复用方式 |
|---|---|
| 视觉与布局 | 保存在公共规则或模板中 |
| 页眉页脚 | 固定结构,填入当前页面信息 |
| 页面内容 | 作为当前任务输入 |
| 代码与公式规则 | 在需要相应内容时加载并验证 |
稳定模板减少反复手写带来的差异,也可能提高可复用前缀比例;但模板复用和模型 KV Cache 是不同层次。是否命中缓存仍取决于实际请求顺序与内容。
一个较大的 Skill 可以先识别项目语言,再只加载该语言的说明和脚本。工作流可能顺序执行、按条件分支,也可能并行处理几个独立子任务;按需加载并不意味着所有任务都只走一条固定根到叶路径。
作用域加载
个人级 Skill 便于跨项目使用,项目级 Skill 表达仓库特有规则。某些宿主使用个人目录和 .claude/skills/ 等项目目录,但具体路径、优先级和启用方式由宿主决定。
渐进披露(Progressive Disclosure) 先加载名称与描述,在任务需要时读取正文,再读取相关参考、脚本和资产。这能减少无关资料占用上下文。
大型 Skill 应把通用流程保留在入口,把数据库、语言或模块规则拆到参考文件。引用路径、依赖与优先级要明确,避免循环引用和同一条件下出现相互矛盾的要求。
脚本负责实现,参考文件负责解释,资产负责模板或数据。按任务读取必要部分,可以同时改善维护和模型理解。
作用域也服务于配置差异。两个项目若使用不同数据库、规范或工具版本,可以分别提供项目级规则,而不把所有差异硬塞进个人级 Skill。目录树描述资源所在位置,Workflow 才描述执行关系;复用同一规则的多个步骤可能形成网络。
例如数据库访问审计需要 db-rules.md,涉及认证与权限时需要 security-rules.md。同一修改可能两者都适用,不能默认把它们写成互斥的 if/else。执行到相应阶段再加载资料,也要保留完整验证要求。
环境参数
Skill 应明确依赖和输入来源。数据库版本、项目路径、Base Commit 等属于任务或项目 Context;操作系统环境变量则是进程配置,二者不能因为都叫“环境”就混用。
| 信息 | 取得与核对方式 |
|---|---|
| 数据库类型和版本 | 用户说明、项目配置或实际连接信息,必要时交叉核对 |
| 允许工具和目录 | 来自宿主策略与已认证权限 |
| 依赖及脚本版本 | 包声明、锁文件和部署配置 |
| 凭证 | 在执行层安全取得,不把明文填入 Prompt |
用户声明“使用 MySQL 8”可以成为任务输入,但涉及实际执行时还需确认服务环境,不能凭声明改变授权或忽略版本差异。
跨宿主复用也需要检查兼容性。SKILL.md 文字可迁移,不保证原脚本的路径、Tool ID、Shell、模型服务和资源接口都存在。第三方 Skill 应按可执行依赖检查其引用与脚本,不能只看入口文字就认为全部内容安全。
托管实现
Java 工程可以通过 Spring 的类型、依赖注入、事务和授权设施封装工具。Python 也能实现这些机制,选型主要取决于现有业务与团队环境。
1 |
|
示例中的业务类型由项目实现,具体工具回调接口与注册方式需要匹配 Spring AI 版本。FunctionCallback、ToolCallback 和函数回调适配属于相关但不应混用的 API 形态。
托管工具调用 Service/DAO,可以复用业务规则与测试替身。@Transactional 和 @PreAuthorize 还需要相应配置及代理调用路径,不能仅添加注解就假设所有入口都受保护。
Controller 的 HTTP 请求与 Tool Call 可以是不同入口,但最终复用同一业务 Service 和 Repository。Record/DTO 描述参数,不代替对象归属、范围和授权检查;事务应保护相应业务边界,而不是只包住一次模型输出。
自研应用可以解析 Skill 形成内部 Spec,登记到 Registry,再按任务选择规则与 Tool。这里的 Spec、Registry 可以是应用自己定义的适配对象,不应假定任何框架都自带同名接口。
Skill 文件与托管能力可以通过适配层结合:读取元数据和规则,选择注册能力,再由受控执行器完成操作。动态装载必须检查路径、字段和版本,加载到的文字不能自行扩大权限。
Spring AI 的 Advisor 可以在模型请求前后加入资料、记忆、观测或结果处理。这与 AOP 的横切思想相似,但 Advisor 是模型调用链的一部分,不表示改写应用全部字节码。
Spring AOP 常通过运行时代理拦截方法,AspectJ 还可以采用编译期或加载期编织,不能把所有切面实现都归为同一种机制。请求增强、工具登记、真实执行和业务事务仍需分别配置。
意图路由
Router Pattern 从能力池中选择与目标相关的候选,减少大量工具同时进入上下文造成的噪声。它可以采用规则、分类模型或语义检索。
1 | 用户目标:审查 Java 服务的 SQL 注入。 |
路由需要明确未知意图和多领域任务的处理。分类置信不足时,可以保留多个候选、请求补充或使用安全的降级流程。
路由正确率与候选召回要分别评估。门控过窄可能漏掉正确能力,过宽则不能有效控制成本。选择能力不等于授权执行能力,权限检查应独立存在。
可以采用两级筛选:先用领域分类缩小候选池,再在池中按描述向量进行 Top-K 搜索,最后让模型读取详细规则。比如先识别历史类任务,再区分古代、现代或不同地区;代码任务则可能同时涉及数据库与权限领域。
第一层分类出错可能把正确 Skill 全部排除,因此需要未知意图、多领域和扩大候选的出口。50、1000 个 Skill 或 Top-3 等都是规模与策略示例,不是必须触发某种算法的固定阈值。
复合能力
Composite Pattern 把多个原子操作封装成一个业务目标,例如 SQL 自动修复可以组合读取、扫描、生成补丁、应用修改和测试。
每个节点应有明确输入输出、依赖和验收,复合能力对外提供统一结果。部分步骤成功、后续失败时,应保留状态并决定重试、回滚或补偿。
1 | 输入:项目路径与审计规则。 |
封装统一接口不会使跨数据库、文件和远端 API 自动成为原子事务。复合流程需要明确哪些改变可回滚,哪些只能补偿。
组合可以通过两种方式表达:在新的 Skill 中描述步骤与引用,也可以在代码中调用已登记的能力。流程固定且条件可明确定义时,代码编排有利于测试和控制;需要自然语言选择或解释的部分,可以保留给模型和 Skill。
跨包复用应通过明确依赖、注册名称和允许的资源引用,不随意用 ../ 绕过目录边界。文件内的引用路径、跨 Skill 的依赖和执行器 API 是不同层次。
降级容错
Fallback Pattern 在主路径不可用时选择备用能力。例如代码搜索服务故障时,可以退回本地索引;较强模型不可用时,可以使用较弱模型并缩小任务范围。
降级必须保持可接受的语义。备用结果应标明能力差异,不能把不完整结果伪装成正常完成。权限错误和非法输入也不能通过换一个服务绕过。
断路器(Circuit Breaker) 控制持续故障:
| 状态 | 行为 |
|---|---|
| Closed | 正常执行,统计可恢复失败 |
| Open | 暂时拒绝主路径,减少无效请求 |
| Half-open | 允许有限试探,成功后恢复或失败后再次打开 |
重试需要次数、总时间和成本上限,并采用适当退避;并发客户端可以加入抖动,减少同时再次请求。降级、重试与断路器解决不同问题,需要协调而不能任意叠加。
远端代码搜索不可用时,可以降级为本地 git grep 一类关键词检索。返回应标明 degraded 及较弱的能力:本地匹配不一定具有远端语义搜索或索引覆盖,不能把两者视为相同结果。
断路器 Open 表示禁止继续请求主路径,Closed 表示正常导通。失败可以跨多次请求累计,不表示每次调用都必须内部重试三遍。当前请求的重试、跨请求的断路状态与备用结果选择需要分别管理。
人机交互
HITL Pattern 把任务转入等待外部决策的状态。需要保存待执行动作、参数、目标版本和审批条件,释放长期等待的执行资源,再由回调恢复。
1 | 运行中 → 生成待审批动作 → 持久化 → 等待审批 |
审批应来自受信身份,并绑定具体任务,不能用模型自己生成的“已批准”作为依据。审批回调还需要防重放,避免重复执行。
长任务不能只停在一个 input() 或长期阻塞线程里。进程重启后仍需知道等谁审批、批准什么以及从哪里恢复。
待审批不是已失败,也不是已执行成功。执行器可以返回 waiting_approval、任务 ID 与审批入口,持久保存动作后结束当前等待;后续明确的批准或拒绝再恢复对应任务。
模型收到“暂挂”结果时,不应自动重新发起同一个删除或支付请求。通知审批者也不能替代决策本身,恢复必须检查原参数、版本和任务身份。
缓存与幂等
缓存 减少重复计算,幂等 避免重复请求产生额外副作用,两者含义不同。相同内容的扫描可以缓存;创建订单或发消息则需要稳定请求标识与原子去重。
只读缓存键应包含内容哈希、规则版本、配置和权限范围。文档或规则变化后,旧结果需要失效。下面展示一个有限容量的进程内缓存。
1 | import time |
这是单进程、单线程示例,不提供跨进程共享或并发保护。副作用幂等通常要在数据库中把请求 ID、执行状态和业务写入原子关联,并区分未执行、已完成和状态未知。
HTTP 方法的幂等性也依据预期效果。GET 的响应可能因数据更新而改变,仍属于读取;重复 DELETE 可以先成功再返回不存在,但目标仍保持删除状态。PUT 表达设置目标表示,重复同一赋值通常不增加额外效果;POST 默认不承诺幂等,业务仍可以用去重键实现。
因此,幂等不等于“每次输出完全相同”,也不以无状态为必要条件。维护状态表恰恰可以帮助保证同一退款请求只执行一次。Tool Call ID 用于消息对应,业务幂等键还应绑定同一个意图、对象与调用者。
Skill 解析缓存又是另一种缓存:它保存从文件得到的结构化 Spec 或注册信息,减少重复读取与解析。它与扫描结果缓存、RAG 答案缓存、模型 KV Cache 都不同。
缓存键需要覆盖会影响行为的内容,例如入口、引用规则、脚本、Tool 定义、示例和解析器版本。不能为了少计算哈希,省略已经变化的脚本或参数契约;也不能随意排序具有执行顺序含义的 Workflow。按键排序序列化对象不等于重排流程数组。
1 | import hashlib |
bundle 应包含相关文件摘要与元数据。SHA-256 提供指纹,不是把内容加密;是否优化读取和哈希,应测量实际成本。权限仍在执行时检查,缓存过一次 Spec 不表示其中全部操作永久获批。
双向校验
Bidirectional Validation 在执行前检查输入,在执行后检查输出。输入包括格式、权限、范围和前置业务条件;输出包括结构、字段值、来源和业务不变量。
例如重构补丁的输入要限制目标路径与版本,输出要检查实际差异、公开接口和测试结果。外部 API 返回 HTTP 200 也可能缺字段或表达失败,因此不能只验证网络成功。
校验失败应说明发生阶段,便于控制器选择补参数、重新读取、补偿或结束。输入验证与输出验证由明确程序完成,比只要求模型“再检查一下”更容易测试。
预演恢复
Dry-Run & Rollback 用于预览变化并在失败时恢复本次操作。预演返回预计差异、影响范围和风险,实际执行前仍需检查资源版本。
数据库事务保护其覆盖的数据库写入,Git 快照保护相应文件差异,外部服务操作可能需要补偿。git stash 还可能包含用户原有修改,并在恢复时发生冲突,不能直接作为所有任务的可靠事务。
隔离工作区可以把任务修改与用户当前工程分开。回滚要限定范围,并核对并发变化;否则恢复旧快照可能覆盖新成果。
预演生成的差分适合展示预计影响,快照用于记录基线,两者都需要绑定真实资源版本。网络超时后,先确认操作有没有完成,再决定重试或补偿;不能因为没有收到响应,就假定外部系统未发生变化。
快照保存状态,幂等限制重复,回滚处理变化。它们解决不同问题,可靠恢复通常需要共同使用。
退款案例
退款工作流可以把不同规则拆成独立检查。一个教学示例使用“超过 500 元需要拦截”与“注册城市和当前定位不一致需要核对”两项规则:
| 金额与位置 | 对应判断 |
|---|---|
| 100 元,位置一致 | 两项示例检查通过 |
| 540 元,位置一致 | 金额检查仍未通过 |
| 100 元,位置不一致 | 位置检查仍需处理 |
| 540 元,位置不一致 | 不能只修正一项就认为全部通过 |
500 元是演示策略,不是通用退款规定。身份、订单、金额、位置来源与规则版本应由受信系统提供或核对,不能让模型改写输入绕过 Guard。通过检查还不代表已经退款,实际写入需要审批策略、幂等与执行凭证。
这个例子说明 Skill 可以根据观察进入不同分支,而不只是按顺序读完全部文本。修改了一项条件后,要重新检查整组约束,并使之前不再适用的批准失效。
上下文管理
Context Pruning 保留当前目标、有效约束、可信状态和重要错误,减少重复日志及过时方案。上下文摘要应保留结构化事实与原始记录入口,避免修订后无法判断事实来自哪里。
Observation 清洗管道还需要脱敏和来源标识。密钥、个人信息与内部诊断不应全部传给模型,删除几个关键词也无法完整消除 Prompt Injection。
日志抽取可以保留失败测试、异常类型、关键堆栈和退出码。过度截断可能丢掉根因,保留完整日志的受控查询能力能减少这种影响。
循环与预算
控制器需要发现没有进展的重复动作。动作指纹 可由工具名称与规范化参数形成,结合观察是否变化进行判断。
1 | import hashlib |
图中存在环不等于任务会无限循环,重复请求也可能是合法轮询。判断需要结合次数、持续时间和目标进展。
一个简单检测方法是维护工具名与规范化参数的指纹历史,取最后 N 次:只有确实已有 N 次且 len(set(recent)) == 1,才能认为这 N 次连续使用同一动作。这是“连续重复”信号,不等于已经证明死循环。
A→B→A→B 的往返不会被该条件捕获;反之,反复查询一个逐渐变化的任务状态可能正常。需要结合观察、资源版本和目标进展判断,避免只凭指纹阻断合法轮询。
Cost Guard 限制 Token、金额、请求数、并发和总时间。执行前可预估成本,执行后按真实用量更新。限流应按租户或任务隔离,并处理超时后执行状态未知的情况,避免预算统计与真实消耗脱节。
重复检测、总预算和频率限制也不同:换用其他工具可以消除连续重复,却仍会消耗步骤与 Token;请求每次成功,也可能超过一分钟允许的次数。总预算通常预留估算额度,完成后依据实际用量结算,不应只在最后发现额度已经超支。
接口适配
Anti-Corruption Layer(ACL) 把外部 API 的字段、状态和版本变化限制在适配器内,内部流程使用稳定契约。
例如 Jira 的外部状态可能变化,适配器可以统一映射为内部待处理、进行中和已完成等状态。遇到未知值应明确报告,不应静默归类为成功。
适配层负责参数转换、认证、错误映射和版本兼容;业务编排不直接依赖外部响应的全部细节。契约测试可以检查接口升级是否破坏现有任务。
例如流程按 API v3 的字段和错误语义设计,而外部服务升级到 v4,适配器需要识别版本与 Schema 的变化。若已有兼容映射,可以验证转换后继续执行;若字段意义、状态或鉴权方式尚未确认,应返回明确的契约错误。只在外层捕获异常,再把不兼容结果作为正常数据传下去,会把问题推迟到后续步骤。
状态与并发
Checkpoint 保存任务 ID、当前节点、输入、结果、版本和待办动作。恢复时需要核对依赖及凭证是否有效,并知道哪些外部操作已经完成。
保存一个字典不代表完整恢复。快照写入应原子进行,Schema 有版本,状态可迁移,读取有权限,过期记录有清理策略。
并行任务修改同一资源时,需要锁、乐观版本检查或事务。多资源锁应使用一致顺序,控制持锁时间,并处理超时与释放;无关只读任务则可以并发提高吞吐。
共享状态的合并还要明确规则。多个任务同时覆盖同一个字段,可能导致较晚结果抹掉较早结果,需按调用 ID 或 reducer 聚合。
灰度与诊断
Shadow Testing 在旁路运行新版本并比较结果,不应产生真实外部副作用。Canary Run 让有限真实流量使用新版本,依据失败率、延迟、质量和成本决定扩大或回退。
影子读操作也可能增加负载并接触敏感数据,因此仍需要与生产一致的权限、隔离和配额。
结构化错误应包含阶段、工具、代码、可重试性和 Trace ID。协议错误、参数错误、业务拒绝和系统故障要分别表达,避免错误重试或吞掉问题。
评估开发
评估驱动开发(EDD) 在修改前保存基线,修改后比较任务成功、路由、参数、执行成本和延迟。只凭个别对话判断 Prompt 变好,容易忽略其他场景退化。
| 指标 | 含义 |
|---|---|
| Pass@1 | 一次尝试完成任务的比例 |
| Precision / Recall / F1 | 能力选择的准确与遗漏情况 |
| Execution Cost | Token、模型调用与工具资源成本 |
| Latency | 总耗时、阶段耗时和 P95 |
| 拒绝与恢复 | 是否正确拒绝越权、能否恢复可处理失败 |
评测数据应有固定对照集,也要吸收真实失败。LLM-as-a-Judge 可以辅助评文本质量,测试、Schema 和业务断言负责硬验证;裁判自身需要校准。
CI/CD 保存 Skill、Prompt、模型与配置版本,变更后运行回归并检查关键类别。门槛应来自业务要求,不能用平均分提高掩盖安全或重要业务退化。
组合案例
一个受控的代码审计与重构任务可以把这些模式按边界组合:先由 Router 选择能力,Composite 编排读取与扫描,双向校验约束输入和补丁,预演展示变化,HITL 处理需要批准的动作,再在隔离环境应用并测试。
调用失败由断路器、有限重试或降级处理;执行进度进入 Checkpoint;上下文只保存必要观察;最终结果进入回归与审查。Spring 托管业务能力和文件型 Skill 规范可以通过稳定接口参与同一链路。
常见问题包括单个 Skill 过大、描述含糊、裸调用大量工具、缺少失败语义、全量日志入上下文、无界重试、隐式副作用和没有评测。修复这些问题应改变接口、状态和控制器,而不是不断增加相似的提示文字。
Skill 设计模式的价值在于把选择、执行、验证和恢复的边界写清楚。每个模式都应有明确状态、失败出口与可验证结果。

