☰
AI Agent技能管理框架:从工具函数到可复用技能的实践指南
2026/10/13 8:00:02 网站建设 项目流程

刚开始做这个 agent-skills 项目的时候,其实没什么宏大目标。我手上当时有三个大模型驱动的内部小工具,每个都各自拉了一堆第三方接口,接口参数、返回结构、鉴权方式全都不统一,每接入一个新能力都要重写一遍调用逻辑。后来我把这些接口统一包成"技能",给每个技能写清楚它是什么、能做什么、需要什么参数和返回什么结果,再让模型根据任务描述自己去选技能、填参数、触发执行。跑通之后这套东西越用越顺手,就从临时脚本整理成了一个独立的小框架,名字就叫 agent-skills。

这篇文章就顺着做这套框架时的真实思路来写:技能协议怎么设计、注册调度怎么做、执行链路怎么打通、哪些坑我踩过现在还记得。如果你正在做智能体应用,或者单纯在纠结上百个工具函数怎么组织,可以直接拿着里面的方案去套。

1. 先想清楚:智能体的“技能”为什么值得单独做一层

网上聊大模型智能体的时候,大家常把 Tool、Function、Skill 混着用。实际上,这三者之间是有差别的。最开始我只是写了几个函数,暴露给模型当工具调用,比如check_stock(sku)、create_order(item_id, count)。函数本身没有问题,问题出在模型根本"看不懂"函数。它只能看到函数名和一串描述,参数含义全靠猜,返回值也没有约定,模型经常拿错误类型去填参数。

1.1 从工具函数到技能,本质上是在补“上下文”

工具函数面向的是开发者,是确定的输入输出。技能面向的是模型,它必须额外表达三件事:这个技能在什么场景下该被调用、参数有什么约束、调用成功后能拿到什么信息。我举个例子,同样是"查库存",裸函数只暴露sku一个参数,但实际业务里用户可能只会说"这个商品还有货吗",模型需要自己判断去调哪个接口、填哪个商品编码,这些判断依据必须写进技能描述里。

技能和工具最大的区别,是技能自带一份结构化的"自我说明"。这份说明不是给人看的注释,而是给模型做决策用的上下文。模型读懂了这份说明,才能在当前对话里做出正确的工具选择。这就好比一个新同事入职,你得先告诉他公司有哪些系统、每个系统管什么、输入什么能查到什么,他才能开始干活。技能层就是给模型做"入职培训"的标准化机制。

1.2 把技能单独抽离一层的三个现实收益

做过几个智能体项目之后,我越来越确定技能层值得独立,理由可以归纳为三点。

第一是复用。一个"查天气"的技能,可以在客服机器人、日程助手、内容生成器里反复用,而工具函数如果不做统一封装,每换一个业务就要重新对接一次接口。技能框架把"描述声明"和"执行逻辑"分开,新项目接入时只需要注册,不需要改代码。

第二是可控。技能执行过程中,超时、重试、权限校验、日志埋点都是横切需求,散落在每个工具函数里写会非常痛苦。统一在技能运行器里做隔离和处理,出问题只看一处日志就能定位是模型选错了技能,还是技能自己执行失败。

第三是模型可感知。这一点最容易被忽略。大模型做工具选择的依据只有函数名和描述的文本。描述如果写得不到位,模型就会选错。技能框架强制要求每个技能写清触发条件、参数说明、返回格式,等于用工程机制倒逼开发者把元信息补全。

agent-skills 这个项目的定位很直接:它不解决模型本身的问题,而是给"技能"这层抽象做一个完整生命周期管理,从定义、注册、发现、执行到反馈,都交给框架统一处理。

2. 核心设计与产品定位:一份描述 + 一段逻辑

整个框架的设计原则就一条:技能描述是给模型看的协议,技能实现是给机器跑的代码,二者严格分离。这样写技能的人可以只关心业务逻辑,模型调度交给通用引擎。

2.1 技能元信息协议是地基

协议设计得不好,后面全盘崩。我参考了 JSON Schema 的思路,给每个技能定了一套描述格式,主要包括身份信息、调用信息、约束信息三块。核心字段如下:

字段含义示例
name技能唯一标识stock_query
description技能的一句话说明查询商品实时库存数量
trigger触发条件与反例用户询问库存、缺货时触发;不处理下单
parameters参数定义,含类型、必填、约束sku: 字符串,长度 6-20
returns返回结构说明{ sku, stock, updated_at }
examples典型调用示例用户问"A 款还有货吗" → 参数 {sku: "A1001"}
timeout最长执行时间5000 毫秒

description 和 trigger 这两个字段花了最多时间琢磨。最早版本只有 description,模型经常把"查天气"和"查空气质量"搞混,因为它们的触发场景太接近。后来加了 trigger 字段,专门写"什么时候该用、什么时候不该用",误选率明显降下来了。

parameters 我直接用 JSON Schema 的能力来描述,因为大模型对 JSON 的理解能力是经过大量语料训练过的,类型、枚举、默认值、必填这些约束都能被准确解析。这里的经验是:参数约束能写多细就写多细,模型填参数的行为稳定性会跟着提升,特别是枚举范围这种约束,能有效挡住大量无效调用。

