Agent技能库重构复盘:从Function Calling到可维护的技能治理框架
2026/9/16 18:02:45 网站建设 项目流程

不管你是做RAG还是做Agent,到了一定阶段都会撞上同一个问题:代码能跑,但离“产品化”还差得远。我最近把agent-skills这个技能库从“能跑”的重构成“能维护”的,过程中踩了不少坑,也把很多模糊的判断变成了可执行的规则。今天这篇就当是一次复盘,把技能该怎么定义、怎么组织、怎么让大模型稳定调用这套东西,一次性讲清楚。

1. 这个项目在解决什么问题

1.1 智能体开发为什么需要技能层

先说个场景:你给智能体接了一个查天气的功能,代码写死了五个函数,模型通过tool calling调。刚开始很爽,需求一多就乱了——查天气的要带城市编码,查机票的要带机场三字码,查酒店的又要带不同平台的商户ID。这些逻辑全部堆在tools/目录下,每个文件几百行,改一个接口要牵连七八个地方,更别提测试和部署了。

agent-skills想解决的,就是这坨“接口泥潭”。它把散落的工具函数、系统提示词、调用约束、错误处理打包成一个独立单元——我管它叫“技能”。每个技能自带说明文档、参数Schema、执行逻辑和失败兜底,模型只需要通过一句话级别的描述去匹配技能,剩下的全部由技能内部处理。

说白了,技能层就是给大模型准备的“标准化接口层”。模型不需要关心天气API是第三方还是自建,不需要知道机场代码存在哪个表里,它只需要知道“这个技能能帮我完成什么任务”。这层抽象一旦建立,后面加新功能、换供应商、改数据结构,都变成技能的内部事务,外部调用方无感。

1.2 agent-skills的技能分层与项目定位

如果你去看agent-skills的仓库结构,会发现它并不是把所有技能平铺在一个目录里,而是按“功能域-具体场景-原子操作”三层来做组织。

第一层是功能域,比如travelofficedata_analysis。第二层是具体场景,比如flight_bookingmeeting_summary。第三层才是真正可以被模型调用的原子操作,比如search_flightssend_emailgenerate_report

这样的分层有两个直接好处。第一,权限控制有了天然的边界——你可以让模型访问travel域下所有技能,但禁止它调用admin域下的任何东西。第二,技能检索的召回效率大幅提升——当模型需要处理一个复杂任务时,先按域缩小搜索范围,再在场景内部做精细匹配,比在几百个平铺技能里暴力检索要可靠得多。

现实里很多项目死在技能数量膨胀上。50个技能以内,平铺也许还能忍;一旦超过100个,模型在意图识别和技能选择上的错误率会肉眼可见地上升。agent-skills从一开始就按三层结构来组织,与其说是一种设计偏好,不如说是为了应对“技能数量一定会增长”这个确定性趋势。

1.3 与Function Calling、Plugin的边界关系

这里必须说清楚,agent-skills不是一个全新的概念,它是在Function Calling和Plugin基础上长出来的一个更完整的开发范式。

Function Calling解决的是“模型怎么调函数”的问题,它定义了一套协议,让模型输出结构化参数,系统来执行。但协议本身不关心你这函数怎么实现、要不要鉴权、失败怎么办。Plugin则更偏向生态层面,解决的是“一个第三方服务怎么接入到Agent体系里”的问题,但它通常只描述接口,对内部实现和上下文管理做得比较浅。

agent-skills站在两者之上,把“一个技能从被选中到执行完成”的完整生命周期都管起来。被选中时,模型看到的是一份精心写的SKILL.md;执行时,系统做参数校验、权限校验;失败时,技能内部有兜底策略和重试机制;执行完,调用记录和分析日志自动归档。

所以你可以这样理解:Function Calling是通信协议,Plugin是接入标准,agent-skills是完整的技能治理框架。三者的关系不是互斥,而是层层递进——我在实现里底层依然走Function Calling协议,但上层所有策略都由agent-skills接管。

