☰
从堆提示词到技能化:agent-skills智能体实践指南
2026/10/7 6:46:04 网站建设 项目流程

1. 为什么我要把智能体能力拆成"技能"而不是堆提示词

大约在几个月前,我开始重新整理自己那一堆散落的智能体项目。当时"agent-skills"这个词反复出现在我的笔记里:不是指某个现成的框架,而是一套把大模型能做的事拆成一个个可命名、可调用、可验证的"技能"的设计思路。这篇内容就是围绕这套思路的记录——包括我为什么放弃用超长提示词硬怼、技能模块到底怎么设计才不容易翻车、以及我在实际搭建技能库时踩过的坑。如果你正在做智能体应用,或者已经发现"同一个模型换了个场景就变笨",这篇文章应该能给你一些可以直接落地的参考。

先交代一下背景。我做的智能体不是那种"聊聊天就完事"的对话机器人,而是要真正替用户执行任务的助手:查库存、订会议室、汇总工单、生成报表。早期项目里,我习惯把各种能力写进一段很长的系统提示词,让模型自己"自由发挥"。结果当然是可以跑的,但维护起来非常崩溃:只要某个业务逻辑变了一点,就要改提示词;模型经常在不需要调工具的时候调工具;同一个能力在不同项目里复制了几份,后面各自长出不同的行为,完全失控。

后来我换了个思路:不再把提示词当作唯一载体,而是把能力明确拆成独立的"技能"模块。一个技能包含一段清晰的用途描述、一个稳定的参数签名、一段可执行逻辑,以及一个明确的退出条件。智能体自身只负责两件事:根据任务的上下文选择需要激活哪些技能,再把技能的执行结果组织成对用户有用的回复。这套设计就是我在项目里称为 agent-skills 的东西。

1.1 从一次翻车说起:给智能体塞了一段万能提示词

先把最典型的翻车案例摆出来。当时我负责一个内部工单系统,智能体的任务是帮运营同学查找工单、提取关键信息、并给出处理建议。最初版本非常简单,我在系统提示词里写了类似"你可以调用查询工单的接口,接口地址是...,参数包括工单号、状态、时间范围"这样一段说明,然后让模型自己拼参数、自己调接口。

上线第一周挺好,第二周开始出问题。运营同学反馈:工单号明明是TK-2024-0815,智能体却把参数传成了2024-0815,接口直接报错;让模型只统计今天的工单,它却悄悄调用了全部工单然后又自己算了一个"大约"的数字;更离谱的是,有些工单描述里带了"营销活动",模型居然凭自己的想象补了一个不存在的标签,导致后续报表统计全部错位。

这个问题的根源在于:我把"调用接口的能力"和"决定什么时候调用的判断力"混在了一起。提示词写得越具体,它能覆盖的场景就越窄;写得越宽泛,模型就越容易自己发挥。而我真正需要的,是一个在结构上就限定好边界的能力单元:它明确知道自己接收什么格式的输入、输出什么格式的结果、在什么条件下不该被调用。这种单元就是技能。

1.2 "技能"到底指什么:一个可命名、可调用、可验证的能力单元

在 agent-skills 的设计里,技能不是一段代码,也不是一个函数,而是一个四层结构。

第一层是技能的元信息,包括名称、用途描述、适用场景、调用限制。第二层是参数签名,定义调用这个技能需要传入什么字段,每个字段的类型、取值范围、默认值。第三层是执行逻辑,也就是真正和外部系统打交道的那部分代码,比如查询数据库、请求第三方API、读写文件。第四层是退出条件和错误约定,规定什么情况下算成功、什么情况下算失败、失败时应该返回什么样的错误码和错误信息。

为什么要分这四层?因为每一层服务的对象不同。元信息是给大模型看的,它决定了模型在什么情况下会想到调用这个技能;参数签名是给解析器和大模型一起看的,它把模型的自然语言理解变成结构化的调用;执行逻辑是给后端系统用的,它是真正做事的地方;退出条件则是给整个智能体的调度器看的,它决定了调用失败之后是重试、降级还是直接放弃。

