1. 从"会聊天"到"会干活":Agent真正缺的是技能模块
最近大半年,AI圈子里最热的一个词就是Agent——智能体。但如果你真的动手搭过Agent应用,大概率会遇到一个非常尴尬的处境:模型对话能力很强,可一旦让它去执行一个多步骤的实际任务,比如"帮我调研一下某个行业的竞品动态,整理成表格发到我邮箱",它就开始频繁出错,不是漏步骤,就是调错工具参数,甚至中途直接"失忆"。
这个问题我想了很久,也踩了不少坑,最后发现症结往往不在模型本身,而在技能(Skills)这一层。大语言模型天生擅长的是语言理解和生成,但"会说话"不等于"会做事"。就像一个人熟读驾驶理论,上了路却不知道怎么打方向盘、看后视镜、判断车距——技能模块就是给Agent补上"实操能力"的那套训练和工具箱。
"agent-skills"这个主题,拆开看就两个词:Agent是执行者,Skills是可复用的能力单元。市面上很多Agent框架都在强调编排、规划、记忆,但真正决定Agent能不能稳定干活的,往往是它掌握了多少高质量、设计良好的技能。这篇文章我会从实践角度聊聊,搭建Agent技能系统时需要考虑的架构设计、Skill的形式与组织方式、加载机制、评测方法,以及我在真实项目中踩过的那些坑。
不管你是正在做Agent产品、研究AI自动化方案,还是只是好奇Agent背后怎么运作,这篇文章都能给你一些可落地的参考。我不打算堆概念,更多是讲我在代码和调优中趟出来的真实经验。
2. 环境准备与初始设计空间:先想清楚三个关键问题
2.1 技能系统的边界:什么算Skill,什么不该算
动手写第一行代码之前,我花了很长时间纠结一个问题:到底什么该被设计成Skill?
第一次做Agent技能模块时,我差点把所有东西都塞进技能里——数据库查询、文件读写、HTTP请求、字符串处理、图像生成……结果就是技能库越来越臃肿,Agent每次决策时要在上百个技能里做选择,性能急剧下降,而且很多技能之间职责重叠,经常出现"用A技能还是B技能都能做,但效果差异很大"的混乱局面。
后来我在一次代码重构中想通了这件事。Skill的定义应该有一个清晰的边界:它是对某个任务的完整解法,而不是底层能力的裸接口。比如"从网页中提取正文内容"是一个技能,它内部可能会调多个底层工具(抓取HTML、解析标签、清理噪音文本),但对外暴露的只有一个语义明确的入口。而"发起HTTP请求"这种则属于工具(Tool)层,不该作为技能参与Agent的决策。
这个区分听起来很简单,但对整个系统的架构影响非常大。工具层关注的是"能不能做",技能层关注的是"该怎么做才靠谱",Agent的规划层关注的是"先做哪步再做哪步"。如果这三层混在一起,你就会得到一个什么都想做、但什么都做不好的混乱系统。
2.2 技能粒度的取舍:粗粒度省事,细粒度灵活
定了边界之后,下一个问题就是粒度。
我测试过三种粒度方案。第一种是粗粒度:一个"竞品分析"技能,内部封装了搜索、抓取、摘要、对比输出整个流程。优点是Agent决策简单,精确率高;缺点是几乎无法复用——换个行业、换个分析维度,就得重写一个。
第二种是细粒度:把搜索、抓取、解析、摘要拆成独立的技能,由Agent自由组合。优点是灵活;但代价是Agent规划链路过长,尤其复杂任务时,模型很容易在组合过程中"跑偏"——它可能调了搜索,忘了抓取,或者抓完数据不知道下一步该干什么。
第三种是我最终采用的混合方案:底层保持细粒度工具,中间层设计"半成品"技能(如"抓取并清洗网页内容"),顶层再根据高频任务场景做几个粗粒度的完整Skill。Agent决策时优先匹配顶层Skill,命不中再回退到中间层组合。这个策略有点像是给Agent做了一个三级工具箱:日常用组合工具包,特殊任务才需要零件级拼接。
实测下来的感受是:粒度不是越细越好,也不是越粗越好,而是要看你的任务场景有多固定。做垂直领域的Agent,粗粒度Skill会大幅提升稳定性;做通用型Agent,则需要给足细粒度空间,同时用良好的技能描述来辅助模型做组合决策。
2.3 技能的自包含性:为什么Skill必须"带去字段"
前期我在设计技能Schema时栽过一个跟头:初期简化了技能描述,只写了技能名和一句话的说明,比如"web_search:搜索互联网信息"。结果模型经常把参数填错,或者在一个明显不适合的场景下调用了错误的技能。
后来我把每个Skill的Schema升级成自包含结构:除了名称和描述之外,还必须声明输入参数(每个参数的类型、含义、约束、示例值)、输出格式、使用场景、使用限制,以及一两个典型调用示例。这样一来,模型在Few-shot决策时可以直接参考示例,而不是靠"猜"。
有个细节值得单独提一下:技能描述里最好写上"什么时候不要用这个技能"。模型在意图识别时,正例有时候不如反例有用。我在"提取网页正文"技能里加了一句"仅适用于内容型网页,不适合登录后才能访问的站点、视频页和需要JS渲染的单页应用",错误调用率明显下降。
3. 技能模块的三种落地形式:从YAML到嵌入式脚本
3.1 声明式技能:用配置描述能力
第一种落地方式就是声明式:用YAML或JSON描述技能的名称、描述、参数、依赖的工具列表和执行策略。声明式的好处是直观、安全、易审计,因为Agent本身不包含可执行代码,它只是在"调度"已经注册好的工具。
我早期搭的技能库大部分是这种形式。一个典型的技能配置长这样:
name: fetch_and_extract_article description: 抓取网页正文内容并提取关键信息,适用于内容型网页 parameters: - name: url type: string required: true description: 目标网页链接 example: "https://example.com/article/123" - name: extract_style type: string required: false enum: ["plain", "key_points", "summary"] default: "plain" tools: - http_fetch - html_cleaner - html_parser usage_rules: - 不适用于需要登录的页面 - 不适用于PDF等非HTML内容 examples: - input: '{"url": "https://example.com/news/456", "extract_style": "summary"}' output: '{"title": "...", "summary": "...", "key_points": ["..."] }'声明式技能的优点在于模型友好。对大模型来说,一份结构清晰的声明式技能描述,远比一段乱糟糟的代码更易懂。而且你可以把整个技能库放进Agent的上下文,让模型在系统提示词里"看到"自己有哪些能力可用。
缺点是表达力有限。如果技能逻辑比较复杂(比如需要多轮条件判断、循环处理、分支爬取),声明式配置就变得很啰嗦,甚至难以表达。这时候就需要引入代码式技能。
3.2 脚本式技能:把代码变成Agent的"肌肉记忆"
脚本式技能在Agent社区里现在非常流行。简单说,就是让Agent技能库中的每个技能对应一个可执行脚本,脚本可以用Python、JavaScript、Shell等语言编写,Agent在执行任务时动态生成脚本内容,由解释器运行并返回结果。
这里要区分一下:脚本式技能有两种流派,一种是Agent动态生成脚本,一种是预置脚本+参数填充。前者更灵活,但不可控;后者更稳定,适合生产环境。
我实际用下来,比较可靠的是后者——预置脚本模板,Agent只负责填参数。比如我做过一个"批量下载网页截图"的技能,底层预置了一个Playwright脚本,Agent只需要提供URL列表和输出目录,剩下的流程全部由脚本保证。这样做的好处是确定性高,Agent不会写出稀奇古怪的代码,而且脚本本身可以经过充分测试,比Agent现场写出来的代码可靠得多。
# 预置的截图脚本示例:填入待抓取的URL列表即可运行 import asyncio from playwright.async_api import async_playwright from pathlib import Path async def snapshot(url: str, output_dir: str, viewport: tuple = (1280, 800)) -> dict: async with async_playwright() as p: browser = await p.chromium.launch() page = await browser.new_page(viewport={'width': viewport[0], 'height': viewport[1]}) try: await page.goto(url, wait_until='networkidle', timeout=15000) filename = url.replace('https://', '').replace('http://', '').replace('/', '_')[:80] filepath = Path(output_dir) / f"{filename}.png" await page.screenshot(path=str(filepath)) return {"status": "ok", "path": str(filepath)} except Exception as e: return {"status": "error", "message": str(e)} finally: await browser.close()用这种模式的时候要注意一点:脚本模板和参数Schema要严格匹配,Agent只通过参数入口与脚本交互,不能允许它自由改脚本。我见过一些项目让Agent直接输出整个Python代码块,再交给执行引擎跑,气死人的是Agent偶尔会在代码里写出import os; os.system('rm -rf ...')这种鬼东西。为了安全和稳定,宁可损失一点灵活性,也不要开放自由代码执行。
3.3 原生工具Markup:直接嵌入模型推理管线的轻量技能
第三种形式比较"轻"——直接在模型的提示词里,用特定格式嵌入技能定义。比如很多框架支持自定义函数调用(Function Calling)格式,把技能定义成函数的JSON Schema,模型通过结构化输出选择调用哪一个。
这种方式本质上把技能系统和模型推理管线合并了。优点是没有额外服务,延迟低,实现快;缺点是技能数量一旦增多,上下文被撑爆,而且模型在长技能列表里做精确选择的能力会下降。
我现在的做法是:基础原子操作走Function Calling(比如搜索、查数据库、发通知),复杂业务流程走脚本式/声明式技能库,两者通过一个调度器串联。前期开发用Function Calling快速验证,跑顺了再沉淀成技能库。
4. 技能的心智加载机制:让Agent在关键时刻想起正确的Skill
4.1 全量注入 vs 按需检索
技能库越来越大之后,一个尖锐的问题浮出来:Agent怎么知道该用哪个技能?
方案一很粗暴:把所有技能的Schema全量塞进Prompt。技能少时没问题,超过20个之后效果急剧下降,一是Token消耗大,二是模型注意力分散,傻傻分不清相似技能。
方案二是按需检索:Agent在执行任务前,先从技能库里检索出和当前任务相关的Top-K个技能,动态注入Prompt。这个思路有点像RAG(检索增强生成),我试过用向量检索来做,也试过用模型分类来做,各有优劣:
| 方案 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| 全量注入 | 实现简单,信息完整 | 技能多时效果差、成本高 | 技能库小于15个 |
| 向量检索Top-K | 可扩展性强,支持大技能库 | 需要维护Embedding,检索质量依赖描述质量 | 通用型Agent |
| 模型分类路由 | 精确度高,可解释性好 | 需要额外调用模型,延迟高 | 技能意图差异明显 |
| 规则+关键词匹配 | 零成本,快 | 覆盖有限,维护困难 | 快速原型验证 |
我自己最后用的是混合路由:先用轻量级规则层(关键词、正则)快速命中一批候选技能,再用向量检索补充,最后让模型从候选里做最终决策。这比单一方案稳得多。
4.2 技能优先级与冲突消解
多个技能都能完成同一个任务时,Agent该选谁?这个问题我会在技能定义阶段就处理掉。
做法是给每个技能加一个priority字段,以及可选标签category、reliability、latency。当Agent在两个技能间徘徊时,系统自主决策优先选择reliability高的;如果两个技能能力相似但延迟差异大,则有延迟敏感偏好机制的系统会倾向低延迟方案。这个逻辑可以做成一个小的Routing Policy引擎,而不是全交给模型"自由发挥"。
还有一个靠谱的办法是给相似技能建立显式关联,在技能的描述里直接写明"本技能与XX技能的区别是什么,什么场景选哪个"。模型看到这种提示后,选择错误的概率会显著降低。
4.3 技能上下文窗口管理
每次动态加载技能时,还有一道坎就是上下文管理。技能描述太长会挤占对话历史空间,太短又导致模型理解不足。
我在技能模板里总结了理想技能描述的五个要素:做什么(一句话)、参数含义(详细)、适用场景(具体)、典型示例(一对以上)、边界与限制(反例)。为了让模型在上下文中少看一点字,我为每个技能还准备了一份"短描述"——用于索引阶段,等技能被选中后再注入完整版。
# 技能索引上下文仅包含简要描述,选中后注入完整Schema SKILL_INDEX = [ {"id": "fetch_and_extract_article", "summary": "抓取网页正文并提取关键信息,适用于内容型网页"}, {"id": "analyze_competitor", "summary": "竞品信息搜集与对比分析,输出结构化报告"}, ]这套机制实际跑起来后,调用的准确率有了明显提升,上下文Token消耗也降了很多。如果你也在做Agent,我建议在技能检索这一步多花功夫,这里优化好了,整体的Cost和质量都会受益。
5. 从Demo到稳定:技能评测与踩坑实录
5.1 技能评测的"黄金三角":成功率、调用准确率、资源消耗
技能系统跑起来之后,我很快发现一个问题:Demo里的效果跟上线后的稳定性之间,差着一整套评测体系。
我搭了三个核心指标来监控技能质量:
第一个是任务成功率,即技能在目标场景里真正完成任务的比例。这个不能只在大模型评测集上测,因为真实环境里的网页结构、接口变化、数据格式千差万别。
第二个是调用准确率,即Agent在决策时选对技能的比例。这个指标和质量相关:如果调用准确率低,你再怎么优化技能内部实现都没用,问题出在决策层。
第三个是资源消耗,包括每次技能调用的Token成本、API调用次数、响应延迟。有时候一个技能看着好用,但它内部要多绕两轮模型推理,成本翻倍,就得考虑优化逻辑或降级为纯代码实现。
我给技能库建了一套简单的CI流程:每次新增或修改技能,自动跑一遍固定场景集,计算三个指标,低于阈值就告警。这在团队合作时尤其重要——一个人改了技能A的配置,可能导致另一个人场景下技能B的调用出错,没有CI根本发现不了。
5.2 我看过的最隐蔽的几个坑
踩坑是难免的,但这几个坑值得你提前绕开。
第一个坑:技能描述与真实行为不一致。早期我有个"保存到数据库"的技能,描述里写着"支持自动建表",但脚本里其实只支持写入已存在的表。Agent信了描述,传了一个新表名进去,脚本报错,Agent却不知道错在哪。从那以后我形成了规矩:技能描述里的每一句话,都要有测试用例覆盖。
第二个坑:Agent在技能链中"过度确认"。某些Agent框架为了减少错误,会在每步行动前询问用户确认。看起来是安全设计,但实际跑多步任务时,Agent会频繁停下来问"我要执行XX,可以吗?",体验非常割裂。解决办法是在技能Schema里增加requires_confirmation字段,只对高风险动作(如删除数据、对外发送消息)开启确认。
第三个坑:错误信息没给模型看。技能执行失败时,返回给模型的内容如果只是一个冷冰冰的{"status": "error"},模型根本不知道下一步该怎么办。我后来统一了错误返回格式:除了状态码,还要带上错误类型、可能原因、建议下一步动作。模型拿到这个信息后,很多失败能自愈。
# 错误返回示例:给模型足够的上下文进行下一步决策 { "status": "error", "error_type": "net_timeout", "message": "请求目标站点超时(超过15秒)", "suggestion": "建议稍后重试,或改用其他备选数据源", "retryable": True }第四个坑:技能参数里藏着不可见的隐含依赖。有一个技能内部依赖某个环境变量,但参数Schema里完全没体现。换了一台部署机器后,技能全面罢工,排查了半天才发现是配置文件没同步。后来我要求所有技能脚本必须是自包含的,所有外部依赖在配置阶段显式声明,不允许出现"隐式依赖环境变量"这种设计。
5.3 规模化的技能组织策略
技能超过50个之后,管理方式也要变,否则就是灾难。我试过几种组织方式,最后留下来的是三级分类法:
底层是原子技能(不可再拆的单一操作),中间层是复合技能(组合多个原子技能形成的业务能力),顶层的场景包是按任务场景绑定的技能集合(比如"竞品调研场景包"包含搜索、抓取、对比、报告生成等技能)。
这种组织方式的好处是,Agent在做任务规划时更容易命中场景包,而不是在50个技能里大海捞针。同时,新增一个业务场景时,只需要组合已有的原子技能和复合技能,不需要从零开发。
每次新增技能时,我还会顺手问自己三个问题:这个技能和其他技能有没有重叠?它能否拆成更基础的技能?它的命名和描述是否足够清晰?回答完这三个问题,技能库的熵值基本能维持在可控状态。
6. 技能系统未来的演进方向:从静态技能库到自我进化
我发现技能系统做完、跑稳之后,真正的瓶颈不是Agent模型本身,而是技能的迭代速度。建立高效的技能编写、调试、验证与发布流程,才是竞争力的来源。
我现在在探索的方向有三个:一是技能效果的自动回测,让技能库每次更新后能自动回归历史场景;二是技能发现,从用户的交互日志里找出高频但未固化的操作序列,提示开发者把它们沉淀为新技能;三是半自动技能生成,模型根据任务描述生成技能初稿,再由人工审核修改后进入技能库。
最后分享一个实际的体会:做Agent技能系统,最忌讳一开始就追求大而全的技能库。更好的做法是从一个高频场景出发,做出三五个精心打磨的技能,跑通验证之后,再逐步扩展。技能系统是典型的"少而精胜过杂而滥"——毕竟Agent最终能不能被别人信服,靠的是眼下这一次任务做得好不好。而把每一次小任务都做好,正是技能模块存在的意义。