2. 技能的定义、组织与实现细节

2.1 技能的核心形态与目录结构

一个技能在agent-skills里就是一个自包含的文件夹,里面至少包含三样东西:SKILL.mdschema.jsonhandler.py(或者handler.js,语言不限)。

我建议的目录结构是这样:

skills/ travel/ flight_booking/ SKILL.md schema.json handler.py tests/ test_handler.py office/ meeting_summary/ SKILL.md schema.json handler.py

技能内部默认不共享状态,每一个调用都是无状态的。如果某个流程需要跨步骤记忆(比如先搜索再订票),由上层编排器负责传递上下文,技能本身不保存任何会话数据。这个设计可能看起来不够“聪明”,但它让技能的调试、测试和复用变得异常简单——你不需要为了一个技能维护一套数据库,跑完一个调用,清掉临时变量,下一次又是全新状态。

2.2 SKILL.md怎么写才不会被模型误用

SKILL.md是整个技能体系里最容易被忽视、也最值得花时间的文件。它决定了大模型能不能在关键时刻正确选中你的技能,以及选完之后能不能正确使用。

一份合格的SKILL.md至少要包含四块内容:技能概述、适用场景、边界声明、使用示例。概述不能超过三句话,要直接说明“这个技能做什么”;适用场景要列举3到5个典型问题,帮模型建立触达联想;边界声明则必须写清楚“什么情况下不要用这个技能”,这点常常被忽略,但特别管用。

举个实际的反面案例。我早期写了一个web_search技能,描述里只写了“执行网络搜索”,结果模型在需要算数学题时也调它,在需要总结文档时也调它,因为模型觉得“搜索”可以做一切。后来我重写了边界声明:“仅当用户明确要求查找事实信息、获取最新资讯时才可使用,任何涉及推理、计算、总结的任务不应调用本技能。”误用率立刻降下来了。

写使用示例时,要给具体的输入输出样例。模型的few-shot能力很强,一段好的示例比十条抽象描述都有效。我一般会写一个“正确调用示例”和一个“错误调用示例”,通过对比让模型理解这个技能的准确边界。

2.3 schema定义的艺术:参数越少越好

schema.json是技能对外暴露的参数协议,它直接决定了大模型调用时的体验。我踩过最大的坑就是参数设计过度精细化。

一开始我做一个send_email技能,参数设计了fromtoccbccsubjectbodyattachmentsprioritytemplate_id,一共九个字段。模型每次调用都要纠结半天,有些字段它根本不知道填什么,直接胡编。后来我把参数压缩到了四个:tosubjectbodyattachments,其他全部做成可选参数并在运行时给默认值。调用成功率反而上去了。

参数设计要遵循一条铁律:必填参数保持在3个以内,所有非关键变量都提供默认值。模型不是数据库工程师,它不会乖乖填完所有字段再调函数,它只会挑它能确定的填,填不全就开始瞎编。

还有一点,参数的描述信息一定要写清楚。比如to字段,不要只写“收件人”,要写“收件邮箱,支持多个地址,用逗号分隔”。模型对输入格式的猜测能力比你想象中弱,给它明确的格式提示,能少很多解析错误。

2.4 handler实现:把脏活累活留在技能内部

handler.py是技能的具体执行逻辑,它不一定要多聪明,但一定要健壮。外部依赖的异常要在这里全部接住,网络超时要在这里统一处理,返回格式要在这里规范成统一结构。

我的handler内部一般默认提供三个方法:validate_input()execute()format_output()validate_input()负责参数初步校验,比如邮箱格式对不对、日期是否合法;execute()是核心逻辑,调用外部API或者访问数据库;format_output()把执行结果转换成模型容易理解的文本结构。

有一个很关键的细节:execute()内部所有外部请求都必须设置超时,不做超时控制,一旦某个API挂掉,整个Agent的响应时间就会被拖死。我统一设成5秒超时,超过直接抛业务异常,由技能内部的失败处理逻辑接管。宁可返回“查询超时请稍后再试”,也不要有任何一次未受控的长时间阻塞。

