ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Agent-Skills实战:从工具函数到技能封装,打造真正会干活的智能体

Agent-Skills实战:从工具函数到技能封装,打造真正会干活的智能体

去年底我在做一个内部的智能体项目,当时团队对“agent-skills”这个词的理解还非常混乱。有人说是给模型挂一堆工具函数,有人说是一套提示词模板,还有人直接把它等同于工作流。折腾了两个月,踩了不少坑,才慢慢摸清楚:agent-skills 到底是什么,它和工具、提示词、工作流之间的边界在哪里,以及真正可落地的一套技能体系应该怎么设计。这篇文章就顺着这条线,把我自己的理解、实操过程和踩过的坑一次性说清楚。

先说结论:agent-skills 是一套让智能体真正“会干活”的能力封装,它不是单个工具函数,也不是一段固定的 prompt,而是把意图理解、参数解析、工具调用、内部逻辑、异常兜底和结果反馈打包在一起的完整闭环。

这篇文章适合两类人:一类是正在搭建智能体应用,但发现自己做的智能体总是“聊得很好、干不了活”的开发者;另一类是已经开始用 skill 但感觉效果不稳定的朋友。下面所有内容都以我实际做过的项目为背景,方案也是验证过的,可以直接照着抄。

1. agent-skills 到底在解决什么问题

1.1 从“能聊天”到“能干活”,差距到底在哪里

如果你只让模型回答“今天是几号”“帮我写一段文案”,那确实不需要 skills,一个对话接口就够了。但一旦涉及“帮我把这批数据清洗后导入系统”“根据昨天的报表生成一份邮件摘要并自动发送”这类多步骤任务,问题就来了。

最直接的体验是:模型经常在关键环节掉链子。它可能知道要调用某个函数,但参数传不对;可能第一步做对了,第二步就开始胡编;更常见的是,遇到异常情况完全不知道该怎么处理,要么卡住,要么给用户编一个假的结果。

我最初以为这是模型能力不行,后来发现根子在于:我们只给了模型一堆零散的能力,却没有给这些能力一个“技能”层面的组织方式。就像你给一个人工具箱里塞满了扳手、螺丝刀,但没告诉他哪把工具对应哪个活儿、干活的顺序是什么、干砸了该怎么办。这人当然干不好活。

1.2 skill 的本质:把“能力+逻辑+兜底”打包

在我现在的理解里,一个 skill 应该具备三样东西:能力本身(能够执行的操作或计算)、使用方式(模型或用户需要提供的输入)、以及行为边界(什么条件下可以用、失败时该怎么反馈)。

举个例子,一个“发送邮件”的 skill,最朴素的写法是一个叫send_email的函数,接收收件人、主题、正文三个参数。但真正生产级的写法需要更多:收件人格式校验、正文敏感词过滤、调用外部邮件接口失败时的重试策略、返回值里带上发送结果和失败原因。

这个例子很直观:模型拿到的是函数,是单一动作;智能体需要的是 skill,是能承担一个完整任务的最小闭环。

1.3 为什么直接写工具函数还不够

市面上很多智能体框架都支持自定义工具(tool),至少形式上支持。但工具给我的感觉是“接口导向”的,它告诉模型“这里有个函数可以调用”,但对模型来说,这个函数只是在某个正确的时机、填对参数才叫“有用”。为了让模型正确调用一个工具,你往往需要在系统提示词里写大量解释、给示例、约束边界。

而 skill 是在工具之上再做一层组织。它可以包含多个工具的调用顺序、内部状态处理、中间结果的判断,甚至可以嵌入一部分业务规则。相当于把“怎么做”的细节藏起来,只把“做什么、需要什么、什么时候做”暴露给模型。

打个比方:工具是字典里的一个词条,skill 是一个完整的短语甚至句子。模型不需要知道这个句子里的语法结构,它只需要知道在什么场景下用这个句子,以及句子里需要填哪些空。

2. 构建技能体系前的必备设计:粒度、接口与注册机制

2.1 skill 的粒度问题:一个 skill 该管多大事

这是我见过最多的翻车点。很多人建 skill 立两极分化:要么建得特别大,把整个业务流程塞进一个 skill 里;要么建得特别碎,一个“读取文件”就建一个 skill,最后注册了几百个,模型选择起来反而更懵。

我自己的经验是:skill 的粒度应该等同于“模型的一次有效决策”。也就是说,当模型看了用户的需求之后,它应该能判断“这个需求可以完全交给某一个 skill 完成”,而不是需要组合好几个 skill 才能完成。

如果一个任务需要拆成好几个步骤,但步骤之间的决策依赖很强、需要临时判断下一步做什么,那把它拆成多个 skill 反而更好。因为模型可以在步骤之间进行判断,这比在 skill 内部硬编码流程要灵活得多。