我用一个生活化的类比来说明。你可以把智能体想象成一个餐厅经理,技能就是后厨的标准化菜谱。菜谱上写着"这道菜适合什么场合、需要什么食材、烹饪步骤、出锅标准"。经理不需要知道每道菜怎么炒,只需要根据客人的需求选择合适的菜谱下单;后厨按照菜谱执行,出品稳定可预期。如果你的"菜谱"只是把食材和做法都写在一张小纸条上让厨师自由发挥,那每个客人吃到的菜可能都不一样。

1.3 agent-skills 想解决的问题边界

需要说明的是,agent-skills 并不是一个银弹,它有自己的适用边界。我理解的这套设计主要解决三类问题:一是能力复用,同一个技能可以接入不同项目,不需要重复实现;二是行为可控,技能的输入输出结构是固定好的,模型不能自由发挥;三是可评测,因为技能是独立单元,所以可以单独做测试、做回归、做灰度。

它不适合解决的场景也有:如果你只是搭一个简单的客服问答机器人,不需要调用任何外部系统,那直接写提示词就够了,搞技能化反而增加复杂度。如果你的智能体需要大量自由探索、创造性生成,比如让模型写小说、做头脑风暴,那么刚性太强的技能会限制它的发挥。我自己通常会在"执行类智能体"上重度使用 agent-skills,在"生成类智能体"上保持轻量提示词。

还有一点很重要:技能的拆分粒度需要根据业务来定。拆得过粗,一个技能里塞了几十个分支,模型很难判断什么时候该用;拆得过细,技能之间互相耦合,调度器要处理大量选择逻辑,反而更累。我常用的标准是:一个技能应该对应一个完整的、对外部系统有明确影响的操作,比如"创建订单""查询库存""发送通知",而不是更底层的"拼接字符串""计算总和"。

2. 技能的最小闭环:定义、参数与退出条件

这一节我讲的是怎么去设计一个单独的技能,而不是整个技能库。很多人一开始就扎进代码里写函数,结果技能之间风格完全不同,有的参数用下划线、有的用驼峰,有的成功时返回字符串、有的返回数字,调度器根本没法统一处理。我建议先定一个统一模板,所有技能都必须遵循这个模板。

2.1 技能的元信息与描述:让模型知道"什么时候该用它"

技能的描述是最容易被低估的部分。很多人写技能描述时只有一句话,比如"查询天气",然后就没有了。但模型在做工具选择时,真正依赖的就是这段描述里的信息量。我踩过的一个典型坑是:我把一个技能描述写得过于技术化,比如"根据城市ID调用OpenWeatherMap接口获取当前气象数据",结果模型在用户说"今天适合洗车吗"的时候根本想不到去调用它。

后来我总结了一个描述模板,包含四个要素:功能概述、触发条件、禁止条件、输出概述。触发条件要写得像用户可能会说的话,而不是像接口文档。比如"当用户询问某个城市当前温度、湿度、风力或未来几小时降雨情况时,使用此技能"。禁止条件也重要,比如"如果用户只是闲聊中提到天气,不需要查询实时数据时,不要使用此技能"。这能让模型在边界情况下的误调用率明显下降。

还有一个细节:技能的元信息里最好带上版本号和负责人信息。因为当技能出问题时,你要能快速知道这一版是谁改的、改了什么东西。尤其是团队协作场景,这些元信息能省掉大量沟通成本。我在实际工作中会把元信息放在技能文件头部的注释里,或者放在一个独立的 JSON 配置中。

2.2 参数签名:把自由文本变成结构化输入

参数签名是整个 agent-skills 里技术含量最高的部分。大模型理解自然语言,但外部系统往往只接受严格结构化的参数,这中间需要一个转换层。参数签名要做的事情,就是给这个转换层提供一个明确的"契约"。

