拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

智能体技能工程实战:从工具调用到可复用技能库的完整设计指南

智能体技能工程实战:从工具调用到可复用技能库的完整设计指南

直接说结论:如果你正在做智能体(Agent)应用,无论是跑在RAG框架里、套在自动化工作流里,还是嵌在对话产品里,agent-skills这个名字背后涉及的,就是给大模型配一套“可复用、可组合、可评测”的行为技能库。模型负责理解意图、拆解任务,技能负责真正把事情做对。这篇文章不聊空泛的概念,我会把技能体系从设计、定义、实现、编排到排查问题的完整路径拆开讲,全程用自己实操过的案例作参考。适合两类人看:一是正在做Agent产品开发、但觉得每次让模型调工具都像在碰运气的工程师;二是刚接触智能体技能设计、想知道从哪下手的初学者。

1. 智能体技能到底是什么

1.1 一个让我彻底想明白的场景

先说一次真实经历。我之前做过一个内部运维助手,功能很简单:查服务器状态、重启服务、看日志、发告警。最初版本把所有能力都写成一个工具列表塞给模型,结果非常不稳定,模型经常把“查日志”和“重启服务”搞混,甚至用查状态的参数去调重启接口,把一台测试机折腾得够呛。后来我把这些能力重构成“技能”,问题立刻缓解了。原因很简单:技能不是单个函数,而是一整套“模型调用规则”,它告诉模型这个能力是干什么的、适合什么时候用、参数是什么、依赖什么前置条件,以及执行后的预期结果。

这个经历让我明白了一件事:Agent的智能程度,很大程度上取决于模型身边那一排“技能”被设计得好不好。模型本身的推理能力再强,如果技能入口混乱、描述含糊、参数约束缺失,最终表现也是失控的。

1.2 技能(Skill)与工具(Tool)的区别

很多资料把两者混为一谈,实际差别很大。Tool通常指“单个可执行函数”,比如一个API接口,输入参数、返回结果,简单直接。Skill是比Tool高一层级的抽象,它至少包含四个要素:

  • 技能名称和命名空间
  • 意图描述:说明这个技能解决什么问题、在什么场景下触发
  • 输入参数Schema:每个参数的类型、必填项、取值范围、默认值
  • 执行器:可以是函数、API调用、脚本,也可以是另一个Agent的调用
  • 后置处理逻辑(可选):对结果的清洗、结构化、错误兜底等

用一个生活类比最直观:Tool好比你家工具箱里的一把螺丝刀,Skill则是“拧下显示器支架螺丝”这件事。同样是螺丝刀,但“怎么握、拧哪颗螺丝、要小心什么、拧不动时怎么办”这些信息,才是真正的技能。Agent开发最大的坑,就是只给了模型螺丝刀,没教它怎么干活。

我自己的定义是:Tool解决“能不能做”,Skill解决“做得好不好、会不会选错、失败了怎么办”。任何复杂的Agent产品,迟早都要从Tool层面走向Skill层面。

1.3 为什么现在特别需要技能工程

模型能力这两年进步飞快,但智能体落地依然很难,核心瓶颈就是“最后一公里”的执行确定性。模型擅长模糊推理,但不擅长精确执行。你问它“北京的天气怎么样”,它能给个大概;你让它“把北京分公司所有在线节点的CPU使用率和内存占用拉一份报告”,它就容易在参数、请求方式、结果整理上犯错。技能工程出现的核心原因,就是把“执行层面的确定性”从Prompt里剥离出来,变成一个结构化、可被程序检查、可被拆分测试的对象。

结构化技能有三个直接收益。第一,可控性强。每个技能的触发条件、参数校验、失败处理都是可枚举的,出了问题能定位。第二,编排灵活。多个技能可以拼接成复杂工作流。第三,评测简单。技能能被单独拿出来测试,不需要每次跑完整的模型对话。

另外从工程效率角度看,技能是可复用的资产。项目A里写的“查询订单状态”技能,项目B可以直接拿来用,只要把执行器换成B的API就行。这种复用性让团队不需要每次从零开始调Prompt。

2. 技能的整体设计与拆解思路

2.1 技能边界的划分原则

设计技能时最常遇到的灵魂拷问是:一个技能应该多大?拆得太细,模型要在几十个技能里做选择,容易浑水摸鱼;拆得太粗,一个技能内部包罗万象,又回到了混沌工具调用的老路。

