
1. 认识 agent-skills不只是“给模型加工具”这么简单过去两年我一直在做智能体相关的基础设施建设从最早的 Function Calling 封装到后面的 ReAct 模式落地再到最近一些复杂任务的编排。期间踩过很多坑也沉淀了一些自己的方法论。今天想好好聊聊 “agent-skills” 这个方向。先说一个很多人容易混淆的点agent-skills 不是“给模型加几个工具函数”就完事了也不是简单地把 API 封一层就归类为技能。它本质上是一套关于“如何让大模型稳定地完成一类任务”的工程化解决方案。你可以在单个技能的内部实现里调用工具、组合工具、写业务逻辑也可以在技能之上做调度、编排、记忆管理。换句话说技能是介于“裸模型能力”和“完整业务流程”之间的一层抽象。我见过不少团队在做 Agent 应用时第一版都特别喜欢把逻辑全塞在 Prompt 里。这个阶段快速验证没问题但一旦业务复杂起来Prompt 会膨胀到几千字模型行为变得不可控改一个细节可能引发连锁问题。后来大家开始做 Function Calling 的封装把每个 API 封装成一个函数让模型按 JSON Schema 调用。这比纯 Prompt 进了一步但依然不够——因为函数只是代码层面的复用它不携带“怎么用、什么时候用、用错了怎么办”的上下文。agent-skills 想解决的恰恰是这个“上下文”的问题。一个技能应该包含四样东西能力描述这个技能是干什么的、调用协议入参和出参的格式、执行逻辑代码层面的实现、以及约束与纠错机制什么时候不能用、失败了怎么处理。只有把这四样东西固化下来模型才能在一个相对可控的边界里发挥它的泛化能力而不是每次都在一个模糊的 Prompt 里自由发挥。这篇文章适合谁看我觉得三种人最适合一是正在做 Agent 应用、但是在“工具调用不稳定”这个问题上反复挣扎的开发者二是想把团队内散落的各类业务能力统一管理起来、形成可沉淀资产的工程负责人三是对 Agent 底层机制感兴趣、想理解“为什么有些技能设计得好、有些技能设计得烂”的产品和技术同学。我尽量把思路、步骤、踩坑经验都写清楚你可以直接参考复现。2. 技能设计的核心思路为什么先抽象再实现2.1 先回答一个关键问题技能和工具的区别到底在哪很多人第一次接触 agent-skills 时最困惑的就是“技能”和“工具”这两个概念的边界。我在之前的项目里也反复纠结过这个问题后来慢慢形成了一个比较清晰的定义。工具Tool是最小可执行的原子能力它通常只做一件事输入输出都是明确的、无状态的。比如“查询天气”“发送短信”“计算两个日期之间的天数”这些就是工具。工具的核心特征是“确定性”——你给我入参我返回结果中间没有歧义。技能Skill则是一个更上层的抽象它封装了“完成一个目标”所需的一系列步骤和判断逻辑。技能内部可以调用一个或多个工具可以包含规则判断、分支处理、甚至嵌套调用其他技能。比如“安排一场跨城市的会议”这个技能它需要查询两地天气、查询参会人日程、对比空闲时间段、生成会议邀请、发送提醒这就不是一个工具能搞定的需要多个工具的协作并且中间还有决策逻辑。从工程上讲工具是“零件”技能是“组件”。零件追求的是单一职责、接口清晰组件追求的是内聚度高、对外行为稳定。如果你把零件当组件用Agent 的流程控制就会变得极其脆弱——模型需要自己判断“先调哪个、后调哪个、结果怎么拼”而这个判断恰恰是大模型最不擅长的。我自己在项目里习惯这样划分凡是需要模型做多步推理、且中间存在条件分支的一律封装成技能凡是纯粹的、单步的、无状态的查询或操作才暴露成工具。这个原则帮我避免了很多设计上的混乱。2.2 技能的五个构成要素我在设计 agent-skills 时会要求每个技能必须有五个构成要素缺一不可。这五个要素不是凭空想出来的而是从大量实际案例中总结出来的“必备字段”。技能名称Skill Name简短、唯一、语义清晰方便模型和其他技能引用能力描述Description说明这个技能能完成什么任务、适用于什么场景、不适用于什么场景参数协议Parameters定义入参和出参的 JSON Schema明确类型、必填项、约束条件执行逻辑Implementation具体的代码实现可以调用工具、访问知识库、做计算和判断错误处理Error Handling定义失败时的行为和降级策略包括异常分类、返回格式、重试规则这里我想特别强调一下“能力描述”的写法。很多人在写描述的时候喜欢写得特别泛比如“这个技能可以帮助用户处理各种日常任务”模型看了等于没看。好的描述应该是精确的、可判别的甚至要写清边界。举个例子“安排会议”技能的描述如果写成这样会更好用于协调两个或更多参会者的日程找到共同空闲时间并创建会议邀请。适用于跨时区、跨日历场景。不适用于需要预订实体会议室或审批流程的会议。这样模型在意图识别阶段就能比较精准地决定要不要调用这个技能而不是把“提醒我下午三点开会”这种简单请求也交给它处理。边界写清楚反而能减少误调用。2.3 技能和技能之间的关系层级与复用当技能数量超过二三十个时就需要考虑技能之间的层级结构了。我在实践中发现把技能设计成扁平结构非常不利于维护也不利于模型选择。更好的做法是引入“元技能”Meta-Skill和“子技能”Sub-Skill的概念形成两到三层的树状结构。元技能是一个“指挥型”技能它自身不执行具体的业务操作而是负责任务的理解和分解。比如“日程管理助手”是一个元技能它可以调度“查询日程”“安排会议”“调整时间”“发送提醒”这几个子技能。模型在收到用户请求后只需要判断应该进入哪个元技能具体的流程编排交给元技能内部的逻辑去处理而不是让模型在几十个平级技能里大海捞针。这种方式的一个显著好处是减少了模型每次决策时的候选集规模。当技能库有 50 个技能时让模型一次性从 50 个里选准确率往往不理想但如果让模型先选元技能比如 5 个到元技能内部再选子技能每个元技能下面 8~10 个两次选择的准确率都会明显提高。这其实借鉴了人类组织知识的方式——先分类再细化而不是把所有选项平铺在桌面上。不过层级设计有一件事要特别注意子技能可以被多个不同的元技能复用这时候子技能的参数设计就不要绑定具体的上游场景保持“通用性”是最好的。举个例子“查询日程”这个子技能应该只接收“时间范围”和“目标人”两个参数而不应该出现“是否包含会议邀请链接”这种和具体场景强相关的字段。一旦子技能被绑死后面的复用就会特别痛苦。3. 技能库的工程化落地从写代码到跑起来3.1 技能在代码层面长什么样说了这么多概念来看看技能在代码层面到底是怎么组织的。我习惯用一种比较轻量的方式来实现技能抽象核心是一个基类加一组规范方法。下面这个 Python 示例是我在实际项目中用过的结构做了简化你可以参考这个思路再去适配自己的技术栈。from pydantic import BaseModel, Field from typing import Any, Dict, List, Optional class SkillParameter(BaseModel): name: str Field(..., description参数名) type: str Field(..., description参数类型: string/integer/object/array) required: bool Field(False, description是否必填) description: str Field(, description参数说明) enum: Optional[List[str]] Field(None, description枚举约束如果有) class SkillDefinition(BaseModel): name: str Field(..., description技能唯一名称) description: str Field(..., description技能能力描述含适用/不适用边界) parameters: List[SkillParameter] Field(..., description参数协议定义) tags: List[str] Field([], description技能标签便于检索分组) class BaseSkill: 所有技能的基础类 definition: SkillDefinition def __init__(self, ctx: Any): self.ctx ctx # 上下文对象包含模型实例、工具注册表、存储等 self._validate_definition() def _validate_definition(self): 校验技能定义是否合法很多问题在注册阶段就要发现 assert self.definition.name and isinstance(self.definition.name, str) assert self.definition.description and len(self.definition.description) 20, \ 技能描述太短模型无法准确识别用途 # 参数名校验避免和系统上下文字段冲突 reserved {query, history, user_id, session_id} for p in self.definition.parameters: assert p.name not in reserved, f参数名 {p.name} 与系统保留字段冲突 async def run(self, **kwargs) - Dict[str, Any]: 执行技能逻辑子类必须实现 raise NotImplementedError async def safe_run(self, **kwargs) - Dict[str, Any]: 统一入口负责参数校验、执行、异常兜底 try: validated self._validate_params(kwargs) return await self.run(**validated) except SkillParamError as e: return {status: param_error, message: str(e)} except SkillExecutionError as e: return {status: execution_error, message: str(e)} except Exception as e: # 兜底未预期异常也要能返回结构化错误 return {status: unknown_error, message: f{type(e).__name__}: {str(e)}} def _validate_params(self, raw_params: Dict[str, Any]) - Dict[str, Any]: 参数校验类型、必填、枚举 validated {} for p in self.definition.parameters: value raw_params.get(p.name) if value is None: if p.required: raise SkillParamError(f缺少必填参数: {p.name}) continue # 实际项目中会做更严格的类型检查 if p.enum and value not in p.enum: raise SkillParamError(f参数 {p.name} 取值必须在 {p.enum} 中) validated[p.name] value return validated class SkillParamError(Exception): pass class SkillExecutionError(Exception): pass这段代码看似简单但有几个细节值得展开说。第一safe_run是统一入口所有外部调用都必须走这个方法不能直接调run。因为只有统一入口才能做到“所有技能都有一致的错误返回格式”否则每个技能各写各的错误处理上层调度逻辑会非常难做。第二safe_run的设计我特意让异常处理不抛错而是返回结构化的状态对象。这里的原因很实际在 Agent 应用里技能的调用者是大模型而大模型无法处理乱糟糟的异常堆栈。它需要的是“这次调用失败失败原因是缺少参数 X”这样的干净信息这样模型才能决定下一步怎么走。如果直接把 Python 异常堆栈丢给模型它可能会产生幻觉甚至编出一个不存在的原因。第三_validate_definition在注册阶段就执行这属于“前置校验”的思路。我踩过不少坑技能开发到一半发现描述太短模型完全不知道该在何时调用或者参数名和系统上下文冲突导致传参错位。与其等问题到了线上才暴露不如在注册阶段就用程序把这类低级问题拦住。3.2 技能注册与发现如何让模型找到对的技能技能写好了还要有注册和发现的机制。这里我分享两种在实践中效果比较好的方案。第一种是“基于描述的语义检索”。把每个技能的 description 向量化存入向量数据库。当用户请求进来先用一个轻量模型把请求转换成语义向量检索 top-k 个相关技能再把候选技能列表和用户请求一起发给主模型做最终选择。这个方案的好处是技能数量很大时也能保持较好的检索效率缺点是增加了系统复杂度需要额外维护向量库。第二种是“基于规则和标签的匹配”。给每个技能打上标签比如“日程”“邮件”“数据分析”“订单处理”等同时在上层做一层意图分类先判断用户请求属于哪个领域再只把该领域下的技能列表交给模型选择。这个方案实现简单、延迟低在小规模场景下效果很好。我目前在业务量不大时倾向于用这种方案因为部署简单、效果稳定。我见过一个比较惨痛的案例某团队把所有技能描述拼成一段超长文本塞进系统提示词里想让模型直接从中选择。当技能数量达到 40 个以上时模型开始漏选、错选而且随着描述文本变长任务遵循度急剧下降——即便调高温度也没用。这几乎是所有“全部平铺进 Prompt”方案的最终归宿。所以我还是建议无论选哪种方案都要做一个真正的“技能路由层”不要指望模型在一个巨大的选项列表里做出可靠选择。技能路由层的核心逻辑可以这样理解它不是让模型在 50 个技能里选 1 个而是先做一次粗粒度的过滤比如从 50 个变成 6~8 个候选再做一次细粒度的选择。第二次选择可以靠模型也可以靠更复杂的规则。工程上这个思路和推荐系统的召回-精排两阶段架构是一模一样的底层逻辑都是“候选集太大时先粗筛再精排”。3.3 技能的参数设计JSON Schema 是契约不是摆设参数协议是技能和模型之间的契约。我在代码里用了 pydantic 来定义参数其实它和 JSON Schema 是等价的。真正让我吃过亏的是参数设计时忽略了“模型侧的理解成本”。一个常见的反面例子是这样设计一个“查询订单”技能参数表里有一个filters字段类型是object描述是“查询过滤条件”。这个字段在代码里很合理但对模型来说却是一个“黑洞”——它不知道里面应该放什么。模型试着生成{filters: {status: shipped}}但你后端真正期望的可能是{filters: [{field: status, op: eq, value: shipped}]}这样的结构一旦对不上就报参数校验错误。这类问题的根源在于参数协议设计时只考虑了“代码好不好写”没考虑“模型好不好生成”。我总结了一些参数设计的经验现在已经在团队里形成规范嵌套层级不超过两层超过两层模型生成的准确性会显著下降。如果一定要嵌套把复杂结构拆成多个平级参数。枚举优先能规定枚举值的字段必须给枚举不给string裸奔。模型在有限选项里选择的准确率远高于让模型自由生成的准确率。默认值要写清楚非必填参数一定要在描述里注明“默认值是多少”这能避免模型因为不知道默认值而强行传一个。参数描述要写“为什么”不只是写“这个参数是什么”还要写“什么时候需要用到它”帮助模型判断何时该传、何时不该传。3.4 完整技能示例从需求到实现说了半天可能还是有点抽象。我们用“查询订单状态并生成简要摘要”这个技能来完整走一遍设计过程。第一步明确这个技能的目标。用户想知道订单现在到哪一步了、什么时候能到、当前有没有异常。这个诉求听起来简单但做起来有几层逻辑要查订单主状态、要查物流轨迹、要判断是否有异常比如滞留超过 48 小时还要把结果组织成一段自然语言摘要。第二步确定参数协议。入参只需要order_id字符串必填和include_history布尔非必填默认 False控制是否返回详细轨迹。出参是一个结构化的 JSON包含订单状态、预计送达时间、异常提示和摘要文本。第三步设计执行逻辑。技能内部先调用“查询订单主状态”工具再根据include_history决定是否调用“查询物流轨迹”工具。如果主状态是“已签收”直接返回不做额外处理。如果状态是“运输中”且轨迹时间戳距今超过 48 小时没有更新则标记为“物流异常”并提示用户联系客服。最后把各个数据拼接到一个模板里生成摘要。第四步定义错误处理。如果order_id不存在返回status: order_not_found并附上一句“请核实订单号”。如果是第三方物流接口超时返回status: logistics_timeout并降级为只返回订单主状态同时提示用户“物流信息暂时无法获取”。这个技能设计好之后模型调用它时不需要关心内部有哪些工具、内部怎么判断异常它只需要提供order_id和include_history两个参数然后接收一个结构化的结果。这就是技能抽象的价值——把复杂性封装在内部对外暴露一个简单稳定的接口。4. 技能的调试、评测与迭代真正拉开差距的地方4.1 没有评测体系的技能建设就是空中楼阁很多团队在 Agent 开发阶段跑得很快demo 演示效果也很好但一到上线就开始出问题。最大的原因是他们根本没有建立评测体系开发阶段的“看起来还行”其实只是少数几条路径跑通了真实的输入分布一旦发生变化行为就不可控了。我在技能建设上踩过最重的一次坑是某个技能在 200 条测试样本上准确率达到 92%上线后线上准确率跌到 61%。问题出在测试样本是我自己构造的覆盖的多是“标准问法”而线上用户的实际表达五花八门经常省略关键信息或者把多个诉求揉在一句话里。从那之后我再也不敢用自造样本评估技能了全部改为从真实日志里抽样、标注、回流进测试集。对于技能评测我现在的做法分三层第一层是“单技能准确率评估”对每个技能构造一批测试请求检查它的“路由命中率”该调用时是否被正确调用和“执行成功率”调用后是否完成了预期操作。第二层是“多技能混淆测试”构造一批跨领域的请求专门测试两个相似技能之间是否会发生误调用。比如“调整会议时间”和“取消会议”这两个技能特别容易混淆需要专门准备边界样本。第三层是“端到端任务评测”从真实业务场景里抽取完整的用户故事跑完整流程检查最终产出是否符合预期。这一层最耗时但也最接近真实效果。这三层评测构成了一个金字塔底层的失败会导致上层无法通过。任何一次技能改动都必须回归跑完这三层测试才能发布上线。这不是什么高深的做法但确实能帮我躲掉很多线上事故。4.2 调试技巧如何快速定位“模型没按预期调用技能”在开发调试阶段最常遇到的场景是我明明写好了技能模型就是不调用它或者调用的时候传的参数不对。这种问题排查起来很费时间我分享几条经验。第一先看“技能描述是否准确匹配用户意图”。我调试过很多“模型不调用技能”的案例最后发现 80% 都是描述写得不对。比如我把“查询天气”技能的描述写成“获取指定城市的实时天气数据”而用户问的是“明天适合穿什么”模型会觉得这是穿衣建议而不是天气查询于是不调用技能直接自己回答。修改描述后加入“当用户询问天气情况、出行穿衣建议等与天气相关的信息时可以使用本技能”调用率立刻提升。第二检查模型的输出格式是否符合你的解析逻辑。如果模型返回的 JSON 与预期结构不一致先别急着骂模型看看你的 Prompt 里有没有给出足够的格式示例。大模型对格式的遵循度高度依赖示例的清晰度我通常会在系统提示词中附上“正例”和“反例”效果比只说“请按照 JSON 格式返回”要好得多。第三善用日志和链路追踪。每一个技能的调用都应该记录用户请求原文、路由结果、模型选择技能的前 3 个候选、实际传入的参数、执行结果。没有这些日志排查问题基本靠猜效率极低。我在项目里强制要求所有技能调用必须打印这类日志哪怕只是为了开发调试也可以先全量打印再逐步收敛。4.3 从最小可用到产品级技能迭代的节奏最后一个我想重点展开的话题是技能的迭代节奏。不要试图一开始就构建一个完整且完美的技能库这会陷入“设计瘫痪”。我倾向的做法是先做最小可用集合再按场景逐步扩展。具体节奏大概是这样第一个版本只做 5~10 个最关键的高频技能把技能的流程跑通。这时候甚至可以允许一些实现是硬编码的、朴素的——关键目标是验证“路由-执行-返回”这条链路是否顺畅。第二个版本开始加入评测体系用真实日志回流数据找出误调用和失败率最高的技能优先优化。第三个版本再做层级化和复用把重复逻辑抽成公共子技能。这样做的原因很简单技能体系是一个高度依赖真实反馈的系统凭空设计容易脱离实际。在初期阶段你根本不知道用户会怎么表达需求与其花三周设计一套完美的抽象层不如花三天做一版能用的然后基于真实数据持续迭代。我见过太多团队花了一个月在设计技能体系、梳理领域模型结果上线后发现真实的用户需求路径和设计时的假设完全不一样推倒重来。5. 写在最后的一些想法从“工具”到“技能”的转变本质上是对大模型能力边界的一种尊重和理解。工具要求模型自己学会编排技能则把编排逻辑沉淀到了代码层。这两者的差别在 demo 阶段看不明显但一旦进入生产环境稳定性、可维护性、可观测性的差距会呈指数级放大。如果你正在做 Agent 应用我建议你现在就可以试试这种方法挑一个你最常用的工具调用场景把它按前面说的五个构成要素重新整理一遍——能力描述、参数协议、执行逻辑、错误处理、边界条件。你会发现光是“写清楚边界条件”这一步就能帮你规避掉大量线上问题。另外说一个实际体验技能体系的建设是一个持续的过程不要指望一蹴而就。我自己的技能库从最初的 6 个技能迭代到今天的 40 多个技能中间经过了不下十轮重构。每一轮重构的动力都来自真实的数据反馈——哪个技能误调用率高哪个技能的描述让模型产生了歧义哪个技能的异常返回格式不统一导致上层处理逻辑写得很难受。这些问题只有上线跑起来之后才能真实地暴露出来。最后再分享一个小技巧给每个技能加一个“版本号”字段并且在日志里记录每次调用时技能的版本。当技能逻辑发生变更时你才能回溯“这个调用是哪个版本执行的”否则你将面临一个经典的困境——技能改坏了但不知道从哪个版本开始坏的。这个字段几乎不花成本却能在调试时节省大量时间。