资讯动态

agent-skills实战:技能化改造让AI Agent从会聊天到会干活

发布时间:2026/10/14 9:13:16 来源:尧图企业网站定制
直接进入主题。最近社区里到处都在聊 agent-skills 这套玩法我自己也在模拟项目X里试了一圈感慨挺多的。很多人以为这是某个新框架的功能其实它更像一套思路与其让 Agent 在那个巨大的上下文里临场发挥不如把能力拆成一个一个独立、可插拔、带说明书和参数的“技能包”。项目标题“agent-skills”所代表的就是这种以技能为基本单位来组织智能体能力的模式。这篇博文我就把从零搭建、核心实现到调参避坑的完整过程写出来给想入坑或者已经卡在路上的朋友做个参考。先说它适合谁。如果你已经跑通过一个能对话的 Agent Demo但总觉得它“会聊天、不会干活”如果你手里有大模型 API却不知道怎么让模型稳定调用某些固定流程——那这套技能化改造正好对症。我也试过直接往 System Prompt 里塞指令清单本质上也能触发工具调用但随着指令变多模型开始混淆相似功能参数传错、调用错技能的情况非常频繁。把技能拆出来之后每条能力都有独立描述、独立参数校验、独立退出条件模型只需要做一件事按说明书决定调用谁。1. 先搞明白agent-skills 到底解决了什么问题很多第一次接触这个思路的人会问这跟普通的 function calling 有什么区别说实话底层机制没有本质区别模型都是输出结构化调用指令再由宿主代码执行。真正的差异在于组织方式和抽象粒度。1.1 从“什么都能答”到“什么都能做”先讲一个直观对比。传统 Agent 项目里的工具函数往往是一堆散落的 Python 函数比如def send_email(user_id, content)然后在 system prompt 里写一句“你可以发邮件”。刚开始没问题但等工具超过十个你会遇到几个很典型的问题模型记不住所有工具的准确签名特别是参数默认值、可选字段这些细节。工具之间的边界模糊比如“查天气”和“查空气质量”描述差不多模型常常选错。给模型看的“工具说明”和真正执行代码之间缺少统一校验层出错时很难定位是模型的问题、参数的问题还是内部逻辑的问题。agent-skills 的解法很直接把每个能力定义成一个目录目录里放一份标准说明文档通常叫 SKILL.md外加几个脚本文件。Skill 的名字、描述、参数 Schema、执行入口、依赖文件全都被约束在固定的目录结构里。于是决定调用哪个技能的不再是模型在作业堆里翻答案而是它读清楚每个技能自己的说明书再给出结构化参数。我当时做了一件很幼稚但很有用的事把所有技能说明全部打印出来自己充当模型模拟“如果我要让系统帮我查三小时后某地天气”应该怎么选。结果发现如果按普通工具的扁平描述很容易同时选中“天气查询”和“历史天气查询”两个技能。但在技能化的结构里我给每个技能加了明确的触发场景与禁止场景字段这种歧义就消失了。这不是模型的进步而是工程上把问题想清楚了。1.2 “技能化”思维把能力做成一级公民技能化这个思路有点像你家里装修时把所有电器说明书各自装订成册而不是全部印在一张大挂图上。每本说明书里只讲这一台机器怎么用、有什么限制、注意事项是什么。对 Agent 来说这份说明书就是 SKILL.md它决定了模型何时、如何、以什么参数去调用这个技能。我在项目里维护了一整套技能库每个技能都遵循同样的模板name: 技能短名英文加下划线比如 weather_now。description: 两三句话讲清楚它做什么什么时候用。when_to_use: 明确列出适用场景甚至可以包含典型请求句式。when_not_to_use: 明确列出不适用场景比如“用户问历史天气时不要用这个”。parameters: 定义入参类型、必填性、默认值、取值范围。returns: 定义出参结构让调用方知道拿到的是什么。有人可能觉得 description 写那么长模型也不一定看。这里我可以给出一个经验数据结构化说明比自然段落的效果好很多。我试过在 description 里写“这是一个用于查询天气的工具”模型偶尔会对不该查天气的请求也触发它。改成“仅当用户明确表达以下意图时使用获取某地未来一段时间内气象条件”之后误用率显著下降。所以别嫌这些字段啰嗦它们才是技能的第一道守门员。1.3 这项目要不要做先看门槛如果你是在单机脚本里调一两个 API那确实没必要上技能化直接写函数就行。但如果你的目标是做一个能应对多种任务的助手或者想长期沉淀一套可复用的能力集合那这个成本是值得的。我自己的判断标准有三个工具或流程数量是否会超过 10 个。是否有多个相似功能需要区分。是否希望后续可以单独给某个能力加鉴权、加缓存、加日志。这三个问题里只要有一个是“是”技能化改造基本不亏。反过来如果数量少、边界清、不需要单独维护那保持简单就是最好的设计。2. 整体方案设计怎么搭一套可扩展的技能库这一节我重点说说目录结构和注册机制。好的技能库设计应该做到“加新技能时不用改主程序逻辑”哪怕写技能的人对整体调度一无所知也能直接塞进去跑通。2.1 目录结构一个技能一个文件夹我采用的目录结构非常朴素但实测下来很稳agent-skills/ ├── skills/ │ ├── weather_now/ │ │ ├── SKILL.md │ │ ├── main.py │ │ └── requirements.txt │ ├── summarize_text/ │ │ ├── SKILL.md │ │ ├── main.py │ │ └── requirements.txt │ └── set_reminder/ │ ├── SKILL.md │ ├── main.py │ └── config.json ├── registry.json ├── scheduler.py └── executor.py每个技能文件夹里SKILL.md 是给模型看的说明书main.py 是给执行器调用的入口。requirements.txt列出该技能额外依赖的 Python 包这对于隔离依赖环境非常有用。就算各个技能用了不同版本的同一个库也能通过虚拟环境或按需安装的方式避免冲突。2.2 SKILL.md 怎么写才不会被模型忽略这是整个设计里最容易被低估的部分。很多人的 SKILL.md 就写一段话模型看完跟没看一样。我建议把说明书做得“像客服系统里的工单模板”固定字段固定顺序少描述、多界定。我在天气技能里写的 SKILL.md 大概是这样的## 技能名称 weather_now ## 技能描述 获取某个地点当前或未来短时间段内的天气实况与预报。 仅当用户包含明确地名或可推断地名并且意图是了解天气情况时使用。 如果用户询问“过去”的天气或“气候统计”请改用历史天气技能。 ## 参数说明 - location: 字符串必填城市名或区县名例如“北京市朝阳区”。 - days: 整数可选默认 1预报天数1 到 7 之间。 - unit: 字符串可选取值 celsius / fahrenheit默认 celsius。 ## 输出格式 JSON 对象字段包括 location、temperature、conditions、forecast。注意我这几个点先讲了“不要什么时候用”这比只写“什么时候用”更能帮模型排除错误候选。参数部分直接给了“必填”“默认值”“范围”模型生成参数时会更有依据不会瞎编。输出格式也提前固定下来调用方不用猜它返回什么。2.3 技能注册表可被发现、可被检索有了说明书还差一层索引。我维护了一个registry.json启动时加载到内存里负责把所有技能的元信息汇总出去供模型按名检索也供执行器按名分发。{ skills: [ { name: weather_now, entry: skills/weather_now/main.py, description: 实时天气查询, enabled: true }, { name: summarize_text, entry: skills/summarize_text/main.py, description: 长文本摘要, enabled: true } ] }注册表的好处是调度器只跟注册表打交道不需要硬编码技能路径。后续添加技能我只需要把新目录丢进 skills 文件夹再往 registry.json 加一行主流程完全不用动。3. 核心技能实现从零手写三个可用技能讲完架构直接看实现。我这里拿三个常用技能做示例天气查询、文本摘要、定时提醒。这三个技能代表了三种典型形态外部 API 调用型、本地模型调用型、状态管理型。3.1 天气查询技能从鉴权到请求失败兜底第一个技能weather_now。它的核心逻辑非常直白解析参数请求某个公开气象服务解析响应返回结构化结果。但真正写起来几个细节坑得很。# skills/weather_now/main.py import json import sys import urllib.request def resolve_location(raw): # 实际项目中这里会接一个地理编码服务 # 演示直接采用参数值返回 return raw def fetch_weather(location, days1, unitcelsius): api_key your_api_key_here url https://api.example.com/v1/weather params { q: location, days: days, unit: unit, appid: api_key } query urllib.parse.urlencode(params) with urllib.request.urlopen(f{url}?{query}, timeout10) as resp: return json.loads(resp.read().decode(utf-8)) if __name__ __main__: args json.loads(sys.argv[1]) try: loc resolve_location(args[location]) data fetch_weather(loc, args.get(days, 1), args.get(unit, celsius)) result { location: loc, temperature: data[current][temp], conditions: data[current][condition], forecast: data.get(forecast, []) } print(json.dumps(result, ensure_asciiFalse)) except Exception as exc: print(json.dumps({error: str(exc)}, ensure_asciiFalse)) sys.exit(1)执行器调用技能时会把模型生成的参数转成 JSON 字符串作为sys.argv[1]传给 main.py。这种方式有几个好处技能脚本独立运行即使一个技能崩了也不会带崩主进程标准输入输出即接口协议想换成其他语言写技能也完全可行。需要注意鉴权信息不要让模型经手的参数去拼应该在技能内部通过环境变量或配置文件读取。模型只负责业务参数密钥这种敏感信息永远不要出现在参数列表里。3.2 文本摘要技能调用本地模型还是远端接口第二个技能是 summarize_text它更像一个“计算型”技能。输入是一段长文本输出是摘要。这个技能的关键分歧在于用远端大模型接口还是本地模型。我两种方案都试过。远端接口效果稳定但存在隐私风险而且调用成本要仔细控制本地模型则方便离线调试但摘要质量参差不齐。最后在模拟项目X里我采用的是混合策略先根据输入长度和敏感级别判断走哪条路太长或包含特定标记的文本优先走本地小模型。# skills/summarize_text/main.py import json import sys import hashlib def detect_sensitive(text): # 仅示例检测手机号等敏感模式 import re return bool(re.search(r1[3-9]\d{9}, text)) def summarize(text, max_length200): # 这里调用文本摘要接口 # 实测返回结果受长度和语言影响较大 payload {text: text, max_tokens: max_length} # 请求省略可能是远端 API也可能是本地模型服务 return 这是摘要结果占位。 if __name__ __main__: args json.loads(sys.argv[1]) text args[text] max_length args.get(max_length, 200) if detect_sensitive(text): result {summary: 输入包含敏感信息已拒绝处理。} else: result {summary: summarize(text, max_length)} print(json.dumps(result, ensure_asciiFalse))文本处理类技能最容易踩的坑是输入长度不加限制。模型有可能把一整个网页源码当作文本来求助技能内部必须做截断或分块处理否则请求很容易超时。我后来在参数 Schema 里对 text 字段加了 max_chars50000 的约束超出就要求模型先做裁剪这才把异常率降下来。3.3 定时提醒技能边缘情况与时间解析第三个技能 set_reminder比较特殊因为它是“有状态”的。用户说“半小时后提醒我喝水”系统不能立刻返回结果就完事而是要真的在半小时后触发一次提醒。这就涉及事件调度和日历解析。# skills/set_reminder/main.py import json import sys from datetime import datetime, timedelta def parse_delay(text): # 自然语言相对时间解析这里仅做演示 if 分钟后 in text: return int(text.split(分钟后)[0]) return None if __name__ __main__: args json.loads(sys.argv[1]) message args[message] when args.get(when) if when is None: delay parse_delay(args[query]) if delay is None: print(json.dumps({error: 无法解析时间}, ensure_asciiFalse)) sys.exit(1) fire_at datetime.now() timedelta(minutesdelay) else: fire_at datetime.fromisoformat(when) print(json.dumps({ reminder_id: remind_12345, fire_at: fire_at.isoformat(), message: message }, ensure_asciiFalse))这里我要特意提醒时间解析的坑千万别让模型直接输出“相对时间表达式”就完事然后由执行器去猜。最稳的做法是让模型输出一个绝对时间 ISO 字符串相对时间由前端提前换算或者模型在生成参数时已经完成换算。因为执行器不具备自然语言理解能力时候解析失败会显得特别蠢。4. 调度与编排主控 Agent 怎么选择合适的技能技能本身写得再好如果主控不会安排一切都是白搭。这一节说说调度器的设计思路以及我在处理多技能协同、优先级冲突时的一些实测经验。4.1 基于描述的匹配策略我把主控的决策过程简化成三步模型读取注册表里所有技能的 description。根据用户请求判断哪些技能候选。为候选中选中的技能填充参数并输出调用计划。其实这跟 function calling 的机制非常像所以也可以用现成的 function calling API 替代。我在项目里是自己拼 prompt 然后让模型以 JSON 方式输出调用计划核心部分长这样你是一个智能任务调度器。当前你可以使用以下技能 1. weather_now: 实时天气查询 2. summarize_text: 长文本摘要 3. set_reminder: 定时提醒 请根据用户请求选择最合适的技能并输出以下 JSON {skill: 技能名, parameters: {...}, reason: 选择理由}这里有个小小的设计讲究我让模型额外输出“选择理由”不为别的就为了排查问题的时候能知道模型是凭哪句话判断出来的。如果理由跟用户请求明显对不上我就知道是描述出了问题而不是参数有问题。4.2 多技能协同与数据传递早期的技能设计是“一个技能一次调用”但真实场景往往需要多步。用户说“北京明天会下雨吗如果下雨就提醒我带伞”这就同时涉及天气查询和定时提醒。我最初的调度器是一次只调一个技能结果这类请求直接失败。后来我把调度器改成支持“技能链”先执行技能 A把它的输出结果作为技能 B 的输入参数的一部分。比如天气查询返回后有雨就自动调用 set_remindermessage 等于“明天出门记得带伞”。实现上我引入了一个简单的中间缓存区用于暂存上一个技能的输出让下次技能可以从prev_result字段里取数。实测下来这种浅层的链式调用非常实用而且不会把复杂度推给模型。模型只需决定调用链的顺序具体数据流转由调度器代码接管。4.3 优先级与冲突消解有些场景下多个技能都可能“接得住”同一个请求。比如用户说“帮我看看明天适合适合跑步”既可能是天气查询也可能是运动建议技能。这类模糊场景模型必须有一个消解规则。我的做法是在注册表里给每个技能增加一个priority字段数值越高越优先。同时要求技能描述里明确写上“如果不满足某个条件请改用其他技能”。测试下来加了优先级的调度器比纯靠模型随机选择的准确率高不少错误调用率下降了非常明显。这里有一个经验优先级字段必须写在注册表里而不能放在 SKILL.md 里这样优化时只需要调一处索引不用逐字改说明书。5. 常见问题与排查技巧实录踩坑是学习的捷径。下面按我实际遇到的高频问题按视频排查思路整理了一份速查表挨个说说现象和解决办法。5.1 技能描述太模糊导致错误调用现象用户问“今天多少度”模型不去调用天气技能而是调用了历史天气技能。排查时先把注册表和 SKILL.md 打印出来如果你自己都觉得两个技能的 description 区分度不高那模型肯定也分不清。解决给每个技能增加 when_not_to_use 字段并明确“当用户询问XX时不要使用本技能”。我在给 weather_now 和 weather_history 两个技能做区分时给前者的 SKILL.md 里加了一句“仅关注当前或未来预报”给后者的加了一句“仅关注历史时间段”。从那以后错误调用基本绝迹。5.2 技能加载失败约定大于配置现象新技能目录放进去注册表也更新了但执行时总是报错 “module not found” 或者 “file not found”。原因往往是路径写错了或者技能目录里缺少入口文件。我在 executor 里加了启动自检把每个注册技能的文件是否存在、能否编译、依赖是否已安装全部校验一遍启动不发警告就跑。排查时别犯懒先看路径用的是绝对路径还是相对路径。相对路径在不同工作目录下表现完全不同统一在配置里设置一个 base目录再拼接技能路径最省心。5.3 循环调用与上下文爆炸这是多技能协同阶段最容易出的问题。技能 A 的输出被传给技能 BB 的结果又被塞回给模型继续决策往复几轮后上下文字符数翻倍暴增响应延迟越来越明显。我的处理方式是每轮调用后对中间结果做摘要把长文本裁剪成要点再拼接进下一轮的上下文。举个例子天气查询返回的 7 天预报每个小时都有数据中间结果只提取“未来三天有雨”“气温低于 10 度”等关键标签后续决策完全够用。5.4 参数校验给模型“上一道保险”模型生成的参数有时候就是离谱。明明 days 限制在 1 到 7 之间它会生成 10 甚至 -3。技能内部如果不对参数做约束外部请求就出怪事。我给每个技能入口都加了一个轻量级校验函数专门检查必填项是否缺失、类型是否正确、取值范围是否合法。经验法则校验逻辑要放在技能脚本自身的最前面而不是依赖调度器统一校验。因为技能是独立运行的万一哪一天你跳过调度器直接调用它校验逻辑依然有效。def validate(args): if location not in args: raise ValueError(缺少 location 参数) days args.get(days, 1) if not isinstance(days, int) or days 1 or days 7: raise ValueError(days 必须是 1 到 7 之间的整数)我看过太多项目把校验放在上层底层技能裸奔一被绕过就是事故。把它放到技能内部之后稳定性提升非常明显。5.5 超时与重试外部 API 不像本地代码那么听话天气查询这种依赖外部接口的技能一定要设计超时和重试。我踩过一次线上事故外部接口响应超过 15 秒调度器一直在等用户端以为机器人死掉了。后来在 fetch_weather 里加上了 timeout10并在技能之外包了一层重试第一次失败后隔 1 秒重试最多 3 次。大部分偶发故障都能被重试吸收真的连失败的话再返回一个友好错误给用户比卡住强太多。5.6 技能内部日志排查问题的关键线索最后提醒一个容易被忽略的点给每个技能入口打日志。不是让技能自己打印到终端而是把输入参数、执行耗时、输出结果统一写到约定的日志文件里。正则日志一旦形成习惯回溯问题会轻松非常多。我在执行器里封装了一个 log_record 函数技能只需要调用一行就能把当次调用的关键信息存档。很多疑难杂症比如“用户偶尔抱怨某条回复很奇怪”如果你有日志翻一下就知道当时模型选择了哪个技能、传了哪些参数、输出是什么。没日志的话就只能在黑盒里瞎猜了。写在最后的体会我在模拟项目X里推完这套 agent-skills 模式之后最直观的感受是Agent 的问题很多时候不是模型不够聪明而是我们没把执行环境整理得足够清晰。技能化让每个能力都有了边界、有说明书、有校验、有日志模型只需要当“调度员”而不是“代码解释器”。如果从头再搭一次我会把 SKILL.md 模板再往细里推一步给每个技能配上几个“反例问题”作为内置测试这样每次改描述都能自动回归防止优化一个技能反而带崩另一个。办不到自动化的时候手动往前翻几个案例也是值得的。最后再分享一个实战小技巧。你给技能写 when_not_to_use 字段的时候别只写抽象的“不适用”最好写一条用户典型原话例如“当用户问前几天下了多少雨时不要调用 weather_now”。模型在识别具体例句的时候比对抽象规则灵敏度高得多。这个小改动帮我省下了一整轮的描述调优成本。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