2.2 注册中心与调度的实现思路

技能注册中心就是一个内存字典,key 是技能名,value 是技能对象。核心对象封装了元信息描述、执行函数、校验器和状态统计。框架启动时扫描技能目录,自动注册所有技能;运行中也可以动态注册新技能,适合做插件化扩展。

调度的核心是"模型先选,规则兜底"。也就是把技能描述列表拼进系统提示词,让模型输出一个结构化的调用意图。框架再去校验模型选出来的技能是否存在、参数是否合法。这套方式比纯规则匹配灵活得多,也比每次把全量技能描述发给模型更省钱。

代码层面,技能注册的核心逻辑大概是这样的:

class SkillRegistry: def __init__(self): self._skills = {} def register(self, skill: "Skill"): if skill.name in self._skills: raise DuplicateSkillError(skill.name) self._skills[skill.name] = skill return skill def get(self, name: str): return self._skills.get(name) def describe_all_skills(self): return [s.metadata for s in self._skills.values() if s.enabled] def load_from_directory(self, path: str): for file in glob.glob(f"{path}/*.py"): module = import_module(f"skills.{file.stem}") for skill in module.__skills__: self.register(skill)

2.3 框架的模块划分

agent-skills 拆了四个小模块,彼此独立,互相只通过标准接口通信。

skill-sdk提供技能基类和装饰器,开发者写技能只需要继承基类、实现 execute 方法。skill-registry负责技能的生命周期管理,包括注册、启停、状态统计。skill-runner是执行引擎,负责参数校验、超时控制、错误归一化和执行日志。skill-context负责上下文传递,把当前会话 ID、用户 ID、调用链 ID 这些信息注入技能执行环境,方便做审计和追踪。

把这几个模块分开的原因是执行路径上的变更频率不一样。业务上增加新技能只动 skill-sdk 层;如果发现超时策略需要调整,只改 skill-runner;这两类改动互不干扰,发布风险也小。实际开发中每减少一次跨模块改动,都会节省大量联调时间。

3. 实操过程:把一个普通接口封装成可复用技能

下面我用一个真实的内部场景完整走一遍:把"查询商品库存"这个接口封装成技能。这个场景简单但有代表性,涵盖了描述编写、实现、注册、调用四个完整步骤。

3.1 定义技能描述与实现代码

先新建skills/stock_query.py,技能描述文件的元信息我直接写在类里。写描述时有一句话口诀:先讲这个技能是什么,再讲什么时候该用,最后讲什么时候千万别用。

from sdk import Skill, SkillMetadata, parameter class StockQuerySkill(Skill): name = "stock_query" version = "1.0.0" description = "查询商品实时库存数量,用于回答有没有货、还剩多少的问题" trigger = "用户询问某个商品是否有货、库存量、是否缺货时触发;不处理下单、不处理价格查询" @parameter( name="sku", type="string", required=True, pattern="^[A-Z]{1,2}[0-9]{4,18}$", description="商品唯一编码,例如 AB123456" ) def execute(self, sku: str, context=None): # 实际项目里这里调用库存服务 HTTP 接口 result = self._invoke_stock_service(sku) return { "sku": result["sku"], "stock": result["quantity"], "status": "in_stock" if result["quantity"] > 0 else "out_of_stock", "updated_at": result["ts"], }

几个细节值得讲一下。正则表达式pattern约束能减少大量无效参数,模型哪怕自己编了一个格式不合理的编码,也在校验层就被拦下来。trigger里的反例描述特别重要,"不处理下单"这种负面说明,反而能帮模型把预测分布收紧。

3.2 技能注册与加载

写好技能文件之后,在框架启动脚本里扫描目录注册。注册过程会把技能元信息缓存起来,供后续生成模型提示词和校验参数使用。

registry = SkillRegistry() registry.load_from_directory("./skills") me = registry.get("stock_query") print(me.metadata) # 元信息自动 JSON 化

这里踩过一个坑:技能目录里如果有语法错误的文件,import 阶段直接抛异常,整个服务起不来。后来给加载逻辑加了容错,单个技能加载失败只记录警告,不阻断其他技能的注册。生产环境里"熔断单点、保住全局"的思路非常有用。

3.3 智能体调用链路的数据流

技能注册好之后,核心调用链路分五步。

模型收到用户消息后,框架先把所有技能的元信息拼成一段"可用技能清单",交给模型做意图识别。模型输出一个 JSON 格式的调用意向,包含目标技能名和参数候选。框架解析这个 JSON,去注册中心查找技能对象,做参数类型和格式校验。校验通过后,运行器负责执行技能,记录耗时和结果。最后,执行结果返回给模型,模型基于真实结果组织回答内容。

这一步的关键在于"模型只负责选和填,不负责执行"——这样即使模型给出了错误参数,也不会直接打到真实业务上,框架可以把一次失败的调用拦截在服务边界之外。

3.4 一次完整调用回放