我常用的判断标准是三个“是否”:是否属于同一领域操作、是否共享同一套前置条件、是否能在一次模型决策内完成。如果三者的答案都是“是”,就合并;否则就拆开。拿电商客服机器人举例。“查询订单状态”和“修改收货地址”都属于订单领域,但修改地址需要先做身份校验,与查询的前置条件不同,所以应该拆成两个技能。而“查询订单状态”和“查询物流轨迹”,虽然列表页不一样,但前置条件都是订单号,也可以合并成一个“订单信息查询”技能,通过enum参数区分查询类型。

技能边界定了,下一步要定义技能的“元信息”。每个技能最好带一个版本号,因为技能的业务逻辑很可能随着产品迭代变化,而模型的调用方式如果跟着变,评测基线就会乱套。版本号能帮助追踪“这个技能在什么版本下表现如何”。

2.2 技能命名的讲究

命名看着是小问题,实际影响巨大。大模型对技能名的语义理解非常敏感,命名模糊的后果就是模型在“不知道用什么”的时候瞎猜。

我们当时的教训是“用动宾结构”。不叫“数据”,要叫“查询用户基础信息”;不叫“订单处理”,要叫“更新订单状态”。动词要具体,名词要能对应到业务实体。用户在对话里说“帮我查一下订单”,模型搜索技能时匹配的就是“订单”“查”这些关键词,动宾结构能让相关性更高。

同时,技能名不建议包含修饰性词。不要叫“高效查询用户信息”,这种词模型无法理解,还污染语义空间。命名应该像函数名一样严谨,甚至更严,因为函数名是给人看的,技能名是给模型看的,模型又是在搜索语义空间里找最接近的匹配项,噪音越少越好。

2.3 技能定义的完整结构

一个合理的技能定义文件,应该包含七个核心字段。前五个容易理解:name、description、parameters、executor、returns。后两个容易被忽略,但至关重要:when_to_use(触发条件)和examples(典型调用示例)。

先说when_to_use。它是一段给模型看的话,说明“当你收到何种意图时,优先选择本技能”。很多技能描述洋洋洒洒写了一大堆功能,模型反而看不出触发时机。我后来把触发条件独立成字段之后,调用准确率明显上升。

再说examples。这是几条示例性的“用户输入-参数映射”对。模型在零样本条件下对参数提取往往不稳定,给了示例之后相当于做了一次“少样本提示”,准确性会大幅提升。比如“查询订单状态”技能,示例里写:“用户说‘帮我看看单号DD123456到哪了’→ 参数order_id=DD123456,query_type=logistics”。模型看到这个结构后,后续类似的说法都能自动对齐。

另有一处细节必须在定义文件里写清楚:技能是否需要前置技能。例如“申请报销单”技能可能需要“查询审批人信息”技能先执行。把这些依赖关系写在技能定义里,编排器才能建立正确的执行顺序。

3. 技能开发与接入实操

3.1 一周做出一套技能库

画个实线路径,我给新接触的人用的五步流程:记录业务动作 → 抽象共性 → 定义Schema → 实现执行器 → 联调回归。

第一步,把产品里所有模型需要执行的动作全部列出来。这一步不要做任何抽象,有多细列多细。第二步,把动作按“意图”归类。归类的时候会发现很多动作共用了同一数据源、同一前置条件,此时再决定合并还是拆分。第三步,为每个技能写出JSON Schema参数定义。第四步,用代码把每个技能的执行器实现出来。第五步,用一个包含典型用户对话的测试集反复联调,记录模型选中技能的准确率、参数提取的完整率、执行结果的正确率。

这套流程第一次做不用追求完美,重点是建立起“技能是可迭代资产”的认知。我见过很多团队直接把一个框架项目里的function列表改名为skills文件,然后跟风发博客,这是自欺欺人。

3.2 一个可以直接抄的示例文件

下面这个示例是我之前一个数据报表智能体里的真实技能定义,我做了简化处理。它展示的技能叫“获取业务指标报表”,参数包括报表类型、时间范围、维度。

