资讯动态

agent-skills:为AI Agent打造可复用的技能调度与管理框架

发布时间:2026/10/13 8:33:16 来源:尧图企业网站定制
做 AI Agent 开发一段时间后会发现一个很尴尬的问题明明每个 Agent 都在做“根据用户意图选择工具、调用工具、整理结果”这件事但每次换一个场景代码就要重写一遍。工具调用的模板塞在 system prompt 里越堆越长模型开始乱选函数、漏传参数、返回格式五颜六色。agent-skills 这个项目就是冲着这个问题去的——它把 Agent 的每一项能力抽象成“技能”用一套统一的结构去定义、注册、调度和执行让 Agent 不再靠猜来使用工具而是有一套明确的匹配逻辑。这个项目适合两类人看一类是正在做 Agent 应用、被工具调用稳定性折磨的开发者另一类是刚接触智能体开发、想搞清楚“技能”和“函数”到底什么区别的学习者。先说清楚它能解决什么问题技能复用、参数校验、调用回退、可观测性。下面是我的完整设计思路和实操记录。1. 项目起点为什么 Agent 需要一份“技能清单”1.1 靠 Prompt 约束工具调用越走越窄很多 Agent 的第一版都是暴力方案把所有函数说明写进 system prompt配合“你必须严格按 JSON 格式输出”的提示词然后祈祷模型别乱来。前 10 个函数的时候还能忍等函数数量上到 50 个、100 个问题就开始集中爆发。我见过最典型的三类失败模型把 function name 拼错大小写不一致、带了奇怪的前缀参数类型搞错明明 schema 里写的是整数模型输出字符串模型把两个相似功能的函数搞混比如“查询天气”和“查询未来一周天气”它经常随机选一个。这些问题的根源在于Prompt 对模型来说只是“建议”不是“约束”。模型在生成文本的时候函数调用本质上也是文本生成的一部分它天然倾向于语义顺畅的内容而不是结构正确的 JSON。所以靠提示词去管工具调用本身就是把可靠性建立在随机性上。1.2 函数与技能的真正区别先说一个容易混淆的点函数是能力的实现技能是能力的“完整包装”。一个函数只有一个名字和一段执行逻辑但一个技能还包含了触发条件、参数契约、执行策略、失败回退。生活化类比一下函数是一把扳手放在工具箱里技能是“使用扳手拧水管接头”的整套流程上面写着什么时候该用、开口要调多大、拧不动的时候该怎么办。Agent 调度器不直接面对扳手它面对的是贴满标签的流程卡。所以在 agent-skills 里我一开始就定了一个原则技能是什么形态都行函数、API 调用、脚本、甚至是一段多人协作流程都可以被包装成技能。关键是外面套的那层“元数据”必须统一。只有元数据统一调度器才能用同一套逻辑处理所有能力。1.3 设计目标让技能可被发现、可被调度、可被观测agent-skills 整个项目围绕三个动词展开发现给定一个用户请求系统能找到最相关的技能调度按技能的参数契约去执行处理好超时、重试、回退观测每一次技能调用都有记录能复盘“为什么这次选了 A 而不是 B”。这三个目标决定了项目架构不会太复杂一个技能注册表、一个技能描述索引、一个调度执行器。剩下的都是在这三个模块里填充细节。2. 技能库的核心设计用什么形态组织“技能”2.1 技能的结构定义每个技能我把它定义成下面这几个字段字段类型说明namestr全局唯一技能名小写加下划线descriptionstr给模型看的匹配描述包含触发场景keywordslist[str]显式关键词用于快速预筛input_schemadictJSON Schema 风格参数定义executorCallable实际执行的函数对象timeoutint单次执行超时秒数默认 10retry_policydict重试次数与退避策略fallbackstr失败后回退到的技能名可空这里最容易被忽略的是 description 和 keywords 的区分。description 是给模型看的keywords 是给规则匹配用的。keywords 可以很机械比如“天气、温度、降雨”description 必须写成“触发场景用户意图”的组合比如“当用户询问某个城市当前或未来的天气情况包括温度、降水概率、风力时使用”。2.2 为什么参数契约用 JSON Schema最开始我图省事直接让技能执行函数声明 Python 类型注解然后靠函数签名生成参数说明。但实际测试发现模型在填充参数时经常不按类型走尤其是嵌套对象和枚举值翻车率很高。后来换成了 JSON Schema效果立刻不一样。原因有两层schema 本身就是模型训练时常见的数据格式生成符合 schema 的 JSON 比生成符合任意 Python 注解的 JSON 要稳schema 可以显式声明 required、enum、format比如日期格式用format: date模型就知道该输出 YYYY-MM-DD。所以我的建议是别省这一步直接用 JSON Schema哪怕只是把 Python 类型手动映射成基础 JSON 类型也值得。2.3 技能间的依赖关系怎么处理复杂场景里一个技能可能需要先调用另一个技能。我在设计里没有给技能之间做显式依赖声明原因是会让调度逻辑变成有向图遍历复杂度直接起飞。我的做法是在 executer 内部自行调用其他技能调度器只负责最外层那个技能。也就是说依赖关系下沉到代码层而不是配置层。这样做的好处是调度器保持简单坏处是没法自动做依赖注入。对大多数场景来说这个取舍是值得的。3. 实操入门技能定义与注册机制3.1 用装饰器注册一个技能agent-skills 的注册接口我用的是 Python 装饰器风格因为最直观写起来也最省事。下面是一个查询订单状态的技能示例from agent_skills import skill, execute skill( namequery_order_status, description当用户询问订单状态、物流进度、发货情况时使用, keywords[订单, 物流, 发货, 快递], input_schema{ type: object, properties: { order_id: {type: string, description: 订单号}, phone_last4: {type: string, description: 收件人手机号后四位用于校验} }, required: [order_id, phone_last4] }, timeout15 ) def query_order_status(order_id: str, phone_last4: str) - dict: # 这里写真实的订单查询逻辑 result query_order_api(order_id) if result[phone_tail] ! phone_last4: return {status: denied, message: 校验失败} return {status: success, data: result}这里有一个容易踩的坑装饰器注册的技能函数参数名必须和 input_schema 里的 properties 键完全一致。不一致会导致参数抽取成功但函数调用直接 TypeError。我一开始没做校验上线后被这个坑炸了两次后来在注册阶段直接加了一层反射比对不一致就报错。3.2 技能的加载与冲突检测技能不是分散在各处就完事了应用启动时会统一扫描所有被skill装饰的函数把它们注册进全局 SkillRegistry。注册阶段做了三件事registry SkillRegistry() registry.on_register def check_conflict(skill): # 1. 同名检测 # 2. 描述长度检测description 少于 20 个字符直接警告 # 3. 关键词去重防止两个技能 keywords 交集过大 pass同名检测好理解。描述长度检测很多人会忽略——如果 description 太短模型匹配阶段信息量不够会出现两个相似技能随机被选中的情况。我的经验是 description 至少 30 到 50 个字必须包含“何时使用”和“何时不使用”两个维度。关键词去重这里多说一句不是完全禁止交集而是当两个技能的关键词重合度超过 60% 时触发告警提醒你检查是不是技能边界划分不清晰。这个设计的价值在技能数量上百之后会非常明显。3.3 技能的批量导入除了装饰器注册我也支持从一个 JSON 文件批量导入技能描述。这个功能主要用于团队协作——算法工程师写好技能元数据业务工程师实现执行函数两边通过一个 JSON 文件解耦。JSON 文件长这样{ skills: [ { name: query_order_status, description: ..., keywords: [订单, 物流], input_schema: {}, handler_module: handlers.order_handlers, handler_func: query_order_status } ] }加载器读到 handler_module 和 handler_func 后会去指定模块里找函数把定义和实现做绑定。这样做的核心原因是让技能描述可版本管理、可 diff而不是散落在代码注释里。每次改 descriptionGit 历史里都能看到来龙去脉。4. 调度器实现Agent 怎么“想起来”该用哪个技能4.1 匹配管线规则 → 语义 → 模型调度器最核心的模块是技能选择器。我把它做成三段式管线第一段是规则预筛。拿用户输入去匹配所有技能的 keywords命中的技能进入候选集。这一段的目的是缩小范围哪怕选错也比全量扫描快。第二段是语义排序。把用户输入向量化和候选技能的 description 向量做余弦相似度排序。这里用的是本地小模型做 embedding速度很快成本也低。候选集超过 20 个时才触发这段不然直接进第三段。第三段是模型精排。把语义排序前 5 的技能 description 拼接连同原始用户输入一起交给模型让模型直接输出应该调用的技能名。为什么前面已经有了语义排序还要模型精排因为 AB 测试下来纯 embedding 匹配的准确率大概在 75% 到 80%加上模型精排能提到 92% 以上。代价是多了一次模型调用但换来的稳定性非常值得。4.2 匹配阈值与拒识技能匹配不能总是硬选一个。用户输入不在任何技能覆盖范围内时强行匹配只会让 Agent 胡说八道。所以调度器里有一个 reject 分支if best_score 0.35: return SkillResult(statusrejected, reasonlow_confidence) elif best_score 0.6: # 进入确认模式向用户复述“你是想问xxx吗” return SkillResult(statusneeds_confirmation, candidatebest_skill)0.35 和 0.6 是我在测试集上调出来的经验值不同领域可能不一样。这个机制解决了一个很实际的问题用户说“帮我订个酒店”系统里只有“查询酒店”和“取消订单”两个技能时不要假装自己会订酒店而是明确告诉用户“我只能查不能订”。4.3 执行器一个简单的状态机匹配完成之后执行器接管。我把技能调用过程建模成状态机pending → scheduled → extracting_args → executing → succeededextracting_args → failed_args → extracting_args(重试) 或 terminal_failedexecuting → timeout → fallback_skill 或 terminal_failed这个状态机写在代码里大概一百多行状态流转用字典映射加状态回调实现没有引入重型框架。好处是每一步都能输出日志复盘时能看到调用链。执行器内部用线程池并发控制技能执行避免一个耗时技能阻塞整个 Agent。默认线程池大小是 8技能执行时间超过 timeout 后直接放弃走重试或回退。5. 参数抽取与结果回填最容易翻车的地方5.1 用约束生成替代自由发挥技能执行前必须从原始用户输入里抽出参数。这一步我踩过的坑最多。最开始我是把整个对话历史连同参数定义一起丢给模型让它“凭感觉”抽参。结果就是模型经常抽取用户没说过的参数或者把地名抽成时间。后来改成约束生成模板给模型的不是对话历史而是经过整理的用户意图摘要加参数 schema并且明确要求“只能从原文抽取不能臆造”。根据用户输入抽取以下参数严格按照 JSON Schema 输出。 用户输入{{user_input}} 参数定义{{input_schema}} 规则 1. 每个参数必须能在原始输入中找到依据 2. 缺少必要参数时对应字段输出 null不要猜测 3. 只输出 JSON 对象不要输出其他内容加了规则之后参数抽取的准确率从大约 70% 提升到 88%。剩下的 12% 错误集中在模型的“补全心理”上——看到缺了城市名就自动把对话历史里的城市填进去但那个城市明明是上一次对话的跟当前请求无关。5.2 参数校验失败后的自愈策略参数抽取完成执行器会对参数做一次 schema 校验。校验通过进入执行校验失败不会直接报错而是带着校验错误信息重新追问一轮补充信息for attempt in range(retry_policy.get(max_retries, 2)): extracted extract_args(user_input, skill.input_schema) errors validate_schema(extracted, skill.input_schema) if not errors: break # 把 errors 反馈给模型让它针对缺失字段再次抽取 user_input_for_retry build_retry_prompt(extracted, errors)这个机制叫做“交互式参数补齐”它让 Agent 在缺参数时能主动发问“请问您的订单号是多少”而不是呆呆地报错。体感上的效果非常明显用户不再觉得 Agent 是个脆弱的玩具。5.3 结果回填别把函数的裸返回直接丢给用户技能执行函数的返回值很多是结构化数据比如一堆 JSON 字段。直接原样返回给用户用户体验很差而且容易暴露内部字段名。所以在执行器尾部有一层结果格式化它做两件事根据结果类型拼接自然语言摘要敏感字段打码比如手机号只保留后四位。这层逻辑每个技能可以自定义一个format_result回调没定义就用默认模板。默认模板会把 dict 的 key-value 拼成一句通顺的中文。这个设计虽小但对 Agent 的“智商感”贡献很大。6. 踩坑清单与排查技巧实录6.1 高频问题与解决方案速查我在开发和调优 agent-skills 的过程中整理了一份高频问题清单直接放这里当速查表现象根因处理方式技能匹配总选到相似项description 没有写“不使用场景”重写 description增加负面约束参数抽取出空值模型缺少上下文原文确实没给启用交互式补齐反问用户技能执行报错但 Agent 无感知异常被吞掉结果状态没传递执行器捕获异常后写入 result.status重复调用同一技能没有做执行结果缓存对幂等技能加 10 分钟缓存技能响应慢导致整体卡顿线程池耗尽或超时设置过大调小 timeout线程池隔离描述过长导致匹配混乱description 超过 200 字信息熵太大控制在 100 到 150 字6.2 描述风格怎么迭代我调试 description 的经验是不要坐在电脑前空想去翻真实用户对话记录。每来一条匹配错误案例就把用户原话和错误选择结果记录下来然后反推 description 缺了什么信息。举个例子用户说“帮我看看快递到哪了”系统匹配到了“查询快递网点”而不是“查询物流轨迹”。看案例发现原 description 里写的是“查询某快递公司网点地址、营业时间”没有提到“包裹、轨迹、当前到哪”。把关键词和描述补上这两个要素之后这个误匹配立刻消失了。6.3 性能优化记录最后说下性能。整个技能的匹配管线中最耗时的不是技能执行而是模型精排那一次调用。我做了个优化规则预筛和语义排序全走本地耗时控制在 50 毫秒内模型精排做异步化和参数抽取合并成一次模型调用。合并之后Agent 从接收请求到技能执行完成的端到端耗时从平均 1.8 秒降到了 1.1 秒。体感好了不少尤其是对话多轮场景。根据我个人实际使用的体会agent-skills 这类技能管理框架的收益在技能规模小的时候感知不强但一旦技能数量突破 30 个它带来的稳定性和开发效率提升是断崖式的。你不需要一开始就把技能定义得多完美先用最朴素的描述跑起来然后靠真实案例慢慢打磨。技能库是一个越用越值钱的东西因为它沉淀下来的不只是代码还有对用户意图和系统能力边界的理解。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