如果你最近在调AI智能体,八成会碰上一个尴尬时刻:模型什么都懂,让它干活却总是差一口气。给它一个工具列表,它要么不知道什么时候该用、要么把参数填得乱七八糟。我最初也以为问题出在提示词上,后来折腾了两三个项目才想明白,真正缺的不是更长的提示词,而是一套能被智能体理解、复用、组合的“技能”。agent-skills 要解决的就是这件事:把大模型的“能力”拆成一个个可以独立开发、注册、编排的技能单元,让Agent从“会聊天”变成“会干活”。
这篇文章适合正在做Agent开发的工程师、AI应用的产品经理,以及想从“写提示词”进阶到“搭智能体”的同学。我会从概念拆解讲起,给出技能的标准结构,再带你把一个“查天气并安排日程”的技能组合完整落地,最后分享我实际调试中踩过的坑。这个过程不依赖特定框架,核心思路换到任何Agent项目上都通用。
1. 为什么智能体需要“技能”而不只是“提示词”
1.1 从对话模型到任务模型:能力视角的转变
先聊一个本质问题。大模型本身是一个对话模型,你问它答,它擅长的是“生成文本”。但Agent要做的却是任务模型:接收一个目标,自己规划步骤、调用工具、检查结果,最后完成任务。这两个模型之间存在一条鸿沟,而“技能”就是用来填这条鸿沟的桥。
举个例子。你直接告诉模型“帮我订一间明天晚上在市中心的餐厅”,它能写出一段漂亮的回复,但它并不会真的打开预订平台、筛选可订座位、提交订单。如果你给它一个“餐厅预订”的工具函数,好一点的模型会用,但用得好不好完全取决于你给的工具描述和调用规则怎么写。而技能这个抽象层,把“该用哪个工具”“工具参数怎么填”“调用返回后怎么处理”“失败了怎么办”全部固化下来,成为一个可复用的能力单元。
我自己的理解是:提示词像口头交代,技能像一份岗位手册。口头交代一个人“帮我去楼下拿快递”,这人可能找到驿站、报手机号、拿包裹回来,但过程中遇到“包裹不在货架”“驿站关门”就得再找你问。岗位手册则会写明找谁问、怎么查、替代方案是什么。你要的不是一个只会听话的对话模型,而是一个有章法的执行者,这就是技能存在的意义。
1.2 “skills”到底封装了什么
把技能拆开看,它绝不是“一段提示词”这么简单。一个完整的技能单元通常包含六个部分:触发条件、参数定义、工具绑定、执行逻辑、结果校验、失败回退。
这六个部分合在一起,才叫技能。单独拎出任何一部分都不够。比如你写了一段很厉害的提示词告诉模型“你要像资深助理一样处理邮件”,但没有绑定发件工具、没有定义收件人参数怎么填,那这段提示词只能得到一封写得很好但发不出去的邮件。反之,你只给模型一个发邮件API、不教它什么时候该发、怎么判断发送成功,那这个工具要么被滥用、要么被遗忘。
我习惯把这六个部分比作一个“带说明书、带标准动作、带应急预案”的微服务。服务端是工具API,说明书是触发条件和参数定义,标准动作是执行逻辑,应急预案是失败回退。Agent运行时,先根据用户的请求判断该调用哪个技能,然后按技能定义把请求翻译成具体的API调用,再把结果翻译回用户能听懂的话。整个过程,技能既是“翻译规则”,也是“执行蓝图”。
1.3 技能库与零散工具的本质区别
有些人会问:我把工具直接塞给模型不也一样吗?反正模型原生支持函数调用。区别非常大。零散工具是“裸奔”的:系统提示词里塞一张工具清单,模型自己看着办,遇到模糊请求时经常选错工具,参数抽取全凭运气,失败之后也没有任何补救策略。
技能库给每个工具穿上了完整的“上下文”:工具在什么场景下使用、参数从哪里来、输出如何验证、异常如何处理。而且技能之间可以复用,比如一个“获取用户当前位置”的技能,可以被“查天气”“找附近餐厅”“规划通勤”多个任务共用。零散工具做不到这种组合,因为它们的调用逻辑散落在各处,没有统一的管理和版本控制。
还有一个容易忽略的点:技能本身就是大模型能力的一种“记忆外挂”。模型的上下文窗口有限,你不可能每次对话都把几十个工具的用法完整写进提示词。但技能库可以只把“技能清单”给模型看,等模型选中某个技能后,系统再把技能的完整定义注入这一轮请求。这样既省token,又减少了上下文干扰导致的误选。
2. 技能“长什么样”:从抽象概念到可落地的结构定义
2.1 一个技能的完整结构
说了这么多概念,来点能落地的。下面是我在模拟项目X里实际用的一套技能定义结构,用JSON表示。我没有选用某个特定框架的格式,而是自己定了一套通用结构,好处是换框架不用改业务逻辑。
{ "name": "query_weather", "description": "查询指定城市当天的天气情况,包括温度、天气状况、风力和湿度。适合用户问'今天冷吗'、'要不要带伞'、'某地天气如何'等场景。", "agent_trigger": { "keywords": ["天气", "下雨", "温度", "带伞", "冷", "热"], "semantic_match": "询问某地当前或今日的气象信息" }, "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称,如北京、上海、广州", "required": true }, "date": { "type": "string", "description": "查询日期,格式YYYY-MM-DD,默认当天", "required": false } } }, "tools": [ { "name": "weather_api", "endpoint": "/api/v1/weather", "method": "GET", "params_mapping": { "city": "city", "date": "date" } } ], "execution_steps": [ "根据用户输入提取城市名称,如果城市不在建议列表里,尝试用城市别名匹配", "调用天气API获取数据", "使用返回数据生成用户可读的天气描述,温度用摄氏度,风力用风力等级" ], "validation": { "rules": ["返回的status必须为success", "温度字段必须是数字且在-50到60之间"], "on_failure": "重新调用API一次,如果仍失败则执行fallback" }, "fallback": { "type": "message", "content": "抱歉,天气服务暂时不可用,请稍后再试或直接告诉我城市名称让我重新查询。" } }这个结构里,我认为最重要的字段是description和agent_trigger。因为模型判断“什么时候该用这个技能”,靠的主要就是这两段文本。description写得越具体、越多含触发场景的动词和名词,模型选中的准确率越高。agent_trigger是我自己加的辅助字段,某些框架会把它编译成工具描述的一部分,也可以用来做前置的规则过滤。
2.2 结构里每个字段的角色与设计要点
name和description是给模型看的,命名要直观,描述要写“这个技能擅长处理什么场景”,不要写“这个技能调用了什么API”。我见过有人把description写成“调用GET请求获取天气数据”,模型看到这种描述根本不知道什么时候该用。
parameters是给模型填参数用的,本质是JSON Schema。这里的关键是要写清楚每个参数的取值范围、默认值、格式。比如日期不写格式,模型可能给你填“明天”而不是“2025-01-15”,后面解析就炸了。枚举值也尽量列出来,能显著降低模型乱填的概率。
tools绑定真实API,我只写endpoint、method和映射关系,具体HTTP层逻辑由运行时框架处理。单技能可以绑多个工具,比如天气技能可能需要一个城市名称转城市ID的工具,再加一个真正查询天气的工具。
execution_steps是给技能执行器读的,它让“调用工具”这件事变成可编排的多步操作。有些执行器只支持单工具调用,那steps就用不上;但如果你的执行器支持多步,steps就是你的“迷你工作流”,甚至可以在steps里写条件判断。
validation和fallback是最容易被忽略却最能提升体验的部分。没有校验,模型里跑出一个“温度是null”的result,它会一本正经地告诉用户“今天温度未知”。有了校验和回退,至少能兜底。我后面还会细讲这两块怎么设计。
2.3 设计技能时最关键的三个问题
第一个问题:这个技能的触发边界在哪?也就是说,什么请求该直达这个技能,什么请求不该。边界划过宽,模型会在用户问“明天穿什么衣服”时也去调用天气技能,虽然也不是不行,但容易抢其他逻辑的活。边界划过窄,用户换个说法就翻车。我的经验是:先用agent_trigger里的关键词和语义描述划出合理的宽范围,跑一周线上日志,再根据bad case收紧。
第二个问题:参数怎么抽?这是技能实战中最常见的失败点。模型从用户口语里提取“城市”不难,难的是处理“北京今天PM2.5多少”这种把多技能参数混在一起的情况。我的建议是:参数schema写得越细,模型抽得越准。给每个参数写一个“用户可能怎么表达这个信息”的示例,比如“城市:北京、首都、BJ这些说法都要转化”。实测下来,带示例的schema比不带示例的参数抽取准确率能高两成以上。
第三个问题:失败之后怎么办?很多初版技能直接让工具报错,然后模型说一句“出了点问题”。但一个合格技能必须设计回退路径:重试一次、降级到另一个数据源、用缓存兜底、或者干脆转人工。哪怕回退只是“让用户换个说法”,也比闷声失败强得多。注意,fallback的设计要写死在技能里,让执行器按规则跑,而不是把“自己想办法”这句话留给模型自由发挥。
3. 实操:实现一个“查天气并安排日程”的技能组合
3.1 第一步:拆解需求,划清技能边界
现在做一个具体的例子。用户可能说“帮我查下北京周末天气,如果晴天就把周五的跑步计划安排在周六上午”。这句话涉及两件事:查天气、安排日程。我为什么不把它做成一个叫“weather_and_schedule”的大技能?因为这两个子任务的复用价值完全不同:查天气可能被“今天适合跑步吗”触发,安排日程可能被“帮我订会议室”触发。硬绑在一起,复用性就差。
正确的做法是拆成两个技能:query_weather和create_schedule_event。前者负责获取天气信息,后者负责在日历里写一条安排。然后通过一个编排层把它们串起来。技能原子性这个原则值得反复强调:当你能清晰说出一个技能“单独解决什么问题”时,它才算合格;一个技能如果承担了两种以上的独立职责,就该拆。
拆完以后,给每个技能写“我能做什么、不能做什么”的边界说明。比如create_schedule_event的description里要写明“本技能只负责创建日程,不负责查询已有日程或修改删除日程”,避免模型让一个技能干三种活。
3.2 第二步:定义接口与提示词
先定义create_schedule_event的接口。这个技能要完成的事:接收事件标题、时间、备注,然后调用日历API创建一条记录。
{ "name": "create_schedule_event", "description": "在用户日历中创建一条新的日程安排。适合用户说'帮我记一下'、'安排个时间'、'添加到我的日程'等场景。只负责新建日程,不负责查询、修改或删除。", "parameters": { "type": "object", "properties": { "title": { "type": "string", "description": "日程标题,如'跑步计划'、'项目评审会'", "required": true }, "start_time": { "type": "string", "description": "日程开始时间,ISO 8601格式,如2025-03-15T09:00:00", "required": true }, "end_time": { "type": "string", "description": "日程结束时间,ISO 8601格式,如2025-03-15T10:00:00", "required": false }, "notes": { "type": "string", "description": "日程备注,默认为空", "required": false } } }, "tools": [ { "name": "calendar_api", "endpoint": "/api/v1/calendar/events", "method": "POST", "params_mapping": { "title": "title", "start_time": "start_time", "end_time": "end_time", "notes": "notes" } } ], "validation": { "rules": ["返回的event_id不能为空", "返回的status必须等于created"], "on_failure": "提示用户日程创建失败,并展示用户提供的原始信息让用户确认" }, "fallback": { "type": "message", "content": "日程创建失败,我已经保留了你说的原始信息:{original_user_input}。你确认一下日期和时间,我重试一次。" } }注意start_time这个参数,我给的是ISO 8601格式。为什么不用“明天上午10点”这种人类友好格式?因为API层只认机器可读格式。这里设计的关键是:模型负责把用户说的“周六上午”换算成具体的ISO时间,这个换算需要当前日期信息和时区信息。有些团队会在系统提示词里注入当天日期,让模型能换算相对时间,我会在调度层里处理这个细节。
3.3 第三步:注册与调度逻辑
技能定义好了,还得让Agent在运行时“看得见”它们。我采用的是两层路由:第一层是规则粗筛,第二层是模型细选。粗筛层用一个轻量级关键词匹配把明显不相关的技能过滤掉,减少模型的选择空间,细选层把候选技能的description列表发给模型做函数调用决策。
# 伪代码:两层技能路由 class SkillRouter: def __init__(self, skills): self.skills = skills # 技能名 -> 技能定义 def coarse_filter(self, user_input: str): candidates = [] for skill in self.skills.values(): # 基于keywords做粗筛,命中任一关键词即进入候选 for kw in skill["agent_trigger"]["keywords"]: if kw in user_input: candidates.append(skill["name"]) break # 如果没有候选,则全部技能进入候选(依赖模型决策) return candidates or list(self.skills.keys()) def route(self, user_input: str): candidates = self.coarse_filter(user_input) # 把候选技能的description拼进system prompt,让模型选择 # 这一步通常结合模型的原生function calling能力 return candidates这段代码说明了一个核心思路:不要让模型从30个技能里选,先帮它过滤到3个,再让它做选择。选错率会低很多。粗筛本身不追求准确率,只求不要漏掉正确技能,所以关键词阈值可以放宽,宁可多筛进来几个,也不能把正确技能过滤掉。
调度层的系统提示词我习惯这样写:
你是一个任务调度助手。用户会提出请求,你需要从候选技能中选出最合适的一个或多个技能来完成任务。只允许使用候选技能列表中的技能,禁止自行编造技能名称。如果用户请求没有匹配到任何技能,直接回复无法处理并说明原因。 候选技能列表: {候选技能的name + description}注意最后那句话:“如果没有匹配到任何技能,直接回复无法处理”。这能避免模型在候选列表为空时瞎编一个调用。瞎编在调试阶段特别常见,尤其是技能库小、模型能力弱的时候,一句“技能不存在”的兜底能省很多排查时间。
3.4 第四步:结果校验与用户确认
技能执行完,不能让API的原始返回直接怼给用户。query_weather返回的是JSON,但用户要的是“北京今天晴,5到10度,西北风3级”。这一步叫“结果翻译”,通常在技能内部完成。我在执行步骤里让模型把API返回的原始数据组织成一段自然语言。
但翻译之前必须先校验。校验的目的不是追求绝对正确,而是避免把明显异常的数据交给用户。我在校验规则里只设几条防线:状态字段必须是success、关键字段存在且类型正确、数值范围合理。超过防线就进fallback。
在“查天气并安排日程”这个组合场景里,还有一个需要处理的关键点:先查天气,拿到结果后向用户确认“周六上午晴天,帮你把跑步计划安排到周六上午十点,可以吗?”,用户确认后再创建日程。这一步是人工确认钩子,尤其在涉及写操作时建议保留。Agent自动执行一切看起来很酷,但用户一旦发现日历被塞了一条错误安排,信任感就崩了。我见过的成熟应用,写操作几乎都带确认按钮,这不是笨,是对用户负责。
4. 技能编排:从单个技能到“一套本领”
4.1 编排模式:顺序、并行、条件分支
单技能能解决单个问题,但用户真实世界的问题往往是组合式的。“天气+日程”是顺序执行,那还有其他典型模式吗?
第一种是顺序模式:前一个技能的输出作为后一个技能的输入。比如“查天气”的输出“周六晴天”,是“创建跑步日程”的时间选择依据。这种模式最简单,也是我用的最多的。第二种是并行模式:多个技能互不依赖,同时执行,最后汇总结果。比如用户问“北京和上海今天天气怎么样”,一次性并行调用两次query_weather,比串行更快。第三种是条件分支:根据上下文决定走哪个技能。比如如果查询结果是雨天,就走“生成室内运动建议”的技能,晴天则走“安排户外跑步”的技能。条件分支最考验编排层的逻辑表达能力。
我的建议是:顺序和并行优先在编排层用代码写死,条件分支可以交给模型决策,但前提是决策点要清晰。比如“如果天气晴朗则安排户外活动,否则安排室内活动”,这种判断给模型一点不慌;但如果条件是“根据用户的历史偏好决定走哪个分支”,那变量就多了,建议还是提取关键特征后在代码里做判断。
4.2 上下文与状态管理:技能间的“交接”
技能拆开之后,最麻烦的不是单个技能的实现,而是它们之间的“交接”。天气技能刚才算出的“周六晴天”,日程技能要用;“周六早上有时间”这个信息,是用户在前面几轮对话里提到的,也要传给日程技能。这些临时数据放哪里?
我用的是一个简单的执行上下文对象。每个技能执行前注入一个上下文,技能从中读取它需要的状态,执行后把结果写回上下文。这样技能之间不需要直接知道彼此的接口,只和上下文交互,降低了耦合。
class ExecutionContext: def __init__(self): self.state = {} # 持久状态,比如用户ID、时区 self.tmp = {} # 临时数据,技能执行产生的结果 self.conversation = [] # 最近几轮的用户消息 def get(self, key): return self.state.get(key) or self.tmp.get(key) def set_tmp(self, key, value): self.tmp[key] = value def to_prompt(self): """把当前上下文的关键信息拼成一段文本,供技能提示词使用""" return f"当前状态:{self.state}\\n临时数据:{self.tmp}"这个设计有一个值得注意的地方:我刻意把state和tmp分开。state是跨会话稳定的用户属性,比如用户常用城市、默认时区;tmp是本轮任务产生的中间结果,用完就丢。之前我在项目里把两者混在一个字典里,跑了几轮就发现状态污染特别严重,比如上次查天气的city值残留给下一次任务用,导致用户问“明天几点开会”时,系统莫名其妙把“北京”注入到日程技能里。
上下文管理还有一个实践要点:每轮任务结束时,主动清理tmp里的临时键。我通常在一个编排任务链条跑完后,把tmp清空,只保留state。这个操作虽然简单,却能避免一大类因状态残留导致的“幽灵参数”问题。
4.3 技能质量评估:怎么知道这套技能好不好用
很多团队把技能上线后就不管了,等到用户反馈才知道哪里出了问题。但技能和代码一样需要测试、需要迭代。我做技能库的时候,维护了一份“回归测试集”,每改一次技能的提示词或参数结构,就把测试集跑一遍,看看有多少用例能正确触发、正确填参、正确输出。
测试集怎么构建?从真实对话日志里挑,覆盖三类:最常见的请求(高频必测)、最难处理的边界case(低频但容易翻车)、过去失败过的bad case(防回归)。每条测试用例记录四件事:用户输入、期望触发的技能、期望的参数值、期望的输出内容。这套测试不需要自动化到很复杂的程度,能半自动跑起来就比完全靠肉眼强。
还要建立一个评估指标:技能调用成功率。但注意,调用成功不等于任务成功。我见过一个查物流的技能,API调用100%返回200,但用户的问题根本没被解决——因为模型总是把“寄件”相关的问题错误路由到这个查物流技能上。所以我用了两个指标:路由准确率(该用这个技能时用对了)和任务完成质量(输出的信息是否正确、完整、易读)。这两个指标分开统计,才能定位问题出在路由层还是执行层。
5. 常见问题与排查实录
5.1 模型死活不调用技能,怎么办
第一种情况是模型根本不选技能,直接把回答当成闲聊输出。排查方向:第一看候选技能列表是否为空,如果粗筛把正确技能过滤掉了,模型看不到它自然就不会调。第二看技能的description是否写得太笼统,模型理解不了“何时使用”。第三看系统提示词是否给了模型太多“自由发挥”空间,有时候你写“你可以使用工具,也可以直接回答”,模型的默认倾向就是直接回答。
我踩过的一个具体坑是:把技能的description写成了“处理天气相关请求。参数:city、date。”。模型看到这种描述无法建立场景联想,最后很少触发。改成“查询指定城市当天或未来几天的天气情况,适合用户问'今天冷吗'、'要不要带伞'、'周末适合出门吗'等场景”之后,触发率立刻上去了。这不是玄学,是description里“场景词”的作用。模型就是通过描述里的动词和名词来建立匹配的,你给它越具体的场景词汇,它越容易在对话里发现这些词的痕迹。
5.2 参数总是抽错,怎么办
参数抽错是技能开发里出现频率最高的故障。常见表现:日期格式错(用户说“周六”,模型填了“星期六”)、城市名带后缀(用户说“北京”,模型填了“北京市”)、值超出枚举范围(接口只支持“晴天、多云、雨”,模型填了“太阳很大”)。
我处理这类问题的顺序是:第一步,把参数schema改细,给每个参数写清楚格式、示例、可选值;第二步,在系统提示词里加一条“必须严格按照参数定义填充,不要转换格式,不要添加额外文字”;第三步,在技能执行器里加一层参数清洗逻辑,针对已知的常见变形做归一化,比如“北京市”转“北京”,“星期六”转成具体的日期。前三步做完,大部分参数抽取问题能解决。剩下的要么是模型能力上限问题,要么是schema本身定义不清,只能继续调。
另外一个建议:不要追求一次抽全。有些参数用户一开始没说,比如“帮我查天气”没带城市,模型可能填一个“未知”或者干脆瞎猜一个“北京”。这时不要强行猜,应该在技能里设计“参数缺失追问”逻辑:如果必填参数缺失,技能返回一个问题让用户补充。这个流程要在技能定义里显式写明,而不是寄希望于模型自己意识到参数缺失去反问用户。
5.3 技能执行结果的可信度怎么保证
这里我想多说一点,因为AI Agent的“一本正经胡说八道”在技能执行结果里同样存在,只是形式上特别隐蔽。比如天气API返回的status是success,但城市字段是空的,模型生成的自然语言却是“北京今天晴”——这其实是模型根据对话上下文脑补的城市名,不是API返回的。这就是为什么我在校验规则里要求“关键返回字段非空”,不仅校验API返回,还要校验最终生成的用户回复里是否包含真实返回的数据。
更稳妥的做法是:在结果翻译阶段,强制模型只基于API返回的字段组织语言,不允许使用对话历史里的其他信息填充。我在提示词里写过这样一句:“以下是你生成的回复草稿,请检查:所有数据是否都来自天气API返回的字段,如果不是,请删掉无法从返回数据中验证的内容。”实测下来,这句话能显著减少“AI脑补数据”的情况。
如果任务对可信度要求很高,比如涉及支付、下单、日程写入,我建议在流程里加入“用户确认”环节。这个确认不是摆设,而是让用户自己核对参数值和执行结果。虽然多了交互轮次,但换来的是错误成本的大幅下降。相比让用户事后发现日程被写错、订单被下错,与其事后补偿,不如事前确认。
5.4 技能多了以后路由冲突,怎么办
技能库从三个涨到三十个时,一个新的问题出现了:多个技能的description长得越来越像,模型开始选错。比如“天气查询”和“运动建议”两个技能,运动建议也依赖天气数据,两者的description里都出现了“适合跑步吗”“下雨”等词,模型就迷茫了。
解决这个问题的办法有三个层级。第一层:给技能划分优先级。在候选列表中按优先级排序,高优先级的技能描述放在前面,模型通常会倾向选择靠前的选项。第二层:做边界互斥。在描述里明确写“本技能不负责判断天气数据,天气数据请调用query_weather技能获取”。这样模型就能理解两者的分工。第三层:升级路由逻辑。粗筛阶段不再只看关键词,而是加一层“排除规则”,比如“运动建议”技能在用户没提到运动相关动词(跑步、骑行、爬山)时,直接不进入候选。
我特别想强调的是第二层:技能描述里的“反向边界”。很多人写技能描述只写“我擅长什么”,不写“我不负责什么”。当技能库小的时候感受不到差异,库一大,模型经常把能力覆盖范围重叠的技能搞混。加上反向边界之后,路由准确率提升非常明显。这也是我为什么一直强调,技能描述是一段需要反复打磨的“微文案”,而不是随手一贴的说明。
5.5 一个完整的Bad Case排查流程
最后分享一个实际排查过程,帮你理解上面这些方法怎么组合使用。模拟项目X里,用户说“帮我看看周五下午适合跑步吗”,系统没有触发任何技能,直接回复了一段废话。
排查第一步:看粗筛结果。用户输入包含“跑步”和“周五”,create_schedule_event的keywords里有“安排”“日程”但没有“跑步”,query_weather的keywords里有“跑步”吗?如果天气技能的keywords里有“跑步”,那粗筛应该筛出天气技能。检查后发现天气技能的keywords里只有“天气”“下雨”“温度”,没有“跑步”,导致粗筛直接漏掉。这是第一个bug:关键词覆盖不全。
排查第二步:即使粗筛通过,还要看系统提示词里注入的候选技能列表是否正确。如果代码里把候选列表写死成了只有日程技能,那天气技能再相关也不会被选中。检查后发现调度层确实有缓存,候选列表还是上一次任务的结果。这也是一个容易踩的坑:缓存导致技能列表过期。
排查第三步:看模型调用记录。把这一轮请求发给模型的完整prompt拉出来,人工看一遍。发现虽然天气技能在候选列表里,但它的description写的是“查询城市天气”,没有提“评估天气是否适合户外运动”这种场景,模型判断这个请求更偏向“日程/运动规划”,所以没有选它。修完描述,这个case就过了。整个过程说明,排查Agent问题不能只看最终的输出,要看路由的每一层决策。
我知道这些经验看起来琐碎,但Agent开发就是无数个琐碎细节堆出来的。技能库不是写完就完事的静态资产,它需要持续从真实对话里收集反馈、修正描述、补充边界、调整参数。一套能稳定跑起来的技能库,背后往往是几十到上百个bad case的打磨结果。我自己做项目的感觉是:不要害怕一开始技能库又小又粗糙,先把能跑起来的技能挂上去,用真实日志驱动迭代,比在文档里反复脑补各种边界要有效得多。
最后分享一个小技巧:在技能上线初期,给每个技能加一个“触发日志”,记录它被选中的场景、用户的原话、最终是否成功。这个日志是技能迭代最重要的数据资产。等技能库里的每个技能都有了上百条真实触发记录,你对这个技能边界和描述的理解,会比任何文档都清晰。agent-skills的核心,说到底就是让每一个能力都变成一个有数据、可迭代、能进化的独立单元,而不是藏在提示词里的一团模糊逻辑。