3. 落地实操:调度、组合与参数设计

3.1 技能注册与路由:让Agent准确找到技能

技能定义好了,下一步就要解决“怎么被模型找到”的问题。我先给每个技能打了一套标签系统,包含categorycapabilitieskeywordscomplexity四个维度。

category对应功能域,capabilities是这个技能的能力列表(支持动词加宾语,比如“发送邮件”“生成图表”),keywords是业务关键词(比如“航班”“会议室”“报销单”),complexity则是技能对上下文的要求等级,从1到5,5代表需要大量背景信息才能执行。

路由时我会先通过向量检索召回Top 20候选技能,再结合模型对任务意图的理解做最终选择。这里要注意,向量检索召回的目的不是直接得出答案,而是把候选集缩到模型容易决策的范围内。如果你把几百个技能全部丢给模型去选,决策质量和速度都会明显下降。

3.2 技能编排与组合模式

在实际项目里,单次调用一个技能的场景很少,更多时候是多个技能串成一个工作流。比如“帮我订明天去上海的机票和酒店”,至少涉及search_flightsbook_ticketsearch_hotelsbook_hotel四个技能。

agent-skills在编排层支持两种模式:顺序模式和条件分支模式。顺序模式就是无脑串起来,前一个技能的输出作为后一个技能的输入。条件分支则是在每一个环节让大模型做一次决策,判断是否需要跳过或切换技能。

顺序模式适合确定性强的任务,比如“搜索-选择-预订”,步骤固定,不需要中途判断。条件分支模式适合意图容易变化的原创任务,比如“推荐餐厅-查评分-订座”,每一步都要根据用户反馈做调整。

我在编排层所有技能调用的中间结果都按JSON格式结构化存储,同时保留一段给用户看的人话版摘要。这样做的好处是,如果最终任务失败,你可以回溯看每一步发生了什么,定位是哪一步的问题。

3.3 技能间的数据流设计:上下文传递与冲突避免

技能编排有一个特别容易翻车的点:数据格式冲突。search_flights返回的城市编码是SHAPEK这种三字码,但book_hotel期望的可能是“上海”“北京”这种中文名。如果不在接口层面做数据格式转换,模型就会出现“知道了但用不上”的尴尬。

我的解决办法是设计一个Context Store,在执行流程开始时定义好本次任务共用的标准数据模型,每个技能从Context Store读取自己需要的字段,执行后把结果回写其中。比如“城市编码”这个字段,在流程初始化时由city_resolver技能统一转换成统一格式,之后所有技能都从那一个字段里取数据。

这样做还顺带解决了一个问题——多技能并行调用时的数据一致性。比如同时查机票和酒店,两个技能用的起飞日期必须一样,从同一份Context Store读取就不会冲突。

3.4 技能编排中的参数解析与默认值策略

参数据面过了,但模型生成的参数往往仍然有格式问题。最常见的几个:日期写成“明天”而不是具体日期,金额写成“差不多3000吧”而不是精确数字,地址写全称还是简称不稳定。

我在每个技能入口都加了一个轻量的参数标准化步骤,通过大模型结合当前上下文推理出缺失参数。具体做法是:在技能执行之前,如果必填参数缺失,系统会触发一次补全对话,把已有信息转换成提示词,让大模型尝试补全缺失参数;如果不能补全,再向用户提问。

这里有个经验:尽量别让Agent主动向用户提问,能根据上下文推断的,先推断一次再问。你每次都问“请问日期是哪天”,用户会被烦死。通过上下文从对话里提取有效信息,是提升体验的关键。

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

4.1 模型总是选错技能怎么办

技能数量一旦超过50个,模型在意图识别阶段就开始飘。让我印象最深的一次,是用户说“把这个表格里的数据统计一下”,模型居然调用了send_email而不是data_analysis。后来查日志发现,问题出在send_emailSKILL.md里写了“通过邮件发送表格数据”,模型看到“表格”两个字就触发联想。