我在设计参数签名时通常遵循以下原则:

  • 所有参数必须有类型声明,包括整数、浮点数、字符串、布尔值、对象、数组。
  • 每个参数必须说明是必填还是选填,选填参数必须提供默认值。
  • 参数名要用小驼峰或者下划线统一样式,不要混用。
  • 尽量把复杂的嵌套结构拆成扁平的字段,减少模型出错的可能。
  • 对于枚举类型,直接在描述里列出所有可取值,并给出每个值的意思。

用一个例子说明。假设我要做一个"查询门店库存"的技能,合理参数签名的 JSON Schema 大体长这样:

{ "type": "object", "properties": { "storeId": { "type": "string", "description": "门店编号,格式为 S 加 4 位数字,例如 S1024" }, "skuIds": { "type": "array", "items": { "type": "string" }, "description": "需要查询的商品编码列表,最多支持 50 个" }, "includeLocked": { "type": "boolean", "default": false, "description": "是否包含锁定库存,默认不包含" } }, "required": ["storeId", "skuIds"] }

如果你是模型,看到这样的参数约束,至少不会把门店号填成"上海静安店"。在实际项目里,我还会在参数描述里写明"如果不确定门店编号,请先调用搜索门店技能",把技能之间的协作关系也写进参数说明,这样模型的调用成功率会高不少。

2.3 执行逻辑与副作用边界

执行逻辑是技能里最传统的那部分代码,但有一个点经常被忽略:技能必须是可重入的。什么叫可重入?就是同一个技能在相同输入下,如果不涉及外部状态变化,应该返回相同结果;如果涉及外部状态变化,比如创建订单、发送消息,则必须设计幂等机制,防止重复调用带来副作用。

我记得有一次线上事故,就是因为智能体在调用"发送通知"技能时,因为上游网络超时自动重试了一次,结果用户收到了两条完全相同的短信。从那以后,凡是有副作用的技能,我一定会在执行逻辑里加上幂等键。具体做法是:参数里增加一个requestId字段,由调度器生成并传入;技能在执行前先查一下这个requestId是否已经处理过,处理过就直接返回上一次的结果,不再重复执行。

另外,执行逻辑里要严格控制超时时间。模型调用技能的等待时间不能太长,否则整体的用户体验会很差。我一般把外部 HTTP 请求的超时设置在 3 到 5 秒,整个技能的执行时间上限控制在 10 秒以内。超过这个限制,宁可返回失败信号让调度器走别的路径,也不能一直卡住。

2.4 退出条件与错误信号:技能失败不等于任务失败

技能执行只有两种结果:成功或失败。听起来很简单,但具体实施时有很多细节。成功的信号不仅仅是"代码没有抛异常",还要看返回的数据是否真的满足调用需求。比如查询用户信息,接口返回 HTTP 200 但 body 是空的,这种情况应该算失败,因为下游无法从空结果中提取任何有价值的信息。

失败的信号也要结构化。我通常让技能返回一个统一的失败对象,包含错误码、错误消息、可重试标记和恢复建议。可重试标记特别重要:如果是参数填错了,比如日期格式不对,重试也没用;如果是上游服务暂时不可用,那重试是有价值的。调度器拿到这个标记之后,才能做出正确的决策。

这里说一个容易踩的坑:不要把所有失败都抛给大模型让它自己"临场发挥"。有些智能体在技能失败后,会直接把原始错误信息拼到回复里,然后问模型"你想怎么处理",结果模型开始一本正经地编造解决方案。我现在的做法是:失败时调度器先尝试自动恢复,比如重新解析参数、换一个可用节点;如果恢复不了,再把结构化的错误信息和用户可选的处理路径给到模型,让模型在限定范围内做决策。

3. 从零搭一套可落地的技能库

讲完单个技能的设计,接下来聊怎么把这些技能组织成一个库,并且接到大模型上。我在 agent-skills 的实践里,技能库不只是"一堆函数文件",它还包含目录结构、注册机制、配置管理和对接层。下面是我比较满意的一套工程方案。

3.1 目录结构与命名规范

技能库的目录结构会影响后续的维护成本,我建议从一开始就按领域划分,而不是按技术类型划分。比如:

