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 Schemaskill.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__, prefixpackage.__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 体系有帮助。