最近在做智能化体项目的时候,我被一个现象反复折磨:模型本身明明很强,知识问答、代码生成都像模像样,但一旦让它去调用工具、串联多个操作,就频繁出错。要么选错工具,要么拿了结果不会用,要么上下文被历史干扰。排查到最后,我发现问题几乎都集中在同一个地方——技能(Skills)体系的搭建。这就是我这次想聊透的“agent-skills”问题。
这篇文章适合谁?如果你准备开发自己的智能体,或者已经在做Agent相关工作但总觉得调度不稳定,又或者你只是好奇为什么各家Agent产品都在强调“技能市场”“技能商店”,那这篇文章应该能帮到你。我会用自己踩过的坑,拆解Agent技能的概念、分类、实现方式,以及最容易被忽视的细节。
1. 从“会聊天”到“会做事”,为什么要把技能单独拆出来
1.1 Agent的本质转变:从语言模型到任务执行器
过去我们习惯把大模型当“聊天机器人”,给一段提示词,它吐一段文字。但Agent不一样,它必须去执行动作:查数据库、发请求、读写文件、调用计算器,甚至操作浏览器。这意味着模型的能力边界不再停留在“生成文本”,而是“做出决策并驱动真实系统”。
这个转变带来一个关键矛盾:模型本身不知道你的业务系统有哪些能力。你有一个查询订单的接口,有一个发送邮件的接口,模型从训练数据里没有见过这些接口。你需要用一种方式,把“有什么能力、什么时候用、怎么用”告诉它。而这个“告诉”的过程,其实就是技能注册与管理。
很多人一开始会简单地把工具描述塞进系统提示词里,结果提示词越来越长,Token消耗巨大,而且模型经常混淆相似功能。我做过一个实验,同时塞了五个查询类工具的描述,模型选择准确率不到六成。后来把技能独立出来,按统一格式注册,并只把当前会话可能用到的技能摘要注入上下文,准确率明显提升。这让我坚定了一个看法:Agent技能的抽象不是锦上添花,而是必需品。
1.2 Agent Skills不是插件,也不是Prompt模板
很多朋友会问:技能是不是就是插件?或者是不是等价于一段精心设计的Prompt?我的理解是,它们有关联,但不在一个层次。
插件是从宿主系统角度描述一个可安装的扩展模块,强调的是代码的打包分发;Prompt模板是从模型角度描述一段固定的指令文本。而技能是从Agent行为角度定义的“能力单元”,它必须同时包含三个部分:能力描述、入参出参定义、可执行的逻辑。简单说,技能是架在模型和系统之间的“操作手册+执行器”。
举个例子,一个“查询天气”的技能,在Prompt工程派眼里可能只是:“当你需要查询天气时,调用如下API……” 但在技能体系里,它需要定义清楚了才能被可靠使用:
- 技能名称:weather_query
- 描述:根据城市名称和时间返回天气预报,适合用来回答与天气相关的问题
- 参数:city (string), date (string,可选)
- 执行逻辑:调用天气API,解析返回值,转换成自然语言摘要
这种结构化定义的最大好处在于,模型不需要从一段自然语言里猜测调用方式,而是直接匹配结构化接口。也让开发者可以独立测试、复用、升级单个技能,而不必担心改一个工具描述把整个系统搞乱。
1.3 把能力单独拎出来管理,带来的三个直观改变
第一,可观测性变好了。以前模型调用工具的过程散落在日志里,很难追踪。技能系统统一了执行入口后,我能清楚地看到每个技能被调了几次、成功多少次、平均耗时多少、哪一步报错。这对调试Agent帮助巨大。
第二,复用性变高了。同一个技能可以在不同项目里直接搬走。我做了一个内部技能库,里面放了数据检索、PDF解析、表格生成等通用技能,新项目起来只需要配置API密钥就能用,省掉了重复开发。
第三,可控性变强了。没有技能层时,模型可以对任何函数产生幻想,甚至调用不存在的接口。技能注册表相当于一个白名单,模型只能在已注册的技能里选择。这大大减少了幻觉调用,也方便做权限管控——比如只给某个Agent注册只读技能,它就天然无法执行写操作。
2. 我整理的Agent技能清单:六类基础能力,一个都不能少
2.1 基础生存技能:工具调用、信息检索、记忆读写
如果把Agent比作一个新员工,那么基础生存技能就是它的“入职培训”。工具调用是最基本的,包括各类API、函数、命令行操作。信息检索负责在需要时从外部获取知识,比如搜索引擎、数据库查询、文档库检索。记忆读写则让Agent能在多轮对话中记住用户偏好、历史决策和临时变量。
这三个技能往往要配合使用。有一次我的Agent需要回答“上周的销售数据是多少”,它先通过信息检索技能定位数据库,然后通过工具调用技能执行SQL查询,最后把结果写入短期记忆,这样用户追问“那环比呢”的时候,它不用再查一遍。如果记忆读写技能设计得不好,Agent就会反复丢失上下文,用户体验非常割裂。
我建议在实现基础技能时,优先保证稳定性和低延迟。因为每一步基础技能都是后续复杂操作的地基。用的时候先注册最拿手的几个,不要一开始就追求全能。
2.2 高级协作技能:任务规划、子Agent委派、自我纠错
当任务变复杂,单个Agent往往需要拆解成多个步骤,甚至多Agent协动。任务规划技能负责把大目标拆成子任务,排定执行顺序。子Agent委派技能允许主Agent把特定子任务交给另一个专门Agent处理,比如一个代码审查Agent、一个数据分析Agent。自我纠错技能则让Agent在检测到结果异常时重新尝试或调整策略。
这三个技能是让Agent从“能做事”变成“会做事”的关键。但也最容易失控。我在早期实现任务规划时,发现模型经常规划出一些无法执行的步骤,比如它规划“调用订单系统”,但该技能根本不存在。后来我在规划技能里加入了自查机制:每产生一个子任务,必须核对是否已有对应技能,没有则重新规划或向用户请求补充。类似这样的约束,让成功率提升了近三成。
子Agent委派要特别注意上下文隔离。我见过一个案例,子Agent把它的内部推理过程写回了主上下文,导致主Agent被“记忆污染”,后续决策全偏了。正确的做法是在委派时只传递必要信息,子Agent返回尽量结构化、精简的结果。
2.3 领域技能怎么塞进去:知识库、规则引擎、垂类API
通用技能解决“什么都能做一点”的问题,但真正的智能体价值体现在领域深度上。比如在法律咨询场景,Agent需要引用法规条文;在金融场景,需要计算复利;在客服场景,需要查询CRM、工单系统。这些能力统称为领域技能。
领域技能的注入方式通常有三种:一是把领域知识向量化后放入知识库,让Agent通过检索获取;二是把业务规则写成规则引擎,让Agent按规则决策而不是自由发挥;三是直接对接垂类API,比如发票查验API、物流查询API。三者可以共存,但需要做好优先级管理。
我做过一个模拟项目X——面向电商的售后Agent,原本只用通用技能,用户问“退款进度”时回答模棱两可。后来接入订单查询API,并引入退换货规则引擎,Agent能直接回答“您的退款已于今天上午进入原路退回流程,预计2-3个工作日到账”。这个体验差距,是因为领域技能让Agent从“猜”变成了“查”。如果你希望自己的Agent有专业壁垒,不要吝啬在领域技能上花时间。
3. 动手实现一个最小技能系统:注册表、调度器、执行器
3.1 技能注册表设计:四个核心字段比什么都重要
技能注册表是技能系统的核心,它决定了模型能不能正确理解和使用技能。我经过多次迭代,最终沉淀出四个必须字段:name、description、parameters、executor。其他如权限、标签、依赖可选,但四件套必须有。
name必须是机器可读的短字符串,例如get_stock_price,不要带空格和特殊符号。description是给模型看的,必须说清楚这个技能做什么、什么时候用、不要什么时候用。比如写“当用户询问股票价格或行情时使用;如果用户问的是历史交易记录,不要使用”,能显著降低误调用。parameters采用JSON Schema描述,包含类型、必填、可选项、说明。executor是实际函数入口,可以是Python函数、HTTP接口调用、命令行脚本等。
我建议用代码来管理注册表,而不是手工编辑JSON,因为代码可以做单元测试和类型检查。下面是一个极简的注册表数据格式,实际项目中我会在此基础上扩展版本号、超时时间等字段。
3.2 技能调度的核心循环:模型决策、技能执行、结果回填
调度器是Agent技能的“大脑”。它要做的事情很简单:让模型根据当前用户请求和可用技能做出调用决策,然后执行对应技能,并把结果返回给模型继续生成回答。
我实现了一个最小循环,伪代码如下所示:
def agent_loop(user_query, available_skills): messages = [{"role": "system", "content": "你是智能体,请根据用户需求选择技能并调用。"}] messages.append({"role": "user", "content": user_query}) for step in range(MAX_STEPS): response = llm_chat(messages, tools=available_skills) # 1. 模型输出一个“技能调用请求” if response.has_tool_call(): skill_name = response.tool_call.name skill_args = response.tool_call.args # 2. 执行技能 result = execute_skill(skill_name, skill_args) # 3. 把结果回填给模型 messages.append(response_message_with_tool_result(result)) else: # 模型直接生成回答 return response.text raise TimeoutError("超过最大步骤")这里有几个关键点需要注意。第一,MAX_STEPS必须限制,否则模型可能陷入无限循环。我一般设为5~8步。第二,每次工具调用的结果都要加入消息列表,模型才能“看到”执行结果并继续推理。第三,execute_skill需要做超时控制和异常捕获,不能因为一个技能报错就拖垮整个Agent。
很多新手忽略的一点是:工具调用结果回填时,要尽量把结果格式化得干净。比如API返回的完整JSON有几百行,直接回填会让上下文膨胀,也会干扰模型提取关键信息。我通常会在执行器里做一步“简化”,只返回最核心的字段和摘要。
3.3 一个“查询天气”技能从零到跑通
为了让你更直观地理解,我以最常见的“查询天气”为例,演示一个完整技能怎么落地。
第一步,注册技能。我把它定义为:
{ "name": "query_weather", "description": "根据城市名称查询实时天气和未来3天预报。当用户问天气、温度、降水概率、风力时使用。", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "城市名称,例如 北京、上海"}, "days": {"type": "integer", "description": "预报天数,1-3,默认1", "default": 1} }, "required": ["city"] }, "executor": "weather_forecast" }第二步,实现执行器函数。这里我用一个伪API替代真实服务:
def weather_forecast(city: str, days: int = 1): # 实际项目里换成真实天气API请求 data = call_weather_api(city, days) # 只保留模型需要的信息 return { "city": city, "date": data["date"], "temp_high": data["max_temp"], "temp_low": data["min_temp"], "condition": data["text"], "humidity": data["humidity"], "wind": data["wind_direction"] + data["wind_scale"] }第三步,把技能列表传给模型。在模型调用接口中,我把所有技能的JSON Schema通过工具参数传入。模型收到用户提问“上海明天冷吗”时,会自行决定是否需要调用query_weather,并填入参数{"city": "上海", "days": 2}。然后系统执行技能、回填结果,模型最终输出自然语言回答。
我测试过一个有意思的场景:用户问“上海和北京哪个更适合明天跑步”。Agent连续调用两次query_weather,拿到两座城市的天气结果,再比较得出建议。这个能力是单纯的Prompt工程很难稳定实现的,因为需要多次调用和比较。技能系统天然支持这种串联调用。
3.4 技能结果缓存与反馈闭环:让系统越用越准
技能系统跑通后,有一个很容易被忽视的优化点:缓存。同一技能、同一参数往往会被反复请求,比如天气预报每分钟被用户问十次。每次都去真实API刷数据,既慢又费钱。我在系统里加了简单缓存,以“技能名+参数哈希”为key存储结果,TTL设为5~10分钟。天气预报这种时效性要求不高的服务,TTL可以设长一些;股票实时价格则不能缓存。
另一个进阶能力是反馈闭环。每次技能调用成功或失败,我都记录下来。定期统计每个技能的调用成功率、平均耗时、误调用次数。如果某个技能频繁被模型误调用(比如用户问历史天气,模型却调了实时天气),我会优化它的描述,而不是改代码。这种基于数据反馈的迭代方式,比拍脑袋改Prompt有效得多。
我举一个真实经历:某跨平台系统中,有个“生成月度报表”的技能,最初描述是“当用户需要报表时使用”,结果模型在用户问“给我看看上个月的业绩”时也调用了它,但其实应该先查数据库再生成。我把描述改成“当用户明确要求生成或导出月度报表文件时使用;如果只是查询数据,不要使用”,误调用率立刻下降。可见技能描述本身就是一个需要打磨的对象。
4. 跑通之后我没说的事:那些容易踩的坑和排查技巧
4.1 技能描述写得太抽象,模型根本不会正确调它
这是新手最容易犯的问题。很多人写技能描述,就一句话:“用于查询天气。”模型面对一堆类似描述会抓瞎。我建议描述至少包含三部分:功能范围、触发条件、排除情况。例如:“查询中国主要城市的实时天气。当用户询问当前温度、天气状况、穿衣建议时使用。如果用户询问的是历史天气数据或空气质量,不要使用,请调用对应技能。”
描述本身也是模型Prompt的一部分,所以应该用模型容易理解的自然语言,但不要写无关的废话。我见过有人把技能描述写成小作文,导致Token开销大,模型也抓不住重点。保持简洁和结构化才是正解。
如果模型仍然经常选错技能,我建议你把所有技能的名字和描述打印出来,站在模型角度读一遍,看看是否会造成歧义。为了便于排查,我会给每个描述末尾加一个“典型用法”,举一个用户问题示例,效果非常明显。类似“你说‘北京下雨了吗’的时候,用这个技能”,模型一下就懂了。
4.2 参数校验缺失,技能一调用就崩
技能执行器接收到模型传入的参数,不能假设它是完全规范的。模型生成的JSON可能缺少必填字段、类型错误、甚至多出无关字段。比如模型为用户查询天气时,可能传入“city: {'name': '北京'}”这样的对象,而不是字符串。如果执行器不做校验,直接报错,Agent后续就难以为继。
我在每个技能执行器入口前都会加一个参数校验层,用JSON Schema库进行校验,不通过则返回一个友好错误信息给模型,让模型重新生成正确的参数。这个设计思路类似于人的“接口防御”。校验信息里最好包含具体错误原因,比如“city字段必须为字符串”,这样模型才能自我修正。
另外,要对参数做边界约束。查询天气时days:如果模型传了100,你的API大概率会报错。我通常在Schema的maximum字段限制最大值,并在执行器里再截断一次。双保险,避免不可控的异常。
4.3 技能之间互相干扰,上下文被污染
多个技能共用同一个消息上下文,如果处理不当,前一个技能的中间结果可能影响后一个技能的选择。比如Agent先查了天气,又查了空气质量,如果天气返回的完整JSON出现在上下文中,模型可能误以为空气质量数据也是同一来源,造成解读混乱。
我建议每个技能执行器都返回“最小必要信息”,不要一股脑把所有字段都堆回来。同时,在每次技能调用前,可以考虑把历史中的其他技能调用结果压缩或删除,只保留用户当前相关对话和最新的技能结果。这个看似麻烦,却能大幅提升系统稳定性。
还有一种上下文污染来自子Agent。我曾把子Agent的完整推理日志回传,导致主Agent把子Agent的内部猜测当成事实。后来明确约定:子Agent只返回结论、置信度、依据摘要,禁止返回推理过程。这样上下文干净很多。
4.4 权限与安全:技能不是越多越好
技能系统让Agent能力变强,也意味着风险面扩大。如果你给Agent注册了一个“删除文件”技能,模型又恰好被恶意Prompt诱导调用它,后果不堪设想。所以权限控制必须从技能设计阶段就考虑。
我的做法是给每个技能打上权限标签,例如只读、身份认证、管理员操作。调度器在调用前会检查当前会话的权限上下文,比如普通用户不能调用管理员技能。对敏感操作,我会要求二次确认,让用户显式同意后再执行。这个在设计阶段就定好,避免上线后紧急补丁。
另外,技能数量不是越多越好。每多一个技能,模型的选择空间就会变大,误选概率也增加。我建议一个Agent默认激活的技能不超过20个,其他技能按场景动态加载。比如用户进入“数据分析”模式时,才加载图表生成、统计计算等技能。这种按需激活机制,能大大降低调度难度。
写在最后:关于Agent Skills,我个人的几个直观感受
折腾了几个月的技能系统,我最深刻的体会是:Agent Skills与其说是一个技术概念,不如说是一种工程思维。它逼着你把“模型能做什么”和“系统能做什么”清楚地区分开,然后用结构化的方式让两者对接。模型负责聪明地决策,系统负责稳定地执行,技能层就是二者之间的“适配器”。
如果你现在正被Agent调用不稳定困扰,我建议你从最简单的技能注册表开始,先接两三个核心技能跑通完整闭环,再去扩展。不要一上来就搭建几十个技能的大平台,那样只会增加排查难度。技能描述写好后,多测试几轮,根据误调用数据持续调整,效果会远超你的预期。
最后分享一个小技巧:技能执行结果返回给模型时,可以用一个统一的“自然语言摘要+结构化关键数据”格式,例如“天气:多云,24-31度,风力东北风3级,关键数据{...}”。这样模型回答会很自然,也不会被原始JSON带偏。这个小改动看起来微不足道,但在多轮对话中效果非常明显。
Agent的技能体系还有很多可玩的方向,比如技能自进化、技能组合推荐、跨Agent技能共享。这些我还在探索中,后面有结论了再分享。希望这篇内容能给你一些实实在在的参考,让你在构建自己的Agent时少走几条弯路。