skills/ common/ time_resolver.py id_normalizer.py inventory/ query_stock.py transfer_stock.py adjust_stock.py order/ create_order.py cancel_order.py query_order.py notification/ send_email.py send_sms.py

每个技能文件只包含和这个技能相关的内容:元信息、参数 Schema、执行逻辑、测试用例。领域目录下可以放一个 README 说明这个领域内所有技能的使用场景和相互依赖关系。命名方面我推荐使用动词加名词的格式,比如create_order、query_stock,避免出现helper、utils这种什么都装但什么都不像的名字。

在 agent-skills 的早期版本里,我把所有技能平铺在一个目录里,总共一百多个文件,后来每次找东西都像在大海里捞针。按照领域分组之后,新技能该放哪里、旧技能该去哪里找,都有了明确规则。特别是团队协作时,目录结构本身就在传达一种"技能归属"的信息,代码 review 的时候也更有针对性。

3.2 注册与发现机制:不要用一堆 if-else 去路由

技能定义好了之后,下一步是注册。很多人会写成if skill_name == "query_stock": return query_stock(data)这样的分发器,这在新技能少的时候没问题,但一旦技能数量上来了,维护成本非常高,而且新增技能时必须修改分发器,很容易漏改。

我现在的做法是给每个技能加上装饰器,由框架在启动时自动发现并注册。用 Python 举个例子:

# skills/registry.py from typing import Dict, Callable, Any SKILL_REGISTRY: Dict[str, Callable] = {} def register(name: str, description: str, schema: dict): def decorator(func): func.skill_name = name func.description = description func.schema = schema SKILL_REGISTRY[name] = func return func return decorator

然后在每个技能文件里使用注册器:

# skills/inventory/query_stock.py from skills.registry import register @register( name="query_stock", description="查询门店商品库存,当用户询问现存量、可用量、锁定量时调用", schema={ "type": "object", "properties": { "storeId": {"type": "string"}, "skuIds": {"type": "array", "items": {"type": "string"}} }, "required": ["storeId", "skuIds"] } ) def query_stock(params: dict) -> dict: ...

启动时只要让框架扫描skills目录,自动导入所有模块,注册器就会把技能收集到全局注册表中。这样做的好处是:新增技能时不需要改任何分发逻辑,只需要新建一个文件、写好注册信息。删除技能时也只需移除文件,框架会自然忽略它。对于小型团队,这个方案比引入完整的插件体系要轻量得多,同时也能满足大部分需求。

3.3 技能实现示例:一个带缓存的天气查询技能

为了把上面的注册机制串起来,我用天气查询这个最常见的场景演示一个完整的技能实现。

# skills/weather/query_current.py import time import requests from skills.registry import register _CACHE: dict = {} _CACHE_TTL = 600 # 600 秒,即 10 分钟 @register( name="query_current_weather", description="查询指定城市当前天气,包括温度、湿度、风力、天气现象。当用户询问今天、现在、当前的天气情况时调用", schema={ "type": "object", "properties": { "city": {"type": "string", "description": "城市名称,如'北京'、'上海'"}, "unit": {"type": "string", "enum": ["celsius", "fahrenheit"], "default": "celsius"} }, "required": ["city"] } ) def query_current_weather(params: dict) -> dict: city = params["city"] unit = params.get("unit", "celsius") cache_key = f"{city}|{unit}" now = time.time() if cache_key in _CACHE and now - _CACHE[cache_key]["ts"] < _CACHE_TTL: return {"success": True, "data": _CACHE[cache_key]["payload"]} api_key = get_api_key("weather_provider") resp = requests.get( "https://api.example.com/current", params={"city": city, "unit": unit}, timeout=5, headers={"Authorization": f"Bearer {api_key}"} ) if resp.status_code != 200: return { "success": False, "error": { "code": "UPSTREAM_ERROR", "message": "天气服务暂时不可用", "retriable": True, "suggestion": "可以稍后重试,或改用默认城市的天气数据" } } payload = resp.json()["data"] _CACHE[cache_key] = {"ts": now, "payload": payload} return {"success": True, "data": payload}

