1. 从一个实际痛点说起:为什么"skills"值得被当成项目来做
去年年中,某团队在一次日常巡检中发现,整个服务集群里散落着大量功能重复但写法各异的处理逻辑。有的是在对话入口里硬编码了一整套文本清洗规则,有的在批处理任务里复制粘贴了同样的数据校验函数,还有的在不同的接口响应逻辑里各自实现了一遍"从用户输入中提取结构化字段"的流程。表面上看功能都能跑,可一旦要调整某个通用规则,就需要满仓库地找人肉改动,漏掉任何一个角落,线上就会出现不一致的行为。
这个问题最终催生了一个名为"skills"的项目。它做的事情本身并不复杂:把那些反复出现、跨场景复用的能力,从具体的业务流程中剥离出来,封装成独立、可描述、可测试、可组合的单元。每个单元就是一个skill,有自己的输入输出约定,有自己的校验逻辑,也有明确的执行边界。
我当时看到这套方案的第一反应是:这不就是把公共函数提取出来吗?但真正深入之后才发现,"skills"这个词在 Agent 应用、自动化流程、甚至团队协作场景里,代表的是一整套设计方法论,远不止"抽个公共类"这么简单。
这篇内容就围绕我在实际搭建和落地这套 skills 机制时积累的经验展开,包括最开始的动机、拆解思路、最小实现、踩过的坑,以及对规模化设计的一些个人看法。如果你也在为重复逻辑蔓延、提示词失控或功能模块难以复用而头疼,这里的思路可以直接拿来参考,不用重走一遍弯路。文章里的代码和配置都以一种通用的 Agent 开发框架为例,具体字段和路径你可以按自己的技术栈调整。
2. 先拆清楚:一个 skill 到底由哪些部分构成
在动手写任何代码之前,我认为最值得花时间的是把"一个 skill 到底是什么"这个问题想明白。如果连边界都划不清楚,后面会越写越乱。
2.1 并非所有逻辑都适合做成 skill
先说结论:不是所有代码片段都值得封成 skill。判断标准我一直用三条:
- 是否跨场景复用?如果一段逻辑只在一个业务分支里出现一次,没必要特殊化。
- 是否有明确的输入输出边界?输入是文本、结构化数据还是文件?输出是判断结果、提取字段还是生成内容?边界模糊的封装会成为"万能函数",后面无穷无尽地改。
- 是否具备相对稳定的行为?经常因为业务需求变来变去的逻辑,过早封装反而增加维护负担。
我自己见过最典型的反面案例,是把"解析用户意图"这种过于宽泛的行为定义成 skill。结果就是不同业务方往里塞不同的规则,不到一个月,这个技能变成了一堆条件分支的集合,没人敢再动它。所以,一个合格的 skill 应该像是一个有明确岗位职责的同事:知道自己负责什么、不负责什么,接到任务后按标准流程给出结果。
2.2 skill 的五层结构
抛开具体框架的术语差异,我认为一个完整的 skill 需要包含五层结构:
| 层次 | 作用 | 具体内容 | 类比 |
|---|---|---|---|
| 触发描述 | 说明技能什么时候被调用 | 一段自然语言描述,列出适用场景和典型使用方式 | 岗位说明书里的职责范围 |
| 入参约定 | 定义需要哪些输入 | 字段名、类型、是否必填、示例值 | 表单的填写规范 |
| 执行逻辑 | 核心处理过程 | 规则、算法、调用外部服务的步骤 | 岗位的工作流程 |
| 输出规范 | 定义返回格式 | 字段结构、成功/失败状态码 | 交付物的验收标准 |
| 校验与回退 | 保证结果可靠 | 入参校验、结果自检、失败时的兜底策略 | 质量检查和应急预案 |
这五层缺一不可。实际项目中我看到很多"半成品 skill"——有描述和入参,但输出规范含糊,导致调用方拿到结果之后还要自己猜结构;也有的完全没有校验与回退,一旦输入不符合预期,整个流程直接崩溃。
2.3 设计上的一个关键判断:技能的原子性
封装粒度是这五层里最难把握的部分,我称之为技能的原子性。拿"处理用户订单"来说,可以拆成"订单信息提取""地址标准化""金额计算""配送时间预估"等多个原子操作。但如果把其中每个操作都做成独立 skill,调用关系会变得非常复杂;反过来,如果全部揉在一起,又失去了复用的意义。
我的经验是,判断粒度是否合适,看两点:第一,这个技能是否可以在超过一个场景中独立使用?比如"地址标准化",无论订单、快递还是活动报名,只要涉及地址,都能复用,那就值得单独拆出来。第二,如果这个技能失败了,业务方能否给出清晰的降级方案?有明确失败边界的技能,粒度通常比较合理。
还有一点容易被忽略:技能之间应该尽量保持独立,不要互相依赖。比如"A 技能内部调用 B 技能"这种写法,看起来省了事,实际上让两个技能的版本演进互相牵制。我会在后面的踩坑部分展开讲这个问题。
3. 从零手写一个最小可用 skill:一步一步跑通
理论聊多了容易飘,下面直接进入实现。我以一个非常贴近日常需求的场景为例:做一个从用户输入里提取日历事件信息的 skill。输入可能是各种口吻的句子,比如"下周三下午三点和数据分析团队开会,地点在三层会议室",目标是输出结构化的日期、时间、参与者和地点。
3.1 第一步:定义技能描述文件
第一步是写技能描述文件,相当于这个技能的"简历"。在通用框架里,这个文件通常是 JSON 或 YAML 格式,核心是告诉上层系统"我是谁、什么时候用我"。
{ "name": "event_extractor", "description": "从自由文本中提取日历事件的结构化信息,包括日期、时间、参与者和地点。适合用户提供自然语言日程描述的场景。", "parameters": { "input": "原始文本,string,必填", "output": "结构化事件对象,object" } }这里的 description 字段极其重要。它决定了上层系统什么时候会把这个 skill 推给大模型使用。写得太宽泛,比如"处理用户输入",会导致大量无关请求被路由过来;写得太狭窄,比如只提"会议室预订",则在用户用口语说"帮我约个会"时无法命中。
我的建议是,description 至少包含三部分:技能的核心功能、适合的触发场景、明确的触发边界。第三部分常被忽略,但它能有效减少误调用。上面的示例里,"适合用户提供自然语言日程描述的场景"就是在划边界。
3.2 第二步:实现核心执行逻辑
技能描述文件只是给调度系统看的,真正干活的部分是执行逻辑。对于提取类的 skill,一个稳定可靠的做法是采用"模板约束 + 大模型抽取 + 规则校验"的组合方案。
以 Python 为例,核心函数大概是下面这个形态:
def run_event_extractor(raw_text: str, model_client) -> dict: # 1. 基础清洗:去掉多余空白 cleaned_text = " ".join(raw_text.split()) # 2. 调用大模型按固定 schema 抽取 prompt = build_extract_prompt(cleaned_text) result_json = model_client.complete(prompt, response_format="json") # 3. 规则校验与兜底 validated = validate_event_result(result_json) if not validated["ok"]: validated = rule_based_fallback(cleaned_text) return validated这里最值得关注的是第三步——校验与兜底。大模型抽取的结果不会每次都符合预期,比如日期格式可能不规范、参与者字段可能为空。这些情况必须在 skill 内部消化掉,不能期待上层调用方来处理。
我的兜底逻辑是:先尝试用正则表达式从原始文本中直接匹配日期时间和地点,匹配不到的字段再回退到模型抽取结果。双保险之后,这个技能的稳定性会明显好于仅依赖大模型。
3.3 第三步:定义输出规范和失败模式
输出规范需要比你预想的更严格。我吃过亏之后,现在每个 skill 的输出都会定义成如下结构:
{ "status": "success", "data": { "date": "2025-07-02", "time": "15:00", "participants": ["数据分析团队"], "location": "三层会议室" }, "confidence": 0.93 }status 字段标明整体执行结果,confidence 表示置信度。失败模式下,data 为空,status 为 failed,同时附一个 reason 字段说明失败原因。
这套结构的好处是,无论上层是人还是另一个自动化流程,拿到结果后都能做统一的后续处理:status 是 success 才往下走,否则进入人工确认流程。confidence 还可以用来做阈值判断,比如低于 0.8 的结果强制要求用户确认。
3.4 第四步:本地测试用例设计
实现完之后,不要急着接入业务。我会先准备一组测试用例,按难易程度分三层:
- 基础层:规范句式,比如"明天上午9点开会",验证正常路径。
- 干扰层:带有无关信息的长句,比如"顺便说一下,上次那个文档还没写完,对了,周五下午两点和产品团队对需求",验证技能不被干扰信息带偏。
- 边界层:缺失关键信息的句子,比如"下周找个时间聊聊",验证失败时的兜底表现。
这三层用例分别对应准确性、鲁棒性和边界处理。哪怕没有自动化测试框架,手动跑一遍也能发现大部分问题。我见过不少团队省掉这一步,直接上生产,结果技能在遇到一点点口语化表达时就开始胡说八道。
4. 跑通之后才是开始:我在实际接入中踩过的三个坑
最小可用的 event_extractor skill 跑通之后,我把这套模式复制到了团队内部的其他场景,随之而来的是各种意想不到的问题。这里挑三个最有代表性的,完整还原当时排查的思路和最终的解决方案。
4.1 踩坑一:技能之间的隐式依赖,让故障排查变成灾难
第一个坑发生在引入第二个 skill——"会议室预定助手"。当时为了图省事,我在这个新 skill 的执行逻辑里直接调用了 event_extractor,心想既然已经有现成的提取能力,直接复用不是很好吗?
前期确实很顺,直到某次线上反馈:会议提取结果出现日期错乱。排查到后面才发现,问题不在 event_extractor 本身,而是会议室预定助手的某个中间步骤改了系统时间格式,影响了全局配置,而 event_extractor 恰好依赖了这个全局状态。
这个问题的本质,是技能之间存在了非显式的运行时依赖。表面上是两个独立 skill,实际运行时会互相影响。我们的修复方案是:所有技能必须通过受控接口进行通信,禁止直接共享全局上下文;如果需要复用提取能力,就把 event_extractor 作为一项独立服务调用,而不是在代码层直接混在一起。
这件事给我的教训是:任何封装的边界都不只是代码层面的,运行时的隔离同样关键。把技能当作独立的部署单元来对待,虽然在初期会增加一些成本,但后期排查问题的效率会高出很多。
4.2 踩坑二:入参校验不彻底,导致技能被循环调用
第二个坑发生在入参校验上。某个统计类 skill 接收一个时间范围参数,我在参数定义里写了"start_date 和 end_date 必填",但没有校验两者的先后关系。结果有一天,一个上游流程把 start_date 和 end_date 传反了,skill 内部计算时长时得到负数,直接抛异常。
异常发生时,上层调度系统以为是临时故障,自动重试了一次,结果又传了同样的参数,再次失败,如此循环了好几次,白白消耗了计算资源。
排查链路是这样的:先看日志发现同一时间参数被多次执行,再看参数来源,发现上游是一个日期选择组件,在某些情况下返回的顺序不符合预期。修复分两步,第一是在 skill 的入参校验阶段增加 start_date 不能晚于 end_date 的检查,不符合就直接返回带明确 reason 的失败结果;第二是在调度系统里增加重试次数的上限和退避策略,避免无意义的重复调用。
这件事让我意识到,skill 的入参边界越是宽容,对自身的伤害越大。严格校验不是给用户添麻烦,而是保护自己的第一道防线。
这里涉及到一个通用问题:校验规则应该在技能内部做还是交给上游?我的答案很明确——技能内部必须做自己的守门人。分散在多个调用方的校验,一定会有漏洞。你无法确保所有调用方都按你的预期准备参数,作为被调用的那一方,最低限度要做到"输入不合法,输出要清晰"。
4.3 踩坑三:技能描述写得太模糊,大模型频繁误触发
第三个坑比较隐蔽,问题出在技能描述文件上,也就是我最开始强调需要仔细打磨的触发描述。当时团队里有个同事在写技能描述时用到了一句"处理各种信息提取任务"。
这句看起来没什么问题,但在实际运行中,系统把大量本不该路由到这个技能上的请求都送了过来。比如用户问"帮我查一下明天的天气",这个技能也被触发,然后因为输入格式不符返回失败,用户看到的就是各种失败提示,体验非常糟糕。
根因在于大模型是通过语义相似度来做路由的,过于宽泛的描述会让它认为什么都能处理。我还专门去看了当时的日志,发现一个很有意思的现象:触发成功率的波动和描述文件的措辞高度相关。把描述改得更具体之后,路由准确率有了明显提升。
修复的方法是把描述按"适用场景 + 典型请求示例 + 明确不适用场景"的结构重写:
"description": "从自由文本中提取日历事件的结构化信息。适用于用户直接描述日程安排、会议邀约、行程记录的句子。处理范围包括日期、时间、参与者、地点四类字段的提取。不适用场景:查询天气、预定机票、播放音乐等非日程类请求。"加上"典型请求示例"特别有效,因为示例给了大模型一个非常具体的参照系,比抽象的描述更容易对齐。我给团队定的规则是:每个技能必须至少有五个真实历史请求作为示例,并且要从中选出三个最容易混淆的负面示例写在边界说明里。
5. 规模化落地时的设计取舍与团队协作经验
当一个项目里的技能数量从几个增长到几十个,设计上的问题就会从"单个技能怎么写"变成"整个技能体系怎么治理"。这一节聊几个在这个阶段遇到的真实取舍。
5.1 命名规范:反常识但很重要的前缀策略
技能名称看着只是小问题,实际影响巨大。技能多了以后,上层调度系统需要按照描述和名称做检索,如果命名随意,比如团队成员各自起名,检索的准确定会明显下降。
我们最终采用的命名规范是"领域前缀 + 动作 + 对象"三段式。例如 booking_create_meeting、extract_event_info、check_room_availability。前缀的作用在于形成天然的分类命名空间,后期做权限管理和统计时也方便,直接按前缀筛选即可。
一个直觉上可能觉得反常识的点是:技能名称中尽可能不要包含"和"这种并列词。一个技能名称如果不得不用"和"来连接两个对象,说明它很可能承担了超出原子性的责任,应该继续拆分。这条规则简单可执行,比任何抽象的管理口号都有用。
5.2 技能目录与文档化:让复用真正成为可能
技术上是把技能做出来了,不等于团队里其他人能找得到、用得上。在我经手的第二个团队里,技能数量到 40 个左右的时候,出现了明显的"重复建设"现象——两个小组分别实现了功能几乎相同的技能,只是因为起的名字不同。
打破这个问题的关键,是建立一份活的技能目录。目录字段至少包括:技能名称、用途描述、典型输入输出示例、维护人、最近变更时间、已知限制。这份目录不需要做得像产品手册一样精美,但一定要嵌入到日常的开发流程里。我们的做法是:每次新增技能前,必须先查询目录,并说明与现有技能的差异,否则不允许入库。
这种流程上的约束看起来有些繁琐,但实际执行下来,最大的收益不是避免重复建设,而是逼着每个开发者去理解已有的技能体系,从而在设计中自然地复用现有能力。
5.3 版本的演化:什么时候该改,什么时候该重构
最后一个取舍,是处理技能的变更。早期我倾向于频繁优化每个技能的描述和逻辑,但后来发现,频繁变更对上层系统造成的适配成本非常高,调用方很难跟上变化。
一个比较实用的做法是:将技能变更分为"兼容性变更"和"破坏性变更"两类。兼容性变更,比如内部提示词优化、错误消息更加友好,灰度观察后直接更新即可。破坏性变更,比如修改输出结构的字段名或删除某个参数,则需要走一段时间的双版本并行期,并在目录中标注废弃时间。
这种策略没有什么高深之处,但执行起来需要很强的纪律性。团队内部达成共识:技能一旦发布,对外承诺的接口就像是契约,打破契约必须提前沟通并提供过渡方案。有了这个共识之后,扯皮的数量明显下降了。
关于技能体系的设计与落地,前前后后经历了不少曲折。最深的体会是:技能系统真正的难点不在于单个技能的技术实现,而在于厘清边界、规范契约、建立治理机制。这些工作没有耀眼的成果,短期看起来甚至有些繁琐,但时间越长,复利效应就越明显。如果一个又一个的复用单元能够像积木一样组合出各种流程,那这套体系就已经真正成熟了。