
1. 从agent-skills这个命名说起它到底指什么第一次看到agent-skills这个词很多人会下意识地把它理解成某个具体的库或者框架。但如果你真的去翻一圈资料会发现它更像是一个概念性的命名——描述的是智能体所具备的技能集合这件事本身。换句话说它不是一个能pip install就完事的包而是一类问题的统称如何让一个智能体agent拥有可复用、可组合、可扩展的能力模块。我在实际做智能体相关项目的时候最头疼的从来不是能不能调用大模型而是这个智能体到底会干什么、干得好不好、下次遇到类似任务能不能复用。这三个问题恰好就是 agent-skills 要解决的核心。它关注的不是单次对话的质量而是能力的沉淀与编排。打个比方大模型本身像是一个刚毕业、知识面很广但没什么实操经验的年轻人。你让他写一段文案他能写你让他查个数据他也能凑合。但如果你要他每天早上自动汇总昨天的销售数据、生成图表、写一段分析、再发到群里他就懵了——因为这需要一串技能按顺序组合起来还需要每一步都能稳定复现。agent-skills 要做的就是把这些技能拆出来、定义清楚、再组装回去。所以这篇文章适合谁看如果你正在做智能体应用、想让自己的 agent 从玩具变成能干活的工具或者你只是好奇智能体技能这个概念到底怎么落地那接下来的内容应该对你有用。我会从概念拆解讲到目录结构设计再讲到技能注册、编排、调试和踩坑尽量把每个环节的为什么讲透。需要先说明一点agent-skills 目前并没有一个官方统一的强制标准不同团队、不同框架下的实现差异很大。我下面讲的内容是基于常见的工程实践总结出来的一套通用思路你可以把它当成设计参考而不是唯一答案。2. 拆解 agent-skills 的核心构成技能、注册表与执行器要理解 agent-skills得先把它拆成几个基本零件。我习惯把它分成三层技能单元Skill、技能注册表Registry、技能执行器Executor。这三层各司其职缺一不可。2.1 技能单元一个技能应该长什么样一个技能最朴素的定义是输入 → 处理 → 输出。但真正落地的时候光有这三样是不够的。我见过太多人把技能写成一个裸函数结果用起来各种别扭。一个合格的技能单元通常需要包含这些字段name技能的唯一标识建议用动词开头比如fetch_weather、summarize_text而不是weather这种名词。description给模型看的自然语言描述。这一项极其关键因为智能体决定要不要用这个技能时靠的就是这段描述。parameters参数的 schema 定义包括类型、是否必填、默认值、取值范围。handler真正的执行逻辑。returns返回值的结构说明方便下游技能消费。为什么 description 这么重要因为在大模型驱动的智能体里技能的选择往往是通过把技能列表塞进 prompt让模型自己挑来实现的。描述写得含糊模型就会挑错技能或者该用的时候不用。我踩过的坑是早期把描述写成获取天气信息结果模型在用户问今天出门要不要带伞时居然没调用它——因为它没意识到带伞和天气有关。后来把描述改成查询指定城市的实时天气包括温度、降水概率、风力用于判断出行是否需要雨具命中率立刻上来了。2.2 技能注册表让技能可被发现注册表的作用是集中管理所有可用技能。它解决的问题是智能体怎么知道我现在有哪些技能可以用最简单的注册表就是一个字典key 是技能名value 是技能对象。但工程上更推荐做成一个类支持动态注册和查询class SkillRegistry: def __init__(self): self._skills {} def register(self, skill): if skill.name in self._skills: raise ValueError(f技能 {skill.name} 已存在) self._skills[skill.name] skill def get(self, name): return self._skills.get(name) def list_all(self): return list(self._skills.values()) def describe_all(self): return [ { name: s.name, description: s.description, parameters: s.parameters, } for s in self._skills.values() ]这里有个细节值得说describe_all返回的是给模型看的精简版不包含 handler。因为 handler 是代码塞进 prompt 既浪费 token 又没意义。这个对内对外两套视图的设计是我觉得注册表最容易被忽略但最有价值的地方。2.3 技能执行器把模型的选择变成真实动作执行器负责接收模型给出的技能名 参数找到对应技能并执行然后把结果返回给模型。它要处理的事情比想象中多参数校验模型生成的参数经常有类型错误比如该传数字传了字符串。异常捕获技能执行失败不能直接崩掉整个流程要返回结构化错误。超时控制某些技能比如网络请求可能卡住必须有超时。结果序列化返回值要能转成模型看得懂的格式。我一般会把执行器写成这样import json class SkillExecutor: def __init__(self, registry, timeout10): self.registry registry self.timeout timeout def execute(self, name, params): skill self.registry.get(name) if not skill: return {error: f未知技能: {name}} try: validated skill.validate(params) result skill.handler(**validated) return {result: result} except Exception as e: return {error: str(e), skill: name}注意validate这一步。很多人图省事直接handler(**params)结果模型传了个多余参数就报TypeError。加一层校验把错误在进入业务逻辑前拦下来能省掉大量调试时间。3. 技能注册与编排的实操路径从零搭一个能跑的最小系统光讲概念没意思我们直接搭一个最小可运行的系统。目标很简单让智能体能根据用户输入自动选择并组合技能完成任务。我会用查天气 生成出行建议这个场景来演示。3.1 定义两个基础技能先写两个技能一个查天气一个生成建议。为了不依赖外部服务查天气这里用模拟数据class Skill: def __init__(self, name, description, parameters, handler): self.name name self.description description self.parameters parameters self.handler handler def validate(self, params): # 简化版校验只检查必填项 required [k for k, v in self.parameters.items() if v.get(required)] for key in required: if key not in params: raise ValueError(f缺少必填参数: {key}) return params weather_skill Skill( namefetch_weather, description查询指定城市的实时天气返回温度、降水概率和风力用于判断出行是否需要雨具, parameters{ city: {type: string, required: True, desc: 城市名称} }, handlerlambda city: {city: city, temp: 22, rain_prob: 0.7, wind: 3级} ) advice_skill Skill( namegenerate_advice, description根据天气数据生成出行建议输入需要包含温度和降水概率, parameters{ temp: {type: number, required: True}, rain_prob: {type: number, required: True} }, handlerlambda temp, rain_prob: ( 建议带伞降水概率较高 if rain_prob 0.5 else 无需带伞天气不错 ) )这两个技能的设计有个讲究第二个技能的输入正好是第一个技能输出的子集。这样编排的时候前一个技能的结果可以直接喂给后一个不需要额外转换。这种接口对齐是技能编排能顺畅跑起来的前提。3.2 把技能注册进去并暴露给模型注册很简单把两个技能塞进注册表registry SkillRegistry() registry.register(weather_skill) registry.register(advice_skill) executor SkillExecutor(registry)然后构造给模型的 prompt。核心是把describe_all()的结果序列化后塞进去让模型知道有哪些技能可用import json skill_desc json.dumps(registry.describe_all(), ensure_asciiFalse, indent2) system_prompt f你是一个可以调用技能的智能体。 可用技能如下 {skill_desc} 当用户请求需要用到技能时请以如下 JSON 格式回复 {{skill: 技能名, params: {{...}}}} 如果不需要技能直接回复文本即可。 这里有个经验JSON 格式的约束一定要写死并且给出示例。我试过只写请返回技能名和参数模型返回的格式五花八门有的用自然语言有的用 YAML解析起来非常痛苦。给出明确的 JSON 模板后稳定性提升非常明显。3.3 编排循环让技能一个接一个跑起来真正的智能体不是调一次技能就结束而是要根据结果决定下一步。这就是编排循环def run_agent(user_input, max_steps5): messages [ {role: system, content: system_prompt}, {role: user, content: user_input} ] for step in range(max_steps): reply call_llm(messages) # 假设这是你封装的大模型调用 parsed try_parse_skill_call(reply) if not parsed: return reply # 模型直接给了文本答案结束 result executor.execute(parsed[skill], parsed[params]) messages.append({role: assistant, content: reply}) messages.append({ role: user, content: f技能执行结果{json.dumps(result, ensure_asciiFalse)} }) return 达到最大步数限制这个循环的关键在于max_steps。没有这个上限模型有可能陷入调技能 → 看结果 → 再调同一个技能的死循环。我一般设 5 到 8 步具体看任务复杂度。超过这个步数还没收敛基本说明任务设计有问题或者技能描述有歧义。跑一遍北京今天要带伞吗理想流程是模型先调fetch_weather拿到rain_prob0.7再调generate_advice最后输出建议带伞。整个过程两步技能调用干净利落。4. 技能描述与参数设计的坑模型选错技能的真实原因系统能跑通只是第一步。真正上线之后你会发现大部分问题都出在技能描述和参数设计上而不是代码逻辑。这一节专门讲我踩过的坑。4.1 描述太短模型想不到用这个技能前面提过天气技能的例子这里再展开说。模型选择技能本质是一个语义匹配过程它把用户的话和技能描述做相似度比较。描述越短可匹配的语义特征越少命中率越低。我做过一个粗略的对比测试同样是查天气的技能三种描述下的命中率差异很明显描述写法用户问明天出门穿什么时的命中情况查天气经常不命中模型直接凭常识回答查询城市天气信息偶尔命中取决于问法查询指定城市的实时天气含温度、降水、风力用于出行、穿衣、活动安排判断基本都能命中结论很直接描述里要把使用场景写进去。不要只写技能做什么还要写什么时候该用它。这跟给人写工作手册是一个道理你只写负责报销别人不知道什么情况找你你写负责差旅费、办公用品采购的报销审核大家就清楚了。4.2 参数命名含糊模型传错值参数名也有讲究。我见过有人把参数命名成arg1、data、info这种模型根本猜不出该传什么。好的参数名应该自解释差data、input、param好city、start_date、max_results另外参数描述也要写清楚。比如city这个参数如果只写城市模型可能传北京市朝阳区这种带行政区的值如果写城市名称只填城市级别如北京、上海模型就会规范很多。还有一个隐蔽的坑枚举值要显式列出。比如一个技能参数unit可以是celsius或fahrenheit如果你不写清楚模型可能传C、摄氏、celsius各种变体导致校验失败。把可选值写进描述能省掉一堆兼容代码。4.3 技能粒度过粗或过细粒度是个平衡问题。太粗一个技能干太多事模型不好组合太细技能数量爆炸模型选择困难。我的经验法则是一个技能对应一个原子操作即不能再拆、且语义完整的最小单元。查天气是原子的查天气并生成建议就不是应该拆成两个。反过来把字符串转成大写这种太细的如果只是某个技能内部的一步就没必要单独暴露成技能藏在 handler 里就行。判断标准可以问自己一句这个技能会不会被单独复用会就独立出来不会就合并。5. 调试与可观测性技能调用链怎么追踪智能体最让人抓狂的地方是它为什么不按我想的走。这时候如果没有可观测性基本就是盲人摸象。我在项目里强制要求加三样东西调用日志、步骤快照、失败重放。5.1 调用日志记录每一次技能调用每次执行器被调用都要记下来谁调的、传了什么参数、返回了什么、耗时多少。格式建议结构化方便后续分析import time import logging logger logging.getLogger(agent.skills) def logged_execute(executor, name, params): start time.time() result executor.execute(name, params) elapsed time.time() - start logger.info({ skill: name, params: params, result: result, elapsed_ms: round(elapsed * 1000, 2) }) return result有了这些日志你就能回答模型到底调了哪些技能哪一步慢了哪一步报错了这些关键问题。我遇到过性能问题最后就是靠日志发现某个技能平均耗时 3 秒成了整个链路的瓶颈。5.2 步骤快照还原模型的决策过程光有技能日志还不够还要记录模型每一步的原始输出。因为技能调错往往是模型决策错了而不是技能本身有问题。把每轮的 messages 完整存下来出问题时可以逐轮回看看模型是在哪一步想歪了。我一般会把整个 messages 列表在每轮结束后 dump 成一个 JSON 文件按会话 ID 和时间戳命名。这样复现问题时直接加载对应文件重跑即可。5.3 失败重放把线上问题搬到本地有了快照就能做重放。写一个脚本读取快照里的 messages重新跑一遍编排循环观察是否复现。如果复现就可以在本地逐步调试如果不复现说明可能是模型随机性导致的需要多跑几次看概率。这个能力在排查偶发失败时特别有用。智能体因为模型有随机性同一个输入可能这次成功下次失败。没有重放机制你根本没法定位。6. 技能复用与扩展从单智能体到多智能体协作当技能积累到一定数量你会自然想到两件事能不能跨智能体复用以及能不能让多个智能体协作。这两个方向都是 agent-skills 概念的延伸。6.1 技能复用把注册表做成共享服务如果每个智能体都自己维护一份技能注册表重复劳动不说还容易出现同一个技能在不同智能体里行为不一致的问题。更好的做法是把注册表抽成一个独立的技能服务所有智能体通过接口查询和调用。这样做的好处是技能只维护一份更新一处全局生效技能的质量可以被统一监控新智能体接入时直接复用已有技能池不用从零开始。当然代价是引入了网络调用延迟会上升。所以我的建议是技能数量少比如 20 个以内时本地注册表就够了超过这个量级再考虑服务化。过早服务化是典型的过度设计。6.2 多智能体协作技能作为协作的接口多智能体协作时技能可以充当智能体之间的契约。比如一个负责数据处理的智能体对外暴露clean_data、aggregate等技能一个负责报告的智能体通过调用这些技能来获取数据。这样两个智能体之间不需要知道对方的内部实现只通过技能接口交互。这种设计的好处是解耦。数据处理逻辑变了只要技能接口不变报告智能体完全不用改。这跟微服务的思想是一致的只不过这里的服务是技能。不过要注意多智能体协作会显著增加调试难度。我建议先用单智能体把技能跑顺确认每个技能都稳定了再考虑拆分。一上来就搞多智能体很容易陷入不知道是哪个环节出错的困境。7. 我在实际项目里总结的几条经验最后分享几条踩坑踩出来的经验都是文档里不会写、但实际做项目一定会遇到的东西。第一条技能的错误信息要写给模型看不是写给人看。技能执行失败时返回的 error 字段模型是会读的。如果你返回Error 500模型完全不知道该怎么办如果你返回城市名称无法识别请提供有效的城市名如北京模型下一轮就可能自己修正参数重试。把错误信息当成给模型的提示来写能大幅提升自愈能力。第二条给技能加幂等意识。有些技能比如写文件、发消息重复执行会有副作用。如果模型因为某种原因重试了可能造成重复操作。我的做法是给这类技能加一个可选的idempotency_key参数相同 key 的重复调用直接返回上次结果。这个细节在演示时看不出来但上线后能避免很多尴尬。第三条技能数量增长后要做分组。当技能超过三四十个全塞进 prompt 会让 token 消耗暴涨模型选择也会变慢变差。这时候可以按领域分组先让模型选组再在组内选技能。两级选择比一级选择更稳也更省 token。第四条定期清理僵尸技能。项目迭代过程中总有一些技能被新技能替代但没删掉。它们留在注册表里不仅占 prompt 空间还可能被模型误选。我一般每个季度过一遍技能列表把近三个月零调用的技能标记出来确认无用后删除。第五条技能要有版本概念。技能的行为可能变化比如查天气的技能从返回摄氏改成返回华氏。如果直接改依赖它的智能体可能就崩了。稳妥的做法是保留旧版本新版本用新名字如fetch_weather_v2让调用方自己决定何时迁移。这些经验听起来琐碎但每一条都是真金白银换来的。agent-skills 这个概念本身不复杂复杂的是把它用稳、用久。希望这些内容能帮你少走点弯路。