如果你最近在折腾大模型应用,肯定绕不开AI Agent这个词。我最早接触Agent的时候也挺懵,感觉这不就是让模型多轮对话吗?直到真正上手做项目才意识到,Agent能不能打,关键不在模型有多大,而在技能机制怎么设计。说白了,技能机制就是给大模型配一个工具箱,让它不再只会“嘴上说”,而是遇到具体任务时自己选扳手、拧螺丝、上电钻,真正把活儿干完。这篇内容就是把这个机制拆开揉碎讲清楚,适合刚接触Agent开发的程序员、做产品方案的同学,以及对大模型玩法感兴趣的爱好者。建议你收藏,不是标题党,而是后面第三部分和第五部分你实际操作时真的会翻回来对照。
1. 先说结论:技能机制到底解决了什么问题
技能机制,英文语境里常叫Skill,也有人叫Tool Calling或Function Calling。名字不同,内核一致:把一组预先定义好的可执行动作注册给大模型,让模型在对话或任务处理过程中,根据用户需求主动挑选合适的动作并发出调用请求。系统收到请求后执行真实逻辑,再把结果反馈给模型,最后由模型组织成自然语言回复用户。
为什么要搞这么一层?因为大模型本质是一个文本预测引擎,再强也只擅长生成文本,它不掌握实时天气,不会真的给你发邮件,也没法修改你硬盘上的文件。它擅长的是理解意图、拆解步骤、组织表达,而真正“动手”的能力必须通过外部技能注入。没有技能机制,用户说“帮我查一下明天的天气”,模型要么承认做不到,要么一本正经地编一个气温给你,这种一本正经的胡说八道在真实产品里是致命的。
用一个生活类比来理解最直观:模型就像一个经验丰富的老师傅,但老师傅不能凭空变出工具。技能机制就是把锤子、螺丝刀、电钻、水平仪全部放进一个工具箱,摆在老师傅面前。任务来了,他自己判断要用什么工具。你说要挂一幅画,他拿起电钻而不是螺丝刀;你说要拧个螺丝,他绝不会扛着电钻硬怼。老师傅还是那个老师傅,但有了工具箱,能干的活瞬间多了几个量级。
这个类比藏着技能机制最核心的设计思想:决策和执行分离。模型负责“用哪个工具”和“怎么描述参数”的决策,技能系统负责“工具内部怎么实现”和“结果怎么保证可靠”。两边各管一摊,互不干扰。这也是为什么技能机制比“把操作逻辑写进提示词”或者“专门微调一个模型学会操作”要靠谱得多。提示词塞操作逻辑,稍复杂一点就乱,模型记不住那么多步骤;微调模型让它学会操作,业务规则一变就要重新训练,成本高不说,还容易把模型原本的语言能力搞退化。
技能机制还不只是解决“能不能动手”的问题,它同时解决了灵活性和可扩展性。今天你想让Agent支持发邮件,写一个邮件技能注册进去;明天想增加日程查询,再写一个日程技能。工具箱里的工具可以不断增补,而模型本身一行代码都不用改。我见过不少团队从零做Agent,第一版往往只想验证“模型能不能自己调函数”,做着做着就会发现,真正决定项目边界的不是模型多聪明,而是你往工具箱里塞了什么,以及每个工具的说明书写得够不够清楚。
2. 核心设计思路:为什么“工具箱”模式靠谱
2.1 从“模型全会”到“模型调度”
很多第一次接触技能机制的开发者,第一反应是:能不能直接把操作能力塞给模型?比如把“发邮件”的流程写进系统提示词,让模型自己模拟执行。早期我也是这么干的,结果很快就发现一个问题:流程短还好,流程一长,模型就开始“自由发挥”,漏步骤、乱排序、编造中间结果,调试起来非常痛苦。
技能机制换了一个完全不同的思路:模型不需要“会”操作,它只需要“会调度”。操作细节封装在技能实现里,模型看到的是一个个带有说明书的功能入口。模型像一个施工现场的指挥,他不需要亲自砌墙,但他必须清楚哪道工序该叫哪个班组进场。指挥得对不对,取决于他手里的工序清单写得好不好。这就是技能描述的价值。
这种拆分带来的直接好处是故障隔离。技能内部逻辑出bug,不会影响模型的调度能力;模型开始乱选技能,也不会破坏技能本身的实现。两边可以各自迭代。我在实际项目里吃过教训:某次把参数校验逻辑和技能业务逻辑写在一块,后来业务规则调整时不小心把校验逻辑也带偏了,结果模型正常发出的请求被批量拒绝,排查了半天才定位到问题。从那以后我坚持技能只负责“执行”,校验、权限、重试这些横切逻辑全部放到调度层统一处理。
2.2 技能、工具、插件、函数到底是什么关系
新手最容易混淆的就是这几个概念。我刚开始也被绕晕过,后来总结了一套自己的理解方式:它们不是同一个维度的东西,不要混着谈。
工具是最底层、最原子的可调用单元,通常对应一个具体的函数或API接口,比如“查询天气接口”“发送邮件的函数”。技能是面向任务的能力封装,它可能包含多个工具,也可能包含一段固定的执行流程和兜底策略,比如“安排出差行程”这个技能,内部可能依次调用差旅查询、审批提交、通知发送三个工具。插件则更多是一种分发和集成形态,负责把若干个技能、配置、依赖打包到一起,方便在不同项目里复用一个能力集合。而函数是编程实现层面的叫法,技能最终落地时总会体现为一个或几个函数。
这么说还是有点抽象,我用表格帮大家把维度拉开:
| 概念 | 粒度 | 核心职责 | 类比 |
|---|---|---|---|
| 工具 | 最小可执行单元 | 完成一个具体动作 | 单个螺丝刀 |
| 技能 | 面向任务的组合能力 | 编排多个工具,完成业务目标 | 一套维修工具箱 |
| 插件 | 打包分发形态 | 集成并交付一组能力 | 按项目定制的整套工具包 |
| 函数 | 代码实现形态 | 承载技能的具体逻辑 | 螺丝刀的具体机械结构 |
实际设计时,我建议不要刻意追求这些概念的标准定义,而是按团队协作方式去切。如果你们有多个Agent要复用同一组天气能力,那把天气相关工具做成独立技能,再打包成插件,供不同Agent安装;如果只有一个Agent,技能粒度可以直接等于工具粒度,少一层抽象反而更好维护。很多花里胡哨的架构是把简单问题复杂化,不是越多层越好。
2.3 一个技能最少包含什么
不管概念怎么绕,一个能被模型使用的技能,落地时至少要包含三个部分:身份信息、执行逻辑、返回处理。
身份信息是给模型看的,包括技能名字、功能描述、参数声明。名字要短、要唯一、要有语义,让模型一眼能定位到它;描述是重中之重,决定模型在什么情况下选择它;参数声明要明确每个字段的类型、是否必填、允许的取值范围。这部分本质上就是一份给模型读的“接口文档”,写得越清楚,模型用错的可能性越低。
执行逻辑是给系统看的,就是真正干活儿的代码。它接收模型传过来的参数,经过校验、鉴权,然后调用真实的服务或API,最后得到一个结果。这里要特别提醒:模型的参数并不可靠,千万不要拿模型生成的参数直接去执行,一定要在调度层做校验,否则用户一句“查一下天气”模型给你传一个不存在的城市名,技能实现里又没有兜底,整个链路就崩了。
返回处理是给下一轮对话看的。技能执行完的结果不能原封不动丢给模型,尤其当结果是一大段JSON、几百行日志或者上万字文档时,直接塞回上下文既浪费token,又会干扰模型理解。我在生产环境里常用的做法是:执行结果先做摘要裁剪,只保留模型需要的关键信息,比如查询结果条数、核心字段、错误码,再返回给模型。模型拿到的是“精简版”,更容易组织出准确的回答。
3. 手把手配一个技能系统
3.1 技能注册表怎么设计
技能系统的地基是注册表。不管用Python dict、JSON文件还是数据库表,核心都是一套统一的技能描述Schema。每次调用模型之前,系统从注册表里挑出当前场景需要的技能,连同请求一起发给模型。
我第一版做技能注册表时很随意,直接散落着各种函数定义,结果技能一多就乱。后来统一改成集中注册,每个技能遵循相同结构,调试和扩展都轻松很多。下面这份JSON是我常用的最小示意:
{ "skills": [ { "name": "query_weather", "description": "查询指定城市在指定日期的天气情况。当用户询问天气、气温、降雨、出行穿衣等话题时,优先考虑调用该技能。", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市中文名" }, "date": { "type": "string", "format": "date", "description": "日期,格式YYYY-MM-DD" }, "unit": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "温度单位" } }, "required": ["city"] } } ] }这份注册表有两个容易被忽略的细节。第一个是description里包含了“什么时候使用”的触发场景提示,而不只是“这个技能是什么”。模型选择技能时靠的是语义匹配,触发场景写得越具体,选对概率越高。第二个是parameters里的每个字段都带了description,尤其像unit这种取值范围有限的字段直接用enum约束。模型生成参数时会参考这些约束,比你在执行逻辑里反复判断要可靠得多。
3.2 描述怎么写,模型才愿意听你的
技能描述是整个机制里性价比最高的优化点。代码逻辑写得再漂亮,描述不清楚,模型就乱选、错选、甚至不选。我总结了一套“四要素描述法”:触发条件、功能边界、参数说明、使用示例。
触发条件要回答“什么情况下用我”。比如查询天气技能,不要只说“查询天气”,要写“当用户询问天气、气温、降雨、湿度、是否需要带伞、出行穿衣建议等话题时”。这等于在给模型划一个明确的语义范围。
功能边界要回答“什么情况别用我”。比如邮件发送技能,要写“仅用于发送邮件,不包含邮件查询、删除、通讯录管理”。模型看到清晰的负面边界,才不容易把相似需求错误路由到你这个技能上。
参数说明要写到字段级,不仅写类型,还要写含义和取值偏好。比如日期字段,直接写“格式必须为YYYY-MM-DD”,比写“日期”两个字好十倍。模型虽然不一定会严格遵守格式,但会给执行层的校验省掉很多麻烦。
使用示例是最容易被忽略的。在描述里直接写“例如用户说‘北京明天会下雨吗’,应调用该技能,city=某城市,date=明天的日期,unit=celsius”,模型会像看例题一样理解你的期望。这比在系统提示词里用一大段话解释“你是一个会使用工具的助手”要高效得多。
3.3 一次任务调度的完整流程
从用户输入到最终回答,一次标准的技能调用流程可以拆成八个环节:接收输入、组装技能列表、调用模型、解析调用意图、校验参数、执行技能、回填结果、生成回复。
我用一段极简伪代码来展示这个循环,实际项目可以在此基础上不断丰富:
def run_agent(user_input, skill_registry, max_steps=5): messages = [{"role": "user", "content": user_input}] for step in range(max_steps): response = call_llm( messages=messages, skills=build_schema_from_registry(skill_registry) ) if response.tool_call is None: return response.content skill_name = response.tool_call.name arguments = response.tool_call.arguments if not validate_skill(skill_registry, skill_name, arguments): messages.append({ "role": "tool", "name": skill_name, "content": "参数校验失败,请检查参数后重新调用" }) continue result = execute_skill(skill_registry, skill_name, arguments) messages.append({ "role": "tool", "name": skill_name, "content": format_tool_result(result) }) return "处理超时,请稍后再试"这段代码里有几个关键点值得展开说。max_steps是必须的,模型偶尔会陷入调工具的死循环,比如查询失败后反复用不同参数重试,设置步数上限能保证整个系统不会无限卡住。参数校验失败时,不要把错误直接抛给用户,而是作为工具返回消息重新给模型,让模型自己调整参数再试一次,这比服务端直接拒绝要智能得多。工具返回结果一定要经过format_tool_result处理,把大段JSON压缩成模型容易理解的一条摘要,不然下一轮模型很容易被无关字段带偏。
3.4 几种常见的技能编排模式
技能系统跑通之后,你就会发现单技能调用只是入门,真实场景更多是多技能组合。我实践中碰到过四种高频编排模式,你可以直接套用。
单技能直接调用适用于需求单一的场景,用户问天气,Agent查天气,回答完事。这种模式最简单,但也是最容易做稳的,第一版项目建议先从这种模式开始。
串行编排适用于上下游依赖的场景,比如“明天适合穿什么”,Agent先调用天气技能拿到气温和降水信息,再把天气结果作为输入调用穿衣建议技能,最后汇总成回答。串行模式要注意把前一个技能的输出精简后再传给下一个技能,避免中间结果太臃肿。
并行编排适用于几个独立信息同时获取的场景,比如“帮我安排周末出行”,Agent同时调用交通查询和酒店查询两个技能,再把结果合并。并行能显著降低延迟,但实现复杂度也上来了,系统里要做结果聚合和冲突消解。我实际试下来,并行不是必须的,除非对响应速度有硬要求,否则先用串行跑通再说。
反思重试模式是兜底神器。技能执行失败时,不要把错误一句话结束对话,而是把错误信息反馈给模型,让模型判断是换参数、换技能还是结束任务。比如用户要查“某城市明天的天气”,城市名传入失败,可以反馈错误后让模型尝试用城市ID或拼音重新查询,很多问题在这一步就能自我修复。
4. 模型怎么决定“用哪个工具”
4.1 函数调用背后的底层逻辑
很多第一次接触技能机制的人会问:模型到底是怎么知道该调用哪个技能的?是不是有什么神秘协议?其实没有,底层逻辑朴素到你可能会失望:模型就是在做格式化文本生成。
我们发送给模型的请求里,包含了一份完整的技能Schema列表,把每个技能的名字、描述、参数结构都告诉它。模型在阅读这些内容之后,推理出当前用户请求更适合哪个技能,然后输出一段符合约定格式的结构化文本,里面写清楚技能名字和参数。调度层看到这段结构化文本,解析出来,再去执行对应的代码。
这个机制和模型做阅读理解题本质上是同一回事。技能描述就是题干,用户请求就是问题,模型要选出正确答案并用规定的格式作答。这也是为什么描述的好坏直接影响选工具的正确率:题干写得不清不楚,再强的模型也不可能选出符合你预期的答案。我见过有人花大量精力调模型参数来提升选工具准确率,效果却一般,后来把技能描述重写了一遍,准确率立刻上来了,这件事给我印象很深。
4.2 参数注入和返回处理的“夹心层”
如果把模型比作大脑,技能比作手脚,那中间还有一个“夹心层”——调度服务,它是整个技能机制真正干活儿的地方。这一层要处理三件脏活累活:参数注入、返回处理、状态管理。
参数注入是指把模型生成的参数转换成技能实际执行需要的格式。模型给出的日期可能是“明天”“后天”这种相对表达,技能执行需要的是“2025-06-18”这种绝对日期;模型给出的温度单位可能是中文“摄氏”,技能接口只认“celsius”。这些转换都必须在调度层完成,不能指望模型自己输出标准格式,也不能指望技能内部兼容各种表达方式。我在设计时习惯把这一层做成可插拔的“参数转换器”,每个技能按需配置,避免把所有转换逻辑堆在一起变成大泥球。
返回处理是夹心层第二个重点。技能返回结果的格式五花八门:有的是数组、有的是嵌套对象、有的是超长文本。全部塞回给模型会让上下文迅速膨胀,导致更昂贵的token成本和更慢的响应速度。我的经验是制定一个统一的“工具结果摘要”规范,规定返回内容最多保留多少字,必须包含核心结论字段,异常时统一输出错误码加错误描述。模型拿到的是一个标准化的“工作汇报”,而不是一堆原始数据。
状态管理是整个夹心层里最容易被忽略的一环。多轮对话中,用户可能连续提出多个需求,Agent可能有多次技能调用,系统必须把调用历史记录下来并一起传给模型,模型才知道“刚才已经查过天气”“现在正在推荐穿衣”。我在生产里直接维护一张执行状态表,记录每一步调用的技能、状态、结果摘要,随对话历史一并参与下一轮生成,效果很稳。
4.3 安全边界必须要划线
技能机制把模型从“只能说话”升级成“能动手做事”,但这也意味着风险等级上了一个台阶。一个会调工具的Agent,如果权限控制没做好,比一个只会聊天的机器人危险得多。
安全边界我总结成四个字:最小暴露。默认情况下,只向模型暴露当前对话真正需要的技能,而不是把整个注册表几百个技能全塞给它。技能越多,模型选错或乱用的概率越大,风险面也越大。比如用户只是问天气,你没必要把“发送邮件”“删除文件”这种高风险技能也挂在模型嘴边。
写操作和涉及资金、隐私的操作必须有额外确认环节。我做了一个“高危技能拦截表”,凡是包含发送、删除、转账、下单这类动作的技能,触发前一律输出一条确认消息给用户,用户明确确认后才会真正执行。虽然多一轮交互,体验略打折扣,但安全永远优先于体验。我还加了一层频率限制和配额控制,防止模型在异常状态下连续高频调用单个技能,把外部系统的接口打爆。
另外,技能返回内容在回传给模型之前,也要过一遍合规过滤,尤其是涉及用户隐私或第三方数据时。这个步骤看起来多此一举,但真出问题的时候,它是保护团队和产品的一道底线。
5. 常见问题与排查技巧实录
5.1 模型死活不调技能
这是新手最常遇到的问题:技能明明注册好了,描述也写了,用户问题也很匹配,但模型就是不用,还一本正经地给出一个编造的回答。我排查这种问题有一套固定顺序。
先看模型返回的原始日志,确认它是不是根本没收到技能列表。很多框架默认只把技能Schema放在系统提示词里,如果系统提示词被其他长文本挤掉或者截断,技能信息就丢了。再看温度参数,温度过高会让模型更倾向于自由发挥而不是遵循结构化的工具调用约束,一般建议在技能调用场景把温度调低。然后是技能列表数量,塞了几十个技能给模型,模型也可能因为选择困难而放弃。最后检查描述本身,是不是“当用户询问天气时使用”写成了“这是一个查询天气的工具”,前者给了触发信号,后者只是自我介绍,效果差别很大。
如果排查完还是不行,最有效的手段是给模型一个标准示范。在对话历史里人为加入一条历史消息:“当用户询问明天天气时,你可以调用query_weather技能,参数为...”。模型看到示例之后,模仿倾向非常强,很多时候一条示例就能解决问题。
5.2 技能调用了但参数总是错
模型愿意调用技能,但传进来的参数惨不忍睹:城市名多了一个字、日期格式不对、单位传了“摄氏度”而接口只认“celsius”。这类问题属于参数契约没对齐。
第一步是在技能Schema里做硬约束。能用enum的别用string,能用format的别自由填,必需字段全部标清楚。模型生成参数时会参考这些约束,虽然不能保证百分百遵守,但遵守率会大幅提升。第二步是在调度层做宽松处理,不要模型参数拿来直接执行,先做归一化转换。比如日期字段,用解析器把“明天”“周五”“2025/06/18”这种格式都转换成标准格式再往下传;单位字段做一个中文和英文的映射。第三步是给必填参数配兜底值,尤其是那些缺失了也能合理执行的字段,用默认值顶上比报错强。我现在做技能设计时都会先问一个问题:用户最不可能填对的是哪个参数?然后针对它做最充分的兜底处理。
5.3 技能多了以后互相干扰
技能从三个涨到三十个之后,一个新的问题出现了:模型开始选错技能。用户问天气,模型去调日历查询;用户问日历,模型去调天气。原因通常是多个技能的描述在语义上存在重叠,模型分辨不出来。
解决思路有两个方向。一是让描述差异化。每个技能描述开头第一句话必须写明“我是谁,我跟谁不一样”,把容易混淆的技能放到一起对比着改,直到它们的触发场景、功能边界互不重叠。二是动态加载。不要一次性把所有技能暴露给模型,而是先做一个“意图预分类”,识别出用户请求属于哪个领域,只加载该领域的几个技能。比如用户提到邮件,就只挂邮件相关技能,天气技能压根不出现在请求里,自然不会选错。
我还在生产里试过另一种办法:把容易混淆的技能合并成一个“总技能”,由总技能内部根据参数路由到具体逻辑。比如天气查询和空气质量查询合并成“天气综合查询”,模型只需要记住一个技能,执行时再根据city和query_type分发。这对模型最友好,但代价是技能实现内部逻辑变复杂,适合在技能数量已经失控时用来止血。
5.4 什么时候真的别用技能机制
不是所有场景都适合上技能机制。我见过一些团队什么功能都想用Agent套一层,结果反而把简单事情搞复杂了,这里帮大家泼几盆冷水。
简单问答和纯知识提取场景不需要技能机制。用户问“什么是区块链”,模型直接回答就行,技能机制反而多了一次模型往返,响应变慢,还可能出现误调用。固定流程业务可以直接写代码。比如需要定时拉取报表并汇总,这种逻辑是确定的,直接写定时任务比让Agent每次决策更可靠、更容易排查。对延迟极度敏感的场景也要慎重。每次技能调用都意味着一次额外的模型请求,链路拉长会带来几百毫秒甚至几秒的延迟,如果产品对响应时间有硬指标,不如把能力封装成普通接口供前端直接调用。
最后一种要警惕的场景是高强度安全管控。如果系统不允许任何未经审核的操作发生,那模型自由决策调用技能本身就是风险点。这种场景应该把决策权从模型手里收回到规则引擎,用流程引擎发起操作,Agent只负责对话和解释,不直接操作核心系统。
6. 技能机制的正确打开方式
做了几个项目之后,我对技能机制最深的体会是:它的难点从来不是写函数,而是定义边界。给模型发一套积木很容易,难的是让模型知道什么情况下用哪块积木、怎么拼、拼错了怎么收场。所以我做技能设计时一直坚持一个习惯:第一版只做三五个技能,先把一条端到端链路跑通,确认模型能在完整流程里正确调用、正确传参、正确应对结果,然后再一点点加技能。每加一个技能之前问自己三个问题:描述跟现有的技能会不会混淆?参数格式用户或模型容易弄错吗?这个技能真的需要模型主动决策调用,还是直接写死在流程里更省事?
最后再分享一个小技巧。技能描述写完之后,不要急着上线,找个AI测试平台反复用不同方式表达同一需求,看看模型是不是每次都能选对技能。比如你写了天气技能的描述,就分别用“明天出门要穿什么”“这个周末适合露营吗”“帮我看看这几天的降雨概率”去问,选错了就改描述,直到把边界磨清楚。这个过程很琐碎,但比上线后被用户吐槽和投诉要舒服太多。技能机制是个一层一层磨出来的系统,每磨一轮,工具箱就会更顺手一点。