资讯动态

agent-skills:面向AI Agent的技能定义、注册与执行轻量级方案

发布时间:2026/10/29 13:57:04 来源:尧图企业网站定制
直接讲agent-skills这个项目吧。先说结论这不是又一个提示词合集或者工具框架的套壳封装而是一套面向AI Agent的技能定义、注册、装载与执行的轻量级方案。我最初是给一个多模态Agent做工具层重构时攒出来的这套东西后来发现它可以脱离那个项目单独复用就单独拆出来维护了。这篇文章把完整思路、核心代码、落地上踩过的坑一次性讲清楚适合正在做Agent工具层、函数调用或技能编排方向的开发者参考也适合想理解Agent技能到底是什么的朋友当作入门材料。1. 为什么需要一套独立的Agent技能体系1.1 从一堆工具到一套技能早期做Agent最朴素的方式就是把所有能力塞进一个巨大的system prompt然后用function calling让模型去选工具。一开始只有几个工具时还好一旦工具数量超过二十个问题就开始冒头一是描述越来越长把上下文预算吃得很厉害二是工具之间的调用关系、前置条件、资源共享完全没有地方能表达只能靠提示词里零散的如果……请先调用……来维持三是每加一个工具要么同步改提示词要么改脚本里的映射逻辑耦合严重。后来我逐步意识到工具只是Agent的动作原语技能才是有上下文、有前置条件、有反馈闭环的能力单元。比如说查询天气是一个工具根据天气规划出行是一个技能——技能必须知道天气数据从哪里来、拿到后怎么处理、什么情况下输出什么格式、失败时如何降级。这样一个技能的概念能把工具、参数、逻辑、异常处理全部打包Agent层只需要声明我要用技能A剩下的事情由技能内部解决。1.2 技能和工具、提示词、工作流之间的边界很多朋友问这套技能体系和LangChain的Tool、或者Coze的工作流有什么区别。我自己的理解是这样工具是最小动作只有一个输入和一个输出不做决策工作流是把多个工具按固定顺序串起来适合确定性流程技能是带上下文和状态的能力封装模型可以动态选择执行哪条技能分支提示词负责表达意图技能负责承载可执行逻辑。在agent-skills里我采用的设计原则是技能是模型可感知的最小执行单元一个技能内部可以包含多个工具调用但对外只暴露一个统一的调用入口和一套参数schema。这样做的好处是Agent在规划时不用关心技能内部怎么做决策空间被压缩到技能数量这个级别而不是工具数量这个级别。1.3 这个项目解决的核心问题回到标题agent-skills解决的是三类问题第一类是技能管理问题。几十个技能如何清晰注册、如何防止命名冲突、如何支持动态启用和禁用、如何规范化描述格式。第二类是技能执行问题。技能被选中后如何高效地装载、参数校验、调用工具、处理错误、返回结果。第三类是Agent与技能之间的协议问题。模型怎么知道该调用哪个技能、参数怎么填、返回结果怎么被模型理解。这三类问题如果混在业务代码里处理很快就会变成一团乱麻。把它们抽象成独立的技能体系之后Agent的主循环可以保持非常干净感知、规划、执行、反馈——中间只依赖一个技能执行器的接口。这篇文章不卖关子直接给出我最终的实现方案。2. 技能体系的整体架构与核心设计2.1 技能抽象层设计我先给技能定义了一个五要素结构名称、描述、参数Schema、执行器、依赖。名称是技能的唯一标识建议用动词_名词格式比如search_web、read_screen、get_weather。描述是给模型看的一段自然语言说明必须写清楚这个技能做什么、什么时候用、什么时候不要用。参数Schema是对模型可填参数的结构化约束我用的是JSON Schema格式这样不管模型端用什么协议都能无缝适配。执行器是实际干活的部分可以是一个Python函数、一个API调用封装、一段脚本甚至是一个子Agent。依赖声明了该技能运行时需要的资源比如需要联网、需要读取某个目录、需要特定环境变量。这里有一个很关键的决策点参数校验放在哪一层。我把校验放在技能执行器内部而不是解包前统一校验。原因是不同技能对参数要求的严格程度不一样有的技能允许缺省参数用默认行为有的技能一旦缺参数就应该直接报错。统一校验会导致很多多余的麻烦。2.2 技能注册中心设计注册中心是整个体系的地基。早期版本我用的是一个全局Python dict每个技能装饰器注册进去。后来人数变多、技能变多我换成了基于目录约定的方式每个技能是一个独立的目录或模块模块内存放skills.json描述文件和executor.py执行脚本注册中心启动时扫描目录并动态装载。目录约定的好处是技能可以像插件一样随时追加和移除不需要改核心代码。我最终采用这样的目录结构skills/ ├── search_web/ │ ├── skills.json │ └── executor.py ├── calc_expression/ │ ├── skills.json │ └── executor.py └── ...每个skills.json里包含技能的名称、描述、参数Schema、依赖声明、超时设置。注册中心启动时遍历这个目录读取每个技能的JSON描述文件把元信息汇总成一份技能清单同时将executor.py延迟加载——只有技能被调用时才真正导入模块避免启动时把所有技能都加载进内存。2.3 执行器的统一接口为了让Agent主循环与技能内部实现彻底解耦我给所有执行器定义了一个统一接口def run(context: dict) - dict输入是一个context字典里面包含模型填充的参数、当前会话上下文、共享存储等输出是一个字典包含结果数据、状态码、错误信息等。所有技能无论内部逻辑多复杂对外部都只呈现输入一个dict输出一个dict的形态。这里有一个容易被忽视的点不管要不要用每个执行器都必须返回一个包含status字段的字典。status的取值我规范为三种success、error、retry。Plan机制看到status为error时会根据错误消息决定是纠正参数重试还是换技能看到retry时会等待一段时间后重试。这个小小的状态约定直接决定了Agent在失败场景下的行为可控性。3. 核心实现技能的定义、装载与执行3.1 技能描述文件规范直接给一个search_web的例子——注意这个例子只是阐述技术方案不涉及具体访问行为{ name: search_web, description: 搜索互联网信息并返回排名靠前的结果摘要。适合用户提问实时信息时使用。, parameters: { type: object, properties: { query: {type: string, description: 搜索查询词}, max_results: {type: integer, default: 5, minimum: 1, maximum: 10} }, required: [query] }, timeout: 15, dependencies: [network], version: 1.0.0 }这个描述文件就是给模型看的技能说明书。模型在收到用户提问之后会拿到注册中心汇总的技能清单其中每一项都包含name、description、parameters。模型通过阅读description决定是否调用该技能再根据parameters约束生成合理的参数。你可能会问为什么不用自动生成的OpenAPI规范因为Agent场景下的参数约束不是为了强校验而是为了引导模型正确填参。JSON Schema足够灵活也足够被大模型理解而且所有主流模型API的function calling都能直接兼容不需要二次转换。实际测试中我甚至发现模型对JSON Schema中default和required字段理解得非常到位很少出现填错必填参数的情况。3.2 技能注册与动态装载实现注册中心的实现我贴一个极简版本。这个版本可以跑通完整的注册流程实际项目里我在这个基础上增加了热更新和并发锁。import json import importlib import threading from pathlib import Path class SkillRegistry: def __init__(self, skills_dir: str skills): self.skills_dir Path(skills_dir) self._skills {} self._locks {} self._lock threading.RLock() self.reload() def reload(self) - None: with self._lock: new_skills {} for skill_dir in self.skills_dir.iterdir(): if not skill_dir.is_dir(): continue meta_file skill_dir / skills.json if not meta_file.exists(): continue with open(meta_file, r, encodingutf-8) as f: meta json.load(f) new_skills[meta[name]] { meta: meta, path: skill_dir, executor: None, } self._locks[meta[name]] threading.Lock() self._skills new_skills def list_skills_meta(self) - list[dict]: with self._lock: return [ { name: v[meta][name], description: v[meta][description], parameters: v[meta][parameters], } for v in self._skills.values() ] def _load_executor(self, name: str): with self._locks[name]: entry self._skills.get(name) if entry is None: raise KeyError(fSkill {name} not found) if entry[executor] is None: spec importlib.util.spec_from_file_location( fskill_{name}, entry[path] / executor.py, ) module importlib.util.module_from_spec(spec) spec.loader.exec_module(module) entry[executor] module.run return entry[executor]这个方法里最关键的一个细节是_load_executor里用了每个技能独立的threading.Lock。因为技能可能在并发请求中被同时调用而importlib的module加载并不是完全线程安全的所以加载执行器时必须加锁。锁粒度只覆盖加载阶段真正执行时不需要持锁避免串行化。3.3 技能执行器标准流程执行器的代码结构我按校验-执行-包装三段式组织。拿一个识别屏幕信息的技能来举例这个技能会读取当前屏幕的截图并调用OCR识别文本。执行器长这样def run(context: dict) - dict: # 1. 参数提取 params context.get(params, {}) region params.get(region, full) # 2. 前置条件检查 if not screen_access_available(): return {status: error, message: screen access not available} # 3. 执行核心逻辑 try: image capture_screen(region) text ocr_recognize(image) except TimeoutError: return {status: retry, message: capture timed out, retry_after: 2} except Exception as e: return {status: error, message: focr failed: {str(e)}} # 4. 结果包装 return { status: success, answer: {text: text, region: region} }注意我特意把核心逻辑包在异常处理里然后把不同异常映射成不同的status。TimeoutError映射为retry这是我从大量失败案例里总结出来的很多临时性错误网络抖动、资源竞争重试两次就能成功但如果直接标成errorAgent会误以为技能本身不可用从而放弃整个任务链路。你可能会好奇context里除了params还有什么。我目前的标准context包含这几个字段params模型填充的参数、session_id会话标识、memory技能间共享的临时存储句柄、call_stack调用路径用于日志追踪。特别说一下memory这是给技能组合用的比如先调用read_screen获取屏幕内容再调用search_web搜索相关词前一个技能的结果可以通过memory传给后一个技能而不是每次都走模型的完整上下文。4. Agent与技能的编排实践4.1 用规划模块调度技能技能本身只是弹药真正决定打不打的是Agent的规划模块。我在agent-skills里没有做成一个完整的Agent框架只提供一个skill_planner的参考实现它接收用户意图结合技能清单返回一个技能调用序列。这里最大的经验是不要试图在规划阶段做太细的步骤规划给模型留出执行后反馈再调整的空间。早期版本我让模型一次性输出完整技能链结果模型经常在第一步执行失败后无法自主纠偏因为后续步骤都已经事先定死了。改成执行一步、观察结果、再决定下一步的循环模式后成功率提升很明显。伪代码流程while not done and step max_steps: result planner.decide_next_skill(user_query, skill_meta_list, execution_history) if result.action no_more_skill: done True break skill_result executor.execute(result.skill_name, result.params) execution_history.append({skill: result.skill_name, result: skill_result}) if skill_result[status] error: corrected planner.recover(skill_result[message]) # 根据错误信息决定改参数重试、换技能、放弃max_steps我一般设成5这个数字是实测出来的。太少了遇到链路复杂的任务容易半途失败太多了模型容易在错误步骤上反复打转。4.2 技能组合与状态管理的经验技能组合是这套设计里最值得细说的地方。组合有两种方式顺序组合和条件组合。顺序组合就是先A后B的硬依赖比如要根据新闻生成摘要必须先search_news获取内容再summarize_text。条件组合是根据中间结果决定下一步分支比如如果读取到登录页面就调用fill_login_form否则调用parse_dashboard_data。我的实现思路是把组合关系记录在技能描述文件里通过before_skills和after_skills字段声明依赖但只在规划阶段做参考提示不把依赖当作硬性校验。原因是模型有时候会找到你没想到的执行路径硬性依赖会把这个可能性堵死。我见过一个典型的例子某技能声明before依赖temp_calc但实际执行时用户直接提供了计算结果模型绕过了temp_calc直接调最终技能居然跑通了。状态管理方面我强烈建议不要在技能间共享可变状态除非你明确知道在做什么。共享存储我通常只放会话级上下文、缓存的计算结果、用户偏好等只读或者只追加的数据。如果你发现两个技能需要频繁读写同一个变量优先考虑把这两个技能合并成一个复合技能而不是让它们通过共享状态耦合。这个原则让我少排了好几个隐蔽bug。4.3 规格参数与超时机制技能的timeout参数值得单独说。JSON描述文件里我设timeout执行器必须遵守。不同技能的合理超时差异很大本地计算类的技能一般3到5秒涉及外部API的最少15秒涉及大文件处理的可以到60秒。我执行器内部用concurrent.futures包了一层统一的超时控制from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutTimeoutError def run_with_timeout(run_func, context, timeout): with ThreadPoolExecutor(max_workers1) as pool: future pool.submit(run_func, context) try: result future.result(timeouttimeout) return result except FutTimeoutError: future.cancel() return {status: error, message: fskill timeout after {timeout}s}超时之后不能只返回error还尽量把已经产生的中间结果整理出来放在message里。这样Agent在恢复时可以根据中间结果决定是重试还是换个策略而不是两眼一抹黑。5. 常见问题与排错经验5.1 参数Schema导致的调参失败这是我遇到的频率最高的问题。模型生成的参数经常和Schema不完全一致比如integer类型的参数模型给成了字符串required字段漏填等。早期版本我拿到参数直接塞给执行器速度是快但经常出现执行器内报错错误信息还难以理解。后来我在执行器入口加了轻量校验但异常分支不会让校验失败直接返回error而是返回retry并附带参数类型不正确请检查xx字段的信息。原因是模型具备在收到明确反馈后自我修正参数的能力给它一次重新填参的机会比当场判定失败要实用得多。我推荐校验用jsonschema库还总结了一个避坑点不要开启additionalProperties: false。一旦开了模型多填了一个你Schema里没声明的字段就校验失败频繁触发不必要的重试。默认允许未知字段存在执行器只提取自己需要的字段更宽容也更稳定。5.2 技能描述引发模型误调第二个高频问题是模型在错误场景下选择了错误的技能。排查了半天发现根源往往不在模型推理能力而在于技能description写得太宽泛。比如description里写处理用户可能提出的查询模型就会倾向于什么都往这个技能上靠。解决这个问题我会在description里同时写清楚什么时候不要用。格式类似description: 当用户需要查找实时信息时使用。注意若用户询问的是个人偏好或观点问题不要使用本技能。加上否定性描述后误调率下降明显。实测对比中对于一个推荐电影的问题原来的技能选择是search_web和recommend_movies各占一半加了否定描述后模型稳定选择recommend_movies。另外技能名称本身也很重要。我见过把技能命名为process的模型根本分不清它和别的技能有什么区别。命名一定要带明显的动作和对象特征parse_resume、extract_contacts这类就比process_data清晰得多。5.3 重试风暴与全局熔断技能互相调用、组合链路较长时最危险的故障是一种雪崩某个底层技能大面积超时上层每个技能都在原地重试整个Agent系统被拖垮。我见过一次比较严重的全部并发请求涌入一个已经故障的搜索接口Redis连接池被打满连带影响了其他无关服务。之后我加了全局熔断模块记录每个技能近1分钟的失败率超过40%时开启熔断熔断窗口内直接返回error并附带skill temporarily unavailable提示同时通知Agent切换到备用技能或放弃。恢复策略是半开状态熔断结束后放行1个测试请求如果成功则关闭熔断失败则继续熔断。这个机制让人工智能系统从一个个技能自己疯狂重试变成了系统性感知故障并主动降级稳定性提高了不止一个层次。5.4 调试工具与日志规范调试技能调用链时看得清执行顺序比看得清结果更重要。我在每次技能调用时都会记录一条结构化日志格式是[skill_exec] sessionabc123 skillsearch_web params{query:...} statussuccess duration1.2s日志全部走标准库logging不自己造轮子。的技能调用链直接能通过session_id全链路拉出来。排查问题时我第一件事就是看session_id对应的所有调用记录看status变化和duration指标马上能定位到具体是哪个环节出了问题。6. 性能优化与扩展方向6.1 技能预热与懒加载的平衡动态装载这种模式的一个潜在性能短板是首次调用技能时import消耗时间碰到复杂技能可能要多等300到500毫秒。这个延迟在Agent交互场景里体验比较明显。我的解法是两级策略启动时预载高频技能其余技能保持懒加载。在配置里加一个preload_skills数组列出常驻技能注册中心初始化时直接加载这几个技能的executor模块。其余技能仍然懒加载节省内存占用。实测下来高频技能首调延迟从400毫秒降到了20毫秒以内。6.2 结果缓存策略很多技能是典型的重复输入会得到相同结果的场景比如固定参数的字符串处理、本地文件解析。我给执行器增加了一个可选的cache_ttl配置执行前先查缓存执行成功后写入缓存。Redis或者本地内存缓存都行key的设计我用skill_name:sha256(params_json)保证同一技能同一参数命中同一缓存。缓存粒度要小心。这里的基本判断标准是带副作用的技能千万不能缓存比如发送邮件、新建文件查询类技能可以缓存混合型技能查询加写入建议默认不缓存。我吃过一次亏一个技能内部会轮询探测端口状态后再检测服务类型结果缓存了10分钟的旧状态导致配置变更后很久新服务状态才生效。6.3 扩展把技能体系接入多模态Agent这套技能体系最顺手的扩展方向就是多模态Agent。屏幕理解、截图、OCR、音频转录这些能力本质上都可以封装成标准的技能模块。比如听写技能就是audio_capture加speech_to_text两个工具的组合对外参数只需要duration和language模型感知到的就是听用户说话并转成文字这个能力。接入多模态后技能的参数Schema需要增加模态标记。我目前的做法是在description里显式说明输入内容的模态比如audio用户语音的base64编码。模型对这个字段理解得很好基本不会把图像数据和音频数据弄混。结尾这套agent-skills方案不是重框架而是轻方法论加薄实现。我实际用下来的体会是做Agent工具层与其不停往一个巨型提示词里堆工具不如把能力单元抽象成技能用统一的注册、装载、执行、反馈闭环去管理。它让Agent主循环保持了极度的简单也让新增一个能力变成了写一个目录这么简单的事。如果你准备给自己的Agent项目搭一套技能体系我建议从最小的可行版本开始一个目录放技能、一个注册中心、一个执行器、一个planner。先接三到五个技能跑通链路再把超时、熔断、缓存这些机制逐项加上。这里面最值得花时间的还是技能描述的打磨描述写得清楚模型的选择准确率提升是立竿见影的。后面我会把技能版本管理和技能热更新相关的内容再整理一篇那两个方向踩过的坑也不少值得单独说一说。

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

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

免费获取报价 →
↑