ARTICLE DETAIL

资讯详情

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

Agent技能体系设计实战:从Function Calling到SKILL.md

Agent技能体系设计实战:从Function Calling到SKILL.md 做AI Agent开发的朋友八成都有过这种体验工具函数一多模型就开始犯迷糊。我去年在做内部智能助手的时候最初把十几个工具的函数定义全部拼进system prompt结果提示词长到每次请求都心里发慌模型还是频繁选错工具。后来我搭了一套专门的技能体系取名agent-skills才算是真正解开了这个结。这篇文章不聊花哨的概念就把我设计这套东西时的完整思路、关键选型和踩过的坑拿出来晒一晒。适合正在做Agent功能开发的工程师参考也适合想给自己小助手加技能包的独立开发者。看完你应该能自己搭出一套可维护的技能注册表出来。1. 为什么Agent需要一套独立技能体系——function calling的痛点1.1 老办法把几十个工具定义全部塞给模型先说我最开始的做法估计很多人跟我一样。当时系统里有大概17个函数每个函数都是标准的JSON Schema格式函数名、参数类型、必填字段、描述全部拼进system prompt。刚开始只有七八个工具的时候还好模型基本能选对。但等工具数量爬到二十多个问题立刻暴露了。首先是prompt膨胀。17个工具的定义加起来大概4000多个字符到30个工具的时候直接逼近7000字符。每次请求都背着这么重的上下文token成本上去了模型的注意力也被稀释了。最要命的是函数签名的表达能力非常有限。比如查天气我有两个接口get_weather(city) get_weather_detail(city, days)从函数名上看好像一个查当前天气一个查多天预报。但模型的决策逻辑不是人脑它看到这两个名字相近的工具经常在该用get_weather_detail的时候调了get_weather或者反过来。尤其当用户问明天出门要不要带伞这种需要综合判断的问题时模型根本不知道应该先去查天气再看温度最后给我调了个最像的。等函数数量再往上走你会发现每次新增一个工具都得重新通读所有已有工具的描述担心新工具的加入会把旧工具挤下去。这种牵一发而动全身的维护方式基本宣告了全量塞prompt这条路走到头了。1.2 技能的本质原子能力加使用说明加执行策略后来我换了个思路与其让模型在一个巨大的工具列表里大海捞针不如把每个能力封装成一个技能。一个技能不只是包一层函数它是三样东西的组合原子能力真正干活的执行函数比如发起HTTP请求、读文件、调API。使用说明书描述这个技能在什么场景下用、参数怎么填、输出怎么解读、有什么注意事项。执行策略超时时间、失败重试次数、权限要求、副作用处理。你可以把function calling理解成一本电话簿每个工具就是一个人名和一个电话号码。电话簿只告诉你号码但不会告诉你这个人是什么性格、什么时候该找他、找他办不成事该找谁替补。而技能像一份岗位说明书模型需要明确知道这个岗位什么时候介入、负责什么、边界在哪里。这个思路其实特别像给人写交接文档你不可能让新人把公司所有系统源码都读一遍但你一定会给他一份遇到什么问题找哪个系统的清单。技能就是AI的交接文档。1.3 技能边界怎么划一个用户可感知的能力划分技能边界的时候我总结出一个简单的判断标准如果一个用户问题可以被一个技能完整回应那这个技能的边界基本合适。反过来如果一个技能内部塞了多个互不相关的操作比如既查天气又订酒店又算出行路线那它就是一个缝合怪应该拆开。我给团队定的自测问题清单这个技能的能力能被一句话说清吗说不清就拆。用户问你能帮我做X吗X能对应到这个技能吗对不上就说明技能缺失或描述有偏差。技能内部的多个分支是同一类操作的变体还是完全不同的能力后者需要拆分。技能之间是否会频繁出现我调你、你调我的情况会的话说明编排逻辑应该上移。单个技能的描述必须单一清晰。技能本身不追求大而全追求的是让模型一看描述就知道这事归我管。2. 技能怎么设计才不绕目录结构、描述文件与运行时链路2.1 文件目录即技能注册表我在设计agent-skills时技能存储没有用数据库而是直接用文件目录Git管理。每个技能一个文件夹命名就是技能ID结构如下agent-skills/ registry/ city_weather_query/ SKILL.md schema.json implement.py meeting_minutes/ SKILL.md schema.json implement.py air_quality_query/ SKILL.md schema.json implement.py loader.py executor.py这个结构有几个好处。第一每个技能的所有信息内聚在一个目录里新增技能就是新增文件夹删除技能就是删文件夹代码评审的时候一目了然。第二天然支持Git版本管理技能的Description改了什么、schema调整了什么历史记录清清楚楚。第三不引入额外的基础设施团队里任何人打开目录就能看懂当前系统有哪些能力。我在loader.py里做的事很简单遍历registry/下所有子目录解析每个目录里的SKILL.md和schema.json构建出内存中的技能索引。每次服务启动时加载一次加载完就缓存住。技能文件有变化就重新加载实现基本的热更新。2.2 SKILL.md写给模型看的岗位说明书技能描述是整个体系里最核心的部分它直接决定模型能不能选对技能。我见过很多人把技能描述写成一行的功能列表比如查询天气这种描述基本等于没写模型只能靠猜。一份合格的SKILL.md至少包含这些部分字段作用写作要点名称与摘要让模型快速识别技能主题一句话说清做什么适用场景告诉模型什么时候优先选我列出具体场景关键词和典型用户表达不适用场景告诉模型什么时候别选我明确区分相邻技能的边界参数说明指导模型正确填参数说明语义、单位、枚举值、默认值使用示例给模型一个模仿样例提供典型问题到调用参数的映射注意事项补充分歧规则比如用户未提参数时不要默认填充我之前踩过一个很典型的坑天气查询和空气质量查询这两个技能在描述里各写各的结果模型经常把北京空气好吗这种问题调到了天气查询上。后来我在天气技能的SKILL.md里加了一段不适用场景用户咨询空气质量、PM2.5、雾霾等请使用air_quality_query技能选型准确率立刻上来了。这个细节非常重要相邻技能的边界要靠互指来划清。下面是我后来沉淀出的一个技能描述模板示例# city_weather_query 查询指定城市当前天气和未来3天天气预报。适用于出行规划、户外活动安排、穿衣建议等场景。 ## 适用场景 - 用户询问北京今天冷不冷上海周末会下雨吗等天气相关表达 - 用户需要根据天气做决策是否带伞、是否适合户外活动、要不要加衣服 - 用户给出城市名并明确或隐含要求天气信息 ## 不适用场景 - 查询历史天气数据请使用 history_weather_query - 查询空气质量指数、PM2.5、雾霾情况请使用 air_quality_query - 查询未来超过7天的天气预测请使用 long_term_forecast_query ## 参数 - city: string必填中文城市名例如 北京、上海、深圳 - days: number可选预报天数取值0-7默认3 ## 示例 用户问深圳周末适合爬山吗 调用city_weather_query(city深圳, days3) 用户问北京明天什么天气 调用city_weather_query(city北京, days1)你可能会问这份文档是给人看的还是给模型看的答案是给模型看的。SKILL.md会被加载到prompt里作为模型选择技能的依据。所以写作时要用模型容易理解的方式场景词越多、示例越具体模型选对的概率越高。2.3 运行时链路加载、选型、校验、执行技能的运行时流程我分四步走加载技能索引服务启动时loader.py扫描注册表把每个技能的SKILL.md摘要和schema.json放入索引。这一步只加载元信息不加载真正的实现函数保证启动速度快。模型选择技能把用户问题和技能索引一起发给模型要求模型返回结构化的技能调用指令输出的格式是{skill_id: city_weather_query, params: {city: 深圳, days: 3}}。参数校验系统层拿到模型返回的调用指令后先用schema.json里的JSON Schema做严格校验。这一步绝对不能在模型层直接执行因为LLM的输出是概率性的参数幻觉很难完全避免。执行与返回校验通过后executor.py加载implement.py里的具体函数执行时带上超时时间。执行结果统一包装成{status: success, data: ...}或{status: failed, error: ...}。第3步的严格校验非常重要。我见过很多Agent项目栽在这里模型输出了一段符合语法的JSON但参数语义不对比如用户没让我查未来5天模型生成了days5或者城市名生成了英文拼音Beijing而不是中文北京。这些问题如果在执行层才暴露不仅浪费一次调用还会让用户觉得这个助手很笨。3. 选型与取舍存储、编排、多Agent共享3.1 技能存哪文件目录、代码注册还是数据库技能元信息的承载方式直接影响团队的维护体验。我对比过三种方案方案优点缺点适合场景文件目录Git可读性好、评审直观、版本管理天然动态热更新弱、不适合大规模动态分发技能数量50、团队维护能力强代码装饰器注册类型安全、IDE补全友好技能信息散落在代码中非技术人员难维护纯研发团队、技能全部代码化数据库存储支持动态增删、按用户分配、远程管理基础设施成本高、变更要过服务技能数量大、多平台共享、需要运营配置我自己的选择是文件目录起步因为小团队最需要的是降低理解和维护的摩擦。等技能量上来、多个Agent平台共用一套技能库的时候再迁移到数据库这是更平滑的路径。技能描述这种文本放数据库反而不如放文件夹里读起来直观。3.2 编排策略全动态选择还是半编排技能编排有两种模式一个是纯靠LLM在完整技能列表里动态选择另一个是先用规则或分类器锁定候选技能集再让LLM在候选集内选择。纯动态选择的优点是灵活技能列表再大模型也能看到全部但问题是当技能数量到30个以上时模型的选择准确率明显下降而且每次请求传给模型的技能描述很长token成本高。我采用的是半编排入口处加一个意图识别模块把用户问题初步归入某个技能域比如天气决策、会议纪要、日程管理然后只把这个域内最多五个候选技能的描述发给模型。模型在候选集里做选择决策空间大幅缩小准确率提高了开销也降下来了。这一步可以用简单的关键词规则分类模型做不必每次都用满血大模型。3.3 多Agent场景技能共享和权限隔离当多个Agent共用一套agent-skills体系时要考虑的不只是有哪些技能还有这个Agent能用哪些技能。比如一个客服Agent和一个内部运营Agent前者应该能调用退款查询、订单状态技能后者能调用数据分析、报表生成技能。如果两者共享同一个技能注册表风险很大。我的做法是在技能元信息里加一个scope字段声明这个技能允许哪些Agent使用。Agent启动时从注册表拉取自己的技能列表形成权限视角。同时每个技能有version版本号注册表强制(scope, skill_id, version)唯一禁止同ID不同版本同时加载。升级技能时显式标注版本变更不让线上Agent悄悄换了行为。4. 踩坑记录模型不选技能、参数幻觉、技能冲突4.1 模型死活不选这个技能怎么办先说一个让我挠头的问题明明技能库里有air_quality_query用户问北京今天能见度怎么样适不适合跑步模型却调了city_weather_query返回了一堆温度和降水概率就是没有空气质量。排查过程是这样的我先看日志确认模型不是没看到air_quality_query这个技能因为技能列表里明晃晃地在第二行。然后我怀疑是描述触发词不够用户问题里虽然有跑步这个户外活动词但技能描述里根本没有跑步运动锻炼这些场景关键词。模型的选型逻辑是语义匹配描述里没有的场景词它很难自己推导出来。解决办法是给air_quality_query的SKILL.md加场景词跑步、户外运动、敏感人群、哮喘、儿童防护这一类同时在天气技能描述里增加负面清单用户关注空气污染、跑步条件、呼吸健康时请使用air_quality_query。改完这版描述之后实测这个场景的选择准确率从82%提到了94%左右。我的经验是技能描述的外部性很重要只写自己是什么远远不够还要明确说自己不是什么、什么情况下应该去隔壁技能。4.2 参数幻觉模型自作主张填参数这是我在Agent里遇到的最普遍的问题。用户说查下杭州天气模型调city_weather_query(city杭州, days7)用户明明没说要7天用户说看看明天上海温度模型调参数时把city填成北京市——这种幻觉五花八门。我的应对策略有三层第一层schema.json把能约束的都用JSON Schema约束住。比如city字段虽然是string但我会在描述里明确枚举常见城市并标注用户未提供城市名时不得默认填充。有时间字段的用枚举值限制能不让模型自由发挥的地方尽量不给自由发挥空间。{ type: object, properties: { city: { type: string, description: 中文城市名如北京、上海、深圳。用户未明确提及时不要填写。 }, days: { type: integer, minimum: 0, maximum: 7, default: 3 } }, required: [city] }第二层系统层严格校验。校验失败时绝不执行技能而是返回一条类似参数校验失败days字段不在允许范围内的错误信息再让模型根据错误信息重新生成参数或者向用户补充提问。第三层在SKILL.md里写死一条规则所有参数必须是用户明确给出的信息推导而来缺少必要参数时须回问用户严禁擅自填充。这一条单独写和放在注意事项里都行关键是语气要明确不要给模型留含糊空间。4.3 技能同名冲突和版本混乱有段时间我放开让几个同事各自注册技能结果出现了两个report_generator一个生成周报一个生成月报。加载顺序一换行为就变了非常隐蔽。注册表强制唯一键后彻底解决了这个问题。唯一的键是namespace skill_id version比如team_A.report_generator.v2。另外加了一道构建期的静态检查遍历所有SKILL.md发现重复的skill_id直接报错不让服务起来。这道检查花不了多少代码量但省了我很多半夜排查问题的时间。4.4 技能互相调用导致的死循环早期版本我允许技能内部调用其他技能想着可以快速复用逻辑。但很快发现技能A在某种边界情况下调了技能B而技能B内部又在某种情况下调回技能A两个技能在运行时互相等待直接把Agent卡死。后来我定了硬性规则技能内部禁止调用其他技能。技能保持原子性技能之间的所有组合逻辑统一上升到编排层处理。编排层可以设计DAG有向无环图来显式声明技能的调用顺序也可以允许LLM按顺序生成多个技能调用。集中控制之后组合逻辑清晰可审计出了问题也容易回滚。5. 质量评估与持续迭代5.1 三层测试金字塔技能体系上线后必须像正规软件工程一样建测试不然描述改着改着就把行为改歪了。我按三层来组织第一层是技能级单元测试直接测implement.py里的函数。输入边界值、异常入参、mock外部API保证每个技能自身行为正确。第二层是选型场景测试这是技能体系特有的测试层。准备一批典型的用户问题比如明天北京气温多少附近有适合跑步的公园吗断言这些问题应该映射到哪个技能。这个测试直接验证SKILL.md的描述质量每次修改描述都要跑一遍防止改A技能把B技能的选型率拉低。第三层是端到端测试走完整的用户对话流程验证问题是否真正被解决而不是只停留在模型调了正确的技能这一步。5.2 用调用日志反推迭代描述技能描述不是写完就完了它是需要拿着数据持续迭代的活文档。我上线后重点看三组指标指标含义触发迭代的信号技能调用率技能被选中的次数长期为0的技能要检讨描述大概率场景词没写对误选率模型选中但用户反馈不对超过一定阈值就说明描述与真实场景有偏差参数修正率首次参数校验失败后重试成功高就表明SKILL.md的参数说明不够清楚有一段时间history_weather_query调用率极低排查发现是它的描述写得太像city_weather_query模型不管什么天气问题都选了后者。我把历史查询技能改成强调历史、往年、去年同期这些触发词调用率明显回暖。技能描述要用数据说话不要靠直觉拍脑袋。5.3 下一步从单一技能到技能组合当技能数量积累到一定程度真正的价值开始体现在组合上。比如周末活动建议这个需求单靠天气技能回答不好它需要city_weather_query加air_quality_query加poi_recommend三个技能协作。我不会把这三个技能揉成一个巨型技能而是在编排层定义一条组合技能配置指定技能调用顺序、参数流转、结果整合方式。组合技能的配置文件里记录用到了哪些原子技能、各自版本这样原子技能升级后组合技能可以重新验证。往远了说如果技能协议足够标准化跨团队共享技能库、甚至做一个内部技能市场都是水到渠成的事。技能按调用量、好评率排序新项目直接订阅需要的技能包这比从零开始积累每个Agent的能力高效得多。回头看我搭agent-skills这套体系最值钱的不是代码而是把模型怎么选技能这件事想透了选型靠的是描述的场景化不是词数堆砌执行靠的是严格的系统校验不是模型自觉维护靠的是数据反馈不是拍脑袋。你要是也想给自己的Agent加一套技能体系不用上来设计太复杂先把十个技能的SKILL.md写扎实、写清楚再跑一轮场景测试一定会比一把梭把所有API都塞给模型舒服得多。
返回列表