{ "name": "get_business_report", "namespace": "analytics.report", "version": "1.2.0", "description": "获取一个或多个业务指标的统计数据报表,支持按小时、天、周聚合", "when_to_use": "当用户请求查看PV、UV、转化率、GMV、订单量等业务指标,或要求对比不同时间段的业务表现时使用", "parameters": { "type": "object", "properties": { "metrics": { "type": "array", "items": {"type": "string", "enum": ["pv", "uv", "conversion_rate", "gmv", "order_count"]}, "minItems": 1, "description": "需要查询的指标代码列表" }, "start_date": { "type": "string", "format": "date", "description": "统计开始日期,格式YYYY-MM-DD" }, "end_date": { "type": "string", "format": "date", "description": "统计结束日期,格式YYYY-MM-DD" }, "granularity": { "type": "string", "enum": ["hour", "day", "week"], "default": "day", "description": "数据聚合粒度" }, "dimensions": { "type": "array", "items": {"type": "string", "enum": ["channel", "device", "region"]}, "default": [], "description": "分组维度" } }, "required": ["metrics", "start_date", "end_date"] }, "examples": [ { "user_query": "看看上周每天的GMV和订单量", "arguments": {"metrics": ["gmv", "order_count"], "start_date": "2025-02-10", "end_date": "2025-02-16", "granularity": "day"} }, { "user_query": "对比一下这个月1号和15号的转化率", "arguments": {"metrics": ["conversion_rate"], "start_date": "2025-02-01", "end_date": "2025-02-15", "granularity": "day"} } ], "executor": { "type": "http", "method": "POST", "url": "https://api.internal.example.com/v1/reports", "headers": {"Authorization": "Bearer ${TOKEN}"} }, "returns": { "type": "json", "schema": { "type": "object", "properties": { "report_id": {"type": "string"}, "rows": {"type": "array"}, "total": {"type": "number"} } } } }

注意几个细节:metrics用了数组类型并在items里限定了enum,这能有效防止模型乱填指标名;dimensions默认给了空数组,模型不填也能正常查询,但填了就能做更细致的分组;examples两条示例分别覆盖了“多指标查趋势”和“对比两个日期”两种常见诉求,传递出的信息是“不要只查单个数字,要能帮我做对比”。

执行器我用的是HTTP调用,实际场景还可能是Python函数、SQL查询或另一个Agent。HTTP方式的优点是技能与执行逻辑彻底解耦,后续改执行器不用动技能定义。

3.3 参数Schema设计的三个原则

参数Schema是整个技能定义里最容易被低估的部分。很多开发者随手写一个宽松的Schema,觉得模型反正会理解。实操下来,Schema越宽松,模型越倾向于“自由发挥”,后续解析错误越多。三个原则:

第一,必填项必须显式声明。可填可不填的参数多了,模型无从判断。声明required是把决策压力交给模型,本质上是在逼它把用户的信息问清楚。第二,能枚举就枚举。能用enum定义取值范围就不要开放自由文本。自由文本带来的问题是值域不可控,下游处理困难。enum相当于给参数划定合法边界,模型选择起来更轻松,下游处理也能省掉一大部分脏数据清洗。第三,类型要尽量窄。能用integer就不要用number,能用string但配合format(如date)就不要裸用string。窄类型能提前拦截一类错误,避免执行阶段才发现参数类型不匹配。

我在实践中还发现一个规律:参数的description不宜写太长,最好控制在20个字以内,而且应该描述业务含义,不是描述代码含义。比如startDate参数,应该写“统计开始日期”,而不是“请求起始时间戳”。模型不是编译器,它理解的是语义,不是变量名。

4. 技能编排与组合使用

4.1 从单技能到技能链

实际业务中一次对话很少只触发一个技能。用户说“把昨天的数据汇总发到邮箱”,实际要执行三个技能:查询数据 → 生成报表 → 发送邮件。如果让模型一次调用三个技能,看起来没问题,但一旦第二步生成报表失败,第三步发送邮件就变成一个“没有依赖对象”的无效调用。更合理的做法是定义成一条技能链,带依赖顺序。

技能编排的核心是“前置条件检查”。执行第二个技能之前,必须确认第一个技能的产出物已经存在且格式正确。我建议在编排器里做一个状态机:每个技能执行之后维护一个artifacts清单,记录产出物类型、存储位置、有效时间,下一个技能执行前检查清单。这个设计能让失败定位从“说不上来哪步错了”变成“第2步生成报表无产出物”,排障效率完全不同。

4.2 编排器的配置与实现思路

我以一次数据查询与报告推送的编排配置为例,展示编排层如何组织技能调用:

workflow: id: daily_report_pipeline trigger: intent: "日报推送" steps: - step_id: query_data skill: get_business_report next: generate_digest - step_id: generate_digest skill: summarize_report depends_on: query_data requirement: "query_data.result.rows is not empty" next: send_email - step_id: send_email skill: send_mail depends_on: generate_digest requirement: "generate_digest.result.file_path exists"

这个编排里最核心的是每个步骤的requirement字段。它定义了执行条件,编排器在执行前用轻量逻辑判断一次,不满足就跳过。这样技能链就有了“短路保护”:查询数据为空时,后续生成摘要和发送邮件直接不执行,并返回提示给用户。

