
最近一两年只要聊到AI应用落地“agent-skills”这个词的出现频率越来越高。我第一次认真研究它是因为自己做一个RPA类助手需要让大模型去查订单、发邮件、改表格结果发现工具函数写了一堆Agent还是经常干错事。后来才明白问题不在模型推理能力而在“技能”这一层设计得太粗糙。所谓agent-skills就是给智能体构建一套可注册、可调度、可管理的技能库让大模型在正确的时候调用正确的能力。它能解决的问题很直接让Agent从“聊天机器人”变成“真能干活的人”。如果你正在做Agent应用或者准备从单轮对话往复杂任务方向走这篇文章里讲的设计思路和踩坑经验应该能帮你少走不少弯路。1. agent-skills是什么先搞清楚我们在聊什么1.1 从一次Agent开发经历说起我在去年年中接手了一个项目核心诉求是让AI助手帮运营同学处理日常重复工作查订单状态、整理报表、发群通知。第一版我天真地把所有Python函数塞给模型模型每轮对话都能看到十几个函数定义结果就是让它查订单它去调了发送邮件让它生成报表它把参数写成了一串乱码。当时我一直在调提示词效果时好时坏。坏的时候模型会用“思考”代替“调用”好的时候又诊断不出来它到底调了哪个函数。后来和一个朋友复盘他提了一句“你就是缺一层技能管理。”这句话点醒了我——函数只是技能的载体技能本身应该是一个包含描述、参数协议、执行策略、权限控制的完整单元。从那以后我重新梳理了项目里的所有工具调用把它们统一封装成“技能库”一切开始变得可控。1.2 技能、工具与插件的边界很多资料把工具调用叫function calling把插件叫做plugin现在又流行说skills。它们到底什么关系我的理解是工具是最底层的能力单元一个函数、一个API能完成一个具体操作插件是工具的集合通常面向某个产品形态打包而技能是更高一层的概念它描述的是“在什么场景下、用什么参数、按什么流程完成一个完整任务”。举个例子。你会写一个get_weather函数这是工具。你把查天气、查空气质量、查风向打包成“天气插件”。而“根据用户目的地和出行时间规划穿衣搭配并生成提醒”这就是一个技能——它内部可以调用多个工具有前置条件、决策分支、默认参数和兜底策略。agent-skills真正要设计的是这一层让模型不需要关心底层函数怎么写的只需要知道有这个技能存在以及该怎么触发它。1.3 为什么技能层值得独立设计很多人觉得Agent本质就是循环调用模型多封装一层技能无非是多了个装饰器。但实际做下去你会发现没有独立技能层的话会有几个特别头疼的问题。第一模型每轮对话看到的schema会越来越大。工具一旦超过二十个Context里的函数定义占用几千token光靠描述让模型选对就已经很吃力了。第二权限边界很难控制。RPA场景里有些操作是不可逆的比如批量删除、覆盖文件如果这些函数直接暴露给模型等于给了模型一把万能钥匙。第三回归测试没法做。技能层独立出来以后你可以针对每个技能写用例校验参数解析、边界条件、错误返回如果所有逻辑都散落在Agent代码里测试成本会成倍增加。所以在我的团队里技能层已经是Agent项目的标配甚至比模型选型更早确定下来。2. 技能库设计的五个关键点做对了Agent才不翻车2.1 技能描述LLM靠什么找到你的技能先说最容易忽略但影响最大的部分技能描述。大模型做工具调用本质上是一个“匹配”过程。它把你的技能列表和用户意图做语义匹配然后选出最合适的一个。既然是这样描述写得好不好直接决定召回率。我踩过的坑就是把描述写成函数注释。比如“获取天气信息”这种描述模型基本无视。正确的写法应该包含三块技能的能力边界、典型的触发场景、不建议使用的场景。这里放一组对比表是我自己项目里整理的经验描述写法示例实际召回效果只说能力获取天气信息用户问“明天去杭州适合穿什么衣服”模型经常不调用自己编能力触发场景根据城市名称查询未来三天天气包括温度、降水概率和风力适合出行规划、穿衣建议、活动安排时使用能正确触发但偶尔会误用能力触发场景反例根据城市名称查询未来三天天气状况适合出行、穿衣、活动安排不适合查询历史天气不提供空气质量召回率和准确率最稳定描述里加反例表面看是啰嗦实际上对模型帮助极大。LLM在做工具选择时语境里的正向信号和负向信号都会影响决策。你明确说“不要做什么”模型就少一种选错的路径。2.2 参数协议JSON Schema是唯一的契约技能能不能稳定执行参数协议比严格类型检查更关键。目前主流做法是给每个技能定义JSON Schema模型按照Schema生成调用参数。既然它是契约就必须把话说死。先说参数描述。每个字段的description要写清楚业务含义和边界。比如city字段我会写“城市名称中文如北京、上海如果用户给出的不是标准城市名请先转换成标准名称再调用”。再比如时间字段我会写“格式YYYY-MM-DD只接受北京时间不接受相对时间表达式”。再说类型和约束。能用enum约束的就用enum能用minimum、maximum约束的绝不放过。模型生成参数的时候偶尔会“幻觉”你定义integer它填字符串这种问题靠提示词挡不住必须靠Schema约束加上运行时校验双保险。参数级别的另一个设计是默认值。给关键参数设定合理的默认值可以让技能适配更多模糊场景。比如查天气的unit我默认摄氏温标模型没传我就补一个避免后续流程被None值卡住。2.3 返回值设计给LLM喂“干净的数据”技能执行完返回什么、怎么返回是agent-skills里最容易被低估的一环。模型拿到返回值后需要把它转化成对用户的回答如果你返回一堆嵌套很深、没有语义标记的JSON模型很容易在总结时出错。我的经验是统一返回结构。所有技能返回一个字典至少包含三个键status、data、error。status表示执行成功还是失败data里放结构化结果error放错误信息。并且每次返回的数据量要克制。你查一个订单列表拉回一百条明细每条八个字段整个结果几百行塞给模型。模型不是处理不了是它处理的时候会丢掉关键字段而且费token。现实中我会在技能内部做精简映射只保留后续决策需要的字段把长文本字段截断把列表限制条数必要时直接给模型返回摘要而不是完整数据。2.4 权限与确认哪些技能不能直接自动执行这个必须单独拿出来说。Agent出现严重事故基本都是权限失控。我在设计技能库时加了一个字段叫auto_exec只允许两类技能自动执行只读类操作比如查询天气、查订单状态低风险操作比如给日程表加一条无冲突的记录。凡是涉及写操作和外部影响的自动执行必须关掉。比如发送邮件、删除文件、修改线上配置。这些技能在执行之前Agent要停下来把准备调用的参数展示给用户得到明确同意后才继续。有些场景我会把这类技能拆成两步第一步预执行生成操作预览不落库不发送第二步确认执行用户点了同意才真正落地。实际上这个设计并不是在限制Agent的能力反而是在保护它。因为一旦出错用户只会记住是Agent捅的娄子不会关心是哪一行代码触发的问题。2.5 版本与命名技能多了以后的管理问题当技能数量超过五十个命名冲突就成了真问题。我建议技能名采用“域_动词_对象”的格式比如order_query_detail、schedule_add_event、mail_send_single。这样的好处是既能从名字一眼看出归属域又能避免不同模块之间重名。版本管理同样容易忽略。技能改了内部实现Schema没动你是静默上线还是通知调用方我现在的做法是技能注册表里带version字段对外暴露的只保留最新版本但历史版本在每个技能对象里留档。一旦线上调度出问题我可以快速回滚到上一个版本而不是让Agent带病运行。3. 动手实现从零搭一个可用的技能库3.1 技术选型PythonJSON Schema不整花活很多朋友问我要不要上重量级框架或者一上来就上Kubernetes。我的看法是在技能数量还没到几百个之前一个Python模块加一张注册表就够用了。复杂框架带来的抽象成本在小规模阶段远大于收益。我选择用Python实现理由很简单AI生态的SDK基本都先支持Python我的业务代码做函数封装也最方便。Schema规范用标准的JSON Schema因为OpenAI、Anthropic甚至一些开源模型都原生支持这套格式。把模型无关的协议固定下来以后换模型供应商不需要改技能库。3.2 核心代码注册器、Schema导出与调用分发下面这段代码是我常用的一套最小实现。核心是一个SkillLibrary类支持装饰器注册支持导出模型需要的tool schema支持统一分发调用。from typing import Any, Callable, Dict, Optional import json class SkillLibrary: def __init__(self): self._skills: Dict[str, Dict[str, Any]] {} def register( self, name: str, description: str, parameters: Optional[Dict[str, Any]] None, auto_exec: bool True, version: str 1.0.0, ): def decorator(func: Callable): self._skills[name] { name: name, description: description, parameters: parameters or {type: object, properties: {}}, auto_exec: auto_exec, version: version, func: func, } return func return decorator def export_schema(self) - list: return [ { type: function, function: { name: skill[name], description: skill[description], parameters: skill[parameters], }, } for skill in self._skills.values() ] def call(self, name: str, arguments: Dict[str, Any]) - Any: skill self._skills.get(name) if not skill: raise KeyError(f本次调用未找到技能: {name}) if not skill[auto_exec]: raise PermissionError(f技能 {name} 需要用户确认后才能执行) return skill[func](**arguments)这段代码里有几个细节值得说。第一auto_exec不等于没有执行入口它只是说不允许Agent自动调度。第二call方法一定要先做技能存在性检查再做权限检查顺序不能反。第三export_schema只导出元数据不导出函数对象这样你甚至可以把技能库拆成独立服务通过网络暴露schema给远端Agent。3.3 Agent主循环把技能接到LLM上有了技能库下一步就是让模型能用它。下面是一个最基础的ReAct风格主循环OpenAI SDK的实现方式def run_agent(user_query: str, llm, skill_lib: SkillLibrary, max_steps: int 5): messages [{role: user, content: user_query}] for step in range(max_steps): response llm.chat.completions.create( modelgpt-4o-mini, messagesmessages, toolsskill_lib.export_schema(), ) msg response.choices[0].message if not msg.tool_calls: return msg.content messages.append(msg) for tool_call in msg.tool_calls: try: result skill_lib.call( tool_call.function.name, json.loads(tool_call.function.arguments), ) except Exception as exc: result {status: error, error: {code: type(exc).__name__, message: str(exc)}} messages.append({ role: tool, tool_call_id: tool_call.id, content: json.dumps(result, ensure_asciiFalse), }) raise RuntimeError(Agent达到最大步数限制任务未完成)这套主循环的核心逻辑很简单模型要么给最终回答要么给一批工具调用如果给了工具调用循环就把结果回填给模型继续走。注意异常要包在单个工具调用里而不是让整个循环崩掉。现实中一个技能失败应该让模型基于错误信息决定下一步而不是直接中断任务。另外max_steps必须设置这是一个兜底手段。模型在复杂任务上会反复尝试没有步数上限一个Agent可能在死循环里消耗掉巨额token。3.4 实战示例写一个“查天气安排行程”的技能链我用一个组合示例来展示技能层怎么联动。先注册查天气和登记日程两个技能skill_lib SkillLibrary() skill_lib.register( nameweather_query_forecast, description根据城市名称查询未来三天的天气状况包括温度、降水概率和风力适合出行规划、穿衣建议、户外活动安排时使用。不适合查询历史天气不提供空气质量。, parameters{ type: object, properties: { city: {type: string, description: 城市名称中文如北京、上海}, unit: {type: string, enum: [celsius, fahrenheit], description: 温度单位默认celsius}, }, required: [city], }, ) def weather_query_forecast(city: str, unit: str celsius): # 真实项目中在这里调用天气API return { status: success, data: { city: city, forecast: [ {date: 2025-01-10, temp: 8, precip: 10, wind: 东北风3级}, {date: 2025-01-11, temp: 10, precip: 20, wind: 东南风2级}, ], }, } skill_lib.register( nameschedule_add_event, description为指定日程表添加一条日程记录。适合在用户要求安排会议、提醒、行程登记时使用。该操作会写入日程表建议在得到用户确认后执行。, parameters{ type: object, properties: { title: {type: string, description: 日程标题}, start_time: {type: string, description: 开始时间格式YYYY-MM-DD HH:mm}, end_time: {type: string, description: 结束时间格式YYYY-MM-DD HH:mm}, }, required: [title, start_time, end_time], }, auto_execFalse, ) def schedule_add_event(title: str, start_time: str, end_time: str): return {status: success, data: {event_id: evt_12345, title: title}}这段代码演示了两个关键点。第一个查天气是只读技能允许Agent自动执行第二个登记日程是写操作我关掉了auto_execAgent必须停下来等用户确认。实际运行的效果是用户说“帮我看看北京明天会不会下雨如果下雨就在下午安排一场室内会议”。模型第一步调天气查询第二步给出会议日程的预览参数并向用户确认“根据天气预报明天北京降水概率20%我准备在下午3点到4点添加日程‘室内项目会’确认请回复是”。这个体验才是Agent该有的样子。4. 踩坑实录技能系统常见问题与排查方法4.1 技能永远被忽略问题出在描述上这是一类最高频的现象技能列表里明明有“根据城市查三天天气”用户问“明天杭州穿啥”模型却自己编了一声“预计15度建议穿外套”。排查思路是先看模型的tool_calls里到底选没选技能。如果选了但被忽略多半是描述里的触发场景没覆盖这次提问的表达方式。用户说的是“穿啥”但描述里写的是“出行规划”。这时候把触发场景换成“穿衣建议、出行规划、天气查询”问题往往立刻解决。我一般还会给重要技能加一个“trigger hint”字段追溯到schema描述里写成“当用户提及穿衣、带伞、出行、活动安排时优先调用本技能”。描述写得越像用户真实说出来的话召回率越高。4.2 参数幻觉LLM把字符串填进了整数参数幻觉属于模型能力边界问题。前一阵我遇到的例子是模型在调用日期参数时传了“明天下午3点”而不是“2025-01-11 15:00”。原因很简单技能描述里没有写明日期格式要求。修复的方法是在字段description里直接写“必须转换为YYYY-MM-DD HH:mm格式后再传参如果是相对时间先计算绝对时间再把结果传入”。光靠描述还不够运行时一定要做严格校验。我在每次技能调用前加一个简单的assert或pydantic校验字段类型不对就返回error让模型自己纠正。这个方法看起来很笨但它防御住了绝大多数的不规范参数。4.3 无限循环Agent停不下来有一次我给了Agent一个“批量给客户发通知”的任务它先查了客户列表然后发现自己要循环发邮件。它没有调用“批量发送”技能而是一个一个地调“发送单封邮件”接口结果几十个客户它打算逐个发一遍。步数限制到了才停下来浪费了大量token。这个问题的本质是技能粒度不对。正确做法是在技能层提供“批量版”技能彻底避开逐条循环。另外我在主循环里加了步数告警超过四步还没回到“生成最终回答”的迹象就把当前状态和已调用的技能列表打印出来方便人工介入。无限循环除了技术手段要控制产品逻辑上要重视Agent一旦进入循环用户体验损失很大。4.4 上下文爆炸返回值把对话窗口塞满了上下文爆炸是技能层设计粗糙的直接后果。我有一个技能是“导出本月订单明细”最初就是把全量订单格式化返回。一次任务里Agent连续五位订单返回结果每次几千字对话轮次一多窗口就被撑爆了。后来我强制所有列表型返回值都做了截断和摘要订单明细只保留前十条加一个总数统计字段。模型如果需要更详细的数据会通过另一个技能按ID去拉详情。这里有个原则技能库返回给模型的永远是“为决策服务的最小数据集”而不是“API返回的完整数据”。4.5 可观测性每个调用都要有迹可循Agent出问题时最可怕的是你不知道它干过什么、以什么顺序干的。我为技能库加了日志埋点每次调用记录五件事技能名、参数、返回结果前200字符、耗时、本次调用的追踪ID。追踪ID非常重要因为一个用户请求会演化成多轮对话和多次调用我们需要一个ID把它们串起来。日志级别我会分开三层调测阶段打出完整参数线上环境打脱敏后的摘要错误场景额外记录原始异常栈。有了这些记录排查上面那几类问题的时候基本几分钟就能锁定根因。5. 从技能库到技能生态MCP与标准化之路5.1 MCP解决了什么问题当你的技能不只服务一个Agent而是要服务多个产品线的时候技能共享和标准化的诉求就出来了。现在开发社区提的比较多的方案是MCPModel Context Protocol。它的价值在于把技能封装成标准接口让不同Agent、不同应用都能挂载着用。用一个生活化的类比以前你出门要带一堆充电线相机一根、手机一根、耳机一根。大家都不想为每个设备做一根专门线材于是有了Type-C。MCP在技能生态里做的就是Type-C的活订一套统一协议数据输入、操作执行、结果返回都走标准通道。如果你的团队要维护大几十个技能很值得认真评估迁移到MCP的ROI如果只有十个技能我觉得没必要为概念而概念。5.2 技能评估什么时候该拆、什么时候该合并技能库不是一成不变的需要定期评估。评估时我会看两个指标召回率和精准率。召回率看的是该调的时候有没有被调出来精准率看的是不该调的时候有没有误调。如果某个技能召回准确率长期偏低就要重新写描述如果多个技能总是被同时触发说明它们边界重叠了该合并或加优先级。还有一条经验技能粒度的判断不看“函数大小”而要看“决策边界”。同一个技能内部如果存在明显互斥的分支比如“添加日程”和“删除日程”那就要拆成两个独立技能因为它们的触发场景完全不同混在一个技能里会让模型选错参数。5.3 下一步建议别急着搞大平台最后说点个人体会。我见过很多团队上来就规划“AI中台”“技能平台”各种注册中心、技能网关、权限平台一起上最后发现连最基础的技能描述都没对齐。这个领域的复杂度不是靠架构堆出来的而是靠业务打磨出来的。我现在更倾向于做得更轻先让技能层在业务里跑起来等到确实出现跨团队复用的需求再把技能迁到标准协议上。从语法结构上说技能库就像积木积木本身被验证好用平台才有意义。目前agent-skills还是一个快速演进的领域我给自己定的原则是优先解决生产环境的稳定性再考虑架构上的先进感。如果你也在做Agent项目建议从今天开始把现有工具函数全部归拢到技能库里哪怕只是加一层装饰器和统一的schema导出。这步做完你会发现Agent的调试体验和稳定性都会明显上一个台阶。