1. 从“能聊天”到“能干活”:Agent 技能体系为什么是分水岭
做大模型应用开发这几年,我观察到一个很有意思的现象:很多人拿着市面上最强的模型 API,兴致勃勃搭了一个 Agent,结果发现它除了会聊天、会写一些空洞的总结之外,根本干不了实事。问题出在哪?不在于模型聪明不聪明,而在于你根本没给 Agent 配一套像样的技能。
“agent-skills”这个概念,看起来只是“Agent 技能”的英文组合,但它背后是一个完整的方法论:把大模型从“什么都懂一点但什么都做不精”的通才,训练成“知道自己擅长什么、该调用什么工具、该怎么一步步把事情做完”的专才。简单说,技能是一个 Agent 完成特定任务的最小可用能力单元——它可能是调用一个搜索接口,可能是执行一段 Python 代码,也可能是按照某个 SOP 完成一次客户回访。
我见过不少团队在搭 Agent 时踩同一个坑:一上来就追求“大而全”,希望一个 Agent 能搞定所有业务问题。结果越做越臃肿,prompt 写得比业务代码还长,模型在复杂指令面前频频失灵,排错排到怀疑人生。而把 Agent 拆成“技能”来设计和维护,恰恰是为了解决这个局面——每个技能职责单一、接口清晰、可独立测试,Agent 只是扮演调度者的角色,根据用户意图选择并组合技能。
这篇文章,我就从自己在实际项目中搭建和落地 agent-skills 体系的完整经历出发,聊聊技能到底是什么、如何设计、怎么实现、以及最容易踩的坑。如果你正打算把 Agent 从“玩具”推向“生产力工具”,这篇文章应该能帮你少走不少弯路。
2. 解构“技能”:Agent 技能设计的三层拆分
2.1 技能不是 prompt,也不是 API 封装
先说一个最常见的认知误区:很多人以为给 Agent 写一段详细 prompt 就算给它配了一个技能,或者把某个第三方 API 包一层函数就算技能。这两种做法都有问题。
纯靠 prompt 驱动的问题在于:Prompt 是非结构化的自然语言,模型每次执行时的“理解”都可能漂移。你今天写的规则,明天换个措辞问它,它可能就理解偏了。而且 prompt 很难被测试,你没法在 Agent 之外单独验证“这段 prompt 是不是稳定可靠”。
单纯封装 API 的问题在于:API 只是技能的执行后端,技能还应该包含触发条件、输入输出约定、错误处理策略、边界条件等完整逻辑。比如“查天气”这个 API,遇到城市名为空怎么办?遇到网络超时怎么办?返回的数据怎么格式化成用户能看懂的答案?这些都不是简单的 API 封装能解决的。
所以我在实践中更倾向于把技能定义为一个可独立运行、可独立评估、具备清晰输入输出契约的模块化能力。它包含三个部分:
- 技能描述:用结构化文本告诉 Agent 这个技能是干什么的、什么时候该用、什么时候不该用;
- 执行逻辑:真正干活的代码或工具调用链;
- 校验机制:对输入参数做合法性校验,对执行结果做质量检查。
2.2 从任务需求反推技能粒度
设计技能时,最让人纠结的往往是粒度问题:技能拆细了,Agent 要来回调度很多次,效率低;技能拆粗了,每个技能内部要处理一堆分支,又变得臃肿难维护。
我的经验是从具体业务任务出发去反推粒度。举个例子,之前我做一个智能客服 Agent,业务方提的需求是“能帮用户查订单、退换货、开发票”。如果直接按这三个需求设计三个技能,看起来合理,但一落地就发现问题:“查订单”这个动作后面还跟着“查物流”“查售后状态”“预估送达时间”等细分场景,用户在对话中随时会切换意图。
后来我把技能粒度调整成“订单信息查询”和“售后服务”两个中等粒度的技能,每个技能内部维护了一个小型工具链,可以调物流接口、售后接口、商品接口。这样 Agent 调度层面的压力小了很多,用户表达的需求也能被更快匹配到正确的技能上。原则就是:技能粒度以“一次完整业务意图”为准,而不是以“底层操作”为准。
2.3 技能描述怎么写才不会被模型“视而不见”
技能描述是给模型看的“说明书”,很多人在这一步特别敷衍,写一句“查询订单信息”就完事了。结果就是模型根本不知道什么时候该调用这个技能,或者明明该调用 A 技能的时候调用了 B 技能。
我总结了一套写技能描述的实用套路。首先要有明确的“触发条件”,告诉模型什么情况下使用这个技能;其次要有“不适用场景”,告诉模型什么情况下千万别用;最后要提供“示例输入”,让模型对参数格式有直观的认识。
比如我写的“查快递”技能描述是这样的:
当用户询问包裹的物流进度、配送状态、快递到达时间等与已下单商品配送相关的问题时,使用此技能。不要将此技能用于查询订单金额、修改收货地址或申请退款等操作,这些由其他技能负责。示例输入:{"tracking_number": "SF1234567890"}。
这样写下来,模型对技能的调用准确率明显提升。不要心疼描述的长度,模型理解技能的准确度,往往和描述的质量成正相关。
3. 从零搭建一个技能框架:设计原则与目录结构
3.1 为什么我选择“技能即文件”的设计
在技术选型上,我见过不少团队用数据库表存技能配置,用管理后台去维护技能内容。大公司这么做没毛病,因为它们有专门的平台团队。但对大多数项目来说,我更推荐一个轻量级方案:把每个技能实现成一个目录下的独立文件。
这样做的理由很现实:第一,技能文件可以直接扔进 Git 仓库做版本管理,每次改动都有记录,出问题可以随时回滚;第二,新技能可以靠文件目录结构自动发现,不需要手工在多个地方注册;第三,Code Review 的流程可以直接覆盖技能修改,质量把控天然融入开发流程。
我搭的技能目录大概是这样的:
skills/ ├── __init__.py ├── registry.py ├── base.py ├── order_query/ │ ├── __init__.py │ ├── skill.py │ ├── schema.json │ └── description.md ├── after_sales/ │ ├── __init__.py │ ├── skill.py │ ├── schema.json │ └── description.md └── common/ ├── api_client.py └── validators.py每个技能目录固定包含三个文件:description.md给模型看的技能说明,schema.json定义输入输出的 JSON Schema,skill.py是实际的执行逻辑。这个结构的好处是——新成员入职后看到这个目录结构,五分钟就能明白这套体系是干嘛的,不需要翻一堆文档。
3.2 技能基类与统一输入输出契约
技能的执行逻辑必须有一个统一的接口约束,否则 Agent 调度器就没法用一套代码管理五花八门的技能实现。我定义了一个很薄的基类:
from abc import ABC, abstractmethod from typing import Any, Dict class BaseSkill(ABC): """所有技能必须继承的基类""" skill_name: str = "" description: str = "" input_schema: Dict[str, Any] = {} output_schema: Dict[str, Any] = {} def __init__(self, context: Dict[str, Any] | None = None): self.context = context or {} @abstractmethod def execute(self, params: Dict[str, Any]) -> Dict[str, Any]: """执行技能,必须返回结构化的结果字典""" raise NotImplementedError def validate_params(self, params: Dict[str, Any]) -> None: """校验输入参数,不符合 schema 的直接抛异常""" for field, rule in self.input_schema.get("properties", {}).items(): if field in rule.get("required", []) and field not in params: raise ValueError(f"缺少必填参数: {field}") @staticmethod def success(data: Any) -> Dict[str, Any]: return {"status": "success", "data": data} @staticmethod def fail(message: str) -> Dict[str, Any]: return {"status": "fail", "message": message}核心约束就一条:execute方法必须返回一个包含status字段的字典。调度器只看status判断这次技能调用成没成功,具体业务数据放在data里。这个设计特别土,但特别稳。复杂的状态码定义、嵌套错误体系,在 Agent 调度场景里只会让模型更困惑。
3.3 技能注册机制:让 Agent 自动发现可用能力
有了技能文件后,还需要一个注册中心来收集所有技能,并且生成 Agent 可读的技能清单。这里我踩过一个坑:一开始是每个技能写好后手动在列表里加一行配置,后来技能多了,经常漏掉注册,调半天发现 Agent 根本不认识新技能。
后来改成自动扫描注册:
import importlib import inspect import pkgutil from typing import Dict, List, Type from skills.base import BaseSkill class SkillRegistry: _instance = None _skills: Dict[str, BaseSkill] = {} def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance def auto_discover(self, package_name: str) -> None: """自动扫描包内所有技能模块并注册""" package = importlib.import_module(package_name) for finder, module_name, is_pkg in pkgutil.walk_packages( package.__path__, prefix=package.__name__ + "." ): if is_pkg: continue module = importlib.import_module(module_name) for _, obj in inspect.getmembers(module, inspect.isclass): if ( issubclass(obj, BaseSkill) and obj is not BaseSkill and getattr(obj, "skill_name", "") ): self._skills[obj.skill_name] = obj def list_skills(self) -> List[Dict[str, str]]: """生成 Agent 可读的技能清单""" return [ { "skill_name": skill_cls.skill_name, "description": skill_cls.description, } for skill_cls in self._skills.values() ]这个注册器做好之后,新增一个技能只需要在skills/下新建目录,实现好skill.py,重启服务就会被自动发现。接入方的开发成本降到了最低。
4. Agent 与技能之间的调度逻辑:让模型学会“知人善用”
4.1 技能调用路由:不是所有问题都要走模型
很多 Agent 框架喜欢把所有请求都先甩给大模型做 intent classification,再让模型决定调用什么工具。这种方式灵活,但有两个麻烦:一是每次请求都要花一次模型调用的时间,延迟上不去;二是模型在不确定性面前经常“想太多”,明明有现成的技能路径,它非要自己发挥一段。
我在设计路由时做了一个简单但有效的优化:先走一层基于规则的“快速匹配层”,命中确定性规则就直接执行对应技能,没命中才交给模型做意图识别。比如用户消息里出现“订单号”“物流单号”这样明确的关键词,并且格式匹配某类编号规则,就直接触发订单查询技能。这样我实测把 40% 的请求拦在了模型调用之前,平均响应时间从 3.2 秒降到了 1.1 秒。
快速匹配层用简单的关键词匹配就能实现,不需要上复杂的模型:
KEYWORD_ROUTES = [ { "keywords": ["物流", "快递", "运单", "tracking"], "skill": "express_query", }, { "keywords": ["退款", "退货", "换货", "售后"], "skill": "after_sales", }, { "keywords": ["发票", "开票", "报销"], "skill": "invoice_service", }, ] def quick_route(user_message: str) -> str | None: for rule in KEYWORD_ROUTES: for keyword in rule["keywords"]: if keyword in user_message: return rule["skill"] return None不要小看这个土办法,它的价值在于给 Agent 建立了一条最直接的路径,模型不需要重复劳动,系统也不需要为所有请求付出不必要的模型 token 成本。
4.2 多技能联调:做任务编排时给模型“轨道”而非“方向盘”
当用户请求比较复杂,需要调用多个技能才能完成时,问题就变成了“技能编排”。比如用户说“我前天买的手机到今天还没发货,帮我查一下订单如果超过承诺时间就申请退款”。这个请求至少包含:订单查询、发货时效判断、退款申请三个阶段。
我的做法是把编排逻辑写成固定的“技能链”配置,把模型的能力限制在“选择哪条链”而不是“现场编排链”。以这个场景为例,我在系统里预置了order_refund_pipeline这条链路:
PIPELINES = { "order_refund_pipeline": { "name": "订单退款处理链", "description": "处理订单未按承诺时间发货申请退款的场景", "steps": [ {"skill": "order_query", "require_result": True, "retry": 2}, {"skill": "ship_promise_check", "require_result": True}, {"skill": "refund_apply", "require_result": True}, ], "fallback": "human_service", } }模型在 Agent 调度层的任务只是判断“用户是否有退货意图”以及“是否符合执行退款链路的前置条件”,一旦判断通过,后续步骤完全由调度器按预设链路驱动。这样比让模型自己一步步想“下一步该调什么”稳得多。
为什么这么做?因为多技能连环调用一旦中间出现错误,模型自己兜底的能力很不稳定——它可能编造一个不存在的状态来“解释”错误,导致用户收到错误反馈。预设链路配合每步校验,错误能被及时拦截并走兜底路径。
4.3 调度上下文管理:别让 Agent“失忆”
Agent 在多技能调度之间有一个常被忽略的问题:上下文丢失。模型在处理“帮我查订单 A,如果有问题再帮我查订单 B”这类请求时,每一步技能执行完,都需要把关键结果塞回上下文里,否则下一步决策就成了无源之水。
我在实践中使用了一个很笨但有效的方法:维护一个短期的“执行状态对象”,把关键中间结果都存进去,每次调用模型前先把状态对象转成文本片段拼进 prompt。比如执行完订单查询后,状态对象里记录“当前订单已超过承诺发货时间”,后续模型判断是否执行退款申请时,就直接基于这段文本做决策,而不是依赖模型靠记忆还原前一个步骤的结果。
这本质上是一种“外部化记忆”的思路。Agent 的长对话能力再强,也不如把关键信息落盘在结构里踏实。尤其在技能返回数据量很大的时候,全量塞回上下文既费 token 又干扰模型判断,我会让状态对象只保留和后续决策相关的摘要字段。
5. 技能评估与测试:如何证明你的技能真的“好用”
5.1 单元测试覆盖技能核心路径
技能是 Agent 系统里离业务最近的一层代码,如果技能本身有 bug,模型再聪明也没用。所以技能库必须像普通业务代码一样有单元测试覆盖。我的习惯是每个技能至少覆盖三个用例:正常输入、边界输入、异常输入。
以订单查询为例,正常输入是一个合法订单号;边界输入是订单号格式合法但订单不存在;异常输入是订单号为空或格式完全错误。每个用例都要断言执行结果是否符合预期状态码。这是我给某个技能写的测试代码片段:
import pytest from skills.order_query.skill import OrderQuerySkill def test_query_existing_order(): skill = OrderQuerySkill(context={"user_id": "test_user_001"}) result = skill.execute({"order_id": "SO20250101001"}) assert result["status"] == "success" assert result["data"]["order_status"] in ["pending", "shipped", "completed", "cancelled"] def test_query_non_existent_order(): skill = OrderQuerySkill(context={"user_id": "test_user_001"}) result = skill.execute({"order_id": "SO99999999999"}) assert result["status"] == "fail" assert "订单不存在" in result["message"] def test_query_empty_order_id(): skill = OrderQuerySkill(context={"user_id": "test_user_001"}) with pytest.raises(ValueError): skill.execute({"order_id": ""})这套测试跑完后,技能的可靠性就有了基本保障。不要觉得单元测试是服务端开发的事,Agent 技能同样需要。很多 Agent 项目最后死在“线上表现不稳定”上,根源恰恰是最底层的技能不可靠。
5.2 基于真实场景的回归语料集
单元测试只能保证代码逻辑没错,但没法保证模型在真实对话中能正确调用技能。为此,我另外维护了一个“场景回归语料集”,里面收集了大量真实用户对话记录,标注了每条对话应该触发哪个技能、期望什么结果。每次修改技能描述或调度逻辑后,我都会用这个语料集做一轮回归验证。
回归验证的流程是:把语料里的用户消息输入 Agent,然后比对 Agent 最终选择的技能和期望技能的匹配率。我要求匹配率不能低于 95%,低于这个线说明改动影响了模型对技能的理解。
这里分享一个血泪经验:有一回我把“发票”相关描述改得更详细了,结果发现模型开始把所有退款请求都路由到了发票技能,回归匹配率直接掉了 25 个百分点。原因是我把发票场景描述里的“报销”“财务”这些关键词写得太靠前,模型产生了误判。这个教训让我意识到:技能描述里涉及场景限制的部分,语气要坚决,不能模棱两可。
5.3 技能性能观测:延迟、成功率和 token 消耗
上线之后,每个技能的质量必须可持续观测。我通常给每个技能埋四个指标:
- 调用次数:判断技能的真实使用频率,太低的技能要考虑是不是描述写得有问题;
- 成功率:执行过程中产出正常状态的比例,低于 90% 就需要排查;
- 平均延迟:包括模型调用和实际工具执行的耗时,用于优化路由策略;
- Token 消耗:技能描述被拼接进 prompt 后占用的 token 量,太长了要考虑简化。
我见过一个典型的“僵尸技能”问题:某个技能上线两个月,调用次数是 0。排查了一圈发现不是没人遇到这个需求,而是技能描述里全是技术术语,模型根本没把用户的问题和这个技能关联起来。后来我用大白话重写了技能描述,加入具体触发示例,次周调用次数直接从 0 涨到了 400 多次。
6. 踩坑实录:技能体系落地中的三个经典麻烦
6.1 Agent 死活不调用某个技能?
这是最常见的问题。我遇到的情景是:技能写得清清楚楚,单测也过了,可模型就是不调用它,反而用自己的“常识”去回答业务问题。
排查步骤我建议从三个方向入手。第一,确认技能描述里的触发条件是否足够具体,别写“当用户需要帮助时”这种废话;第二,确认技能清单有没有被真正加载进 Agent 的上下文,一些框架有技能数量上限,超出部分会被截断;第三,确认其他技能描述里有没有“抢活”的表述,比如两个技能都写了“处理用户关于订单的问题”,模型就会纠结。
加一个“不适用场景”到技能描述里往往能缓解这类冲突。技能之间的边界描述得越清楚,模型的选择就越果断。
6.2 技能执行结果不稳定:同一输入有时成功有时失败
如果同一技能每次执行的结果还不一样,优先怀疑远程依赖。比如技能调用了第三方接口,对方接口超时、限流、数据格式变动都会导致结果飘忽。定位这类问题,最好在技能层加一个标准化的“执行审计日志”,把每次调用的输入、输出、耗时、错误信息都记下来,排查时一眼就能看出是哪个外部环节出了岔子。
我常用的做法是在技能基类的execute方法外层包一层日志装饰器:
import functools import logging import time logger = logging.getLogger("skill_executor") def skill_trace(func): @functools.wraps(func) def wrapper(self, params: dict): start = time.time() try: result = func(self, params) logger.info( "skill=%s params=%s status=%s cost=%.2fms", self.skill_name, params, result.get("status"), (time.time() - start) * 1000, ) return result except Exception as exc: logger.error( "skill=%s params=%s error=%s cost=%.2fms", self.skill_name, params, repr(exc), (time.time() - start) * 1000, ) raise return wrapper有了这个日志,任何技能层面的不规律波动,都能从时间线上看清出在哪一步。排查的效率比对着模型输出猜来猜去高好几倍。
6.3 上下文爆炸:多技能执行后 prompt 塞不下
多技能串行执行时,每一步的工具返回数据都会占用上下文空间,几轮下来 prompt 长度可能翻好几倍,既烧钱又影响模型响应速度。我的处理原则是“结构化截断 + 摘要保留”:原始大段数据只保留在技能内部做逻辑判断,返回给 Agent 上下文的结果只包含关键字段组成的摘要。
比如订单查询技能内部拿到完整的订单大对象,包括商品明细、优惠明细、地址、发票信息等十几个字段,但返回给调度器的只有三个字段:订单状态、承诺发货时间、当前时间是否超时。三个字段够模型做后续决策了,其他的信息在技能内部写日志里记录,需要时再查。
这个优化做完,我的 Agent 平均每轮调用的 token 消耗下降了接近一半,响应速度也快了 30% 以上。上下文从“拿来即用”变成“按需提取”,多技能场景下是非常重要的设计意识。
7. 从单技能到技能生态:我的一些扩展思考
技能体系的好处在于它是可积累的。每接一个新业务,本质上是往技能库里加一个新技能,而不是重新搭一个 Agent。当技能数量超过二十个的时候,我就开始考虑技能之间的互相组合和沉淀。比如“查订单”和“算运费”组合能衍生出“订单结算预估”这个更上层的技能能力。
对大部分团队来说,与其追求一个通用人工智能式的全能 Agent 形态,不如踏踏实实先建一个质量过硬的技能库。技能库越扎实,Agent 的上限就越高。我自己在实际操作中的体会是:把一个复杂 Agent 拆成清晰的技能集合后,系统的稳定性、可维护性和迭代速度都会上一个台阶,这个回报远超当初拆分时付出的那一点额外功夫。希望这篇文章里写到的思路、代码和踩坑经验,对你搭建自己的 agent-skills 体系有帮助。