为了方便复盘,框架每次调用都会生成执行记录,字段包括会话 ID、技能名、输入参数、执行结果、耗时、错误信息。我在开发环境跑了一次完整链路,记录大致长这样:

{ "session_id": "s_8f2a91", "skill": "stock_query", "input": {"sku": "AB123456"}, "output": {"sku": "AB123456", "stock": 23, "status": "in_stock", "updated_at": "2024-05-02 10:22:11"}, "elapsed_ms": 320, "model_selection": "auto" }

看到这份回放就能明白框架的价值:一次请求从模型意图到业务结果的完整数据流全部可追踪,出了问题能精确定位是模型选错技能、参数解析失败,还是技能接口本身超时。没有这套记录,排查智能体问题的过程会非常痛苦。

4. 踩坑记录与排查思路:四个高发问题

做了大半年 agent 相关开发,遇到的高频问题主要集中在技能选错、参数不匹配、超时和状态污染四类。每个都吃过亏,把排查思路整理成速查表放在下面,供直接参考。

4.1 模型频繁选错技能:先审视描述再埋怨模型

最典型的现象是,用户问"这个周末有什么活动",模型却调用了天气技能。原因通常是技能描述里写了太多泛化词汇,或者两个技能的场景边界模糊。

排查步骤是先打开注册中心,看两个技能的元信息在文本上是否可区分。理想状态是:一个技能只负责一件事,描述尽量具体,在 trigger 字段写清反例。如果技能数量超过二十个,模型一次读太多描述会产生注意力稀释,这时候把常用技能放前面,把冷门技能折叠成摘要提示,效果会好很多。

4.2 参数类型对不上:字符串和数字的隐形冲突

模型在填参数时,经常把数字类型的参数输出成字符串。比如count参数要求是 int,模型输出"3",直接传给接口就会报类型错误。这个问题高频且隐蔽,因为同一个模型有时输出 int,有时输出字符串,表现不稳定。

解决办法是在框架层做宽松类型解析,校验之前先做一次智能转换,把合法字符串转成数字,同时保留严格报错兜底。我给参数校验器加了一个coerce开关,开启后先把"3"转成 3,转不成再抛错。这种方法在实践中把参数错误率降低了三分之一左右。

4.3 技能执行超时:给每个技能设定独立时限

技能涉及外部网络调用时,网络抖动会让技能挂起十几秒,模型等不到结果,只能给用户回复"不知道"。不同技能的基础延迟差异很大,统一超时时间不科学,比如查库存 300 毫秒,而导出报表可能需要 10 秒。

我的做法是为每个技能独立配置 timeout,超时后立即返回一个结构化错误,让模型用兜底话术回答,同时把异常标记为待重试。超时时间从技能元信息的测试数据里取 p95 延迟再乘 1.5 倍,宁可宽松一点也不要任务轻易失败。

4.4 技能状态污染:并发环境下共享变量是隐形杀手

技能代码里如果用了模块级变量存中间结果,高并发场景下会出现数据交叉污染。我第一次做并发压测时就遇到过:用户 A 的查询结果污染了用户 B 的会话上下文,排查了很久,根因是一个技能里用了全局字典缓存会话数据。

解决方案其实简单:技能内部不要保存任何可变全局状态,会话相关数据全部通过 context 参数显式传递。框架的 skill-context 模块由运行器统一建立和销毁,每个调用都隔离上下文对象,彻底切断跨会话的数据共享。

5. 后续扩展思路与个人心得

做完核心链路后,我又往框架里加了几个实用功能,这些扩展能明显提升技能的工程化程度。

5.1 技能版本管理与灰度发布

业务迭代后同一个技能可能产生新逻辑,直接覆盖老版本有风险。我引入版本号机制,每个技能在注册中心按name + version区分,智能体侧可以指定用哪套版本。灰度发布的办法是编写一个按 query 特征做路径分配的策略,让 10% 的线上流量走 v2,稳定后逐步调高比例。这套做法对技能接口逻辑升级特别有用,感觉有问题随时可以切回旧版本。

5.2 技能编排与组合:复合技能取代硬编码流程

单技能解决线性问题,遇到多步骤任务就需要编排。比如"查商品并生成补货建议"实际上涉及两个技能的组合调用:先查库存,再跑规则引擎生成建议。与其在应用层硬编码流程,不如定义复合技能,内部通过框架的编排器依次调用子技能。复合技能同样暴露标准元信息,上层模型只看到"补货建议"这一个选项,调用复杂度被框架消化掉了。

5.3 一点真实体会

最后分享我在整个项目里的几点感受。第一,技能描述是第一优先级,工程上花在描述上的时间永远值得,模型选不准技能时先改文字,别急着换更大的模型。第二,日志和回放数据是智能体项目的隐形资产,没有完整调用记录,任何问题排查都像摸着黑走路。第三,技能的边界要刻意划窄,一个技能只做一件事,以后编排时才可能松耦合。这套框架现在还在演进,下一步我准备支持技能依赖注入和跨进程调用,后面实现完再单独写一篇分享。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询