ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Agent技能体系实战:从函数调用到稳定可控的AI应用

Agent技能体系实战:从函数调用到稳定可控的AI应用 最近好几个做AI产品的朋友跟我聊到一个问题Agent类的应用从demo推到生产环境最难的不是模型选型也不是Prompt调优而是把Agent真正“用起来”的那一层能力体系。大家不约而同提到了一个词——agent-skills。这个词最近在圈子里出现频率很高但它不是某个开源库也不是一个标准协议它更像是一套关于“怎么把Agent能力模块化”的方法论。简单说就是围绕“技能”来做设计怎么定义技能、注册技能、编排技能、评测技能让大模型从“会聊天”变成“会干活”。这篇文章我会从实际落地角度把agent-skills这套思路拆开来讲。你会看到技能体系到底解决了什么问题技能应该如何设计一个可用的技能系统长什么样以及我在真正跑通这套流程时踩过的坑。如果你正在做Agent应用或者刚把一个带工具调用的原型跑通、准备往更稳定的方向迭代这篇文章应该能帮你少走不少弯路。1. Agent技能到底是什么——先搞清楚要解决的痛点1.1 从“会聊天”到“能干活”Agent的能力分层先想一个问题一个裸的大模型能做什么它能理解你的话能生成流畅的回答但让它去查一下明天的天气、订一间会议室、统计一份Excel里的数据它做不到。原因很简单大模型本身没有“行动能力”它只能基于训练数据和上下文做预测。真正让大模型“动起来”的是外挂的工具、API、代码脚本这些执行体。这就引出一个关键问题怎么把“执行体”组织起来给大模型用早期做法很简单把一堆函数塞进Prompt里让模型自己选。比如你定义了好几个函数每个函数写清楚名称、参数、作用然后让模型在需要时返回“我要调用某个函数”。这在Demo阶段没问题工具少场景简单模型选错的概率低。但一旦业务复杂起来工具数量超过十几个Prompt写得越来越长模型开始乱选参数开始填错整个调用链就开始失控。我后来意识到问题不在于模型的智商而在于我们给模型的东西太“平”了。一堆平铺的函数描述就像让一个新员工看一本全是名词解释的手册却不告诉他什么场景下该用哪个、多个工具之间的先后顺序是什么、失败了怎么补救。Agent技能就是干这件事的把工具、流程、规则、异常处理封装成一个相对独立、可复用、可评测的“能力单元”让大模型只需要理解“技能是什么、能解决什么问题”而不需要关心技能内部到底调用了多少个API。1.2 技能和工具、插件、MCP到底有什么区别很多人会问技能和工具Tool、插件Plugin、MCPModel Context Protocol是不是一回事我的理解是它们不在一个维度上。工具是最细粒度的执行单元比如“查询天气”“发送邮件”“计算两数之和”。插件通常是一组相关工具的集合比如一个“邮件插件”可能包含发邮件、收邮件、搜索邮件三个工具。MCP解决的更多是“协议层”的问题它定义了一套标准化的方式让大模型应用可以接入各种外部数据源和工具服务。而技能更多是指“面向某个任务场景的能力封装”它的粒度通常在工具之上、流程之下一个技能可能要调用好几个工具也可能嵌套其他技能还可能包含一系列决策逻辑。举个例子一个“日程管理技能”它内部可能要做解析用户自然语言里的时间、地点、参与人调用日历API创建日程检测冲突如果冲突则按预设规则调整最后向用户确认。这四个步骤拆开看每个都可以是一个工具但把它们组合成一个“技能”对大模型来说就友好得多。模型不需要记住“先调解析API、再调日历API、再调冲突检测API”这个顺序它只需要知道“有一个技能叫创建日程输入是自然语言描述输出是日程创建结果”具体怎么执行是技能内部的事。所以我说技能体系的本质是把复杂度从模型侧转移到工程侧。让模型做它擅长的事——理解意图、做计划、选择技能让工程师做他们擅长的事——把逻辑写稳、把边界定义清楚、把失败处理做好。1.3 为什么技能体系能解决“不可控”的问题做Agent应用最大的痛点是不可控。同样是“帮我订下周三下午三点的会议室”模型可能这次选择了“会议室预订”技能下次莫名其妙地调用了“日程查询”技能。Prompt稍微改一个词输出就全变了。这就是LLM的本性概率输出天然带有随机性。但如果我们把整个调用路径封装成技能模型需要做的决策就从“一堆工具的任意组合”变成了“有限集合里的单项选择”不确定性会被大幅压缩。这也是我看重agent-skills思路的根本原因它不是为了炫技而是为了让Agent应用真正达到可上线、可维护、可评测的水平。这一点在后面讲评测体系时还会展开这里先记住一个观点——技能是一层“人为定义的能力边界”边界越清晰系统越可控。2. 技能设计从任务拆解到技能注册2.1 任务拆解是设计技能的第一步我见过不少人一上来就写代码先把一个“万能工具函数”写完再说。结果就是那个函数里塞了十几个参数、七八个if-else分支既难维护大模型也搞不懂该怎么填参数。做技能设计的第一步一定不是写代码而是做任务拆解。拿一个常见的场景举例做一个会议助手Agent。初看需求可能就一句话——“帮我管理会议”。但如果直接把这做成一个技能你会发现这个技能内部其实揉了很多完全不同的操作创建会议、查询会议、取消会议、查找空闲时间、发送会议邀请、记录会议纪要、会后跟进任务。这些操作有各自不同的输入参数、不同的执行逻辑、不同的失败模式硬放在一起只会让每一个都做不深。合理的拆法是按照“用户意图”来切分。用户说出“帮我安排一个会”这是一个意图说出“我明天有哪些会”这是另一个意图。每一个意图对应一个技能技能内部再决定调用什么工具、走什么流程。拆分的时候有几条原则可以参考职责单一一个技能只解决一类问题边界清晰技能之间不要有重叠的职责比如“查日程”和“建日程”必须分开独立可测每个技能都能单独拿出来测试而不依赖其他技能的运行。当然拆得也不是越细越好。标准是“模型能否稳定决策”。如果一个意图你自己都说不清楚什么时候触发或者两个意图之间的边界连人都分不清那模型更分不清。我个人的实践经验是先用一周时间把所有业务场景列出来挨个写“用户可能怎么说、期望系统做什么”然后聚类每一类就是一个候选技能。这个动作很笨但非常有效。2.2 技能描述与参数Schema——决定模型能否正确调用技能拆好了下一步是把每个技能注册到系统里。注册的核心信息除了技能名称最重要的是两件事技能描述和参数Schema。这两个东西直接决定了大模型能不能在合适的时候选对这个技能、并且把参数填对。技能描述这件事很多人会忽视以为随便写一句“创建会议”就行了。但实际上描述写得好不好对调用准确率的影响非常巨大。我的建议是描述要包含四个要素技能能解决什么问题、在什么场景下使用、输入是什么、输出是什么。举个例子“创建会议技能”的描述可以写成当用户希望安排一场新的会议时使用支持指定会议主题、时间、参与人、地点输入为用户的自然语言描述输出为会议创建结果包括时间、参与人确认信息。这样的描述就像一个“使用说明”大模型读到之后能够更准确地判断这个技能是否匹配当前用户意图。参数Schema同样关键。这里强烈建议使用JSON Schema格式而不是简单地在描述里写“参数一时间参数二地点”。JSON Schema的好处在于它可以用机器可读的方式定义每个字段的类型、是否必填、取值范围大模型在Function Calling模式下可以更稳定地生成符合规范的参数。比如时间字段你可以在Schema里明确格式为“YYYY-MM-DD HH:mm:ss”并注明“如果用户说‘下周三下午三点’请转换成具体时间再填入”这样就能少很多时间格式解析的麻烦。还有一个细节必填参数尽量少。每多一个必填参数模型填错的概率就高一分。能用默认值解决的就不设必填能自动推断的就不让模型硬编。比如创建会议地点可能默认是“待定”时间如果用户没说可以先留空再通过追问补齐。给系统留出容错空间比逼着模型一次把所有参数猜对要现实得多。2.3 技能编排复杂任务需要多个技能协作单技能能解决简单问题但现实场景很少有“一个意图到底”的情况。比如用户说“帮我安排一场和客户的会顺便把会议邀请发到群里”这里面至少涉及创建会议、发送通知两个技能。所以技能系统必须有一套编排机制决定技能之间的调用顺序和协作方式。编排方案大体分两种。第一种是把编排交给模型让模型在对话中自主决定先调哪个技能、再调哪个技能。优点是灵活适合开放域场景缺点是流程不稳定模型可能跳步、漏步甚至反复调用同一个技能。第二种是把编排写死在代码里做成固定的流程模板。比如“创建会议后必须发送邀请”这一步直接写在技能内部逻辑里不依赖模型决策。优点是稳定可控缺点是灵活度差新流程需要开发介入。我现在的做法是两者结合主干流程用代码固化分支决策交给模型。比如“创建会议”技能内部一定会执行“解析信息→创建日程→检测冲突→确认结果”这个顺序不能让模型来定但要不要顺便“通知参与人”可以作为一个可选步骤由模型根据用户语气和上下文决定。这样既保证了核心链路不跑偏又保留了应对复杂需求的灵活性。3. 实操从零搭建一个Agent技能系统3.1 技术选型不要一上来就上框架聊完设计理念下面进入实操。先说说技术选型。如果你是要快速验证技能体系是否可行我建议不要一上来就上一个重量级框架。直接用你最熟的开发语言配合大模型的Function Calling能力先把一个最小的闭环跑通这个闭环包括技能注册、技能发现、技能调用、结果解析。我用的是Python原因很朴素大模型生态里Python的支持最好不管是OpenAI SDK还是其他模型平台都是Python优先。另外一个建议是如果你只是想验证思路先用假数据、假工具把流程跑通不要一上来就接真实的外部API。这样能排除很多网络、鉴权、接口波动带来的干扰因素快速验证“模型能不能在给定技能集里做对选择”。3.2 核心代码技能注册与调用的最小实现下面给一个极简的Python实现完整演示一个技能系统的最小闭环。这个示例包括两部分技能注册表和调用分发器。from dataclasses import dataclass, field from typing import Any, Callable import json dataclass class Skill: name: str description: str parameters_schema: dict handler: Callable[..., Any] class SkillRegistry: def __init__(self): self._skills {} def register(self, skill: Skill): self._skills[skill.name] skill print(f[技能注册] {skill.name} 已注册) def get_skill(self, name: str): return self._skills.get(name) def list_skills(self): for name, skill in self._skills.items(): print(f- {name}: {skill.description}) def to_function_list(self): 将技能列表转换为Function Calling需要的JSON格式 functions [] for skill in self._skills.values(): functions.append({ type: function, function: { name: skill.name, description: skill.description, parameters: skill.parameters_schema, } }) return functions接着定义两个简单的技能查天气和创建日程。关键是handler函数里要处理真实业务逻辑这里为了演示直接返回假数据。def handle_weather(city: str, date: str) - str: # 这里在实际项目中会调用天气API return f{city}在{date}的天气晴气温18~25℃ def handle_create_event(summary: str, start_time: str, attendees: list[str]) - str: # 这里在实际项目中会调用日历API return ( f会议创建成功{summary}开始时间{start_time} f参与人{, .join(attendees)} ) def build_registry() - SkillRegistry: registry SkillRegistry() registry.register(Skill( namequery_weather, description( 当用户询问某个城市在某一天的天气情况时使用。 例如北京明天天气怎么样 ), parameters_schema{ type: object, properties: { city: {type: string, description: 城市名例如北京}, date: {type: string, description: 日期格式YYYY-MM-DD如果用户说明天请自行换算} }, required: [city, date] }, handlerhandle_weather, )) registry.register(Skill( namecreate_event, description( 当用户希望创建一场会议、约会、日程时使用。 例如帮我约下周一上午10点和张三开会。 ), parameters_schema{ type: object, properties: { summary: {type: string, description: 会议主题}, start_time: {type: string, description: 开始时间格式YYYY-MM-DD HH:mm:ss}, attendees: {type: array, items: {type: string}, description: 参与人列表} }, required: [summary, start_time] }, handlerhandle_create_event, )) return registry接下来是核心的“Agent循环”拿到用户输入后把技能列表和用户问题一起发给大模型让模型返回结构化输出再根据输出执行对应技能。def run_agent(user_input: str, registry: SkillRegistry, llm_func): llm_func: 你的大模型调用函数输入为 messages 和 tools 返回为 model_response 和 tool_calls messages [ {role: system, content: 你是一个智能助手请根据用户问题调用合适的技能。}, {role: user, content: user_input}, ] functions registry.to_function_list() response, tool_calls llm_func(messages, functions) if not tool_calls: return response results [] for call in tool_calls: skill_name call[function][name] arguments json.loads(call[function][arguments]) skill registry.get_skill(skill_name) if not skill: results.append(f未找到技能: {skill_name}) continue print(f[调用技能] {skill_name}, 参数: {arguments}) result skill.handler(**arguments) results.append(result) return \n.join(results)# 模拟一个LLM调用函数实际使用时请替换为真实的大模型API def fake_llm(messages, functions): # 这里模拟模型返回调用 create_event 技能 tool_calls [ { function: { name: create_event, arguments: json.dumps({ summary: 产品评审会, start_time: 2025-03-10 10:00:00, attendees: [张三, 李四] }) } } ] return 将调用技能, tool_calls if __name__ __main__: registry build_registry() print( 已注册技能 ) registry.list_skills() print() output run_agent(帮我安排周一上午10点和张三、李四开产品评审会, registry, fake_llm) print(fAgent最终输出\n{output})运行这段代码你会看到Agent成功识别出需要调用create_event技能并把提取出的参数传给对应的handler函数最终返回“会议创建成功”的结果。这就是一个技能系统的最小闭环注册技能模型做意图识别和参数抽取系统执行技能并返回结果。3.3 大模型接入从伪代码到真实Function Calling上面示例里的fake_llm只是为了展示流程真实现场要用大模型API替换。以OpenAI为例Function Calling的调用方式大致如下from openai import OpenAI client OpenAI() def real_llm(messages, functions): resp client.chat.completions.create( modelgpt-4o-mini, messagesmessages, toolsfunctions, tool_choiceauto, ) message resp.choices[0].message tool_calls [] if message.tool_calls: for tc in message.tool_calls: tool_calls.append({ function: { name: tc.function.name, arguments: tc.function.arguments, } }) return message.content, tool_calls国内的一些大模型平台也都有类似的Function Calling接口用法大同小异无非是字段名略有差异。接入真实模型后你会发现前面技能描述写得好不好立刻就能体现出来。描述含糊的技能模型大概率不会选Schema设计不合理的模型返回的参数经常对不上。所以不要急着怪模型不够聪明先回头检查自己的技能定义。4. 实测中踩过的坑技能调用的稳定性问题4.1 模型不按Schema出参怎么办这是我在实践中遇到最多的问题。明明Schema里写了start_time的格式是“YYYY-MM-DD HH:mm:ss”模型硬是返回“下周一上午10点”这种自然语言或者把日期格式写成“2025/3/10”。这类问题的根源一方面是因为模型在看用户输入时被原文带偏了另一方面是因为Schema里的description写得太“技术化”没有告诉模型“用户说相对时间时要先换算成绝对时间”。解决方案有两个层次。第一层是在描述里写清楚比如你可以在description里追加一句“如果用户输入的是相对时间如今天、明天、下周一请先换算成具体的日期时间字符串”。这能解决大部分问题。第二层是引入一层“参数清洗”机制在技能被执行前加一个校验环节。校验不通过就自动触发一次“参数修正”调用让模型对照Schema把错误的参数重新整理一遍。虽然多一次调用但对稳定性的提升非常明显。4.2 技能数量变多后模型开始“选错技能”技能从5个增加到20个之后一个新的问题出现了模型经常选错技能。典型的场景是“查天气”和“查空气质量”两个技能功能相近模型拿不准该用哪个或者用户问“明天开会要穿什么”模型没有选择任何技能而是自己“编”了一个回答。这个问题我摸索了很久最后发现最有效的做法是给技能描述加上“正例”和“负例”。也就是在描述里明确写出“适用于哪种场景不适用于哪种场景”。比如“查天气”描述里可以加一句“本技能只用于查询天气不负责穿衣建议穿衣建议请用穿搭推荐技能”。这么做的逻辑在于大模型做意图判断本质上是在做语义匹配你的描述里提供了越多的对比信号匹配就越准。另外一个有效手段是“前置意图分类”。在技能数量变大时先让模型用一次轻量调用从几个技能类别中选出候选集合比如先判断“用户的问题是日程类、天气类还是邮件类”然后再在候选集合内精确选择技能。两次决策的粒度都变细了准确率自然会提高。4.3 多技能协作时的状态管理问题当Agent的对话变长、多次调用技能之后状态管理就成了一个隐形杀手。比如用户先说“帮我查一下明天的天气”Agent调了查天气技能得到“明天有雨”的结果接着用户又说“那明天开会记得带伞”这时候前面的天气结果已经不在上下文重点里了Agent可能会忘记自己刚才查过天气。更麻烦的是如果多个技能之间需要共享中间结果比如“创建日程”需要“查询空闲时段”的输出状态没有管理好整个流程就断了。我的经验是不要把状态都丢给大模型的上下文窗口而是要在系统层面做显式管理。最简单的做法是维护一个“全局变量区”每次技能执行完把结果的关键信息存进去每次模型决策前把相关的历史摘要注入Prompt。比如查完天气后在记忆区里存一条“用户关注明日天气有雨”下一次模型决策时能看到这条摘要就不会忘记上下文了。4.4 评测体系怎么证明技能在变好很多团队做Agent开发时最缺的是一套可量化的评测体系。Prompt改了是变好了还是变坏了不能靠感觉要靠数据。我的做法是为每个技能准备一个测试集包含三类样本正向样本也就是应该调用该技能的用户问法反向样本也就是不应该调用该技能的相似问法边界样本也就是语义模糊、需要特别判断的问法。每次修改技能定义或Prompt后跑一遍测试集记录三个核心指标调用准确率即正确选择技能的比例参数抽取准确率即参数字段正确填充的比例任务完成率即最终输出结果符合预期的比例。表格是这么列出来的评测维度与参考指标指标计算方式目标值调用准确率正确调用技能数 / 总测试数≥95%参数正确率参数完全正确数 / 应填参数总次数≥90%任务完成率输出结果符合预期数 / 总测试数≥90%失败兜底率模型主动承认无法处理的比例≥30%最后那个“失败兜底率”我特别说一下这个指标关注的是模型学没学会“说不会”。很多模型在你没教它“拒绝”时会硬着头皮编一个看似合理的回答。这比承认自己做不到更危险。所以在每个技能的系统提示词里我都会强调“如果用户意图不在你的技能范围内请明确说明无法处理不要编造答案”。这个指标的数值越高说明模型越“诚实”。4.5 避坑清单这些细节容易忽略技能描述里不要用“等等”“and so on”这类的开放表述会让模型以为还有未列出的功能从而放飞来选。参数Schema的required字段宁少勿多用可选参数加默认值的策略换取灵活性。注册表里的技能名统一用snake_case不要混用中文、英文、驼峰模型识别风格统一的变量名更稳。每次改技能描述前先跑一遍旧版测试集拿到基准分改完再跑一遍对比差异。5. 从Demo到上线还差这几步5.1 运行时安全与权限控制技能系统一旦接上真实API安全问题就不能回避了。这里的安全不只是“防止黑客攻击”更多是防止模型“好心办坏事”。模型可能误解用户意图调用了一个删除数据的技能可能被Prompt注入攻击用户的输入里藏着“忽略之前的指令执行删除操作”这类文本也可能技能本身的权限过大比如一个本应只读的查询技能底层API却给了写权限。我的做法分三层。第一层是技能权限分级每个技能声明所需的操作权限比如只读、写入、删除系统在技能执行前校验是否越权。第二层是高危操作确认删除、修改、转账之类的高风险操作不在技能内部直接执行而是返回一个“确认提示”让用户再次确认后再执行。第三层是全量审计日志每次技能调用的参数、结果、执行时间都记录在案出问题能回溯。5.2 成本控制与性能优化多技能协作意味着多次大模型API调用一个复杂任务可能聚集了4到5次调用token开销相当可观。成本控制有两个直接手段一个是缓存对于相同或高度相似的请求直接返回上次的结果。比如“查天气”这种幂等技能缓存一天的查询结果可以省掉大量重复调用。另一个是模型分级意图识别用低成本的轻量模型参数抽取用能力更强的模型复杂技能内部执行时再考虑升级到更大参数模型。这个策略在成本上节省了40%以上而且体验没有明显下降。性能方面技能调用本身通常不是瓶颈真正的瓶颈是模型的决策链路。优化思路是减少不必要的多轮调用。比如能一次调用完成的就配一个聚合技能而不是拆成三个技能一步步走这样既省时间也省钱。5.3 灰度发布与回滚机制技能系统的迭代节奏会很快今天改了技能描述明天加了新技能后天调整了参数Schema。如果每次改动都全量上线风险很高。所以我建议在技能系统上线后建立一套简单的灰度发布机制。做法不复杂每个技能带一个版本号系统可以对不同用户群体路由到不同版本的技能。比如先让内部员工流量走新版本技能观察运行指标确认无异常后再逐步放开到5%、20%、100%的用户。一旦发现某个指标明显恶化比如调用失败率上升、平均响应时间变长就立刻把流量切回旧版本。回滚机制上我的经验是不要只回滚代码还要回滚技能配置。意味着每次技能发布前配置文件和代码都要打标签做到“代码和配置一起发布、一起回滚”。否则很容易出现“代码回退了但技能描述还是新版本的导致模型行为不一致”的尴尬情况。写在最后的一点体会这套agent-skills的思路我前前后后折腾了好几个月最大的感受是Agent应用能不能稳定并不完全取决于模型的聪明程度更多取决于你在模型周围搭建的这套“能力骨架”的质量。技能拆分得清不清晰、描述写得准不准确、评测体系完不完善直接决定了这个系统的天花板。如果让我给刚起步的团队一个建议就是别急着追求技能数量先把一两个技能做到极致——调用准确率稳定在95%以上、异常场景都能兜住、评测集覆盖足够多再往外扩。技能体系这个东西宁可少而精不要多而糙。每多一个技能模型的决策空间就大一分出错的概率也跟着涨一分。把每一层细节打磨到可控Agent才能在真实场景里真正扛得住事。
返回列表