1. 先从"使唤人办事"说起:技能机制到底解决什么问题
1.1 为什么单纯聊天不够用
前阵子我帮朋友调试一个AI助手,需求特别朴素:用户说"帮我看看明天杭州适合穿什么衣服",助手要能查到明天的天气,再结合温度给出穿衣建议。一开始我们走的是最常规路线——把所有天气数据一股脑塞进提示词里,让模型自己"理解"。结果效果惨不忍睹:要么模型把"最高温32度"理解成"建议穿羽绒服",要么数据更新不及时,明明明天下雨,它还一本正经地说"适合户外运动"。
问题出在哪?聊天式交互的边界太模糊了。大模型本质上是一个"文本生成器",它擅长的是理解和生成语言,而不是执行确定性的动作。当你让它"查天气"的时候,它并不会真的去调用气象接口,它只是在基于训练数据"猜"一个答案。这种猜测在知识类问题里勉强够用,但在需要实时数据、需要操作外部系统、需要执行确定性流程的场景里,就是灾难。
1.2 技能机制的本质:把大模型变成调度中枢
后来我把方案改成了"技能机制",效果立刻不一样了。所谓技能机制,通俗点说就是:你预先定义好一系列可以被调用的能力模块,每个模块负责一件具体的事,大模型本身不直接干活,它只负责理解用户意图、决定调哪个模块、然后把模块返回的结果组织成自然语言回复给用户。
打个比方。你雇了一个特别聪明的秘书(大模型),但这位秘书不会自己做饭、不会自己开车、也不会自己修电脑。你给秘书配了一个工具箱:里面有微波炉操作手册、地图导航App、维修工的联系方式。用户说"我饿了",秘书不会自己去炒菜,而是翻开微波炉手册、按照步骤热一份饭端上来。技能机制里的"技能",就是这个工具箱里的一件件工具。
这个转变很关键:大模型从"什么都要自己会"变成"只需要知道谁会"。它的定位从"执行者"变成了"调度者"。而每一项技能,就是把大模型的"语言理解能力"和外部世界的"确定性操作"焊接起来的那根焊条。这也是为什么现在几乎所有AI Agent框架都把"技能"或"工具调用"作为最核心的抽象概念——没有技能的Agent只是个聊天机器人,有了技能的Agent才具备"干活"的资格。
2. 技能机制的四层骨架:注册、调度、执行、反馈
2.1 技能注册:让Agent知道你会什么
技能机制的第一步是"注册",也就是把你定义好的能力以结构化的方式告诉大模型。这一步相当于给每个技能办一张"身份证",上面写清楚三件事:这个技能叫什么、它是干什么的、调用它需要提供哪些参数。
我见过不少初学者在这一步偷懒,觉得"反正模型聪明,我口语化描述一下就行了"。这是大忌。技能描述写得好不好,直接决定模型在意图判断时的准确率。比如你写"根据城市名获取当前天气信息,返回温度、湿度、风力等数据",模型基本一眼就能判断该不该调这个技能。但你要是写成"天气这个功能可以给我搞来天气数据,参数就是城市",模型的判断就会开始飘。
更讲究的是参数说明。每个参数不光要有名字和类型,还要把取值范围、格式约束、默认行为写清楚。比如"city"这个参数,你要说明它接受的是中文城市名还是城市ID,是否需要加"市"后缀。这些细节不写清楚,模型填充参数的时候就容易填出各种奇怪的值。
2.2 意图调度:让Agent知道该用哪个
注册完成之后,真正决定技能机制"智商"的是意图调度这一层。当用户说"明天上海下雨的话,我就坐地铁上班",模型需要做的事情是:识别出这句话里有条件判断(下雨)、有城市(上海)、有日期(明天),然后决定需要调用天气查询技能,再根据返回结果决定是否调用"地铁路线规划"技能。
这个过程在实现上通常是"函数调用"机制完成的。大模型会在生成回复之前,先输出一个结构化的"调用请求",里面包含技能名称和参数。你的系统拦截到这个请求,执行对应的技能,再把结果回传给模型。如果你用过市面上主流的Agent开发框架,你会发现它们的核心循环基本就是这个:理解意图 -> 决定调哪个技能 -> 执行 -> 看结果 -> 决定下一步。
这里有个细节很多人会忽略:意图调度不是只做一次。复杂任务往往需要多轮调用。比如用户说"帮我订一间明天能带宠物入住的酒店",Agent可能需要先调用"查询宠物友好酒店"技能,拿到结果后还要调用"比价"技能,最后调用"下单"技能。每一轮调用都是一次"看结果再决策"的过程。
2.3 参数填充与执行:让Agent知道怎么调
调度决策做完之后,就是参数填充和实际执行。参数填充这一环特别有意思,它考验的是大模型对上下文的"信息抽取"能力。用户不会规规矩矩把参数列给你,他可能说的是"帮我看看那边天气怎么样"——"那边"到底是哪儿,模型得结合对话历史里的位置信息去推断。
实际执行环节则需要你在技能内部处理好所有"脏活"。比如查询天气这项技能,内部要拼装HTTP请求、处理超时、解析JSON、处理异常状态码、把原始数据整理成模型容易理解的结构化文本。一个合格技能的逻辑应该是:输入简单干净的参数,输出同样简单干净的中间结果,所有复杂性都封装在技能黑盒内部。
我还想特别强调一下执行结果的"投喂"格式。很多开发者直接把API返回的原始JSON丢给模型,这会导致两个问题:一是字段太多太杂,模型在生成回复时容易抓错重点;二是字段含义不直观,模型可能误解。我自己的习惯是:在技能内部就把数据整理成一段精炼的摘要文本,比如"杭州明日多云,最高32度,最低25度,西南风3级,紫外线较强",再交还给大模型。这样模型只需要把这段摘要翻译成用户友好的回答,几乎不会出错。
2.4 结果反馈:让Agent知道干得怎么样
最后一层是结果反馈,它决定了Agent能不能"从错误中恢复"。现实世界的API调用不可能永远成功:可能是网络超时、可能是参数校验不通过、可能是上游服务返回了意料之外的数据。没有反馈机制的Agent,一旦遇到错误就会陷入"死循环"或者直接摆烂。
正确做法是在技能内部做一次完整的"风险分级",然后把处理过的状态返回给模型。比如:
- 成功:返回正常数据。
- 可重试的错误:比如临时超时,返回"服务暂时不可用,可稍后重试"。
- 不可重试的错误:比如参数格式错误,返回"参数xxx缺失或格式不正确,请引导用户补充信息"。
模型拿到分级反馈后,行为会截然不同:遇到可重试的错误,它会换一种表述再尝试一次;遇到参数问题,它会主动向用户追问澄清。这一层做得好不好,直接决定了用户感知到的"智能感"。
3. 手写一个天气查询技能:从需求到联调的完整链路
3.1 需求定义与边界划定
光讲概念有点虚,我拿一个真实的练手项目来拆一遍。假设我们要做一个"穿衣建议助手",第一个技能就是天气查询。动手之前,先把需求问清楚:
这个技能到底要解决什么问题?答案是"获取指定城市未来一段时间的天气,供上层语义分析使用"。注意,不是"生成穿衣建议"——穿衣建议是后续由大模型根据天气数据再做的加工,不是技能本身的事。
输入是什么?城市名、日期。要不要支持模糊日期(比如"后天")?我建议日期参数由模型去把自然语言转换成标准格式,技能只接受"2025-01-15"这种明确的ISO格式,这样技能内部的逻辑最干净。
输出是什么?我不建议技能直接输出给用户看。它应该输出一份结构化的天气简报给模型,再由模型组织措辞。所以技能返回的字段要覆盖:日期、天气现象、最高最低温度、风力风向、降水概率、紫外线强度、穿衣提醒的原始素材(比如温度体感描述)。
3.2 技能描述与参数Schema编写
接下来定义技能的"身份证"。这里我习惯直接用JSON Schema来定义参数结构,大部分Agent框架都支持这种格式:
{ "name": "get_weather", "description": "查询指定城市在指定日期的天气情况,返回天气现象、温度范围、风力、降水概率、紫外线强度等信息。当用户询问出行、穿衣、户外活动相关问题时,可调用此技能获取实时天气数据。", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "目标城市的中文名称,如'杭州'、'北京'。不带'市'后缀。如果用户说的地名不明确或为'本地''这里'等指代词,需结合对话上下文推断,无法推断时询问用户。" }, "date": { "type": "string", "description": "需要查询的日期,格式为'YYYY-MM-DD',只能是今天或未来7天内的日期。如果用户说'明天''后天'等相对日期,请先换算成绝对日期。" } }, "required": ["city", "date"] } }写这段描述时有两个心得。第一,description里要写明"什么时候该调用",这会大幅提升意图调度的准确率。我特意写了"当用户询问出行、穿衣、户外活动相关问题时",模型就会在类似场景下优先考虑这个技能。第二,参数说明里要明确写出"什么不该做",比如"不带市后缀",就是为了避免模型老老实实把"杭州市"填进去,而你的执行端又得做字符串清洗。
3.3 执行端的封装细节
技能的执行端,我通常封装成一个函数。这里的关键不在业务逻辑,而在容错处理:
import requests import json def get_weather(city: str, date: str) -> dict: """ 执行天气查询技能。 返回给大模型的统一结构:status + message + data """ try: resp = requests.get( "https://example-weather-api.com/query", params={"city": city, "date": date}, timeout=5 ) if resp.status_code != 200: return { "status": "retriable_error", "message": f"天气服务暂时不可用(HTTP {resp.status_code}),请稍后重试", "data": None } raw = resp.json() today_data = raw.get("data", {}) # 这里把原始字段整理成模型易于理解的结构化简报 summary = ( f"{city}在{date}的天气:{today_data['condition']}," f"最高{today_data['temp_max']}度,最低{today_data['temp_min']}度," f"{today_data['wind']}风,降水概率{today_data['precip_prob']}%," f"紫外线{today_data['uv_index']}" ) return { "status": "success", "message": summary, "data": today_data } except requests.Timeout: return { "status": "retriable_error", "message": "天气服务响应超时,可稍后重试", "data": None } except Exception as exc: return { "status": "fatal_error", "message": f"天气查询失败:{str(exc)},请向用户说明并建议稍后再试", "data": None }这段代码看似简单,但里面藏着一个很重要的设计原则:技能永远不要抛异常,永远返回结构化的状态。因为调用你技能的不是Python代码,而是一个大模型——它不能try-except,它只能根据你返回的文本内容来判断下一步怎么走。你返回的状态字段和message,就是它做判断的唯一依据。
3.4 联调与效果验证
技能跑通之后,联调阶段是最能暴露问题的。我当时的验证方法是准备一批"刁钻"测试用例:
- "明天出门要不要带伞?"——考察是否能识别出需要调天气技能,且能把"明天"换算成日期。
- "上海呢?"——结合上文城市追问,考察上下文参数补全。
- "帮我看看大后天三亚的太阳大不大。"——考察口语表达到参数映射。
- "天气这功能怎么老出错?"——考察会不会在该调用的时候不调用。
做完这轮联调你会发现,真正影响体验的往往不是执行端的逻辑,而是模型对技能描述的理解。某个参数描述改一个词,意图命中率可能提升十几个百分点。所以别怕反复调试description,这是技能机制里投入产出比最高的一步。
4. 技能机制和提示词堆砌差在哪:实测对比
4.1 提示词方案的三个死穴
可能有人会想:我不搞什么技能,直接在提示词里写"你是一个天气助手,你能获取天气",再给几个示例,效果不也一样吗?实测下来差别很大。提示词堆砌方案有三个绕不开的死穴。
死穴一:模型会"幻觉"执行结果。没有真正的外部调用,模型在回答天气时本质上是在编。它可能基于训练数据里的气候常识给你一个"看起来合理"的答案,但这个答案和真实天气没有关系。技能机制强制让模型先生成一个调用请求,拿到真实数据后再组织语言,从机制上杜绝了编造实时数据的可能。
死穴二:复杂流程无法控制。提示词里可以写一百条规则教模型"先查天气再查路况再查酒店",但模型输出的随机性决定了它随时可能跳步。技能机制则不同,每一步都是一次确定的函数调用,流程控制权在你手里。
死穴三:可维护性差。每加一个新能力都要去改那坨巨大的提示词,改完还可能影响其他能力。技能机制下,每个技能独立封装、独立测试,加一个新能力和加一个独立的Python模块一样简单。
4.2 技能机制的四个核心优势
用表格对比会更直观:
| 对比维度 | 纯提示词方案 | 技能机制方案 |
|---|---|---|
| 数据实时性 | 依赖模型训练数据,无法保证 | 每次真实调用外部接口 |
| 流程稳定性 | 高随机性,容易跳步 | 确定性强,每步可控 |
| 扩展成本 | 改提示词,牵一发动全身 | 新增独立技能,互不影响 |
| 错误恢复 | 模型"硬编"错误信息 | 结构化状态 + 分级重试策略 |
实际上,这两个方案可以组合使用。大模型的角色设定、语气风格、回复格式这些"软能力"走提示词;和外部世界交互的"硬能力"走技能机制。软硬分离,既保留了模型的灵活表达,又保证了操作的确定性。
我记得第一次把天气技能接入整体系统时,用户反馈特别有意思。之前问"杭州明天穿什么",模型给的答案虽然也是"短袖",但会附带一句"根据历史气候数据推测"。接入技能之后,同样的问法,模型会直接说"明天杭州多云转阴,最高30度,体感闷热,建议穿短袖或薄棉麻衬衫,如果早晚出门最好带件薄外套"。用户能明显感觉到AI"靠谱"了——因为它不再靠猜,而是真的"看"到了明天的天气。
5. 真实项目里踩过的技能设计坑
5.1 参数描述太"文学化",模型反复追问
第一个坑来自一个记事的Agent。我给它写了个"添加待办事项"技能,参数描述用的是"需要被记住的代办事项的详尽内容,要包含用户所有想表达的要点"。结果模型每次执行这个技能,都会把用户一句话里夹带的情绪、背景、未来计划全塞进一个参数字段,然后返回给用户时又啰嗦地复述一遍。
后来我把描述改成"简洁的待办内容,5到15个字,例如'周四下午三点和客户开会'",效果立竿见影。技能参数描述的目标不是让模型发挥创造力,而是让模型填出符合预期的、干净的值。越是简单直接的描述,模型执行得越准。
5.2 技能执行时间过长,整个对话被卡死
第二个坑是同步阻塞问题。我一开始图省事,所有技能都用同步方式调用。结果遇到一个技能要查询三个不同渠道的价格再汇总,整个请求耗时接近15秒。用户在聊天界面等着,体验极差——而且模型在长时间等待中如果超时,会直接放弃,整个对话流程就断了。
后来改成异步模式:技能先返回一个"已提交,处理中"的状态,Agent可以先回复用户"正在查询,请稍等",处理完成后通过回调把结果推进会话。如果你的Agent涉及外部系统耗时操作,一定要提前规划异步回调机制,不要等到上线被用户催了才改。
5.3 技能之间的边界模糊,互相"抢活"
第三个坑出现在技能数量变多以后。当我的Agent里有"查天气""推荐穿衣""规划行程"三个技能时,用户说"明天去北京穿什么",模型有概率去调"规划行程"而不是"查天气"。因为三个技能的description在某些场景下是重叠的。
解决办法有两个方向。一是给技能划分明确的责任边界,在description里直接写"本技能不负责X,X请调用Y技能"。二是设定技能优先级,让框架在意图判断得分相近时,默认选择更具体、更底层的技能,而不是大而全的复合技能。经验是:技能尽量做到"单职责",一个技能只干一件逻辑上不可再分的事。看似"多合一"很方便,实际会大幅提高模型的决策难度。
6. 多技能协同编排:从单兵作战到工具箱联动
6.1 三种编排模式
单个技能解决单个问题,但真实用户需求往往是多个问题的组合。在技能数量多起来之后,你会面临编排问题。我实际用过且验证有效的三种模式:
顺序模式:一个技能的输出是另一个技能执行的依据。比如"查天气"得出了明天有雨,"然后"触发"规划地铁通勤路线"。这是最常用的模式,实现方式是设置技能之间的依赖关系,让模型在拿到前一个结果后再决策是否调用下一个。
并行模式:多个技能之间没有依赖关系,可以同时调用。比如用户问"明天下班后适合去哪个商圈逛街",Agent可以同时调用"查询天气预报"和"获取商圈优惠信息",两个结果一起返回后再组织答案。并行模式能显著缩短复杂任务的响应时间。
条件分支模式:根据技能执行结果走不同路径。典型的例子是"如果明天下雨就推荐室内活动,否则推荐户外活动"。这种模式需要框架支持"结果判断"逻辑,我习惯把判断逻辑写成一个小工具函数,让模型根据工具返回的布尔值来决定下一步调用哪个技能。
6.2 技能粒度怎么定
编排之前必须先解决粒度问题。技能粒度过粗,一个技能包罗万象,模型很难匹配;粒度过细,一个简单的用户需求要编排五六个技能,调用链长、失败概率高。
我总结出一个比较实用的标准:一个技能应该对应一个"用户在达成目标过程中可感知的最小完整动作"。"查询天气"是一个完整动作,"查询天气并推荐穿衣并规划路线"就不是一个动作,而是一个流程。流程应该由Agent在上层去编排,而不是揉进一个技能里。按照这个标准去拆分,你的技能库会有几十个"原子技能",加上几个"流程编排规则",整体结构会非常清晰。
最后分享一个我个人的习惯:每加一个新技能,都会在测试环境跑一轮"边界问答",专门用那些意图不明确的、带指代词、带省略的句子去砸它。技能机制不怕模型"笨",怕的是技能描述写得不到位。很多时候,改一句description的收益,比优化一整天代码都大。这套机制跑顺之后,Agent的扩展方式就从"改代码"变成了"加技能",我自己的开发效率提升了不止一倍。