1. 从"agent-skills"这个命名说起:它到底指什么
第一次看到"agent-skills"这个词,很多人会下意识地把它理解成某个具体的库或者框架。但如果你真的去翻一圈,会发现它更像是一个概念容器——用来描述"智能体(Agent)具备哪些能力"这件事本身。换句话说,它不是一个能pip install的包,而是一套关于"如何给智能体装配技能"的方法论和工程实践。
我在过去一年多的时间里,陆续做过几个和智能体相关的项目,从最简单的问答机器人,到需要多步推理、调用外部工具、维护长期记忆的复杂系统。踩过的坑不算少,也慢慢摸索出一套相对稳定的思路。这篇文章就把这些经验整理出来,围绕"agent-skills"这个核心概念,讲清楚三件事:技能到底是什么、怎么设计一套技能体系、以及在实际落地时会遇到哪些坑。
先给一个我自己的定义,方便后面对齐:智能体技能(Agent Skill),是指智能体在特定情境下,为达成某个目标而能够稳定调用的一组能力单元。它可以是调用一个API、执行一段代码、检索一类知识、触发一次流程,也可以是几个基础能力的组合。关键在于"稳定"和"可复用"——一次性的、写死在提示词里的操作,不算技能。
为什么这个概念值得单独拿出来讲?因为绝大多数智能体项目失败,不是因为模型不够强,而是因为技能体系设计得太乱。提示词里塞了几百行规则,工具函数散落在各个文件,模型不知道该在什么时候调用什么,最后表现就是"时好时坏"。这不是模型的问题,是工程的问题。
这篇文章适合三类人看:正在做智能体应用但感觉"越做越乱"的开发者、准备从零搭建智能体系统的工程师、以及想理解智能体能力边界的产品同学。我会尽量用大白话把原理讲透,同时给出可以直接抄的代码结构和配置示例。
2. 技能的本质:把"会做"拆成"能稳定做"
2.1 技能不是提示词,也不是工具函数
很多人一开始会把技能等同于"写在系统提示词里的一段指令"。比如"你是一个客服助手,当用户询问退款时,按照以下步骤处理……"。这确实能让模型做对一次,但它有三个致命问题:不可测试、不可复用、不可组合。
我见过一个项目,系统提示词写了将近三千字,里面混杂了角色设定、业务规则、输出格式、异常处理。结果就是每次改一个小需求,都要重新读一遍全文,改完还容易顾此失彼。这就是把"技能"和"提示词"混为一谈的代价。
技能应该是独立的、有明确输入输出的单元。它有自己的名字、描述、参数定义、执行逻辑和失败处理。提示词只是技能被调用时注入给模型的那部分上下文,而不是技能本身。
2.2 一个技能的最小结构
我习惯把每个技能定义成这样一个结构,用JSON或者YAML描述都可以:
name: query_order_status description: 根据订单号查询订单当前状态,返回状态码和描述 parameters: - name: order_id type: string required: true description: 订单号,通常为12位数字 returns: type: object fields: - status_code: integer - status_text: string - updated_at: string failure_modes: - code: ORDER_NOT_FOUND handling: 提示用户确认订单号是否正确 - code: TIMEOUT handling: 重试一次,仍失败则告知用户稍后再试这个结构看起来简单,但它解决了几个关键问题:模型知道这个技能是干什么的、需要什么参数、会返回什么、失败了怎么办。这四点缺一不可。我见过太多项目只写了name和description,结果模型调用时参数乱填,或者拿到返回值不知道怎么用。
2.3 为什么"稳定"比"聪明"更重要
这里要讲一个反直觉的观点:在智能体系统里,技能的稳定性比智能程度重要得多。
原因很简单。一个智能体在一次任务里可能要调用五到十个技能,如果每个技能的失败率是5%,那么整体成功率就只有 0.95^10 ≈ 60%。也就是说,十个"看起来还行"的技能组合起来,系统就有四成的概率失败。这个数字在实际项目里是灾难性的。
所以我在设计技能时,第一优先级永远是降低不确定性。具体做法包括:参数做严格校验、返回值做结构化约束、失败路径提前定义、能确定性完成的事情绝不交给模型判断。比如"订单号必须是12位数字"这种规则,就应该在技能入口处用代码校验,而不是指望模型每次都记得。
提示:判断一个技能是否合格,有个简单的标准——如果把它单独拿出来,写一个单元测试,你能不能明确地断言"输入A一定得到B"。如果做不到,说明这个技能的边界还不够清晰。
3. 技能体系的分层设计:别把所有能力堆在一层
3.1 三层结构:原子技能、组合技能、编排技能
当技能数量超过十个之后,如果不分层,整个系统会迅速失控。我一般会把它分成三层:
原子技能(Atomic Skills)是最底层的能力,直接对接外部资源。比如"调用天气API""读取数据库某张表""发送一封邮件"。这一层的特点是:单一职责、无状态、可独立测试。
组合技能(Composite Skills)是把几个原子技能按固定顺序组合起来,完成一个稍大的任务。比如"查询用户最近订单并生成摘要",它内部可能调用了"查询订单列表"和"文本摘要"两个原子技能。这一层的特点是:有固定的编排逻辑,但仍然不涉及复杂的条件分支。
编排技能(Orchestration Skills)是最上层,负责根据情境动态决定调用哪些组合技能、以什么顺序调用、遇到分支怎么处理。这一层通常由模型驱动,也是整个系统里最"智能"的部分。
分层的价值在于隔离变化。底层API变了,只改原子技能;业务流程变了,改组合技能;策略变了,改编排技能。三层互不干扰。我见过不分层的项目,改一个接口地址要翻遍整个代码库,那滋味真的不好受。
3.2 每层该放什么、不该放什么
这里有个容易踩的坑:把业务判断逻辑放错层。
举个例子,"VIP用户的订单优先处理"这条规则,应该放在哪一层?如果放在原子技能里,那每个原子技能都要判断一次用户等级,重复且难维护。正确做法是放在编排层,由编排逻辑统一决定调用顺序。原子技能只管"查订单",不关心"谁优先"。
再比如,"查询订单失败时返回空列表还是抛异常",这属于原子技能的内部约定,应该在这一层定死,不要让上层去猜。上层的组合技能只需要知道"这个技能要么返回有效数据,要么返回明确的错误码"。
我总结了一个简单的判断标准:如果一个逻辑和"外部资源怎么访问"有关,放原子层;如果和"多个资源怎么配合"有关,放组合层;如果和"根据情况选哪条路"有关,放编排层。
3.3 技能注册表:让模型知道有哪些能力可用
分层之后,还需要一个技能注册表(Skill Registry),把所有技能的名字、描述、参数、返回值集中管理。这个注册表有两个用途:一是给模型看,让它知道有哪些能力可以调用;二是给代码用,做参数校验和路由。
注册表不需要很复杂,一个JSON文件或者一个数据库表就够了。关键是要保持和实际技能实现同步。我吃过这个亏:注册表里写了一个技能,但实际代码里已经删了,结果模型调用时直接报错。后来我加了一个启动时的自检,遍历注册表里的每个技能,确认对应的实现存在,否则直接启动失败。这个检查帮我省了无数次排查时间。
# 启动自检的简化示例 def validate_registry(registry, implementations): for skill_name in registry: if skill_name not in implementations: raise RuntimeError(f"技能 {skill_name} 在注册表中但无实现") for impl_name in implementations: if impl_name not in registry: raise RuntimeError(f"技能 {impl_name} 有实现但未注册")这段代码很短,但它保证了一个基本契约:注册表和实现永远一致。在多人协作的项目里,这个检查尤其重要。
4. 让模型"会用"技能:描述、示例与约束
4.1 技能描述怎么写,模型才看得懂
技能注册表建好了,接下来要解决的是"模型怎么知道该调用哪个技能"。这取决于技能描述的质量。我见过很多描述写得像给人类看的文档,模型根本抓不住重点。
好的技能描述应该包含三个要素:什么时候用、需要什么、会得到什么。而且要用模型容易理解的自然语言,避免歧义。
对比一下:
差的描述:"查询订单状态。"
好的描述:"当用户询问某个订单当前处于什么状态(如已发货、已签收、待付款)时使用此技能。需要提供订单号。返回订单的当前状态码和文字描述。"
差别在哪?好的描述明确了触发场景(用户询问订单状态)、输入要求(订单号)、输出内容(状态码和描述)。模型看到这样的描述,就知道在什么情况下该调用它。
4.2 给每个技能配一两个调用示例
光有描述还不够,模型有时候会"理解偏差"。这时候示例是最有效的补充。我一般会给每个技能配一到两个典型的调用示例,包括输入和期望的输出。
{ "skill": "query_order_status", "example_input": {"order_id": "202405170001"}, "example_output": { "status_code": 3, "status_text": "已发货", "updated_at": "2024-05-17T10:23:00Z" } }示例的作用是锚定格式。模型看到示例后,调用时参数格式、返回值理解都会更准确。实测下来,加了示例的技能,调用正确率能提升不少,尤其是参数格式比较特殊的技能。
4.3 用约束把模型的自由度收窄
模型的能力是"灵活",但灵活在工程里往往意味着"不可控"。所以我的原则是:能约束的地方尽量约束,把自由度留给真正需要判断的地方。
具体做法包括:参数类型用枚举而不是自由文本、返回值用固定结构而不是自然语言、调用顺序用流程图而不是让模型自己排。比如"订单状态"这个参数,如果可能的值只有"待付款、已付款、已发货、已签收、已取消"这五种,那就用枚举,不要让模型自由发挥。
注意:约束不是越多越好。约束太多会让模型"不敢动",遇到稍微超出预期的情况就卡住。我的经验是,约束应该集中在输入输出格式和关键业务规则上,中间的推理过程可以留给模型。
5. 实战中最容易翻车的几个地方
5.1 技能粒度过粗或过细
粒度问题是设计技能时最纠结的。太粗,一个技能干太多事,复用性差;太细,技能数量爆炸,编排复杂度飙升。
我的经验法则是:一个技能对应一个"用户可感知的完整动作"。比如"查询订单"是一个动作,"发送通知"是一个动作,但"查询订单并发送通知"就不该是一个技能,而应该是组合技能。
判断粒度是否合适,可以问自己:这个技能的名字能不能用一句话说清楚,而且这句话里没有"并且""然后"这样的连接词。如果有,说明它该拆了。
5.2 错误处理被当成事后补丁
我见过太多项目,技能的错误处理是最后才加的,结果就是各种边界情况没覆盖,线上频繁出问题。正确做法是在设计技能时就把失败路径想清楚。
每个技能至少要回答三个问题:失败了返回什么、上层怎么知道失败了、失败了之后怎么办。这三个问题的答案应该写进技能定义里,而不是散落在代码各处。
我一般会把失败分成三类:可重试的(如网络超时)、不可重试的(如参数错误)、需要人工介入的(如权限不足)。不同类型的失败,处理策略完全不同。可重试的自动重试,不可重试的直接返回错误,需要人工介入的触发告警。
5.3 技能之间的依赖关系没理清
当技能数量多了之后,依赖关系会变得复杂。A技能依赖B技能的输出,B技能又依赖C技能。如果没理清,很容易出现循环依赖或者调用顺序错误。
我的做法是画一张依赖图,明确每个技能依赖哪些其他技能。然后在编排层严格按照依赖顺序调用。这张图不需要很正式,手画或者用简单的文本描述都行,关键是要有。
query_user_profile └── query_user_base_info └── query_user_order_summary └── query_order_list └── query_order_status有了这张图,排查问题时就能快速定位是哪个环节出了岔子。
5.4 忽略了技能的"上下文需求"
有些技能在执行时需要额外的上下文信息,比如当前用户ID、会话历史、时间戳等。这些信息如果没提前准备好,技能调用时就会缺参数。
我的做法是在编排层维护一个上下文对象,所有技能需要的信息都从这里取。技能本身不关心上下文从哪来,只关心"我需要的东西在不在"。
context = { "user_id": "u_12345", "session_id": "s_67890", "timestamp": "2024-05-17T10:00:00Z", "history": [...] }这样设计的好处是,技能的输入变得统一,测试时也容易构造。我可以在测试里直接塞一个假的context,不需要真的去查数据库。
6. 从零搭一套技能体系的完整路径
6.1 第一步:列出所有需要的能力
不要一上来就写代码。先拿一张纸,把所有"智能体需要会做的事"列出来。这个列表可以很粗糙,想到什么写什么。
列完之后,按"用户可感知的动作"做一次归并。比如"查订单""查物流""查退款进度"可能可以归并成"查询订单相关信息"这一类。归并的目的是减少技能数量,提高复用性。
6.2 第二步:给每个能力定义输入输出
对归并后的每个能力,定义它的输入参数和返回值。这一步要尽量具体,参数类型、是否必填、取值范围都写清楚。
我一般会用表格来整理,一目了然:
| 技能名 | 输入 | 输出 | 失败模式 |
|---|---|---|---|
| query_order_status | order_id (string, 必填) | status_code, status_text | ORDER_NOT_FOUND, TIMEOUT |
| send_notification | user_id, message | success (bool) | USER_NOT_FOUND, RATE_LIMITED |
| query_user_profile | user_id | profile (object) | USER_NOT_FOUND |
表格整理完,技能体系的骨架就出来了。
6.3 第三步:实现原子技能并单独测试
先实现最底层的原子技能,每个都写单元测试。测试要覆盖正常路径和至少两个失败路径。这一步不能省,因为原子技能是所有上层的基础,它们不稳,上层再花哨也没用。
我一般会用一个简单的测试框架,给每个技能写三到五个测试用例。跑通了再往上搭。
6.4 第四步:组合技能和编排逻辑
原子技能稳定之后,开始搭组合技能。组合技能的逻辑相对固定,可以用代码写死,也可以用简单的配置描述。我倾向于用代码,因为调试方便。
编排逻辑是最灵活的一层,通常需要模型参与。这一层的设计要点是:给模型足够的自由度,但用约束兜底。比如允许模型决定调用顺序,但每个技能的参数必须符合定义。
6.5 第五步:端到端测试和迭代
最后是端到端测试。构造一批真实的用户请求,看整个系统能不能跑通。跑不通的地方,回到对应的层去修。
这个过程通常要迭代好几轮。我的经验是,前两轮修的是明显的bug,后面几轮修的是"边界情况"和"体验问题"。边界情况包括空输入、超长输入、特殊字符等;体验问题包括响应太慢、错误提示不友好等。
7. 一些不那么显然的经验
7.1 技能命名要"动词+名词",别用缩写
命名这件事看起来小,但影响很大。我坚持用"动词+名词"的格式,比如query_order_status、send_notification。不要用缩写,不要用拼音,不要用内部黑话。
原因很简单:模型是靠名字和描述来理解技能的。名字越清晰,模型越不容易搞错。我见过用qos这种缩写的项目,模型经常把它和别的技能搞混。
7.2 给技能加"使用频率"标记
有些技能经常用,有些很少用。我一般会在注册表里加一个frequency字段,标记为high、medium、low。这个信息有两个用途:一是编排时可以优先考虑高频技能,二是排查问题时可以优先看高频技能。
7.3 定期清理"僵尸技能"
项目做久了,总会有一些技能不再被调用。这些"僵尸技能"会占用注册表空间,干扰模型判断。我一般每个季度做一次清理,把连续一个月没被调用的技能标记出来,确认无用后删除。
7.4 技能版本管理
技能是会变的。今天query_order_status返回三个字段,明天可能变成五个。如果直接改,上层的组合技能可能就崩了。所以技能需要版本管理。
我的做法是在技能名里带版本号,比如query_order_status_v2。新版本上线后,旧版本保留一段时间,等所有调用方都迁移完再下线。这样虽然名字丑一点,但安全。
7.5 别让模型"猜"技能的存在
有些项目为了"智能",不给模型明确的技能列表,而是让模型自己"想"有什么能力。这种做法在demo里很酷,在生产环境里是灾难。模型不知道有哪些技能,就会瞎编,或者该调用的时候不调用。
正确做法是把技能列表明确地告诉模型,让它从已知的技能里选。这样虽然少了点"惊喜",但稳定得多。
8. 关于技能评估:怎么知道体系搭得好不好
8.1 三个核心指标
评估一套技能体系,我主要看三个指标:调用准确率、任务完成率、平均调用次数。
调用准确率是指模型选对技能的比例。这个指标低,说明技能描述或示例有问题。任务完成率是指端到端任务成功的比例。这个指标低,可能是编排逻辑有问题。平均调用次数是指完成一个任务平均调用多少个技能。这个指标异常高,说明技能粒度太细或者编排效率低。
8.2 用真实请求做回归测试
每次改动技能体系后,我都会跑一遍回归测试。测试集是从真实请求里采样出来的,覆盖各种典型场景。跑完对比改动前后的指标,确认没有退化。
这个习惯帮我避免了好几次"改一个地方,坏三个地方"的事故。
8.3 定期做"技能审计"
除了指标,我还会定期做一次"技能审计",检查每个技能是否还在被使用、描述是否还准确、失败处理是否还合理。审计的结果通常是:删掉几个僵尸技能、更新几个过时的描述、补充几个遗漏的失败模式。
这件事不紧急,但很重要。不做的话,技能体系会慢慢腐化,最后变成一团乱麻。
9. 我个人的一点体会
做智能体系统这几年,最大的感受是:技能体系的设计,本质上是在"灵活"和"可控"之间找平衡。模型越强,我们越容易倾向于给它更多自由,但生产环境要求的是稳定。所以我的策略一直是:把确定性的事情用代码锁死,把不确定的事情交给模型,但用约束兜底。
另一个体会是,技能体系不是设计出来的,是迭代出来的。一开始想得再周全,实际跑起来总会发现新的问题。所以不要追求一次设计完美,而是搭一个能快速迭代的框架,然后根据实际反馈不断调整。
最后分享一个小技巧:如果你不确定一个技能该怎么设计,就先写它的"失败处理"。把失败路径想清楚了,正常路径往往也就清晰了。这个方法我用过很多次,屡试不爽。