这两年做大模型应用的人应该都听过一个词agent-skills。不少团队其实已经把它用起来了但网上聊得都比较散要么是在讲理念、要么是贴论文截图真正能把“技能”这件事从头到尾讲清楚、说人话的文章很少。这篇文章我就想用自己的实际经验把 agent-skills 这个东西掰开揉碎聊一遍它到底解决什么问题、技能目录怎么组织、参数怎么定义、调用链路怎么设计、以及我在实际项目中踩过的几个坑。如果你是做智能体应用开发的工程师或者正准备把 AI 能力集成进现有系统的技术负责人这篇应该能帮你少走不少弯路。1. 为什么智能体需要一套独立的“技能体系”先聊点背景。大模型本身的推理能力确实强但单靠一个模型对话窗口是干不了“打开浏览器订机票再发个日历日程”这类组合操作的。真正的智能体应用得能调用外部工具、访问外部数据、操作具体系统这就引出了一个问题怎么让模型知道“什么场景该干什么事”1.1 技能拆分的核心逻辑我在早期的项目里走过一段弯路。当时的做法非常直接把所有能调用的函数全写进 System Prompt让模型自己挑。功能少的时候还好一旦功能超过二十个问题就来了——模型经常漏掉某些工具或者在完全不相干的场景下强行调用某个工具上下文一长效果更是断崖式下跌。后来我换了个思路就是 agent-skills 的做法把“能力”从“模型提示词”里抽出来重新组织成一套独立的、可被选择性加载的技能模块。每个技能模块描述自己“能干什么”“需要什么参数”“如何执行”而模型只需要在合适的场景下根据任务描述加载对应技能即可。这背后的逻辑其实有点像微服务改造——把一个大单体拆成多个独立部署的小服务每个服务职责单一组合起来才能形成完整业务能力。1.2 技能和工具、插件到底有什么区别很多文章把工具、插件、技能这几个概念混着用但它们其实是有层级关系的工具Tool是最底层的原子操作比如“发送HTTP请求”“读取本地文件”它不关心业务。技能Skill是基于工具的封装包含了一个完整的“能做什么”的描述、执行逻辑、可能需要的子步骤甚至还包括该在什么场景下被触发。比如“查询今日天气”就是一个技能它背后可能调用了两个 HTTP 工具。插件Plugin通常指一组相关的技能集合比如“出行助手插件”可能包含了“查航班”“订酒店”“查天气”等多个技能。从调用关系上看技能是模型感知的最小单位。模型不需要了解底层工具的 HTTP 请求是怎么组装的它只需要知道“这个技能负责什么”“该传什么参数”具体执行交给技能内部的程序逻辑处理这种抽象层级能显著减少模型的决策负担。2. 技能目录的设计与组织方式技能体系搭建的第一步不是写代码而是设计目录结构。目录结构设计得好不好直接决定了后续技能扩展顺不顺畅、模型匹配准不准。2.1 技能清单 Manifest 的设计要点一个标准的技能目录通常包含几个核心要素技能名称、描述、参数定义、触发条件、执行逻辑引用。在工程实现上我习惯用一个 Manifest 文件来描述技能的元信息类似下面这种结构# manifest.yaml name: weather_query description: 查询指定城市未来N天的天气预报 version: 1.0.0 author: agent-team triggers: - 天气 - 气温 - 下雨 - 带伞 parameters: - name: city type: string required: true description: 城市名称如“北京”“上海” - name: days type: integer required: false default: 3 description: 查询的天数范围最大支持7天 execution: type: python module: skills.weather_query entrypoint: run这里有几个关键点描述description必须写清楚“这个技能有什么用”。这句话是给模型看的。模型会根据任务描述和技能描述做语义匹配写得太笼统模型可能搞不清楚什么时候该用它。比如“查询天气”就比“天气相关的数据处理”要清晰得多。触发条件triggers是给召回用的关键词。实际项目中我发现光靠描述做匹配会产生不少漏召回。配合一组明确的触发关键词能大幅提高命中率。这里的关键词可以理解为“如果用户消息里包含这些词优先把技能拉出来让模型评估”。参数定义要给足约束。模型在抽取参数时如果缺少约束很容易丢字段或者多传字段。所以我在参数定义里都加了类型、必填标记和默认值这能显著降低参数幻觉的概率。2.2 技能仓库的目录结构参考一个中型智能体项目的技能目录我通常是这样组织的agent-skills/ ├── manifests/ # 所有技能的注册清单 │ ├── weather_query.yaml │ ├── calendar_create.yaml │ └── email_send.yaml ├── skills/ # 技能的实际执行代码 │ ├── weather_query/ │ │ ├── __init__.py │ │ ├── run.py │ │ └── utils.py │ ├── calendar_create/ │ │ └── ... ├── registry.py # 技能装载器负责扫描、索引、注册 ├── matcher.py # 技能召回与匹配模块 └── config.py # 全局配置注册清单和执行代码分开是一开始就要坚持的结构。如果清单和执行逻辑混在一起等技能数量超过十个每次改动都会变成一场灾难。我自己经历过一次重构就是因为早期图省事把技能描述直接写在代码里结果要改一个描述得翻半天源码。3. 技能召回的匹配机制到底怎么选技能召回是 agent-skills 架构里最容易被低估的一个环节。它解决的核心问题是面对用户的一句话系统怎么知道该启用哪些技能3.1 关键词匹配与语义匹配的取舍最初级的方式是关键词匹配用户提到“天气”就把天气技能拉出来。这种方式效率高、可解释性强但泛化能力差用户说“今天出门要不要带伞”就匹配不上了。进阶一点的做法是用 Embedding 做语义匹配把用户消息和每个技能的描述向量化算相似度相似度高的技能被召回。这种方式泛化能力强但需要一个好的向量化模型和相似度阈值策略。我在项目中实测下来RAG 里的向量召回经验完全可以迁移到技能召回上。还有一种混合策略我更推荐。先用关键词做粗筛选出候选集再用语义匹配做精排def recall_skills(user_message: str, threshold: float 0.42): # 第一阶段关键词硬匹配用于快速过滤 candidate_scores {} normalized_msg normalize(user_message) for name, manifest in registry.iter_manifests(): score 0.0 # 触发词必须有细微的模糊处理才有效 for trigger in manifest.triggers: if trigger in normalized_msg: score 0.5 # 第二阶段语义精排仅在关键词阶段无法决断时启用 if max(candidate_scores.values(), default0) threshold: for name, manifest in registry.iter_manifests(): emb_msg embed(user_message) emb_desc embed(manifest.description) semantic_score cosine_similarity(emb_msg, emb_desc) candidate_scores[name] max(candidate_scores.get(name, 0), semantic_score) ranked sorted(candidate_scores.items(), keylambda x: x[1], reverseTrue) # 返回超过阈值且排名前3的技能 return [name for name, score in ranked if score threshold][:3]这套方案的好处是常见的高频表达靠关键词就能快速命中成本和延迟都很低表达方式比较花的靠语义兜底。实测下来关键词语义的组合召回率能比纯语义提升7%~10%左右。3.2 阈值设定的经验值阈值设置是另外一个容易翻车的地方。设得低了无关技能会被召回干扰模型判断设得高了该召回的没召回技能就“哑火”了。我在多个项目里沉淀了一些经验值供参考匹配方式推荐阈值说明纯关键词匹配直接命中即通过如果命中≥1个trigger直接进入候选集语义相似度0.35~0.45低于0.35误召回太多高于0.5漏召回明显混合策略综合分0.5左右关键词得分和语义得分加权合并注意阈值不是拍脑袋定的要拿真实用户语料去回测。我一般会留出几百条线上真实请求标注好“期望调用哪个技能”然后反复调参直到准确率和召回率的平衡点满足业务要求。4. 技能执行的链路设计与上下文管理召回只是第一步真正体现工程水平的是技能执行的链路设计。这里涉及到技能内部怎么跑、结果怎么回传给模型、上下文怎么保持。4.1 技能内部执行流程每个技能的内部执行我建议统一遵循五步流程参数校验 → 执行准备 → 调用外部能力 → 结果格式化 → 异常兜底。以日历创建技能为例async def run(payload: dict): # 第一步参数校验 title payload.get(title) start_time payload.get(start_time) if not title or not start_time: raise SkillParameterError(缺少必要参数title/start_time) # 第二步执行准备比如初始化SDK客户端 client CalendarClient() # 第三步调用外部能力 event_id await client.create_event( titletitle, start_timestart_time, durationpayload.get(duration, 60), attendeespayload.get(attendees, []), ) # 第四步结果格式化给模型一个清晰的结构化结果 return SkillResult( statussuccess, data{event_id: event_id, event_url: fhttps://cal.example.com/e/{event_id}}, summaryf已创建日程: {title}, )我特别想说一下参数校验这一步。很多同学写技能时默认“模型传的参数一定是对的”这绝对是天真了。模型在抽取时间时可能给你一个“明天下午三点”也可能给你一个“后天上午”这些自然语言必须要做解析和标准化。我的做法是每种类型的参数都定义一个 parser日期统一转成 ISO 格式、城市统一走地理编码接口、时长统一转成分钟数在进入业务逻辑之前把脏数据全部处理干净。4.2 技能执行结果如何回传给模型执行完技能之后结果不是直接丢给模型看而是要经过一层“结果摘要”的处理。原因是很多外部接口返回的数据对用户来说有用但对模型做下一步决策来说可能是噪音。比如天气查询技能外部接口可能返回温度、湿度、风速、空气质量、日出日落时间等几十个字段但模型只需要知道“明天北京多云12~23度有3级北风”那么在执行层就应该把结果压缩成简洁的结构化文本。这样模型读起来轻量后续回答问题的 token 成本也能省不少。回传的格式上我建议至少包含状态status、结果对象data、给模型看的摘要summary。摘要用自然语言写model 可以直接引用避免模型对接原始 JSON 时“看不懂、乱解读”。4.3 多轮对话中的上下文隔离这是 agent-skills 里一个非常细但非常重要的实操点。当一次对话中要连续调用多个技能时比如用户说“帮我查一下北京明天天气顺便把后天下午三点订一个和客户的会议”系统可能会先召回到天气查询再召回到日历创建。这时候第 2 个技能需要拿到第 1 个技能产生的部分信息吗需要。但它需要完整看到天气接口的原始响应 JSON 吗不需要。所以我在设计上把上下文分成了两层全局会话上下文和技能本地上下文。全局上下文保存用户偏好、实体信息比如用户所在城市、历史对话摘要技能本地上下文只包含该技能执行所需的输入输出。技能之间的数据流通统一通过一个“共享内存”接口来完成context AgentContext() context.set(user.city, 北京) context.set(weather.result, {city: 北京, date: 明天, condition: 多云}) # 日历技能只读取它关注的成都 user_city context.get(user.city)这种隔离设计避免了技能之间的数据污染。早期我踩过一次坑天气技能把原始 JSON 写进了上下文结果日历技能在生成会议地址的时候居然读到了天气 JSON 里的某个字段把“气温12度”当成了会议备注写进了日程。隔离之后这种问题就再没出现过。5. 技能编排的容错与降级任何一个真实系统都绕不开容错。技能调用外部接口不可能永远成功。外部服务可能挂掉、超时、返回非预期数据而模型在遇到这些错误时往往不知道该怎么优雅处理。5.1 三级降级策略我在项目中给每个技能都配置了降级策略这里分享一个通用模板。故障级别表现处理方式L1参数解析失败对参数进行纠错尝试二次抽取若仍失败向用户提出澄清问题L2依赖接口超时返回“暂时无法获取”并自动将技能标记为“不可用”避免模型反复尝试同一技能L3业务执行失败尝试备选技能比如查天气失败尝试查天气预警接口若全部失败返回结构化错误这里关键是L2 的处理。模型面对超时错误时如果没有降级逻辑它大概率会像“没有感情的机器”一样反复重试同一个失败的技能既浪费 token 又拖慢响应。在技能框架层做一个“熔断标记”让同一技能在一次会话内最多只允许被调用两次第二次失败就标记为不可用模型会自动选择其他路径或者坦白告诉用户“这个功能暂时不可用”。这个策略在线上跑下来用户的“无意义等待”少了很多。5.2 多技能协作时的回滚机制多技能协作时还会出现复杂问题比如用户先要求创建日程然后又要求给参会人发邮件。如果创建日程成功、发邮件失败系统应该怎么办是把邮件放到“草稿箱”还是把日程也一并取消我的建议是在设计技能编排时要区分“可回滚操作”和“不可回滚操作”。执行业务操作前先判类型。像“发邮件”“发消息”这类动作原则上不可回滚应推迟到最后执行。像“创建日程”“修改配置”这类操作如果后续环节失败应提供“撤销/取消”的接口或者能力。如果做不到回滚至少要给用户提供一个明确的“状态告知”不让用户误以为所有操作都成功了。这块不能全靠代码保障还得在技能描述里写清楚哪些技能会产生“不可撤销”的副作用让模型在编排计划的时候就有所顾虑。6. 技能调试与评估别等上线了才叫苦说到调试和评估这在 agent-skills 社区里讨论得不多但我觉得恰恰是决定项目能不能持续迭代的核心环节。6.1 离线回放与单测场景技能栈的特殊性在于不仅要测代码逻辑还得测“模型在什么场景下触发它”。我在团队里推了一套“场景回放测试”把线上的真实用户请求全部录下来标注好期望行为再跑一遍技能匹配和调用链路对比实际输出和期望输出的差异。相当于把多轮对话系统变成了一套自动化测试集。def test_weather_skill_triggered_in_daily_query(): user_msg 明天去杭州出差穿什么衣服合适 expected_skills [weather_query, packing_suggestion] actual_skills skill_matcher.match(user_msg) assert set(expected_skills).issubset(set(actual_skills)), ( f期望召回 {expected_skills}实际召回 {actual_skills} )一开始这套测试跑起来百分之四五十都是红的因为描述写得太抽象模型经常只召回其中一部分技能。经过几轮对描述、触发词的调优命中率慢慢到了百分之九十以上。这个过程痛苦但价值非常大。6.2 线上请求的“劣化”监控技能上线后还要关注两个关键指标技能召回率和技能执行成功率。召回率 本次实际正确召回的技能数 / 本次期望召回的技能数。执行成功率 技能执行成功次数 / 技能被调用总次数。如果召回率低问题大概率出在描述、触发词和用户实际表达方式的语义鸿沟上如果执行成功率低大概率是参数解析、外部依赖的健壮性问题。这些指标要通过日志系统持续统计效果下降时及时定位是哪些技能在掉链子。7. 跨平台可移植性与技能生态最后聊点大的技能体系跨平台可移植的问题。现在各家智能体平台都有自己的 agent 框架但它们对“技能”的抽象并不完全一致。有人用 JSON Schema 定义函数有人用封装好的 API 插件有人自己搞一套 DSL。如果团队将来需要从一个平台迁移到另一个平台或者同时对接多个平台技能体系的“表达能力”就必须足够通用。7.1 技能与特定框架解耦我的做法是技能的核心执行逻辑尽量写成普通 Python 模块不依附于任何特定 agent 框架。Manifest 文件里只描述元信息执行代码里不带任何框架 SDK 的调用。框架相关的适配逻辑单独抽一层适配器。比如同样是一个“搜索网页”技能在适配器层可能有LangChainAdapter、OpenAIAdapter、自研AgentAdapter三种实现但它们都调用同一个核心技能逻辑。这样框架升级、切换都不需要重写技能本身只需要更换适配器。7.2 技能市场的可组合性技能复用做到一定程度后自然会出现“技能市场”的诉求——团队内部共享技能、跨项目复用技能、甚至对外发布技能。我在团队内部搭过一个简单的技能索引服务每个技能上线时提交一份 manifest 和对应测试集服务自动完成质量评估并登记入库。别的项目要用直接通过 registry 拉取对应技能包即可。这套机制跑起来之后技能的平均开发周期从原来的两周缩短到了四五天因为很多基础能力都是现成的只要做组合和微调就行。从长远看我觉得 agent-skills 的场景容器化特征已经越来越明显。基础性技能比如查天气、查日历、发邮件会逐步标准化而真正有价值的是在这些技能之上组合出的业务闭环。8. 几个老生常谈但总会踩的坑说了一堆设计思路最后再把我在真实项目里踩过、改过、总结过的几个高频问题列出来相当于一个快速避坑清单。8.1 Manifest 写的技术词汇太多模型“看不懂”这是出现频率最高的问题。很多工程师写技能描述时会用“获取用户地理坐标并检索周边POI”这种描述。模型也许能理解但召回效果通常不如“查询附近的餐厅、商场、地铁站”这种偏用户视角的描述。我的建议是描述里至少包含两个用户会直接使用的动词和名词而不是纯技术术语。8.2 参数复用导致上下文爆炸在多技能协作时模型可能需要把同一个参数传给多个技能。比如建日程时用到的“参会人列表”发邮件时也要用到。如果每个技能都把完整参数写在会话里多轮过后上下文就会非常臃肿。我的做法是用“参数引用”代替“参数拷贝”比如让日历技能输出一个entity.id发邮件技能通过这个 ID 去共享上下文里取参会人列表而不是把完整列表再次塞进对话历史。8.3 一个技能试图干太多事“技能职责单一”这条边界很容易被突破。比如一开始“天气查询”只管查天气后来有人要求它顺便推荐穿衣搭配再后来又要求它推荐附近避雨的地点结果这个技能的描述越来越庞大触发场景越来越多匹配准确率一路下滑。后来我把它重新拆成了“天气查询”“穿衣建议”“避雨场所推荐”三个独立技能用编排链串起来整体效果反而更好了。技能可以组合使用但单个技能的职责边界必须清晰。8.4 日志里看不到“为什么调用了这个技能”线上调试最头疼的就是黑盒问题用户发了一句“帮我看下明天出行安排”系统居然调用了天气查询而不是日程查询。如果日志里只有模型最终调用的结果没有记录召回阶段的得分、候选排序排查将会非常痛苦。我在框架里强制要求每一步召回和调用都记录结构化日志日志包括候选技能、匹配得分、模型最终选择、调用结果。出问题时直接翻日志链路基本 5 分钟内能定位到是哪一环出了问题。这个习惯强烈建议从一开始就养成。聊到这其实 agent-skills 的核心要点也就这么些技能描述要清晰、召回策略要稳健、执行链路要标准化、上下文要可控、技能边界要清晰、日志要完整。这套方法论我在好几个真实项目中反复实践过不能说毫无问题但整体稳定性和迭代效率是看得见的提升。如果你正在搭自己的智能体应用不妨从一个小技能开始跑通这套体系再逐步扩展应该会少走很多弯路。