这个例子里有几个我特别想强调的实践。第一个是缓存。天气类数据变化频率不高,缓存 10 分钟对用户感知几乎没有影响,却能显著降低上游 API 的压力和成本。第二个是错误返回的结构化,我统一用了success+data/error两种形态,这样调度器解析时不需要做各种特判。第三个是get_api_key这个函数,它从配置中心读取密钥,而不是写在代码里,防止密钥随着代码仓库泄露。

3.4 把技能接到大模型上:Function Calling / Tool Calling 的对接

技能库本身并不感知大模型的存在。它只提供了一组可被调用的能力,真正把它们接到模型上的是对接层。以 OpenAI 兼容接口的 tool calling 为例,对接层要做的事情是:把注册表里的技能 Schema 转成模型可以识别的工具定义,模型在对话中返回一个 tool call 请求,对接层解析这个请求并找到对应的技能执行,最后把结果作为工具消息回传给模型。

这里有一个非常关键的细节:工具定义里的description字段长度是有限的,而且模型对过长描述的注意力会衰减。我的经验是每个技能的 description 控制在 100 到 200 字以内,把最核心的触发条件放在开头,禁止条件放在后面。如果描述超过 200 字,模型的工具选择准确率会显著下降,这不是玄学,而是我在多次对比测试里观察到的现象。

对接层还有一个容易被忽视的问题:模型返回的 tool call 参数有时会反序列化失败。比如参数里某个字段的值是 "null" 字符串,或者数组少了一个右括号。我一般会做一层容错解析:先用标准 JSON 解析,失败后用宽松模式尝试提取字段。容错逻辑不要写得太复杂,否则对接层本身就变成新的故障点。我会在简单容错后仍然失败的情况下返回参数错误信号,让模型自己重新生成一次调用。

下面是一个简化版的对接逻辑:

def handle_tool_call(tool_call): name = tool_call["function"]["name"] raw_args = tool_call["function"]["arguments"] skill = SKILL_REGISTRY.get(name) if not skill: return {"success": False, "error": {"code": "UNKNOWN_SKILL"}} try: params = json.loads(raw_args) except json.JSONDecodeError: params = loose_parse(raw_args) if params is None: return {"success": False, "error": {"code": "ARG_PARSE_FAILED"}} return skill(params)

3.5 技能的依赖注入与配置管理

随着技能库变大,不同技能之间难免会有共享的能力。比如很多技能都需要做用户身份校验、权限校验或日志上报,如果每个技能各自实现一遍,代码会非常冗余。我建议把这些横切关注点做成装饰器或中间件,放在技能执行之前统一处理。

依赖注入解决的是另一个问题:技能不能硬编码外部服务的地址和密钥。否则测试环境、预发环境、生产环境就没法切换了。我在技能代码里从不直接引用环境变量,而是通过一个配置对象获取,这个配置对象在进程启动时根据当前环境初始化。这样同一个技能代码可以直接跑在三套环境里,只需要改配置,不需要改业务逻辑。

def get_api_key(provider: str) -> str: return config_manager.get(f"api_keys.{provider}")

同时我还会给技能执行加上一个简单的链路追踪,把技能名、参数摘要、耗时、成功与否、错误码写入日志。这些数据后面会用于做技能的评测和优化。没有这些埋点,你永远不知道模型是在"某个技能总失败之后硬编答案",还是"在正确调用了某个技能但技能本身有 bug"。

4. 技能质量的评估、回归与迭代

做了技能库之后,必然会遇到一个新的问题:我怎么知道这个技能质量好不好?模型什么时候会调用错?技能执行的成功率到底有多高?这一节专门讲评估和迭代,这也是 agent-skills 体系里最容易被人忽略、但回报率最高的部分。

4.1 离线评测:用真实任务集给技能打分

离线评测是我每次修改技能之后必做的第一件事。我把线上收集到的真实用户请求整理成一个测试集,每条测试数据包含用户问题、期望调用的技能、期望的参数取值、期望的返回结果。然后我在不触发真实外部系统的情况下,用模拟数据跑一遍完整链路,看模型是否在正确时机选择了正确技能、参数是否解析正确、技能执行是否成功。

