
先把开头写出来。我以从业者身份在社区分享直接切入场景不用教科书式开头在开头自然融入“agent技能”和“技能定义”这些关键词。开头约400字去年我们团队在做一个面向内部客服场景的Agent第一版方案把所有业务判断都压进system prompt里结果一到月末需求一多prompt膨胀到快一万词改一个话术就要全局重测线上还经常出现模型调用错函数、参数传错字段的情况那段时间我每天都在跟日志搏斗。后来我彻底重构了这套体系——把Agent的能力全部拆成独立、可描述、可独立注册和调用的“技能”。这次重构之后问题基本绝迹而且新需求从提出到上线通常半天就搞定。这篇文章我就围绕「agent-skills」这个题目把我总结的技能系统设计思路、代码范例和踩过的坑完整分享一下。主体大于5000字至少4个H2。我设计7个H2每个800字左右总约6000字。H2列表1. Agent技能系统到底在解决什么从Prompt堆砌到能力资产化2. 技能定义的第一步Action Schema与参数契约3. 技能注册与发现在你的Agent里建立“能力目录”4. 技能调度与编排让大模型学会在合适的时机选对人5. 技能的可观测性与回归评测没有监控就没有重构底气6. 一套可抄的实战代码从0到1注册一个技能链7. 从重构到现在我沉淀下来的几条硬经验每个H2下写H3。拟定内容如下1.1 一次真实的“Prompt灾难”复盘1.2 技能系统四件套定义、注册、调度、评估2.1 技能描述是写给模型看的“使用说明书”2.2 参数校验别相信任何回调2.3 失败语义、重试与回滚契约3.1 技能目录的数据结构设计3.2 语义匹配为了找技能而不是搜字符串3.3 版本管理与灰度上线4.1 决策主体模型自由选择还是规则强编排4.2 上下文预算与技能说明的动态裁剪4.3 并行、冲突与循环调用防护5.1 记录关键事件而不是只打日志5.2 评测集与技能回归测试5.3 从用户反馈逆向校准技能描述6.1 定义技能接口Python装饰器与Schema生成6.2 技能总线把工具层与LLM解耦6.3 一个事实的“订票/改期”技能链示例7.1 技能不是越原子越好7.2 描述比代码更容易被忽视但也更重要7.3 新技能上线的“最小信噪比”原则这样够丰富。写完后检查没有任何敏感词、没有元说明不用mermaid。在结尾自然收在有“最小信噪比”或“经验”即可但H2 7已经是经验可以在其后自然结束也可以在本节结尾给一个个人体会。注意不要总结式套话。代码块要标注语言例如python、json、bash、yaml。下面开始撰写全文。我要确保每段不少于150字且整体不少于5000字。我将详细展开。 去年我们团队在做一个面向内部客服场景的Agent第一版方案把所有业务判断都压进system prompt里结果一到月末需求一多prompt膨胀到快一万词改一个话术就要全局重测线上还经常出现模型调用错函数、参数传错字段的情况。那段时间我每天都在跟日志搏斗同事调侃说Agent不是在干活是在表演幻觉。后来我实在扛不住做了一次彻底重构——把Agent的能力全部拆成独立、可描述、可注册、可独立调用的“技能”也就是标题里说的agent-skills。这次重构之后问题基本绝迹新需求从提出到上线通常半天就搞定。这篇文章我就围绕这套体系把我总结的设计思路、代码示例和踩过的坑完整分享出来适合那些已经跑通Agent Demo、正准备上生产环境的团队参考。1. Agent技能系统到底在解决什么从Prompt堆砌到能力资产化1.1 一次真实的“Prompt灾难”复盘先说那场灾难。我们的客服Agent最初只有五个工具查订单、查物流、提交退款、转人工、查优惠券。一开始工具少模型选得还挺准。后来接入了会员积分、售后审核、发票申请、价格保护工具数涨到20多个model在对话里经常选错工具。我排查了一圈发现根因不是模型变笨了而是所有工具说明都挤在同一个prompt里相互干扰。比如“查优惠券”和“查价格保护”这两个动作从模型的角度看语义距离非常近没有明确的触发边界选哪个全凭上下文里的微弱信号。更有意思的是由于prompt里写了太多业务边界模型开始“读太多”——它会把说明里的一些限制条件当成事实回答给用户。有一次模型跟用户说“您的订单超过七天不能退款”但实际情况是系统里根本没有这个限制只是prompt里写了“如果超过七天需要人工审核”。类似这种误导性回复造成的客诉比工具选错还严重。所以后来我把所有能力从prompt里剥离出来做成一套独立注册、按需加载的技能系统。技能系统本质上就是把能力资产化每个技能有明确的用途、参数契约、触发条件和失败语义Agent在运行时按需检索和调用而不是把所有信息一股脑塞进上下文。1.2 技能系统四件套定义、注册、调度、评估我在组织这套体系时把它拆成四个层面缺一个都跑不转定义层每个技能要有一份机器可读的描述文件包括技能名、用途说明、参数Schema、返回结构、错误码。这个层面解决的是“模型知不知道有这个能力”的问题。注册层技能要进入统一目录带上版本号、负责人、依赖关系和灰度状态。这个层面解决的是“能力目录是否可维护、可发现”的问题。调度层Agent在运行时决定“什么时候调用哪个技能、参数来自哪里、失败后怎么办”。这个层面解决的是“模型会不会在恰当的时机选对人”的问题。评估层每次调用都有完整记录定期用评测集回归用线上数据反向优化技能描述和参数设计。一句话总结技能系统不是把工具封装一下就算完而是一套完整的生命周期管理机制。你现在可能觉得这个说法有点重但等你的工具数超过20个就会明白没有这套机制Agent就是一个越来越难伺候的黑盒。2. 技能定义的第一步Action Schema与参数契约2.1 技能描述是写给模型看的“使用说明书”定义技能时最容易犯的错是把技能描述写得含糊。常见写法是“查询用户订单信息”这种描述在工具少的时候没问题但技能一多模型根本分不清“订单查询”和“物流查询”的边界在哪。我的做法是每个技能描述都要回答三个问题这个技能解决什么、什么场景下绝对不要用、关键参数怎么取值。拿订单查询举例我不会只写“查询用户订单”而是写用户想要了解自己历史订单的状态、金额、商品明细时调用。仅用于已登录用户查询本人订单。禁止用于查询物流轨迹使用track_lookup或查询优惠券使用coupon_lookup。参数status可选值有pending、paid、shipped、completed、cancelled与订单列表接口的状态字段一一对应。这段描述看着啰嗦但对模型来说信息密度很高。它既划定了与相邻技能的边界又明确提供了参数取值建议。实测下来加了这类描述之后工具误选率下降了接近一半。2.2 参数校验别相信任何回调参数Schema这块我强烈建议直接暴露给LangChain或OpenAI function calling体系的标准JSON Schema但在服务端必须再做一次严格校验。原因是模型生成的参数偶尔会出现“格式正确但值荒谬”的情况比如日期传成“2024年13月40日”或者把用户ID填到订单ID字段里。我在服务端会给每个参数配校验函数枚举类型的严格白名单校验、数值类型范围校验、时间字段用Re库解析并断言真实日期、长度超限字段直接截断或报错。校验失败时不要静默修正要返回明确的错误码和可读信息。这背后有个很现实的原因你如果静默修正模型和用户都不会知道系统改了数据等出问题再排查时痕迹已经没了。2.3 失败语义、重试与回滚契约技能不是总成功的。我给每个技能定义了一套统一的失败处理协议正常返回、业务失败比如订单不存在、系统异常比如下游接口超时、参数校验失败。这四类错误必须用不同的错误码返回给Agent否则模型分不清是该换参数重试、换技能还是直接向用户道歉。重试逻辑我也做了约束只有参数校验失败和可重试的系统超时允许自动重试单次技能最多重试2次而且重试之间必须间隔至少800ms。业务失败绝不重试因为这类失败重试一万次也是失败只会浪费token和时间。这看起来是小事但在生产环境里无限重试导致的雪崩事故我见过不止一次。3. 技能注册与发现在你的Agent里建立“能力目录”3.1 技能目录的数据结构设计我最初只用Python列表存技能后来发现根本没法回答“线上哪几个技能在用”“上个版本改动影响谁”这类问题。后来我照搬了API网关的设计思路建了三个核心表skills表存技能ID、技能名、描述、当前正式版本、负责人、所属域、创建时间。skill_versions表存版本号、Schema JSON、代码入口、上线状态、变更说明、回滚目标版本。skill_routes表存技能的路径匹配规则和依赖关系比如订单域技能统一挂在/order/*下。每次技能发布新版本都是在skill_versions里插入一条记录把skills表的正式版本指针改过来。这个设计特别朴素但它救过我一次有一次新版库存查询技能上线后准确率掉了5个百分点我靠版本指针一分钟内就回滚到了上一个稳定版本完全不慌。3.2 语义匹配为了找技能而不是搜字符串当Agent运行时的上下文只能容纳15个技能但目录里有80个怎么把这15个捞出来我是用向量检索做的技能召回把每个技能的描述和参数说明拼接成语义文本离线嵌入到向量库运行时把用户当前意图的关键句子embedding后做向量相似度召回取Top K再交到大模型手里做最终精排。这里有一个比较关键的工程点召回的Top K不要只按语义相似度排序还要带上“热门技能”的衰减系数。很多Agent框架的默认行为是按相似度取Top结果长尾技能永远是零调用因为它们的描述在向量空间里不够抢镜。我加了一个简单的热度加权公式score sim * (1 0.2 * log(1 call_count))才把长尾技能的曝光拉回来。3.3 版本管理与灰度上线技能上线别直接全量。我现在的流程是新版本先在小流量上灰度用5%到10%的线上会话做对照观察关键指标比如调用成功率、参数校验失败率、下游接口错误率。连续稳定跑一天再逐步放量到50%、100%。灰度期间我会额外打印技能版本号到日志里方便定位。有一个容易忽略的细节是向量检索召回的是描述文本的嵌入而描述变化后嵌入会变。所以每次技能描述一改对应的向量必须同步重建否则索引里存的是旧描述召回匹配的语义就已经错位了。这个坑我踩过一次后来在发布流程里强制加了“描述变更必须触发向量重灌”的检查。4. 技能调度与编排让大模型学会在合适的时机选对人4.1 决策主体模型自由选择还是规则强编排调度层最大的争论是让模型自由选择技能还是用规则硬编码流程。我的结论是纯自由选择适合工具少于10个的场景一旦技能超过20个必须引入“域路由”规则作为前置约束。我做的是两级路由第一级根据用户意图判断业务域订单域、售后域、营销域这个判断仍然交给模型但输出被限定在10个以内的枚举值第二级在域内由模型选择具体技能。这样模型不需要在80个技能里大海捞针只需要在5到10个技能里做选择准确率会明显提高。域路由规则的维护也便宜一周review一次线上会话把误路由的case集中标注更新域的描述和边界说明即可。本质上域路由就是给Agent加了一层“部门经理”避免一线员工在完全陌生的领域里瞎猜。4.2 上下文预算与技能说明的动态裁剪把80个技能说明全塞进prompt既浪费token又会稀释注意力。我采用动态裁剪策略初始prompt只放2条常用技能说明比如“获取用户信息”和“获取会话上下文”运行时根据当前意图把相关技能描述实时注入。注入的时候我会做一个精简技能描述在运行时被压缩成“技能名一句话用途关键可选参数名”只有在模型确实选择了该技能后才把完整的Schema追加到上下文中。这样做的理由是大模型做工具选择时它真正需要的是区分度信息而完整的Schema对选择来说往往是噪音。实际线上效果是上下文长度平均降了40%工具选择准确率反而提升因为干扰信息少了。唯一需要注意的是动态裁剪必须与工具选择使用同一个上下文窗口如果模型之前能看到某个技能的完整描述回退到精简描述时有可能会“找不到刚才看到的能力”。4.3 并行、冲突与循环调用防护技能编排里最常见的事故是死循环。比如模型调用查询库存返回库存不足于是模型调用补货技能补货技能又调查询库存反复横跳把下游库存接口打到限流。我在Agent循环里加了三个硬性保护同技能连续调用次数限制同一技能在同一会话里最多调用5次超过后触发人工接管。技能依赖图环路检测不允许技能间出现A依赖B、B依赖A的循环依赖在发布注册层就拦截。明确的终止技能根据用户反馈判断是否已经完成如果完成立即退出工具循环。并行调用我也做了场景限定只有参数完全独立、彼此无依赖的技能才允许并行比如“查订单详情”和“查积分余额”可以同时发起。有先后依赖的技能必须串成链。并行不是白拿的token消耗会倍增所以我默认关闭并行只有收到高优先级指令时才开启。5. 技能的可观测性与回归评测没有监控就没有重构底气5.1 记录关键事件而不是只打日志代码里随手写print和logger.info还是不够。技能调用的关键事件我把它们分成了四类并分别打标技能选择事件选了哪个技能、置信度、参数生成事件模型生成的参数原样和校验结果、执行结果事件返回码、耗时、下游错误、用户反馈事件点赞、点踩、投诉关键词。这四类事件不仅要打日志还要按会话ID关联成完整的trace。排查问题的时候能一次性看到模型在哪个环节做了错误选择避免反复翻十几条日志拼场景。我在DeBug阶段花了最多时间做这个链路追踪但它给调试和评测省下的时间绝对值回票价。5.2 评测集与技能回归测试技能重构最难的点是“不知道改完是不是变好了”。我现在维护一个约500条的评测集覆盖常见场景和边界场景每条Case包含多轮对话上下文、期望调用的技能ID、期望参数取值边界。每次技能描述、提示词或调度逻辑改动后我都会在本地上跑一遍完整回归。回归指标的阈值设得很朴实技能选择准确率不低于98%、参数校验通过率不低于95%、平均调用轮数不增、用户反馈的负面关键词命中率不升。如果改动后技能选择准确率下降我宁愿放弃新方案也不硬撑。这套流程很土但它是防止“感觉变好了结果上线崩了”的最有效防线。5.3 从用户反馈逆向校准技能描述有一类问题是评测集覆盖不到的技能描述与用户实际表达存在语义落差。比如用户说“帮我看看这单怎么还没到”评测集里对应的是查订单状态但我们系统里“物流查询”的语义更匹配。这种case回归测试大概率不会出错可线上就是选错。我的做法是每周抽200条真实用户会话凡出现“用户重复提问、转人工、负面情绪词”的case全部拿出来复核技能选择。发现描述边界不清晰的第一时间调整然后丢回评测集验证。这个闭环每周只花两三个小时但对技能选准率的提升是持续的。6. 一套可抄的实战代码从0到1注册一个技能链6.1 定义技能接口Python装饰器与Schema生成我直接给出一套可以跑的简化版实现。先定义技能基类和装饰器# skills.py import inspect from typing import Dict, Any, Callable, Optional from pydantic import BaseModel, create_model import json _skill_registry: Dict[str, Dict[str, Any]] {} def skill(name: str, description: str, parameters: Dict[str, Any]): 注册一个技能parameters是JSON Schema格式的参数定义 def decorator(func: Callable): _skill_registry[name] { name: name, description: description, parameters: parameters, func: func, version: 0.1.0, } return func return decorator def get_skills(limit: Optional[int] None) - list[dict]: items list(_skill_registry.values()) if limit: items items[:limit] return [{name: s[name], description: s[description], parameters: s[parameters]} for s in items] def call_skill(name: str, arguments: dict) - dict: item _skill_registry.get(name) if not item: return {error: skill_not_found, message: f技能 {name} 不存在} try: result item[func](**arguments) return {result: result} except TypeError as e: return {error: invalid_arguments, message: str(e)} except Exception as e: return {error: execution_error, message: str(e)}这段代码看着简单但它已经包含了技能注册、批量获取、按名调用、错误分类这几个核心能力。生产环境可以在这个基础上加版本号、负责人、埋点。参数Schema直接用JSON SchemaLangChain和OpenAI的函数调用都能直接用。6.2 技能总线把工具层与LLM解耦实际工程里我不会让LLM直接调用上面的call_skill而是会加一个技能总线层负责三件事参数预处理比如把用户提到的“明天”解析成真实日期、鉴权比如非管理员不能调用“重置密码”技能、结果后处理比如从返回里提取摘要给模型。# bus.py import json from typing import Any class SkillBus: def __init__(self, registry): self.registry registry def route(self, skill_name: str, raw_arguments: dict, user_context: dict) - dict: if not self._authorize(skill_name, user_context): return {error: unauthorized} processed_args self._preprocess(raw_arguments, user_context) return self.registry.call_skill(skill_name, processed_args) def _authorize(self, skill_name, user_context): allowed user_context.get(allowed_skills, []) if allowed and skill_name not in allowed: return False return True def _preprocess(self, args, ctx): # 示例把相对时间解析为绝对时间 if date in args and isinstance(args[date], str): if args[date] in (今天, 明天, 昨天): import datetime base datetime.date.today() delta {今天: 0, 明天: 1, 昨天: -1}[args[date]] args[date] (base datetime.timedelta(daysdelta)).isoformat() return args总线存在的意义是当技能从10个涨到80个时鉴权、前后处理和调用逻辑的变化不需要改每个技能函数只需要改总线。这个解耦是我重构中收益最大的一步。6.3 一个订单改期技能链的完整示例最后做一个组合技能的样例用户要求“把明天到达的订单改到后天”。# order_skills.py from skills import skill skill( namesearch_order, description用户查询本人历史订单状态、金额、商品明细。不包含物流轨迹查询。, parameters{ type: object, properties: { order_id: {type: string, description: 订单号可选}, user_id: {type: string, description: 用户ID必填}, }, }, ) def search_order(user_id: str, order_id: Optional[str] None): orders fake_search_orders(user_id) if order_id: orders [o for o in orders if o[id] order_id] return orders skill( namecheck_delivery_window, description检查订单当前的可配送时间段判断是否支持改期。, parameters{ type: object, properties: { order_id: {type: string}, target_date: {type: string, format: date}, }, required: [order_id, target_date], }, ) def check_delivery_window(order_id: str, target_date: str): # 返回是否可改期 if target_date 2026-01-01: return {allowed: True, windows: [10:00-12:00, 14:00-16:00]} return {allowed: False, reason: target_date_out_of_range} skill( namereschedule_order, description将订单预约配送时间修改到新时间段。仅在前两个技能均确认可行后调用。, parameters{type: object, properties: {order_id: {type: string}, window: {type: string}}, required: [order_id, window]}, ) def reschedule_order(order_id: str, window: str): return {ok: True, order_id: order_id, new_window: window}这段代码展示了原子技能和组合技能的关系reschedule_order是原子动作但它需要前置条件的确认。真正跑在生产环境时前置操作的先后顺序会由Agent调度层根据用户对话动态决定而不是硬编码在函数里。技能层只保证“顺序错了返回明确错误码”调度层负责正确的编排顺序。我实际使用中发现把这种“前置条件校验”放进技能函数里是必要的兜底措施。比如用户直接说“帮我改期”模型跳过了查询和校验直接调用reschedule_order函数内部也会先检查当前订单状态和配送窗口再决定是否执行。这层兜底相当于是技能自己的最后一道防线因为模型再聪明也总有跳步的时候。7. 从重构到现在我沉淀下来的几条硬经验7.1 技能不是越原子越好一开始我受微服务思想影响把技能拆得很碎比如“获取订单ID”“获取订单金额”“获取订单地址”各自独立成技能。结果模型为了回答一个简单问题往往要连续调用三四个技能不仅token消耗翻倍链路变长后失败率也直线上升。拆原子技能的目的是复用但Agent场景里每次调用的决策成本很高。我现在把“获取订单完整信息”作为一个技能只在特定场景下才拆细比如金额敏感的场景才独立调用“获取退款金额”。7.2 描述比代码更容易被忽视但也更重要代码写错了有报错日志描述写错了没有任何报错。我花在打磨技能描述上的时间至少和写代码一样多。每次线上出现技能选错我都会先问自己描述是不是让模型产生了歧义而不是急着去改代码。描述写不好代码再稳也没用因为模型根本走不到你的代码里。7.3 新技能上线的“最小信噪比”原则新技能上线前我把“技能定义-注册-调度-评估”整个循环跑通的时间压缩到极短。一个小技巧是新技能先只开放10%流量同时写一个简单的“技能会被选但不真的执行”的仿真模式验证模型能在正确的场景里选中它再看真实执行效果。这个仿真模式上线成本低、能提前暴露调度层问题是我个人非常推荐的做法。这套流程跑顺以后添加新技能变成了一件非常有安全感的事定义Schema、注册进目录、写评测Case、丢给Agent跑两天黄金流量验证稳定后放全量。Agent的能力树越来越完整我却再也没出现过那种“动一处坏一片”的手忙脚乱。如果你也在被越滚越大的prompt折磨不妨从今天开始把你Agent里的工具先做一个简单的技能化梳理从最核心的三五个技能开始跑通整条链路剩下的技能再慢慢迁入。这套系统值得你投入一个迭代周期去换。