反之,如果步骤完全固定,比如“先查天气再根据天气推荐穿衣”,那就应该打包成一个“穿衣推荐” skill,而不是拆成“查天气”和“推荐穿衣”两个。拆开之后,模型可能调用第二个时忘了第一个的结果,或者天气查到了却不知道怎么传给推荐函数。

2.2 skill 的入口与出口:输入输出 schema 是关键

我给 skill 设计接口时,最核心的原则是:参数数量要少,参数概念要直白,参数约束要清晰。

参数数量上,能少则少。能用一个复合参数就不要拆成两个平铺参数。以“发送邮件”为例,与其拆成to、cc、subject、body、scheduled_time五个参数,不如设计成email_message一个对象,里面包含主题、正文、收件人、抄送人。模型生成一个嵌套结构,远比生成五个平铺字段更不容易出错。

参数概念上,要避免让模型去“猜业务含义”。比如一个“生成周报”的 skill,如果你给模型的参数是aggregated_data,模型根本不知道传什么;如果你写成work_summary_notes(本周工作要点笔记),模型即使没有真实数据也可以把会话中提到的内容放进去。

输出方面,不要只返回一个成功标志。要给模型一个结构化的结果,至少包含:任务是否完成、执行结果摘要、需要继续处理的信息(比如出错原因、缺失参数)。这很重要,因为模型下一步往往要根据这个结果来决定再调用哪个 skill。

2.3 skill 和 tool 的关系:组合与分工

基于我的实践,tool 更贴近“原子操作”,比如读文件、调 API、查数据库;skill 则是一种“脚本化能力”,它既可以直接实现逻辑,也可以编排多个 tool。

一个反直觉的经验是:不在乎用的是框架内置的 tool 还是自定义写死逻辑,关键是让 skill 的对外表现稳定。稳定性指的是:在同样的输入条件下,skill 的输出格式和返回结构要保持一致,哪怕是失败,失败信息的结构也得统一。这样模型才能学习到规律,否则它会因为找不到规律,开始随缘调用。

2.4 技能注册中心:让智能体知道自己有什么

在动手写代码之前,一定要先设计一个“技能注册中心”。它的作用是统一管理每个 skill 的名称、描述、参数定义和环境要求,让智能体运行时能拿到一份“技能列表”来决策。

注册中心里我特别重视两个字段:description和when_to_use。前者告诉模型这个技能能做什么,后者告诉模型什么场景下该用它。很多人写技能描述时喜欢堆关键词,比如“邮件、发送、通知、群发”,其实模型更需要的是一句场景化描述:“当用户需要发送邮件,包括单发和批量发送时使用。用户在对话中提到收件人、主题等邮件要素,可以使用此技能”。

我后来把技能注册中心设计成一个纯 JSON 的清单,提交新的 skill 时就加一条记录。好处是技能列表不混在代码里,方便排查看,也方便给智能体做“能力路由”。

3. 实操:从零实现一个靠谱的 skill

3.1 从目录结构开始:每个 skill 都是独立小项目

我建议每个 skill 在代码上都是一个独立目录,而不是把全部逻辑堆在一个文件里。一个标准的 skill 目录长这样:

skills/ ├── send_report_email/ │ ├── __init__.py │ ├── skill_meta.json # 注册信息 │ ├── main.py # 技能执行入口 │ ├── validator.py # 参数校验逻辑 │ └── tests/ │ ├── test_happy_path.py │ └── test_failures.py

这么做的好处非常直接:首先,技能的边界清晰,不会互相乱引用;其次,测试可以独立运行,而不需要起整个智能体环境;最后,当某个 skill 出问题时,可以快速定位是它自身的 bug 还是外部依赖的问题。

3.2 接口定义与参数收敛的示例

下面这个例子是模拟项目中的一个 skill:“查询订单物流状态”。它的参数被收敛成两个:order_ids(用户下单选中的订单号列表)和user_context(当前对话上下文中的用户身份信息)。

from dataclasses import dataclass, field from typing import List @dataclass class LogisticsSkillInput: order_ids: List[str] = field(default_factory=list) user_context: dict = field(default_factory=dict) @dataclass class LogisticsSkillResult: success: bool summary: str detail: List[dict] = field(default_factory=list) error_type: str = "" error_message: str = ""

我把输入和输出都定义为 dataclass。输入定义参数的结构,输出统一为结果对象。这样开发时 IDE 能自动补全,测试时也能很方便地构造样例。

3.3 内部的防御机制:校验、重试、失败反馈

skill 的代码虽然不会很长,但内部的防御一点不能少。我最常见的问题是模型传参不规范,比如order_ids传成了一个字符串而不是列表。这种事在测试时很少遇到,一旦到了真实把模型接到运行时,天天遇到。

所以在入口处,我会做一层强制的参数归一化:

