Agent Skills实战指南:构建LLM稳定调用的可复用技能库

发布时间:2026/10/7 2:15:14
Agent Skills实战指南:构建LLM稳定调用的可复用技能库 最近如果你在折腾AI Agent开发应该对agent-skills这个词不陌生。它不是什么新框架而是一整套关于“如何把能力封装成让大模型稳定调用的小模块”的实践经验。说白了就是把那些让智能体干活的动作——查数据、调接口、算结果、写文件——从散落的函数和prompt里拎出来做成有明确输入输出、有清晰描述、能复用又能组合的“技能包”。这篇文章适合正在做Agent应用、或者想把LLM接入业务系统但被function calling折磨得够呛的开发者。我会用自己真实做过的项目来拆解技能该怎么设计、怎么实现、怎么测试以及踩过哪些坑。1. 项目概述Agent Skills到底解决什么问题1.1 从零散函数到技能库的演进最早做Agent的时候我的代码长得很丑一堆工具函数堆在同一个文件里每个函数都要写一段又臭又长的description给模型看参数schema反复改prompt动不动就超过上下文窗口。更气人的是模型经常把参数传成字符串或者明明该调用A函数却调了B函数。后来我去翻Anthropic和各大框架的文档发现大家都在往同一个方向收敛把“工具函数”升级成“技能”。“技能”不是简单地把函数包一层而是把函数的描述、参数规则、执行逻辑、错误处理、示例绑定在一起形成一个有明确边界的可复用单元。这个转变很有价值。想象一下你有一堆技能就像工具箱里各种专业的钻头、扳手和螺丝刀。模型Agent作为使用者不看每个工具内部怎么造只看标签和说明来选。技能设计得好不好直接决定了Agent能不能在复杂任务里做出正确的选择。我见过太多项目功能其实都实现了但就是因为技能的“外部接口”没定好导致Agent行为像醉汉一会儿调对一会儿调错。1.2 Agent Skills的核心价值可复用、可组合、可维护为什么强调“技能库”而不是“工具集”因为技能有三大特质。第一是可复用。一个“获取用户当前地理位置”的技能既可以在旅游规划Agent里用也可以在外卖推荐Agent里用不需要改一行代码。只要把技能注册表做对每个Agent都能加载自己需要的子集。第二是可组合。复杂的业务动作往往需要多步。比如“生成客户周报”这个任务可以拆成“拉取CRM数据”“计算变化率”“套用Markdown模板”“发送邮件”四个技能。Agent根据目标动态编排这些技能而不是写死一条流水线。这种组合能力让系统能应对没有预设过的任务路径。第三是可维护。传统工具函数分散在各种服务里改一处参数要全局排查。技能如果按统一规范封装接口文档、版本信息、依赖关系都内聚在一起升级时只要保证“输出契约”不变外部调用方就无感。我后来把项目里所有Agent能力统一改造成技能库之后新增一个业务功能从改半天缩短到半小时收益非常明显。2. 技能设计的关键原则与思路拆解2.1 技能粒度多大算合适这是我在项目里争论最多的问题。粒度太粗比如“处理用户请求”这种技能内部逻辑极其复杂模型很难判断什么时候调用粒度太细比如“字符串转大写”这种基础的不能再基础的函数模型在99%的场景下不需要单独为它做路由反而加重了prompt的负担。我从实践中总结了一条判断标准一个技能应该对应一个完整的、可独立验收的业务动作。也就是说给这个技能取个名字别人能从名字和描述里立刻知道它“能交付什么结果”。举个例子“解析用户简历并提取结构化字段”是合适的技能因为简历转JSON这个动作独立且结果清晰“解析简历第2页的教育经历”就太细了这种应该作为内部逻辑而不是独立技能。另外要避免把多个动作绑在一起。比如“查询天气并生成穿衣建议”这种看起来很方便但如果你有另一个Agent只想查天气、不想要建议就没法复用了除非加参数开关。一旦开始给技能加一堆布尔开关说明粒度设计失败了。正确做法是拆成“查询天气”和“根据天气生成穿衣建议”两个技能由Agent在需要的时候组合调用。2.2 输入输出契约让模型“用不坏”Agent技能的核心问题不是“人”怎么调用而是“模型”怎么调用。模型的本质是从token序列生成token序列它对你的类型系统没有天然的敬畏之心。如果你定义了一个整数参数但描述写得不清楚模型真的会把“温度23度”整句传进去。所以在设计参数schema时我的原则是三句话参数尽量扁平、类型尽量严格、必填尽量少。扁平的意思是不要搞多层嵌套的JSON对象因为模型生成深层嵌套时括号和引号总是容易出错。严格的意思是在schema里明确标注每个字段的type、枚举和格式很多框架支持JSON Schema一定要写全。必填尽量少的意思是能让模型少填一个参数就少填一个因为每个需要模型“编”的字段都是出错机会。下面是一个我常用的技能参数定义示例以天气查询为例SKILL_SCHEMA { type: object, properties: { location: { type: string, description: 城市名称例如北京、上海。必须是中文城市名。 }, date: { type: string, description: 查询日期格式YYYY-MM-DD默认为今天。 } }, required: [location] }返回结果的规范同样重要。我统一要求技能返回标准JSON结构至少包含三个字段status、data、message。这样Agent可以快速判断技能调用成功还是失败而不是靠读异常堆栈。失败时message返回人可以看懂、模型也能理解的原因比如“无法连接到天气服务请稍后再试”。2.3 技能描述模型选择技能时的“说明书”模型是怎么决定调哪个技能的靠的是你的描述。描述写不好调用必乱。很多人的技能描述就一句话“获取天气数据”这完全不够。我写描述时会包含四类信息触发场景什么时候该用这个技能比如“当用户想了解某地天气情况、出行是否需要带伞、周末是否适合户外活动时”。输入限制参数有什么约束模型容易忽略的值提前写出来。输出约定返回什么结构方便模型理解结果。典型示例给一句自然语言和对应的调用参数相当于给模型做了一次few-shot。比如天气技能的描述我最后写成这样技能名称获取天气信息 用途查询指定城市在指定日期的天气情况包括温度、降水概率和风力适合在用户询问“今天冷不冷”“明天是否下雨”以及做行程规划时调用。 参数注意location必须是标准中文城市名不要用拼音或英文。date参数如果用户没有明确给出不要自行填写保持为空。 返回示例{status: success, data: {city: 北京, date: 2025-05-02, temp_high: 26, temp_low: 15, precip_prob: 0}, message: ok}改了描述之后模型乱调技能的频率明显下降。你可以在调试日志里统计“技能命中率”从我的经验来看一段好的描述能把命中率从60%拉到90%以上。3. 实操过程从零搭建一个Agent Skill库3.1 环境准备与目录结构我建议不要把所有技能写在单个文件里而是做成目录化的技能库。每个技能独占一个文件夹包含代码、配置、测试和README。这样做的好处是后续可以通过扫描目录动态注册技能不需要在代码里写死if-else。一个典型的目录结构长这样agent-skills/ ├── skills/ │ ├── weather/ │ │ ├── __init__.py │ │ ├── skill.py │ │ ├── schema.json │ │ ├── test_weather.py │ │ └── README.md │ ├── calendar/ │ │ ├── __init__.py │ │ ├── skill.py │ │ ├── schema.json │ │ └── test_calendar.py │ └── report/ │ ├── __init__.py │ ├── skill.py │ ├── schema.json │ └── test_report.py ├── registry.py ├── runner.py └── requirements.txt每个技能文件夹里skill.py是核心它暴露一个统一接口schema.json描述输入输出test_*.py负责测试。registry.py负责扫描skills/目录把所有技能注册到一个字典里runner.py则是运行时引擎接收模型发出的“调用请求”根据技能名找到对应处理函数执行。在实际项目中我是用Python写的这套框架因为Python做Agent生态最成熟但底层思路一样换成Node.js或Go也完全可行。3.2 用Python实现一个可落地的技能模块下面我以“天气查询”技能为例完整展示一个技能是怎么实现的。为了让例子不依赖任何平台我直接用一个开源免费接口来演示核心逻辑是一样的。首先skill.py里定义技能类import json import requests from datetime import date class WeatherSkill: name get_weather description ( 查询指定城市在指定日期的天气情况包括温度、降水和风力。 在用户询问“今天冷不冷”“明天是否下雨”以及做行程规划时调用。 参数注意location必须是标准中文城市名date为YYYY-MM-DD格式不填则默认今天。 ) def get_schema(self): return json.load(open(schema.json, r, encodingutf-8)) def run(self, arguments: dict): # 输入校验绝对不信任模型生成的内容 location arguments.get(location) query_date arguments.get(date, date.today().isoformat()) if not location: return {status: failed, data: None, message: 缺少location参数} # 调用外部API设置超时防止卡死 url https://api.open-meteo.com/v1/forecast params { latitude: location_to_lat(location), longitude: location_to_lon(location), daily: temperature_2m_max,temperature_2m_min,precipitation_probability_max, start_date: query_date, end_date: query_date, timezone: Asia/Shanghai, } try: resp requests.get(url, paramsparams, timeout5) resp.raise_for_status() except requests.Timeout: return {status: failed, data: None, message: 天气服务请求超时} except requests.RequestException as exc: return {status: failed, data: None, message: f天气服务调用失败: {exc}} data resp.json().get(daily, {}) result { city: location, date: query_date, temp_high: data.get(temperature_2m_max, [])[0] if data.get(temperature_2m_max) else None, temp_low: data.get(temperature_2m_min, [])[0] if data.get(temperature_2m_min) else None, precip_prob: data.get(precipitation_probability_max, [])[0] if data.get(precipitation_probability_max) else None, } return {status: success, data: result, message: ok}注意location_to_lat和location_to_lon这两个辅助函数实际项目中通常会接一个城市经纬度对照表如果城市名不在表里就返回失败信息让模型换个说法。这就是“先校验后执行”的典型例子。3.3 注册与测试让技能可用还要可用得稳技能写好之后关键的下一步是注册和测试。注册表的作用是给框架一个“技能清单”当模型说“我要调用get_weather”时框架能一秒找到对应的run()方法。注册逻辑很简单import os import importlib SKILLS {} def register_all(): skills_dir os.path.join(os.path.dirname(__file__), skills) for item in os.listdir(skills_dir): if not item.startswith(_) and os.path.isdir(os.path.join(skills_dir, item)): module importlib.import_module(fskills.{item}.skill) for attr_name in dir(module): attr getattr(module, attr_name) if isinstance(attr, type) and hasattr(attr, name) and hasattr(attr, run): SKILLS[attr.name] attr()动态注册会带来一个隐患如果技能目录里有语法错误整个加载可能失败。所以我在注册时加了精细的异常捕获让一个技能出错时只打日志不影响其他技能加载。测试则应该兼顾“人能测”和“模型能测”。我写了两类测试第一类是单元测试直接构造合法的参数调用run()断言返回结构第二类是“模拟Agent调用”把模型生成的原始参数喂进来故意构造一些残缺参数、错误类型看技能会不会优雅失败而不是抛异常。下面是测试文件的一个示例import pytest from skills.weather.skill import WeatherSkill def test_weather_skill_basic(): skill WeatherSkill() resp skill.run({location: 北京, date: 2025-05-02}) assert resp[status] success assert data in resp and temp_high in resp[data] def test_weather_skill_missing_params(): skill WeatherSkill() resp skill.run({}) assert resp[status] failed assert message in resp把这类测试接入CI每次改动技能后跑一遍能有效防止自己不小心改坏了接口。3.4 多技能联动让Agent像一个团队一样协作技能库建好之后单技能调用已经没问题了但真正的Agent应用通常需要多技能配合。我举一个实际例子做一个“出差行程秘书”。当用户说“我明天去上海出差帮我看看天气顺便记一条提醒”Agent需要调用两个技能天气查询和日历提醒。在技术上多技能联动有两种主流做法。第一种是框架自动编排比如LangChain、CrewAI这类框架里定义好工具列表Agent自己决定先调哪个再调哪个。第二种是显式工作流你自己写一段业务代码按顺序调用技能。我这里更推荐先用第二种因为可调试性高适合业务逻辑相对固定的场景。下面是我用轻量代码实现的多技能联动骨架def handle_travel_request(user_input: str, executor): # 先用一个技能提取用户意图中的关键信息 extract_resp executor.call(extract_travel_intent, {text: user_input}) if extract_resp[status] ! success: return 抱歉我没听懂你的出行安排 city extract_resp[data].get(city) date extract_resp[data].get(date, date.today().isoformat()) # 调用天气技能并把结果转成自然语言 weather_resp executor.call(get_weather, {location: city, date: date}) if weather_resp[status] success: weather_text format_weather(weather_resp[data]) else: weather_text 天气信息暂时获取失败 # 写入日历提醒 reminder_resp executor.call(add_calendar_reminder, { title: f出差去{city}, datetime: date 09:00, }) return f行程安排好了。天气{weather_text}。提醒已经添加。这种显式编排虽然少了“全自动”的酷炫感但胜在可控、可测。你可以在任何一步插入日志、失败兜底和人工确认这在生产环境里非常重要。4. 常见问题与排查技巧实录4.1 模型总是调错技能怎么办这是提得最多的问题。用户对Agent说“北京天气怎么样”结果模型调了一个“计算器”技能。排查时我先看日志确认模型到底拿到了多少技能。如果技能数量过多超过15个模型容易“选择困难症”。这时候优先做的是技能分组把相关技能放到一个父技能下面或者用路由技能做前置分流。另一个原因是描述里没有给出正面和反面示例。只写“查天气用这个技能”不够还要写“用户问今天穿什么衣服如果只是基于天气给建议不要调用这个技能应该调用穿衣建议技能”。给两个例子之后模型犯错率能降一半以上。4.2 参数校验的血泪教训有一段时期我的技能完全不校验输入结果模型传了一个locationnull外接API直接500Agent进入死循环。后来我悟了模型不是你的同事它是个记忆力不太好的实习生。你说“必须传location”它转头就忘了。所以每个技能的run()入口必须做防御式校验缺参数时返回结构化错误而不是让异常冒出去。参数校验最好用JSON Schema或者pydantic做。比如from pydantic import BaseModel, ValidationError class WeatherParams(BaseModel): location: str date: str | None None try: params WeatherParams(**arguments) except ValidationError as e: return {status: failed, data: None, message: f参数格式错误: {e}}这样模型即使传了乱七八糟的类型技能也能给出明确反馈。不少Agent框架会根据错误反馈自动修正参数也算给模型一次“改过自新”的机会。4.3 技能性能与并发控制技能内部往往要调外部API外部API一慢整个Agent响应就卡住。我的经验是三个字限超时。给所有HTTP请求设置timeout绝对不能默认无限等待。再进一步要给外部调用加上熔断器连续失败3次接下来5分钟直接返回失败不发起真实请求。这可以避免一次API故障把下游所有任务拖死。并发量大的场景还需要考虑限流。因为Agent可能并行跑多个任务每个任务又会同时调多个技能。我在技能库外面包了一层简单的信号量控制同一时间打到外部API的请求数比如最多10个并发超出就排队或者快速失败。这个数字需要实际压测确定不是越大越好要兼顾外部服务的承载能力和用户体验。4.4 版本管理技能演化不能乱技能是代码就得用代码管理的方式对待。我要求每个技能目录里必须有README.md说明技能用途、变更记录、依赖信息。版本号语义化大改动输出结构变了升主版本小优化升次版本修Bug升补丁版本。输出结构变化是高风险事件因为Agent的应用层可能已经依赖了旧的返回字段。我吃过一次亏把temp_high改成high_temp结果下游解析全部失效。改名这种事哪怕只改一个字段也要做兼容过渡——同时输出新旧字段运行几个版本再下线旧的。此外技能库要放进Git仓库每次改动都走diff review。别觉得Agent的东西很轻量就不当回事生产环境里一个技能问题可能影响的是成千上万个请求。5. 扩展方向与我的实战体会做了一轮完整的技能库后我明显感到再往后走不是“写更多技能”的问题而是“怎么让技能体系自我进化”的问题。第一个方向是技能质量评估。可以给每个技能记录调用次数、成功率、模型选错率把这些指标做成看板。一段时间的统计会告诉你哪些技能描述有待优化哪些技能根本没有被调用可以考虑删掉哪些技能经常返回失败需要检查下游依赖。第二个方向是技能共享与复用。我后来把所有技能打包成一个独立库提供给团队内多个Agent使用效果很好。如果将来做更大规模的共享可以往技能市场方向发展类似于把优秀技能像插件一样分发并配套签名校验和沙箱执行。第三个方向是多模态技能。现在文本类技能已经很成熟但Agent正在快速进入“能读图、能听声音、能操作页面”的阶段。技能库的设计如果能从一开始就把多模态输入输出考虑进去比如统一用包含content_type的消息结构后面扩展会省很多事。最后说一点个人体会Agent项目的成败很可能不在于模型多聪明而在于你给它准备的技能工具有多趁手。我说一句实话很多看起来“人工智障”的客服机器人问题就出在技能描述写得稀烂、参数校验形同虚设、异常处理一塌糊涂。把这套底层的“agent-skills”功夫练好你的Agent能力立刻就能上一个台阶。别急着堆花哨的框架先把自己的技能库做扎实比什么都重要。