刚开始做编排时总忍不住加条件判断,后来发现一个准则:编排器只负责顺序和依赖,不负责业务。业务判断(比如数据量是否足够多)应该下沉到技能内部,而不是放在编排层。编排层做的是控制流程,不是理解业务,否则后面编排逻辑会越来越庞大,最终无法维护。

4.3 并行执行与合并结果

有些场景下技能之间没有依赖关系,可以并行。比如用户要求同时查“订单量”和“库存水位”,两个技能互不依赖。对这类无依赖技能,我用异步调度的方式并行执行,再做一个结果合并器,把双方结果组装在同一份回复里。

并行执行的收益在交互式Agent中尤其明显。串行等待两次API往返大约需要好几秒,并行能直接减半。不过并行带来的复杂度也需要权衡:并发数上去以后,要给每个技能调用加超时控制,否则一个技能挂住了,合并器一直等它,反而比串行更慢。我现在的做法是为每个技能单独配一个timeout_ms,合并器取所有技能的超时最大值,整体执行时间不会超过这个上限,宁可超时降级为“部分结果返回”,也不让它无限期等待。

另外合并结果时要注意格式化冲突。两个技能返回的JSON结构可能不一致,合并器应该先做归一化,把字段名统一,再决定展示顺序。有个经验:展示顺序尽量跟用户请求里提到的顺序一致。用户说“先看订单再看库存”,合并结果就把订单放在前面,这种细节对体验影响很大。

5. 常见问题与排查技巧实录

5.1 模型死活不调用技能怎么办

这个问题太常见了。技能文件写得完整、清晰,但模型在对话里就是不用,自己瞎编一通。排查思路按顺序来:先看技能描述与用户输入的相关性,再看示例是否覆盖了目标表达方式。

我遇到过的一种典型情况是,技能描述写得太“程序化”,比如“根据用户提供的参数组合调用业务报表API”,这种描述模型难以判断何时触发。后来改成“当用户想查看任何业务指标数据、做趋势对比或维度分析时”,触发率立刻上升。另一个技巧是检查示例里是否有与目标表达接近的句子。模型对没见过或距离远的表达方式,天然容易漏选。

还有一种原因是技能间彼此打架。几十个技能同时放进提示词列表,模型的注意力有限,排在后面的技能容易被漏掉。这种情况该考虑分层索引:先做一层粗粒度技能组路由(比如“财务类技能组、运营类技能组”),再做组内精排。而不是把所有技能都堆在一个大列表里,指望模型每次都能全看一遍。

5.2 技能执行失败后怎么自动恢复

技能执行失败是日常。最怕的不是失败,而是失败之后整条链路僵死。在设计技能时一定要预埋错误处理逻辑,至少覆盖三种情况:重试、降级、反馈。

  • 重试:网络抖动、瞬时错误,加上指数退避重试,两三秒内自动恢复。
  • 降级:主执行器挂了,切换到备用的只读数据源。比如查询报表的主API超时,可以降级到离线数仓的预聚合结果。
  • 反馈:重试降级都失败,要把错误结构体返回给模型,让模型生成可读的提示语,并主动向用户提问是否需要替代方案。

错误反馈的信息一定要结构化。不要只返回“Request failed”,应该返回“skill=get_business_report, error_code=TIMEOUT, msg=上游channel网关5s未响应”。让模型机会根据错误信息给用户更准确解释,例如明确告诉用户现在查不到某个渠道的数据。我曾经因为偷懒只返回了一个错误码,模型对着用户说“系统异常”,用户完全摸不着头脑。

5.3 技能描述污染上下文怎么办

接入技能太多,每秒调用都把全部技能定义塞进Prompt,上下文被大量技能描述占满,留给对话历史和业务上下文的窗口就少了。我的实测经验是,一份技能的JSON Schema平均400到600 token,如果注册了50个技能,光技能描述就吃掉两万多token,多轮对话的上下文会被严重挤压。

解决的思路是做“动态技能加载”。不把所有技能一次性注入,而是根据对话历史先做一次意图初筛,只加载相关度最高的5到8个技能。这个过程类似信息检索里面的召回-精排两步走。实现方式不复杂,可以先用关键词匹配或者一个轻量分类模型做初筛,候选技能只保留TopN,再连同完整描述注入模型。这样上下文占用可以压缩到原来的十分之一左右,而且触发准确率不会明显下降,因为每次模型只需要做选择题而不是大海捞针。

5.4 多技能冲突和权限问题

