1. 从“agent-skills”说起一个被低估的工程化命题第一次看到“agent-skills”这个词很多人脑子里蹦出来的可能是“智能体技能库”这种大而空的概念。我一开始也这么想直到真正动手把一个智能体从“能聊天”推到“能干活”才发现这四个字背后藏着一整套工程化的硬骨头。简单说agent-skills 指的是给智能体Agent装配的一套可复用、可组合、可独立测试的能力单元——它不是模型本身的能力而是围绕模型搭建的“手脚”和“工具箱”。比如让智能体去查数据库、调用某个内部接口、解析一份 PDF、按规则生成一份报表这些具体动作的封装就是一个个 skill。这件事解决的核心痛点很直接大模型本身只会“说”不会“做”。你问它今天库存还剩多少它能给你编一个看起来很像样的数字但它没法真的去查你的库存表。agent-skills 要做的就是把这层“说”和“做”之间的桥搭起来而且搭得足够稳、足够可维护。它适合谁我认为三类人最该关注一是正在做智能体应用落地的开发者二是需要把重复业务流程自动化的运营或技术同学三是想理解“智能体到底怎么才能可靠干活”的产品和项目负责人。哪怕你只是刚接触这块看完这篇也能明白一个 skill 从设计到上线要过哪些关。我踩过的坑是早期我把 skill 写成一个巨大的函数什么都往里塞结果调试时根本不知道是哪一步出的问题改一处崩三处。后来才慢慢摸索出一套分层、分职责的做法。下面我把这套东西完整拆开讲包括设计思路、核心细节、实操流程和排查经验尽量让你能直接抄作业。2. 整体设计与思路拆解为什么不能把 skill 写成一坨2.1 核心思路把“能力”当成独立产品来做我见过太多项目skill 就是主流程里随手写的一段代码跟业务逻辑缠在一起。这种写法在 demo 阶段没问题一旦 skill 数量超过五个维护成本就指数级上升。我的核心思路是每一个 skill 都当成一个独立的小产品来设计它有明确的输入、输出、边界和失败模式。这样做的直接好处是任何一个 skill 出问题我能单独把它拎出来测试不用启动整个智能体流程。为什么强调“独立产品”因为智能体的调用是动态的、不确定的。模型可能在任何一步选择调用某个 skill也可能传进来一个格式奇怪的参数。如果 skill 本身没有清晰的契约模型传错参数时你连报错都看不懂。把 skill 独立出来之后我可以给它写单元测试、可以单独压测、可以版本化管理。这跟微服务的设计哲学其实是一回事——只不过服务对象从“前端请求”变成了“模型的决策”。另一个关键考量是可组合性。一个复杂的业务动作往往需要多个 skill 串起来。比如“生成月度销售报告”这个动作可能拆成“查询销售数据”“计算同比环比”“生成图表”“套用报告模板”四个 skill。如果每个 skill 都设计得足够纯粹组合起来就非常灵活如果每个 skill 都自带一堆业务假设组合时就会互相打架。我实测下来纯粹度越高的 skill复用率越高后期改动越少。2.2 方案选型为什么用“注册 描述”而不是硬编码早期我试过在代码里用 if-else 硬编码每个 skill 的调用逻辑模型输出一个意图我就 switch 到对应的函数。这种做法在 skill 少于三个时还能忍超过之后代码就变成了一棵巨大的判断树加一个 skill 要改五处地方。后来我改成注册制每个 skill 自己声明名称、描述、参数 schema 和执行函数统一注册到一个注册表里模型通过描述来选择合适的 skill。这个转变的价值在于解耦。模型不需要知道 skill 内部怎么实现只需要知道“这个 skill 能干什么、需要什么参数”。注册表负责把模型的调用请求路由到正确的执行函数。加一个新 skill只需要写一个新的注册项主流程一行都不用改。这就像给智能体装了一个“应用商店”skill 是里面的 App注册表是应用商店的索引模型是用户。描述description的写法尤其重要这是很多人忽略的点。模型是靠描述来判断该不该调用某个 skill 的描述写得含糊模型就会乱调或者不调。我的经验是描述里要写清楚“这个 skill 做什么”“什么时候该用”“什么时候不该用”“参数大概是什么”。比如“查询库存”这个 skill描述里我会写“用于查询指定商品在指定仓库的实时库存数量当用户询问库存、余量、还有多少货时使用不适用于查询历史库存变动”。这种带边界的描述能显著降低误调用率。2.3 优势与规避的问题注册制 独立 skill 的组合带来的优势我总结成三点。第一是可测试每个 skill 可以脱离智能体单独跑测试用例输入输出都是确定的。第二是可观测每次调用都能记录是哪个 skill、传了什么参数、返回了什么、耗时多少出问题能快速定位。第三是可演进skill 可以独立升级版本旧版本保留兼容新版本灰度上线不会因为改一个 skill 把整个智能体搞挂。它规避的核心问题是“意大利面条式”的调用逻辑。没有这套设计智能体的代码会随着 skill 增多而迅速腐化最后没人敢改。有了这套设计代码结构始终是清晰的注册表是索引skill 是模块主流程是调度器各司其职。注意注册制不是银弹。如果 skill 之间存在强依赖比如 B 必须在 A 之后调用光靠描述是不够的还需要在调度层做编排。这一点后面会细讲。3. 核心细节解析与实操要点一个 skill 到底该怎么写3.1 输入输出的契约设计一个 skill 最核心的部分是它的输入输出契约。我见过最常见的错误是输入参数用一堆可选字段输出返回一个巨大的字典调用方根本不知道哪个字段有用。我的做法是输入用严格的 schema 约束输出用固定的结构返回。输入 schema 我一般用 JSON Schema 来描述明确每个参数的类型、是否必填、取值范围、默认值。这样模型在生成调用参数时有明确的格式参考减少格式错误。输出结构我坚持一个原则永远返回一个包含 status、data、error 三个字段的对象。status 表示成功或失败data 是成功时的业务数据error 是失败时的错误信息。为什么这么设计因为模型需要根据 status 来判断下一步动作。如果 skill 直接抛异常模型可能收到一个它无法理解的错误堆栈导致整个流程卡死。返回结构化的错误模型就能理解“哦这个 skill 失败了原因是参数不对我该换个参数重试或者换一个 skill”。参数校验必须放在 skill 内部做不能指望模型传对。我实测下来模型传错参数的概率不低尤其是涉及日期格式、枚举值、数值范围的时候。所以每个 skill 入口第一件事就是校验参数不合法直接返回明确的错误告诉模型哪个参数错了、期望什么格式。这个反馈对模型自我修正非常关键。3.2 描述文本的写法与边界声明描述文本是 skill 和模型之间的“接口文档”写得好不好直接决定调用准确率。我的写法模板是功能一句话 使用场景 不适用场景 参数说明 返回说明。功能一句话要短最好控制在 20 字以内让模型一眼抓住重点。使用场景列举两三个典型问法帮模型建立触发联想。不适用场景同样重要能有效防止误调用。举个例子一个“发送通知”的 skill描述我会这么写“向指定用户发送站内通知。当用户要求通知某人、提醒某人、发消息给某人时使用。不适用于发送邮件或短信也不适用于群发。参数user_id 为目标用户 IDcontent 为通知内容最长 500 字。返回发送成功返回通知 ID失败返回错误原因。”这种描述模型基本不会用错。参数说明里我会特别标注哪些参数是必填、哪些有默认值、哪些有格式要求。比如日期参数我会写“格式为 YYYY-MM-DD例如 2024-01-15”。模型看到这种明确格式生成参数时就会照着来。返回说明也要写清楚尤其是失败时的返回结构让模型知道怎么处理异常。3.3 错误处理与重试策略错误处理是 skill 设计里最容易被轻视、但实际最影响稳定性的部分。我的原则是错误分类 可重试标记。错误分三类参数错误、业务错误、系统错误。参数错误是模型传错了这类错误不可重试直接把错误信息返回给模型让它修正。业务错误是业务规则不允许比如“库存不足”这类也不可重试但要让模型知道原因。系统错误是网络超时、依赖服务挂了这类可重试skill 内部可以做有限次重试。可重试标记怎么用我在返回结构里加一个 retryable 字段。调度层看到 retryable 为 true就自动重试为 false就把错误抛给模型处理。这个机制能显著提升整体成功率因为很多失败其实是瞬时的网络抖动重试一次就好了。但要注意重试必须有次数上限和退避策略否则可能把下游服务打垮。我一般设最多重试两次间隔用指数退避第一次 1 秒第二次 2 秒。提示涉及写操作的 skill比如下单、扣款要特别小心重试。重试前必须确认操作是幂等的否则可能重复下单。我的做法是给写操作加一个幂等键同一个键的重复请求只执行一次。3.4 超时与资源控制每个 skill 都必须设超时这是铁律。我见过因为一个 skill 卡住导致整个智能体流程挂起的案例排查了半天才发现是某个外部接口没设超时。超时时间怎么定我的经验是查询类 skill 设 3 到 5 秒写操作类设 5 到 10 秒涉及复杂计算的设 10 到 30 秒。超过这个时间还没返回基本可以判定有问题直接超时返回错误让模型决定下一步。资源控制还包括并发限制。如果某个 skill 依赖的下游服务承载能力有限就要在 skill 层面做并发控制比如用信号量限制同时执行的请求数。我一般会给每个 skill 配一个最大并发数超过就排队或直接拒绝。这个参数要根据下游服务的实际承载能力来定不能拍脑袋。实测下来合理的并发控制能避免很多“雪崩”式的故障。4. 实操过程与核心环节实现从零搭一个 skill 注册与调度系统4.1 环境准备与目录结构我用的技术栈是 Python因为生态成熟、上手快。核心依赖就几个pydantic 做参数校验jsonschema 做 schema 描述fastapi 做 skill 的 HTTP 暴露如果需要跨进程调用。目录结构我习惯这么组织agent_skills/ registry.py # 注册表核心逻辑 dispatcher.py # 调度器 skills/ __init__.py query_stock.py # 单个 skill send_notify.py gen_report.py schemas/ common.py # 公共 schema 定义 tests/ test_query_stock.py每个 skill 一个文件文件里定义 skill 的元信息名称、描述、参数 schema和执行函数。注册表在启动时扫描 skills 目录自动加载所有 skill。这种结构的好处是加 skill 只需要加一个文件不用改任何现有代码。4.2 注册表的实现注册表的核心是一个字典key 是 skill 名称value 是 skill 的元信息和执行函数。加载时遍历 skills 目录用 importlib 动态导入每个模块读取模块里约定的变量比如 SKILL_META 和 execute 函数注册到字典里。这里有个细节skill 名称必须全局唯一加载时要做冲突检测重名直接报错避免运行时才发现问题。注册表还要提供一个“列出所有 skill 描述”的方法这个方法返回的文本会拼接到模型的系统提示里让模型知道有哪些 skill 可用。描述文本不能太长否则会占用大量上下文。我的做法是只拼接名称和一句话功能描述详细描述在模型决定调用某个 skill 时再按需提供。这样既省上下文又不影响调用准确率。4.3 调度器的实现调度器负责接收模型的调用请求路由到对应的 skill处理超时、重试和错误。核心流程是解析请求拿到 skill 名称和参数从注册表查 skill校验参数执行捕获异常按错误类型决定是否重试最后返回结构化结果。调度器还要记录每次调用的日志包括 skill 名称、参数、结果、耗时、是否重试。这些日志是后续排查问题的关键。重试逻辑我单独抽成一个装饰器套在 skill 执行函数外面。装饰器里判断返回结果的 retryable 字段决定是否重试。这样 skill 本身不用关心重试职责更纯粹。超时控制用 signal 或者线程池实现我一般用 concurrent.futures 的 ThreadPoolExecutor提交任务后等指定时间超时就取消。注意 Python 的线程取消不是强制的所以 skill 内部也要配合检查超时标志。4.4 一个完整 skill 的代码示例以“查询库存”为例完整实现大概是这样from pydantic import BaseModel, Field from typing import Optional class QueryStockParams(BaseModel): product_id: str Field(..., description商品ID必填) warehouse_id: Optional[str] Field(None, description仓库ID不传则查所有仓库) SKILL_META { name: query_stock, description: 查询指定商品在指定仓库的实时库存。当用户询问库存、余量、还有多少货时使用。不适用于查询历史库存变动。, params_schema: QueryStockParams.schema(), timeout: 5, max_retries: 2, } def execute(params: dict) - dict: try: validated QueryStockParams(**params) except Exception as e: return {status: error, data: None, error: f参数错误: {e}, retryable: False} try: # 实际查询逻辑这里用伪代码代替 result do_query(validated.product_id, validated.warehouse_id) return {status: success, data: result, error: None, retryable: False} except TimeoutError: return {status: error, data: None, error: 查询超时, retryable: True} except Exception as e: return {status: error, data: None, error: f系统错误: {e}, retryable: True}这段代码里参数校验、错误分类、可重试标记都齐了。do_query 是实际的业务逻辑可以替换成任何查询实现。注意参数校验失败返回 retryable False因为模型传错参数重试也没用得让它改参数。系统错误返回 retryable True因为可能是瞬时故障。4.5 参数计算与选择过程超时时间怎么定我一般先测 skill 在正常情况下的 P99 耗时然后超时设成 P99 的 2 到 3 倍。比如查询库存 P99 是 800 毫秒超时设 3 秒比较合理。重试次数怎么定看下游服务的稳定性稳定的话重试 1 次就够不太稳定可以 2 次但不要超过 3 次否则总耗时可能超过上游的超时限制。并发数怎么定用压测工具测下游服务在不同并发下的响应时间和错误率找到错误率开始上升的拐点并发数设在这个拐点以下。这些参数不是拍脑袋定的都要有实测数据支撑。我见过有人把超时设成 30 秒结果一个 skill 卡住整个智能体等 30 秒才返回用户体验极差。合理的超时应该是“比正常耗时略宽裕但不会让用户等太久”。5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 模型不调用 skill 或调用错误的 skill这是最常见的问题。模型该调用某个 skill 时没调用或者调用了错误的 skill。排查思路分三步。第一步看描述描述是不是太模糊或者有歧义。我遇到过描述里写“处理数据”模型完全不知道这是查数据还是改数据结果该查的时候它去调了改的 skill。改成“查询数据”之后问题就解决了。第二步看 skill 数量skill 太多超过 20 个时模型的选择准确率会下降这时候要考虑分组或者用两级路由。第三步看参数有时候模型想调用但参数生成不出来就放弃了这时候要检查参数 schema 是不是太复杂。我的经验是描述文本里加上“当用户说 XXX 时使用”这种触发词能显著提升调用准确率。因为模型本质是在做模式匹配给它明确的触发模式它匹配得就准。另外skill 名称也要起得直观别用缩写或者内部代号模型看不懂。5.2 参数格式错误频发模型生成的参数格式经常不对尤其是日期、枚举、数值范围。排查时先看 schema 描述够不够明确。我一开始写“日期参数”模型生成什么的都有后来改成“日期参数格式 YYYY-MM-DD例如 2024-01-15”错误率立刻降下来。枚举值要把所有可选值列出来别只写“类型”。数值范围要写清楚最小值和最大值。还有一个技巧是在 skill 内部做参数归一化。比如模型传了“2024/01/15”skill 内部自动转成“2024-01-15”。这种容错能减少很多不必要的失败。但归一化要有限度不能把明显错误的参数也强行修正否则会掩盖真正的问题。5.3 skill 执行超时或卡死超时问题排查先看 skill 内部有没有设超时。我见过 skill 调用外部接口没设超时外部接口挂了skill 就一直等最后把整个流程拖死。所以每个外部调用都必须设超时而且要比 skill 本身的超时短留出处理超时的时间。其次看是不是有死循环或者锁竞争这种问题一般伴随 CPU 或内存异常用监控工具能看出来。卡死的另一个常见原因是资源泄漏比如数据库连接没关、文件句柄没释放。这种问题在低并发时看不出来高并发时连接池耗尽就卡住了。排查方法是看连接池的使用率如果一直居高不下基本就是泄漏。解决方法是确保每个 skill 用 with 语句管理资源或者用连接池的自动回收机制。5.4 常见问题速查表问题现象可能原因排查方法解决方向模型不调用 skill描述模糊、skill 太多检查描述文本、统计 skill 数量优化描述、分组路由调用错误 skill描述有歧义、名称不直观对比相似 skill 的描述加边界声明、改名称参数格式错误schema 描述不明确看模型生成的参数样本补充格式示例、加归一化skill 超时外部调用没设超时检查外部调用超时配置逐层设超时、加熔断高并发卡死资源泄漏、连接池耗尽看连接池使用率用 with 管理资源、调大池重试导致重复操作写操作不幂等检查是否有幂等键加幂等键、限制重试5.5 独家避坑技巧第一个技巧给 skill 加“干跑”模式。干跑模式下 skill 只校验参数和检查依赖不实际执行。这个模式在调试时特别有用能快速确认参数和依赖有没有问题不用真的去改数据。我一般在 skill 上线前用干跑模式跑一遍所有测试用例确认没问题再开真实执行。第二个技巧记录模型的原始调用请求。有时候 skill 返回了错误但你不确定是模型传错了还是 skill 处理错了。把模型生成的原始请求记下来对比一下就能定位。这个日志我建议长期保留因为很多问题是偶发的事后复现很难有日志就能回溯。第三个技巧给 skill 设“熔断”开关。某个 skill 依赖的下游服务挂了如果还一直调用只会不断超时和重试拖垮整个系统。这时候要能快速熔断让调度器直接返回错误不再调用。熔断可以手动触发也可以基于错误率自动触发。我一般设错误率超过 50% 且持续 30 秒就自动熔断5 分钟后半开试探。第四个技巧skill 版本化。skill 的修改不要直接覆盖而是发新版本旧版本保留一段时间。这样如果新版本有问题可以快速回滚。版本号我一般用日期加序号比如 20240115-1。调度器根据配置决定用哪个版本灰度时可以让一部分流量走新版本。6. 影响范围与延展思考agent-skills 能走多远6.1 对智能体落地的影响agent-skills 这套东西本质上是把智能体从“演示品”变成“生产工具”的关键一步。没有 skill 的智能体只能做问答和文本生成价值有限。有了 skill智能体能真正操作业务系统价值就完全不一样了。我观察到的一个趋势是越来越多的团队在把智能体接入实际业务流程而 skill 的质量直接决定接入的成败。skill 设计得好智能体就能稳定干活设计得差智能体就成了“人工智障”还不如人工操作。影响范围还不止于此。skill 的标准化可能会催生一个“skill 市场”。就像手机应用商店一样有人专门开发通用 skill有人开发垂直行业 skill团队按需采购或订阅。这会大大降低智能体的落地门槛因为不用什么都自己写。当然这也带来 skill 质量参差不齐的问题需要有一套评估和认证机制。目前这块还在早期但方向是清晰的。6.2 延展方向从单 skill 到 skill 编排单个 skill 能做的事有限真正的价值在于 skill 的组合编排。比如“处理客户投诉”这个复杂任务可能需要“查询订单”“查询物流”“判断是否符合退款条件”“发起退款”“发送通知”五个 skill 串起来。怎么编排目前主流有两种做法。一种是在提示里写清楚流程让模型按步骤调用。这种灵活但不可控模型可能跳步或者乱序。另一种是用代码编排把流程写死模型只负责填充参数。这种可控但不够灵活。我的实践是混合主流程用代码编排保证可控分支和异常处理交给模型决策。比如退款流程主步骤写死但“是否符合退款条件”这个判断交给模型模型调用对应的 skill 拿到结果后决定走哪条分支。这样既保证了主流程的稳定又保留了应对复杂情况的灵活性。未来这块可能会演化出专门的编排语言或者框架让 skill 组合像搭积木一样简单。6.3 我个人在实际操作中的体会做了这么多 skill我最大的体会是skill 的质量比数量重要得多。与其写二十个半吊子 skill不如把五个核心 skill 打磨到极致。一个高质量的 skill描述清晰、参数严谨、错误处理完善、有测试覆盖模型用起来顺手出问题也好排查。一个低质量的 skill描述含糊、参数随意、错误处理缺失模型用一次错一次最后没人敢用。另一个体会是skill 的设计要站在模型的角度想。模型不是人它没有常识只能靠描述和 schema 来理解 skill。所以描述要写得像给一个完全不了解业务的人看参数要约束得像给一个粗心的人用。你多花十分钟把描述写清楚可能就省下后面十个小时的排查时间。这个投入产出比怎么算都划算。最后分享一个小技巧定期回顾 skill 的调用日志看看哪些 skill 从没被调用过哪些 skill 错误率特别高。从没被调用的要么是描述有问题要么是根本不需要可以考虑下线。错误率高的重点优化。这种定期清理能让 skill 库保持精简和健康避免越积越多最后变成一团乱麻。