1. 从"会聊天"到"会干活":Agent Skills 解决的到底是什么
过去大半年,我一直在折腾基于大模型做自主智能体(AI Agent)的工程化落地。从最初"模型加提示词"的玩具玩法,到后来必须面对真实业务里的任务拆分、工具编排、失败恢复,我越来越确认一个判断:Agent 能不能真正干活,瓶颈往往不在模型智商,而在"技能"(Skills)怎么设计、怎么组织。agent-skills 这个概念,说的就是让智能体从"能聊"走向"能干"的那条关键路径。
先说个直观对比。纯对话场景里,模型只需要理解用户意图并生成文字;但要让它执行任务,比如"帮我把这批订单数据按区域汇总,再生成一份周报发给负责人",模型自己做不到——它没有访问数据的权限,没有操作表格的能力,也不知道"发周报"这个动作对应哪个系统。这时候你就需要把"数据汇总"、"生成周报"、"发送消息"这些能力封装成一个个可被模型调用的技能模块。每个技能模块承载一段具体能力,模型在推理过程中根据需要挑选并调用它们,整个 Agent 才能完成闭环。
Skill 和常说的 Tool/Plugin 有区别,这点值得深究。早期 Agent 框架里的 Tool 一般指一个单一函数,比如"搜索网页"、"执行代码",输入输出都很简单。但真实业务里,一个完整任务往往需要多步操作:查询数据库、清洗数据、调用另一个 API、按模板输出结果。把这些步骤硬拆成多个 Tool 让模型自己拼接,极易出错,模型经常记不住步骤顺序、漏掉中间环节。Skill 更像一个"打包好的工作流单元":包含了目标定义、输入契约、执行逻辑、校验规则和失败处理。对模型来说,一次调用完成一件事,容错率明显提高。
我的理解是,Agent Skills 的本质是把"模型决策"和"具体执行"解耦。模型只负责判断"现在该用哪个技能、参数怎么填",技能内部的事情由代码保证。这就像你请了个实习生:你自己负责安排任务和验收结果,至于他具体怎么操作,只要在既定流程内完成就行。这个抽象层次一旦搭对了,Agent 的稳定性会有质的提升。
适合读这篇文章的人,我大致分三类:一是正在用 LangChain、CrewAI 等框架做 Agent 原型,但总觉得"能跑通但不可靠"的开发者;二是想在业务系统里接入智能体能力,却不清楚技能边界怎么划的架构师;三是纯粹好奇"模型怎么能操作外部系统"的技术爱好者。下面我说的都是自己实测过的方案和踩过的坑,不一定是最优解,但肯定能帮你少走几周弯路。
2. Skill 的解剖学:一条技能从声明到执行要过哪几关
一个合格的 Skill,在设计层面至少要拆成五层:元信息层、参数契约层、执行器层、校验层、权限层。很多人只写了执行器,其他几层全省略,结果技能在 demo 里好用,一上真实场景就各种翻车。
2.1 元信息层:模型靠这一段文字认识你的技能
元信息是模型决策的依据,通常包括技能名称和描述。别小看这段描述,它直接决定模型在什么场景下会选你这个技能。描述写得含糊,模型就可能该用的时候不用、不该用的时候乱用。
我见过最典型的反例:有人写了一个query_orders技能,描述是"查询订单"。结果模型在处理"统计本月销售额"时,宁可用一个名为analyze_data的通用技能瞎猜,也不愿意调query_orders,因为"查询订单"听上去只是"拉列表",跟"算销售额"没关系。后来把描述改成"根据指定时间范围、区域、客户等条件从订单数据库查询原始订单记录,返回结构化字段列表,供后续统计或报表分析使用",路由准确率一下子从六成不到提到了九成以上。
2.2 参数契约层:给模型一个"填空题"而不是"论述题"
参数契约就是技能接收的输入结构,用 JSON Schema 描述即可。核心原则是:能枚举的字段就枚举,能设默认值就设默认值,不要把开放式的自然语言留给模型自由发挥。
比如一个发送通知的技能,参数应该明确channel字段只能是email、sms、webhook三种值,而不是让模型传任意字符串。模型不是不聪明,而是你一旦给了它自由,它就会发挥想象力,传一个wechat出来。参数裸奔的技能,轻则报错,重则出现安全风险。
2.3 执行器层:技能的核心逻辑,保持纯粹
执行器是技能真正干活的代码。我的建议是:执行器内部只做与"该技能职责"相关的事情,不要掺入模型推理逻辑、不要掺入业务规则之外的附加操作。一个技能只做一件事,做好,做彻底。
举个实际的例子。我写过一个generate_weekly_report技能,最初版本里既包含数据聚合逻辑,又顺便把汇总结果写进了数据库,还在最后给管理员发了封邮件。结果一次数据源抖动,整个流程在聚合阶段就失败了,但因为邮件发送在最后一步,导致用户收到一封"周报生成成功"的邮件,但实际周报根本没生成。这就是技能职责不纯粹带来的连锁事故。后来我把发邮件拆成独立技能,由上层编排按需调用。
2.4 校验层:执行前和执行后都要过一道"安检"
执行前校验输入参数的范围、格式、必填项;执行后校验输出结果是否符合预期。输出校验常被忽略,但这恰恰是 Agent 可靠性的分水岭。
比如一个fetch_webpage_content技能,返回结果可能是正常的正文,也可能是 404 页面、验证码页面、空页面。你必须在技能内部做一层"是不是有效内容"的判断,而不是把任何字符串都当作成功结果返回给模型。模型看到乱七八糟的内容,会基于垃圾信息继续推理,错误会一路放大。
2.5 权限层:技能能力的边界,必须有硬约束
每个技能都应该声明自己需要的最小权限。技术上建议在技能层做两件事:一是对操作对象做白名单限制(比如只允许访问指定目录、指定 API 域名);二是对高风险操作加入人工确认点。
我团队里有个血的教训:一个execute_shell_command技能完全裸奔,Agent 在一次任务中执行了一条rm -rf命令删掉了测试环境的临时目录,虽然没造成生产事故,但所有人都吓出一身冷汗。从那以后,我们的所有技能都强制过权限层,高危操作必须二次确认。技能能力边界画清楚,Agent 才能放心地交到用户手上。
3. 路由决策的真相:LLM 到底是怎么"选中"你的技能的
很多做 Agent 的人都有一个误区:以为给 Agent 塞的技能越多越好。实测下来恰恰相反,技能库膨胀到一定规模后,模型的选型准确率会明显下滑。理解模型的"选技能"机制,是设计技能库的前提。
3.1 模型选技能的底层逻辑:描述匹配加上下文联想
目前主流 Agent 框架里,模型选技能基本靠"两看":一看技能描述与当前任务的语义相似度,二看技能参数与用户请求里信息的匹配度。这个过程很像你在搜索引擎里输入关键词,系统选出最相关的几条结果。所以技能描述的写作质量,直接决定它能不能被"搜中"。
一个很实用的写法是"场景描述"式写法:不用干巴巴说这个技能"是什么",而是说这个技能"解决什么问题、适合在什么情况下用"。比如描述一个translate_text技能,与其写"翻译文本",不如写"将用户提供的中文、英文或其他语言文本翻译成指定目标语言,适用于跨语言交流、文档翻译、内容本地化等场景"。描述里出现越多与真实使用场景重叠的词汇,模型选中的概率越高。
3.2 技能数量陷阱:为什么"库越大越不聪明"
我做过一个对比实验:同一套任务集,分别用 5 个技能、15 个技能、40 个技能的配置跑。结果令人意外,15 个技能时任务成功率最高(约 88%),5 个技能时因为能力不足成功率为 72%,40 个技能时反而掉到 76%。技能越多,模型在"选哪个"这一步消耗的上下文越多,选错的可能性也越大,而且多个相似技能之间会产生干扰。
这里有个实操建议:每新增一个技能,先问自己三个问题——这个技能和已有技能的功能边界是否清晰?有没有可能是某个已有技能的参数变体?用户需求里出现这个技能对应场景的频率真的高吗?如果三个问题都答得不干脆,就先别加,用"在已有技能里加参数"来替代。
3.3 路由测试:不要等上线了才发现选错
路由准确性是可以前置测试的。我的做法是建一个"路由测试集":准备 50 到 100 条真实用户请求,每条请求标注"期望选中技能"和参数期望值,跑一遍 Agent 调用链,统计选型准确率。这个测试集应该持续积累——每次线上遇到路由选错的情况,就把对应请求加入测试集并修正描述,形成回归测试。
实测下来,路由准确率在 90% 以上才适合上生产环境。低于这个线,问题大多数出在描述语焉不详,少数出在技能边界重叠。先处理描述,再考虑合并技能,一般都能提上来。这一步看起来占用时间,实则是 Agent 上线投产前性价比最高的投入。
4. 手写一个 Skill 的完整过程:从需求到能跑通
理论知识说了一堆,下面用一个实际例子走一遍完整流程。我选一个比较有代表性的技能:summarize_document,作用是读取文档内容并生成结构化摘要。这个技能覆盖了解析、调用模型、输出校验三个典型环节,适合当模板参考。
4.1 需求拆解:先写清楚"这个技能不做什么"
动手写代码前,先把这个技能的边界钉死。summarize_document的需求可以拆成这样:
- 输入:文档路径、摘要长度(短/中/长)、摘要语言
- 做什么:读取文档、清洗文本、调用 LLM 生成摘要、输出结构化结果
- 不做什么:不做文档翻译、不保存摘要结果(除非上层另行要求)、不处理加密文档
- 失败处理:文档不存在返回明确错误;文档为空返回提示;LLM 调用超时重试一次
边界写清楚的最大好处是,后续写summarize_document的人不会顺手把翻译功能也塞进来。技能职责混乱,九成是从"顺手加个功能"开始的。
4.2 技能清单与执行器:代码层面的最小实现
技能清单用 YAML 声明,结构如下:
name: summarize_document description: > 读取本地指定路径的文档内容(支持 txt/md/pdf), 按用户指定长度生成结构化中文摘要, 适用于文档速读、报告提炼、会议纪要整理等场景。 parameters: type: object properties: doc_path: type: string description: 文档的本地绝对路径 length: type: string enum: [short, medium, long] default: medium language: type: string enum: [zh, en] default: zh required: [doc_path] returns: type: object properties: summary: type: string description: 生成的摘要文本 word_count: type: integer description: 摘要字数执行器部分我用 Python 写:
def run(ctx, doc_path, length="medium", language="zh"): # 1. 校验参数 if not path_exists(doc_path): return {"success": False, "error": "document_not_found"} # 2. 读取并清洗文本 text = read_document(doc_path) if len(text.strip()) < 20: return {"success": False, "error": "document_empty"} text = clean_whitespace(text) # 3. 调用 LLM 生成摘要 max_len_map = {"short": 100, "medium": 300, "long": 600} try: summary = llm_summarize(text, max_len=max_len_map[length], lang=language) except TimeoutError: summary = llm_summarize_retry(text, max_len=max_len_map[length], lang=language) # 4. 输出校验:摘要不能为空且有实际内容 if not summary or len(summary) < 10: return {"success": False, "error": "summary_generation_failed"} return {"success": True, "result": {"summary": summary, "word_count": len(summary)}}注意第三步的重试逻辑,只对超时这种"可重试错误"做重试,对模型返回内容不合规这种"确定性错误"不做无谓重试,直接走失败分支。很多人把所有异常都包一层重试,结果模型每次都返回同样错误,白白浪费时间和 token,这个习惯要改。
4.3 真实场景验证:拿"脏"数据试,别拿"干净"数据试
技能写完,第一轮测试你会发现一切正常,因为你用的是精心准备的 PDF 和文档。第二轮请务必拿真实世界里的"脏"数据试:扫描版 PDF(没有文本层)、加密文档、超大文件、只有图片的 Markdown、编码混乱的 txt。每个都能单独写一个处理分支。
我遇到最多的情况是 PDF 解析出来的文本里夹杂大量换行和特殊字符,直接把文本塞给模型,摘要质量很差。后来我在清洗环节加上了一个简易的段落合并规则:连续换行超过两个的,视为段落分隔符;单个换行且前后都是中文的,视为同一段落。清洗后摘要效果提升明显。这类处理细节不会写进框架文档,但 Skills 稳定性的差距就是这么一点点抠出来的。
5. 线上踩坑实录:Skill 最容易翻车的五个场景
前面讲的是设计方法,这一节专门说坑。以下五个问题,是我在多个项目和客户现场真实遇到过的,按出现频率从高到低排列。
5.1 描述歧义导致路由错乱
这是频率最高的问题。表现是:同一个用户请求,这次选中技能 A,下次选中技能 B,结果完全不一样。比如技能库里同时有analyze_sentiment和extract_keywords两个技能,描述都包含"分析文本"这个短语。用户在请求里说"帮我看看这段评论的态度",模型一会儿选这个一会儿选那个。
解决办法就是前面说的:描述里尽量写"场景话术",把技能适用场景的典型表达写进去,减少模糊匹配的空间。如果两个技能的描述都改过之后还是容易混淆,就说明这两个技能的边界本身就不清晰,考虑合并成一个技能,用参数区分。
5.2 参数幻觉:模型编出根本不存在的参数
LLM 存在幻觉,不仅体现在回答内容上,也体现在填参数上。用户说"帮我查一下上周的数据",模型可能把日期参数填成上周一的日期,但你的系统里"上周"的定义是自然周还是滚动 7 天?模型不知道,于是它按自己的理解填。
解决这个问题的思路不是试图让模型更聪明,而是在参数契约层把不确定性消掉:能定义枚举就定义枚举,能用"相对时间描述"就用相对时间描述,不行就在参数说明里给出明确示例。我们有一个query_analytics技能,参数里有个date_range字段,说明里直接写着"支持格式:today、yesterday、last_7_days、last_30_days、this_month、YYYY-MM-DD 到 YYYY-MM-DD",模型填参数准确率立刻上了一个台阶。给模型越明确的"选项",模型就越少"自由发挥"。
5.3 非幂等操作遇上重试机制
Agent 框架普遍内置重试逻辑:技能调用失败就重试一次。对幂等操作(查询、读取、生成)没问题,但对非幂等操作(创建订单、发送邮件、扣减库存)就是灾难。我亲眼见过一个场景:Agent 调用"创建工单"技能超时,框架自动重试一次,结果同一个工单创建了两遍。
如果你在使用某个框架,务必检查它的重试策略是否对所有技能生效,然后在技能清单里对非幂等技能显式声明retryable: false。如果你的框架不支持这个字段,就在技能描述里写清楚"该操作不可重复执行,如果超时请先查询确认操作结果再决定是否重试"。有些坑是框架自带的,你不主动绕,它迟早让你交学费。
5.4 上下文污染:上一个技能的输出干扰下一个技能的决策
Agent 长期运行多个技能后,对话上下文里会堆积大量中间结果。模型在做下一次技能选择时,会受这些历史输出影响。最经典的场景:用户先让 Agent 查了某公司的财报数据,然后说"帮我也查一下对比公司的数据"。模型可能因为上下文里已经有一堆某公司数据,就把对比公司误识别为同一家公司。
缓解手段包括:技能之间传递结果时,只传必要字段,不要全量 dump;每轮任务完成后对上下文做压缩或截断,保留用户原始意图和下轮所需的最小信息;对敏感场景,干脆给 Agent 限定"每轮任务独立执行,不参考上一轮输出"。上下文污染没有一个一劳永逸的解法,但能做到"及时清理"就能规避大部分问题。
5.5 技能状态共享和竞态冲突
多线程或并发场景下使用技能,如果技能内部依赖共享状态(比如模块级变量、临时文件、数据库全局表),会出现竞态问题。我遇到过一个诡异 bug:两个用户同时触发同一个技能,A 用户拿到的结果里混着 B 用户的数据。
排查下来发现是技能内部用了一个模块级缓存字典,没有加锁。教训是:技能执行器应该遵循无状态设计,所有状态显式存放于上下文中,或由外部存储管理。如果业务上必须有缓存,也要做好隔离(按用户维度或任务维度分 key)。这不仅是技术洁癖,而是一旦 Agent 被多用户使用就会暴露的问题。
6. 让 Skill 真正"可维护":测试、版本与灰度的一点体会
技能写出来只是开始,真正考验功夫的是后续迭代维护。这一节分享我在技能测试、版本管理和灰度发布上的实操做法。
6.1 给每个 Skill 配一套"夹具测试"
前文提到路由测试集,这里再说技能功能测试。每个技能都应该有一套固定的输入输出样例,类似单元测试里的 fixture。执行器改代码之前,先跑一遍 fixture 确保没回归;改完之后再跑一遍确认修复生效。
我的 fixture 分三类:正常输入(验证主流程)、边界输入(空值、超长值、非法值)、异常输入(文件不存在、服务超时、无权限)。每类准备 3 到 5 个样例就够。实测下来,这个做法的价值不在"能发现多少 bug",而在"改代码时敢不敢下手"。没有 fixture 保护的技能,每次改动都像走钢丝。
6.2 技能也要版本化,描述变化更要慎重
技能版本管理有两个层面:执行器代码层面的版本控制(用 Git 就行),以及技能清单本身的语义化版本。特别要提醒的是:描述字段的改动对线上行为影响极大,因为它直接改变模型的路由选择。我把"改描述"视为高优先级变更,必须走 review 流程,不能像改普通注释那样随意。
有一次我改了某个技能描述,只是加了一句"也支持处理 CSV 格式",结果第二天线上监控显示这个技能的调用量暴涨了三倍,因为模型开始把各种"文件处理"请求都路由到它头上。描述就是技能的"门面",你想清楚再改。
6.3 小流量灰度,用数据说话再全量
Agent 技能更新,强烈建议做小流量灰度。在真实业务里,你把旧技能替换成新技能之前,先让 5% 到 10% 的流量跑新的,对比成功率、耗时、token 消耗这三类核心指标。等新技能连续稳定运行一段时间后,再逐步扩到全量。
这一节想强调的是:技能不是写出来就完事的静态资产,它需要持续观察、迭代和淘汰。我每次迭代一个技能,都会把这次的改动原因和效果记录在技能的 README 里,三个月之后再回看,你会很清楚每个决定是怎么来的。
最后说点实在话
如果只留一条经验给你,我想说:Agent 的能力上限,不是模型决定的,而是你技能库的质量决定的。模型是那个"想做事的实习生",Skill 是"你给实习生准备好的工具和流程"。工具顺不顺手、流程清不清晰,决定了他能不能办好差事。
我在实际迭代中还有一个习惯:每个季度把技能库整体清点一遍,删掉低调用量的技能,合并边界模糊的技能,重写描述含糊的技能。这个动作像给仓库做大扫除,每次做完,后续新技能的定位都会更清晰,路由准确率也会跟着提升。
关于是否有必要换更复杂的 Agent 框架,我的态度是:先把手上的技能打磨扎实,再考虑框架升级。框架只是壳,技能才是核。希望这篇文章能帮你把技能设计这份功夫练扎实,少踩几个我踩过的坑。