这个测试集不需要一开始就很大,我建议从 50 条开始,覆盖每一类技能的典型场景、边界场景和反向场景。典型场景是"用户直接表达了明确需求",边界场景是"用户没有给出关键参数、需要追问",反向场景是"用户提到了关键词但实际不需要调用技能"。反向场景尤其重要,因为技能被"滥用"往往比"不用"更危险。

离线评测的指标我常用三个:

  • 技能选择准确率:应该调用某个技能时,模型是否正确调用;不该调用时,模型是否没有调用。
  • 参数解析准确率:调用之后,参数是否被正确提取和格式化。
  • 执行回报率:技能逻辑是否按预期返回成功和正确的数据。

这三个指标分开统计,能帮你快速定位问题出在哪个环节。如果技能选择准确率低,说明描述写得不够清楚;如果参数解析准确率低,说明 Schema 设计有问题;如果执行回报率低,说明技能代码本身有 bug 或外部依赖不稳定。

4.2 在线观测:调用日志里到底该埋哪些点

离线评测解决的是"改版之后没有退化"的问题,但线上情况永远比测试集丰富。所以在线观测是必须的。我在技能调用的链路里固定的埋点包括:技能名称、技能版本、调用来源、参数摘要、耗时、返回码、错误详情、用户会话 ID、模型生成 tool call 时的原始上下文摘要。

其中有一个经常被忽略的埋点是"模型在犹豫之后最终没有调用技能"的轨迹。也就是模型已经输出了某个技能的名称,但没有真正发起调用,或者先说了别的技能又改口。这类轨迹能帮你发现"描述排序很接近、模型自己也分不清"的技能组合,从而去调整两个技能的描述边界。

在线观测的数据我通常不会实时全量展示,而是每天跑一个聚合分析,生成一份技能健康度报告。报告用几个维度给每个技能打分:调用次数、成功率、平均耗时、错误分布、误触发率。误触发率是一个需要人工标注的指标,需要抽查一些会话日志来判断某次调用是否真的合理。我在项目初期会每周抽 100 条日志做人工标注,后期模型稳定之后放宽到每周 30 条。

4.3 灰度与回滚:技能升级不是换个 prompt 那么简单

很多只有一两个智能体项目的团队不太会想到灰度这件事。但一旦你做了技能库,并且服务多个业务方,技能升级就必须走灰度流程。我经历过一次真实的教训:我给"订单查询"技能加了一个新字段,没有做灰度直接全量上线,结果模型在调用时偶尔会生成非法的时间格式,导致大量查询失败,业务方直接来找我,我花了半天才定位到是技能 Schema 变更引发的兼容性问题。

从那之后,我所有技能改动都会遵循一个最小灰度流程:

  • 先在测试集上跑离线评测,确认选择率和参数解析率没有下降。
  • 再在模拟环境里跑全链路联调,确认真实接口兼容新参数。
  • 然后把新版本技能部署到预发环境,用一小部分线上流量做影子调用,只记录不返回给用户。
  • 最后按 10%、30%、100% 的比例逐步放量,每步观察错误率和调用延迟。

如果新版本在某一阶段出现异常,必须支持快速回滚。回滚不是简单地把代码切回旧版本,还要考虑线上日志和缓存的兼容性。我的做法是:每个技能文件里的函数都保持旧版本接口不变,新逻辑放在新增的版本化函数里,通过注册表版本字段切换路由。这样即使回滚,也不用改代码重发,只需要在配置中心改一下版本号。

5. 我在 agent-skills 实战里踩过的坑

最后这部分我不讲框架设计了,讲几次血泪教训。这些坑单独看都不大,但每一个都让我付出了不少调试时间。如果你正在搭建自己的技能库,提前避开它们能省很多事。

5.1 描述写得太抽象,模型在错误时机调用

