
1. 从agent-skills这个命名说起它到底指什么第一次看到agent-skills这个词很多人会下意识地把它理解成某个具体的库或者框架。但如果你真的去翻一圈会发现它更像是一个概念集合而不是一个可以直接pip install的包。这个词拆开来看就是两块agent智能体和 skills技能。合在一起它描述的是一套让智能体具备可调用、可组合、可复用能力的组织方式。我在实际接触这类项目的时候最直观的感受是大多数人把注意力放错了地方。他们一上来就研究用哪个模型用哪个框架却忽略了 agent-skills 真正要解决的核心问题——智能体怎么知道自己会什么、不会什么以及怎么在需要的时候把对应的能力调出来。这个问题不解决模型再强也只是个什么都懂一点、什么都做不精的聊天机器。打个比方。一个刚入职的实习生学历再好如果没有人告诉他公司有哪些系统、每个系统怎么用、遇到什么事该找谁他也没法独立干活。agent-skills 要做的就是给智能体配一本动态的能力手册而且这本手册不是写死的是能根据任务现场组装的。所以这篇内容适合谁看如果你是刚接触智能体开发、被各种框架名词绕晕的人它能帮你把概念理清楚如果你已经在做多工具编排、但总觉得工具一多就乱的人它能给你一套组织思路如果你只是好奇智能体到底怎么干活的普通读者也能从里面的类比和案例里看明白个大概。需要提前说明的是agent-skills 目前并没有一个统一的官方标准不同团队、不同项目对它的实现差异很大。下面我讲的内容是基于这类项目最常见的工程实践做的合理还原和补充不是照搬某个具体仓库。你在实际落地时需要根据自己用的框架做适配。2. 为什么技能这个概念值得单独拎出来2.1 工具调用和技能不是一回事很多人会把技能和工具调用混为一谈。我一开始也是这么想的直到踩了几次坑才反应过来工具是原子操作技能是带上下文的操作组合。举个例子。你给智能体一个查询天气的工具它接收城市名、返回温度。这是工具。但用户问的是我明天要去外地出差该带伞吗这时候智能体需要做的是先确认出差城市、再查天气、再判断降水概率、最后结合要不要带伞这个意图给出建议。这一整套动作才叫一个技能。区别在哪工具是无状态的、单次的技能是有意图的、可能多步的。如果你只给智能体一堆工具它每次都要从零开始想我该先调哪个、再调哪个效率低不说还特别容易在中间步骤跑偏。而技能把常见的组合固化下来智能体只需要判断当前该用哪个技能剩下的编排交给技能自己处理。这个区分看起来是概念游戏但它直接决定了你的系统架构。我见过不少项目把所有能力都平铺成工具列表结果工具数量一过二十模型的选择准确率就断崖式下跌。原因很简单选项太多模型在选哪个这一步就消耗了大量注意力。2.2 技能是给智能体减负的手段接着说上面那个问题。为什么工具一多模型就选不准这里有个很实际的工程约束上下文窗口是有限的而模型在长列表里做选择的准确率会下降。你可以做个简单的实验。给模型 5 个工具让它选它基本不会错。给 50 个工具它开始犹豫、开始选错、开始把两个工具的功能混在一起。这不是模型笨是任务本身的难度上去了——从 50 个选项里挑对的比从 5 个里挑信息量大了十倍。技能机制的价值就在这里。它相当于给工具做了分层第一层是技能数量控制在个位数到十几个第二层是每个技能内部的工具模型在选定技能之后才需要面对这些细节。这样每一层的选择空间都被压缩到模型能稳定处理的范围内。我自己的经验是单层选项控制在 7 到 12 个之间模型的稳定性和灵活性平衡得最好。低于 7 个说明你的技能划分太粗一个技能里塞了太多不相关的东西高于 12 个模型开始出现选择困难。这个数字不是绝对的但可以作为一个调优的起点。2.3 技能让能力可以版本化和灰度还有一个容易被忽略的好处技能是可以独立迭代的。如果所有能力都揉在一个大 prompt 里你改一处可能影响另一处测试起来非常痛苦。但技能是独立的单元你可以单独给某个技能升级、单独测试、单独回滚。这在生产环境里太重要了。举个我遇到过的场景。有个技能负责从一段文本里抽取结构化信息一开始用的是规则匹配后来换成模型抽取准确率上去了但延迟也上去了。因为它是独立技能我可以先在一部分流量上灰度观察延迟和准确率的权衡确认没问题再全量。如果它和别的逻辑耦合在一起这种灰度根本做不了。所以技能这个概念值得单独拎出来不是因为它时髦而是因为它同时解决了选择效率、迭代效率和可维护性三个问题。这三个问题在智能体规模小的时候不明显一旦能力多起来就是绕不过去的坎。3. 一个技能在工程上到底由什么组成3.1 技能描述模型看到的说明书技能描述是模型决定要不要用这个技能的唯一依据。它写得好不好直接决定技能能不能被正确调用。我见过太多项目在这里偷懒技能描述就写一句处理用户请求。这种描述等于没写模型根本不知道什么时候该用它。好的技能描述应该包含三块信息这个技能做什么、什么时候用、什么时候不用。什么时候不用这一条特别容易被忽略但它极其重要。因为技能之间经常有重叠如果不明确边界模型会在两个相似技能之间反复横跳。比如你有一个总结长文的技能和一个提取要点的技能如果不写清楚区别模型可能把该总结的拿去提取要点结果输出一堆零散句子。我的写法习惯是这样的先用一句话说清楚技能的核心动作再用两到三句话说明适用场景最后用一句话点明它和相邻技能的区别。整个描述控制在 100 到 200 字之间。太短信息不够太长模型抓不住重点。3.2 输入输出契约别让模型猜格式技能被调用时需要明确的输入格式执行完需要明确的输出格式。这两端如果不定死后面全是坑。输入这边我建议能用结构化 schema 就用结构化 schema别让模型自由发挥。比如一个发送邮件的技能输入应该是{收件人, 主题, 正文}这样的字段而不是让模型自己拼一段自然语言。结构化输入的好处是校验方便——字段缺了、类型错了在调用前就能拦下来不用等执行到一半才发现。输出这边同理。我踩过的一个坑是技能返回的是自然语言描述结果下游要解析这个描述再决定下一步解析逻辑写得极其脆弱稍微换个措辞就崩。后来改成返回固定字段的 JSON下游直接读字段稳定性立刻上来了。提示如果你的技能输出要喂给下一个技能务必用结构化格式。自然语言在技能之间传递是 bug 的高发区。3.3 执行逻辑同步、异步还是流式技能的执行方式有三种常见形态选哪种取决于任务特性。同步执行最简单调用方等着结果返回。适合那些耗时短、结果必须立刻拿到的技能比如计算两个数的和。异步执行适合耗时长的技能调用方发起后先去干别的结果好了再回来取。流式执行适合输出内容长的技能比如生成一段文案边生成边返回用户不用干等。这里有个经验默认用同步除非你明确知道这个技能会慢。很多人一上来就搞异步结果复杂度上去了收益却没多少。判断标准很简单——如果这个技能 90% 的情况下能在 2 秒内返回就用同步超过这个数再考虑异步或流式。3.4 错误处理技能失败了怎么办技能执行失败是常态不是异常。网络抖动、参数不对、下游服务挂了都可能让技能失败。关键是失败之后怎么办。我的做法是给每个技能定义明确的失败语义。失败分两类可重试的和不可重试的。可重试的比如超时自动重试一到两次不可重试的比如参数错误直接返回错误信息让上层决定怎么处理。还有一点很重要错误信息要写给人看也要写给模型看。因为很多时候技能失败后是模型来决定下一步的。如果错误信息是一串堆栈模型看不懂如果是一句参数 city 不能为空模型就知道该去补这个参数。所以错误信息要包含哪里错了和怎么修两个部分。4. 技能怎么被组织和调度4.1 技能注册表让智能体知道我会什么技能注册表是技能机制的中枢。它记录了当前智能体有哪些技能可用、每个技能的描述是什么、怎么调用。模型在决定用哪个技能时看的就是这张表。注册表的组织方式有两种静态注册和动态注册。静态注册是启动时把所有技能都注册进去简单直接适合技能数量固定的场景。动态注册是根据当前上下文决定加载哪些技能适合技能多、但每次任务只用到一小部分的场景。我倾向于混合使用核心技能静态注册保证随时可用边缘技能动态注册按需加载。这样既保证了基础能力的稳定又不会让注册表无限膨胀。注册表还有一个细节要注意技能的排序会影响模型的选择。模型在列表里做选择时靠前的选项有一定优势。所以我会把最常用、最核心的技能放在前面把那些兜底性质的技能放在后面。这个顺序不是随便排的要根据实际调用数据来调。4.2 技能路由从用户意图到具体技能路由是用户说了句话到该用哪个技能之间的桥梁。这一步做得好不好直接决定用户体验。最简单的路由是让模型直接选把技能列表和用户输入一起给模型让它输出技能名。这种方式实现快但前面说过技能一多就不准。进阶一点的做法是两阶段路由先用一个轻量模型或规则做粗筛把候选技能缩小到三五个再让主模型做精筛。粗筛可以用关键词匹配、向量相似度这些手段成本低、速度快。精筛阶段候选少了模型准确率自然上去。再复杂一点的是多技能路由也就是一个用户请求可能需要多个技能协作。这时候路由就不只是选一个而是选一组并排好序。这种场景我建议用显式的编排逻辑而不是让模型自由发挥。因为多技能协作的失败模式太多显式编排至少能保证顺序可控。4.3 技能编排串行、并行还是条件分支技能编排决定了多个技能怎么配合。三种基本模式串行、并行、条件分支。串行就是 A 完了 BB 完了 C。适合有严格依赖关系的任务比如先查数据、再算指标、最后生成报告。并行是 A 和 B 同时跑都完了再汇总。适合相互独立的任务比如同时查三个数据源。条件分支是根据中间结果决定下一步走哪条路适合有判断逻辑的任务。实际项目里这三种模式经常混着用。我的建议是先用串行把流程跑通再针对瓶颈做并行优化。一上来就设计复杂的并行和分支调试成本极高而且很多并行其实没必要——你以为的瓶颈可能根本不是瓶颈。注意并行不是越多越好。并行会带来资源竞争和结果合并的复杂度。只有当单个技能耗时明显、且技能之间确实独立时并行才划算。4.4 上下文传递技能之间怎么交接技能之间传递的不只是数据还有上下文。比如用户前面说过我要订下周三的票后面一个技能在处理时需要知道下周三这个时间信息。上下文传递有两种做法显式传递和隐式共享。显式传递是把需要的上下文作为参数传给下一个技能清晰但啰嗦。隐式共享是维护一个全局的上下文对象所有技能都能读写方便但容易乱。我的经验是以显式传递为主隐式共享为辅。关键信息显式传避免技能之间产生隐式依赖一些通用的、所有技能都可能用到的信息比如用户 ID、会话 ID放共享上下文。这样既保证了清晰度又避免了每个技能都传一堆重复参数。5. 落地时最容易踩的几个坑5.1 技能粒度过粗或过细粒度问题是技能设计里最难的。太粗一个技能干太多事复用性差太细技能数量爆炸路由压力大。我判断粒度的标准是一个技能应该对应一个用户能理解的完整动作。比如发送邮件是一个完整动作填写收件人不是。用户不会说帮我填写收件人但会说帮我发封邮件。按这个标准划分粒度基本不会太离谱。如果拿不准就先粗一点用着用着发现某个技能内部有明显可以独立出来的部分再拆。反过来如果一开始就拆得很细后面合并起来更麻烦因为调用方已经依赖了那些细粒度技能。5.2 技能描述和实际行为不一致这是最隐蔽的坑。技能描述写的是查询订单状态实际实现里却顺手改了订单备注。模型按描述去用结果产生了意料之外的副作用。避免这个坑的办法是把描述当成契约来维护。技能改了什么行为描述必须同步改。我甚至建议在代码里加一层校验技能的实际输入输出和声明的 schema 对不上时直接报错而不是默默执行。这样能在测试阶段就把不一致暴露出来。5.3 忽略技能的副作用标注有些技能是只读的比如查询有些技能是有副作用的比如发送、删除、修改。这两类技能在调度时应该区别对待。只读技能可以放心重试、可以并行、可以缓存。有副作用的技能要谨慎重试可能导致重复发送并行可能导致顺序错乱缓存更是绝对不能做。我的做法是在技能元数据里加一个side_effect标记调度器根据这个标记决定策略。有副作用的技能默认不重试、不并行、不缓存除非显式声明可以。这个标记看起来简单但能避免很多生产事故。5.4 技能版本管理缺失技能是会迭代的。今天这个技能返回三个字段明天可能变成四个。如果调用方硬编码了字段数量升级就会出问题。我建议技能从一开始就带版本号调用时指定版本。新版本上线后老版本保留一段时间给调用方迁移的时间。这样升级是平滑的不会因为一个技能改动导致整个系统崩掉。版本号怎么定我习惯用语义化版本大版本号变了说明有不兼容改动小版本号变了说明只是加了功能。调用方可以指定我要 1.x 的最新版这样小版本升级自动享受大版本升级需要手动确认。6. 从零搭一个最小可用的技能系统6.1 先定义技能的数据结构不管用什么语言技能的数据结构大概长这样class Skill: name: str # 技能唯一标识 description: str # 给模型看的描述 input_schema: dict # 输入参数的结构定义 output_schema: dict # 输出结果的结构定义 side_effect: bool # 是否有副作用 version: str # 版本号 handler: callable # 实际执行逻辑这个结构不复杂但每一块都有用。description给模型看input_schema和output_schema做校验side_effect给调度器看version做兼容handler干活。我建议先把结构定下来再往里填技能。很多人反过来先写一堆技能再想怎么组织结果结构被技能带着走越改越乱。6.2 写一个最简单的路由最小可用的路由不需要多复杂把技能描述拼成一段文本和用户输入一起给模型让它输出技能名就行def route(user_input, skills): skill_list \n.join( f- {s.name}: {s.description} for s in skills ) prompt f根据用户输入从下面的技能中选择最合适的一个。 可用技能 {skill_list} 用户输入{user_input} 只输出技能名不要输出其他内容。 return call_model(prompt)这段代码很朴素但能跑。技能少的时候完全够用。等技能多了、准确率下降了再考虑加粗筛、加向量检索这些优化。6.3 执行和错误处理路由选出技能后就是执行def execute(skill_name, params, skills): skill find_skill(skill_name, skills) if not skill: return {error: f技能 {skill_name} 不存在} try: validate(params, skill.input_schema) result skill.handler(**params) validate(result, skill.output_schema) return {result: result} except ValidationError as e: return {error: f参数校验失败{e}} except Exception as e: return {error: f执行失败{e}}这里的关键是两道校验执行前校验输入执行后校验输出。输入校验拦住参数错误输出校验拦住实现 bug。两道都过了才把结果返回给上层。6.4 把上下文串起来最后是把路由、执行、上下文串成一个完整流程def run(user_input, skills, context): skill_name route(user_input, skills) params extract_params(user_input, skill_name, context) result execute(skill_name, params, skills) context.update(result) return result这个流程简单到有点简陋但它包含了技能系统的全部核心环节路由、参数抽取、执行、上下文更新。你可以在这个骨架上加东西——加重试、加日志、加监控、加多技能编排——但骨架本身不用变。我自己的习惯是先用这个最小版本跑通一个真实场景比如查天气或者发提醒确认整条链路没问题再往上加复杂度。一上来就设计大而全的系统往往跑不起来。7. 技能系统跑起来之后怎么调优7.1 用调用数据反推技能划分系统跑一段时间后你会积累一批调用数据哪个技能被调得多、哪个技能经常被误选、哪个技能调用后用户不满意。这些数据是优化技能划分的最好依据。我一般会看三个指标调用频次、误选率、任务完成率。调用频次低的技能要么是描述没写好要么是这个能力根本不需要误选率高的技能说明它和相邻技能的边界不清任务完成率低的技能说明实现有问题或者粒度不对。根据这些指标调整技能划分比拍脑袋想有效得多。我见过一个项目原本把查询和筛选分成两个技能结果误选率一直很高。后来合并成一个查询并筛选的技能误选率立刻降下来了。这就是数据驱动的调整。7.2 描述文案的 A/B 测试技能描述是可以做 A/B 测试的。同一批请求一半用旧描述一半用新描述对比调用准确率。这个方法听起来有点重但对于核心技能值得做。我做过一次这样的测试一个总结技能旧描述是对文本进行总结新描述是将长文本压缩成三到五句话的核心要点保留关键数字和结论。新描述的调用准确率比旧描述高了将近二十个百分点。原因很简单新描述给了模型更明确的判断依据。所以别小看描述文案它是零成本的优化手段。改几个字可能比改代码效果还好。7.3 什么时候该拆技能什么时候该合拆和合是技能系统里永恒的话题。我的判断标准是看调用模式。如果两个技能经常被连续调用说明它们其实是一个动作的两半应该合并。如果两个技能经常被混淆说明它们边界不清要么合并要么把描述改清楚。如果一个技能内部有明显的分支不同分支的逻辑差异很大说明该拆。拆和合没有绝对的对错关键看是否降低了系统的整体复杂度。拆完之后路由更准了、维护更方便了就是对的拆完之后技能数量爆炸、调用关系更乱了就是错的。7.4 监控和告警怎么设技能系统上线后必须有监控。我关注的核心指标有四个调用量、成功率、延迟、误选率。调用量突然掉了可能是路由出了问题成功率掉了可能是某个下游服务挂了延迟上去了可能是某个技能变慢了误选率上去了可能是新加了技能导致选择空间变大。这四个指标任何一个异常都值得查一查。告警阈值不要设得太敏感否则天天报警人就麻木了。我的做法是先观察一周摸清正常波动范围再把阈值设在正常范围之外。这样报警了就是真有问题不会被噪音干扰。8. 关于 agent-skills 我的一些个人体会做这类系统这几年我最大的体会是技能系统的难点不在技术在克制。技术上路由、编排、上下文传递这些都有成熟的方案照着搭就行。真正难的是忍住再加一个技能的冲动。每加一个技能系统的复杂度就上去一点路由的准确率就下降一点。很多时候不加那个技能用现有技能组合一下效果反而更好。另一个体会是描述比实现重要。我见过太多项目实现写得漂漂亮亮描述写得一塌糊涂结果模型根本不会用。技能是给模型用的模型只能通过描述来理解技能。描述写不好实现再好也白搭。最后一个体会是先跑通再优化。技能系统很容易陷入过度设计——想着要支持并行、要支持嵌套、要支持动态加载结果核心流程还没跑通。我的建议永远是先用最简单的版本跑通一个真实场景有了真实数据再决定优化什么。没有数据支撑的优化都是自嗨。这套东西没有标准答案不同场景、不同团队的做法差异很大。上面这些是我自己踩过坑之后总结出来的不一定对但至少是真实跑过的。你在落地的时候结合自己的实际情况调整就行。