"Agent技能"这个词,最近在我身边的工程圈子里出现频率明显变高了。从最早大家争论Agent该不该有长期记忆,到后来讨论怎么规划任务、怎么调用工具,再到现在,所有人的关注点都落在了同一个问题上:怎么让Agent在处理真实任务时,稳定地拿出"正确的处理能力"。
这个"正确的处理能力",落到工程上就是一套体系化的技能库(skill library)。你可以把它理解成Agent的手和脚:模型负责"想",技能负责"做"。模型判断当下该解决什么问题,技能就是它从工具包里抽出来的那张解题卡。没有技能的Agent,模型再聪明也只能空转;技能定义混乱、描述含糊,模型再强也会被带偏。
这篇文章我把Agent技能从定义、设计、实现到维护完整拆开讲一遍,适合正在做Agent应用开发、或者打算把业务能力往Agent框架里接入的开发者参考。不需要你提前掌握什么复杂框架,只需要写过一点脚本、看得懂配置文件,就能跟着搭出一套可用的技能模块。
1. 先对齐一个概念:Agent技能到底是什么
先说结论:Agent技能是一个自洽的、可被Agent按语义触发并执行的功能单元。它由四部分组成:描述信息、接口定义、实现代码、依赖声明。这句话看起来简单,但很多人一开始都会把技能和"工具""插件"混为一谈,导致后面设计出来的东西既不像工具也不像技能,Agent调用的时候全是问题。
我见过不少团队把Agent技能简单理解成"给模型加了一个函数"。这个方向不能算错,但漏了最关键的一层:技能的核心是"面向任务场景"的匹配,而不是"面向功能"的暴露。同一个功能,如果用传统接口暴露,调用方需要明确知道函数签名和参数格式;如果用技能的方式暴露,Agent只需要理解"这个场景该用这个能力",剩下的参数组装、调用、结果处理都由技能模块自己负责。
这种差异直接影响了设计方式。技能里的描述、触发条件、参数默认值、错误处理策略,权重都比普通函数要高得多。说得夸张一点,技能设计得好不好,有时候不取决于代码写得多漂亮,而取决于描述文案写得是否让Agent"看得懂、选得对"。
1.1 它和工具、插件的区别在哪
很多框架里,"工具""插件""技能"这三个词经常混用。但如果细看它们在工程上的设计焦点,还是有明显差异:
| 维度 | 工具 / 插件 | Agent技能 |
|---|---|---|
| 调用的驱动方式 | 偏功能入口,模型按需调用 | 偏任务场景,模型按语义匹配后调用 |
| 粒度 | 通常较粗,一个接口对应一组操作 | 更细,面向一个完整的小任务闭环 |
| 描述的重要性 | 中等,有函数签名就能用 | 极高,描述决定召回率与触发准确度 |
| 参数处理 | 多为显式传参,调用方负责组装 | 内置参数校验、默认值与组装逻辑 |
| 生命周期 | 模块级维护 | 技能库级维护,可组合、可评测、可回滚 |
我这么说不是要制造概念对立。实际框架中可以完全不做区分,模块照样能跑。但设计思路不同,后期维护成本会很不一样。如果你把技能当工具来做,就会忽略触发条件、负面样例、失败回退这些关键设计点;如果你把工具当技能来做,又会陷入"一个能力拆成几十个技能"的过度设计,搞得Agent每次决策都在一堆相似描述里纠结。
1.2 技能在Agent执行循环里的位置
一个典型的Agent工作循环分成四段:感知、规划、执行、观察。感知负责理解用户输入和环境信息,规划负责把任务拆解成步骤,执行负责调用具体能力,观察负责评估执行结果并决定下一步。
技能在"规划"和"执行"之间扮演了关键桥梁。没有技能体系的时候,Agent的执行就是固定写好的几个函数,模型只能挑着用;有了技能体系之后,Agent可以先检索技能库,选出最匹配的技能,再传入上下文参数执行。而且,技能的调用结果还会带着结构化的元信息返回给模型,比如执行状态、耗时、错误码,这些信息会直接影响模型下一步的决策。
我在实际项目里的感受是:技能模块做得扎实的Agent,明显更"听话"。表现在两个地方:一是模型能准确判断什么时候该用哪个能力,而不是频繁误调用或者反复试探;二是执行出问题的时候,模型知道怎么上报、怎么回退,而不至于硬着头皮往下走,最后给用户一个莫名其妙的答案。
2. 拆解Agent技能的核心构成与分类
理解技能是什么之后,接下来要搞清楚它的内部结构。很多开发者觉得"写个函数加段描述就行了",结果技能一多,整个库马上就乱了。一个可维护的技能,一定不是一份代码加一段话那么简单。
2.1 一份技能的四层组成
我一般建议把技能拆成四个层次来看,每一层都有明确责任:
- 描述层:技能叫什么、干什么用、什么场景触发、什么场景不要触发。这一层是给Agent的规划模块看的,决定了它会不会被正确选中。
- 接口层:输入参数有哪些、必填还是选填、默认值是什么、输出结构是什么、失败时怎么返回。这一层是给调用链路的契约,决定了技能能否被稳定执行。
- 实现层:实际的逻辑代码、脚本、数据处理模板。这一层是执行体,可以很简单,也可以很复杂。
- 依赖层:运行环境依赖、外部服务连接、权限要求。这一层决定了技能在什么条件下能跑起来,部署时最容易出问题。
这四层缺一不可。只写描述不写接口,模型选中了也不知道怎么传参;只写接口不写依赖,本地跑得好好的,换台机器就崩。我在帮某个团队梳理历史技能时,发现大量"僵尸技能"都是死在依赖层——描述和接口都还正常,但底层依赖库的版本环境已经没人能还原了。
2.2 从基础能力到元技能
技能不是越细越好,也不是越粗越好,而是要有一个层次结构。按我常用的分类方法,技能大概可以分成四类:
- 基础能力技能:文件读写、网络请求、数据清洗、格式转换这类通用能力。它们不依赖具体业务,可以被上层技能反复组合。
- 领域业务技能:电商订单处理、客服工单流转、财务报表生成这类特定业务场景的完整流程。它们往往由多个基础技能组合而来。
- 复合编排技能:把多个步骤编排成一个稳定流程。比如"日常舆情分析"就是抓取、清洗、关键词提取、生成报告四个动作的组合。
- 元技能:用来管理和优化Agent自身行为的技能,比如任务拆解、自我反思、上下文压缩、技能调用失败后的补救策略。
基础能力和领域业务之间的界限需要根据团队现状判断。技能粒度太粗,模型调用不灵活;粒度太细,模型每次都要做大量组合决策,既慢又容易出错。实用经验是:先从一个完整的业务闭环切入,跑通之后再往下拆,不要一开始就建两三层的技能树。
2.3 技能设计的三条红线
第一,单一职责。一个技能只负责一件完整的事。有些开发者为图省事,把"搜索、抓取、存档"写成一个大技能,结果模型只是想做一次普通搜索,也被迫走了全套流程,又慢又占资源。正确做法是拆成三个技能,再写一个复合技能把它们串起来。
第二,最小依赖。技能能少依赖外部服务就少依赖,尽量把基础设施型操作封装在内部。依赖越少,部署越容易,故障面也越小。一个技能如果对数据库、文件系统和外部API都有硬依赖,那它就是个随时可能爆炸的雷。
第三,边界明确。技能必须清楚定义"自己管什么、不管什么",尤其要写清不该触发的情况。比如一个"订单查询"技能,描述里需要明确"只处理订单状态查询,不处理退款申请",不然用户说"帮我退掉这笔订单"时,模型可能误选这个技能,然后一脸茫然地返回一个"功能不支持"的错误。
3. 从零手写一个Agent技能
理论讲多了容易飘,下面直接落地上手。我带大家写一个常见的技能:网页正文提取。它接收一个URL,抓取网页并清理噪声,返回干净的标题和正文。这是一个典型的基础能力技能,足够说明全流程。
3.1 先用一页纸把接口定清楚
写代码之前,先别急着敲键盘。拿一张纸,把这些问题列清楚:
- 这个技能解决什么问题:给定网页地址,提取正文内容。
- 输入是什么:URL字符串。
- 输出是什么:网页标题、清理后的正文、抓取时间。
- 可选参数是什么:最大返回长度、是否保留图片链接。
- 哪些情况不该触发:用户只是闲聊提到一个链接,不要求提取内容;用户想要的是截图而不是正文。
- 依赖条件:需要网络请求库和HTML解析库。
这个过程看起来简单,但我见过太多人跳过了。直接写代码会导致参数设计得很随意,后面模型调用时各种传参错误。接口定清楚,后面每一步都有据可依。
举个例子,我常看到有人把最大长度参数设计成必填项,用户每次请求都得多带一个数字,一旦不带,模型就会自己随机编一个。正确做法是给默认值,而且默认值要贴合大多数使用场景。
3.2 描述文案写得好不好,直接决定召回率
技能描述是给模型读的,不是给人读的。写描述的核心目标是让模型在正确的场景选中它,在错误的场景忽略它。我见过最典型的错误写法是:
此技能用于网页内容页面内容的信息获取和内容抽取。
这句描述表面上没毛病,但信息量太低。模型只能判断出"这技能和网页有关",至于"什么情况下用、参数带什么、输出长什么样"全都得靠猜。
我用的是"场景加边界"的写法,一份可用的描述长这样:
name: "web_content_extractor" version: "1.2.0" description: | 从指定URL抓取网页正文,自动去除导航、广告、页脚等噪声, 返回结构化标题和正文文本。 适用于用户要求"抓一下这个页面""总结这篇文章""把这个链接的内容存档"等场景。 注意:本技能只负责正文提取,不处理正文摘要,不做内容翻译, 也不适用于截图、登录后才能访问的页面。 tags: [web, crawl, extraction]注意看最后一句"注意:本技能只负责……不处理……"这行不是废话,而是给模型划定的决策边界。实测下来,带有明确负例的描述,触发误判率明显低于只有正面描述的版本。我甚至见过只靠一句负例描述就解决了一整周误召回的案例。
3.3 目录结构、注册加载与初次验证
在技能库中,一个技能推荐使用独立目录存放。目录结构可以这么组织:
skills/ └── web_content_extractor/ ├── config.yaml # 技能元数据、参数定义、依赖声明 ├── skill_main.py # 核心实现代码 ├── test_cases.json # 回归测试用例 └── README.md # 面向维护者的说明config.yaml 里的内容,除了描述之外,还要把接口层和依赖层写清楚:
inputs: url: type: string required: true description: "目标网页的完整URL,包含协议头" max_length: type: int default: 10000 description: "返回正文的最大字符数,超出部分截断" outputs: title: type: string content: type: string fetched_at: type: string depends_on: - requests>=2.31 - beautifulsoup4>=4.12 permissions: network: true filesystem: false注册加载时,我用两种方式验证:一是直接让Agent处理一段自然语言指令,观察是否会选中这个技能;二是构造几个负例指令,比如"帮我截个图""翻译这篇文章",确认模型不会误选。
这里有个实操技巧:验证时不要每次都重新编场景,而是把触发测试的指令沉淀成测试集。第一次可能就五六条,跑通后慢慢补充到几十条。这些指令以后就是技能回归测试的基础资产。
4. 调用执行链路实战拆解
当技能设计和注册完成后,真正决定体验的是Agent执行技能的全过程。这块最容易出问题,也最值得提前想清楚。
4.1 触发与匹配:Agent是怎么选中它的
模型选中一个技能的过程,整体上分为四步:意图解析、候选检索、打分排序、选定执行。
意图解析是把用户的自然语言转换成结构化意图,比如"帮我把这个网页存下来"会被解析成"网页内容存档"意图。候选检索是在技能库中找出可能与这个意图相关的技能,这一步通常依赖语义匹配或关键词匹配。打分排序是根据技能描述与当前意图的匹配度进行排序,选出最优项。最后模型执行选定技能的调用。
了解了这条链路,你就会明白描述文案为什么那么重要。如果描述里关键词覆盖不到"存档"这类表达,候选检索阶段就把它筛掉了;如果描述里没有边界说明,打分排序阶段可能把不相关技能排到前面。我曾经遇到过一个"文本摘要"技能被模型用来处理"图片描述生成",就是因为描述里写了"文本内容信息处理"这种过于宽泛的表述,导致边界完全失效。
4.2 参数组装、执行与结果标准化
选定技能后,模型需要把用户请求中提取的信息填入技能参数。这一步的最大坑是:模型填入的参数经常不够完整,或者格式不符合预期。解决思路是接口层给足兜底。
我习惯把所有参数都设计成"要么有默认值,要么有校验规则"。比如URL参数,如果模型没传或传了非URL格式,技能内部要能直接报错并返回友好提示,而不是等到发起网络请求时才暴露问题。参数校验放到最前端,可以省掉大量难排查的运行时错误。
执行成功后,输出要做标准化。拿我们的网页提取技能来说,输出统一成包含title、content、fetched_at的结构。这样做的好处是,上层复合技能和推理模型都能按固定格式读取结果,不用每次去猜返回字段。我在一个项目里踩过坑:两个技能的返回结构不一致,导致上层Agent在组合使用时不得不做大量二次解析,还经常解析失败,最后统一结构后才彻底稳定。
4.3 失败链路:重试、回退与如实上报
技能不是每次都能执行成功。网络超时、页面结构异常、编码识别错误,各种意外都可能发生。失败链路设计得好不好,对用户体验影响非常大。
我的策略是"快速失败加一级回退"。快速失败是指技能在执行初期就要做好参数校验和环境检查,哪怕多花几十毫秒,也远好过执行过半了才发现问题。一级回退是指当技能内部处理失败后,不直接把原始报错抛给用户,而是先尝试一次简化处理,比如网页正文提取时,CSS选择器主规则失败了,就尝试按段落标准标签提取。
回退也失败时,一定要如实上报。我见过某些项目为了"让Agent看起来更智能",失败了还硬编一个结果回来,这比直接报错更要命,因为用户会拿着错误结果往下走,后面的决策全被带偏。失败上报至少应该包含:哪个技能失败了、错误类型是什么、重试了几次、最后采用了什么回退方案。
5. 技能库的长期维护与安全边界
单个技能能用起来之后,真正考验工程能力的是技能库的整体治理。技能一多,命名、管理、版本、安全这些问题会接踵而至。
5.1 从单技能到技能库的组织策略
技能库不是把一堆技能目录堆在一起就完事了,它需要层级和命名规范。我常用的组织方式是分三层目录:
skills/ ├── core/ # 基础能力技能,跨业务复用 │ ├── file_ops/ │ ├── http_ops/ │ └── data_process/ ├── biz/ # 领域业务技能,按业务域隔离 │ ├── order_svc/ │ ├── content_svc/ │ └── report_svc/ └── meta/ # 元技能,管理Agent自身行为 ├── task_decompose/ └── context_compress/命名方面,我倾向用"业务域_动作"的格式,比如order_query、content_summarize。这样命名空间清晰,也方便写检索阶段的关键词匹配规则。
版本控制上,每个技能目录用独立版本号,推荐语义化版本:主版本号在接口不兼容时递增、次版本号在增加可选参数时递增、补丁号在修复内部逻辑时递增。技能库整体用代码仓库管理,配置文件和实现代码一起入库。这个习惯能在技能行为变化时快速回溯,是谁、在什么时候、动了什么,一目了然。
5.2 用测试集守住技能召回率
技能和普通代码一样,会随迭代而"腐化"。描述被改了一次之后,触发准确率可能悄悄下降,也许测试时没发现,上了生产才暴露。所以,回归测试不是可选项,而是必备项。
我建议每个技能都维护一个测试集,包含三类用例:
- 正向触发用例:用户意图匹配时,技能应该被选中。
- 负向触发用例:意图不匹配时,技能不应该被选中。
- 参数执行用例:给定输入,技能执行结果与预期是否一致。
评估指标可以用这张表来盯:
| 指标 | 含义 | 建议值 |
|---|---|---|
| 召回率 | 应该触发时正确触发的比例 | 越高越好,至少大于0.9 |
| 精确率 | 触发结果中正确触发的比例 | 越高越好,至少大于0.85 |
| 任务完成率 | 执行成功返回有效结果的比例 | 依据技能场景而定 |
| 平均耗时 | 从调用到返回的耗时 | 视场景,越短越好 |
不用追求每次都是100%,因为用户语言表达本身有模糊性,但至少要在可控范围内。每次技能描述或实现变更后,跑一遍测试集,指标掉得厉害的,一律打回重改。这个过程很枯燥,但它是技能库长期稳定性的基石。
5.3 权限、沙箱与输入校验
技能一旦被Agent自主调用,就涉及权限边界问题。我的原则是最小权限:技能默认没有文件系统写入权限、不读取环境变量中的敏感信息、非必要不访问内网服务。每次申请权限时都要问一句:这个权限真的需要吗?
执行环境尽量隔离。哪怕本地开发机,也建议把技能执行放在临时目录或容器化的沙箱里。这样做的好处是即使技能代码有缺陷,也不会把宿主环境搞乱。
输入校验这块,核心是防注入。技能参数里如果有URL、文件路径、查询语句这类内容,先做格式校验和偏差处理,再交给底层执行模块。举个小例子:文件读取技能拿到路径参数时,不能直接拼接给文件系统,要先检查路径是否在允许访问的根目录内。这些细节看着琐碎,但安全风险往往就藏在这种地方。
6. 常见问题与排查速查表
技能开发过程中踩过的坑,我整理成一份速查表,方便大家遇到问题快速定位。
6.1 常见的四类故障与修复方向
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 技能偶尔不触发,但有时又正常 | 描述文案覆盖不全,缺少某些表达方式的触发词 | 分析用户实际表达,补充同义表达和示例 |
| 模型总在无关场景选错技能 | 描述缺乏负例边界,或两个技能描述过于相似 | 给技能增加"不适用"说明,拉开技能描述差异度 |
| 参数经常传递不完整 | 接口定义不清晰,缺少默认值和校验规则 | 用默认值兜底,增加参数格式校验 |
| 技能运行一段时间后准确率下降 | 上游页面结构或依赖服务发生变化 | 检查外部依赖、更新测试集、对比历史版本输出 |
| 本地执行正常,部署后异常 | 依赖层不一致,缺系统级库或权限不足 | 核对依赖清单、检查沙箱权限、复现运行日志 |
排查时我通常按"先描述层、后接口层、再环境层"的顺序进行。因为触发类问题九成出在描述和接口,只有最后的运行异常才需要往环境和依赖里钻。
6.2 我踩过的三个实操坑
第一个坑是技能误触发的惨痛教训。曾经给一个"销售数据查询"技能写了很常规的描述,结果用户问"帮我查下明天天气怎么样"时,模型居然调用了这个技能,因为描述里有一句"可以查询各种实时数据"。后来我花了一天时间把所有技能的描述都重新梳理了一遍,统一加上了"不适用场景"这一节,误触发才彻底消失。自那以后,我再也不写"各种""所有"这类无边界词。
第二个坑是参数默认值设计不合理。有个技能把"返回条数"默认值设成了20,用户每次让模型查询时,结果都只有20条,而业务上需要最多200条。后来模型为了要到200条,只能在请求里额外强调"要全部数据",但有时候强调不到位,又回落到20条。改成默认值200并按需传参后,问题立刻解决。设计默认值一定要按"大多数场景下最合理的值"来定,而不是按技术实现方便来定。
第三个坑是版本迭代导致历史请求异常。有一次改了技能内部的数据解析逻辑,没升主版本号,结果线上Agent还在按旧规则组装参数,新老版本混跑了好几天,部分请求返回了完全错误的结构。那次之后,我强制要求所有技能变更必须走同一套代码评审和版本发布流程,这个问题就再没出现过。
最后分享一点个人习惯
做了这么多技能设计的工作,我个人最深的一点体会是:技能的本质不是代码封装,而是把"模型能理解的任务意图"和"系统能执行的具体能力"之间那条桥,修得尽量宽、尽量稳。描述写得精,模型才不会选错;接口定得好,执行才不会卡壳;边界划得清,用户才不会被误导。
我还有一个小的习惯,就是在每个技能里加一个"变更备忘录"文件,记录这个技能从诞生到每次迭代的关键决策。这个备忘录看起来不起眼,但在几个月后回头排查各种奇怪问题时,它往往是还原现场和判断得失的最有力依据。
如果你正准备做或者已经在做Agent技能相关的开发,建议先别急着堆技能数量。从一两个真正高频的场景起步,把描述、接口、回退、评测这四件事做扎实,然后慢慢扩展。技能库的魅力不在于大而全,而在于每个技能都可靠、可预期、可维护。