多个技能都能处理同一类请求时,模型可能选错。比如“查询报表”和“导出报表”,用户一个模糊表述“把数据导出来”,模型可能会选择导出技能,但导出前又没做格式选择,导致结果千奇百怪。这种冲突需要在技能描述里明确区分场景。我的做法是在when_to_use里加“前置条件描述”,比如导出技能里写“仅当用户明确指定导出格式或要求发送文件时使用,若用户仅要求查看数据请用查询技能”。

权限问题也有讲究。有些技能对用户身份有要求,比如“修改订单状态”需要管理员权限。建议不要在技能定义文件里标注权限信息,因为定义文件会被模型读取,告诉模型“这个技能需要管理员”相当于向旁路泄露了权限边界,模型可能会替用户想办法绕过限制。权限应该在执行器层强制校验,模型只负责提交调用请求,执行器根据token或用户上下文拒绝无权限的调用。这个边界一定要硬,不能靠模型自觉。

6. 技能评测与迭代策略

6.1 评测一个技能从四个维度看

技能开发出来必须验证效果好,不能靠感觉。我常用的评测体系是准确率、完整率、时效率、回归率四条维度。

  • 准确率:模型在测试对话里选对技能的比例。分母是测试集所有需要调用技能的情况。
  • 完整率:选对技能后,参数提取是否完整,必填参数是否都拿到。
  • 时效率:从模型输出调用请求到执行器返回结果的总时长,是否在可接受范围。
  • 回归率:本次迭代有没有破坏之前已经通过的功能。

这四项需要分别打分。准确率主要考察技能描述与when_to_use是否清晰;完整率主要考察参数Schema与示例是否充分;时效率主要考察执行器性能和编排器调度做得如何;回归率则对应整体工程质量的稳定性。缺了任何一项,技能都可能在某一个维度上暴雷。

6.2 构建一套回归测试集

技能迭代最大的隐患是改A技能,B技能悄悄变差。所以我强烈建议每个技能配一个专属回归测试集。测试集不用特别大,每个技能准备20到30条真实用户对话加上对应的期望调用的技能名和参数组合就初步够用了。

把测试集跑起来时要关注三类错误。第一是误触发,用户没有这个意图,模型却调用了技能。第二是漏触发,用户明确表达了意图,模型迟迟不调用。第三是参数错误,技能选对了,但参数映射错乱。这三类错误对应的修复手段完全不同:误触发是描述太泛,需要缩小when_to_use的范围;漏触发是语义距离没覆盖到,需要补充示例和改写描述;参数错误则是Schema约束不足或示例缺失,需要用enum、format、required等字段加固。

6.3 从日志里反推迭代方向

评测集做得再完善,也覆盖不了线上复杂情况。我每周固定做一次“技能日志复盘”:把模型没有按预期选择技能的case捞出来,用脚本聚类相似对话,按问题类型分组,然后针对性改进。这个过程能持续发现新的表达方式、新的意图边界,慢慢形成一套“对边界情况越来越熟悉”的技能库。

日志复盘最有价值的一条经验是:能复现问题才能改问题。捞日志时我会把模型当时收到的Prompt原文和技能列表快照一起存档,否则几周后再想复盘当时的上下文,根本无从下手。技能版本号在这个场景里发挥了大作用:快照里存了技能版本,问题case定位就能直接对到定义文件的具体版本,不用猜是哪个版本的技能出了问题。

另一个从日志里能发现的重要信号是“技能空转”。有些技能被高频调用,但返回结果几乎不被使用,说明这个技能触发了但不解决用户问题。这时候要判断是技能本身没价值,还是结果格式不对用户看不到。二选一,要么删掉技能,要么改输出呈现。

最后说点实际操作的事

技能体系是一个越滚越有价值的资产。最开始把第一个技能写出来时,会感觉只是加了一个函数描述,跟原来直接列工具列表没什么区别。坚持迭代几个月后,技能库开始覆盖产品里绝大多数常见动作,此时模型在场内发挥的稳定程度,会明显高于没有技能支撑的裸模型。

但我个人要强调一句:技能设计不是一次性的,它是一种持续投入的工程日常。每次用户抱怨“怎么又答错了”,背后几乎都能归结到某个技能定义不够清晰,或者某个技能缺失。跟随着日志、评测、现场反馈,把技能库打磨到能覆盖目标场景的80%以上,Agent才算真正“能干活”。这套方法是笨功夫,但非常可靠,我希望你把工具技能化这件事落实到自己的项目里,别只让它停留在概念阶段。

返回列表