ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AI Agent技能封装实践:用agent-skills构建可持续演进的智能体

AI Agent技能封装实践:用agent-skills构建可持续演进的智能体 做了两年多AI Agent相关项目踩了不少坑之后我越来越觉得“agent-skills”这套设计思路是被低估的。很多团队做Agent一开始都忙着调prompt、接模型、上框架结果做到后面发现系统根本跑不动——不是模型不够聪明而是能力的组织方式出了问题。agent-skills的核心思想其实很朴素把智能体能够执行的每一个原子能力不管是查天气、发邮件、写SQL还是调内部接口都封装成一个独立的“技能”Skill。每个技能有自己清晰的语义描述、参数定义、执行逻辑Agent在收到用户请求后先判断需要哪些技能再按技能约定的格式去调用。看起来只是多了一层封装但正是这层封装决定了Agent是能持续演进还是永远停留在demo阶段。这篇文章我会从设计思路、实现细节、踩坑记录三个角度把agent-skills这套玩法讲透。不管你是正在搭自己的Agent应用还是想把已有的能力体系接入LLM都能从这里找到可以直接照抄的方案。我尽量不堆概念全部以能落地的代码和真实经验展开。1. agent-skills是什么把智能体的“手脚”标准化1.1 为什么“万能Agent”走不通我见过很多团队的第一版Agent代码结构大致长这样一个大函数里面if else判断用户意图然后调用各个业务函数。prompt里塞了一大段“你能做什么”的描述模型输出一个动作名代码匹配到就执行。这种架构在技能少于十个的时候勉强能跑一旦技能多起来马上就会出三个问题。第一个问题是意图识别经常出错。模型面对几十个动作名很难分清“查余额”和“查账单”的区别尤其是动作名是getData、handleInfo这种毫无语义的命名模型根本无从判断。第二个问题是每次新增能力都要同时改prompt、改函数逻辑、改参数解析三个地方同步少改一处就出bug。第三个问题是技能之间没有统一的错误处理和数据格式有的返回字符串有的返回JSON有的直接抛异常Agent无法判断下一步该怎么做。我最早带团队做客服机器人Agent时就吃过这些亏。当时接入的第三方系统有十几个核心动作接近三十个如果按单函数硬怼代码量爆炸不说模型准确率也一直在百分之七八十徘徊。后来我下了决心把全部能力重构成技能体系每个动作都封装成标准Skill再统一注册管理模型的正确率才提上来新增技能的效率也翻了倍。1.2 技能Skill与普通函数调用的本质区别有人会觉得技能不就是函数吗把函数包装一下有什么稀奇。表面上看确实差不多底层都是代码逻辑但从Agent系统的角度技能和普通函数有三个本质区别。**区别一技能带语义描述。**普通函数只有函数名和参数类型对模型来说毫无信息量。技能则有一段面向模型的自然语言描述告诉模型“这个技能在什么场景下使用、接收什么参数、返回什么结果、有哪些注意事项”。这段描述是给LLM看的是模型做意图路由的核心依据。**区别二技能有统一的调用协议。**所有技能对外暴露相同结构的接口输入是符合JSON Schema的参数输出是统一格式的结果对象包含状态码、数据、错误信息。Agent执行循环不需要关心具体某个技能内部怎么实现的只要按同一套协议调用就能正确处理结果或异常。**区别三技能是热插拔的。**普通函数写死在代码里要改就得重新部署。技能通过注册表管理可以动态注册、替换、停用。今天加一个技能明天调整某个技能的逻辑都不需要动Agent主体的代码。这也是技能体系能支撑系统长期演进的关键。我打过一个比方没有技能体系的Agent像一个只有手没有工具箱的修理工看到什么问题都得现找工具有技能体系的Agent像一个装备齐全的工程师打开工具箱就知道哪个螺丝刀配哪个螺丝工作起来有条不紊。agent-skills要解决的就是这个“装备管理”的问题。2. 技能系统的整体设计先定规则再写代码2.1 注册制 vs 动态发现我为什么选注册制设计技能系统第一件要决定的事就是技能怎么被Agent发现。主流的做法有两种一种是注册制所有技能在启动时或运行前显式注册到技能列表里Agent的prompt直接携带完整的技能目录另一种是动态发现制Agent运行时不提前知道有哪些技能而是通过某种协议去查询或调用MCP这类标准化接口。我个人的建议是项目初期和中期无脑选注册制。原因很简单注册制让Agent对自身能力边界有完整认知模型在决策时能看到全部可选动作意图路由的准确率会高很多。动态发现制虽然扩展性更好但它要求Agent在提问前先“发现”技能多一轮查询就多一次模型调用而且技能目录的提示本身也占用大量上下文。当然注册制也有一个代价当技能数量超过50个时把所有技能描述塞进prompt会变得臃肿影响模型的表现。我的处理方法是搭一个分层注册表按领域分桶。比如营销域的技能放一个组数据域的技能放一个组Agent先做领域级路由再在领域内做技能级路由。这样既保留了注册制的可控性又避免了技能列表过长的问题。等到技能数量真正到了几百上千的规模再考虑引入动态发现机制也不迟。2.2 技能描述的写法模型“看懂”的关键技能描述是整个技能系统里最容易被低估的部分。很多开发者的第一版技能描述就一句话“查询用户订单。”这样写虽然简单但模型在复杂场景里根本不知道该不该用它。我总结了技能描述应该包含的四个核心要素。第一个是适用场景。明确写出这个技能解决什么问题适合在哪种用户请求下调用。比如“当用户想查看自己历史购买记录、物流状态、订单金额时使用”这样模型就有明确的触发条件。第二个是参数说明。每个参数的含义、格式、取值示例都要写清楚尤其是可空参数和默认值一定要注明。第三个是返回说明。告诉模型技能返回的数据结构以及关键字段代表什么意义。第四个是边界和注意事项比如“仅支持查询近一年的订单”“当用户未登录时返回错误码AUTH_001”这类信息提前暴露给模型能减少大量错误调用。我实际测试过一段写详细的技能描述和不写描述的版本对比模型在复杂意图下的正确选择率可以提高20个百分点以上。当然描述也不能写太长控制在200字以内突出关键信息否则会稀释模型对核心指令的注意力。2.3 技能编排单技能、串行、并行与路由技能体系的第二个设计重点是编排。用户的一个请求往往需要多个技能协作完成。我梳理下来实践中常见的编排模式有四类。第一类是单技能模式用户意图明确一个技能搞定一切比如“帮我查一下今天天气”。第二类是串行模式技能之间有先后顺序前一个技能的结果作为后一个技能的输入比如“查天气然后帮我把防晒霜加进购物车”——先查天气再决定是否推荐。第三类是并行模式多个技能相互独立可以同时调用再合并结果。比如“对比一下这几款手机的价格和配置”。第四类是路由模式Agent根据条件从多个候选技能中选一个执行。比如用户说“提醒我下午开会”需要判断是创建日程还是设置闹钟。在代码实现层面串行和并行都比较好处理串行就是循环执行并行就是并发调度。真正考验设计的是路由模式它需要依赖技能描述的质量和用户请求的清晰度。我的建议是在Prompt里显式地把“技能选择”做成一个独立步骤先让模型输出候选技能列表和理由再调用执行这个两阶段设计能明显降低路由的错误率。3. 从零实现一套agent-skills模块3.1 目录结构与数据模型直接看一个可落地的实现。下面是我在一个日程管理Agent里用过的技能模块结构清晰而且容易扩展。skill_agent/ ├── skills/ │ ├── base.py # 技能基类 │ ├── registry.py # 技能注册表 │ ├── calendar.py # 日历相关技能 │ ├── reminder.py # 提醒相关技能 │ ├── weather.py # 天气技能外部API │ └── __init__.py ├── core/ │ ├── agent.py # Agent执行循环 │ ├── context.py # 上下文管理 │ └── llm.py # LLM调用封装 ├── schemas/ │ └── skill_schema.py # 参数Schema定义 └── main.py # 入口每个技能的数据模型包括四个部分字段类型说明namestr技能唯一标识如calendar.create_eventdescriptionstr面向模型的自然语言描述含场景、边界、示例parametersdictJSON Schema格式的参数定义executecallable技能的实际执行逻辑这个数据模型看着简单但我特别强调一点description和parameters要分离存储不要混在docstring里。把它们做成结构化字段后续做技能列表筛选、动态生成技能、做评估的时候都会方便很多。3.2 注册表与技能基类先写技能基类。我在base.py里定义了一个BaseSkill抽象类所有具体技能都继承它。# skills/base.py from abc import ABC, abstractmethod from typing import Dict, Any class BaseSkill(ABC): 技能基类所有技能必须继承并实现以下接口 name: str description: str parameters: Dict[str, Any] {} abstractmethod def execute(self, params: Dict[str, Any]) - Dict[str, Any]: 执行技能返回统一结构的结果 pass def validate_params(self, params: Dict[str, Any]) - Dict[str, Any]: 参数校验返回修正后的参数或抛出异常 # 实际项目中建议接jsonschema库做校验 return params然后写注册表。注册表的核心职责就两件事注册技能、按名字查找技能。# skills/registry.py from typing import Dict, List from .base import BaseSkill class SkillRegistry: def __init__(self): self._skills: Dict[str, BaseSkill] {} def register(self, skill: BaseSkill): if not skill.name: raise ValueError(技能必须定义唯一name) self._skills[skill.name] skill print(f[registry] registered: {skill.name}) def unregister(self, name: str): self._skills.pop(name, None) def get(self, name: str) - BaseSkill: return self._skills.get(name) def list_skills(self) - List[BaseSkill]: return list(self._skills.values())注册表的实现非常简单但有一个容易被忽略的设计点注册时机。我建议将所有技能在主程序入口处集中注册而不是在每个技能文件里自行注册。集中注册的好处是技能列表一目了然排查问题时能快速确认哪些技能真正上了线。3.3 三个典型技能示例接下来定义三个有代表性的技能。一个调用外部API一个操作本地数据一个依赖另一个技能的返回值。# skills/weather.py import requests from .base import BaseSkill class GetWeatherSkill(BaseSkill): name weather.get_current description ( 查询指定城市的当前天气。 当用户询问天气、气温、是否下雨等情况时使用。 参数city为城市中文名如北京。 返回格式{temperature: 26, condition: 晴, humidity: 60} ) parameters { type: object, properties: { city: {type: string, description: 城市名如北京、上海} }, required: [city] } def execute(self, params): city params[city] # 这里假设调用一个真实的天气API resp requests.get(fhttps://api.weather.example/v1/current, params{city: city}, timeout5) data resp.json() return {status: success, data: data}# skills/calendar.py from datetime import datetime from .base import BaseSkill class CreateEventSkill(BaseSkill): name calendar.create_event description ( 在日历中创建一条日程事件。当用户想安排会议、添加日程、预约时间时使用。 参数title为事件标题start_time为ISO格式的开始时间end_time为结束时间。 注意start_time和end_time必须是YYYY-MM-DDTHH:MM格式。 成功返回事件id失败返回错误信息。 ) parameters { type: object, properties: { title: {type: string}, start_time: {type: string, format: date-time}, end_time: {type: string, format: date-time} }, required: [title, start_time, end_time] } def execute(self, params): # 这里将事件写入本地存储 event_id fevt_{int(datetime.now().timestamp())} # 模拟写入成功 return {status: success, data: {event_id: event_id}}# skills/reminder.py from .base import BaseSkill class CreateReminderSkill(BaseSkill): name reminder.create description ( 创建一条提醒。当用户说提醒我...、到时间叫我...时使用。 参数content为提醒内容remind_at为触发时间。 注意该技能只创建提醒不读取日历。若用户同时给出约会日程先调用calendar.create_event。 ) parameters { type: object, properties: { content: {type: string}, remind_at: {type: string, format: date-time} }, required: [content, remind_at] } def execute(self, params): # 创建提醒逻辑 return {status: success, data: {reminder_id: rmd_001}}这三个技能覆盖了“外部API调用”“本地数据写入”“技能间协作”三种常见模式。特别说明一下reminder技能里的描述我显式写了“只创建提醒不读取日历若用户给出约会日程先调用calendar.create_event”。这句话非常重要它能引导模型在复杂场景下做出正确的技能编排决策而不是拿一个技能硬套所有情况。3.4 Agent执行循环从意图识别到结果返回技能定义好了接下来是整个系统的核心——Agent执行循环。这个循环本质上是三步让模型决策要调用哪些技能从模型输出中解析参数执行技能并返回结果。先写Agent主体的执行逻辑。# core/agent.py import json from skills.registry import SkillRegistry from core.llm import llm_complete class Agent: def __init__(self, registry: SkillRegistry): self.registry registry self.messages [] # 对话历史 def build_skill_prompt(self) - str: 把技能列表组装成语义化提示词 lines [] for skill in self.registry.list_skills(): lines.append(f- 技能名: {skill.name}) lines.append(f 描述: {skill.description}) lines.append(f 参数Schema: {json.dumps(skill.parameters, ensure_asciiFalse)}) return \n.join(lines) def run(self, user_input: str) - str: # 1. 组装系统提示词 skill_prompt self.build_skill_prompt() system_prompt f 你是一个日程管理助手。请根据用户需求选择最合适的技能并传递正确的参数。 可用的技能列表如下 {skill_prompt} 请以严格JSON格式输出你的决策格式为 {{skill: 技能名, params: {{参数名: 参数值}}}} 如果用户请求满足多个技能组合请按顺序输出JSON列表。 如果用户请求无法用任何技能满足输出{{skill: NONE}} # 2. 调LLM获取决策 response llm_complete(system_prompt, user_input) decisions self.parse_decision(response) # 3. 执行决策 results [] for dec in decisions: if dec[skill] NONE: return 抱歉我暂时无法处理这个请求。 skill self.registry.get(dec[skill]) if not skill: continue result skill.execute(dec[params]) results.append({skill: dec[skill], result: result}) # 4. 把结果交给LLM生成最终回复可简化 return self.format_response(results)这个循环只做了一件简单的事把技能列表丢给模型让模型输出结构化决策再执行。但就是这么简单的循环已经能支撑大多数单轮、技能数不超过20个的Agent场景。实际部署的时候我还会做两个增强。第一是加参数补全用户可能说“帮我创建明天下午三点的会议”模型输出的start_time可能是“明天下午三点”这种相对时间。我会在调用技能前调用一次LLM做自然语言参数补全把相对时间翻译成绝对时间。第二是加循环护栏技能执行失败时把错误信息回传给模型让模型重新决策或换一种参数组合最多重试两轮避免死循环。顺便说一个我踩过的大坑模型输出JSON时经常带多余的前缀或反引号比如json ... 。在parse_decision里必须做健壮性处理先把反引号和无关文本剥掉再尝试JSON解析。这个看似不起眼的问题实际占了我早期调试时间的很大比例。4. 实战中的高频问题与排查方法4.1 意图张冠李戴技能描述背锅技能系统的第一个高频问题是模型选错了技能用户说“帮我查一下下周三下午有没有空”模型却调了提醒创建没有调日历查询。很多人第一反应是模型不够聪明换更强的模型。但我排查过大量案例后发现超过一半的情况是技能描述写得不够清晰。比如日历查询技能的描述只写了“查询日历日程”模型根本不知道它应该处理“有没有空、什么时间空闲、某天有什么安排”这类请求。我把描述改成“查询用户在指定时间段内已有的日程安排判断是否有空。当用户询问某时间段是否空闲、有何安排时使用”准确率立刻上来了。也就是说排查第一步永远先看技能描述而不是急着换模型。你可以把用户原始请求和模型实际决策的日志拉出来对照技能描述逐字审查找到描述与用户语言的语义落差。4.2 参数幻觉参数Schema没约束住第二个高频问题是模型编造参数。用户说“提醒我明天早上吃药”这个吃药提醒本不需要地点模型却自作聪明填了个location参数。技能执行端没有做严格校验直接拿这个幻觉参数去查询结果就是报错或返回空数据。解决办法其实很简单技能执行前必须先过参数Schema校验。用jsonschema库对模型输出做一次强校验非必填参数传了就丢弃必填参数缺失就返回错误并让模型补传。我在BaseSkill.validate_params里就是干这个的。这个校验看起来多了一道步骤但能把大量隐蔽错误拦截在技能执行之前非常值。4.3 技能返回结果太长把上下文撑爆了第三个高频问题是技能返回的大段数据挤占了大量上下文空间。比如查询订单列表一个技能把全部历史订单都返回了模型为了生成一句“您总共有12笔订单”白白消耗了上万token的上下文。我的做法是给技能执行结果加一个裁剪层。具体规则文本类结果保留前500字列表类结果最多返回10条所有结果都要标注数据量比如“返回12条记录此处展示前10条”。裁剪后的结果既能让模型完成回答又不会挤占后续对话空间。另外超长数据宁可落库并通过一个查询技能补取也不要让Agent直接吞下全量数据。4.4 并发执行时的共享状态污染这个坑我只在并行技能编排里遇到过。两个技能同时执行其中一个把某个全局变量改了结果另一个技能读到的数据就变了。排查这类问题最快的方法是做隔离而不是找是谁改了变量。我最终的方案是技能执行时复制一份独立的上下文快照技能内部只能读写这个快照严禁访问全局状态。这样无论串行还是并行技能之间都不会互相污染。5. 从单体技能到技能生态5.1 技能市场与动态加载功能做到一定程度你会发现自己维护的技能越来越多已经不只是自己团队在用了。这时候可以考虑把技能体系升级成“技能市场”模式技能以插件形式打包Agent可以从远端仓库拉取技能包并动态注册。做法是在注册表里加一个load_remote函数从URL下载技能包的Python文件用importlib动态加载然后执行注册。这个方案技术上不复杂但需要配套签名校验和沙箱执行否则随便加载一个恶意技能脚本整个Agent环境就沦陷了。5.2 用LLM自动生成技能技能体系的另一个有意思的玩法是让LLM参与技能本身的生成。具体来说你定义好BaseSkill基类后可以给LLM提供一份技能需求描述和一个代码模板让模型自动写出技能实现。我实测过对于调用内部API、格式化数据这类标准化程度高的技能模型生成的代码只需要少量人工review就能跑起来。这样就把“实现技能”从纯人工变成了“半自动流水线”对生产力提升很明显。注意自动生成的技能一定要先过代码审查和单元测试不能直接上线。5.3 技能评估的粗粒度方法最后聊聊怎么评估一套技能系统的质量。最粗粒度、也最有效的指标是三个技能选择准确率、参数填充准确率、技能执行成功率。这三个指标可以直接从Agent的运行日志里统计出来。准备一组覆盖各技能场景的测试集跑一遍统计命中率每隔一段时间用新日志回放重测就能看到每次技能调整带来的影响。比“我觉得效果好了”靠谱得多。我个人在实际项目里的体会是把技能的选择和参数决策流水记录下来每隔一周做一次回放分析对提升整个Agent水平的帮助比调prompt模板大得多。因为技能体系的问题往往藏在具体的描述细节和边界处理上只有回放到具体case才能看到模型是怎么被误导的。再多说一个小技巧给每个技能加一个“使用次数”埋点定期统计哪些技能频繁触发、哪些技能从来没有被调用过。频繁触发的技能要考虑拆分成更细的技能从来不触发的技能说明描述与真实用户需求对不上要么重写描述要么直接下线。这种基于真实调用数据的技能治理是长期维护Agent系统必不可少的一环。agent-skills这条路一开始走会觉得只是加了一层封装越往后越会发现它是Agent架构里最值得认真设计的部分。希望你做完这套之后也有同样的感觉。
返回列表