def run_skill(inputs): # 参数归一化 order_ids = inputs.get("order_ids") if isinstance(order_ids, str): order_ids = [order_ids] if not isinstance(order_ids, list) or len(order_ids) == 0: return LogisticsSkillResult( success=False, summary="订单号缺失或格式不正确", error_type="PARAM_ERROR", error_message="order_ids must be a non-empty list", ) # 业务逻辑...

重试逻辑也比较讲究。有一次调用物流查询接口时,第三方接口偶发 5xx,最开始没加重试,结果模型看到失败反馈后,自己编了一个“订单已签收”的回复,这非常危险。后来我在所有可能临时出错的环节都加了一轮重试,最多两次,间隔 500 毫秒。重试仍失败就返回明确的错误码,让模型如实告诉用户接口暂不可用。

3.4 把 skill 暴露给模型:描述怎么写

注册信息里的描述直接决定模型在什么时机调用这个 skill。写描述有两条硬要求:第一,不能夸大能力,不能把只能查物流说成“还能预测送达时间”;第二,要把输入要求写清楚,比如“用户需要提供完整的订单号列表”。

我的经验是,写描述时多写“不适用”的场景,比多写“适用”场景更管用。比如这么写:

“当用户询问多个订单的物流轨迹时使用。仅支持查询已下单的订单状态,不支持下单、修改地址、申请退款等操作。若用户没有提供订单号,请先向用户索要订单号。”

这里面的关键是最后一句:“若用户没有提供订单号,请先向用户索要订单号”。有了这句,模型在参数不全时就不会硬填一个假订单号,而是会主动追问用户。我之前在这个细节上栽过几次跟头,后来把参数缺失时的引导话术写进描述里,问题直接少了一大半。

3.5 联调与自测:别只测 happy path

这部分我会启动一个模拟对话环境,用户发一段请求,观察模型是否选对了 skill、参数是否正确、结果是否传达清楚。除了开心路径,我专门准备了一批刁钻输入:

  • 用户口述的订单号是中文数字(“一二三号订单”);
  • 用户同时问了订单状态和售后入口(涉及两个 skill 的决策);
  • 用户要求查询一个根本不存在的订单号;
  • 技能接口超时并返回 5xx,观察模型是否如实说明。

这些用例能暴露很多提示词和注册信息的问题。我甚至有一个暴力的测试方式:故意写错 skill 描述里的一个关键词,跑一遍,看模型的行为会发生什么变化。这个方法很费时间,但对理解模型与 skill 的交互非常有帮助。

4. 常见问题与排查技巧实录

4.1 模型总是不调用该调用的 skill 怎么办

现象:用户需求明确,描述也写得清清楚楚,但模型就是不走 skill,而是自己凭常识回答。

这种情况我排查三步走。

第一步,检查是不是别的高优先级指令干扰了。有些项目的系统提示词里写了“你是一位无所不能的助手”,模型觉得自己什么都会,自然不会看技能描述。把系统提示词往收敛方向改会好很多。

第二步,检查 skill 描述是否“任务导向”。描述里写“这是物流查询模块”效果很差,改成“当用户需要查询订单的物流轨迹时,必须使用该模块”效果立竿见影。

第三步,检查注册列表里是否有相似技能。如果一个“订单详情查询”和一个“物流轨迹查询”同时存在,模型很容易混淆。这属于技能矩阵设计问题,建议合并或者写清楚二者的差异边界。

4.2 参数传得乱七八糟,模型不按 schema 来

我在某个项目里遇到了一个特别离谱的现象:模型把整个用户输入原封不动地塞给了唯一的参数。比如我把参数定义成order_ids,模型直接传“请查询订单号 123 和 456 的物流信息”这一整句。

后来发现,问题出在我给的参数描述不够“原子化”。我在参数描述里只写了“订单号列表”,没有说明“这是一个纯数据字段,不要包含任何自然语言”。后来我把参数描述全都改成“该字段只接受订单号数据,禁止混入描述性文本、标点符号或疑问句”,情况立刻好转。

另外,尽量用明确的枚举类型或对象结构,而不要用自由字符串。比如状态字段,给模型选项而不是让它自己发挥:

{ "name": "query_status", "type": "string", "enum": ["pending", "shipped", "delivered", "failed"], "description": "用户想要查询的物流状态节点" }

模型在枚举约束下出错率明显低于自由文本。

4.3 skill 内部出错,模型开始胡编结果

这是最可怕的一种情况。某个 skill 返回了{"success": false, "error_message": "服务超时"},但模型给用户的回复是“您的订单已派送,预计明天到达”。

原因我后来想明白了:技能返回的错误信息不够“解释性”。模型看到success是 false,但知道下一步该怎么办。这时候必须返回一个可供模型直接转述给用户的消息。