第一次设计技能库时,我把一个技能命名为data_analysis,描述是"对用户提供的数据进行分析并生成报告"。听起来没毛病,但这个描述抽象到几乎覆盖了所有场景。结果模型在用户问"昨天销售额怎么样"时调用它,在用户问"帮我看看这个 Excel 里哪个字段是日期"时也调用它,甚至在用户说"分析一下这个方案的利弊"时也试图调用它。

数据仓库里根本没有存这个技能能处理的数据格式,于是大量调用失败,用户体验很差。后来我把data_analysis拆成了query_sales_summary、detect_column_types、compare_two_datasets三个技能,并且给每个技能都写清楚了"什么时候用"和"什么时候绝对不要用"。拆完以后,误触发率下降了大约 70%。

5.2 技能返回值缺少结构,下游解析全靠猜

早期我有一些技能为了省事,返回值直接是纯文本字符串,比如"查询成功,共有 120 件商品"。模型拿到这个字符串之后再去组织回复,看起来没问题,但如果下游要拿这 120 做进一步的计算,就还得再解析一遍文本,非常脆弱。

有一次我想把"查询库存"的结果直接接到"生成采购建议"的技能里,因为第一个技能返回的是纯文本,第二个技能只能重新去调用一次接口,浪费了大量时间和 token。后来我把所有技能返回结构统一为结构化 JSON,关键的数值字段独立存放,文本描述只作为辅助展示。这样技能之间可以直接传数据,整体链路一下子干净了很多。

5.3 过度建模:把简单查询也做成了技能

和上一节对应的是另一个极端:我把一些根本不需要拆的东西也拆成了技能。比如一个获取当前时间的操作,我也给它设计了参数 Schema,让它支持时区、格式、语言等字段。结果模型每次回答"现在几点"都要额外发一次工具调用,增加了延迟、增加了 token 消耗,却没有任何收益。

从那以后我给自己定了一条原则:如果这个能力只是对已有信息做一次简单格式化,或者模型本身就能完成,就不做成技能。技能应该留给那些需要访问外部系统、执行确定性操作、或者产生不可逆副作用的能力。简单的东西过度抽象,只会让智能体变慢、变笨。

5.4 技能间的隐式耦合与上下文污染

这个坑比较隐蔽,但影响很大。我有一组技能分别是query_customer和create_customer,它们都涉及客户信息。其中一个改动把客户编号从纯数字改成了带前缀的字符串,我没有同步修改另一个技能的参数描述,结果模型经常在用create_customer创建客户时传旧格式编号,然后在query_customer里查不到数据。

技能之间的隐式耦合是最难排查的问题,因为它们没有直接依赖关系,而是通过业务规则间接关联。我的对策是在领域目录的 README 里维护一张"业务字段与技能依赖关系表",每次修改一个技能时先检查这张表,看有没有其他技能共享同一业务概念。团队协作时,这一步要在代码 review 阶段强制完成。

5.5 关于技能版本管理的一点建议

最后聊一下版本管理。技能的版本和大模型的关系很微妙:同一个技能代码,在 GPT-4 上表现良好,在另一个开源模型上可能就经常被错误调用。这意味着技能的版本管理不能只看代码仓库里的 commit,还要记录"当前技能定义与哪些模型版本做过适配验证"。

我现在会把模型版本作为技能配置文件的一个字段记录下来,比如在每个技能的 Skillfile 里写compatible_model_versions: ["gpt-4o-mini-2024-07-18", "llama-3.1-8b-instruct"]。当模型供应商发布新版本时,我不会直接切流量,而是先在离线评测集上跑一遍所有技能,看选择率是否下降。如果下降明显,就要调整技能描述或者参数 Schema,然后再灰度上模型。这种做法把技能的稳定性和模型的迭代解耦了,后续升级模型时心里有底很多。

如果你刚开始做技能库,我的建议是先别急着追求完美的框架,把一个最核心的业务技能按照上面说的四层结构做扎实,跑通注册、调用、评测、灰度这一整个闭环。一个真正稳定的技能,比十个只写了一行描述的技能有用得多。等我后面再遇到新的坑,会继续更新这篇实践记录。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询