2026/10/11 22:05:59

agent-skills:AI Agent技能管理与模块化编排实战

agent-skills:AI Agent技能管理与模块化编排实战 最近一个月我都在和 AI Agent 打交道越做越发现真正卡住开发进度的往往不是大模型本身的能力而是 Agent 的“手脚”不够灵活。你可以让模型记住对话、理解任务但要让它在真实环境里干活——查数据、调接口、操作文件、调用工具——就必须有一套清晰可控的技能机制。这也是我决定折腾 agent-skills 这个项目的直接原因。简单说agent-skills 不是一个重型的 Agent 框架它更像是一套“技能管理方案”解决的是 Agent 开发里最烦人的几个问题功能代码散落各处、工具调用逻辑和业务耦合太深、想复用某个能力要到处复制粘贴。如果你现在正在做 Agent 应用或者准备把一个原型级的 Agent 产品化这篇文章应该能帮你省不少时间。1. 项目定位与核心痛点Agent 开发到底缺什么1.1 从一坨代码到可编排的技能模块我先说个现象。大多数接入大模型 API 的 Agent 项目早期都会经历一个阶段把所有工具函数写在一个文件里每个函数被大模型以 JSON 格式调用看起来没什么问题甚至很爽。但一旦业务变复杂痛点就全冒出来了。第一个痛点是技能和业务逻辑互相纠缠。比如我要做一个内部的数据看板 Agent它需要查数据库、调用外部报表接口、把结果格式化返回给大模型。如果这三件事都写在同一个函数里那大模型调用的时候必须知道内部细节不然参数都传不对。更麻烦的是当你新增一个数据源时可能要改动原有函数的签名这对已经上线的技能是致命的——因为大模型会严格按照之前的函数定义来生成参数。agent-skills 解决这个问题的方式是把每个技能做成一个自治模块。技能的定义、校验、执行、返回逻辑全部收敛在一个单元里对外只暴露清晰的入参和出参。大模型只需要知道这个技能是干什么的、参数长什么样至于它内部是连数据库还是查缓存那都不是模型该关心的事。第二个痛点是技能没有“体检”机制。生产环境里我会遇到一种情况某个数据源超时了或者某个接口改了字段但 Agent 完全不知道依旧按照老的 schema 去生成调用然后拿到错误结果还在煞有介事地“分析”。本质上之前我把技能当成普通函数来写函数不报错就默认成功完全没有考虑技能执行结果的可信度。这个项目从一开始就强制每个技能返回结构化的执行报告里面包含状态、耗时、数据质量标记、错误上下文。有了这份报告Agent 才能做出正确的下一步决策。这一点在 Agent 类应用里体验差异相当明显。第三个痛点是——也是我觉得最核心的——技能复用效率太低。我在一个 Agent 里写好了日期解析、去重、格式化这些通用能力换到另一个 Agent 项目的时候只能复制粘贴代码然后祈祷别改漏了。agent-skills 统一了技能注册和加载入口通用技能通过配置即可接入业务技能独立开发再注册两套逻辑分开走互不污染。这就好像你从“在每个房间里分别放一套工具箱”变成“每个工人随身带标准工具包按需取用”。1.2 什么人、什么场景适合直接上手如果你正在做这几类项目我觉得 agent-skills 特别对路。第一类是多工具助手型 Agent。比如你要做一个能查天气、查日程、发邮件的个人助理技能模块化能让你快速组装能力而且单个技能的改动不会干扰其他技能。第二类是行业专属 Agent。比如客服 Agent、法律咨询 Agent、运维 Agent技能本身就是领域经验的沉淀。把业务人员的经验和开发者的代码结合成技能模块这个过程其实就是知识编码化的过程。第三类是同一套 Agent 能力需要对接到不同业务线的情况。典型场景是某公司内部的三四个部门都要做自己的 Agent 应用但都共用一套用户信息查询、权限校验、数据统计能力。如果没有技能层每个项目组都得各自实现一遍而用 agent-skills 的做法公共技能发布一次各业务线按自己的需求引入即可。当然我也得说实话这项目不太适合超小型的玩具 demo。你如果只是调一次 API 给朋友演示那直接写函数就行根本不需要技能管理层的复杂度。agent-skills 的意义在“多技能、多场景、可演进”这个阶段才会充分体现。2. 整体架构设计为什么技能层不等于普通工具集合2.1 技能的最小数据模型让 Agent 正确理解能力边界我在设计 agent-skills 的时候最重要的一条原则是技能层不是简单的“函数注册表”而是要给 Agent 足够上下文信息的“能力描述系统”。为此我为每个技能建立了一个结构化定义包含这么几个部分。每个技能都有一个稳定的标识符比如user.query_profile或report.sales_fetch。标识符的命名规范参考了领域驱动的思路前面是技能所属域后面是具体操作这样做的好处是方便日志检索和权限控制。入参和出参都采用 JSON Schema 描述。选择 JSON Schema 而不是自定义类型是因为大模型对 JSON 的理解本身就很好而且我可以通过严格的 schema 定义做参数校验减少大模型幻觉参数进入实际执行链路。每个参数都要写明类型、是否必填、枚举范围、示例值示例值这个东西非常有用模型参考它生成的参数准确率会高不少。技能执行体是可调用的函数或者异步任务这是实际干活的部分。它接收已经校验过的参数返回结构化结果。在 agent-skills 里执行体不直接面对大模型的自由文本输出这层隔离很重要等于势力范围划分模型只负责理解意图并按 schema 生成参数业务代码只负责执行并返回标准结果。一个可选的但很关键的字段是技能依赖用来声明当前技能的运行条件。比如某个技能需要先有用户登录态或者需要另一个技能先执行完成。把这些依赖关系显式表达出来技能编排的时候才不会乱套。这个最小数据模型我实测下来最大的效果就是“信息密度”提升了。模型不再需要从一堆注释和函数名里猜技能怎么用每一条定义都按标准排列模型可以在更少的 token 里找到需要的信息。这也直接反映在调用的成功率上减少了无效的参数构造尝试。2.2 注册与发现让技能像插件一样接入系统有了技能定义下一步就是要让它能被 Agent 运行时发现并调用。agent-skills 采用两阶段设计注册阶段和发现阶段。注册阶段发生在项目启动时技能模块会被扫描并加载。开发者只需要在技能实现文件上标注类型信息注册器自动收集技能名称、schema、执行入口构建出一张技能清单。这里我特意把注册做成自动的降低接入成本。你不需要在某个中心文件里手动罗列每个技能新写的技能只要在项目目录内就会被发现。发现阶段是运行时行为。Agent 在执行一个任务前首先要“看到”当前可用的技能列表。agent-skills 支持按需加载技能描述什么意思呢就是我不会把成百上千条技能定义全部塞进提示词里而是先按业务场景筛选出一个候选集合再交给大模型选择。候选集合的筛选可以用语义检索也可以用规则匹配看业务侧的资源情况。这种设计换来的好处是每一个技能的 prompt 空间占用是可控的。技能多到一百个以上的时候如果全部塞进上下文光是描述技能定义就可能超过 token 限制模型也更容易混淆。而如果只有五六个技能在候选集里模型的调用准确率会明显提升项目大了以后这个架构优势会很突出。注册与发现分离还有一个附带好处运维上可以做灰度。某一个技能的新版本可以先注册到预发布环境跑一遍回归测试再切到生产环境。对于已经上线稳定运行的技能也可以做版本冻结不让它们受新技能的干扰。2.3 执行上下文技能之间如何安全地共享信息技能往往是需要互相配合的。比如“查用户订单”这个技能执行前可能需要“获取当前用户身份”这个技能的结果。如果每个技能的输入都完全独立编排起来会非常低效。agent-skills 里引入了执行上下文的概念它是一个贯穿整个 Agent 任务生命周期的数据容器。上下文里可以存放当前用户、会话 ID、已经取得的中间结果、安全令牌等。每个技能在声明时可以选择“读取上下文”和“写入上下文”的具体字段而不是把整个上下文暴露出去。这一点是安全隔离的核心。我不希望某个第三方写的技能能读取所有用户数据所以上下文的访问控制必须细化到字段级别。一个技能只能读取它声明的字段也只能写入它声明要输出的字段。运行时框架做校验写一个不在声明范围内的字段会被拦截并告警。在多数场景下上下文共享确实能大幅提升效率。比如多个技能都要校验用户角色那就做成一个内置的权限校验技能运行一次结果放入上下文后续技能直接从上下文读取省去了重复调用和重复延迟。但每次新增上下文字段时都要回顾一下访问范围别怕麻烦等出了数据泄漏问题再收紧就晚了。3. 从零开始搭建agent-skills 的执行引擎实战3.1 技能注册器实现让自动发现替代手动登记我在实际动手实现 agent-skills 的时候第一步做的不是技能本身而是技能注册器。这个模块负责扫描项目目录、读取技能定义、构建运行时清单。我用 Python 写的正好 Python 的装饰器机制可以让技能声明变得非常简洁明了。具体做法是这样先定义了一个基础装饰器skill它接收技能所需的 schema 信息。开发者把它挂在任意一个异步函数上注册器启动时遍历指定目录下的所有模块找到带这个装饰器的函数自动提取函数的 name、description、参数 schema注册进全局技能表。整个注册过程只需要一次启动扫描之后运行时就固定访问这份静态清单。我贴一段简化版的核心代码展示注册器的骨架逻辑# registry.py import inspect import importlib import pkgutil from dataclasses import dataclass, field SKILL_REGISTRY {} def skill(name, description, parametersNone, dependenciesNone): def decorator(func): SKILL_REGISTRY[name] { name: name, description: description, parameters: parameters or {}, dependencies: dependencies or [], handler: func, } return func return decorator def discover_skills(package_name): package importlib.import_module(package_name) for _, module_name, _ in pkgutil.iter_modules(package.__path__): importlib.import_module(f{package_name}.{module_name}) def get_skill(name): skill_def SKILL_REGISTRY.get(name) if not skill_def: raise KeyError(fSkill not found: {name}) return skill_def这段代码看起来短但我实际用下来有几个细节值得注意。装饰器里的parameters必须和handler 实际接收的参数严格对齐否则启动时没问题调用时会传参报错。我一开始没加校验结果某次上线后才发现有两个技能传参多了字段模型生成调用时倒是正常执行但执行结果和预期对不上排查了很久。后来我加了一条规则注册阶段就读取 handler 函数签名比对 schema 声明的参数和函数实际参数不一致直接启动失败。这个强制校验虽然前期有点烦人但确实是技能正确性的第一道防线。还有一点是关于异步的支持。Agent 场景里技能执行往往涉及网络调用用同步函数会阻塞事件循环。我没有强制所有技能必须是异步的注册器会检测 handler 是否是协程函数如果不是就自动包一层线程池调用。这样既保证了老代码的兼容性又避免了事件循环阻塞。3.2 技能调用执行器参数校验、超时控制与错误归一技能被模型选中后接下来就是执行器的工作。执行器要处理这么几件事参数解析与校验、超时控制、错误归一化、结果封装。每一件单独看都不难但合在一起直接影响 Agent 的执行稳定性。参数校验我直接用了 jsonschema 库。大模型生成的参数偶尔会出现字段名写错、类型不对、缺字段这些问题如果不校验直接传给业务函数业务函数往往抛出难以理解的异常。我写了一个统一的校验入口先把模型输出的参数体解析成 JSON再和技能声明的 schema 做校验校验不通过就把错误信息整理成人类可读的提示返回给模型要求模型修正后重新调用。超时控制这块我建议所有技能必须设置超时时间。某些接口偶尔抽风卡住如果技能不设超时整个 Agent 任务就卡在那里。我实现的时候给每个技能加了默认超时时间和可配置超时两层当技能声明里没有指定超时时执行器使用全局默认值。超时后会抛特定异常执行器捕获后把超时信息返回给模型模型可以选择重试或者更换思路。错误归一化是我觉得最影响体验的一块。以前直接把底层异常抛给大模型大模型经常被异常文本里的无关信息带偏导致它回复一堆不知所云的内容。现在我要求执行器捕获所有异常并且把它翻译成一段标准化的错误描述技能名称、失败原因、可能的解决方案。模型看到的是干净的错误信息而不是一堆堆栈调用。这个翻译工作本身不难难的是你要理解每个技能的典型报错场景写出一句模型能看懂并做出决策的话。结果封装方面所有技能出参都必须是一个字典至少包含ok、data、error三个字段。ok是布尔值data是成功时的数据error是失败时的错误信息。这个统一结构让 Agent 后续处理逻辑非常简单先看ok再决定继续还是切换技能。3.3 技能编排示例用上下文串联一个多技能任务只有单技能的 Agent 其实没什么好编排的真正体现 agent-skills 表达力的场景是多个技能按顺序配合完成一件事。比如我要实现一个“查询用户本月消费总结”的能力它就会拆成三个技能先获取用户身份再拉取消费明细最后聚合生成总结。这三个技能不是任意调用的它们之间存在依赖。我通过执行上下文的读写声明来约束流程。第一个技能写user_id到上下文第二个技能声明从上下文读取user_id并写raw_orders第三个技能读取raw_orders并输出monthly_summary。运行时检查到前置依赖未满足时会暂停执行并把需要先执行的技能列表返回给模型。我把这段编排逻辑简化成伪代码帮助理解上下文怎么串起来# executor.py简化示例 async def execute_skill(skill_name, params, context, timeout30): skill_def get_skill(skill_name) # 校验依赖依赖的字段必须已在 context 中 for dep in skill_def[dependencies]: if dep not in context: return { ok: False, error: fMissing dependency: {dep} in context, data: None, } merged_params {**params, **{k: context[k] for k in skill_def[dependencies]}} # 参数校验、执行、错误归一化的省略…… result await asyncio.wait_for(skill_def[handler](**merged_params), timeouttimeout) # 写回 context if result.get(ok): context.update(result.get(data, {})) return result这里有一个设计取舍参数合并的时候模型传参的优先级要高于上下文中的同名参数。因为上下文里的数据可能是几分钟前取得的而模型在最新一次交互中可能拿到了更新的值不让模型参数覆盖会有数据过期风险。反过来如果优先级低模型就得被迫每一步都把旧参数重新传一遍浪费 token 还容易传错。编排的粒度也得注意。不要把过大的业务逻辑封装成一个技能那样技能内部的黑盒程度太高模型无法在中间步骤做适应性调整。我一般控制在单一职责的粒度一个技能完成一个明确的数据变更或查询操作。这个粒度过细也有问题会带来过多的调用轮次实际开发中要踩几次坑才能找到那个平衡点。4. 常见问题与排查技巧从日志到上下文污染4.1 模型生成的参数与 schema 不匹配这是 Agent 开发里我遇到的最频繁的问题。模型在生成调用参数时偶尔会把字段名写成相似词比如user_email写成email_address偶尔会把字符串类型写成了数字还有时候会传入完全没在 schema 里声明的字段。解决这个问题的第一道防线是强校验执行器校验不通过直接返回可读错误。但这就够了吗不够。我观察下来错误信息能不能准确指导模型修正决定了失败后是“一次修正成功”还是“反复试错浪费 token”。所以我在校验失败的错误提示里不仅告诉模型“缺少哪个字段”还尽量给出正确的字段名和可接受的值范围减少模型的猜测。如果频繁出现参数不匹配我会检查技能定义是不是不够清楚。比如参数的描述、示例值是否写明白了模型可读的信息够不够。我从实践中得到的经验是给每个必填参数加一个具体的示例值比一堆字段描述有效得多。模型参考示例值生成参数成功率能提升不少。4.2 技能执行成功的错误结果Agent 被假数据带偏这问题最阴险因为从程序层面看技能执行没有报错ok为真但返回的数据在语义上是错的。最典型的情况是数据库查询成功但没有数据接口返回了 200 但数据体是空值。我对策是在技能内增加数据质量检查。不是所有技能都要做但涉及外部数据读取的关键技能执行器在返回前会检查数据的基本形态列表是否为空、关键字段是否存在、数值是否在合理范围。检查不通过时技能可以返回okTrue但附带warning标记或者直接返回okFalse说明数据为空。Agent 看到 warning 后可以决定是换一种查询方式还是直接告诉用户“没有查到相关数据”。宁可明确说查不到也不能让模型拿空结果强行编一个答案。4.3 上下文污染跨任务残留数据干扰判断上下文设计得很好用但也带来了一个隐患如果上下文容器复用到了下一个会话上一个任务的残留字段就可能污染下一个任务的技能调用。我碰到过一次某次查询用户 A 的订单后下一轮会话查询用户 B 时上下文里的user_id没有更新导致查询结果全是用户 A 的数据而 Agent 没有察觉。这个问题的根因是任务生命周期内没有清空上下文。我的修改方案有两个方向默认情况下每个新会话创建全新上下文需要保留跨会话信息的场景必须有显式的“持久化字段”声明且这些字段要经过白名单校验。这样既能享受上下文共享的效率又防止数据串台。排查上下文问题时我建议在开发环境打开上下文变更日志每次写入都记录字段名、写入技能、时间戳、写入前后的值。一眼就能看出是不是被错误字段覆盖了。4.4 技能超时与重试别让单次失败拖垮整个会话技能超时在真实 Agent 应用中太常见了外部接口万一抖动几十秒的等待足以让用户认为整个应用卡死了。除了设置超时时间我还会在重试策略上做一些精细控制。比如网络类错误可以自动重试一次但如果错误类型是参数错误重试就没有意义直接返回错误让模型调整方案。重试之间加上随机延迟避免多个技能同时重试造成热点请求。超时时间的设置不能一刀切。数据库查询类技能我会给 10 秒外部 Web 请求给 20 秒本地计算类技能给 5 秒。每个技能的声明里都可以单独配置执行器读取配置后生效。这个参数粒度对整体体验优化很有帮助大模型不会干等一个注定要失败的技能。5. 进阶实践与扩展思考技能库如何长成能力资产5.1 技能的版本演进与兼容性策略当技能数量增多且多个 Agent 复用时版本管理就变得不可避免。agent-skills 支持每个技能带一个语义化版本号并且在技能定义里声明兼容版本范围。别的技能在依赖当前技能时会检查版本是否匹配不匹配的话在注册阶段就会暴露问题。版本策略上我坚持一个原则旧技能在废弃前要保持一段共存期。比如技能 v1 和 v2 可以同时注册新 Agent 默认使用 v2已有的老 Agent 仍走 v1等老 Agent 逐步迁移完毕后再下线 v1。这样保持了业务的连续性也留了灰度验证的时间窗口。代价是多维护一组代码但相比直接影响线上业务的成本这是划算的。5.2 技能监控与可观测性设计技能一旦上线必须能观测到它的运行状态。我为 agent-skills 补了一套基础监控每次技能调用会记录技能名称、耗时、结果状态、参数规模、错误类型。这些日志会汇总起来用于回答几个关键问题哪些技能被高频调用哪些技能失败率偏高哪些技能的耗时拖累了整个会话除了统计指标我看得比较多的还有“技能调用链”。一次 Agent 任务从开始到结束依次调用了哪些技能每个技能的输入和输出是什么这是一条很有价值的轨迹记录。技能调用链可以帮助分析 Agent 的推理过程它为什么决定调用这个技能参数为什么这么构造这个分析结论反过来又能帮助改进技能定义和提示词。5.3 从技能库到业务能力中枢模块化技能维护久了以后会自然沉淀出一套稳定的业务能力中枢。这些能力不再仅仅是代码接口而是同时包含了 schema 描述、测试用例、监控指标和版本记录。当新项目到来时团队先查看已有的技能库能够直接复用的就通过配置接入无法覆盖的再开发新技能这种模式对交付效率的提升非常明显。我还尝试过把技能库抽象成内部 API 平台让非技术成员也能用自然语言描述一个业务需求经过必要的流程生成对应技能定义再由开发者补充实现。这一步做到了以后技能不再是开发者的专属品而是整个团队共用的资产库价值一下子就不一样了。6. 写在最后的实战体会agent-skills 这个项目的核心收获不是代码写得多漂亮而是让我把“技能层”单独抽出来思考以后整个 Agent 开发的方式都变了。以前写 Agent 是先把模型串进去再临时加工具函数代码越写越乱现在是先定义能力边界再实现技能模块最后才让模型接入技能层逻辑上顺了很多。我最后再分享一个从踩坑里总结出来的原则技能接口的定义要尽可能稳定内部实现可以高频迭代。因为大模型的 prompt 和技能定义是强相关的接口一变模型生成参数的规律就要重新适应中间那段混乱期最容易出幻觉。而保持接口稳定内部怎么改都不会影响模型的调用习惯整个系统就会很稳。后面我计划继续做两件事。一个是扩展技能模板的覆盖面把更多常见的业务场景预置成可复用模板降低新项目接入的成本。另一个是把技能测试体系补完整每个技能都要带着自动化测试用例发版这样多个 Agent 复用一个技能时底层更新才不会牵一发动全身。这条路还很长但方向已经清楚了剩下就是一步步把技能库做成真正可靠的基础设施。