
做AI Agent开发的时间一长你会发现最让人头疼的往往不是模型本身而是“技能管理”这件事。我指的“技能”就是Agent能调用的那些函数比如查订单、发邮件、导报表。这些函数一旦超过二十个代码就开始失控命名随意、参数时好时坏、不知道哪个Agent用了哪个版本最糟糕的是调试起来像大海捞针。agent-skills这个项目就是我从这些坑里爬出来后沉淀出来的一套轻量技能管理框架。它解决的问题很聚焦如何让技能的注册、发现、调用、观测变得像搭积木一样清晰。这篇文章我会从设计动机、核心模型、实现链路、实战案例到踩坑记录把整套思路完整讲一遍希望对正在做Agent工具层的朋友有帮助。1. Agent技能管理到底难在哪里为什么我会动手写agent-skills1.1 从一次智能体开发事故说起去年夏天我负责一个客服Agent功能范围不大查订单、申请退款、改收货地址外加一个售后留言。一开始非常顺利每个能力就是一个函数我把它们以JSON Schema的形式拼进System Prompt让模型自己选。上线第一周就翻车了。用户问“我上周买的那个保温杯退款到哪了”这本来是一个查询类请求模型却先调用了查订单函数拿到了订单号紧接着又把同一个订单号塞进“申请退款”接口系统在后台直接生成了一个新退款工单。用户只是想看进度结果我们真的给他退了一次款。如果不是业务方及时拦截这就是一次实打实的事故。排查那天下班后我打开tools.py十个函数全挤在一个文件里里面到处是try/except和临时加的if分支。真正让我后背发凉的是整个文件没有任何“能力边界”没有统一的入参规范没有调用权限控制也没有任何日志可以告诉我模型在每一轮到底选了哪个函数、传了什么值。那一刻我意识到这个问题不能在函数堆叠层面解决必须有更高一层的抽象。1.2 技能不等同于函数它应该是带完整契约的能力单元很多Agent项目把“Agent技能”直接等于“Python函数”这是我认为最大的误区。函数只描述“怎么执行”但技能还需要回答另外三个问题什么时候该被调用调用需要什么约束结果如何与Agent里的其他状态衔接举个例子。“查订单”这个能力放在订单查询场景里不需要用户敏感信息放在退款场景里就需要完整的退款详情。如果只是一个函数你只能在函数内部加参数去判断“当前是哪个Agent在调用我”这会让业务代码充满上下文判断。而把它提升为技能对象后我可以定义两个技能实例一个permissionread一个permissionadmin分别注册到不同Agent的注册中心里代码天然隔离逻辑清晰。所以在agent-skills里技能是一个包含完整契约的数据结构名称、描述、参数Schema、执行函数、依赖项、超时、权限等级。名称和描述决定模型如何理解“什么时候用我”参数Schema决定输入约束依赖项决定前置技能超时和权限决定运行时边界。这样一来“技能”就不再是隐性的代码而是一等公民。1.3 为什么不直接上LangChain非要自己维护一套轻量机制有人会问LangChain、Semantic Kernel这些框架已经提供了工具注册和调用编排为什么不直接用我的答案是看需求规模。如果你的Agent需要复杂的记忆、向量检索、多LLM提供商适配那用完整框架是合理的。但如果只是想把一批业务函数接入LLM重框架反而会成为束缚。我当时需要的东西非常简单技能的注册、查找、Schema导出、执行校验、基础编排。这些功能如果自己实现不过几百行代码但换成通用框架我还要学习它的插件机制处理它的版本升级绕开它的默认行为。agent-skills不绑定任何LLM服务商不预设对话管理策略只做技能这一件事。你可以把它接到OpenAI的Function Calling也可以接到本地模型的工具调用协议甚至不用模型直接走规则匹配。这个范围克制是它最大的好处也是我坚持不膨胀它的原因。2. agent-skills的整体设计把技能当成一等公民2.1 核心数据模型技能名称、描述、参数与执行体我先定义了技能的最小数据模型所有模块都围绕它来工作from dataclasses import dataclass, field from typing import Callable, Any, Dict, List dataclass class Skill: name: str # 唯一技能名例如 get_order_info description: str # 给模型看的描述越具体越好 parameters: Dict[str, Any] # JSON Schema描述入参结构 handler: Callable[..., Any] # 真正执行业务逻辑的函数 dependencies: List[str] field(default_factorylist) # 前置技能名列表 timeout: float 5.0 # 执行超时秒数 permission: str read # read / write / admin 权限等级这个模型最巧妙的地方在于它把“模型看到的描述”和“开发者看到的执行函数”绑在了一起。生成Function Calling协议时我们直接取name、description、parameters完全不需要维护第二套Schema。开发者注册一个技能时只需要思考“我该写一个多么清晰的描述”而不是去查框架文档。我还特意让parameters采用JSON Schema标准因为它已经是很成熟的生态很多人熟悉。后续不管对接哪个模型厂商的工具调用协议几乎都能直接用。这个选择帮我避免了很多次“新模型不支持某种格式”的头痛。2.2 注册中心技能统一存放与查找的核心技能对象有了接下来需要一个地方把它们存起来。我实现了一个SkillRegistry类内部就是按技能名维护一个字典并提供注册、查找、导出Schema、权限过滤等接口class SkillRegistry: def __init__(self, max_permission: str admin): self._skills: Dict[str, Skill] {} self._max_permission max_permission def register(self, skill: Skill) - None: if self._max_permission read and skill.permission ! read: print(f跳过技能 {skill.name}: 权限等级过高) return if skill.name in self._skills: raise DuplicateSkillError(skill.name) self._skills[skill.name] skill def get(self, name: str) - Skill: if name not in self._skills: raise UnknownSkillError(name) return self._skills[name] def list_skills(self) - List[Skill]: return list(self._skills.values()) def schemas(self) - List[Dict[str, Any]]: return [ { type: function, function: { name: skill.name, description: skill.description, parameters: skill.parameters, }, } for skill in self._skills.values() ]这里有两个细节值得说。第一同名技能直接抛异常而不是默默覆盖。这个看起来严苛的设计在后面帮我拦住了一次很隐蔽的生产事故。第二max_permission参数允许在Agent启动时限定整个注册中心能接受的最大权限等级。比如一个只读分析Agent初始化时传max_permissionread那么任何write甚至admin技能都会被静默跳过并打印警告从源头防止“只读Agent顺手写了库”。2.3 声明式配置的好处不写模板不藏业务逻辑早期我做Agent技能时最常犯的错误是把“这个技能是否可用”的逻辑写成if/else埋在各个函数里。比如订单技能在客服Agent里可用在后台管理Agent里又要脱敏结果一个函数里全是“如果当前环境是xx则yy”。这种代码一旦多起来根本没法维护。用声明式配置后这些问题全部变成数据字段。技能是否可被某个Agent使用取决于它的permission等级与注册中心的max_permission技能是否依赖其他技能写在dependencies里技能的执行超时和安全等级也直接作为元数据。业务函数只管业务逻辑其他横切关注点全部交给框架。例如“查订单”技能我可以生成两个不同配置的实例read_skill Skill( namequery_order, description查询订单基础信息不含退款详情。, parametersbase_order_params, handlerquery_orders_basic, permissionread, ) admin_skill Skill( namequery_order, description查询订单完整信息包含退款详情与用户隐私字段。, parametersbase_order_params, handlerquery_orders_full, permissionadmin, )一个注册到客服Agent的Registry一个注册到售后后台Agent的Registry两边互不干扰。这比传参判断不知道清爽多少。3. 核心实现从注册到调用的完整链路3.1 写一个新技能只需要10行装饰器封装为了让团队里没有框架背景的同学也能快速上手我给Registry封装了一个装饰器。它的作用是把普通函数包装成Skill对象并注册到注册中心。这个设计几乎不改变业务函数的写法registry.decorate( namecreate_calendar_event, description创建或更新一个日历日程传入标题、时间、地点和备注。, parameters{ type: object, properties: { title: {type: string, description: 日程标题}, time: {type: string, description: ISO格式时间例如2026-07-01T15:00:00}, location: {type: string, description: 地点}, }, required: [title, time] }, permissionwrite, timeout3.0, ) def create_calendar_event(title: str, time: str, location: str ): # 调用日历API写入日程 return {status: created, event_id: evt_20260617_001}装饰器的实现并不复杂核心逻辑是生成一个Skill实例然后调register。我之所以用装饰器而不是让开发者继承一个SkillBase是因为继承会强迫他们写一堆抽象方法装饰器则可以把业务函数原封不动保留。实际项目中新同学只需照着一个已有技能的例子抄一遍十分钟就能上手写技能这是很多重框架给不了的体验。3.2 参数校验与类型转换模型传参不靠谱这一关必须守住LLM调用技能时参数是模型自己生成的格式飘忽不定是常态。我见过模型把时间字段传成“明天下午3点”把数字字段传成“一二三”。如果不做校验这些脏数据会直接进入业务层产生各种奇怪的结果。所以我给技能注册阶段增加了一个validator可选参数。如果没有自己提供校验器框架就根据parameters里的JSON Schema自动生成一个校验器。执行handler之前先跑校验def _validate(skill: Skill, raw_args: Dict[str, Any]) - Dict[str, Any]: validator skill.validator or build_validator(skill.parameters) try: return validator.validate(raw_args) except ValidationError as e: raise SkillParameterError(skill.name, e.messages)这里的关键是校验失败时返回的错误信息必须具体到字段级并且给出修正建议。比如time字段需要ISO格式例如2026-07-01T15:00:00。为什么因为模型会读取这个错误信息来修正下一轮调用。你给的错误越具体模型修正得越快。一开始我图省事只写了“参数不合法”结果模型连续四轮生成同样的错误参数改成具体提示后一次就改对了。除了校验我还做了轻量的类型转换。比如模型传入的top_k: 5会由泛型校验器转换为int(5)传入全角逗号等符号也会在转换层修复。这个小设计减少了大量因编码习惯不同导致的小问题。3.3 技能编排顺序执行、条件分支与上下文传递多技能协同是Agent的刚需。用户说“帮我约个明天下午的会议室并通知参会人”这需要先查会议室空闲再创建日程最后发通知。agent-skills里我实现了一个很轻量的SkillPipeline它的职责是保证“当模型决定要依次调用这些技能时执行链路是稳定且可观测的”。class SkillPipeline: def __init__(self, registry): self.registry registry async def run(self, skill_names: List[str], initial_context: Dict[str, Any]): context dict(initial_context) for name in skill_names: skill self.registry.get(name) result await self._execute_with_timeout(skill, context) context[skill.name] result return context async def _execute_with_timeout(self, skill: Skill, context: Dict[str, Any]): try: return await asyncio.wait_for( skill.handler(**self._pick_args(skill, context)), timeoutskill.timeout, ) except asyncio.TimeoutError: raise SkillTimeoutError(skill.name, skill.timeout)_pick_args的作用是从当前context里挑出技能需要的参数。因为前一个技能的结果可能放在context[skill.name]里后一个技能需要引用它。如果某个场景需要条件分支我更倾向于把“分支决策”交给模型模型根据上下文选择下一批技能名代码里的pipeline只负责无条件执行给定列表。这样做的原因是前期的条件分支往往不是唯一解硬编码会埋没模型的灵活性。P.S. 测试时也更好写因为pipeline的行为是确定性的。4. 实战案例让一个个人助理Agent同时管理日程、天气和文档4.1 从需求到技能清单拆解一个助理Agent需要哪些技能为了验证框架的可用性我搭了一个个人助理Demo它能查询天气、创建日程、搜索本地文档。这三个技能看似简单但背后各有各的复杂——天气要城市名转经纬度日程要支持时区与重复事件文档搜索要按相关度排序并返回摘要。我拆出来的技能清单是这样的技能名用途关键参数权限依赖get_weather查询指定城市、日期的天气情况city, datereadgeocode_citygeocode_city把城市名转成经纬度cityread无create_calendar_event新建一条日历日程title, time, locationwrite无search_documents按关键词搜索本地文档query, top_kread无注意我特意把“城市名转经纬度”拆成独立技能而不是塞在天气技能内部。因为它太常被复用了查询天气要用后续算两地距离要用地图导航也要用。拆出独立技能后模型可以在不同场景里自由组合它而不是每次都要单独写一套转换逻辑。4.2 把注册表翻译成LLM能懂的Function Calling协议在Agent主流程里我只需要把注册表里的Schema导出传给LLM接口的tools参数functions registry.schemas() # response await llm.chat(messages, toolsfunctions)实际运行时模型会在对话过程中“觉得”需要调用某个技能时返回一个tool_calls指令。我的Agent层捕获这个指令解析技能名和参数调用pipeline执行再把结果以tool消息喂回给模型。为了验证“描述优先级高于硬编码”我在get_weather的description里明确写了“调用前需要通过geocode_city将城市转为经纬度并将经纬度结果传入。”结果在测试对话中模型真的会先在用户问天气时调用geocode_city拿到经纬度后再调get_weather非常听话。不过也遇到过模型跳过前置技能的情况用户问“北京明天会下雨吗”模型没有调用geocode_city而是直接把北京当参数传给天气接口。我预先在天气参数的Schema里设置了pattern要求纬度必须是-90到90之间的数字因此在缺失时直接校验失败。随后错误信息“缺失lat/lng参数请先调用geocode_city”被喂回给模型模型立刻补了前置调用。这一整套流程跑下来让我坚信校验错误信息是引导模型行为的一等工程手段。4.3 实测中的幻觉模型越界调用了你没给它定义的东西跑了一段时间后我攒了不少“模型幻觉”案例。最典型的是参数幻觉用户问“查一下明天早上9点的天气”模型把时间参数传成了tomorrow 9am不符合ISO格式。因为有了校验层这个请求被拦截模型根据错误提示重新生成了2026-07-02T09:00:00用户毫不知情。如果没这层拦截10个应用里可能有8个会把tomorrow 9am直接传给天气API返回“无法识别时间”的报错而用户会一脸问号。还有一个更隐蔽的幻觉多技能协同时的字段误用。模型在搜索文档后拿到返回的doc_id字段接着把它当成title传给了创建日程技能。乍一看没啥但日程系统里多了一条叫“doc_12345”的日程用户根本不知道那是什么。这个问题的根源是技能描述的字段语义不够清晰。后来我在日程技能的时间字段上加了“ISO格式时间不是文档ID”并在title字段的description里写了“用户自定义的标题”这类误用才基本消失。5. 我在迭代中踩过的五个坑和对应的解决方案5.1 重名技能被静默覆盖给注册表加命名空间给客服项目加新模块时我写了一个get_user_info技能另一个同事在订单模块里也写了一个同名技能结果后者在注册时把前者静默覆盖了。我没有异常日志整个系统表现诡异客服报“查不到订单用户”后台却正常。排查近两个小时后我才发现注册表里同一个名字只保留了一个。这个坑的解法有两个层面第一注册时遇到重名直接抛异常第二支持带命名空间的技能名比如user.get_info和order.get_info。从那次以后我再也没遇到过“不知被谁覆盖”的情况。5.2 模型返回的JSON参数多了一个未知字段有一次我惊喜地看到Agent跑通了一个五步复杂流程但最后一步下游API突然崩了。查日志发现模型在调用create_calendar_event时除了我定义的title、time、location之外还自己加了一个attendees字段。虽然我的handler没用这个字段但因为我在执行时直接把参数原样透传给了日历API导致API序列化失败。解法很简单在参数Schema中开启additionalProperties: false未在Schema里声明的字段直接在校验阶段被拒绝。如果你确实想允许额外字段必须显式声明在Schema里。这样模型再怎么“发挥”也不会污染下游系统。5.3 技能调用超时拖垮整个对话Agent响应速度取决于最慢的技能调用。有一次我的“路况查询”技能内部有个傻循环遇到某类输入会重复做无用计算单次响应拖到几十秒。用户端表现为Agent一直转圈最后超时。我给装饰器加了timeout参数执行时用asyncio.wait_for包住handler超时后返回结构化错误“技能执行超时请简化问题或稍后再试”。长耗时的技能我建议设计成独立异步任务不要占用LLM响应链路。这个机制让一个卡住的任务不至于毁掉整个对话体验。5.4 没日志查不了“模型为什么这么选”我在前面踩的坑大部分靠日志才定位到。给agent-skills加上调用日志之后我要求每个技能调用都必须记录skill_name,input,output,duration,error五要素。刚开始只是print到控制台后来直接接到了可观测系统里。有了日志你能很清楚地看到模型选择了什么技能、传参是否合理、哪个技能慢了几百毫秒、哪个技能抛了异常。有一次用户投诉“Agent乱说话”我看日志发现模型连续三次调用了同一个错误技能根源是该技能的description里有一个错别字导致模型误解。改个词就解决了。没有日志这种问题只能靠猜。5.5 别只测正常路径单元测试要覆盖“脏调用”我的很多测试早期只覆盖“合法输入返回预期结果”结果一上线就被模型的花式脏数据打脸。后来我要求每个技能至少写三个用例正常输入、缺少必需参数、参数类型错误。同时写了一个“脏调用”测试直接调用注册中心的校验器断言它返回普通用户可读的错误结构而不是Python异常。这里有一点很关键断言出错时异常信息本身也是产品的一环。因为Agent会把异常信息原样反馈给模型所以对错误信息的措辞要像写文案一样认真。我甚至专门写过几个“引导性错误文案”的测试确保模型看到后能立刻修正而不继续犯傻。6. 扩展从单Agent到多Agent的技能共享6.1 把技能做成可安装的插件包复用比复制轻得多单个Agent的技能管理捋顺后下一个痛点是多Agent复用。我的客服Agent和运营分析Agent都要查订单数据早期做法是复制粘贴一份代码过去结果两边各自修改几个月后出现了两套行为完全不同的“查订单”。现在的做法是把技能做成一个普通的Python包里面包含register_skills(registry)函数。Agent启动时调用这个函数把技能注册进自己的Registry。这样技能模块与Agent业务彻底解耦升级包即可更新所有接入方的能力。新Agent接入技能的时间也从半天缩短到十几分钟。6.2 给技能配置权限等级别让只读Agent顺手写库技能共享之后权限管理成了不可回避的问题。我在Skill模型里加的permission字段配合Registry的max_permission可以让每个Agent只注册它应该拥有的技能。实际操作方法是在Agent的配置里声明max_permissionread注册中心遇到权限高于read的技能就跳过并打印警告日志。一次我在生产环境误把运营侧的配置写成了write权限日志立刻打印出大量“跳过技能”警告这才意识到配置弄错了。如果权限机制缺失那些写操作技能就可能在一个毫无防备的Agent里偷偷跑起来。6.3 关于未来一个可组合的技能协议而不是另一套框架聊到最后我想说点更宏观的体会。agent-skills虽然是我自己的一个轻量组件但它的设计思路其实指向一个更大的可能性Agent技能可以被标准化描述进而像一个开源库那样被分享和安装。现在每家Agent框架都有自己的一套工具定义互相不兼容。如果有一天大家都能用统一的JSON Schema加上模型可读的描述来定义技能那么社区里就会涌现出无数可组合的“技能包”。OpenAPI做过类似的事但它针对的是HTTP接口而Agent技能还需要额外地提供“何时使用”、“依赖什么”以及“权限边界”这些维度。对我个人而言这个项目最大的收获不是代码量而是让我看清了“技能描述”的重要性。你为一个技能写的description不只是给模型看的说明更是给整个系统的“使用说明书”。描述写得越准确模型编排就越顺利踩坑越少。这也是我想对所有做Agent的朋友说的一句话别急着堆函数先把技能的契约想清楚。