ARTICLE DETAIL

资讯详情

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

Agent技能库设计:从function calling到稳定落地

Agent技能库设计:从function calling到稳定落地 这些年做AI应用我最大的一个体会是模型选型定下来之后真正决定Agent能不能落地的往往不是提示词写得有多花哨而是脚下那个“技能层”厚不厚。我最近在维护一个叫agent-skills的个人项目简单说它就是一套给Agent用的技能库设计方案前端对接大模型后端接各种工具接口中间负责技能注册、参数校验、调用编排、结果反馈这一整套机制。这个项目解决的是特别具体的一个痛点Agent有了模型推理能力之后怎么才能真正“动手干活”。它适合正在做Agent产品化、准备接function calling但不知道如何组织工具的开发者也适合那些想从单个Demo Agent往多Agent协作方向走的团队。今天这篇就把我在agent-skills项目里的完整设计思路、实现细节、踩坑经验一次讲清楚你可以直接照着搭一套自己的技能库。1. 项目想解决的问题Agent为什么需要独立的技能库1.1 模型负责“想”技能库负责“做”先理清一个容易被忽略的事实大模型本质上是推理器不是执行器。你问它“北京明天天气怎么样”它能接话但它自己并不会真的去查天气接口。要让它真正完成一次查询必须给它提供可调用的外部能力也就是工具或者函数。但这里有个尴尬的地方如果你只给模型一堆零散的API接口它经常不知道该用哪个、怎么用。比如你同时提供了“获取天气api”和“穿衣建议api”模型可能为了回答“明天穿什么”直接调了天气接口然后自己瞎编一段穿衣建议而不是先查天气再调穿衣建议接口。这类问题不是模型不够聪明而是工具层太混乱缺少一层“能力管理”的中间层。agent-skills的核心思路就是在这中间加一层把原子化的API封装成有明确语义的“技能”每个技能包含名称、描述、参数规范、执行实现。模型面对的不再是一堆裸接口而是一份结构清晰的技能清单。它只需要理解每个技能是做什么的、需要什么参数然后像点菜一样选择技能剩下的参数补齐、接口调用、结果解析都由技能层完成。1.2 一个让我决定重构的线上案例我最早做Agent的时候图省事直接在系统提示词里写了一大段工具说明然后把十几个Python函数注册为核心调用。初始demo跑得很顺直到上了真实用户问题接踵而至。最典型的一个案例那是个日程助手Agent用户说“帮我看看这周五下午有没有空”。模型理解得很对也确实触发了工具调用但它把参数里的日期传成了“周五下午”这个字符串而不是具体的日期格式日历接口直接报错。更麻烦的是报错信息返回给模型后模型又开始“脑补”说“好的我已经帮你查询了周五下午的安排”实际上什么都没查到。第二个案例是技能冲突。我当时同时在工具列表里挂了“发送邮件”和“发送短信”两个功能它们的参数结构几乎一样只是接口地址不同。结果模型经常在用户说“发消息”的时候随机选一个完全看它当时心情。这让我意识到简单的工具列表根本没有约束力必须有一套成体系的技能管理框架。于是agent-skills项目成型了。它要解决的核心问题有三个让模型稳定选对技能、让参数传递不出错、让技能执行失败时能被识别和处理而不是被模型“脑补”过去。1.3 agent-skills的项目定位与设计边界agent-skills定位成一套轻量的、面向Agent的能力管理框架它只解决“模型正确调用技能”这一件事不碰对话管理、不碰用户状态、不碰记忆系统。你可以在任何Agent框架里集成它也可以单独把它当成一个工具调度服务来用。项目的几个核心模块分别是技能注册中心、技能描述管理、参数校验器、技能执行器、结果标准化模块、技能编排器。后面我会逐个拆开讲。它不适合做什么也值得一提如果你只有两三个工具不需要上这套框架直接在提示词里做好说明就行。技能库的价值在技能数量超过十个、甚至几十个之后才会充分体现出来。还有就是它不负责解决模型本身的推理质量问题模型太弱技能库再强也白搭。2. 技能库的整体结构设计2.1 任务、技能、工具三层抽象为什么是三层最早期我试图直接让Agent调用工具函数后来发现这是个坑。工具的命名往往偏底层比如send_sms_via_aliyun、query_mysql_order模型看到这种名字根本很难判断它适不适合当前任务。更麻烦的是同一个业务能力可能包含多个底层操作模型需要自己编排组合这对推理能力要求极高经常出错。所以agent-skills引入了三层抽象任务层、技能层、工具层。任务层是用户意图比如“提醒我下午开会”技能层是Agent能够理解和操作的能力单元比如“创建日程提醒”工具层是真正执行操作的具体函数和接口比如“写入日历API”加“发送通知短信”。一个技能可以封装多个工具。以“创建日程提醒”技能为例它的内部实现可能是先调用日历服务创建日程再调用消息服务发送提醒。模型只需要选定这个技能剩下的内部编排由技能实现代码完成。这相当于把组合逻辑从模型推理下移到了确定性代码里大幅降低模型的认知负担。实际价值很直观技能数量级可以控制在模型能够轻松理解的范围内而底层工具可以无限扩展反正不需要全部暴露给模型。这也是agent-skills能支撑大型项目的原因。2.2 技能描述卡的写法决定模型能不能选对模型触发技能靠的是“描述”。很多人在这一步特别随意觉得description随便写两句就行结果模型总是选错技能。我测试下来技能描述的质量直接决定了技能选择准确率有些场景下差异能到30个百分点以上。一份合格的技能描述卡我一般包含四个部分。名字要动词开头语义明确比如create_calendar_event比event_calendar好太多。描述要讲清楚“这个技能在什么场景下用、解决什么问题、有哪些边界”特别是在相似技能之间必须明确区分边界。参数列表要完整每个参数都要有类型、含义、取值范围、默认值最好还带一两个示例值。最后是可选的调用示例给模型一个标准用法参考。举个我在项目里对比过的例子。旧描述是“发送邮件功能参数有收件人、主题、正文”。新描述是“当用户需要给指定联系人发送电子邮件时使用支持普通文本和简单的HTML内容。收件人参数必须使用标准邮箱格式如nameexample.com主题长度不超过100个字符正文长度不超过5000个字符。如果用户没有明确指定正文内容需要在调用前主动询问并确认。”改完之后这个技能的触发准确率从81%提到了96%。对应到代码结构每个技能描述卡我建议用独立配置来维护不写在代码里。下面是一个实际配置片段{ skill_name: create_calendar_event, description: 为用户创建一条日历日程。当用户表达需要安排会议、定时提醒、记录行程等意图时使用。, parameters: { type: object, properties: { title: {type: string, description: 日程标题, maxLength: 50}, start_time: {type: string, description: 开始时间ISO 8601格式如2025-03-01T10:00:00}, end_time: {type: string, description: 结束时间ISO 8601格式必须晚于start_time}, attendees: {type: array, items: {type: string, format: email}, description: 参会人邮箱列表} }, required: [title, start_time, end_time] }, examples: [ {title: 产品评审会, start_time: 2025-03-01T14:00:00, end_time: 2025-03-01T15:00:00, attendees: [pmexample.com]} ] }2.3 目录结构与技能注册流程agent-skills是拿Python写的目录结构简单每个技能都是独立模块方便多人协作和动态加载。核心目录大概长这样agent-skills/ ├── skills/ │ ├── calendar/ │ │ ├── skill.yaml │ │ └── impl.py │ ├── email_sender/ │ │ ├── skill.yaml │ │ └── impl.py │ └── weather_query/ │ ├── skill.yaml │ └── impl.py ├── core/ │ ├── registry.py │ ├── validator.py │ ├── executor.py │ └── router.py ├── config.yaml └── main.py注册流程分三步。第一步在skills目录下新建技能文件夹把skill.yaml描述文件和impl.py实现文件放进去第二步在配置文件里声明启用这个技能第三步触发热更新框架自动扫描新技能并注册到技能中心。整个过程不重启服务适合线上热迭代。skill.yaml和我在前面展示的JSON描述卡是对应的只不过改成了YAML格式写起来更省事。impl.py里需要实现一个统一接口接收参数dict返回标准化结果dict。框架通过约定大于配置的方式自动识别这两个文件完成注册。2.4 热更新机制让新技能上线不影响线上服务Agent类应用有个特性技能迭代频率极高。今天加一个“查询物流”技能明天优化一个“生成报表”技能如果每次都要重启服务用户侧的连接就会断体验很差。热更新我是这样做的框架启动一个后台线程用文件系统监听库我用的watchdog监控skills目录的变化。一旦发现skill.yaml文件被新增或修改就重新加载配置、校验合法性然后更新技能注册中心的技能列表。为了并发安全技能列表采用读写锁保护读取技能时走快照更新时重新构建快照后原子替换。还有一个容易踩的坑旧技能还在执行中新的实现已经上线了如果强行中断旧任务可能导致数据不一致。我的处理方案是让技能列表支持多版本共存已有执行的技能继续用旧版本跑完新请求走新版本。这个在热更新里一定要处理不然线上执行中的任务会莫名其妙报错。3. 核心模块实现与关键参数3.1 两阶段技能召回先粗筛再精排省token又提准技能少的时候把全部技能描述塞给模型做选择即可。但当技能数量超过二十个token成本和选错率都会明显上升。我实测过技能清单超过二十个之后模型的选择耗时和错误率都会快速增加特别是相似技能多的时候。agent-skills的解决方案是两阶段召回。先把当前用户请求和所有技能描述做一次快速匹配筛选出排名靠前的候选技能再把这些候选技能的完整描述卡塞给模型做最终决策。这跟搜索系统里的召回加精排是一个思路。初筛阶段我用的是两层混合召回。第一层利用向量库例如本地sqlite-vss对用户请求和技能描述做embedding相似度检索召回到Top 20第二层做关键词匹配把请求里出现的实体词、动作词和技能名、技能描述中的关键词匹配一轮找出可能被向量召回漏掉的技能。两层结果合并去重后取Top 5作为候选集。最后精排阶段把Top 5候选技能的描述卡完整拼进提示词让模型选出最合适的一个。这一步的提示词我固定为结构化格式要求模型输出技能名和置信度不允许多选、不允自定义。如果模型输出的技能名不在候选列表里框架直接拒绝并让模型重新选择这个约束能拦掉大量幻觉输出。3.2 参数Schema与幻觉拦截别让模型拿错参数调接口模型在生成技能参数时经常会产生幻觉典型的表现包括日期格式随意写、枚举值编造一个不存在的选项、必填参数漏传。这些问题如果不拦截直接传给下游接口轻则报错重则产生脏数据。agent-skills对参数校验非常严格。每个技能在注册时都会根据skill.yaml生成一份JSON Schema执行前先用这个Schema做校验。校验分三层类型校验、必填校验、业务规则校验。类型校验看字段类型是不是字符串、数字、数组这些必填校验看required字段有没有遗漏业务规则校验是自定义逻辑比如结束时间必须晚于开始时间、邮箱格式必须合法、枚举值只能在下拉列表里选。校验失败时框架不会简单把错误直接丢回给模型而是生成结构化的“补参建议”告诉模型具体哪个参数错了、正确的写法示例是什么。比如模型传了start_time: 星期五下午校验器回传的错误信息是“参数start_time格式无效期望ISO 8601格式示例2025-03-07T14:00:00”。我测试下来加上这个明确反馈后模型再生成参数的正确率提升非常明显很多情况下它看一眼示例就能自行修正。参数校验这块我单独抽了一个validator模块方便其他项目复用。核心校验逻辑很简单基于jsonschema库实现基础校验再挂自定义校验器。如果有需求可以对校验器的性能做优化目前单次校验的平均耗时不超1毫秒基本不构成性能瓶颈。3.3 结果标准化与模型反馈闭环技能执行完返回给模型的结果如果是一段混乱的报错模型很容易误解导致它顺势“编造”成功。这是Agent落地中非常容易翻车的地方。agent-skills对执行结果做了标准化封装所有技能返回值统一为固定结构。返回值结构包括三块status标明执行状态取值范围是success、partial_success、faileddata存放业务数据格式由技能自己定义但必须是可序列化的JSONmessage给人看的说明文字也是给模型看的总结语。任何异常都会被捕获并转换成failed状态不能让底层异常直接抛到模型层。这里有一个容易忽略的细节模型能读到的结果不要包含过多无用信息。比如查询订单返回了50个字段模型根本处理不过来。我的经验是每个技能在编写实现时就要考虑为模型提供一份精炼后的数据视图只保留关键字段并附上一段人类可读的总结。这不仅降低了模型理解成本也减少了它因为信息过载而产生幻觉的概率。3.4 一次技能调用的完整链路我用一次“查询订单并发送提醒”的任务来梳理完整链路方便你理解各模块是怎么协同工作的。请求进来后先由路由模块做两阶段召回找到最匹配的query_order技能和send_notification技能。框架按编排规则先执行query_order将用户请求中的订单号参数进行Schema校验合法后调用实现代码查询数据库结果封装成标准化结构返回给编排层。编排层读取status为success后将订单状态作为参数传给send_notification技能再次校验然后调用短信接口发送。发送结果依然封装成标准化结构最终由编排层把所有执行信息汇总成最终回复回传给模型。整个过程模型只需要做两次决策其余全部由确定性代码完成。这也是agent-skills方案的核心价值把风险高、模型不强项的部分转移到可控代码里模型只负责理解意图和选择技能。4. 技能编排与组合的实践玩法4.1 串行编排一个复杂任务拆成多段技能调用单个技能解决不了复杂任务需要技能编排器把多个技能串起来。agent-skills的编排器支持声明式和代码式两种编排方式。声明式适合固定流程比如“用户投诉处理”固定走“查订单、查物流、生成回复”三步代码式适合动态流程比如根据第一步的结果决定后续走哪个分支。以行业调研任务举例这个任务可以拆成“搜索行业资讯”“整理资讯要点”“生成调研报告”三个技能。用户提出请求后编排器先执行第一个技能拿到原始资讯列表再根据资讯内容调用第二个技能提炼要点最后调用第三个技能生成报告。如果中间任意一步失败编排器会根据重试策略处理重试仍失败则直接终止任务并把失败原因返回给模型不允许模型自己假装成功。串行编排最需要注意的是参数传递链。前一个技能的输出要能映射到后一个技能的输入。我通常会在编排定义里写明参数映射关系避免技能间参数名不一致导致传参丢失。比如技能A输出order_id技能B需要orderId就需要在编排层做一次字段映射而不是靠模型自己去猜。4.2 并行技能与结果合并串行执行慢而且有些任务步骤之间没有依赖关系是可以并行执行的。比如一个“写周报”工具需要同时查本周日程、查本周任务完成情况、查本周邮件数据这三个查询互相独立完全并行执行能大幅降低总耗时。agent-skills的编排器支持声明并行组。开发者可以指定某个步骤组内的多个技能并发执行。实现时用的是Python的并发任务机制比如asyncio.gather或者线程池。各技能执行完后编排器等待所有任务返回再做结果合并。并行执行有两个坑。第一个是下游接口的并发限制比如同时调五个查询技能每个都要访问第三方API很容易触发限流。我的解决办法是给每个技能配置独立的并发上限并使用信号量控制并发量。第二个坑是部分技能失败的情况并行组里如果有一个技能失败是让整个任务失败还是继续通常应该继续执行其他任务最后在汇总阶段标记部分失败并让模型知道哪些信息是缺失的。4.3 冲突消解与降级策略技能库里技能多了以后会出现能力重叠。比如“获取天气”和“获取空气质量”用户说“今天适合跑步吗”模型可能纠结调用哪个。我在设计描述卡的时候会主动写出区分规则但如果模型还是选错就需要冲突消解机制兜底。我的做法是为相似技能组设置优先级当模型输出多个候选技能且有重叠时按优先级排序取最高级。同时相似技能在描述卡里互相添加交叉引用比如天气技能描述里加一句“如果需要查询空气质量指数请使用get_air_quality技能”这样模型在模糊场景下也能得到引导。降级策略也是必须设计的。比如“发送邮件”这个技能正常走SMTP服务如果发送失败可以降级为保存草稿并通知用户手动发送。agent-skills里每个技能可以配置一个降级链当主路径失败时按顺序尝试备用路径。降级逻辑必须前置设计否则在故障时临时写逻辑很容易出事故。5. 踩坑记录与问题排查5.1 模型死活不调技能大概率是描述卡的问题如果模型面对用户请求完全不做技能调用第一个要查的就是技能描述卡和当前技能列表是否匹配。我遇到过一种情况技能库里明明有查询天气的技能描述也写得很清楚但模型就是不调排查半天发现是技能描述放在系统提示词的旧版本里新版本没有包含。还有一种高频原因技能描述太过晦涩。比如你写“获取天气预报数据支持通过地理坐标参数检索该坐标所在地理位置的天气情况”模型看了可能压根反应不过来这是“查天气”。描述要面向用户意图来写而不是面向实现细节。后来我统一改成“当用户询问某地当前或未来几天的天气情况时使用”触发率立刻上来了。最后一个排查点是确认模型输出了tool_calls但被框架拦截了。仔细看日志如果模型确实输出了技能名但没有输出参数或者参数缺失需要检查是不是参数schema设置不合理比如必填参数太多模型生成不全。建议必填参数尽量精简能默认的就给默认值。5.2 参数幻觉查不出来用“示例值强校验”双保险模型幻觉参数是高频问题。虽然前面讲了Schema校验但校验只能发现格式错误不能解决语义错误。比如用户问“上海明天天气”模型生成了正确的技能调用但把经纬度填成了北京的坐标格式没错语义错了校验器根本拦不住。针对这种问题我的经验是两步走。第一步在描述卡和参数schema里强化示例值与约束信息比如写明“当用户提到上海时传入lat31.23, lon121.47”这相当于在提示词层给模型“喂标准答案”。第二步是在技能实现里加入语义校验比如天气查询技能发现经纬度对应城市与用户问题是同一区域时才放行否则返回失败并要求确认。不要指望模型一次就把参数生成对这是不现实的。正确思路是让错误参数被识别出来然后给模型一个清晰的修正路径允许它重试。agent-skills做了参数错误自动重试机制最多允许两次自纠错超过后转为人工确认流程。5.3 技能链路超时和失控循环加了Agent之后一个不容忽视的问题是响应时间。模型决策本身要几秒钟再加上多个技能串行执行用户端很容易体验到几十秒的等待。如果不控制Agent还可能会在技能之间来回跳转形成失控循环一直不收敛。我的处理方案是给整条调用链设置超时和轮次上限。每个技能单独设超时时间一般是5到10秒超时后终止执行并返回failed。整个任务设置最大执行轮数比如8轮超过后强制停止并让模型基于已有信息做总结不能再发起新的技能调用。在编排层的实现里每一轮迭代结束都要检查是否到达最大轮数同时记录每轮的技能调用历史。如果发现同一对技能反复调用超过三次直接判定为死循环终止任务。这些兜底机制在真实线上场景里很重要没有它们Agent很容易处于不受控的状态。5.4 问题排查速查表现象可能原因排查方向解决方案模型不调用任何技能技能描述未注入提示词检查当前提示词里技能清单从技能注册中心动态生成提示词段落模型选了错误技能技能描述相似度高、边界不分对比相似技能的描述卡增加边界说明与交叉引用参数格式错误Schema约束不够明确查看校验错误日志加强类型约束补充示例值参数语义错误描述卡缺地理或业务映射检查参数是否按示例生成描述内写明用户说法到参数的映射技能执行失败但模型说成功结构异常被模型吞掉检查返回标准化结构结果统一封装为statusdatamessage任务超时技能链路过长或循环看执行日志中的轮次记录设置最大轮数和技能超时热更新后新技能不可用配置未声明或YAML解析失败看启动日志的注册信息检查配置声明与YAML语法6. 度量、反馈与迭代路线6.1 技能调用质量的四个核心指标不要凭感觉优化技能库数据要能度量。agent-skills在上线后我会重点看四个指标。第一个是技能选择准确率即模型选中的技能是否为正确技能分母是所有技能调用次数分子是正确调用次数。第二个是参数校验通过率即一次调用中参数第一次就通过校验的比例。第三个是技能执行成功率即技能代码实际返回success的比例这个指标能反映下游接口稳定性和内部逻辑质量。第四个是任务完成率即多技能编排任务最终按预期完成的比例这个指标最接近用户体验。指标计算方式建议目标技能选择准确率正确选择次数 / 总调用次数高于90%参数校验通过率首次校验通过次数 / 总调用次数高于85%技能执行成功率success返回次数 / 总调用次数高于95%任务完成率编排任务成功数 / 编排任务总数高于80%这些指标在框架里都有埋点。每次技能调用都会记录一条日志包含请求ID、技能名、召回候选、模型决策、校验结果、执行状态、耗时。日志以JSON格式写入日志文件后续同步到分析平台做可视化看板。没有度量就没有优化方向这一步千万不能省。6.2 用失败case反推技能优化指标只能告诉你哪里有问题不能告诉你为什么有问题所以要建立失败case复盘机制。我会定期把技能调用失败的日志拉出来按失败原因分组逐个分析。有个印象很深的case天气技能执行成功率一直只有85%左右排查日志发现是第三方天气接口经常在整点前后返回超时。于是我给这个技能加了一级缓存把最近10分钟内的天气结果缓存起来超时的时候走缓存兜底成功率直接提到了97%。这个case如果只看指标你可能会往模型和参数方向排查但实际根因在下游接口稳定性。失败case复盘这件事一定要常态化。建议每周固定抽时间把上一周的高频失败case过一遍形成问题清单逐个修复。agent-skills能有现在的稳定性很大程度是靠这轮轮复盘堆出来的。6.3 从技能库到技能市场后续可以这样扩展agent-skills目前的核心能力集中在单体技能库但它天然具备向更多方向扩展的潜力。首先是可以做多Agent共享技能库多个Agent服务同时连接同一个技能注册中心技能发布后所有Agent在线生效不用各自维护一份配置。其次是技能包的概念把一组相关技能打包成一个独立模块通过配置文件引入类似npm包的管理方式方便在不同项目间复用。还有灰度发布。现在技能更新是全局生效的如果新版本有问题影响面会很大。后续计划按用户维度灰度比如先让5%的请求走新技能版本观察指标后再逐步放量。最后是技能评估集给每个技能准备一批标准测试case每次修改描述或实现后自动跑一遍回归测试确保优化A技能不会影响B技能。这条路走通之后agent-skills就不再是个人工具而是一套可以对外服务的能力平台了。我个人在实际操作中最大的经验是维护Agent技能库功夫要花在模型之外。每次给Agent新增一个技能我会先把它的失败case截下来存好攒到10个case再统一复盘。很多表面上看是“模型不够聪明”的问题实际上一看描述文本就能发现是技能名太模糊、参数示例引用错了字段、或者返回格式没有标准化。把这些细节抠到位Agent才能真正稳定地干活。
返回列表