多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Agent技能库实战:设计技能、接入Function Calling与避坑指南

Agent技能库实战:设计技能、接入Function Calling与避坑指南 如果你最近在折腾 AI Agent应该会对agent-skills这个词不陌生。我大概从半年前开始把自己跑在 LLM 上的自动化 agent 全部改造成“技能库驱动”的模式。agent-skills说起来玄乎落地其实是一整套让模型稳定复用能力的方法论把重复性、确定性的动作从提示词里剥离出来沉淀成结构化、可检索、可执行、可回退的技能单元。这篇文章不聊框架源码只讲我在真实项目里怎么设计技能、怎么定义 schema、怎么把技能接入模型调用链路、以及踩过哪些坑。想做 Agent 工程化、或者正在被“模型今天能跑明天不能跑”折磨的朋友可以直接抄作业。1. 为什么要把 Agent 能力拆成技能库1.1 从“模型灵光一现”到“确定性技能”先讲一个让我决定做技能库的深夜调试现场。当时我给一个日程助手写提示词让它处理“帮我在下周找三个适合晚饭的时间段”模型发挥稳定的时候会输出结构化时间段但换个问法、换一批数据它就偶尔把时间格式从2025-06-03 19:00改成“下周三晚上七点”甚至直接凭空捏造一个不存在的聚餐用地。问题不在模型聪明不聪明而在“整理时间窗口”这件事本身是确定性的逻辑不该每次让模型临时发挥。技能化的思路是把这类动作变成一个个“技能”技能有名字、有描述、有参数声明、有对应的可执行函数。模型需要完成某个子任务时先通过技能描述判断该调用哪个技能再把目标参数提取出来交给技能执行最后把技能返回的确定性结果拿回去继续推理。这样一来凡是能写到代码里的逻辑全部交给代码模型只负责理解意图、解析上下文、串联流程。我实测下来同一组任务的输出稳定性能从 60% 提到 95% 上下核心就是“不再让模型做它不擅长的事”。1.2 技能、工具、插件的边界很多朋友会把技能和工具混为一谈我在设计agent-skills时把两者的边界划得很清楚。工具是最小可执行单元比如“发送 HTTP 请求”“查询数据库”“读文件”它不关心业务语义是纯原子能力。技能则是面向目标场景的组合单元比如“为用户推荐空闲时间窗”这个技能它内部可能调用日历查询工具、时间计算工具、格式校验工具甚至嵌套调用其他技能。插件在 my 的理解里是“技能的打包分发格式”一个插件可以携带多个技能和配置。简而言之工具给能力技能给场景插件给分发。维度工具Tool技能Skill插件Plugin粒度原子、单一职责组合、面向任务一组技能的发布包是否依赖上下文否是通常要理解场景目标是含配置与依赖声明外部表现一个函数一个带描述与参数的接口一个可安装的目录/包示例调用搜索 API整理会议纪要并邮件发送日程管理技能包这个区分不是教条而是直接影响后续的维护成本。技能库里如果有大量只有一行代码的“伪技能”检索和匹配会变得非常低效工具层足够厚技能层才可能轻巧。2. 技能库设计结构、命名与描述2.1 技能清单的本质是一份接口文档我最早犯的错是把技能清单写成“能力列表”比如“可以查询天气”“可以搜索网页”结果模型面对用户问题经常选错技能。后来我意识到技能清单本质上是写给模型看的接口文档不是给人看的宣传册。每条技能记录要回答四个问题这个技能做什么什么情况下应该用什么情况下绝对不要用需要哪些参数、每个参数长什么样我的技能 manifest 通常长这样{ name: recommend_time_slots, description: 根据用户的空闲时间范围和活动类型推荐合适的开始时间与持续时长组合。, when_to_use: 用户要求安排会议、约饭、订活动等场景且已经具备明确的开始日期、结束日期与时长偏好时使用。, when_not_to_use: 用户只是在闲聊时间话题并未提出具体安排请求时不要调用用户没有给出任何时间范围时不要调用。, parameters: { type: object, properties: { start_date: { type: string, format: date, description: 可用时间窗口的起始日期格式 YYYY-MM-DD }, end_date: { type: string, format: date, description: 可用时间窗口的结束日期格式 YYYY-MM-DD }, duration_minutes: { type: integer, enum: [30, 60, 90, 120], description: 活动建议时长单位分钟 } }, required: [start_date, end_date, duration_minutes] } }每次为技能写 manifest我都把它当成一次“面向陌生模型同事的接口评审”。描述写得越精确模型的选择准确度越高。这个规则直接影响了后面所有技能的设计。2.2 描述与参数的写作规范在agent-skills这个体系里写描述是有明确技巧的绝不是“把功能说清楚”就行。我总结了几条强制规范团队里新同学也按这个来效果稳定。第一描述必须以动词开头并且明确动作目标。不要写“这是时间推荐功能”要写“根据空闲时间与活动时长推荐一组符合约束的起始时间”。动词开头能给模型一个明确的执行指向减少歧义。第二写“何时不该用”比写“何时该用”更重要。模型在不该调用的场景下误触发通常是因为反向约束缺失。比如天气查询技能如果不写“用户问历史天气且没有城市参数时不要调用”模型就可能带着空城市去调用返回错误结果还影响了主流程。我在每个 manifest 里都保留when_not_to_use字段实测能减少三成以上的误调用。第三参数名和枚举值要贴近领域语言。参数叫dur不如叫duration_minutes枚举值给 30/60/90/120 而不给 “short/medium/long”。模型本质上是在做文本匹配清晰、具体、可校验的参数在解析时几乎不会被填错。2.3 技能的版本与依赖技能库用得越久版本问题越突出。技能 A 依赖技能 B 输出的数据结构如果 B 改了输出格式A 毫不知情等到线上断链才发现。我采用的方案是每个技能有独立的version并在 manifest 中声明dependencies。举个例子技能recommend_time_slots依赖技能parse_user_time_context的输出后者如果从返回{date: 2025-06-03}改成返回{start: 2025-06-03T00:00:0008:00}前者必须有明确的适配版本。我用语义化版本号管理dependencies里写parse_user_time_context: ^2.1.0并在加载器里做版本校验不符合声明就直接拒绝加载并提示。这样虽然多了一点维护动作但换来的是技能库在快速迭代中依然能保持稳定。3. 技能注册、发现与执行流程3.1 注册一个技能的完整步骤技能库不是把代码扔进目录就完事我有固定的注册流程避免“代码能用但系统不认识它”。第一步在skills/下建立技能目录结构与模块名一致比如skills/recommend_time_slots/。第二步写manifest.json定义名字、描述、参数、依赖版本。第三步编写执行文件run.py暴露统一入口def run(inputs: dict, context: dict) - dict: ...这里的context会传入调用链上游已经落地的临时变量、用户画像、环境配置等数据inputs则是模型解析出的参数。第四步运行本地校验脚本它会自动检查 manifest 的 JSON schema 完整性、run函数签名是否合规、依赖版本是否存在。全部通过后技能才会进入注册表被检索索引收录。我遇到过跳过校验直接把代码推到线上结果技能列表能看到但执行器一直报module not found后来就再也没省略过这一步。3.2 技能调度选择、匹配与回退技能库大了之后不可能像最早那样把所有技能描述都塞进系统提示词。我现在的做法是两阶段调度先从索引里召回候选技能再把候选技能描述交给模型做精排。召回阶段用传统检索就够了把技能描述、参数说明、使用场景文本拼成一个文档向量用 embedding 加固件关键词匹配做 top-k 召回。精排阶段把这 k 个技能的 manifest 交给 LLM让它根据用户问题判断用哪个。这里有个关键设计如果精排结果里所有技能的匹配分数都低于阈值我宁可让 agent 走“通用小模型兜底路径”也就是只用最普通的模型能力回答也不去硬调技能。实践证明强行调用的失败成本远高于不调用的成本。我还实现了简单的回退机制。模型选了技能 A但执行抛出参数校验错误或超时调度器不会直接死心而是把错误信息反馈给模型让它重新整理参数再来一次。默认只回退一次超过一次就转向人工兜底避免模型在同一错误上无限循环。这个机制看着朴素但极大提升了整个 agent 系统的容错能力。3.3 执行监控与结果校验技能执行过程必须可观测否则出了问题你根本不知道是模型选错技能、参数解析错、还是技能代码本身有 bug。我有一套基础的日志格式每次技能执行都记录五段信息技能名、入参快照、出参快照、耗时、错误堆栈。入参快照尤其重要因为后续排查“模型是不是填错参数”全看它。结果校验层面每个技能返回的数据都要做 schema 校验不允许返回任意结构。比如时间推荐技能规定返回{slots: [{start: ..., end: ...}]}如果执行函数返回了别的结构会被判定为执行失败纳入回退流程。这看起来会增加一点代码量但对后面的技能复用和依赖管理帮助巨大——你可以放心地让技能 A 调用技能 B因为知道 B 的输出一定符合契约。4. 手把手实现一个最小技能库4.1 从零定义三个技能理论讲再多不如直接撸一个最小可跑的技能库。假设我要做一个日程助手 agent先定义三个技能parse_time_expression把“下周一晚上八点”解析成结构化时间、query_calendar_free_slots查询日历空闲时段、recommend_duration根据活动类型推荐活动时长。每个技能都独立目录、独立 manifest、独立 run 函数。核心代码不复杂关键在统一约定# skills/parse_time_expression/run.py from datetime import datetime, timedelta def run(inputs: dict, context: dict) - dict: text inputs.get(text, ) # 这里是简化的解析实现生产环境建议用专门的时间解析库 if 明天 in text: base datetime.now() timedelta(days1) elif 周一 in text: base datetime.now() timedelta(days(0 - datetime.now().weekday() 7) % 7 or 7) else: base datetime.now() return {parsed_start: base.strftime(%Y-%m-%dT%H:%M:%S), confidence: 0.9}query_calendar_free_slots内部会调用一个模拟的日历 API返回一组空闲区间recommend_duration则根据活动类型查表返回建议时长。这三个技能组合起来就能处理“帮我安排下周一出差前的晚饭”这样的真实请求。4.2 将技能接入 LLM 的 function calling定义好技能后下一步是把技能 manifest 转化为模型接口认识的 tools 参数。现在主流模型基本都兼容 OpenAI 风格的 function calling我直接用tools数组传进去tools [skill.to_tool_schema() for skill in loaded_skills] resp llm.chat( messages[{role: user, content: 帮我安排下周一晚上的聚餐时长一个半小时}], toolstools, tool_choiceauto )to_tool_schema()做的事情很简单把 manifest 里的name映射成function.namedescription拼上when_to_use和when_not_to_useparameters直接透传。这里有个值得注意的细节很多框架默认只把 manifest 的description传给模型把“使用约束”丢了我强烈建议拼进去这对选择准确率的影响非常直观。模型返回 tool call 之后我的调度器会解析出function_name和arguments再从技能注册表里加载对应模块执行最后把结果拼回对话上下文继续让模型产出最终回答。整个流程串起来就是一个最小可用的agent-skills原型。4.3 测试一条完整 Agent 任务链最小技能库能跑之后我用一个固定用例做回归输入“帮我安排下周一晚上的聚餐时长一个半小时”期望结果是模型先调用parse_time_expression解析出具体时间再调用query_calendar_free_slots查出空档再调用recommend_duration确认时长最后输出一个可执行的建议日程。实测过程中我发现模型有时会跳过“解析时间”这个技能直接拿用户原文里的“下周一晚上”去查日历。这时候调度器收到了日期解析失败的报错回退机制会触发模型在下一轮意识到需要调用时间解析技能。我把这类情况写进自动化测试用例每次改技能描述或参数都会跑一遍回归防止“这个技能好了那个技能废了”的连锁反应。5. 常见问题与排查技巧实录5.1 模型死活不调用技能这是出现频率最高的问题。我排查过二十多次原因集中在三个方向。第一个是技能描述和用户意图之间缺少桥接词比如用户说“约饭”技能描述里却只出现“会议”“日程安排”模型匹配不上。解决方法不是增加抽象描述而是在when_to_use里显式列出常见口语别名像“聚餐”“约饭”“组局”都写进去。第二个是多技能高度相似模型分不清该用哪个。我会检查召回阶段是否把相似技能同时送给了模型如果是就提高精排阶段的差异度或者在描述里用“本技能与 xxx 的区别在于”来消歧。第三个是 temperature 设置过高把选择技能这一步也变得随机我统一把temperature降到 0.2 以下处理技能选择和参数抽取都建议用低温度。5.2 参数总被填错参数解析错误最常见的原因是参数边界定义不清。比如我有个技能接收city参数但用户说“去深圳出差”模型可能把city填成“出差”因为描述里没写清楚这个参数是“活动所在城市而非活动类型”。解法是给每个参数补充更细的description必要时给几个 few-shot 示例。另外一个高频 bug 是日期格式不统一。模型的训练语料里时间表达五花八门我建议在参数定义里强制format字段并在描述里写死格式样例比如“格式必须为 YYYY-MM-DD例如 2025-06-03”。执行侧对format做正则校验不合法就触发回退让模型重新抽参而不是在代码里偷偷做模糊转换那样只会掩盖问题让错误在后续环节继续发酵。5.3 技能之间的隐式依赖技能组合使用是常态但隐式依赖是个大坑。我遇到过一次技能 A 返回的字段名是available_time技能 B 读取的是free_slots两个技能单独测试都正常组合起来 B 永远拿不到数据。排查了半天才发现是字段命名不一致。从那以后我在技能库规范里加了一条“跨技能数据必须走显式 context”技能 B 需要的数据不能自己去摸 A 的原始返回而是由上游技能在context里统一写入标准命名比如context[free_slots]。同时引入 schema 契约测试让 A 和 B 定义同一个FreeSlotsSchemaA 写、B 读用代码保证一致性而不是靠人眼 review。5.4 技能膨胀库太大、检索失灵当技能数量超过三十个检索失灵就是必然结果。top-k 召回里经常出现一堆不相关的技能把真正需要的那个挤出去。我做了两件事来控制膨胀。第一件是技能分层把“全局技能”和“场景技能”分开索引全局技能只留跨场景通用的比如时间解析、用户身份识别场景技能按业务域维护独立索引检索时先定位业务域再在域内召回。第二件是定期复盘调用日志找出 30 天内从未被调用、也没有被其他技能依赖的技能直接下架归档。我还会用“调用命中率”这个指标来判断技能质量。如果某个技能被召回三十次却只被选中一次说明它的描述和真实适用场景错位需要重写 manifest而不是继续靠加大曝光来补救。6. 技能库的扩展方向与个人体会技能库做到后面就不再只是“给模型接几个函数”这么简单。我开始把多轮对话中的记忆整理也做成技能把用户偏好更新做成技能甚至把短时记忆的写入、长期记忆的归档都抽象成可复用的技能节点。这样整个 agent 的每一次状态变更都有迹可循而模型本身只需要专注于“理解意图”和“决策下一步调用什么”。我也在尝试给技能加“学习”机制记录每次调用后的结果反馈自动微调技能描述的用词让检索质量随时间慢慢进化虽然现在还比较初级但方向是明确的。根据我的实际体验agent-skills最关键的收益不是“让模型学会了更多能力”而是“让系统的确定性部分彻底脱离模型”。模型像是指挥官技能柜是装备库指挥官可以换但装备库里的每一件武器都必须保养得清清楚楚。所以比起疯狂加功能我更建议大家先把描述写规范、把契约测试补全、把回退机制做扎实。这套基本功打好了Agent 的能力边界才真正可控你可以放心地让它去处理越来越复杂的真实任务。
返回列表