Agent技能层设计与实践:从工具调用到可编排的agent-skills 最近在折腾个人 Agent 项目的时候我发现自己陷入了一个很尴尬的状态模型已经能理解用户要什么也能把任务拆成步骤可一到真正执行就频繁出错。要么调错能力要么参数缺字段要么两个能力抢同一个活。痛定思痛之后我把这套问题收敛到了一个词上——agent-skills。简单说就是在模型和工具之间补上一层可复用、可评测、可编排的“技能”模块。这篇文章就是我这段时间实践下来的完整记录包括技能的定义、落地、踩坑和迭代希望能帮到正在做 Agent 工具接入的朋友。1. 技能库不是工具列表agent-skills 到底想解决什么很多人一听说 Agent 技能第一反应是“不就是把 API 包装成函数给模型调用吗”。一开始我也是这么想的直到做模拟项目 X 的时候被一个非常简单的场景打脸用户说“帮我看看这件衣服有没有优惠”模型确实调用了查价接口但它把折扣率当成固定值传了进去最后算出来的价格跟页面显示完全不一致。问题的根子不在于模型笨而在于我交付给模型的不是一个“技能”而是一个没有任何使用边界的裸函数。1.1 从一次多步任务失败说起模拟项目 X 做的事情很简单用户输入一段商品描述Agent 需要完成价格查询、优惠计算、库存判断、生成推荐语四件事。我把四个能力都做成独立接口然后像填表格一样把接口列表丢给模型去选。结果线上的失败率简直让人崩溃最典型的一种失败是模型先调用优惠计算再调用价格查询顺序反了算出来的优惠金额毫无意义。这就是典型的“只有工具没有技能”的状态。工具描述的是“我能做什么”技能描述的是“在什么场景下、按什么顺序、用什么约束来做”。后来我重新设计把“商品优惠分析”整体作为一个技能封装起来内部决定是先查价还是先算折扣对外只暴露一个输入项“商品标识”。模型只需要判断用户是不是在问优惠而不需要理解优惠计算里的业务规则准确率一下子提上来了。这个案例给我最大的启发是技能是一种把复杂决策封装起来的边界单元它不应当是模型手里的最后一环而应当是连接“模型意图”和“业务能力”的中间层。模型负责决定“要不要用”技能负责保证“用了能对”。1.2 技能与工具、插件、流程的边界到底在哪动手设计之前我花了不少时间区分几个相近的概念工具、插件、流程、技能。工具是最小可执行能力比如发一个 HTTP 请求、算一次哈希插件是工具的集合通常和某个外部系统绑定流程是固定的步骤序列几乎不给模型决策空间而技能恰好站在它们中间它把若干工具组织成一个有语义的单元同时允许模型在触发时传入意图相关的参数。我用这样一张对照表来帮助团队确定边界类型粒度模型参与度典型例子工具细低发送请求、读取文件插件中低第三方平台适配器技能中高高商品优惠分析流程高低订单定时处理脚本判断一个能力到底算不算技能我现在的标准很简单它是否包含“语义理解”的成分。如果只是机械操作那就是工具如果要在多个工具之间做选择、做顺序安排并且这种选择依赖用户自然语言中的隐含信息那它就应该被抽象成技能。把这个想清楚之后agent-skills 的整个架构就顺了——先有边界再有实现最后才是测试。2. 拆解技能的第一步把“会做”变成“可调用”一旦确定某个能力值得做成技能第一个要面对的问题就是该怎么定义它的样子才能让模型在几百个技能里准确命中它并且填对参数。这块做不好后面所有优化都是空中楼阁。我见过很多人写技能描述时只写一句“获取订单信息”结果模型在用户问物流的时候调了它在用户问退款的时候也调了它——因为描述里根本没有写清“适用边界”。2.1 输入输出契约让模型知道何时用、怎么用技能的定义必须包含两部分触发条件和输入输出契约。触发条件决定“什么时候用”输入输出契约决定“用了以后怎么填、怎么处理”。我在模拟项目 X 里用的是一套轻量级声明规范每个技能包含名称、描述、输入参数、输出格式和副作用标记。下面的示例是我给“计算折后价”技能写的原始定义skill( namecalculate_discount, description计算商品促销折扣后的到手价。当用户询问优惠、打折、降价、划线价时可以调用。 如果用户没有给出折扣率先调用 lookup_promotion 获取不要默认使用 0.8。, inputs{ price: {type: number, description: 商品原价单位元, required: True}, discount: {type: number, description: 折扣率0.1 到 1.0 之间, required: False} }, output{type: number, description: 折后价保留两位小数}, side_effectFalse ) def calculate_discount(price: float, discount: float | None None) - float: actual_discount discount or 0.8 return round(price * actual_discount, 2)一个很容易被忽略的细节是side_effect。这个字段用来告诉模型调用这个技能会不会改变外部状态。如果会模型在生成回答之前通常需要先向用户确认如果不会模型可以直接执行并把结果当作事实依据。很多失败案例比如“模型擅自帮用户下单”“模型反复调用查询接口制造大量无效流量”根源都是没有标清副作用。标注这个字段是我在梳理技能契约时收获最大的一个习惯。2.2 技能描述怎么写才能提高触发准确率技能描述是我踩过最多坑的地方。早期我写得很简略像数据库里的字段备注一样导致模型触发准确率只有六成左右。后来我总结了三个技巧效果立竿见影。第一列出典型用户问法而不是抽象业务术语。比如“查询天气”就明显不如“当用户询问温度、下雨、风力、穿衣建议时使用”来得直观。模型是在语言空间中做匹配的你要用它的语言习惯去喂它。第二把“不要用什么”写进去。我见过有人在描述里写一堆正向场景但模型还是会在语义模糊时误触法。后来我在描述末尾追加了负例比如“不要用于已下单订单的物流咨询”误触率立刻下降。模型其实很擅长读约束前提是你得给它约束。第三把技能之间的边界写清楚。比如同时有“查订单”和“查物流”两个技能时必须在描述里明确区分“查订单返回订单商品和金额”“查物流返回配送轨迹和预计到达时间”。不要指望模型自己区分类似概念你要替它划清。这些看起来像文案工作实际上就是在给模型做“预训练”之外的最后一道调校。技能描述写得越清楚后面排查模型调用问题花的时间就越少。3. 技能实现的技术选型与集成细节定义清楚之后就要考虑真实环境里的技能实现。这个阶段有两个问题最让人头疼一是技能该用什么形态暴露给模型二是技能运行时怎么隔离、怎么保证安全。我在项目早期采取了“把所有技能直接塞进同一个进程”的做法结果一个技能里的内存泄漏能把整个 Agent 拖垮从那以后我老老实实做了隔离设计。3.1 函数式技能与声明式技能的选择实际落地时技能的暴露形态主要有两类一类是函数式技能直接让大模型产生函数调用参数由模型生成另一类是声明式技能模型先生成一个结构化的意图再由一个调度器去映射到具体执行体。两种模式我都测试过结论是看你对模型可控性的要求。函数式技能在主流模型上接入快模型理解力强时几乎不用额外开发。缺点也很明显模型输出的参数容易缺类型、缺必填项而且一旦技能数量超过几十个模型自己挑技能的错误率会显著上升。声明式技能需要多写一层“意图映射”相当于把一部分判断逻辑从模型手里拿回到代码里更稳定但前期成本更高。我自己实践下来的折中方案是对高频、低风险、参数少的场景用函数式技能对低频、高影响、参数复杂的场景用声明式技能。比如“计算折扣”用函数式“发起退款”用声明式。本质上是把风险大的操作交给程序来做最终审核而不是完全信任模型的即时判断。3.2 环境隔离、依赖管理与安全边界技能一旦多起来环境隔离就无法回避。在最朴素的做法里每个技能运行在一个独立的子进程或轻量级容器中只允许通过白名单方式访问外部服务。我当时给技能框架设计了一个基础运行时每个技能都声明自己的依赖包和允许访问的接口列表启动时由管理器统一校验。下面是我在模拟项目 X 里使用的运行时配置片段{ skill_id: calculate_discount, runtime: { type: subprocess, timeout_seconds: 10, max_memory_mb: 256, allowed_domains: [api.example.com], allowed_files: [] }, environment: { PYTHONPATH: /skills/calculate_discount/src, LOG_LEVEL: INFO } }这个配置看起来简单但帮我挡掉了很多“看不见的坑”。比如某个技能依赖的第三方库版本和其他技能冲突隔离后就不会互相污染再比如某个技能的 API 密钥不小心被模型输出到了日志里隔离环境的权限收紧也能防止泄露。不要嫌这一步麻烦技能规模超过 10 个以后环境隔离就是保命用的。安全边界还有一个容易被忽视的点技能超时。模型生成内容的速度很快但下游接口不一定快。我发现如果不设置超时某次第三方接口迟迟不响应整个 Agent 会卡住十分钟用户体验极不。后来统一加了超时和重试策略每个技能默认 10 秒上限超过就返回一个“我还在尝试中”的兜底回答至少不会让对话一直僵着。4. 实测中踩过的坑技能触发失败的根因排障即便定义、实现都做完了真正上线你会发现问题不是没有而是以你想不到的方式冒出来。我在联调阶段记录了三个最典型的问题链路每一个都花了不少时间排查这里直接分享排查思路你可以按这个顺序去定位自己的问题。4.1 问题一模型选了技能但参数一塌糊涂现象用户说“帮我算一下这个 99 块的雨伞折后多少钱”模型正确选中了calculate_discount但传入的discount字段是字符串打折price字段还是空。最初我以为是模型参数生成能力不行后来打印出模型的完整请求才发现问题出在技能定义里没有明确说明“如果折扣率未知应该先调用另一个技能”。这一类的根因几乎都是“技能描述里缺少补全逻辑”。解决方式有两个一是在描述里增加“在调用此技能前若缺少必填参数请先调用 xxx 技能获取”二是把参数补全逻辑移到技能内部也就是让技能函数自己根据price去查优惠规则而不是依赖模型传discount。我后来更倾向于后者因为模型处理多步调用的失误率仍然比代码高不少。排查这类问题时一定要把模型的原始调用输出打出来。不要只看最终回答好不好要看它生成的参数结构。很多“模型很蠢”的结论其实是技能设计没有把上下文喂够。4.2 问题二多个技能互相抢占同一种意图现象用户说“我不想要这件了”结果模型同时触发了“退货申请”和“取消订单”两个技能差点把用户订单状态搞乱。这种问题在技能数量变多以后非常常见因为不同技能的描述里有相似的关键词。我的排查方法是先做一个意图冲突矩阵把每个技能描述里的核心触发词抽出来两两一比凡是相似的都要处理。处理方式有三种合并成一个技能在描述里互相加排除语句或者在调度层加优先级规则。对于“退货申请”和“取消订单”我最终的方案是合并成一个“售后处理”技能内部再根据订单状态决定走哪条分支。模型只需要做一次判断剩下的分支判断交给代码。这个坑让我意识到技能设计不能只从业务功能出发还要从模型视角出发。模型的判断依赖语言相似度如果两个技能在语言空间里长得太像你就要主动替它把边界收敛好。4.3 把排查过程沉淀成可复用的测试集上面这些问题靠临时分析是不能长治久安的。我后来建了一个“手工标注测试集”把用户最常见的 200 条自然语言输入收集起来每条都标好期望触发的技能和参数。每次调整技能定义都跑一遍这个测试集看触发准确率和参数正确率的变化。这个测试集不需要一开始很庞大关键是覆盖三类情况清晰意图、模糊意图、跨技能意图。清晰意图用来防止回归模糊意图用来观察模型是否乱猜跨技能意图用来验证冲突处理。跑完以后我只看两个指标技能命中率和参数有效百分比。任何一次修改如果让这两个指标下降我都不会让它上线。这不是团队流程这是我个人保持在真实场景里不跑偏的基本纪律。5. 多技能编排从单技能到技能链当技能数量突破两位数单技能调优已经不够了。真正让 Agent 像“有经验的人”而不是“按钮机器”的是技能之间的编排逻辑。所谓编排不只是让模型决定先调谁后调谁更包括在多个技能都有可能成立时的取舍。5.1 编排器怎么判断下一步调用哪个技能我在模拟项目 X 里尝试过三种编排策略纯模型决策、规则前置过滤、模型与规则的混合决策。纯模型决策最灵活但不可控规则前置过滤太死板用户话术一变化就失效混合决策最让我满意先用轻量规则做一轮候选筛选再让模型在候选集里做选择。具体做法是这样的所有技能的描述先被转化成一段摘要存入一个用于快速匹配的索引。当用户输入进来时系统先做一个粗召回挑出 5 个以内的候选技能然后把这些候选的描述和用户输入一起交给模型让模型做最终选择。这样做的好处是模型不需要面对全部技能注意力更集中误选率大幅下降。粗召回阶段我会用关键词匹配加语义向量的组合但我不会让召回结果直接决定最终答案。它的职责是“别漏掉”真正“拍板”的还是模型。你可以把它类比成线下门店的导购导购先根据顾客的话筛选出三件衣服再由顾客做最后决定而不是直接把整面墙的衣服都推给顾客。5.2 冲突消解与优先级设计即使有了候选筛选技能之间的冲突依然存在。比如用户说“帮我把订单金额算一下”可能既适合“优惠计算”又适合“价格展示”。我的做法是给每个技能配一个priority字段配合场景码一起使用。场景码代表当前对话上下文所属的模块比如“售前咨询”“售后处理”“订单查询”。调度器在多个候选技能都通过阈值时优先选择场景码匹配度高的如果场景码相同再选择优先级高的。这样设计的关键是让场景码足够稳定。我后来把场景码压到五个以内避免过度细分导致模型和调度器都拿不准。整个编排架构不一定要一开始就做得很大。我的建议是先跑通“单技能直接调用”再逐步加入粗召回和冲突消解。过早引入编排框架只会让问题复杂化因为每个技能本身还没成熟编排器去编排一堆不稳定的东西结果就是灾难。6. 技能评估与迭代让技能库越用越准技能系统跟模型一样上线只是起点真正的工作量在持续迭代。我一开始以为技能定义定好了就可以放着不管结果两周后发现准确率自己往下跌了百分之十几原因很简单用户表达习惯越来越多样而我的技能描述还停在最初的假设里。6.1 离线评测集怎么建离线评测集是我每次修改技能定义前必须跑的一套“体检”。它不追求覆盖所有情况而是聚焦在回归风险最大的场景。我通常把一个评测样本写成这样的结构{ id: case-021, user_input: 这个外套不想要了可以退吗, expected_skill: after_sale, expected_params: {order_id: MISSING, reason: 不想要}, category: ambiguous }这里有个细节expected_params里允许出现MISSING。因为有些场景下模型中确实不应该编造参数而是应该反问用户。如果评测集要求每个参数都必须填反而会把模型往错误的方向推。我建议区分“应该缺失的参数”和“被错误遗漏的参数”前者是合理的后者才是 bug。每轮迭代时我会把新跑出来的失败案例补充进评测集然后调整技能描述或边界规则再重新跑全量。这个过程虽然枯燥但它是技能库质量最直接的保障。我见过很多人把精力花在更换更强模型上但忘了用评测集固化已有水平结果模型一换之前的优化全部归零。6.2 在线反馈与版本回滚离线评测再怎么全面也赶不上真实流量的多样性。我在技能系统里加了一个轻量的反馈通道每次技能调用结束记录技能名称、模型参数、执行结果、用户后续是否有抱怨或纠偏。这里的“抱怨”我用一个简单的关键句检测用户在下一条消息里出现“不是这个”“又错了”“说了不对”等短语时自动为刚才的技能调用打一个负向标记。这些标记不会立刻改变线上行为而是每天晚上汇总到一张表格里第二天我再看哪些技能被负向标记的频率最高。这个方法帮我发现了不少冷门但严重的问题比如某个技能在极少数情况下会把单位“元”当成“千克”导致推荐话术出现荒谬内容。这种问题靠人工随机抽查看不出来在线反馈统计很容易暴露。版本回滚也要提前设计好。我采用的方式是每个技能定义都有版本号运行时读取的是当前生效版本。如果某次更新后离线评测下降超过 5 个百分点或者在线负向反馈飙升我可以一条命令把指定技能回滚到上一个版本。这个机制听着简单但在关键时刻能救命尤其是在快速迭代的时候。7. 最后的几点个人体会做完这个技能系统之后我最大的感受是agent-skills 从来不是一个“写完就结束”的功能模块它更像是一套需要持续维护的语义资产。模型能力变化、业务规则更新、用户语言演化每一项都会让你的技能库慢慢“过时”。如果让我给刚开始做这件事的人一个建议我会说先别急着堆技能数量而是把一个最核心、最常用的技能做到“模型不假思索就能正确使用”的程度。一个稳定命中率达九成以上的核心技能比二十个命中率只有五成的边缘技能有价值得多。技能库里应该宁缺毋滥因为每多一个技能模型的选择空间和你需要维护的描述边界就多了一份复杂度。还有一个小技巧技能描述不要怕写得啰嗦我见过很多追求简洁、每行都像 API 文档一样干巴巴的描述但模型根本不吃那一套它需要的是接近于“操作 SOP”的语言甚至可以直接加入错误示范。相信我在描述上多花半小时往往能帮你省下后面两天的排障时间。如果你也正在被 Agent 的工具调用搞得焦头烂额不妨回到技能层重构一下重新审视每个能力的边界把“让模型做对”变成“让模型更容易做对”。这套 agent-skills 的方法不一定适合所有业务但对绝大多数“模型知道答案、却执行不到结果”的场景值得一试。