Agent Skills 实战:从工具调用到稳定技能编排

发布时间:2026/10/21 21:51:16
Agent Skills 实战:从工具调用到稳定技能编排
我见过太多Agent项目翻车现场了。模型换得再大Prompt写得再花一碰到具体业务场景还是抓瞎。原因通常只有一个Agent手里没有真正趁手的“家伙”。这个“家伙”就是Agent Skills也就是给Agent注册的、能明确识别并被稳定调用的一组专业能力单元。项目标题“agent-skills”看起来像是一个工程命名但内核是一个很现实的命题怎么把Agent从“什么都会一点”变成“关键场景能顶住”。这篇总结我想从设计思路、技能结构、实操落地到问题排查完整拆一遍我在这套体系上踩过的坑和沉淀下来的做法适合正在做Agent应用、被工具调用折腾得不轻的工程师也适合想弄明白Skills到底比普通工具调用强在哪里的产品和技术负责人。1. Agent Skills到底解决什么问题1.1 一个触发我关注Skills的现场前段时间某位开发者找到我说他的客服问答Agent有个老毛病用户问“我这个月的账单怎么比上个月高”Agent回答得头头是道但给的数字经常对不上。后来我让他把SQL取数、报表解析、异常波动解释这三件事拆成三个独立的技能模块分别描述清楚适用场景和入参格式再让Agent按需调用准确率很快就上来了。这个案例很典型。它暴露的本质问题是之前的Agent像一个只会背口诀的实习生所有知识都堆在上下文里遇到问题时靠临场发挥。而引入Skills之后它变成了一个带工具箱的老师傅每个工具对应一类明确任务什么时候用、怎么用、用完怎么收尾全都有章法。之所以强调“独立技能模块”是因为我发现很多项目在早期把所有能力都塞进一个函数列表里看起来有十几个工具实际调用时Agent根本分不清哪个负责什么。Skill体系的出现就是强制你把这些能力做一层更规范、更带“使用说明”的封装。1.2 Tools、Skills和普通Prompt的本质区别很多人会问Agent Skills不就是工具调用Function Calling/Tools吗这句话对了一半。工具调用是底座Skills是在底座之上加了两个关键层一个是更严格的技能声明另一个是技能间的编排和复用。我用一张对比表来说明维度普通工具调用Agent Skills触发方式靠模型当场理解函数名和描述靠技能描述触发条件元数据命中更准确参数校验依赖函数schema错了一点就报错增加了参数示例、使用场景提示模型更容易生成正确入参技能复用每个业务流程各自接一遍技能作为独立单元多个Agent可共用同一套上下文影响每个工具结果都要塞回上下文技能可自定义输出摘要减少上下文噪声组合能力一般只有单次调用支持“计划-调用-校验-再调用”的编排循环普通Prompt的局限更明显。你把“你会检查账单异常”写在系统提示词里模型也只是“知道”这件事但你把“账单异常分析”做成Skill它就变成一个可执行、可反馈、可回归测试的独立单元。知道一件事和能稳定做成一件事这是Prompt和Skill的根本区别。用生活类比的话Prompt是给员工看岗位职责说明Tools是给他发一堆没贴标签的工具Skills则是给每件工具贴上操作手册并告诉他“什么场景拿哪个”。后者看起来繁琐但真正出活的是它。1.3 什么阶段需要认真考虑Skill体系我见过不少团队在一开始就拼命堆技能结果收益很低。根据我的经验出现下面三类信号时才真正值得上Skill体系同一个能力要在多个Agent或场景里复用且每次复制粘贴都容易出现行为偏差工具结果占用的上下文太多导致Agent经常“忘记”用户真正的问题线上出现“同一类问题十次回答九次不同”的稳定性问题尤其是需要精确计算、格式化输出的场景。如果你只是做一个几天就上线的Demo用普通函数调用就够了。Skills解决的是规模化、可维护和可度量的问题不是用来给简单场景增加仪式感的。2. 技能系统的结构设计与核心细节2.1 一个技能文件长什么样业界常见的Skill实现方式是把每个技能做成一个目录内含描述文件、实现代码和测试用例。我习惯用一组精简的配置来表示用一个叫“账单异常分析”的技能举例name: bill_anomaly_analyzer description: 分析用户账单金额波动输出波动原因和置信度。仅在用户对费用金额有疑问时使用。 trigger_conditions: - 用户提到账单金额变化 - 用户对扣费明细有疑问 input_schema: bill_id: type: string description: 账单编号 compare_period: type: string enum: [month_over_month, year_over_year] description: 对比周期类型 output_schema: anomaly_items: type: array items: item_name: string delta: number reason: string summary: type: string max_output_tokens: 300这个文件的核心并不在参数定义而在description和trigger_conditions。很多初版Skill失败都是因为描述写得像API文档模型根本不知道什么时候该调它。好的技能描述应该包含任务边界、适用场景、典型入口说法。2.2 技能元数据的设计原则技能元数据是给模型看的说明书。我在做了几十个Skill之后总结出四个必须遵循的原则第一描述要素要包含“任务边界”。所谓边界就是写明“这个技能负责什么、不负责什么”。不要小看这个细节。模型在判断调用时如果技能描述里全是能力宣传它会把什么都往这个技能上靠反过来你写清楚了边界误调用率能降一半以上。第二参数必须给“示例值”。很多团队只写参数类型不写示例。结果模型生成的入参格式经常不对比如日期写成了“昨天”而不是具体的“2025-06-01”。在参数描述里塞一个example: 2025-06-01模型生成合规入参的概率会高很多这是代价极低、收益极高的优化。第三输出约束要写“给Agent看的摘要格式”。不要让技能把原始返回全部丢回给Agent。我通常在output_schema里设定一个summary字段让技能内部把结果浓缩成几行关键信息。因为上下文是最贵的资源让Agent读一百行原始数据再让它总结远不如在技能里做好总结再抛回去。2.3 为什么技能要“声明式”而不是“命令式”所谓声明式就是我只描述“这个技能是什么、接收什么、返回什么”不描述“你必须在第几步调用它”。命令式的做法是写死流程先调A再调B再调C。这两种模式应对的场景完全不同。命令式流程适合稳定不变的内部链路但Agent面对的往往是变化多端的用户表达。声明式技能的好处是Agent可以根据当前对话状态动态决定调用顺序甚至在一次任务中多次调用同一个技能。我还发现声明式技能更容易做单元测试。因为每个技能不依赖外部流程状态你把一个输入丢进去它就应该给出符合output_schema的输出。这种可测试性是后面做技能质量管理的基础。3. 实操从零注册一个可用的Agent技能3.1 落地的选型思路技能体系一般依赖具体的Agent框架或SDK。我的建议是不要迷信大而全的框架先画清楚自己的调用场景再选一个能快速验证的方案。以我的习惯我会先用一个支持技能注册的轻量SDK跑通端到端再决定要不要引入更重的编排引擎。选型时我会重点看三件事第一SDK是否支持技能的动态加载也就是新增技能不用重启整个服务第二是否支持技能结果的流式反馈因为有些技能执行时间很长第三是否有现成的调用链日志这直接决定了线上排查成本。3.2 把“触发直觉”训练好技能系统里最玄学也最关键的部分就是Agent能不能“一眼看出”该调用哪个技能。训练这种触发直觉靠的是两样东西技能描述和召回入口。我分享一个自己打磨描述的实操流程第一步先写一个“差生版本”。也就是把想到的能力点全塞进去不讲究措辞。然后丢几条真实用户问题观察Agent的调用命中率。第二步根据失败case修改描述。如果该调用的没调用就在trigger_conditions里补上用户可能的口语化表达如果不该调用的调用了就在description里补负面样本比如“不要用它来查询账户余额”。第三步反复跑一个固定的回归集。我平时维护着大约50条测试问题每次改描述都要全部跑一遍保证修了东墙不塌西墙。这套方法和训练搜索引擎的意图识别其实是同一个逻辑只不过对象从搜索词变成了对话上下文。3.3 技能内部实现的关键代码技能描述写得再好最终还是要落地为可靠的执行代码。我习惯用Python来实现技能逻辑加上类型标注和反馈状态。下面是一个简化但完整的技能实现骨架# skills/bill_anomaly_analyzer/skill.py from typing import Any import json class BillAnomalyAnalyzerSkill: def __init__(self, config: dict): self.db_client config[db_client] self.metrics_client config.get(metrics_client, None) def invoke(self, params: dict[str, Any]) - dict[str, Any]: bill_id params.get(bill_id) compare_period params.get(compare_period, month_over_month) # 1. 参数预校验 if not bill_id: return self._error(缺少bill_id参数) # 2. 查询账单数据真实项目会加缓存与降级 bill_data self._query_bill(bill_id) # 3. 核心计算逻辑异常波动检测 anomalies self._detect_anomalies(bill_data, compare_period) # 4. 输出压缩只返回对Agent有用的摘要 return { status: success, summary: self._build_summary(anomalies), data: anomalies[:5], } def _query_bill(self, bill_id): # 这里走数据访问层注意超时控制 return self.db_client.query(bill_idbill_id) def _detect_anomalies(self, bill_data, compare_period): # 波动率计算用阈值识别异常项 result [] for item in bill_data[items]: current item[current_amount] previous item[previous_amount] if previous 0: continue delta_ratio (current - previous) / previous if abs(delta_ratio) 0.2: result.append({ item_name: item[name], delta: round(delta_ratio, 4), reason: 较上期波动超过20%阈值, }) return result def _build_summary(self, anomalies): if not anomalies: return 未发现超过阈值的异常波动项。 return f发现{len(anomalies)}项异常变动其中波动最大的是{anomalies[0][item_name]}变化率{anomalies[0][delta]:.1%}。 def _error(self, message): return {status: error, summary: message}这个代码里最关键的是第4步也就是输出摘要。我见过的失败案例里有相当一部分是技能返回了大量原始数据把Agent“淹死”了。技能应该像一名汇报工作的下属只说结论和必要证据而不是把Excel整个甩给老板。3.4 注册与验证的完整流程技能实现完我不会马上接进Agent正式链路而是先做三个层面的验证第一层单元验证。直接调用技能的invoke方法传入畸形的、正常的、缺参数的三类输入确认返回格式稳定。第二层Agent级验证。把技能注册进测试Agent用一组真实但脱敏的用户问题跑对话。这里重点看“调用时机”和“调用参数”是否合理。第三层回归验证。把新技能和原有技能放在一起跑一遍完整回归集。这一步最容易被忽略但它能发现技能之间的描述冲突。注册环节的关键操作就是把技能目录挂到SDK的加载路径下并执行一次技能列表检查。通常我会在启动脚本里加一条如下命令agent-skill-cli list如果输出里能看到新技能的名称、版本、描述摘要就说明注册成功。记得顺手把技能的版本号打出来方便后续回溯。4. 技能运行时的调用机制与上下文管理4.1 Agent是怎么把任务分配给技能的写清楚技能只是第一步运行时的分配机制才是真正的考验。当前主流做法是让Agent先做一次轻量规划把用户指令拆解成技能调用序列。这个过程的本质是“意图路由”。意图路由做得好不好主要看三个环节。一是候选技能数量。我建议单一场景下的候选技能不要超过十个超过十个之后模型的选择准确率显著下降需要分级分组。二是技能描述的区分度。两个技能如果描述高度相似模型经常随机选一个。三是上下文衰减。如果对话轮次过长模型可能忘记某个技能的存在所以有些实现会在每轮把技能清单重新注入一次。这里有一个非常实际的调优手段在触发条件中加“排除条件”。比如账单分析技能里写明“不要用于余额查询”这等于提前给意图路由打了补丁比在代码里硬编码规则更灵活。4.2 多技能协作与冲突处理真实项目很少只靠一个技能。多技能协作时我会遵守一个底线原则技能之间不要互相依赖。A技能不要代调B技能所有编排都交给Agent路由层去完成。为什么这么设计因为如果技能之间互相调用调用链就变成一条不可控的网状结构。一旦某个技能出现超时或异常错误定位会非常困难。相反所有技能保持扁平和无状态Agent每次只做一次决策异常链路就好排查。多技能冲突处理同样值得提前想清楚。最常见的情况是用户的一个问题同时命中多个技能。比如用户说“为什么我上个月和这个月费用不一样而且余额也不多”这时账单分析和余额查询两个技能都在候选列表。我会在技能描述里明确各自的任务边界并在路由层设置优先级让“费用波动解释”优先于“余额查询”。这样做不是限制Agent能力而是保证它先回答用户更关心的核心诉求。5. 常见问题与排查技巧实录5.1 高频问题速查表现象可能原因排查与处理技能从未被调用描述过于泛化或候选技能太多用回归集测试调用命中率补充触发条件和负面样本误调用不该调的也调了trigger_conditions写得太宽在描述中增加“不适用场景”并减少技能数量参数频繁报错schema缺少示例值或格式约束为每个参数补充example和正则格式说明技能结果污染上下文未做输出摘要原始数据全量返回强制技能返回summary字段限制输出token技能之间行为打架描述重合度高拉齐描述模板划清领域边界避免自由度表述调用超时但Agent无感知未设置反馈机制技能执行中返回进度事件超时后由路由层重试或降级5.2 我在排查过程中养成的三个习惯做技能系统以来我养成了三个能救命的习惯值得记下来。第一个习惯是所有技能都要写日志。而且不是简单的成功失败是记录调用参数、模型决策结果、技能返回摘要三件套。这样每次线上问题都能回答“模型当时看到了什么”“它决定调用什么”“工具返回了什么”。第二个习惯是保留技能的“影子模式”。简单说线上业务继续走旧链路新技能在旁路模拟运行只记录结果不下发执行。我用这个方式积累了大量真实case用来完善技能描述。效果非常明显改版后的技能上线即稳定。第三个习惯是定期查看技能命中分布。如果发现某个技能一个月都没被调用一次我大概率会把它下线或者重组因为技能数量越少其余技能的命中准确率越高。这里面有个典型的“数量-质量”权衡并不是技能越多越好。5.3 两个印象深刻的翻车案例第一个案例是关于输出格式的。我们曾经做了一个报表生成技能模型总是把结果渲染成带“单位”的文本但下游系统只接受纯数字JSON。排查了整整两天最后发现是技能output_schema里少写了一个unit字段的枚举约束。从那以后我给所有输出字段都规定了严格类型甚至在关键字段上加了正则校验。第二个案例发生得非常低级但很值得分享。有一个技能测试环境一切正常一到线上就偶尔调用失败而且时间是随机性的。后来看日志才发现线上服务重启时技能目录权限没有继承导致间歇性加载失败。那次之后我把技能加载结果加入服务健康检查只要技能列表不完整就直接拒绝上线。6. 进阶技能编排与质量度量6.1 把技能当产品来迭代做完第一个能稳定跑通的技能系统后我开始把它当产品来经营。每个技能都有版本号、负责人、更新日志和健康度指标。我强烈建议团队在技能数量超过十个时就引入这套机制否则很快会因为技能无人维护而腐烂。一个技能的健康度我主要看四个指标调用命中率该技能被正确调用的次数 / 它出现在候选列表的次数执行成功率技能代码正常运行并返回statussuccess的比例结果采纳率Agent在收到技能返回后最终回答引用该结果的次数占比回归通过率固定回归集上的测试通过比例。这四个指标不需要做得很重关键是形成固定的观察节奏。我一般是每周跑一次回归集每月复查一次指标趋势。6.2 技能编排模式的演进单技能阶段之后必然会走到编排阶段。我的建议是从简单的“顺序编排”开始不要一上来就做复杂的条件分支和循环编排。先让Agent按顺序调用技能A、B观察前面的输出如何影响后面的调用。稳定之后再加入条件判断比如“如果A返回异常则转调用C”。再往后可以尝试并行调用彼此独立的技能减少整体延迟。我特别想提醒的是编排逻辑不要写死在Agent代码里。把这些编排规则下沉为可视化配置或者在描述文件里以编排指令的形式声明。这样做的好处是调流程不需要发版本改配置就能生效。6.3 给技能加一个“退出机制”很多团队只关注技能怎么进不关心技能怎么退。我一直认为一个健康的技能系统必须包含下线机制。当一个技能的连续回归通过率低于某个阈值时应当自动标记为非候选状态不再参与意图路由。这背后是一个很朴素的想法技能系统本质上是一个可进化的工具库它必须有能力丢弃坏工具保留好工具。如果不加退出机制技能库会逐渐积累一堆“偶尔能跑通但不可靠”的能力最终拖垮整个Agent的稳定性。我在落地时会给每个技能加一个status字段取值是active、shadow、deprecated三种。转正、影子测试、下线都走标准流程。这套机制看起来多花了一点管理成本但它避免了线上最可怕的“玄学问题”。最后分享一个小技巧技能描述写完后大声读一遍如果念起来像人话说明Agent也更容易理解。我见过太多写满术语的技能描述模型调用时抽风人看的时候头疼。把描述当作用户手册来写往往比堆技术名词更有效。这套“agent-skills”的体系说到底就是在帮Agent变得更像懂行的协作者而不是一个碰运气的问答机器。