就刚刚那个场景,我改成在summary字段里直接返回:“物流接口暂时不可用,建议用户稍后再试,或拨打人工客服电话咨询。”模型看到这句,就不会自己编了。

原则是:skill 的失败信息,要写成可以直接给用户看的句子,而不是给程序看的错误码。错误码可以附在另一个字段里,但summary必须是可转述的自然语言。

4.4 提示词里技能文档太长,上下文放不下

技能多了之后,全部描述塞进提示词里,占的上下文会非常可观。我遇到过智能体上下文明显变笨,经常忘事,排查后才发现是技能描述占了将近一半的上下文窗口。

解决思路有两个:一是浓缩描述,二是动态加载描述。浓缩描述就是在注册中心里只保留最核心的一句话,更多细节放到一个“技能详情文档”里,只有当模型选择调用该技能时,才把详情文档塞进上下文。动态加载类似于路由,实际上是先用一句话粗筛,再展开详细说明。这个策略在我自己的项目里省出了大约 40% 的上下文空间。

4.5 安全问题:skill 越权访问数据

之前我忽略了一个问题:skill 一旦能被模型自由调用,它的入口就相当于暴露给了模型。如果输入里带了一些不该给它的敏感信息,比如用户身份凭据,模型可能会把这些内容作为普通参数传给 skill。

后来我给 skill 加了一层访问策略:只有特定场景才允许访问用户敏感数据,并且敏感字段不会直接出现在技能上下文中。同时,skill 的日志里会对敏感信息做脱敏处理。

提示一个更隐蔽的坑:skill 的输出会被模型看到后再生成回复,所以如果 skill 从外部接口拉到的数据包含用户隐私,这些数据也会进入模型上下文。在设计时,要确认哪些信息逐字透出、哪些信息只给一个结果摘要。

5. 技能库的演进与维护:做一次能长期用的设计

5.1 技能复用:先沉淀“通用能力”,再长“业务技能”

我曾经为了赶进度,把业务逻辑写死在技能里,比如直接在“查询物流”技能里连了某个商家后台的数据库。后来另一个项目也要查物流,但因为业务数据源不同,只能复制一份逻辑,改改配置再上。这既浪费了代码,也增加了后期的维护成本。

更合理的方式是:把“连接物流系统并查询轨迹”沉淀为一个底层工具,把“将用户需求转化为查询参数”做成 skill。这样不同业务的技能,可以复用同一个底层工具。技能负责理解用户,工具负责对接系统,各管一头。

5.2 技能版本迭代:改一个描述也可能影响全局

技能的描述不是改完就完。我遇到过这样一个事:更新了一个技能描述,说要加上“支持批量查询”的说明,结果原本正常走单查询请求的用户,也开始被模型引导去做批量查询,而批量查询的性能根本扛不住。一个小改动就能改变模型的调用习惯,所以凡是涉及描述、参数、触发场景的改动,我都建议在独立环境上跑一遍回归测试再上线。

5.3 建立一套轻量的评估集

说到评估,不要一开始就追求复杂指标。我建了一个非常简单的 JSON 文件作为评估集,里面塞了三十条用户请求,每一条都已经标注了“期望调用的技能名称”和“期望参数值”。

每次改完代码,跑一遍这个评估集,看看有几条正确命中。这样,技能库改成什么样,心里有数。后来我把它接入了开发流程,只要 skill 代码有变更,就自动跑一遍,避免上一次改动把以前的逻辑搞坏了。

这个评估集维护起来也不算很费劲,看到新出现的错误样例就补充进去就好。半年下来,已经有几百条,覆盖率比人工反复点测试高出不少。

6. 我对 agent-skills 的几点个人体会

这套体系做完之后,最大的感受是:agent-skills 不是模型能力,而是架构能力。模型本身就那一个,技能库设计得好不好、描述写得清不清楚、边界划得明不明朗,基本决定了一个智能体是“好用”还是“鸡肋”。

在实际操作中,我发现建模 skill 的时候,多花时间在参数设计和描述上是最值的。那些细节不看几个真实项目,很容易被忽略。比如一个“查询物流”的技能,八十行逻辑没什么难点,但“参数里不要混自然语言”这七个字,比一整段业务代码更关键。

还有一点切身的体会:别太迷信“智能体自己会编排”。至少在我做过的实际项目里,让模型在多个 skill 之间做自由组合,很考验框架设计和提示词设计。对大部分生产需求,开发一个覆盖面合适的 skill 目录,让模型只做“选哪个技能”的决策,而不是“怎么组合技能”的设计,可靠性要高很多。

最后分享一个小技巧:每做完一个 skill,花十五分钟写一段“我上回在这个技能上翻过什么车”的备注,放在目录的 README 里。技能多了之后,人一定会忘,但这个备注会成为排查问题时最快的线索来源。这也是我目前所有技能目录里,最被团队其他人看重的文档。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进