做 AI 应用开发这几年我遇到最多的一个问题是模型能力肉眼可见地在变强可真把它丢到业务现场却总是一碰就碎。用户问一句帮我查一下XX商品还有没有货模型能流利地回复一段话但就是没法真正去查库存——因为它没有手没有脚也没有一套可以随时调用、可验证可兜底的能力模块。后来我把注意力从调提示词转向搭技能系统才慢慢打开局面。所谓 Agent Skills翻译过来就是智能体技能核心思路是把 Agent 能执行的所有操作封装成标准化的技能让模型像人一样按需取用。这篇文章我会从设计思路、分类体系、代码实现到编排协同把我在实际项目里积累的经验完整拆出来适合正在做 Agent 应用、或者准备把大模型接入业务系统的人参考。1. 先想清楚Agent Skills 到底解决什么问题1.1 从会聊天到能干实事的差距大模型本质上是一个概率性的文本生成系统它的强项是理解、归纳、生成而不是执行。你问它北京今天适合穿什么它能给你一个很漂亮的回答但如果你问帮我订一间明天下午的会议室它就无能为力了因为它根本没有访问会议室系统的通道。这中间的差距就是业界常说的最后一公里问题。模型负责大脑但身体的执行能力——查数据库、调接口、操作文件、发送通知——需要一套外挂机制来补齐。Agent Skills 就是这套外挂机制的载体它把每一个可执行动作封装成有名字、有描述、有参数协议、有实现代码的模块模型只需要理解什么时候该用哪个技能剩下的脏活累活交给技能本身去完成。这个思路乍一听很简单但实际落地时远比想象中复杂。因为真实业务里不是只有一个技能而是有成百上千个技能之间还有依赖、有先后顺序、有权限边界如何组织这些技能让模型在万千选项里精准选对才是核心难点。1.2 技能、工具、插件别再傻傻分不清很多人会把工具调用和 Agent Skills 混为一谈其实它们有本质区别。工具调用是一种底层能力指的是模型输出结构化指令、由外部函数执行的机制比如函数调用。而技能是一个更上层的概念它把工具 使用说明 适用场景 参数规范 后处理逻辑打包成一个完整的单元。插件则是另一个维度的概念它通常是某一个平台或框架里的一组扩展包可以包含多个工具、多个技能、甚至一组界面元素。我用一个生活化类比来解释工具像螺丝刀技能像一个会用螺丝刀组装宜家柜子的完整操作指南插件则是一整个工具箱。螺丝刀解决的是单个动作技能解决的是在什么条件下、按什么步骤、做完之后怎么办这整件事。所以你在设计 Agent 系统时直接让模型调用工具通常只能应付最简场景。一旦业务逻辑复杂起来模型很快就不知道该选哪个工具、参数怎么填、结果怎么收尾。而把工具升级为技能多了一层情景化的智慧等于给模型配了一个现场老师傅它看着技能描述就知道该不该出手、怎么出手。1.3 为什么技能必须独立抽象我见过不少团队一开始图省事把业务逻辑直接写死在系统提示词里比如在 prompt 里写一大段当用户询问库存时请调用 query_inventory 函数参数是 sku_id……。这种方案在小规模 demo 里能跑通但稍微一变就崩技能数量超过十个系统提示词被撑得巨长模型注意力被稀释调用准确率直线下降技能逻辑一改又要重新调 prompt维护成本极高。技能独立抽象的意义在于把模型该知道的事和系统该做的事彻底分离。技能的名称、描述、参数定义作为元数据传给模型让模型做选择和决策技能的实现代码则完全脱离 prompt由后端可靠地执行。两者之间只通过结构化的 JSON 通信边界清晰各司其职。这样一来新增一个技能不需要改动任何业务代码和 prompt只需注册一条元数据调整某个技能的内部实现也不会影响模型的调用习惯。这是 Agent 系统能否规模化演进的分水岭。2. Agent Skills 的体系设计与命名规范2.1 按能力边界划分技能层级我在实践中发现把技能一股脑地放进一个大列表里是最快的翻车方式。模型面对几十个甚至上百个技能描述时选择压力会急剧上升出现误调用、漏调用的概率也水涨船高。合理的做法是给技能分层让模型按层级逐步缩小选择范围。原子技能不可再拆的最小执行单元比如查库存算运费发送邮件。原子技能直接对接某个接口或函数逻辑单纯一个技能只干一件事。业务技能面向某个完整业务诉求的技能通常会内部编排多个原子技能。比如处理退货申请这个技能内部可能依次调用了查订单校验退货规则生成退货单通知仓库四个原子技能。流程技能跨多个业务环节的复杂技能往往是多条业务链路的固化成形例如完成一笔订单的全流程履约。这样分层之后大模型在意图识别阶段的决策空间大幅缩小。它首先判断用户诉求落在哪个业务域再去该域所挂接的技能子集里做精细匹配而不是从全量技能里大海捞针。这个先粗筛、后精选的漏斗式思路是技能体系设计的第一原则。2.2 技能描述写给大模型看的说明书技能描述是决定调用准确率的最关键因素没有之一。同一套技能实现描述写得好与差效果可能差出一大截。因为大模型只有靠文本来理解技能它看不懂代码只能读你写的说明。一个合格的技能描述至少包含四个要素技能职责、触发场景、禁用场景、使用注意。我用一个实际案例来对比。写得潦草的描述查询库存。 这个描述过于空泛模型在遇到这个商品什么时候能到货时可能也会去调库存技能因为到货和库存语义相近但实际上应该调用物流查询技能。写得完整的描述查询指定商品的实时库存数量。当用户询问某商品是否有货、剩余多少件、库存是否充足时调用此技能。如果用户询问的是物流配送时间或到货预测请勿调用本技能应使用物流查询技能。注意本技能返回的是当前各仓实时库存不包含在途库存。 这样的描述给了模型清晰的触发条件、边界条件和不适用场景模型就不容易跑偏。我习惯在每条描述里都用当用户……时调用如果用户……请勿调用这样的句式把正例和反例都写清楚。描述要像产品需求文档一样精准而不是像散文一样飘逸。2.3 参数 Schema定义人机协议的关键技能的参数定义在行业里一般用 JSON Schema 来描述。这部分的严谨程度直接决定了模型能否正确地为技能填充参数。如果参数定义模糊模型就会靠猜猜错的结果就是程序报错或返回脏数据。在写参数 Schema 时我踩过不少坑现在总结出四条铁律参数命名用具象的英文名不要用缩写。good 用inquiry_reason而不是ir避免模型误解。每个参数都要写清晰的 description说明值的含义、格式、取值范围必要时给示例值。用 enum 约束的参数一定要列全可选值不要指望模型自由发挥就对了。required 数组里只放绝对必需的参数尽量减少前置条件给模型留出弹性空间。举例来看{ name: query_inventory, description: 查询指定商品的实时库存数量。当用户询问某商品是否有货、剩余多少件、库存是否充足时调用此技能。如果用户询问物流配送时间或到货预测请勿调用本技能应使用物流查询技能。, parameters: { type: object, properties: { sku_id: { type: string, description: 商品唯一标识通常为8位字符如 AB123456 }, warehouse: { type: string, description: 优先查询的仓库可选值华东仓、华南仓、华北仓。不传则默认查询所有仓库, enum: [华东仓, 华南仓, 华北仓] } }, required: [sku_id] } }注意sku_id的 description 里给了具体的格式和示例warehouse用 enum 限制了取值范围这样模型即使在不完全理解业务的情况下也能按图索骥地填出合法参数。2.4 技能清单治理数量增长后的管理策略技能数量少的时候怎么都好办一旦超过 20 个你就得开始操心技能治理了。我见过某项目开发三个月后技能列表里躺了 60 多个技能名称五花八门有叫check_order的还有叫get_order_info_by_id的语义高度重叠模型经常选错。我的习惯是建立一套命名规范。技能名统一用小写字母 下划线动词开头例如query_user_order、create_return_request、calculate_shipping_fee避免出现形容词开头或名词开头的怪名字。同时每个技能在登记时要明确归属的业务域例如order.query、inventory.query、logistics.track这样一方面便于后端代码管理另一方面可以按域做技能子集的索引喂给模型前先做一次粗过滤。定期做技能审计也很重要。我有一个习惯每迭代一个版本就统计一次技能的使用率把从未被调用过的技能找出来要么删掉要么合并。技能清单不是越全越好每多一个技能就多一份模型混淆的风险。保持精简是技能系统长期健康运行的不二法门。3. 实操搭一套可用的 Agent Skills 框架3.1 技能注册与存储设计技能的实现和注册我推荐用装饰器 全局注册表的简洁方案。不同语言都有各自的装饰器或注解机制核心思想是一致的把技能的定义和实现放在同一个地方写完函数的同时就完成登记避免出现改了函数忘了改注册信息的问题。在 Python 里举一个简单的例子# skill_registry.py from dataclasses import dataclass from typing import Callable, Dict, Any dataclass class Skill: name: str description: str parameters: Dict[str, Any] handler: Callable # 全局技能注册表 SKILL_REGISTRY: Dict[str, Skill] {} def register_skill(name: str, description: str, parameters: Dict[str, Any]): def decorator(func: Callable): SKILL_REGISTRY[name] Skill( namename, descriptiondescription, parametersparameters, handlerfunc, ) print(f[skill] registered: {name}) return func return decorator然后业务代码里写一个技能就是定义一个函数再加一个装饰器# skills/inventory_skills.py from skill_registry import register_skill register_skill( namequery_inventory, description查询指定商品的实时库存数量。当用户询问某商品是否有货、剩余多少件、库存是否充足时调用此技能。如果用户询问物流配送时间或到货预测请勿调用本技能应使用物流查询技能。, parameters{ type: object, properties: { sku_id: { type: string, description: 商品唯一标识通常为8位字符如 AB123456 }, warehouse: { type: string, description: 优先查询的仓库可选值华东仓、华南仓、华北仓。不传则默认查询所有仓库, enum: [华东仓, 华南仓, 华北仓] } }, required: [sku_id] } ) def query_inventory(sku_id: str, warehouse: str all): # 这里是实际的库存查询逻辑对接库存服务网关 # 为了演示返回一个模拟结果 result { sku_id: sku_id, warehouse: warehouse, available_qty: 128, unit: 件 } return result这里有一个细节值得注意技能的实现函数要保证永远返回结构化 JSON而不是抛异常或者返回 None。因为大模型后续需要把技能的返回结果结合上下文生成自然语言回答如果函数返回了一个异常对象模型就很难组织出让人满意的回复。所以在实现层异常要做吞掉处理转成 JSON 里的error_code、error_msg字段。3.2 意图路由与调用链实现技能注册好之后核心机制就是意图路由让模型从技能注册表里选出最匹配的技能并填好参数。目前主流的做法是把技能目录拼接进提示词让模型输出结构化的调用指令然后由程序做解析和执行。我这里封装一个极简的路由函数方便你看清本质import json from typing import List, Dict, Any def route_to_skill(user_input: str, llm_func) - Dict[str, Any]: # 从全局注册表取出技能目录元数据 catalog [ { name: skill.name, description: skill.description, parameters: skill.parameters, } for skill in SKILL_REGISTRY.values() ] system_prompt ( 你是技能路由引擎。请根据用户请求从技能目录中选择最匹配的一个技能 严格按要求输出 JSON格式{skill_name: 技能名, arguments: {参数对象}}。 如果没有任何技能能处理用户的请求输出 {skill_name: NONE}。 ) user_prompt ( f技能目录如下\n{json.dumps(catalog, ensure_asciiFalse, indent2)}\n\n f用户请求{user_input} ) response llm_func(system_prompt, user_prompt) # 从 response 里解析出 JSON result extract_json(response) return result调用之后程序侧拿到skill_name和arguments再到注册表里找到对应的 handler动态执行def execute_skill(invocation: Dict[str, Any]) - Dict[str, Any]: skill_name invocation.get(skill_name) arguments invocation.get(arguments, {}) if skill_name NONE: return {result: 未找到匹配技能需要转人工或其他兜底处理。} skill SKILL_REGISTRY.get(skill_name) if skill is None: return {error: f技能 {skill_name} 不存在} # 动态调用 try: output skill.handler(**arguments) return {skill: skill_name, output: output} except TypeError as e: return {error: f技能参数错误: {e}}这段流程虽然简单但它确立了 Agent 系统的大脑 手脚分工模型负责决策和参数抽取程序负责可信执行。在生产环境里你还需要加上参数校验、超时控制、限流、日志追踪等环节但骨架就是上面这样的。3.3 执行结果回填与状态管理技能执行完之后结果不能就这样扔在一边。大模型要基于结果继续对话所以你需要把执行结果回填到模型的上下文中。现在的对话模型大多是无状态的每次请求都携带全部上下文所以把技能结果精准插入上下文成了关键步骤。我常用的做法是把技能执行结果作为一条tool角色的消息插入对话历史再让模型基于这段内容生成回答。示意如下# 在完成一次技能调用后把结果追加到 messages 里 messages.append({ role: tool, content: json.dumps(execution_output, ensure_asciiFalse), }) # 然后让模型总结生成最终回答 final_answer llm_func(messagesmessages)如果你做的 Agent 是长任务型的比如执行一个多步骤流程需要跨步骤共享中间状态。比如用户先问我想买一件冲锋衣你查了库存、比较了价格、算了运费最后用户说下单这中间的状态如果没有记住你就得重新查一遍。解决办法是把关键状态写进一个临时存储比如 Redis或者用上下文压缩技术把重要信息提炼后再塞回模型上下文。不要指望模型自己记住什么老实把状态落在外部存储里最可靠。3.4 完整 Demo让 Agent 学会查库存、算价格这里我给你看一个完整的迷你版实现。技能端我注册了两个技能query_inventory查库存calculate_total_price根据商品数量计算总价含运费规则。用户输入你好请问冲锋衣还有货吗我有三件的话一共要多少钱模型的意图路由会先调query_inventory查库存得到库存充足的结果后再调calculate_total_price算三件的总价。# demo.py from skill_registry import register_skill, SKILL_REGISTRY from router import route_to_skill, execute_skill # 技能一查询库存 register_skill( namequery_inventory, description查询指定商品的实时库存数量。当用户询问某商品是否有货、剩余多少件时调用此技能。, parameters{ type: object, properties: { sku_id: {type: string, description: 商品唯一标识如 JACKET-001}, warehouse: {type: string, description: 仓库名可选华东仓、华南仓, enum: [华东仓, 华南仓]} }, required: [sku_id] } ) def query_inventory(sku_id: str, warehouse: str 华东仓): # 模拟库存服务 stocks {JACKET-001: {华东仓: 20, 华南仓: 5}, PANTS-002: {华东仓: 0, 华南仓: 12}} qty stocks.get(sku_id, {}).get(warehouse, 0) return {sku_id: sku_id, warehouse: warehouse, available_qty: qty} # 技能二计算总价 register_skill( namecalculate_total_price, description根据商品单价和购买数量计算总价购买超过2件时享受95折优惠运费按10元每单收取。当用户询问价格、金额、费用时调用。, parameters{ type: object, properties: { unit_price: {type: number, description: 商品单价单位元}, quantity: {type: integer, description: 购买数量} }, required: [unit_price, quantity] } ) def calculate_total_price(unit_price: float, quantity: int): discount 0.95 if quantity 2 else 1.0 total round(unit_price * quantity * discount 10, 2) return {unit_price: unit_price, quantity: quantity, total_price: total} # 用户输入 user_msg 你好请问 JACKET-001 这个冲锋衣有货吗我买3件要多少钱 # 路由并执行第一轮 first route_to_skill(user_msg, llm_funccall_llm) output1 execute_skill(first) print(第一轮调用结果, output1) # 把结果回填后模型可能会继续发起第二次技能调用在实际实现中需要循环判断是否还有待调用的技能 second_invocation {skill_name: calculate_total_price, arguments: {unit_price: 299.0, quantity: 3}} output2 execute_skill(second_invocation) print(第二轮调用结果, output2)在这个 demo 里你能看到 Agent Skills 的精髓模型不需要提前知道库存服务用什么接口、单价存在哪个字段它只需要根据描述选择技能、填参数剩下的细节被完全封装在技能的 handler 里。从理解意图到结果返回的全链路结构化数据贯穿始终非常适合接入复杂的业务系统。4. 进阶技能编排与多技能协同4.1 串行编排把大任务拆成技能流水线单个技能的能力再强也有限真实业务里一次用户请求往往要串起多个技能。比如用户说帮我看看附近有没有能洗羽绒服的干洗店顺便预约明天下午的位置这个请求就要先调地点搜索技能再调门店详情技能最后调预约技能三个技能依次执行前者的输出作为后者的输入条件。串行编排最简单可靠的实现方式是维护一个待执行技能队列模型或编排器依次决定下一个要执行的技能直到完成全部步骤。伪代码如下def run_pipeline(user_input, skills_queue): context {original_input: user_input} while skills_queue: current skills_queue.pop(0) invocation route_to_skill(current[prompt], current[skill_options]) output execute_skill(invocation) # 把输出写进共享 context供后续技能或最终回答使用 context[invocation[skill_name]] output # 判断是否需要追加新技能例如根据输出决定下一步分支 if output.get(next_step): skills_queue.append(output[next_step]) return context这里的关键是不要在提示词里把整个任务的每一步都写死而是让模型边做边看根据上一步的结果决定下一步。这样做的好处是灵活性高能应对输出不确定的复杂场景。缺点是增加了不可控性所以我在实际项目里通常会给这种流水线加一个最大步数限制比如最多执行 6 个技能防止模型在技能之间绕圈圈。4.2 并行与条件分支决策型任务的编排有些场景下技能之间不存在先后依赖则可以并行执行来缩短总时长。比如用户咨询如果我再买一条裤子总价和库存分别是多少你就完全可以同时调库存技能和价格计算技能拿到结果后合并输出。并行实现的要点是做好结果聚合。每个技能返回一个结构化的结果块用技能名做 key 存进聚合池等所有并行任务都返回了再统一交给模型生成最终回答。注意并行任务不要共享可变状态否则容易出现竞态问题。条件分支则更贴近决策树的感觉。我封装过一个简单的规则引擎根据技能输出里的某个字段值决定下一个动作。比如query_inventory返回的available_qty如果为 0就转去调用query_similar_product推荐相似商品如果大于 0就直接回答有货并询问是否下单。这种分支逻辑虽然也可以用模型来判断但对于确定性比较强的规则写在代码里更稳妥既省了 token 又避免了模型抽风。4.3 编排器Orchestrator的定位与取舍技能数量一大你就会开始想要不要引入一个专门的编排器来自动规划技能的执行顺序。我在项目早期也动过这个心思但后来发现编排器是把双刃剑。好处很明确编排器能让 Agent 在初始时就是全知的它能根据用户目标自动制定一个技能调用计划而不是走一步看一步。这在任务步骤多、技能依赖强的场景中很管用。坏处也很明显编排逻辑本身也是一段需要维护的代码一旦编排规则覆盖不全或者模型规划出错误的技能顺序调试起来非常痛苦。我的建议是先不要急着写一个庞大的编排器。小规模项目里直接在对话循环中加一个下一步决策的提示词让模型每轮判断是否需要继续调用技能就足够用了。等技能数量超过 30 个、任务链条明显变长时再引入图编排或状态机也不迟。记住一个原则能用代码写死的分支就不要让模型自由发挥模型的作用是理解意图而不是变成分布式系统的调度中心。5. 常见问题与排查技巧实录5.1 技能调用错误排查速查表在 Agent 系统的日常运行里出问题是常态不出问题才是意外。我把高频问题整理成了一张速查表每一条都是真实踩坑换来的经验症状表现可能原因排查方法解决方案模型反复调用同一个技能不进入下一步技能描述里没有写清结束条件和边界查看该技能的完整调用日志看模型是否在重复填相同的参数补全描述中的何时不要调用本技能部分或增加技能内部的状态判断技能参数总是填错比如把日期填成字符串JSON Schema 的类型约束太松检查参数的 type 和 format 定义用严格枚举或正则 pattern 约束参数并在 description 里给示例技能返回了结果但模型的回答文不对题结果回填时上下文位置不对或结果里冗余信息太多检查 messages 中 tool 角色消息的位置和内容精简返回结果只保留模型真正需要的信息并添加一句简要说明技能执行超时界面一直转圈技能 handler 里没有做超时控制看日志确认卡在哪个环节网络IO数据库给技能调用包一层 async timeout超时后返回降级结果同一技能名在代码里重复注册装饰器扫描路径重复或被加载了两次打印技能注册日志检查模块导入路径统一模块加载入口注册前做幂等校验模型随机性导致同一问题有时调A技能有时调B技能两个技能描述语义重叠把两个技能的描述拿来做对比分析合并技能或在描述中互相补充排除条件5.2 高频坑位复盘这些都是我真实踩过的第一个坑技能描述里只写了什么时候调用没写什么时候不要调用。有一次我在客服场景里用户说我想取消订单模型居然调用了查询订单物流轨迹技能因为它识别到了订单这个关键词。后来我在每个技能描述后面统一补了一句如果用户意图是X请勿调用本技能应使用Y技能误调率立刻降了一截。第二个坑技能返回的数据太生。有些技能直接返回数据库原始字段比如create_time、modify_time这种模型面对这些字段经常不知所措回答时只能生硬复述。改造办法是在技能内部做一次面向用户表达的数据加工把所有字段换成自然语言可读的描述比如is_active直接转换成状态在售。技能返回的内容越贴近人话模型的表达就越自然。第三个坑把错误信息直接抛给模型。技能调远端接口经常超时如果你不做异常吞噬把ConnectionTimeout这种堆栈信息塞回上下文模型会当场蒙圈答出系统内部错误请稍后再试这种没营养的话。我在技能层统一做了一个safe_call包装器出错时返回结构化的{error_code: TIMEOUT, error_msg: 库存服务暂时不可用请稍后重试}模型看到后能很自然地帮用户解释并给出替代方案。第四个坑盲目追求技能数量。早期我总觉得技能越多越好结果技能一多模型的选择准确率就下降。后来我做了一次大清理合并了相近技能删掉了低使用率的技能整体调用成功率反而提升了。技术系统不是堆砌得越多越好克制是美德这句话放在 Agent Skills 的设计上同样成立。写在最后在我做过的几个 Agent 相关项目里最深的体会是Agent Skills 不是一个单纯的工程技巧而是一种把不确定性关进笼子的设计哲学。大模型天生是不确定的它理解语言的方式是概率性的但技能层通过标准的元数据、严格的参数 Schema、可靠的结果回填把模型输出之外的所有环节都变得确定和可验证。模型负责聪明系统负责可靠两者各司其职Agent 才能真正从玩票走向实用。最后再分享一个小技巧每个新技能上线前我都会准备一组固定的测试用例比如以不同说法表达同一诉求故意说一个模糊请求输入一个明显超出技能范围的请求用这组用例跑一遍全链路测试观察调用准确率和参数合法率。别等着用户去发现了Agent 系统的返工成本远比你想象的高提前把功课做足后面能省下大量排查时间。如果你也在做 Agent 应用希望这套经验和代码骨架能帮你少走一些弯路。