上个月我把一个内部助手项目彻底重构了一遍,重构后的核心模块我给它取名叫agent-skills。起因其实很朴素:模型能力再强,如果不会调用正确的工具、不知道在合适的时机执行合适的动作,那它就只是一个"很会聊天的API",离真正能干活还差得很远。这次重构没有改模型、也没有换框架,只是把Agent的行为从"写死流程"改成了"声明技能 + 动态编排",效果提升却非常明显。这篇文章就围绕agent-skills这套技能体系的构建思路展开,从为什么需要技能、怎么设计技能、如何注册调度,到实际踩过的坑和一条完整案例,希望对正在做Agent应用的同行有点参考价值。
1. 为什么Agent不能只会聊天:技能体系解决的真实问题
1.1 纯粹对话的边界在哪里
很多人一开始做Agent,就是拿一个大模型API接上对话窗口,用户问什么答什么。这没毛病,但如果你的目标是让Agent真正替你完成工作——比如处理一份会议记录、提取待办、创建日程、再发一封跟进邮件——那纯对话远远不够。
我最初踩过的坑是这样的:让模型直接输出一段包含所有操作步骤的JSON,然后我自己写代码去解析、执行。第一版看起来能跑,但实际用起来非常难受。
一方面,模型一旦在上下文里迷失,输出的JSON结构就会千奇百怪:字段名对不上、嵌套层级漂移、甚至把代码注释当成值塞进去。另一方面,每次新增一个操作,我都得去改解析逻辑和分支判断,系统的复杂度和脆弱性一起涨。最要命的是,模型根本不理解"哪些操作有副作用、哪些操作必须按顺序来、哪些操作是不能随便做的"。它只会按照提示词的惯性往下编。
你会发现,问题不在于模型不够聪明,而在于你根本没有给它一套可以稳定依托的"行为骨架"。这时候技能体系就该上场了。
1.2 技能是什么:给模型一把称手的工具
技能(Skill)本质上是一种标准化的行动单元:它有明确的名字、有清晰的职责边界、有结构化的输入输出契约、有可执行的后端逻辑。你可以把它理解成给模型准备的一把把专用工具。模型本身不负责工具的制造,只负责在正确的场景下选出最合适的工具并调用它。
这里有个很关键的区别:普通函数调用(Function Calling)解决的是"模型能不能调用一个函数"的问题,而技能体系解决的是"模型如何在一堆可用行为中做选择、编排和容错"的问题。前者是单点能力,后者是系统架构。
我做agent-skills时定义了一个核心公式:
可靠Agent = 稳定的技能集合 + 清晰的技能描述 + 合理的调度机制
模型负责"决策",技能层负责"执行",两边通过一套严密定义的接口通信。决策可以偶尔犯糊涂,但只要执行层像瑞士军刀一样每把刀都界限分明,Agent整体就不会崩。
1.3 判断你是否需要技能体系的三个信号
不是所有场景都需要马上引入技能体系。我自己判断的标准是下面三条,命中任意两条就说明该动手了:
- 重复动作开始堆积:你发现Agent经常要做同类型操作,比如"查天气、查日历、发通知",每次都要在提示词里重新描述一遍,甚至复制粘贴上一段逻辑。
- 条件分支变多:你的流程开始出现"如果会议冲突就换个时间,如果邮件没有收件人就提示用户补全"这种分支,纯写死在提示词里很快会失控。
- 需要跨系统协作:Agent要同时操作内部API、数据库、第三方服务,而每个系统都有自己独立的鉴权方式和错误码。
当初项目进行到第三周,这三条全部命中,我才下定决心把技能层独立出来。如果你已经是"提示词越来越长、效果越来越飘"的状态,大概率也到了该重构的节点。
2. 从工具调用到技能编排:设计边界与拆解原则
2.1 function calling 与技能体系的关键差异
先明确一个容易被混淆的点:很多人觉得"我已经在调工具函数了,不就是Agent技能吗?"还真不是。
Function calling 通常是平台原生支持的机制,你给模型声明一批函数,模型判断该调用哪个、生成参数,然后把结果返回给模型继续推理。这个机制本身没有问题,它是技能体系的底层基础。但我把它当作"传输层",而不是"业务层"。
真正的技能体系要在这之上叠加四样东西:
- 语义化描述:不只是函数签名,还要告诉模型这个技能是干什么的、什么时候用、什么时候忌用、返回什么格式。
- 版本管理:技能会迭代,旧会话里模型可能引用了旧逻辑,没有版本意识就会让历史数据错乱。
- 组合编排:技能之间可以互相调用,形成一个稍大粒度的复合技能,而不是每个任务都要从最小单元开始串。
- 可观测性:每次技能调用的入参、出参、耗时、错误都要能追踪,否则上线后只能靠用户反馈来发现问题。
拿开会这件事举例。底层函数可能是get_user_timezone、parse_datetime、create_calendar_event,它们都算工具。而"创建一场会议并自动发送邀请给参会人"是一个技能,它内部调用多个底层函数,且任何一步失败都有明确的回退策略。模型面对的不是一堆散装函数,而是一个意图明确的技能按钮。
2.2 一个技能应该多大:拆解粒度的经验法则
技能拆解的粒度是agent-skills设计过程中讨论最激烈的问题。粒度太粗,技能就成了一个写死的模板,适用面窄、复用性差;粒度太细,模型每次要做的事太多,编排成本高、容易出错。
我最终采用的经验法则是三句话:
输入是否清晰?输出是否可判定?副作用是否单一?
如果这三个问题的答案都是"是",这个技能的大小基本合适。比如"读取指定日期的日程列表",输入是日期范围,输出是日程数组,副作用为零,这就是一个非常健康的原子技能。再比如"分析会议记录并生成待办事项",输入是文本或会议ID,输出是结构化的待办列表,副作用是写入任务系统,虽然动作变多了,但边界仍然清晰,可以作为复合技能存在。
如果某个技能的输入需要用一大段自然语言描述才能说清楚,那就说明它承担的职责太多了,要拆。如果输出结果需要用户在界面上二次确认才能判定对错,说明它应该拆成"生成草稿"和"正式提交"两段。
我在项目里有一个实际例子:最初设计了一个叫handle_meeting_request的大技能,能完成从解析邮件到创建会议再发通知的全流程。看起来很方便,但模型经常因为一句话里没有明确的会议时间而卡死。后来拆成了extract_meeting_info、check_availability、create_event、notify_attendees四个技能,模型只需要先调用第一个技能提取信息,如果失败就直接问用户,不再硬撑着往下走。整条链路的成功率直接上了一个台阶。
2.3 技能分层:原子技能、复合技能与轻量工作流
随着技能数量增多,我建立了一个简单的分层模型,避免所有技能都平铺在模型面前:
| 层级 | 定义 | 示例 | 模型感知方式 |
|---|---|---|---|
| 原子技能 | 单次无状态操作,副作用最小 | 查询日程、读取文件、翻译文本 | 直接暴露给模型 |
| 复合技能 | 按固定逻辑组合多个原子技能 | 会议纪要转待办、批量发送周报 | 以一个整体技能暴露,内部执行路由 |
| 轻量工作流 | 需要用户交互确认或跨实体协调 | 订会议室+发邀请+建日程 | 通常由调度器统一编排,模型只触发入口 |
这里我想强调一个容易忽视的点:模型不一定要看到所有底层技能。有些内部步骤是固定逻辑,不需要模型参与决策,直接把复合技能暴露给模型就好。否则模型面对几十个技能,选择困难不说,token消耗也难以接受。
分层设计之后的另一个好处是方便做权限控制。原子技能可以细粒度地配置"谁能调用",复合技能则可以对用户呈现统一入口。内部执行时即使某一步失败,也只需要在复合技能内部做重试或降级,模型无感。
3. 技能定义三件套:描述、Schema与执行器
3.1 一份可以直接抄的技能注册模板
在agent-skills里,每个技能通过一份结构化的定义文件注册,我用的是 YAML + JSON Schema 的组合。下面这份模板是我实际在项目里用的简化版,可以直接抄走:
name: create_todo_from_text description: > 从用户提供的自由文本中提取待办事项,并创建到默认任务列表。 适合用户说"帮我记一下明天下午给客户回电话"这类场景。 如果文本中没有明确的待办动作,不要调用此技能,应先向用户确认。 version: 1.2.0 input_schema: type: object properties: text: type: string description: 包含待办信息的原始文本,必填。 priority: type: string enum: [high, medium, low] default: medium description: 待办优先级,用户未指定时使用默认值。 due_date: type: string format: date description: 可选截止日期,格式YYYY-MM-DD。 required: [text] execute: runtime: python entry: skills/create_todo/run.py timeout_seconds: 10 allowed_actions: - todo.create这份定义看起来简单,但每一条字段都对应着一个真实的工程问题。name要稳定,不要频繁改名,因为历史会话和日志里会大量引用技能名;description是模型做选择的第一依据,写得不好后面全是坑;input_schema负责约束参数,防止模型自由发挥;execute则是真正的后端入口,需要声明超时和权限边界。
我强烈建议把版本号写进定义里。技能迭代是常态,没有版本号,你根本无法判断一次日志里的调用到底跑的是旧逻辑还是新逻辑。
3.2 description是第一优先级:怎么写模型才听得懂
这是整个技能体系里最容易被低估、也最容易翻车的地方。
很多人在写描述时喜欢用抽象的功能介绍,比如"该技能用于管理任务"。然后模型根本不知道什么时候该用它。我自己的经验是,description 至少要回答下面四个问题:
- 这个技能做什么:一句话说清动作对象和结果。
- 什么场景下触发:给出典型的用户表达样例,帮助模型做语义匹配。
- 什么场景下不要触发:明确负面条件,防止误用。
- 有没有关键前置条件:比如"必须先确认用户登录"或"必须先查询到具体日程ID"。
正例和反例的差别是很明显的:
# 反例 description: 创建一个待办事项。 # 正例 description: > 从用户提供的自由文本中提取待办事项,并创建到默认任务列表。 适合用户说"帮我记一下明天下午给客户回电话"这类场景。 如果文本中没有明确的待办动作,不要调用此技能,应先向用户确认。正例里写清楚了触发条件、边界和前置要求,模型对它的理解会准确得多。你可以把 description 理解为给模型看的"使用说明",而不是给开发者看的"功能注释"。两者视角完全不同。
3.3 input_schema与执行器的常见设计陷阱
参数Schema这块,踩过的坑比description还多。首先要避免的误区是"把所有可能的参数都加上",因为它会诱导模型填一堆似是而非的值。更推荐的做法是只暴露必需参数和少量高价值可选参数。
我习惯在Schema的每个字段描述里写明取值约束。比如priority字段直接给枚举,而不是让模型自己发明"urgent"、"high-level"这种措辞。日期字段尽量用format: date约束,避免模型输出"下周二"这种无法直接入库的文本。
执行器设计同样有讲究。timeout_seconds是硬性要求,技能调用必须有时限,否则一个第三方服务超时就会拖死整个Agent循环。allowed_actions是一个轻量权限声明,它表示该技能只能操作哪些系统资源,内部执行时可以做二次校验,防止某个技能因为代码bug越权操作不属于它的数据。
还有一个细节:执行器最好用独立进程或线程池来跑,不要在模型推理的主线程里直接执行。一方面是为了超时控制更干净,另一方面是避免技能里的异常把整个推理服务打崩。
4. 注册与调度:Agent如何知道该用哪个技能
4.1 三种技能注册方式与适用场景
技能设计好之后,接下来要解决的问题是:模型如何知道你有这些技能。
目前我实际验证过的方式有三种,各有适用场景:
- 全量注入系统提示词:把所有技能定义直接放到系统提示词里。优点是实现简单,模型对技能都有感知;缺点是token占用大,技能一多就塞不下。
- 工具列表注册(Function Calling):在支持工具调用的模型平台上,把技能以函数形式注册,由模型在推理时决定调用。优点是平台帮你处理了参数格式化和结果返回;缺点是一次注册数量仍然受上下文窗口限制。
- 技能仓库检索:把所有技能描述放到一个向量索引里,收到用户请求后先做语义检索,只把最相关的几个技能注入上下文。优点是技能数量可以做到几百上千;缺点是增加了检索一跳延迟,且需要配套的召回评估。
我的建议是:如果技能少于20个,用第二种方式最省心;如果技能持续膨胀,就尽早切换到第三种。第一种方式我建议只在原型验证阶段用,生产环境几乎撑不住。
agent-skills项目采用的就是第二种加第三种混合:常用高优先级技能常驻工具列表,长尾技能走向量检索。这样既保证了核心体验,又控制了上下文成本。
4.2 上下文预算:当技能清单塞不下时怎么办
技能描述是要占token的,而且通常每个技能要占几百到上千token。我给你算一笔账:假设你平均每个技能描述约300 token,注册50个技能就是15000 token,再加上用户消息、历史对话、系统指令,很多模型的上下文窗口直接就吃掉一大半。
所以上下文预算必须提前规划。我常用的两个手段是:
- 描述压缩:把description压缩到模型能理解的最短形式,删除冗余修饰词,保留触发场景和关键边界。但注意不要为了省token把"什么时候不要用"这句删掉,那部分是防误用的关键。
- 分级暴露:把技能分成"常驻热技能"和"按需加载的冷技能"。热技能永远在上下文中,比如
create_todo_from_text这种高频入口;冷技能只在用户意图命中关键词时才被检索载入。
分级的好处很明显:热技能数量控制在10个以内,token占用完全可控;冷技能再多也不影响主上下文。
4.3 调度机制的两种路线:LLM直接选择与语义召回路由
调度指的是"当前这个用户请求,到底应该触发哪个技能"。两条主流路线我都试过,简单说说结论。
路线一:LLM直接选择。模型看到技能列表,根据用户消息自行决定调用哪个。好处是利用了模型的语义理解能力,复杂意图下表现很好;坏处是技能一多、描述一长,模型容易选错,而且每次选择都要消耗推理token,成本上升明显。
路线二:语义召回路由。先用文本嵌入模型把用户请求向量化,在技能向量库里做相似度检索,拿到TopK候选再交给LLM做最终判断。好处是能应对海量技能,坏处是前期要维护向量索引,而且检索本身有失败率。
我在实战中验证下来,最稳的不是二选一,而是两者结合:先用轻量分类规则做粗筛,把明显不相关的技能先排除;再让模型在候选集里做精选择。粗筛阶段可以用关键词匹配,也可以用向量召回,看你的技能描述质量来决定。只要候选技能不超过5个,模型的正确率会非常可观。
还有一个细节:每次调度都要记录命中情况。哪条用户消息命中了哪个技能、最终是否正确,这些日志是后续优化技能description和检索权重的唯一依据。没有日志做反馈,调度优化就是闭着眼开枪。
5. 避坑实录:我在Agent技能项目里踩过的五个坑
5.1 坑一:description写得又长又抽象,模型宁可自己瞎猜也不调用
有一版我把一个技能描述写成了产品文档风格,动辄一百多个字,还把各种边角案例都塞进去。结果模型遇到对应场景时,反而判定"这个技能太复杂,应该不是干这个用的",干脆直接自己编了一个答案返回给用户。
排查链路是这样的:我看调度日志,发现这个技能近一周的调用数为零。于是手动构造了几十条本应命中的用户消息,逐一测试,发现模型要么不调用,要么调用了一个名字相近但功能完全不对的其他技能。
问题出在描述的主干信息被大量噪音淹没了。修法也很直接:把description开头第一句改成"该技能做什么 + 一个典型触发例句",其他补充信息全部挪到后面的"边界说明"段落里。改完再跑测试,命中率立刻回到了正常水平。
这里我总结出的规律是:模型对description的理解是"头部加权"的,前一两句话决定了大方向。重要信息一定要放在最前面,千万别把核心用途埋在最后。
5.2 坑二:参数Schema过于"宽容",幻觉参数打穿下游接口
另一个非常隐蔽的坑来自input_schema的设计。最初我为了让技能"好用",把很多可选参数放得很宽,比如允许模型自由填写location、duration、remind_type等字段,没有给枚举约束。
结果在一次跨时区会议创建场景里,模型把duration填成了"2 hours",把remind_type填成了"pop-up 15 min before",当然后端接口解析直接失败。而模型并不知道失败,它只是把这次调用的"成功"当成了既定事实,继续向下游发通知,差点造成了乌龙事件。
后来我把所有自由文本参数都收紧成枚举或者明确格式,比如距离改为整数分钟、提醒方式改为app、email、sms三选一。更重要的是加了一道服务端校验:参数不合法时返回标准错误码,并把错误信息回传给模型,让它自主修正后重试。这套兜底机制上线后,幻觉参数导致的事故基本清零。
5.3 坑三:技能互相调用缺乏护栏,递归直接把额度烧光
技能之间是可以互相调用的,这是组合能力的来源,但也会带来一个危险的副作用:循环调用。
我遇到过一次真实事故。A技能负责"处理用户请求并调用B",B技能里又设计了"如果信息不完整则回退调用A"。正常情况下这没什么,但当一次请求同时触发了两者且双方都判定"信息不完整"时,两个技能就互相踢皮球,无限递归。那个下午的API调用额度在十分钟内被烧掉了一整天预算的大半。
修复方案分三层:第一层是每个技能调用都记录调用链深度,超过三层直接终止;第二层是同一个用户请求的技能调用总数设上限,比如最多5次;第三层是给每个技能声明"依赖方向",禁止循环依赖出现,在注册阶段就做静态检查。现在我在技能注册表里始终维护一张调用关系图,每次新增技能都会跑一遍环路检测。
5.4 坑四:执行器没有任何观测,技能报错无从排查
技能上线初期我只关心"模型有没有调对技能",完全没管执行器内部的日志和监控。结果有一次多个用户反馈某个技能偶尔失败,但模型又显示调用成功——相当诡异。
顺着问题排查才发现,执行器内部依赖了一个第三方服务,该服务有15%的请求会超时。而超时发生后,执行器仅仅抛出一个通用异常,异常信息没有记录入参和堆栈,也没有上报到监控平台。模型收到异常后只会把它包装成"暂时无法完成"之类的客套话,用户看到的就是"不稳定的技能"。
从那以后我要求每个执行器必须输出三条结构日志:开始(含入参摘要)、结束(含出参摘要或错误摘要)、耗时。所有技能调用都会同步到一个中心日志系统,按技能名、用户ID、时间范围都可以检索。这一步做完,很多原本要猜的问题,现在翻日志就能直接定位。
5.5 坑五:技能更新没有版本意识,旧会话全部错乱
有一段时间为了让技能更快适应反馈,我直接在原技能定义上反复修改,既没有升级版本号,也没有保留旧版本镜像。结果一个会话里模型先是看到了新版技能的行为描述,执行器却还是旧版代码;或者反过来,执行器已经换新了,模型上下文里仍然描述着旧版参数。
最典型的问题出现在参数格式变更时。旧版参数要求date_str,新版改成了date_ts,但同一份上下文里可能同时混着两种格式。模型一旦在推理中搞混,参数校验就疯狂报错。
现在我坚持的规范是三件事:任何技能变更必须升版本号;训练或推理配置里显式锁定技能版本;会话级上下文记录它快照过的技能版本,恢复会话时使用同一版本。这些规则很基础,但真的能避免大部分"技能混乱"问题。
6. 案例实测:从会议记录到日程创建的一条技能链路
6.1 场景设定与技能拆分
前面讲了不少理论,最后我拿一个真实场景完整走一遍,你就能看到agent-skills是怎么运转的。
场景:用户给Agent发了一段会议录音转写文本,其中提到"下周二下午三点跟李总过方案,后续要拉上王工一起评审,周五前把会议纪要和相关材料发到项目群"。
过去我的老做法是让模型直接输出一个包含"解析、创建日程、生成纪要、发起通知"所有动作的JSON,然后写代码去拆解执行。现在改成技能编排,我把这个场景拆成了四个技能:
extract_actionable_items:从文本中提取结构化动作项,比如会议、任务、截止日期。create_calendar_event:创建一个日历事件,需要标题、时间、参与人。generate_meeting_minutes:生成会议纪要草稿。send_project_notification:向指定群组发送通知消息。
这四个技能里,第一个是解析入口,后面三个分别对应不同的下游系统。模型不需要一次性生成所有参数,而是一次只聚焦一个动作。
6.2 注册清单与系统提示词示例
我在Agent的初始上下文里这样注入技能清单(简化版):
可用技能列表: 1. extract_actionable_items 用途:从会议转写或对话文本中提取动作项。 典型触发:用户提供一段文本并要求整理待办/会议/日程。 参数:text(必填,原始文本)。 注意:若无明确动作项,回复用户确认,不要强行提取。 2. create_calendar_event 用途:向日历系统创建新日程。 典型触发:用户明确要求安排会议/预约时间。 参数:title(必填)、start_time(必填,ISO8601)、 attendees(可选,邮箱列表)、duration_minutes(可选,整数)。 注意:时间格式必须为ISO8601,无法确定开会时间时应先要求用户补全。 3. generate_meeting_minutes 用途:基于会议转写生成结构化纪要。 典型触发:用户要求生成纪要/会议记录。 参数:transcript(必填,会议文本)、include_actions(可选,布尔值)。 4. send_project_notification 用途:向指定群组发送通知。 典型触发:用户要求发到项目群/通知成员。 参数:channel_id(必填)、message(必填,纯文本)。我还会在系统提示词里加一句编排原则:"处理用户请求时,可以依次调用多个技能,每个技能的参数应基于它的执行结果来生成,不要一次性猜测所有参数。"这句话非常关键,它暗示模型要分步执行,而不是一口气编造整条链路。
6.3 实际调用过程与输出验证
我录制了一次完整的调用序列,展示模型是如何一步步完成任务的:
- 第一步:模型接收到原始文本,判断需要提取动作项,调用
extract_actionable_items,参数为整段转写文本。 - 提取结果显示三条动作项:下周二15:00跟李总过方案;邀请王工评审;周五前发会议纪要和材料到项目群。
- 第二步:模型针对第一条动作项调用
create_calendar_event,这里它需要确定一个准确时间。文本里"下周二"没有绝对日期,模型必须结合当前日期推算,如果无法确定,就触发参数校验失败并请求用户确认。这一步非常考验描述里"时间格式必须为ISO8601"这一约束,避免它随便填一个模糊值。 - 第三步:模型调用
generate_meeting_minutes,生成纪要草稿。此时它并没有把纪要和发送动作绑死。 - 第四步:模型调用
send_project_notification,把纪要和材料发到项目群。这里参数里的message是模型根据纪要内容生成的。
整条链路的关键验证点有两个。第一,每一步的输出都作为下一步的输入依据,而不是凭空生成参数;第二,最后发送前我留了一个人工确认开关,默认可以配置成"发送前需用户确认",避免全自动误发。
从执行日志看,这条链路的成功率高了不少,而且每次失败都能清楚看到是在哪一步、因为参数还是权限还是超时。排查效率和稳定性一起上来了。
如果你现在手上正好有Agent项目,我建议你也动手把最常用的三五个动作整理成技能,按描述、Schema、执行器三件套落一版,再加一行调度日志。先不要追求技能数量多,先追求每一条调用链路都是清晰、可观、可回滚的。技能体系这套东西,越早理清边界,后面的迭代就越省力。