1. Skill 和 Tool:从一次调用到一种行为能力
最近身边聊 Agent 开发的朋友越来越多,大家讨论中几乎绕不开两个词:Tool 和 Skill。很多人一开始会觉得,这不就是同一个东西换了个叫法吗?Tool 是函数,Skill 是工具包,反正最终都是让大模型去调用外部能力。但实际上,这个理解恰恰是后面一系列架构困惑的根源。
我在团队里负责 Agent 服务的基础框架搭建,早期做 Function Calling 的时候,脑子里全是 Tool 那套逻辑。后来接触了 Claude Code、Codex 里对 Skill 的处理方式,再回头看自己写的代码,才发现 Skill 和 Tool 根本不是粒度大小的区别,而是架构定位的区别。Tool 解决的是“模型怎么调用一个函数”,Skill 解决的是“Agent 怎么学会一种行为”。这两者的边界如果不划清楚,技能库设计得再花哨,用起来也是别扭。
拿实际例子来说。你给 Agent 挂一个query_weather(city: str)的 Tool,模型会在需要天气时构造一个 JSON 参数,调这个函数,拿到结果,继续往下走。这是标准的 Function Calling 流程,清晰、直接、没有状态。但如果你让 Agent “组织一次周末户外活动”,它需要的不是一个函数,而是:先查天气、再找场地、评估距离、规划时间、生成清单。这里面涉及多个工具的顺序编排、条件分支、结果依赖,还有失败时的替代方案。你把这些逻辑全写进一个 Tool 里?可以,但那就是个巨型函数,耦合度极高,换个场景就废了。Skill 的定位恰恰是把这一整套“怎么把多个动作组织成一个行为模式”封装起来。
再通俗一点讲。Tool 是“零件”,Skill 是“工艺流程”。零件可以单独采购、单独使用,但工艺流程决定了零件怎么组合、按什么顺序运转、出问题怎么切换备用路线。一个足够成熟的 Agent 不能只有一堆零件,它得具备一套又一套可复用的工艺流程,这正是技能库存在的意义。
理解了这层差异,你就会明白为什么很多做过 Tool 开发的工程师,真正动手写 Skill 时觉得不对劲——因为他们还在用“函数封装”的思维做“行为封装”。这个思维切换不完成,后面说的自演进和动态加载,全都落不了地。
2. Skill 和 Tool 在工程实现上的五个核心差异
我整理了自己在工程里的实际感受,把 Skill 和 Tool 的差异拆成五个维度。这些不是理论推演,都是代码里能看到的实际问题。
接口形态,也就是模型和系统怎么交互。Tool 通常是一段 JSON Schema,参数写死,模型按照 schema 填参数。Skill 则不一样,它除了参数声明,还包含一段执行逻辑,甚至是一段 prompt 模板。Skill 的输入往往不是一个严格结构的 JSON,而是一个自然语言目标,Skill 内部自己去解析、规划、调用底层 Tool。
粒度与复用单元。Tool 的粒度通常很小,一个函数干一件事。Skill 的粒度是一个完整任务闭环,例如“导出周报数据并整理成表格发送到群聊”。Tool 复用的是函数,Skill 复用的是一整套行为模式。Skill 内部也会调 Tool,但它对上层暴露的是一个能力单元,而不是一个接口。
状态管理。Tool 默认是无状态的,被调用、返回、结束,模型自己维护上下文。Skill 不一样,它一定自带状态。一个 Skill 执行过程中会临时记住中间结果、当前步骤、失败次数、剩余资源。Skill 的设计必须考虑状态生命周期,而 Tool 基本不用想这件事。
失败处理与自适应。Tool 失败就是抛异常,模型收到错误信息再换策略。Skill 则必须内建兜底逻辑。比如“查天气”这个 Tool 失败,模型可以换个源再查;但“规划户外活动”这个 Skill 走到一半发现天气接口挂了,它不能直接把自己内部的流程抛给上层模型,而应该自己尝试替代方案,或者降级处理。
学习与沉淀机制。Tool 是死的,它的成长完全靠开发者手动迭代。Skill 可以具备某种“记忆”,当它自己执行过程中发现某个步骤经常失败、某个分支更高效时,可以更新内部模板、调整决策策略,甚至生成新的参数约束。这种自我修正能力,就是技能库自演进的起点。
五个维度说完,大家应该能看出来一个基本判断:Tool 是基础设施,Skill 是建立在基础设施上的“行为封装”。当你的 Agent 只是解决“查一下、算一下、调一下”这类单点问题时,只用 Tool 就够了。但当你希望 Agent 能稳定地完成“多步骤、带决策、要容错”的复杂任务时,没有 Skill 层,代码会膨胀到不可维护。
我团队里有个真实教训。早期我们做了五十多个 Tool,每次需求变更要从头梳理调用链路,谁依赖谁、谁失败会连累谁,痛苦得不行。后来我们把 Tool 收敛成二十个,在这之上封装了八个 Skill,反而逻辑清晰多了,因为大多数业务场景,Agent 只需要“判断该用哪个 Skill”,而不是“现场编排一堆 Tool”。
3. 技能库的自演进机制:从一次执行到一套方法
“自演进”这个词听起来很玄,很多人第一反应是模型自己写代码、自己改系统,觉得不安全也不可控。实际上,工程上说的自演进,远没有那么激进,它指的是技能在执行中不断沉淀、修正、迭代的机制。这里我想讲两个层级:会话内的动态沉淀,和跨会话的经验迁移。
先说会话内。Agent 执行一个 Skill 时,会遇到各种没有预见到的情况:某个上游接口返回了意料之外的数据格式、某个步骤连续失败三次、用户中途改变了需求方向。传统 Tool 架构下,这些情况都是模型通过上下文“临场发挥”来处理,处理完就忘了。Skill 则可以在内部维护一个“本次执行记录”,把成功路径、失败分支、兜底动作记下来。
举个例子。我们有一个 Skill 叫“生成项目周报”,它需要从多个数据源拉取数据,再汇总成表格。第一次执行时,数据源 A 返回的字段是completed_tasks,数据源 B 返回的是finishedTasks,模型发现了这个不一致,并在生成周报时做了字段映射。如果这是一次普通的 Tool 调用链,这个映射逻辑就丢了,下次执行还得重新发现一遍。但我们把这次的字段映射记录在了 Skill 的执行数据里,下次执行时直接复用,甚至可以通过规则更新到 Skill 的全局配置中。这就是最简单的“自演进”:系统自己把一次偶然的成功经验固定成了默认行为。
跨会话的经验迁移更进一层。每一次 Skill 执行完毕,系统会生成一份“技能执行小结”,包含本次执行的决策路径、遇到的异常、用户反馈、最优参数组合。这些小结聚合起来,形成这个 Skill 自己的“经验库”。
当经验积累到一定程度,可以触发技能版本升级。升级不是改代码,而是改 Skill 的决策规则。比如某个分支策略在一百次执行里成功了九十五次,那它就应该被提升为默认策略;某个步骤经常在深夜执行时报错,那 Skill 就会在深夜时自动切换备用路径。整个过程不需要人改一行代码,而是 Skill 在“使用中自己学会了更优的做法”。
很多做 Agent 的团队听到这里会担心失控。我的建议是设置清晰的自演进边界:只允许 Skill 调整内部策略参数、模板内容和分支逻辑,不允许它修改 Tool 的功能定义,不允许它绕过权限校验。自演进的“自”,是在一个沙箱里完成的,外面有护栏。这样既保留了自适应能力,又不会让系统越跑越离谱。
在 Claude Code 这类产品里,你已经能看到这种机制的雏形。那些项目里沉淀下来的 CLAUDE 指令、技能描述、命令约定,很多并不是开发人员一次性设计出来的,而是在使用过程中逐步修正、补全的。本质上,这就是一个小范围、人工参与的自演进。未来的方向,是把人工参与的部分也自动化,让系统自己完成“使用经验 -> 知识沉淀 -> 行为升级”的闭环。
4. 动态加载的落地姿势:目录协议、清单注册与热更新
技能库不是一个文件夹里堆一堆 Skill 就完事了。它要支撑真正的 Agent 应用,必须解决三个问题:怎么识别一个 Skill、怎么按需加载一个 Skill、怎么在不重启服务的情况下更新 Skill。
4.1 定义一个容易识别的 Skill 目录结构
我推荐的设计是把每个 Skill 作为一个独立目录,目录内固定放几个文件:SKILL.md写技能的功能说明、输入输出描述和使用场景,actions/放内部执行步骤的定义,templates/放 prompt 模板,assets/放少量静态资源,meta.json记录元数据。这套结构借鉴了社区里通用的 Agent Skill 规范写法,好处是工具链统一、易于做静态扫描。
SKILL.md的头部一定要有结构化的 YAML front matter,包含 name、description、when_to_use、version、dependencies 这些字段。description 尤其重要,它决定了 Agent 能不能在正确的时候想到这个 Skill。我见过很多技能描述写得含糊其辞,结果就是模型东拼西凑,该用的 Skill 一次没用上。
4.2 启动时扫描注册,用时才真正加载
动态加载不等于把所有 Skill 一股脑装进上下文。上下文窗口永远是不够用的,必须按需加载。
我们的做法是两层:启动时只做“注册”,把所有 Skill 的元信息(名称、描述、适用场景、依赖关系)读进一个索引表;真正执行时,才根据任务内容做匹配,把命中的 Skill 完整加载进来,包括它的模板、步骤定义、参数约束。
这套机制的关键是索引的匹配质量。我们做过测试,在五十个 Skill 的项目里,如果只用关键词匹配,正确率大概七成左右。后面改成用向量化检索加规则兜底,正确率能提到九成五以上。具体做法是:对每个 Skill 生成一段“能力描述向量”,对当前任务生成“任务意图向量”,算相似度,取Top-K候选,再用规则模型做一轮精排,决定真正加载哪几个 Skill。
4.3 热更新机制:版本号就是照妖镜
动态加载的进阶要求是热更新,业务变化了,技能库不能每次都发版重启。我们用的是“目录版本号+加载代理”的组合。
每个 Skill 目录里有个meta.json,里面记录version字段。Agent 运行时,加载代理会监听目录的变更事件,发现版本号变化就重新加载该 Skill 的元信息。已经执行到一半的技能实例不受影响,它会继续跑完本次任务,但是下一次调用就会走新版本。这是业内比较标准的“新旧共存、自然切换”模式,避免了强行中断带来的脏数据。
热更新本身不难,难的是配置一致性和依赖完整性。我们踩过一个坑:更新一个 Skill 之后,忘了检查它的依赖 Tool 是否仍然存在,结果 Skill 加载成功,运行到一半调不到 Tool,直接报错。后来我们加了个启动自检流程,每次检测到 Skill 目录变更,就自动做一次依赖完整性扫描,有问题直接拒载,并输出详细报告。这套机制跑下来,线上事故率降了非常多。
5. Skill 路由与加载代理的设计:我踩过的坑和填平的井
动态加载看起来只是“扫目录、读配置、拉文件”,真正跑起来才发现,这背后还有一层路由逻辑,也就是“任务来了,怎么决定用哪个 Skill、要不要多个 Skill 协作”。我们把这块单独做成一个组件,叫 Skill Router,它经历了三次比较大的重写,中间踩过不少坑,应该能帮大家少走弯路。
第一版是纯规则路由。每个技能注册时写死一组触发关键词,任务来了做字符串匹配。这个方案实现快,但可用性很差。用户的表达千变万化,关键词稍微改个说法就命中不了,最后 Agent 经常自己乱猜,效果很拉胯。
第二版是模型权重路由。我们不再靠规则,而是把候选人 Skill 的元信息塞给模型,让模型自己判断该调哪些。这种方式灵活性高,但代价是增加了每次任务的前置判断延迟,而且模型往往会选多,把不需要的 Skill 也带进来,上下文浪费严重。实测下来整体 cost 上升了接近三成,效果提升有限。
现在用的是混合路由。先用向量检索缩小候选集,再用模型做最终决策,最后用规则强制约束。比如一个任务如果已经是产品经理明确提出的“周报生成”意图,那规则层会直接锁定“生成周报”这个 Skill,不走模型判断,既快又准。如果是模糊任务,才让模型在候选集里做判断。这一步优化之后,路由的准确率、响应速度、性价比都到了可接受的水平。
另外一个被很多人忽略的点是并发加载的锁与缓存。Agent 是并发处理多个会话的,如果两个会话同时触发了同一个 Skill 的加载,程序容易重复读取文件、重复解析模板,严重的会互相踩到正在写的缓存。我们的处理方式很简单:给每个 Skill 的加载过程加一个按目录维度的锁,同一时刻只有一个 goroutine 负责加载,其余请求复用第一个加载完成的实例。这个优化做完,再也没出现过 Skill 加载阶段的数据竞争报错。
6. Skill 的安全边界与权限校验:动态能力必须锁进笼子里
技能库做到最后,一定会面对安全模型的问题。一个可以自演进、动态加载的 Skill 系统,本质上就是一堆可以执行动作的代码。不把安全边界画清楚,Agent 就是一条脱缰的野狗。
我们设计的权限模型分三层:Skill 本身、Skill 调用的 Tool、Skill 产生的副作用。
Skill 本身要有权限声明。它在meta.json里写清楚“我需要访问哪些资源、哪些数据域、哪些外部系统”。系统在加载 Skill 时会检查它的声明和当前的授权策略(我们用的是 ABAC,属性化的访问控制),不匹配就不加载。
Tool 层是真正的闸门。Skill 执行业务逻辑时,对每个 Tool 的调用都要做逐次鉴权,不允许 Skill 拿着一次授权到处乱用。比如一个 Skill 被授予了读权限,它就绝不能发起写操作,哪怕它代码里有改文件的逻辑,到了 Tool 的闸口也会被拦下来。
副作用层管的是 Skill 操作后的影响:是否允许写入数据库、是否允许发消息到外部群、是否允许修改项目配置文件。这些动作通常会记录进审计日志,方便事后追责。
自演进机制本身也纳入审计范围。Skill 每次自我更新,系统都会留存一次“演进快照”,包括改了哪些参数、为什么改、依据的是哪几次执行的统计数据。这套机制让我们在出问题时能快速回滚,而不至于“Skill 自己救了它自己、也顺便隐藏了它犯的错”。
安全策略需要一个核心原则:能力越大,校验越严。允许自演进的 Skill,权限面一定要收窄。那种既能改自己逻辑、又能随便调文件接口的 Skill,在真实项目里就是一颗定时炸弹。
7. 从零实现一个 Skill 的完整步骤:以“生成产品周报”为例
翻过概念讨论,落到代码层面。我们团队近期刚好把一个高频功能“生成产品周报”从 Tool 链改造成了独立 Skill,全套流程跑下来很有代表性,可以完整拆解给大家参考。
7.1 先定义元数据
每个 Skill 的起点是meta.json。我们定义这个技能的版本、适用范围、依赖资源和权限声明:
{ "name": "product_weekly_report", "version": "1.2.0", "description": "生成产品团队周报,汇总项目进度、风险、下周计划", "when_to_use": [ "用户要求生成周报", "用户要求汇总项目进度与风险", "每周五的例行汇报场景" ], "dependencies": { "tools": ["jira_query", "wiki_fetch", "table_render"], "skills": [] }, "permission": { "domains": ["jira", "wiki"], "write": ["cache"], "notify": [] } }这个文件只做声明,不写逻辑。真正执行的内容放在SKILL.md和actions/下面。
7.2 编写行为定义
SKILL.md是给 Agent(准确地说是给路由器和执行引擎)看的执行手册。核心内容不是让人阅读的行文,而是结构化的执行规则:
# 产品周报生成技能 ## 输入 - 时间范围(默认最近7天) - 项目列表(可选,默认全部项目) ## 执行流程 1. 用 task_router 确认输入时间范围,如果缺失则取周一至周五 2. 调用 jira_query 获取各项目进度,字段映射: - `completed` -> 已完成 - `in_progress` -> 进行中 3. 调用 wiki_fetch 获取本周重大变更和风险记录 4. 合并数据,生成周报目录结构 5. 调用 table_render 输出 Markdown 周报 ## 异常处理 - jira_query 超时:重试一次,仍失败则使用本地缓存的最近3天数据 - wiki_fetch 返回空:跳过该章节,在周报中标记"本周无重大变更" - 最终表格渲染失败:返回纯文本列表格式,保证内容可用 ## 优化经验 - 若同一周内用户连续两次要求“只看重点”,后续生成可自动精简为摘要模式看到没有,异常处理和优化经验都在这里。这已经不是普通意义上的“工具调用说明书”,而是一个自带应变逻辑的小型的执行智能。
7.3 实现加载与注册
服务启动时,我们的加载器会扫目录、解析meta.json、校验权限声明和依赖完整性,然后把这个 Skill 加入索引。这一步的代码逻辑我放一段简化示意,展示核心思路:
# skill_loader.py def load_skill_dir(skill_path): meta = json.load(open(skill_path / "meta.json")) skill_id = meta["name"] if not check_permission(meta["permission"]): raise PermissionError(f"Skill {skill_id} lacks required permissions") missing = check_dependencies(meta["dependencies"]["tools"]) if missing: raise DependencyError(f"Missing tools: {missing}") skill = SkillRegistry.register( skill_id=skill_id, version=meta["version"], description=meta["description"], when_to_use=meta["when_to_use"], executor=SkillExecutor.compile(skill_path) ) return skill要重点说的步骤是SkillExecutor.compile。它做的事是把SKILL.md里的执行流程编译成可执行的任务链,而不是每次执行时重新解析一遍 Markdown,那样性能不够。编译结果会被缓存,只有版本号更新后才会重新生成。
从定义元数据,到写行为规则,到实现加载器,这个小而完整的流程支撑了真实业务里每天上百次的调用。这也验证了 Skill 的工程化并不是一个虚的概念,它的每一步都有可以落地的代码形态。如果你正准备在团队里推行技能库,我建议第一步不是设计宏大框架,而是先挑一个频率高、链路中等复杂的任务,老老实实写成 Skill,然后把这一套跑通。信心就是这么一点点建立的。