
最近在折腾AI智能体相关的项目发现一个被反复提起但很少被系统讲透的概念——agent-skills。说白了它就是给AI Agent装上手和工具的那一层。很多人做了半年demo级智能体最后卡住的地方都不是模型能力而是不知道怎么让Agent稳定地调用外部工具、执行具体操作、完成多步任务。这篇东西我准备从一个实际落地者的角度把agent-skills从概念、架构、代码实现到踩坑经验完整掰开讲一遍。1.1 从会聊到会做的关键一跃先说一个最直观的对比。没有技能系统的Agent是什么样的你让它帮我把今天项目周报整理好发到群里它只能回你一段文字好的我建议你打开笔记本写下进度然后再复制粘贴……——这就是纯对话模型它什么都懂但什么事都做不了。而接入了agent-skills的Agent面对同一个指令会自己完成读取今日任务列表技能、调用周报模板渲染技能、搜索群成员技能、调用IM发送技能最后给你一句已发送。这个差别就是会聊和会做的分水岭。我在实际项目中体会很深的一点是模型本身的能力已经非常强了尤其在推理和语言理解上但它是没有任何行动能力的。而agent-skills要解决的核心问题就是如何把模型的语言理解能力翻译成真实世界中的可执行操作。这中间需要一个完整的工程体系来支撑包括技能的注册、发现、调度、参数校验、执行回传和错误处理。这也是为什么我说聊到Agent落地就绕不开agent-skills。1.2 技能系统与传统插件的本质差别很多人刚接触这个概念时会很自然地把它和传统软件里的插件系统划等号——都是挂一堆功能模块嘛。实际上两者有本质区别。传统插件体系里调用关系是确定的。用户在界面上点一个按钮触发对应的插件功能或者由预先写好的业务逻辑来决定调用哪个插件。它的核心特征是人驱动或规则驱动。而agent-skills是模型驱动的技能的调用决策权交给了LLM模型根据用户输入的语义自主判断应该调用哪个技能、传什么参数、调完一个还要不要再接着调下一个。举一个我们项目里的真实例子。我们接了一个财务管理技能集里面有查账户余额创建报销单审批流推进等七八个技能。用户输入帮我看下还剩多少预算顺便把这笔打车费报了传统插件只能按照内置逻辑一个个执行或者弹窗问用户选哪个而接了agent-skills之后模型会先调用查询技能把余额拉出来然后根据余额状态决定走普通报销流程还是加急审批流程整套操作都是动态决策的。这个动态决策能力就是agent-skills和传统插件最根本的分水岭。技能系统实际上是在模型和真实世界之间搭了一座双向桥模型通过技能定义理解世界有什么可用世界通过技能执行结果反馈给模型形成真正的闭环。1.3 Agent技能的分类方式在我们团队内部习惯把agent-skills的技能池分成四类这个分类方式对后续架构设计很有帮助技能类型作用典型例子信息获取型从外部系统读数据把结构化信息喂给模型查天气、查数据库、调第三方API拉数据操作执行型对某个系统产生实际副作用发邮件、创建工单、改配置文件、下单记忆读写型读写Agent自身的状态和记忆存对话摘要、读用户偏好、写任务进度推理计算型外部工具辅助模型完成精确计算或步骤跑Python脚本、执行SQL查询、调用数学库为什么要把技能分类因为不同类型的技能对可靠性、权限、审计的要求完全不一样。信息获取型技能调用错了最多返回一个错误结果操作执行型技能调用错了可能直接产生一笔线上支付。所以后面做权限管理和调用策略时必须按类型分开设计。2. 从零搭建技能系统注册中心、调度引擎、执行沙箱三层架构2.1 技能系统的核心组件划分我自己在开发agent-skills时把系统拆成了三个核心组件注册中心、调度引擎、执行沙箱。这个划分不是从教科书抄来的而是从实际犯错中总结出来的。最早我图省事让Agent直接在一个大函数里调用工具结果技能一多就乱套根本没法维护。后来才认识到技能系统本质上是一个能力管线每层的职责必须单一清晰。注册中心是技能的家。所有技能在上线前都要把技能名、技能描述、参数Schema、执行入口四件套登记进来。它还维护技能的生命周期状态是草稿、已上线、被禁用还是已下线。调度引擎是大脑的二传手。它负责在模型决定了意图之后把用户意图转化为具体技能调用指令中间涉及参数的映射和填充。执行沙箱是手。它真正跑去调用外部API、执行代码、操作数据库并把结构化的执行结果返回给上层。这三个组件必须解耦。我们的第一次架构设计就是因为把调度和执行的逻辑耦合在一起导致每次给技能加一个重试策略都要去改调度代码把所有技能都牵连进来。拆开之后调度引擎只关心调哪个、按什么顺序调执行沙箱只关心怎么调、调失败了怎么退注册中心只关心有哪些技能、它们是否健康。各干各的场景再复杂也不乱。2.2 一次完整的技能调用数据流理解了组件之后直接看一次完整的技能调用链路会清楚得多。当用户输入帮我查一下杭州明天适合跑步吗时系统里发生的事是这样的用户原文进入LLM模型先判断意图确定这是一个天气查询需求。LLM读取agent-skills的技能清单从数十个技能描述中匹配到查询天气技能。模型根据用户输入和技能参数Schema生成结构化的调用参数比如城市杭州日期明天此时并不直接执行任何代码。调度引擎拿到这些参数后走校验逻辑确认参数完整合法再根据技能的注册信息找到执行入口。执行沙箱真正调用后端天气API取回温度、降水概率、空气质量等原始数据。执行结果返回给LLM模型把结构化数据组织成自然语言补充一句明天空气质量良好适合晨跑最终回复给用户。这个过程看起来简单但每一步都有工程化的设计在里面。最关键的一点是LLM在整个链路中扮演的是决策者而不是执行者。它负责理解用户意图、选择合适的技能、生成正确的参数但绝不直接操作外部系统。实际操作全部落在执行沙箱里。这么做的原因很实际模型生成的内容天然存在随机性和不确定性你不可能指望一个概率模型去完成对精确度要求极高的API调用只有在执行层用确定的代码去兜底系统整体才会稳定。2.3 为什么技能描述必须声明式我在给团队做培训时常说一句话技能描述是写给模型看不是写给用户看的。很多开发者刚接触agent-skills时习惯把技能描述写成给人类看的文档比如天气查询功能用于获取中国各城市未来7天的天气预报数据。这种描述太笼统了。模型选择技能时靠的是语义匹配和逻辑推理。你的描述越具体、越结构化模型就能越精准地做路由。我们内部现在对技能描述有一套统一规范第一句点明技能的适用场景第二句说明它接收的核心参数第三句讲清楚执行后的返回值形态最后补充哪些场景不应该调用它。举个例子。同样是查询天气的技能我们现在的描述长这样用于查询指定城市未来1-10天的天气信息。适用场景用户询问天气、温度、降水概率、空气质量、出行穿衣建议。参数city_code城市编码必填、forecast_days预报天数默认7。返回字段date、weather、temp_high、temp_low、precipitation、air_quality。注意用户问过去天气或用天气做销售分析时不调用此技能返回空结果。这么写的好处是模型在面对模糊输入时有足够的信息做判断。之前我们遇到过用户问上海和北京哪个适合本周去旅游模型一开始调了两次天气查询分别查两个城市的天气后又调了交通查询技能。正是因为描述写清楚了适用场景和参数模型才能做出这种组合调用的决策。这叫声明式技能描述——你声明什么模型才能调度什么。3. 技能注册与调用实战一段代码把Agent接入真实世界3.1 技能的声明结构与JSON Schema聊完架构进入实战环节。一个技能在代码层面上到底长什么样这是我目前项目里一个标准技能的定义结构{ name: send_email, description: 向指定收件人发送邮件。适用场景用户要求写邮件、发邮件、回复邮件。, parameters: { type: object, properties: { to: { type: string, description: 收件人邮箱地址多个地址用逗号分隔 }, subject: { type: string, description: 邮件主题 }, body: { type: string, description: 邮件正文内容 }, cc: { type: string, description: 抄送人邮箱地址可选 } }, required: [to, subject, body] }, handler: email_service.send_email, tags: [communication, mail], timeout_ms: 10000, execution_mode: sync }先说字段设计的逻辑。name是技能的唯一标识必须简短明确不能带空格或特殊字符。description是给模型看的写得好不好直接决定模型会不会在关键时刻想起你这个技能。parameters部分强烈建议用JSON Schema这样的标准格式不要自定义一套。为什么必须用JSON Schema因为模型侧的function calling能力原生支持标准JSON Schema。你定义好这个结构后可以直接把整个参数结构传给LLM模型就能生成符合要求的参数对象。如果你自己搞一套参数格式意味着你得自己写转换逻辑还得考虑模型忘记遵守格式怎么办。用标准格式最大的好处是完全兼容模型的tool-calling接口几乎零成本接入。handler字段指向技能的真实执行函数。我特意用字符串而不是直接放函数引用是为了支持技能的热更新和多人协作——配置与代码分离改配置不需要重新发布服务。这也是我在做架构升级时的一个重要决定。3.2 核心实现注册器、加载器与执行器下面给一段Python代码展示注册中心的简化实现。这段代码虽然不长但在我们项目里它支撑了几十个技能的声明、校验和执行import json import importlib import inspect from typing import Dict, Any, Callable class SkillRegistry: def __init__(self): self._skills: Dict[str, Dict[str, Any]] {} def register(self, skill_def: dict): name skill_def[name] if name in self._skills: raise ValueError(f技能重复注册: {name}) # 解析handler字符串加载真实函数 module_path, func_name skill_def[handler].rsplit(., 1) module importlib.import_module(module_path) handler_func getattr(module, func_name) # 执行参数校验确保handler是异步函数防止阻塞事件循环 if not inspect.iscoroutinefunction(handler_func): raise TypeError(f技能 {name} 的handler必须是异步函数) self._skills[name] { definition: skill_def, handler: handler_func, } def list_skills(self) - list: 给LLM用的技能清单只保留描述和参数结构 return [ { name: item[definition][name], description: item[definition][description], parameters: item[definition][parameters], } for item in self._skills.values() ] async def execute(self, name: str, arguments: Dict[str, Any]) - str: if name not in self._skills: return json.dumps({error: funknown skill: {name}}) item self._skills[name] skill_def item[definition] handler item[handler] # 执行超时控制 timeout skill_def.get(timeout_ms, 5000) / 1000 result await asyncio.wait_for( handler(**arguments), timeouttimeout ) return json.dumps(result, ensure_asciiFalse)这段代码有几个地方值得细说。第一handler必须是异步函数这在整个系统并发能力上是非常关键的设计。Agent调用技能时很可能需要同时发起多个并行调用比如查三个城市的天气用同步函数会阻塞整个事件循环。第二给LLM的技能清单list_skills方法和内部完整注册信息是分开的。模型只需要看到名称、描述和参数结构不必知道handler怎么实现。这既减少了传递给模型的token数量也避免暴露内部实现细节。执行器还做了超时控制。我遇到过的真实情况是某个第三方API偶尔会响应极慢拖慢整个Agent的回复。哪个技能慢就暴露哪个技能的超时问题执行器这边把timeout_ms调小等到真需要扩容或者做降级策略时再单独处理。3.3 执行结果的回传与多轮处理技能执行完结果怎么回到模型手里很多Agent新手在这里栽过跟头。有个常见误区是把结果拼一段字符串直接返回给LLM就完了。实际线上调用时模型返回的结果可能是JSON也可能是错误堆栈还可能是个超时异常这些情况要分门别类。我习惯的做法是无论技能内部发生了什么执行层统一返回一个规范的JSON字符串。正常执行时返回数据和字段异常时返回独立的错误码与可读信息。所有返回都会在传给LLM前做一次统一包装。格式如下{ ok: true, data: { weather: 晴, temp_high: 28 }, meta: { skill: query_weather, latency_ms: 342 } }失败时的格式{ ok: false, error: { code: VENDOR_API_ERROR, message: 天气服务商接口返回500请稍后重试 } }为什么非要做这个包装因为模型从错误中学习的能力很强。当它看到错误码和清晰错误信息时下一次决策里就会避开同样的错误比如自动换一个参数再调用。之前我们没做统一包装技能的报错信息随手拼一段模型根本不知道自己错在哪里经常在原地重试白白浪费了好几轮对话。包装之后模型能理解参数格式不对和服务商挂了的区别推理质量提升非常明显。多轮处理时把技能执行结果作为系统消息追加到对话上下文中让模型参考这些信息生成最终回复。这里的顺序很重要先给用户原话再给技能结果再让模型生成自然语言。这样模型既知道用户想要什么也知道工具实际上给到了什么就能自然地做信息整合而不是机械复述结果。4. 让模型正确选技能的关键工程手段意图路由与提示词编排4.1 技能列表是给模型看的菜单有人会想让LLM自动选技能不就行了选错了选对了都靠模型发挥。大模型确实能处理意图识别但是直接让它在一堆技能描述里挑一个就像让一个刚来餐厅的客人从几百道菜的菜单里选一道最适合赶时间、又便宜、又健康的菜。它的选择可能偶有灵感但不够稳定。实际Agent场景里需要给模型铺一条高效的决策路径。这里我先说一个基本的配置——技能菜单怎么传递给模型。在主提示词或者系统提示词里把技能列表作为结构化文本放进去。现在主流做法是直接使用function calling机制把技能Schema传给模型但如果你用的框架不支持也可以在自己的提示词里嵌入。我们内部演进了三个版本的写法。第一版很简单纯粹罗列技能名和一句话描述。效果很差模型经常分不清A技能和B技能的边界。第二版加了场景描述效果好了不少。第三版我们在每个技能下加了不适用场景这个做法在准确率上带来了一次明显的提升模型误调用的比例大约下降了40%。现在我们的提示词片段大概是这样的可用技能清单如下格式为技能名 - 描述 - 参数 - 不适用场景。 query_weather - 查询城市实时天气和预报 - city_code, forecast_days - 不适用过去天气、气候统计、数据分析 send_email - 发送邮件给收件人 - to, subject, body, cc - 不适用读取收件箱、搜索邮件、删除邮件 search_files - 在指定的根目录下搜索文件 - root_path, keyword, file_type - 不适用打开文件内容、编辑文件 create_jira_ticket - 创建JIRA工单 - project_key, summary, description, priority - 不适用查询工单状态、修改工单 ...这个不适用场景的思路有点类似给模型画了一个安全边界。模型在做语义匹配时不仅要判断该不该用还要判断现在是不是不该用这个。边界画清楚了模型的误判率会大幅降低。尤其是在技能数量变多之后超过20个这个边界信息价值极大。4.2 路由策略语义相似度优先、规则兜底虽然LLM本身就是很好的意图识别器但我还是建议在工程上加一层路由策略而不是完全把选技能这件事交给模型。具体做法分两个层面。第一层是规则兜底一部分意图非常明确的指令直接用传统NLP规则或者小模型做初筛。比如用户输入包含天气*城这类结构化表述直接路由到天气技能根本不需要动大模型。这样做有两个好处省token、降低响应延迟。第二层才是LLM决策遇到规则无法覆盖的开放语义才让模型在技能之间做选择。这种规则优先、模型兜底的策略在我们项目里叫宽进严出。技能数量少的时候全走LLM也可以技能多了之后你会发现大量高频指令其实是规则化的没必要每次都让大模型为这点简单事支付推理成本。比如查余额看天气搜文件这类明确指令规则策略几乎不会出错让模型参与纯属浪费。4.3 技能冲突与误调用的兜底设计无论提示词编排得多巧妙模型总有走神的时候。技能系统必须做攻击面防御。这里说三个我实战中验证过的兜底方案。第一个是冷却与频控。如果某个技能在短时间内被反复调用可能不是用户真的有这么多次需求而是模型陷入死循环了。我们给高频技能加了一个简单的频控策略比如同一会话中某个技能调用超过5次就返回提示让模型检查是否陷入循环。这招救回过好多次Agent卡死事故。第二个是敏感技能二次确认。操作执行类技能发邮件、下单、删除文件都走一次二次确认。确认的方式可以是在对话里问用户也可以做成前端UI的确认按钮。技术实现上给技能打一个confirm: true的标签调度引擎遇到这个标签就先返回一个pending状态等用户确认再真正执行。第三个是技能回滚机制。在执行沙箱里一旦操作类技能产生了副作用我们会把原始状态记录下来以便在必要的时候回滚。比如修改配置文件技能在执行前先备份原文件执行后发现问题立刻恢复。这个东西做完之后线上出问题时的恢复时间从小时级降到了分钟级。5. 技能编排进阶组合调用、工作流与多Agent分工5.1 技能编排与编排器的设计思路单个技能能做的事终究有限Agent的价值更多体现在组合调用上。我自己最早做多技能协作时是在主提示词里写如果需要你可以依次调用多个技能来完成用户需求然后让模型自己编排顺序。结果也是时好时坏成功率高的时候运行令人惊喜失败的时候会让用户怀疑系统不可用。后来我意识到简单的多步调用可以交给模型自由发挥复杂的多步链路超过5步、强先后依赖必须用工作流来编排减少模型的自由决策空间。举个例子生成周报并发送给部门群这个任务拆解出来的步骤是读取本周完成任务列表→按模板生成周报→搜索部门群聊对象→发送周报。前两步之间有确定的逻辑关系我不想让模型每次重新推理这一步。于是我们把这些步骤封装成一个周报工作流技能内部编排好每个子步骤的调用顺序模型只需要执行一次这个工作流即可。这个设计在工程上被称为技能编排。它带来了两个明显好处的稳定性和效率都提升了。不用每次让模型从零推理链路发生中间错乱的可能性大大降低对外暴露为一个技能之后提示词里只需要保留很短的一个描述token消耗还变少了。5.2 多Agent协作下的技能分配如果单个Agent的技能池越装越多提示词里要传的技能清单也会越来越长整体的上下文会被挤占。这是agent-skills规模化落地时一定会撞上的天花板。我们的解法是多Agent架构不同职责的Agent各管一摊技能。我们系统的实际配置是一个主调度Agent它不执行具体业务技能只负责理解用户意图然后按职责边界把请求转发给子Agent。每个子Agent维护自己的技能池。比如工作Agent维护和文档、报表、日历相关的技能沟通Agent维护邮件、IM、会议相关的技能数据Agent维护SQL查询、BI图表相关的技能。这么做的好处一眼就能看出来每个Agent的技能清单都短模型做路由时干扰项少了很多。我们实测一个技能池数量从60个降到15个之后路由准确率从81%提升到了93%以上。这就是技能收敛带来的显著收益。另外多Agent结构还天然做了权限隔离。不同职责Agent可以绑定不同的权限账号数据Agent只能读数据库沟通Agent可以发消息但不能删消息。权限边界清晰审计链路也更简单。5.3 技能复用与灰度发布最后聊一下技能体系的迭代管理。agent-skills系统上线之后必然会持续新增、修改技能这个迭代过程如果处理不好很容易翻车。我们做了几件事所有技能上线前均有测试用例覆盖边界条件技能版本管理每次改动保留历史版本以便回滚新版本技能先在单Agent环境灰度确认运行稳定后推全量。灰度发布的设计有意思。我们把技能执行器的配置抽成一个独立的配置中心线上环境、灰度环境、测试环境的技能参数完全隔离。把一个技能从测试提升到灰度只在配置中心里改一行配置不用改任何代码。这个机制让运营人员和算法同学都能自己操作不用每次来麻烦我们开发。6. 落地agent-skills的真实踩坑记录与最终建议6.1 描述冗余带来的路由混乱先说一个我们踩得很冤的坑。项目初期为了让技能定义更详尽团队里一位同学在技能描述里写了一整段带示例的说明文字从背景到用途写了200多字。结果上线之后这个技能经常被模型误调用凡是模糊字段的描述都容易触发它。排查过程是打印出每次技能调用日志把入参和用户原始输入做了对照发现模型只要看到数据分析统计这些词就倾向于调用这个描述里提到的技能。原因正是描述文本过长、关键词分布太密集把模型注意力带偏了。后面把每段描述精简到50字左右加上不适用场景说明误调率立刻降下去。我最终的总结是技能描述要结构清晰、范围明确、示例克制。描述不是文档它给模型做决策用不是给人类阅读用。给人类的说明可以放到系统文档里不要塞进技能描述。6.2 参数与返回值的类型隐患这个坑几乎必踩。有一次用户让Agent帮我查一下最近七天的销售数据模型把forecast_days参数生成为字符串类型的7而不是整数7。我们的JSON Schema定义里写的是整数但模型生成时把引号带上了。执行层校验通过不了技能报错返回模型不知道错在哪又用同样的参数重试一次反复两三次最后直接崩溃给用户一段异常信息。处理办法在调度引擎里加一层参数归一化。数字类型参数可以接受数字或数字字符串字符串参数尽量做trim和编码修正布尔参数兼容多种表达方式。归一化之后模型生成的参数即使不那么精确也能被系统稳稳接住。这层逻辑像是一个容错的翻译官让模型的输出和执行器的输入之间有一个缓冲垫。返回值类型同样要注意。有些第三方API返回XML有些返回超大JSON如果不做预处理直接丢给模型轻则上下文占用严重重则模型被无关字段干扰。我们在执行沙箱里加了一个返回裁剪逻辑把无关字段去掉只保留模型生成回复所需的核心字段。6.3 超时、并发与上下文膨胀Agent场景里技能调用的超时问题是个隐形杀手。早期我们所有技能都设置统一的5秒超时结果查询类技能跑得好好的操作类技能因为下游系统慢频繁超时。后来把超时改成按技能单独配置查询类3秒操作类10秒复杂的报表生成类30秒。然后再把超时重试策略加进去查询类技能允许重试2次操作类技能不重试避免重复提交。并发控制也是个大坑。多个Agent实例同时跑共享同一技能服务如果技能背后对接的API有频控限制你的Agent再聪明也白搭。我们给技能层加了一层简单的信号量限流。特别火的技能最多支持20个并发执行超出的请求排队等待。实际运行中这个排队机制有效保护了下游API不被我们打爆。上下文膨胀问题是所有Agent系统都会遇到的。每执行一个技能就要把技能结果塞进上下文多个技能执行完上下文可能膨胀到几千token。解决办法有二一是对大块技能结果只保留摘要而不是全文二是做完多步操作后用一个小模型对历史工具调用结果做压缩只保留关键结论。6.4 我的最后建议做了这么多集成和优化我对agent-skills的看法是它是Agent从实验品走向生产力工具的必经之路。越早把技能层当成一个独立工程体系来对待后期越省心。架构上做好注册、调度、执行三分数据流上做到技能描述结构清晰、执行结果统一包装、上下文及时压缩运营上做好灰度与回滚机制这套体系就能稳定地跑起来。如果你现在正在开发自己的Agent建议不要先急着堆技能数量而是先打磨三个技能的调用链路跑通一个完整的用户请求→模型路由→技能执行→结果返回→模型总结闭环。这个闭环稳定之后再慢慢加技能、加场景。技能系统这东西开始的姿势对了后面就是坦途多了再重构痛苦指数会直线上升。我个人在实际操作中的体会是agent-skills最迷人的地方在于它是模型智能和工程现实之间的粘合剂。没有它模型再聪明也只能纸上谈兵有了它模型才真正开始动手做事。顺着这个方向继续往深做下一步还能优化技能间依赖关系的自动编排、技能效果AB测试、甚至让Agent自己学会创造新技能组合。这条路还很长但方向一定是往更多自动决策、更少人工干预上走的。