
最近我在折腾智能体开发时被一个高频问题反复卡住模型本身挺聪明一让它干具体活儿就各种掉链子。要么不知道该调用哪个工具要么把参数传得乱七八糟要么只回复“我可以帮你处理”然后就没有然后了。后来我意识到问题不在模型而在我没有给模型一套结构化的技能体系。所以就有了 agent-skills 这套方案把能力拆成可复用的技能模块让模型像人一样按卡操作、按章执行。这篇东西不会讲虚的。我会从 agent-skills 的定位讲起一路拆到技能定义、注册机制、执行器实现、多技能编排再把我在实际调试中踩过的坑一并列出来。适合正在做 AI Agent、机器人流程、或任何需要让大模型操作真实工具的开发者参考。项目本身不复杂但很多细节不亲手磨一遍很难体会到“技能”和“普通函数”之间的本质区别。1. agent-skills 的整体设计与思路拆解1.1 它到底是什么agent-skills 不是某个单一模型也不是一个开箱即用的成品应用。它更像一套“智能体技能管理机制”解决的是当大模型需要完成真实任务时如何把任务拆解成可执行的动作并在动作执行中保证正确性、可复用性和可观测性。我在最初设计时给 agent-skills 定下三个核心能力技能注册、技能调用、技能编排。所谓技能注册就是把一个个可复用能力比如“查天气”“读PDF”“发邮件”以统一格式登记到技能库中。技能调用则是指大模型根据用户意图自动从技能库中选择合适的技能并生成正确的调用参数。技能编排更进一步允许把多个技能按逻辑顺序组织成一个流程去完成一个复杂目标。这三个能力对应的价值也很好理解没有注册机制技能就是一堆散落各处的函数没有调用机制模型永远不知道“该用哪个工具”没有编排机制稍微复杂一点的任务就只能在单次调用里打转无法形成真正的工作流。我在实际使用中感受到的最大变化是agent-skills 把“模型会说什么”和“系统能干什么”彻底解耦了。模型只负责意图理解与规划真正的动作执行全部由技能层完成。这样即使底层模型换掉技能库依然可以复用业务逻辑也不会被模型风格绑架。1.2 为什么非要把“技能”单独拎出来很多人第一反应可能是我在代码里写几个函数让模型通过函数调用不就行了区别就在这里。函数是一对一的固定接口你给它写死参数它就执行固定动作。但技能是带语义的、可被模型动态选择的模块。一个函数只能被显式调用而一个技能却可以被模型在任意场景中主动发现。举个例子你写了一个read_pdf(file_path)函数模型必须先知道存在这个函数而且要知道该在什么时候用。如果你把技能库做得足够清晰模型可以通过技能描述推断出它适合解决哪类问题甚至在用户没有直接提及“PDF”时也能根据“帮我看看这份合同里违约金那条怎么写的”这类意图联想到读取文件技能。这种“可发现性”是技能系统区别于普通函数的关键。它要求每个技能都自带一份高质量的描述包括功能摘要、适用场景、参数说明、返回值结构、使用限制。描述写得好不好直接决定了模型选择的准确率。我在实践里试过用一句话描述技能结果模型经常选错改成详细说明后选对率明显提升。另外单独拎出技能还有个好处可替换性。同一个“发送通知”技能底层可以用短信接口也可以换邮件接口对模型来说看到的都是统一技能接口底层怎么实现完全不影响上层规划。我后来做多场景部署时只需替换技能内部实现模型侧代码一行不用动。1.3 整体架构技能注册表、执行器与规划器agent-skills 在结构上分为三个层次规划层、调度层、执行层。规划层负责把用户输入转换成任务清单。这通常由大模型完成它会读取技能注册表中的全部技能结合用户请求推测需要哪几步操作。这一层的输出不是自然语言而是一份结构化的“技能调用计划”例如先调用技能A再根据技能A的结果调用技能B。调度层是系统的核心。它负责解析计划按顺序触发对应技能并把每一步执行结果反馈给规划层。调度层同时处理超时、重试、异常分支、上下文累积。我在最初实现时把调度逻辑写得非常简单只是按顺序执行列表后来发现真实任务里会出现大量“根据上一步结果决定下一步”的情况于是又把调度层改成支持条件跳转和循环。执行层则是技能的具体实现。每个技能内部可以是任意代码逻辑也可以是对外部系统API的封装。执行层只关心两件事收到合法参数、返回结构化结果。它不关心模型是怎么想的也不关心上层流程怎么走。这三层各司其职组合起来的完整链路是用户输入 → 模型规划 → 调度解析 → 技能注册表匹配 → 执行器运行 → 结果回传。即使是最简单的单技能调用也走这条链路。好处是——每当任务出错我们能精确看到是哪一步出了问题而不是在一个巨大的函数调用的黑盒里抓瞎。2. 核心细节技能定义与注册机制2.1 技能描述的结构设计技能定义是 agent-skills 的灵魂。我给每个技能设计了一套标准Schema包含几个固定字段name: 技能唯一名称小写蛇形 description: 技能功能描述说明它能做什么、解决什么问题 version: 技能版本号便于后续升级兼容 parameters: 调用参数定义包含名称、类型、必填性、描述 returns: 返回结果结构说明 examples: 1-3个典型调用示例这里面最容易被忽视的是 examples 字段。我起初觉得有 description 就够了但实测发现在长尾场景下模型对纯文字描述的理解方差很大。给出一两个典型例子后模型对“什么时候该用这个技能”的判断会稳定很多。尤其是当多个技能职责相近时example 能直接当着“使用场景提示”来用。举个例子一个“获取天气”技能和一个“获取空气质量”技能单看描述都差不多模型容易混淆。如果“获取天气”的 examples 里有“用户问今天适合洗车”模型就会把洗车这种依赖天气的问题关联过来而不是误调空气质量接口。参数定义方面我强烈建议不要使用过度宽松的类型。比如“参数: content类型: string”模型很可能会传入乱七八糟内容。应该把每个参数都写成带业务语义的对象{name: city, type: string, required: true, description: 城市中文名如上海、北京, enum: [北京, 上海, 广州]}。枚举值能收就收这样能挡掉大量无效调用。2.2 技能注册表与动态加载技能注册表在 agent-skills 里承担“技能目录”的职责。一个最简单的实现可以是一个字典key 是技能名value 是技能对象。但真实项目中我建议把技能注册表做成独立的服务或者至少用独立模块管理避免把技能定义散落在业务代码各处。我的习惯是每个技能独立一个文件通过装饰器或注册函数挂载到全局注册表。这样新增一个技能只需要新增一个文件不需要改动调度层。# 示例技能注册装饰器 skill_registry {} def register_skill(skill_cls): instance skill_cls() skill_registry[instance.name] instance return instance这种动态注册方式还有个额外好处可以按需加载。Agent 启动时可以只加载当前任务域所需的技能子集减少模型需要读取的技能描述数量。我实测过当技能注册表里挂载超过40个技能时模型在每次请求中读取全部描述的开销会明显增大且选择准确率下降。按业务域拆分成多张注册表后准确率回升响应速度也快了。注册表内部还应该保存每个技能的调用统计例如调用次数、失败次数、平均耗时。这些数据虽然短期看没什么用但当我需要决定哪些技能要优化时就是直接看统计数据而不是凭感觉。2.3 技能描述与模型上下文的关系大模型能看到技能只是因为我们在系统提示词中拼入了技能描述。这带来一个直接问题描述写太长会占用上下文窗口写太短又影响选择准确率。agent-skills 的做法是这样把描述控制在100到300字之间重点是“适用于什么场景”和“怎么用”。与任务无关的实现细节全都不写。比如一个发送邮件的技能描述里只写“根据收件人地址发送邮件支持正文和附件”不用写“使用SMTP协议超时设置30秒”。另外注意把技能描述的阅读顺序调整成“场景优先”。模型在扫描技能时第一眼看到的应该是“我什么时候会用到这个技能”而不是“我是谁”。我曾经因为把一个技能的功能定义写得太过详细导致模型被具体操作步骤吸引反而忽略了它适用的前置条件。后来重写描述开头直接写“当用户需要向指定邮箱发送电子邮件时使用”选中率立刻变好。3. 实操实现一个简易的 agent-skills 执行器3.1 基础环境与项目结构这里我以 Python 为例搭建一个最小可运行的 agent-skills 执行器。不需要任何框架只需要标准库加一个请求库就行。项目结构可以是这样agent-skills/ ├── registry.py # 技能注册表 ├── skill_base.py # 技能基类 ├── skills/ │ ├── weather.py # 天气技能 │ └── calculator.py # 计算器技能 ├── planner.py # 模拟模型规划可替换成LLM └── executor.py # 调度执行器先把技能基类定义好。所有技能都继承这个基类好处是执行器只依赖基类接口不关心具体技能内部实现。# skill_base.py class BaseSkill: name: str description: str parameters: list [] def run(self, **kwargs): raise NotImplementedError def validate(self, kwargs): # 简单校验参数是否齐全 required [p[name] for p in self.parameters if p.get(required)] for k in required: if k not in kwargs: return False, f缺少参数: {k} return True, 3.2 具体技能示例计算器与天气查询计算器技能是最容易上手的例子。它的入参是两个数字和一个运算符返回值是计算结果。# skills/calculator.py from skill_base import BaseSkill class CalculatorSkill(BaseSkill): name calculator description 执行四则运算支持加减乘除。当用户需要计算表达式时使用。 parameters [ {name: expression, type: string, required: True, description: 数学表达式如 1 2 * 3} ] def run(self, **kwargs): expression kwargs[expression] # 注意这里做简单处理生产环境请使用安全计算库 return {result: eval(expression)}天气查询技能需要接入外部API。为了让示例脱离具体服务商我用一个模拟函数代替。核心是展示一个技能如何封装外部依赖。# skills/weather.py import random from skill_base import BaseSkill class WeatherSkill(BaseSkill): name get_weather description 获取指定城市当前的天气情况。当用户询问天气、温度、穿衣建议时使用。 parameters [ {name: city, type: string, required: True, description: 城市中文名如北京} ] def run(self, **kwargs): city kwargs[city] weather random.choice([晴, 阴, 小雨, 多云]) temp random.randint(10, 32) return {city: city, weather: weather, temperature: temp}我在写真实技能时会额外增加一层缓存。比如天气查询技能如果同一城市在10分钟内被重复查询直接返回缓存结果。这个细节能显著减少外部API调用成本尤其是在Agent做多轮对话时模型经常会对同一城市反复查询。3.3 注册与执行链路注册表只需要把技能类实例化并保存。执行器则需要接收一个“技能名参数”的指令然后调用注册表中的技能执行。# registry.py from skills.calculator import CalculatorSkill from skills.weather import WeatherSkill skill_registry {} def register_all(): for cls in [CalculatorSkill, WeatherSkill]: instance cls() skill_registry[instance.name] instance def get_skill(name): return skill_registry.get(name)# executor.py from registry import get_skill def execute_skill(skill_name, params): skill get_skill(skill_name) if not skill: return {success: False, error: f技能 {skill_name} 不存在} valid, msg skill.validate(params) if not valid: return {success: False, error: msg} try: result skill.run(**params) return {success: True, skill: skill_name, result: result} except Exception as e: return {success: False, error: str(e)}到这里一个最小可用的 agent-skills 执行器已经跑通了。用户说“计算 23”模型返回一个规划结果[{skill: calculator, params: {expression: 23}}]执行器就按这个规划调用拿到结果后回给模型组织最终回答。3.4 真实场景中的扩展点这个简化版执行器可以直接工作但放到真实项目里还需要补几个东西。第一是超时控制。外部技能可能卡住不能让Agent干等。我给每个技能的执行套一个超时装饰器默认5秒超时。手写线程超时很麻烦用concurrent.futures加上一个Future的等待过程即可。如果你在更复杂的框架里也可以直接用框架自带的超时配置。第二是重试策略。不是所有技能都适合重试。对于网络请求类技能我会设置“失败后最多重试2次间隔1秒”对于计算类技能就不需要重试。我会把重试参数写进技能定义由技能自己决定是否允许重试而不是全局一刀切。第三是执行日志。每一步技能调用的入参、出参、耗时、错误信息都要记录下来。我在实际排查Agent问题时发现绝大多数错误其实不发生在模型推理而在技能入参不合法。有日志后才能快速还原现场。4. 技能编排从单技能调用到多步任务4.1 让模型生成可执行的计划如果只做单技能调用agent-skills 的价值有限。真正的威力在于编排。我通常会让模型输出一个 JSON 数组作为执行计划数组每一项代表一个步骤。[ {skill: get_weather, params: {city: 北京}, next: {success: step2, fail: end}}, {skill: calculator, params: {expression: 0}, next: {success: end}} ]执行器拿到这个计划后就按照数组下标顺序执行。如果某一步失败读取fail跳转目标。在更精细的设计里next还可以带着条件表达式比如“当上一步返回的 temperature 小于 10 时执行技能A否则执行技能B”。这个设计让模型从“说一句话”变成了“写一个简单程序”。我一直觉得这是 Agent 能力的分水岭只会单次调用的是聊天机器人能够按照计划连续调用技能并处理中间结果的才是真正意义上的智能体。4.2 条件分支与循环的一个实例举个我最常用的例子一个“穿衣建议”任务。用户问“北京今天适合穿什么”模型规划后生成计划步骤1: get_weather(北京) 步骤2: 判断 temperature 25 → 输出短袖否则 → 步骤3 步骤3: 判断 temperature 10 → 输出夹克否则 → 输出羽绒服这里步骤2和步骤3不是技能调用而是逻辑判断。agent-skills 的执行器需要支持这种“非技能节点”。我的实现方式是给执行器内置一个condition技能专门做表达式判断。def condition(kwargs): expr kwargs[expression] return {result: bool(eval(expr))}然后执行器根据 condition 的结果决定走哪个next。这种结构并不复杂但把模型从“纯文本回复”的束缚里彻底解放了出来。4.3 上下文累积与中间结果多步任务里后一步技能的参数常常依赖前面步骤的结果。我不能指望模型在规划时就把所有参数都猜对因此设计了一个“变量传递”机制。在计划中可以用$step1.result.temperature这样的占位符执行器执行完步骤1后会把结果保存到一个上下文对象里步骤3需要参数时就把占位符替换成实际值。这个操作虽然简单但我认为它是 agent-skills 里最值得花时间优化的地方。一旦占位符替换逻辑出错后面的技能全都会拿到错误参数。我在初始版本里遇到了一个经典问题步骤1的返回结果是个嵌套字典而模型在生成$step1.result.temperature时路径写错导致解析失败。后来我给模型提供的技能描述里加上返回结构样例并严格要求模型生成计划时来自结果字段的引用必须完全匹配技能返回示例才把这个问题压下去。4.4 编排的容错与降级真实任务不免出现“技能A失败但用户也接受预案”的情况。我在 agent-skills 的编排中增加了“降级技能”字段一个技能可以定义备选技能。比如 get_weather 失败时可以降级到 get_climate。执行器在遇到技能失败时如果发现备选技能自动尝试备选并把降级信息记录到日志。这套机制让整体方案的鲁棒性上了一个台阶。我测试过一个场景网络不稳定天气API时不时报错但我把 get_weather 的备选配置成 get_forecast 后整个流程的成功率从70%提升到了95%以上。虽然备选结果精度稍低但用户任务没有被中断体验好得多。5. 常见问题与排查技巧实录5.1 技能描述模糊导致选错技能这是我在 agent-skills 使用中遇到概率最高的问题。现象是用户明明说的是A需求模型却去调用了B技能。排查方法很简单打开执行日志看模型最终实际选择的技能名再看该技能描述和用户输入的相似度。我碰过一种很隐蔽的情况两个技能描述里都有“文件”这个词模型就分不清该读文件还是该写文件。后来我在每个技能描述开头加上“主动动词用户意图”的句式“当用户要求把内容保存到文件时使用”才彻底解决。所以我的建议是技能描述一定要包含“什么时候用”和“什么时候不用”。比如读文件技能可以写明“当用户提到查看、读取、打开文件时使用”写文件技能则写“当用户提到保存、写入、导出时使用”。尽量让两个技能在词汇上减少交集。5.2 参数校验形同虚设很多人写了validate函数但仍然传入错误参数。比如参数定义里写temperature是数值模型却传了字符串“零上10度”。根本原因是模型不知道如何把用户口语转换成参数格式。解决办法是在参数描述中提供转换示例。不要写“温度值”要写“温度值数字如28”。更稳妥的做法是在技能内部做一次强制类型转换和范围校验。我在 get_weather 技能里加了这个逻辑如果温度参数不是数字则调用一个内置解析函数尝试从字符串中抽取数字部分抽不出来就返回错误。另外一个技巧是如果参数可以标准化为枚举值就尽量用枚举。模型在限定选项里的选择准确率远高于开放文本生成的准确率。比如城市参数如果业务只覆盖十个城市就把这十个城市写到enum里模型基本不会选错。5.3 并发调用引发资源竞争当 Agent 一次编排里包含多个技能而这些技能之间没有依赖关系时我们会尝试并行执行。并行速度提上去了但随之而来的是资源竞争。比如多个技能同时写日志文件造成日志串行或者外部API的连接池被占满触发限流。我处理这个问题的思路是给每个技能声明一个“并发等级”。无状态技能可以支持高并发共享外部连接或操作同一文件的技能则必须串行。执行器在调度时读取技能并发等级为低并发等级技能加锁。这算是在性能和稳定性之间做了一个折中。5.4 执行上下文越滚越大多步任务跑久了模型规划时会把所有中间结果都塞给技能导致技能收到的参数里出现大量无关字段。这在“让模型自主规划”的实现里尤其常见。模型倾向于复制整个上下文对象作为下一个技能的参数结果参数越来越大最终超过接口限制。我建议在执行器层面统一过滤参数只提取技能定义中声明过的字段来组装调用。这个“白名单机制”是必须的否则技能接口早晚被污染。而且我发现加上参数过滤后很多奇怪的调用错误也自动消失了因为模型乱传的字段根本没有机会进到技能内部。5.5 模型规划不稳定同一个用户问题模型今天规划三步明天规划五步后天又只规划两步。这种不稳定性对技能系统影响很大。我最终采用了“规划缓存”和“规则兜底”相结合的办法。对已知的固定任务模板直接使用人工写好的规划模板不再让模型自由发挥只有新任务才走模型动态规划。同时每次模型规划成功后会把“用户意图哈希 规划结果”存到缓存里。下次遇到相似请求直接复用。这套策略有效减少了模型的随机性也让 agent-skills 的行为可预期了不少。6. 从技能库到智能体操作系统的延伸6.1 技能版本管理与权限控制技能多了之后版本管理就是绕不开的事。现在我会给每个技能加版本号并在注册表中做多版本共存。调度层默认调用最新版本如果最新版本效果不好可以一键切回旧版本。这种“灰度切换”在实际发布中帮了大忙。权限控制也很重要。某些技能只能管理员调用某些技能需要用户显式授权。agent-skills 在技能定义中增加了required_permission字段。执行器在调用前检查当前会话的权限等级不具备权限就直接拒绝。这是防止Agent在自主执行时越权操作的基础保障。6.2 技能间依赖与共享服务当技能量级上来后你会看到不少重复逻辑。比如多个技能都需要读取配置、获取当前用户信息、记录审计日志。与其在每个技能内部重复写不如把这些共享能力也做成“内部技能”供其他技能调用。这里要区分一个概念模型调用的技能和技能内部调用的技能不是同一层次。模型调用的技能是业务动作内部技能是实现细节。我在 agent-skills 里用“外部技能”和“内部工具”两个命名空间把它们分开避免模型误以为内部工具也是可调用的业务技能。这一步能在很大程度上减少模型误选。6.3 把技能变成可评测可优化的对象我最后想说的扩展方向是“技能评测”。既然技能是独立模块它就应该像代码一样可以被单测。我会为每个技能准备一批“人工标注用例”包括输入、期望输出、期望副作用。每次修改技能内部实现后跑一遍用例确保没有破坏原有功能。这套用例集虽然维护起来要花时间但它让我在改动底层实现时不再提心吊胆。没有评测体系的技能库做大了以后一定会回归混乱。7. 写在最后一点真实体会agent-skills 这套思路带我走过了从“聊天机器人”到“能干活的智能体”之间那段最难的路。最大的教训不是技术实现难而是千万不要把技能库当成一个普通函数集合来写。每个技能都是一等公民它有自己的描述、参数、边界、版本、权限和评测用例。只有把它们当作产品长期维护Agent 才能真正稳定工作。最后分享一个实用小技巧在技能执行器里加一个“观察者模式”把每次技能调用的完整链路发送到一个可视化面板。当Agent某个环节出错时你能一眼看出是模型选择了错误的技能还是参数不合法还是外部接口宕机。这个可视化的调试手段早期可能看不出价值等技能库规模超过30个之后它几乎就是救命稻草。如果你也在做 Agent 相关的东西我的建议很简单先别急着上复杂框架把技能定义、注册、执行、编排这套最小闭环跑通。跑通了再往里面加东西。你会发现智能体的能力上限不在于模型多聪明而在于你的技能系统有多扎实。