遇到这种问题,优先检查两件事:第一,SKILL.md里有没有容易混淆的措辞,把边界声明写得再明确一些;第二,技能之间的capabilities标签是否区分得足够开,已经有重叠就重新定义。还有一招,可以在SKILL.md里增加“负向示例”,明确告诉模型哪种情况不要调用它。这个办法对降低误召率非常有效。

4.2 技能调用很慢,上下文太长,怎么优化

特别典型的一个场景:Agent在完成一个复杂任务时,会把之前的思考过程、中间结果、调用日志全部留存在上下文里,等到调用第五个技能的时候,光是上下文就有几万token。大模型处理慢、容易丢焦点,最后整个流程跑下来又慢又不准。

优化方向有两个:压缩和遗忘。压缩是把中间的思考摘要化,只保留对后续决策有影响的关键信息,比如最终选择、票号、联系人信息。遗忘则是显式地从上下文里移除已经完成且不会再用的临时变量。

具体做法上,我在Context Store里给每一项都加了一个生命周期标记,标记为temporary的数据在步骤完成后自动删除,标记为persistent的数据才会被带入后续技能。实测下来,一个原本需要20k token的流程,压缩后能降到12k左右,响应速度提升明显。

4.3 工具调用失败:怎么设计有效的重试与兜底机制

技能执行失败是常态,不失败才反常。外部服务超时、参数格式错误、限流、数据不存在,一个技能能坏的方式比你想的多。这里最重要的原则是:失败不能让执行链路直接死掉,要有一个兜底出口

我的做法是给每个技能配置一个fallback_steps列表。假如search_flights主接口失败,第一步重试一次;再失败就切换备用供应商;还是没有结果,最后返回一个明确的“当前无法查询航班信息”给用户。

重试的关键是控制节奏。第一次重试等1秒,第二次等3秒,最多三次,再多就没有意义了。还要重点区分“可重试错误”和“不可重试错误”,参数错误这种不可重试的类型,立刻返回修正参数,不要浪费重试次数。

4.4 技能联调时的日志与链路追踪实践

多技能编排最怕的就是问题定位困难。哪个技能调用了哪个技能、传了什么参数、返回了什么结果,全链路没有日志节点就只能靠猜。

所以我在每一个技能入口和出口都打了结构化日志,包含请求ID、技能名、版本号、调用参数、返回状态和耗时。虽然是基础工作,但它帮我在排障时省了大量时间。有一次客户反馈“Agent回复了错误城市”,我顺着请求ID查到是city_resolver技能在传参时把一个城市编码映射错了,整个过程不到10分钟。

版本管理也要单独强调。修改技能代码时必须同时更新SKILL.md和版本号,不然调试时就会遇到“模型看到的说明是老的,代码已经跑了新逻辑”的荒诞情况。

5. 一些让我印象深刻的经验总结

技能库做到最后,你会发现真正复杂的不是代码,而是对“边界”的管理。技能内部实现得再优雅,如果SKILL.md没有写清楚边界,模型就会拿它乱用一气。所以我在每个技能评审时都会问三个问题:模型会在什么场景下想起它?什么场景下不该想起它?如果两个场景描述撞车了,哪个优先级更高?这三个问题想清楚,技能库的质量就稳了。

另外一个体会是,技能不能一次建模建得太完美。早期我总想着把一个技能的所有情况都覆盖到,结果就是每个技能都复杂得要命。后来改成“最小可用加持续迭代”的策略,先让技能能跑通主流程,然后通过日志和用户反馈不断补充边界情况,技能反而越用越顺。

如果你正在做Agent产品,或者准备把现有项目里的工具函数重构成技能体系,建议先从agent-skills的这套结构开始搭,哪怕最开始技能只有三五个,也坚持按SKILL.mdschema.jsonhandler的标准来。技能量小的时候留好规范,规模上来之后才能兜得住。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询