
1. Skills不是“技能点”而是Agent的能力单元格先聊个常见的误解。很多人一听到“Skills”第一反应是游戏里那种“技能树”或者简历上写的“我会Python我会SQL”。但在AI Agent这个语境里Skills完全不是这么回事。它既不是抽象的能力描述也不是一段可以随意粘贴复用的提示词而是一套可以被加载、被解析、被校验、被执行的完整功能单元。我做Agent开发这几年最大的感触是大模型本身的能力上限其实拉不开绝对差距真正拉开差距的是你能给它配上多少高质量、可复用、边界清晰的Skills。举个场景你就明白了——同样用GPT-4级别的模型A团队做出来的客服Agent只会“根据知识库回答问题”B团队做出来的客服Agent却能主动调用订单查询技能、售后规则判断技能、情绪安抚话术技能甚至在用户反复追问时自动通过技能包里的“升级人工”流程。差距不是模型给的是技能包给的。所以这篇文章我打算把“skills”这个东西从工程视角完整拆一遍它到底是什么、和Tools/MCP有什么区别、怎么从零设计一套可维护的技能包、运行时是怎么加载执行的、以及我实际落地时踩过的坑。这篇文章没有太多理论全是能直接拿去用的方案和判断标准。先说一个总纲性的定义Skills是为Agent预设的“能力单元格”每个Skill包含一段清晰的职责描述、一份可执行的代码或指令、以及一份可供模型和系统共同理解的元数据。它和普通函数最大的区别在于函数的调用者是程序员而Skills的调用者是大模型——这意味着你写给模型看的描述和写给系统看的代码必须分开设计缺一不可。如果你正在做Agent应用或者打算给自己的AI工作流加“外挂能力”这篇文章应该能省你几周的试错时间。2. 从Tools到Skills为什么这个抽象层级如此关键2.1 Function Calling时代的痛在Skills这个概念成型之前大家普遍用的是Function Calling。你定义一批函数把函数的名字和描述塞给模型模型决定调哪个。这个模式在单一模型、单一场景下没问题但场景一多就崩了。我把当时的痛点列一下函数列表越长模型的选择准确率越低。实测超过20个函数之后误调用率开始明显上升。函数描述和业务逻辑强耦合。改一个业务规则可能得重新设计函数签名。没有“启用/停用”的概念。所有函数对模型都是全量可见的无法根据当前会话上下文动态筛选。复用性差。换一个Agent就得重新配一批函数函数与函数之间没有任何组织关系。在真实项目里最要命的还不是这些而是上下文长度的压力。函数定义要占用token一堆函数塞进系统提示词里还没开始干活就先烧掉几千token。场景越复杂这个问题越尖锐。2.2 Skills这一层解决了什么Skills的核心理念是让Agent“按需取用能力”而不是“全量背负能力”。它把能力封装成包每个包有独立的元数据——包括技能名称、用途描述、适用场景、输入参数定义、输出格式、依赖条件、版本号等等。系统层面可以随时发现、加载、卸载某个技能包。模型层面只需要在推理时看到当前实际可用的、与任务相关的几个技能而不是几百个函数的堆砌。这里有一个很关键的区别要讲清楚维度Tools/FunctionSkills调用者视角程序员预先接入模型动态选择组织方式散装函数列表分层、可组合的包结构运行时暴露全部暴露按需加载可复用性绑定单一Agent跨Agent、跨场景共享描述成本简单一句完整的元数据指令版本管理很少考虑必须考虑当然这并不是说Skills要取代Tools。实际工程中两者是配合关系Tools是底层执行单元Skills是上层组织单元。一个Skill内部可能依赖多个Function也可能只是一段精心设计的指令流程。2.3 Skills与MCP、Plugin的关系最近MCPModel Context Protocol也特别火很多人跑来问MCP是不是就是Skills不是。MCP解决的是“Agent如何连接到外部工具和数据源”的协议问题它定义了客户端、服务端、工具发现、资源读取这些标准接口。而Skills解决的是“Agent应该具备哪些能力、这些能力如何组织、如何被选择”的问题。你可以这样理解MCP像USB接口它让设备能插上电脑Skills像一个个应用程序它们决定电脑能用来做什么。USB接口是通用的但应用程序才是用户真正感知到的能力。Plugin插件更偏产品层概念强调“可安装、可扩展”。Skills则更偏运行时概念强调“可加载、可执行”。在很多Agent框架里Plugins的目录结构就是挂着多个Skills的一个是壳一个是核。3. 从零搭建一个技能包以“订单状态查询”为例3.1 定义技能包的核心元数据我的习惯是建一个标准化的目录结构每个技能包都是独立文件夹包含描述文件、入口脚本、辅助资源。下面是我常用的结构order_status/ ├── skill.json # 元数据定义 ├── instructions.md # 给模型的指令说明 ├── main.py # 执行入口 └── utils/ # 辅助模块 └── query_helper.pyskill.json是整个技能包的门面模型和系统都会先读它。我贴一份实际用过的配置字段都是反复打磨过的{ name: order_status_query, version: 1.2.0, description: 查询订单的实时状态支持订单列表、单笔明细、物流轨迹适用于用户咨询订单进度、催发货、查签收等场景。, author: agent-core-team, tags: [ecommerce, order, logistics], enabled: true, min_agent_version: 0.8.0, input_schema: { type: object, properties: { order_id: {type: string, description: 订单号必填}, detail_level: {type: string, enum: [simple, full], default: simple} }, required: [order_id] }, output_schema: { type: object, properties: { status: {type: string}, timeline: {type: array}, logistics_company: {type: string} } }, dependencies: [internal_order_api, logger] }有几个字段要特别注意description是给模型看的第一份“简历”。写得好不好直接决定模型在什么场景下会选择这个技能。我的经验是描述里必须包含触发条件和典型问法。比如“适用于用户咨询订单进度、催发货、查签收”——这就是给模型的触发信号。input_schema是给系统做校验用的。模型产出的参数必须先过JSON Schema校验不合格直接拒绝不能往下执行。dependencies声明运行时依赖用于在加载阶段做依赖检查缺了提前报错而不是执行到一半才炸。3.2 编写Instructions这是技能的“大脑”很多人写技能包只写代码不写instructions.md这是本末倒置。技能包的执行逻辑里代码是四肢instructions才是大脑——因为模型需要靠它来理解“什么情况下用”“怎么用”“用的时候要特别注意什么”。我写instructions的经验如下# 订单状态查询技能 ## 技能职责 当用户咨询订单状态、物流进度、签收情况时使用本技能。 ## 执行步骤 1. 从用户消息中提取订单号如果缺失先引导用户提供。 2. 调用main.py中的query函数传入order_id。 3. 如果返回结果中status为picked_up已揽收将物流轨迹按时间正序展示。 4. 如果结果中status为exception异常直接转异常处理流程不要向用户展示原始错误码。 ## 关键规则 - 订单号格式必须是OD开头的13位字符不符合直接提示用户检查。 - 用户同时查询多个订单时逐个调用不要拼接参数。 - 物流轨迹超过3条时默认展示最近3条并提示用户“如需查看更多请告知”。 - 禁止将接口返回的原始JSON直接抛给用户。 ## 不要做的事 - 不要猜测订单号。 - 不要在没有物流信息的情况下承诺“明天送达”。 - 不要调用其他技能查询优惠券或会员积分。这份文档看起来简单但它完成了一个非常重要的任务把业务规则从代码里抽离出来放到模型能“看到”的地方。因为L3模型的推理依赖的是文本理解你写在代码注释里的规则它看不着但你写在instructions里的规则它每一条都能遵守。规则少了模型就放飞自我规则太生硬模型又会死板。这里面的度需要反复调优。3.3 执行入口的编写规范main.py其实不复杂逻辑上就三层校验入参、调业务接口、标准化输出。为了防止模型调用时的各种“意外”我建议入口做三层防御import json import logging from utils.query_helper import query_order_info logger logging.getLogger(__name__) def execute(input_data: dict, context: dict None) - dict: # 第一层入参校验 order_id input_data.get(order_id, ).strip() if not order_id: return { success: False, error_code: PARAM_MISSING, message: 缺少订单号 } if not order_id.startswith(OD) or len(order_id) ! 13: return { success: False, error_code: PARAM_INVALID, message: 订单号格式不正确 } # 第二层业务执行异常兜底 try: detail_level input_data.get(detail_level, simple) result query_order_info(order_id) if result is None: return { success: False, error_code: NOT_FOUND, message: 订单不存在 } return {success: True, data: format_result(result, detail_level)} except Exception as e: logger.error(order_status_query failed: %s, e, exc_infoTrue) return { success: False, error_code: SERVICE_ERROR, message: 暂时无法查询请稍后再试 }3.4 测试和验收技能包不是写完就能上线技能包的测试和普通函数测试不一样普通函数只要测输入输出对得上就行技能包除了测函数逻辑还得测“模型能不能正确理解它”。我的做法是准备一套“场景问法集”比如直接命中“帮我查一下OD20240101001这个订单到哪了”间接命中“我买的东西怎么还没发货”边界情况“我那两个订单什么时候到”负样本“帮我推荐一款手机”前三类是验证描述完整度第四类是防止误触发。每个技能包上线前我会跑一遍这组测试如果负样本触发了技能说明description写得太宽泛要收紧。注意技能包不是“越大越好”。你描述里覆盖的场景越多模型越容易在无关场景误选它。宁可拆成两个技能也不要把所有功能揉进一个包。4. 技能包的运行时机制加载、选择与执行4.1 技能包管理器让你的Agent学会“按需取用”好的Agent架构一定有一个技能包管理器Skill Manager它负责三件事加载已注册的技能包、根据当前任务筛选可用技能、把筛选结果注入到模型上下文。我习惯用注册制而不是扫描制每个技能包在系统初始化时注册到管理器管理器维护一张“技能清单”并依据技能包的enabled状态、版本兼容性、依赖间关系决定是否加载到运行时。扫描制听起来省事但一旦技能包数量超过几十个文件扫描和解析开销不可忽视而且不好处理依赖冲突。技能包管理器的核心接口其实并不多class SkillManager: def __init__(self): self._skills {} self._enabled_skills set() def register(self, skill: dict): # 注册技能包读取skill.json做基础校验 ... def enable(self, skill_name: str): # 启用技能包 ... def disable(self, skill_name: str): # 停用技能包让模型不可见 ... def get_skills_for_task(self, task_description: str, context: dict): # 根据任务描述和上下文筛选可用技能 ...筛选逻辑是关键。我常用的策略是三通道组合语义匹配把任务描述和技能包的description做向量相似度匹配。这个通道保证“模型说想查订单系统就能把订单技能捞出来”。规则标签技能包tags和当前会话的领域标签做交集筛选。比如进入“售后会话”则将售后相关技能放在更靠前的位置。上下文限域有些技能只在特定上下文下有意义比如仅当用户是会员才展示会员权益技能这类判断由技能包内的activation_schema声明管理器在执行前二次过滤。4.2 注入策略如何控制上下文预算选好了技能接下来就是注入。这里有个细节所有技能包全量注入是大忌。每个技能包包括description、instructions、参数Schema、示例加在一起动辄一两千token。如果你有50个技能包全量注入的token消耗会占到上下文的可观比例而且会严重分散模型注意力。我的做法是分三层预算注册层只把技能名称一句话描述放进系统提示词平均每个技能不超过100token。候选层通过语义规则筛选出当前任务最相关的3~5个技能将其完整instructions注入。执行层模型决定调用某个技能时再加载该技能包的参数Schema与示例。这三层分别对应“模型知道有什么”、“模型知道怎么用”、“系统知道怎么执行”。经过这样分层就算技能包数量过百模型上下文中暴露的技能信息也可以控制在合理范围内。配一张我实际的token占用对比供参考单次会话平均值方案技能包数量平均token占用模型选对技能准确率全量注入50约650082%注册层候选层注入50约180095%单技能执行加载50约90097%4.3 错误修正机制技能包执行失败并不等于会话结束技能包执行过程不可能永远顺利。我曾经统计过线上数据技能包第一次执行失败的比例大概在8%~12%。如果失败就直接结束那体验就太差了。一个好的运行时要支持“自愈循环”。我的设计是给每次执行定义一个“修正循环”MAX_RETRIES 3 def execute_with_retry(skill, input_data, context): for attempt in range(MAX_RETRIES): result skill.execute(input_data, context) if result[success]: return result error_code result.get(error_code, ) # 如果错误来自参数问题把错误信息反馈给模型让它重新生成参数 if error_code in (PARAM_MISSING, PARAM_INVALID): correction_prompt ( f技能调用参数不合法{result.get(message)}。 f请检查用户消息并重新提供正确的参数。 ) input_data model.generate_parameters(correction_prompt, skill.input_schema) continue # 如果是API或服务错误等待后重试一次 if error_code SERVICE_ERROR and attempt MAX_RETRIES - 1: time.sleep(2 ** attempt) continue # 其他错误直接终止 return result return {success: False, error_code: MAX_RETRIES, message: 多次尝试后仍失败}特别强调一点参数类错误不要重试业务逻辑而是要让模型重新理解用户意图。很多时候模型第一次提取订单号失败是因为用户消息里有干扰信息让模型“重新看一遍用户消息”往往能成功。5. 从单一技能到技能体系编排、路由与协同5.1 技能不是孤岛一个任务经常需要多种技能配合真实业务里很少有“一个技能包干完所有事”的用例。比如用户问“我那个手机还在路上吗如果明天不到我就退款了”——这句话至少涉及订单查询、退货规则判断、客服话术生成三个技能。这时候就需要技能编排Skill Orchestration。我的做法是把编排逻辑写成“流程技能”Flow Skill它自己不执行具体业务而是定义一组子技能的调用顺序和判断规则。Flow Skill的implementation是一份流程图式的规则但不要写成复杂状态机——模型天然适合理解自然语言写成的步骤而不是死板的状态转移表。我写的一个退款流程示例# 退款流程编排技能 ## 触发条件 当用户发起退款申请、投诉到货慢要求退款、或订单展示异常且用户表达退款意向时。 ## 执行流程 1. 调用order_status_query查询订单状态。 2. 如果订单状态为已签收跳转售后流程。 3. 如果订单状态为运输中结合物流轨迹判断已运输时长。 - 超过5天提示用户可申请平台介入并调用return_policy_query查询政策。 - 未超过5天安抚用户提供物流加速查询结果。 4. 调用customer_service_tone生成回复话术语气需与用户情绪状态匹配。5.2 多Agent共享技能库一套技能包多处复用技能包机制最值钱的特性之一是可共享。在我的架构里技能包是独立于Agent之外的资产——同一套技能库可以被售前Agent、售后Agent、物流客服Agent同时使用。差异只在于每个Agent启用的技能子集不同。这带来的好处是极其明显的业务规则只改一处所有Agent同步生效不用再逐个Agent改提示词。技能包的测试可以集中做不用为每个Agent单独回归。新Agent上线时只需要“装配”已有技能不用从零开发。当然共享也带来一个问题技能包多了之后会逐渐腐烂。描述和实际行为不一致、参数接口变动后没人更新文档、某些技能被废弃但忘记下线。我的建议是建立技能包的定期审计机制比如每月跑一遍“场景问法集”回归测试凡是准确率下滑的技能一律标记为待优化连续两次审计不过就下线。5.3 小样本下的启发式技巧技能不在多在于“会被用到”最后说一个反直觉的观察。很多人一开始喜欢拼命加技能觉得技能越多Agent越强。但实测下来技能包数量和Agent任务准确率并不是单调正相关。原因在于可选项多的时候模型的“吃不透”概率会上升。对我自己线上的数据来说技能包总数保持在20~30个左右且每个Agent活跃技能控制在5~8个整体效果是最好的。技能体系的构建不该以“拼数量”为目标而该以“每个技能都被高频率正确使用”为目标。如果一个技能一周都没被选中一次就值得怀疑是这个技能没用还是它的描述写得让模型根本认不出来6. 落地排查技能包开发中常见的五个深层问题6.1 技能被“遗忘”了——描述与意图的错位如果你的技能包已经写好、注册时模型也能看到但实际对话中它就是不被调用大概率是description写得“只有程序员看得懂模型看不懂”。比如你写“查询订单状态”模型不一定能把这个描述和“我买的手机怎么还没到”关联起来。解决方式很粗暴在description里直接枚举用户的典型问法。改造前description: 查询订单状态的技能改造后description: 查询订单状态包括发货、运输、签收、异常等场景。适用于用户咨询‘订单到哪了’、‘什么时候发货’、‘物流怎么不更新’、‘货物卡住了’等表述。模型是靠语义联想工作的你需要给它足够多的“钩子”让它触发技能。6.2 幻觉式参数生成——模型的自由发挥模型生成参数时容易出现“自作主张”用户没说订单号它编一个OD2023010100012用户说“看一下我的订单”它就默认取第一个订单。要防这个问题在instructions里明确规则“订单号必须从用户消息中直接提取用户未提供时先追问禁止编造”。在技能入口加“置信度判断”如果某个字段是猜测补全的要求模型在参数里附上source: inferred系统据此决定是否放行。6.3 技能包之间的互相抢占当你有“订单查询”和“物流轨迹查询”两个技能时模型经常不知道该选哪个。这种场景需要引入优先级和互斥规则。我的做法是在技能包元数据里增加conflict_rules声明。conflict_rules: { exclusive_with: [order_list_query], priority_over: [logistics_track_query] }管理器在注入上下文时看到互斥规则就只保留一个技能给模型人工消解选择难题。6.4 上下文污染——技能instructions反噬有些技能包的instructions写得过于详细甚至包含内部API端点、数据库字段名这样的实现细节。模型看到这些信息后有时会“幻想”自己可以直接操作数据库说出“我已经帮你拦截了订单”这种完全没有事实依据的话。解决思路给模型看到的instructions要和给系统执行的代码分离。模型只见业务化规则不见技术实现。6.5 缓存与幂等技能包如果涉及查询数据库或调用第三方API尽量加上响应缓存和幂等控制。我采取的方法查询类接口缓存5~30秒显著降低重复查询压力。写操作强校验request_id防止模型重试时重复创建工单。技能执行结果统一记录日志包括入参、出参、耗时、错误码方便复盘模型每一次调用行为。这份日志是后续优化技能包描述最重要的依据——你会发现模型为什么误用餐券技能、为什么漏看了用户地址、为什么在退货流程里执着地推荐新订单。一切优化都要靠数据说话。7. 性能优化三件事延时、缓存、并发线上跑了一段时间我总结出三个最值得优化的点。技能发现延迟技能包数量大时语义匹配会慢。解决办法是给技能描述做预向量化和缓存相似度计算走向量数据库或内存索引而不是每次实时算。技能执行并发同一个技能可能在同一时刻被多个Agent调用。给关键技能做限流和队列避免打爆下游接口。我用的是简单的信号量加超时熔断。技能反馈闭环每次技能执行完把结果成功/失败/耗时/模型是否二次调用写回技能元数据。时间久了就能看到哪些技能经常“一次成功”哪些技能总让模型反复改参数。信号量限流的实现不复杂Python标准库就能搞定import threading class SkillRateLimiter: def __init__(self, max_concurrent: int 5): self._sem threading.Semaphore(max_concurrent) def acquire(self, timeout: float 3.0) - bool: return self._sem.acquire(timeouttimeout) def release(self): self._sem.release()调用时注意一点网络IO密集型的技能并发数可以放大数据库写操作密集的技能并发数要收紧。没有统一答案拿线上数据调。8. 技能包之外让Agent具备“自我进化”的能力技能包模式天然适配一个更高阶的玩法让Agent自己提出“我需要新技能”。当模型在会话中频繁遇到相同类型的请求但手头没有对应技能时系统可以自动记录“技能缺口”。每周整理这些缺口你会发现自己对用户真实需求的理解远超拍脑袋的脑暴。我现在的工作流里有一个“技能缺口汇总表”字段包括用户原话触发会话当时的可用技能列表模型是否有过“尝试但失败”或“直接拒绝”的表现这张表每周汇总一次产出的新技能往往比产品经理提的需求更贴近用户真实意图。日常开发顺序也随之改变不再从“我觉得用户需要什么”出发而是从“实际对话数据暴露了什么缺口”出发。