ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

从零搭建Agent技能体系:技能注册、参数校验与编排实战

从零搭建Agent技能体系:技能注册、参数校验与编排实战 1. 从零搭建 Agent 技能体系为什么每个智能体都需要一份“技能清单”接触过 AI Agent 开发的朋友应该都有这种体会模型本身能力再强如果不给它一套清晰的技能框架它就像一个满腹经纶但不知道从何下手的实习生——什么都懂一点真让它干活就蒙圈。我最近在做一个基于大模型的多步骤任务代理踩了不少坑之后把整套技能体系从设计到落地完整梳理了一遍也就是这个项目标题里说的agent-skills。这篇文章把核心思路和实操过程写出来给正在做 Agent 开发、工具调用链设计或工作流自动化的朋友一个可直接参照的模板。这套技能体系解决的核心问题是让 Agent 从“能聊”变成“能干活”。它把模型之外的能力按模块化方式注册、组合、编排让 Agent 知道在什么场景下调用什么技能、技能之间如何衔接、失败了怎么降级。适合准备做个人效率助手、企业内部知识库问答、自动化报表生成、代码辅助代理等方向的同学参考。2. 整体设计思路技能不该是散装工具而是一张可组合的能力网2.1 为什么要把技能单独抽象一层很多初期的 Agent 项目里工具调用和任务逻辑经常是耦合在一起的。比如一个查天气的功能直接把 API 请求写在任务流程里一个算税收的功能又单独写一段判断逻辑。这样做的结果是每增加一个场景代码就要跟着改一遍模型和业务逻辑完全纠缠在一起后期维护非常痛苦。把技能单独抽象一层本质上是做了职责分离。Agent 只需要知道“我有哪些技能可用、每个技能输入输出是什么”而不需要关心技能内部的实现细节。这就像你请了一个私人助理你只需要告诉他“帮我订机票”这个目标他来决定是打开航司 App、还是调用第三方接口来把这件事办成。任务规划者和技能执行者之间有一个清晰的接口约定这个约定就是技能描述。在我的项目里我把技能定义为一个包含名称、描述、输入参数 schema、输出格式、执行函数、错误处理策略的完整单元。Agent 收到用户请求后先解析意图再从技能注册表中检索匹配项最后通过统一的执行器调用。这样的好处非常明显新技能可以即插即用不影响已有流程每个技能可以独立测试、独立迭代模型只需要和技能描述打交道上下文长度压力更小出现问题时可以快速定位是规划错误还是执行错误2.2 技能分类按“原子性”和“复合性”划分层级在具体设计时我建议把技能分成两类一类叫原子技能一类叫复合技能。原子技能是最小执行单元不可再拆分。比如“读取文件内容”“发送 HTTP 请求”“执行 Python 代码”“查询数据库”这类技能只做一件事输入输出完全确定。复合技能则是把多个原子技能按特定的业务流程编排起来比如“生成周报”这个复合技能内部会调用“查询数据”“解析模板”“渲染 Markdown”“发送邮件”四个原子技能。为什么这样分因为原子技能复用性高你可以像搭积木一样把它们组合成任意复合流程而复合技能更适合面向具体业务场景暴露给 Agent 时描述更直观。如果只有原子技能Agent 每次都要从头规划步骤规划次数多了就容易出错如果只有复合技能业务一变就得改大量代码不够灵活。我的做法是技能注册表里两类技能同时存在复合技能在声明时直接引用原子技能的 ID形成依赖关系。执行时先跑复合技能的编排逻辑再递归调用子技能最终落到原子技能上。2.3 技能描述怎么写直接决定 Agent 调用准不准这块是我踩坑最多的部分值得单独拿出来说。技能描述不是写给程序员看的注释而是写给模型看的路标。模型在决定要不要调用某个技能时靠的是你告诉它的那几百字描述。描述写得模糊模型就会乱调写得过于技术化模型又会理解不了。我后来总结出一套相对稳定的描述模板包括四个部分这个技能是干什么的一句话说清避免抽象什么场景下应该用给出典型用户意图什么场景下不应该用要做负向排除输入参数里每个字段的含义和取值范围举个例子。我写了一个“get_stock_price”技能早期描述只写了“获取股票价格”结果模型经常把它用在查询历史行情、计算收益率这种场景上。后来我把描述改成“获取指定股票代码的实时交易价格。适用于用户询问‘现在价格多少’‘最新股价’等场景。不适合查询历史价格走势、涨跌幅排行、财务数据这些需求请使用其他对应技能。”改动之后调用准确率提升非常明显。3. 核心细节解析技能注册、参数校验与执行器设计3.1 技能注册表让每个技能都有唯一身份技能注册表是整个技能体系的中心。我采用的是很轻量的方案没有引入复杂框架就是维护一个技能描述列表每个技能对应一个 Python 类或者函数再加上一份 JSON Schema 元数据。注册表负责三件事技能信息登记、依赖关系管理、运行时检索。技能定义的元数据字段我做得比较细但核心的只有这几个name全局唯一标识命名用蛇形命名法比如read_csv_filedescription上一节说的面向模型的描述文本input_schema输入参数的 JSON Schema声明每个字段的类型和约束output_schema输出结果的格式约定handler实际执行函数的引用tags技能标签方便按业务域检索注册的动作本身非常简单写一个注册函数把技能对象插入到字典里。但注意一点技能名称全局唯一是你最优先保障的约束。项目早期我因为贪省事直接让各个模块自己往注册表里塞技能结果出现两个名字相近但功能完全两样的技能Agent 经常调错。后来我加了一层注册校验逻辑重名直接抛异常还在 CI 流程里加了技能清单的 diff 检查。3.2 输入校验别让 Agent 的幻觉参数进入执行阶段和 LLM 打过交道的都知道模型在生成参数时偶尔会“一本正经地胡说八道”把参数类型搞错、枚举值超范围、甚至把必填字段漏掉。如果直接把这些参数交给业务函数执行轻则报错重则产生脏数据。所以我在技能执行器前面加了一道参数校验层用的就是 JSON Schema validator。每个技能注册时的input_schema不只是给模型看的文档它是真正参与执行流程的校验标准。校验逻辑分几步走检查必填字段是否有缺失检查字段类型是否匹配检查枚举值、范围限制是否满足对不合法参数做归一化或直接拒绝举个例子。我有个发送邮件的技能recipient字段要求是合法邮箱格式。有一次 Agent 在调用时把收件人写成了纯文本的名字校验层直接拦截并返回错误信息让模型重新生成参数。如果没有这道防线邮件就会发送失败甚至可能发到错误的地址那影响就大了。这里有一个实操经验分享校验失败的返回信息要写得足够结构化最好带error_type和suggested_action两个字段让 Agent 能根据提示自我纠错。单纯返回“参数无效”这种信息模型还是不知道该怎么改。3.3 执行器统一调度处理同步、异步和超时技能的底层执行方式差异很大。有的技能是本地函数毫秒级返回有的技能要调第三方 API可能要等两三秒还有的技能是长时间的批处理任务比如生成一份完整的数据分析报告可能要跑一两分钟。我设计执行器时考虑了这几种场景的分发。本地同步技能直接用线程池处理耗时较长的 API 技能用 asyncio 协程调度特别重的任务则走消息队列先返回任务 ID让 Agent 轮询状态。执行器还统一处理了超时、重试、熔断这三层容错逻辑。超时时间我建议根据技能类型动态配置不要用一刀切的固定值。一个读内存缓存的操作5 秒超时都算慢但一个生成图片的技能30 秒也不一定够。我把超时配置放在技能元数据里每个技能自己声明一个timeout_seconds字段执行器按声明值来控制。重试策略也很有意思。不是所有失败都适合重试。如果错误是参数非法、权限不足这类确定性错误重试一万次也白搭如果是网络抖动、上游服务 5xx那重试倒是很有必要。我把重试条件限定为“瞬时错误”并且设置了指数退避避免上游服务被打爆。4. 实操过程从零实现一个可投用的技能编排系统4.1 技能模块定义用数据类把元数据和函数绑定在一起实际编码时我用 Python 的dataclass定义技能元数据再配一个注册装饰器整个注册过程非常干净。这不是什么高深的设计但在工程实践中能明显提升团队协作效率。import asyncio import json from dataclasses import dataclass, field from typing import Callable, Any, Optional from jsonschema import validate, ValidationError dataclass class AgentSkill: name: str description: str input_schema: dict handler: Callable[..., Any] output_schema: Optional[dict] None timeout_seconds: float 5.0 tags: list field(default_factorylist) retry_on: tuple (TimeoutError, ConnectionError) max_retries: int 2 SKILL_REGISTRY {} def register_skill(skill: AgentSkill) - None: if skill.name in SKILL_REGISTRY: raise ValueError(f技能重名: {skill.name}) SKILL_REGISTRY[skill.name] skill def get_skill(name: str) - AgentSkill: return SKILL_REGISTRY[name] def list_skills() - list[str]: return list(SKILL_REGISTRY.keys())我把技能的元数据定义和执行函数放在同一个数据类里这样你在阅读代码时一个技能的全貌是完整体现的。注册装饰器也可以做但我更倾向于显式构造AgentSkill对象然后调用register_skill。显式比隐式少一些魔法排查问题时更直接。4.2 执行器实现参数校验、超时控制和结果标准化接下来是技能执行器的核心逻辑。它做的事情概括成一句话接收“技能名 参数字典”校验执行把结果转成 Agent 可以消费的标准格式。标准格式我定义为{ status: success | error, data: ..., error: {code: ..., message: ..., suggested_action: ...} }Agent 的后处理逻辑完全依赖这个结构保证格式统一非常重要。def execute_skill(skill_name: str, params: dict) - dict: skill get_skill(skill_name) # 参数校验 try: validate(instanceparams, schemaskill.input_schema) except ValidationError as e: suggest 请检查参数类型和必填字段重新调用该技能。 return { status: error, data: None, error: { code: INVALID_PARAMS, message: f参数校验失败: {e.message}, suggested_action: suggest, }, } # 异步执行 超时控制 try: if asyncio.iscoroutinefunction(skill.handler): result asyncio.run( asyncio.wait_for(skill.handler(**params), timeoutskill.timeout_seconds) ) else: loop asyncio.new_event_loop() result loop.run_until_complete( asyncio.wait_for(asyncio.to_thread(skill.handler, **params), timeoutskill.timeout_seconds) ) loop.close() return {status: success, data: result, error: None} except asyncio.TimeoutError: return { status: error, data: None, error: { code: TIMEOUT, message: f技能 {skill_name} 执行超时超过 {skill.timeout_seconds}s, suggested_action: 提醒用户稍后重试或使用其他技能替代。, }, } except Exception as e: return { status: error, data: None, error: { code: EXECUTION_ERROR, message: f技能异常: {str(e)}, suggested_action: 请尝试更简单的子任务或检查输入参数。, }, }这里有一个实际工程里的注意事项asyncio.run每次执行都会创建一个新的事件循环如果同一个技能在并发场景下被多次调用频繁创建循环的开销比较大。我在这套代码里用了asyncio.to_thread来包同步函数让消耗型 IO 操作不阻塞 Agent 的主调度循环。如果你的技能里包含有状态的对象或需要共享连接池请把相关逻辑封装到技能内部的单例或者全局变量里避免跨事件循环的状态冲突。4.3 技能编排让 Agent 学会调用多个技能完成复杂任务有了原子技能和执行器下一步就是让 Agent 学会编排。我试过多种方案包括用 LangChain 的 Agent 框架、用 Function Calling 的原生机制、以及完全自己写规划循环。最终在项目里定下来的是基于 Function Calling 做一层轻量封装原因是可控性最强不会引入太多黑盒逻辑。核心流程是这样的把注册表里所有技能的描述和参数 schema 拼装成模型 API 的 tools 参数用户输入一个问题模型返回意图判断要么直接回答要么请求调用一个或多个技能开发者调用执行器把技能执行结果返回给模型模型根据结果决定继续调用下一个技能还是生成最终回复重复第 3 步到第 5 步直到模型判断任务完成。我把这个流程封装成一个run_agent函数内部维护一个
返回列表