资讯动态

Agent Skills实战:从ChatBot到会办事的智能体

发布时间:2026/10/21 23:28:21 来源:尧图企业网站定制
这两年做 AI 应用被问得最多的一个问题就是Agent 和普通的 ChatBot 到底差在哪我的答案一直是同一个——差在agent-skills上。同样是接了大模型接口ChatBot 只会“说话”Agent 却会“办事”。而让 Agent 从“会说话”进化到“会办事”的关键就是给它配上一整套可调用、可编排、可兜底的技能体系。这篇博文不聊虚的直接讲讲我在实际项目里怎么设计、实现、测试一套 agent-skills踩过哪些坑以及哪些经验是文档里不会写但你必须知道的。这篇文章适合谁如果你正在做 AI 应用开发想让模型具备调用工具、操作数据、执行业务逻辑的能力或者你已经在用函数调用但总觉得模型“不听话”、调错工具、传错参数那这篇文章就是给你准备的。我会从概念讲清楚再给出一套能直接落地的实操方案。1. Agent Skills 到底是什么先搞懂概念边界1.1 从“会说话”到“会办事”先讲个最简单的类比。你招了一个实习生第一天他能跟你聊行业趋势聊得头头是道但你说“帮我把这个季度的报表整理一下”他懵了——因为他只会“说”不会“做”。Agent 也一样大模型本身只是一个“语言大脑”你问它什么它都能接上话但你要它查数据库、调接口、发通知、算参数它没有手没有脚干不了。Agent Skills 就是给这个“语言大脑”装上的一套“手脚”。每个 Skill 代表一个具体可执行的能力单元比如“查询订单状态”“计算运费”“生成周报”等。模型在理解用户意图后会选择合适的 Skill 来执行然后把执行结果结合上下文组织成最终回复。这一套机制把大模型从“聊天机器”升级成了“能干活的工作流引擎”。我在一个实际项目中的体会是同样的模型、同样的提示词有没有 Skills 体系用户感知完全是两个档次的东西。没有 Skills 的时候用户问“帮我查一下上个月销售额”它只会告诉你“我无法访问您的数据建议您登录后台查看”有了 Skills它真的能查出来再附带一句“和上个月相比下降了 12%”。这个感知差异比任何炫酷的 UI 都管用。1.2 Skill、Tool、Function 到底有什么区别很多人会把 Skill、Tool、Function 三个词混着用其实它们是有层级的。搞不清楚这个后面设计的时候就会乱。Function是最底层的就是一段可调用的函数代码负责具体的执行逻辑。Tool是一个通用说法泛指模型可以调用的外部能力包括函数、API、知识库检索等。Skill是更高维度的封装它不仅包含 Function还包括模型的调用描述、参数 Schema、输入输出约束、错误的兜底逻辑甚至包含示例。用一个比喻Function 是一把螺丝刀Tool 是你工具箱里的任何工具Skill 则是一本“工具使用说明书操作规范”。模型不看螺丝刀内部是怎么造的它只看说明书然后决定“这个场景该用哪把工具、怎么用、用完怎么反馈”。如果你只是做一次性的简单调用定义个 Function 就够了。但当你面对的是一个有多个业务环节的真实项目你需要的是 Skill 体系——它让“模型该在什么情况下调用什么工具”这件事变得可控、可持续维护、可测试。很多项目死在“函数调用一多就乱套”上就是因为没有这一层设计。1.3 不是所有场景都需要 Skills也别一听 Skills 就上头有些项目压根用不上。判断标准就一句话你的应用是不是需要模型去操作真实的外部系统如果只是做纯文本问答、内容生成、角色扮演这种不需要 Skills硬加反而画蛇添足——模型会因为工具描述的干扰影响本身的语言表现。需要上 Skills 的典型场景包括查询业务数据、执行写操作下单、更新记录、调用第三方 API、多步骤任务编排、需要感知实时状态等。判断的时候还有一个实用标准如果这个流程中用户必须切到别的系统才能完成目标那它就值得做进 Skill 里。我自己见过不少失败的案例团队一听说 Agent 火立刻给每个对话都塞上十几个工具结果模型在无关场景下频繁误调用工具用户基础体验反而一塌糊涂。Skills 的正确用法是“该出手时才出手”而不是“把所有工具都摆到台面上”。2. 为什么“会写工具”不等于“会用工具”2.1 模型是“近视眼”它看不到你的代码我在指导团队时最常说的一句话就是模型是近视眼它眼中的工具只是几句话和一个 JSON 结构。你的函数写得再漂亮注释再详尽模型根本看不到。它只看到你在接口层描述的那几行字——工具名称、一段说明文字、参数表。这就导致一个非常普遍的坑开发人员花了大量精力写业务代码但给模型“看”的描述是随便写的。结果模型不知道这个工具是干嘛的、什么时候该用、参数怎么填于是乱调用、瞎传参最后大家还以为是模型智商不行。其实问题出在“接口可见层”太潦草。我后来定了一条铁律给模型写的工具描述要当作给一个聪明但没经验的同事写交接文档来对待。你得说清楚这个工具是干嘛的什么情况下用什么情况下不要用有什么坑。比如你写一个“计算运费”的函数不能只写“计算运费”要写清楚“根据收货地址和包裹重量计算快递运费仅支持国内地址偏远地区需额外加收附加费包裹超 30kg 时不可使用此工具”。这些信息才是模型做决策的真正依据。2.2 描述里藏着模型的所有“决策信息”模型选择工具的决策依据是什么就是工具描述和参数说明。这就像你在搜索引擎里搜东西——网页内容再好标题和摘要写得不明不白你也不会点进去。同样的函数实现得再稳描述写得含糊模型就不知道这个入口通向哪里。实际操作中我建议把每个 Skill 的描述都按固定的结构写是什么一句话说明、适用场景什么时候用、排除场景什么时候不用、注意事项边界和限制、示例可选。你可以理解为给模型准备一份“决策树”只不过这份决策树得用自然语言写。举个例子一个“获取用户订单”的 Skill有的团队这么写“获取订单信息。”模型看了等于没看——它是获取当前用户的订单还是按订单号查要不要权限查不到怎么办你把这些写在描述里模型才能把用户的问题和工具精准匹配上。我实际测试过同样的工具描述从一句话扩成四句话调用准确率能提升 20% 以上这还只是保守数字。2.3 参数 Schema 越宽松模型越会给你“编”另一个常见坑是参数 Schema 设置得过宽。模型在不确定参数值的时候会基于上下文“猜”一个值填进去专业说法叫“参数幻觉”。比如你只声明了参数是字符串没有写格式模型就可能传一个超出预期的值——传了不存在的订单号传了一个不支持的货币单位甚至传了带有特殊符号的文本直接把后面的代码搞崩。解决思路不是靠模型自觉而是从 Schema 层面卡死。能用枚举就用枚举能限定格式就限定格式能标必填就标必填。JSON Schema 里那些enum、format、pattern、minimum、maximum、required不是给前端表单用的同样是给模型用的紧箍咒。模型在生成参数时会尽量遵循约束约束越明确幻觉率越低。我自己的习惯是string 加上enum或正则number 加上 min/maxobject 只开放必要的字段additionalProperties一律设为false。这些细节单独看都不起眼合在一起能省掉大量后面的解析和校验代码。有一句话总结得很好你给模型多大的自由度就得准备多大的容错机制。3. 手把手设计一个 Skill从需求到上线3.1 需求拆解从用户问题反推能力点设计 Skill 的第一步不是写代码而是拆需求。我惯用的方法是列出一批真实的用户问题然后反推“要回答这个问题模型需要具备什么能力”。举个例子假设你要做一个“智能日程助手”。先定义一批用户问题“帮我看看明天下午三点有没有空。”“给张三发个会议邀请时间定在下周二上午十点。”“把和李四的会改到周四。”“下周有哪些需要准备的会议”从这些问题可以反推出至少四个能力点查询日程、创建日程带着参会人信息、修改日程、汇总日程列表。每个能力点就是一个候选的 Skill。别一上来就拍脑袋定义工具先让需求说话。这里还有个容易被忽略的问题同样是“日程”查空闲和查日程明细其实是两个不同的操作你可以合并成一个工具在参数里区分也可以拆成两个工具。我的建议是优先拆开——Skill 粒度越小模型的决策就越简单。一个工具只做一件事描述也更好写测试也更好覆盖。3.2 定义元数据与参数 Schema在代码层面一个 Skill 的注册信息通常长这样。以“查询日程”为例skill_meta { name: query_schedule, description: 查询用户在指定时间段内的日程安排。 适合回答我某天有没有空某时间段安排了什么这类问题。 当用户没有明确指定时间段时默认查询当天。 本工具只做查询不做创建或修改 如需创建或修改日程请使用 create_schedule / update_schedule。, parameters: { type: object, properties: { date: { type: string, description: 查询日期格式 YYYY-MM-DD 例如 2025-01-15。不传则默认当天。 }, start_time: { type: string, description: 可选查询起始时间24小时制 HH:MM例如 14:00。 }, end_time: { type: string, description: 可选查询结束时间24小时制 HH:MM例如 18:00。 } }, additionalProperties: False } }里面每一项配置都有它的意义描述里的“只做查询不做创建或修改”是在给模型划边界参数上的日期格式说明是在引导模型生成合法值additionalProperties: False是拒绝一切不在 Schema 里的多余字段。这些细节全是实战中填过的坑换来的。3.3 实现执行函数与异常处理元数据定义完后实现函数本身。这里有个非常关键的点函数要假设模型会传任何值做好参数校验和异常处理。模型可以遵守约定传一个合法的日期也可能抽风传一个“昨天”或“下周五”这时候不能直接崩。def query_schedule(date: str None, start_time: str None, end_time: str None): # 参数兜底 if date is None: date datetime.now().strftime(%Y-%m-%d) # 格式校验 try: datetime.strptime(date, %Y-%m-%d) except ValueError: return {error: invalid_date, message: f日期格式应为 YYYY-MM-DD收到{date}} # 正常逻辑查询数据库或调用日历 API events calendar_service.query(startdate, start_timestart_time, end_timeend_time) if not events: return {message: 该时间段暂无日程安排, events: []} return {events: [{title: e.title, start: e.start.isoformat(), end: e.end.isoformat()} for e in events]}注意这里我做了两层防护一是默认值兜底二是格式校验并返回结构化错误信息。为什么要返回结构化错误而不是抛异常因为模型能读懂返回值——你告诉它“日期格式错了”它能自己修正后重新调用你直接抛异常模型只能一脸懵地告诉用户“系统出错了”。3.4 把 Skill 注入到模型请求里到这一步Skill 已经定义好了但模型还不知道它的存在。你需要把工具列表注入到每次模型请求的上下文中。目前主流模型服务的函数调用机制都有对应的接口我简化说明流程构造对话消息列表用户问题 历史上下文。在请求参数中加入工具列表即上面定义的 skill_meta。模型根据对话内容决策要么返回普通文本回复要么返回一个“工具调用请求”和参数。你的代码检测到工具调用请求后执行对应的函数把结果追加为一条新的消息。再把“函数结果消息”发回给模型让它基于结果组织最终回复。伪代码大致长这样def run_agent_with_skills(user_query: str, history: list, skills: list): messages history [{role: user, content: user_query}] while True: resp llm_client.chat_completion( messagesmessages, toolsskills # 注入技能列表 ) msg resp.choices[0].message if msg.tool_calls: for call in msg.tool_calls: result execute_skill(call.function.name, call.function.arguments) messages.append({ role: tool, tool_call_id: call.id, content: json.dumps(result, ensure_asciiFalse) }) continue else: return msg.content这里有三个实操要点。第一循环控制在 5 轮以内防止模型在工具链里死循环。第二工具执行结果要转成 JSON 字符串再回传给模型保持结构清晰。第三每轮决策都要保留完整消息历史模型才能理解前一步做了什么。4. 多 Skills 编排技能越多越要讲规矩4.1 技能多了模型就会“乱点鸳鸯谱”单 Skill 的时候一切好说模型没得选。但当你把 5 个、10 个甚至 20 个 Skill 摆到模型面前时问题就来了。我见过一个真实场景某团队做客服系统接了十几个工具模型经常把“查订单状态”和“查物流信息”搞混用户问“我的货到哪了”模型调了订单查询返回一堆数字答非所问。这就是典型的技能编排失控。模型本质上是一个基于概率的匹配器当两个 Skill 的功能描述高度重叠时它的选择就成了一场赌博。你得从设计上降低它的选择难度。我的解决方案有三个一是让 Skill 职责单一且边界清晰二是统一命名和描述风格让模型能快速识别差异三是场景化分组如果有必要根据用户意图类型动态决定注入哪些工具而不是每次全量注入。4.2 统一的命名规范与描述模板技能越多命名规范就越重要。我推荐一套固定的命名风格动词开头 业务对象。比如create_order、cancel_order、query_order_status、query_product_stock。别用那种含糊的名字比如do_sth、handle_data模型根本拆不出含义。描述模板我固定为四个部分定义句、触发场景、排除场景、备注。举一个例子定义句查询用户当前账户余额及最近十笔交易明细。 触发场景用户询问我还有多少钱账户余额最近花了哪些钱等。 排除场景不处理转账、不处理账单分析这些问题请找相应工具。 备注返回的金额单位为分展示给用户前需转换为元。为什么要把“金额单位是分”这种细节都写在说明里因为模型的最终回复要基于工具返回值组织语言如果返回的金额单位不明确模型可能直接把“123456”读成“十二万三千四百五十六”还是“一千二百三十四块五毛六”它没法判断。把单位写清楚它才能给出正确的回复。4.3 冲突消解与降级策略技能之间难免有交叉。比如“查询订单”和“查询全部订单列表”功能相近“修改收货地址”和“新增收货地址”边界有重叠。遇到这种情况我的经验是宁可让两个技能边界有少量重叠也不要让一个技能啥都干。重叠带来的是误调用概率上升但可以通过描述中的“排除场景”来约束。“啥都干”带来的是模型决策不明确问题更大。另一个必须考虑的是技能执行失败后的降级。例如查询库存失败时你要给模型准备一个“转换为常见问题回复”的兜底比如修改日程失败时要允许模型退化成“只读查询”模式先把现有日程列出来让用户确认。没有降级策略的 Agent线上跑起来就是个定时炸弹——一个接口抖动整条对话就废了。我给每个 Skill 都配一个fallback字段标记这个 Skill 失败后模型应该尝试哪个替代 Skill或者直接返回什么样的人性化提示。这个信息同样注入到工具描述里模型在第一步失败时能自行降级。4.4 上下文管理工具描述也是要花 Token 的工具列表越长每次请求的体积越大响应越慢成本越高。尤其是你写了详细描述后10 个工具可能就要吃掉几千 token。这里有个很现实的取舍是将所有技能全量注入保证召全率还是动态注入节省成本我的方案是分层注入核心的 2~3 个技能比如查余额、查订单永远全量注入其余技能根据用户当前会话的意图预测来动态注入。实现方式很简单先用一次轻量的模型调用判断意图类别或者用关键词规则初步筛选再按意图类别加载对应的工具子集。这样既保证模型“看到的工具都是相关的”又把 token 消耗控制在合理范围内。我实测过一个项目动态注入后单次请求的 token 消耗下降了约 40%技能调用的准确率还略有提升——因为可选项少了模型的选择更聚焦了。当然这个方案需要你有一个可靠的意图分类层不然反而会误伤。如果你刚起步建议先从全量注入开始等技能超过 8 个再考虑动态加载。5. 测试与回归Skill 体系靠“喂案例”长大5.1 为什么普通单元测试不够普通的单元测试关注的是函数逻辑传入参数断言返回值。但 Skills 体系多了一层模型决策的不确定性。同一句话模型这次调了工具 A下次可能调工具 B甚至第三次完全不调工具直接回答。所以你的测试必须要覆盖“模型看到什么、选择什么、怎么执行、怎么回复”这一整条链路。我实践下来的做法是建一个行为回归测试集里面存放大量“用户问题 期望行为”的用例。每改一次技能描述、每升级一次模型版本就跑一遍这个测试集看整体表现是否退化。5.2 搭建最小可用评测集评测集的用例不需要很多但覆盖面要全。我常用这个表格结构来维护用例ID用户输入期望调用的Skill期望关键参数允许的回复意图TC-001明天下午有空吗query_scheduledate明天, start13:00, end18:00返回日程或空档TC-002帮我约周四早上开会create_scheduledate周四, time09:00确认创建TC-003我上周花了多少钱query_expense最近一周返回金额摘要TC-004你好无无闲聊回复不应调工具这里有个关键点TC-004 这种“不该调工具”的用例往往比“该调工具”的用例更重要。我曾经在某个版本里把工具描述改得太“主动”结果用户打声招呼模型都去查了一遍数据库整个体验非常诡异。负向用例守住的是模型“不乱来”的底线。5.3 用真实模型跑回归别只信模拟你可能想用 Mock 一个假的 LLM 返回结果来跑测试省成本。我的建议是模拟可以用于调试但回归必须用真实模型。因为 Skills 体系最核心的稳定性问题恰恰来自模型的决策你用假模型测不出真实模型“今天心情好不好”。每次跑回归我是这样操作的准备一批测试用例用自动化脚本逐条调用真实模型服务把模型的行为记录成日志——记录它选择了哪个工具、传了什么参数、工具返回了什么、最终回复是什么。然后逐条跟期望行为比对标出不一致的用例再人工分析是描述问题、参数问题还是模型本身抽风。这个方法费钱费时间但价值巨大。我靠着这套回归机制发现过好几个隐蔽的问题某次工具描述更新后英文内部混合导致断句混乱某次新增参数后旧用例全乱某次模型供应商升级了底层版本后工具调用风格整体漂移。没有回归测试这些问题大概率就直接上线坑用户了。5.4 别忘了测试多轮对话中的 Skill 行为多轮场景是测试的高发雷区。模型的调用决策不只受当前用户问题的约束还很依赖历史上下文。举个真实例子用户先问“帮我订明天北京到上海的机票”模型调了搜票工具接着用户说“换成后天的吧”模型需要基于上一轮已经搜出来的航班信息做修改这就要求历史消息中的工具结果被正确保留并在后续轮次中可被引用。我测试多轮时会刻意构造这样的用例上一轮调用了工具 A下一轮用简称指代上一轮的结果上一轮的工具结果里包含特定数据字段下一轮要求基于这个字段做进一步操作。这些用例能精准暴露“上下文传递”和“工具调用状态管理”的问题。6. 常见问题与排查实录6.1 模型死活不调用技能这是最高频的问题。用户问“查一下我的订单”模型却直接回答“我无法查询订单”。排查清单如下工具列表有没有真正传进请求里很多 SDK 版本字段名变了工具传错层级。工具描述里有没有“被动语态”描述写得太含糊模型会认为它不该主动提供此类服务。模型的工具调用开关是不是被系统提示词压制了有些提示词写了“作为 AI 无法执行操作”模型就直接放弃调用。上下文里是否已经包含了答案如果历史消息里有用户订单信息模型可能直接引用而不调工具。模型版本是否支持函数调用有些轻量模型对复杂工具调用的支持比较弱。大多数情况前 2 条能解决 80% 的问题。6.2 参数幻觉模型传了不存在的字段模型生成参数时经常“无中生有”。比如你只定义了order_id和reason它额外传了一个refund_amount。如果你在代码里直接把这个参数透传给下一个接口可能直接报错。解决办法正如前面说的Schema 卡紧 代码层二次过滤。在函数入口加一个白名单校验只允许 Schema 中声明的字段进入业务逻辑。def execute_skill(name, raw_args): skill SKILL_REGISTRY[name] args cleanup_args(raw_args, skill[parameters]) # cleanup_args 移除未声明的字段并做类型转换6.3 工具返回了数据但模型答非所问工具结果不是文本是 JSON。模型要基于这个 JSON 组织用户易懂的回复中间有一段“翻译”过程。经常出问题的是工具结果里字段含义不明确比如status: 1没注释这个 1 是“已支付”还是“已取消”或者返回格式嵌套太深模型“迷失”在 JSON 里漏掉了关键字段。解决方案返回结果保持扁平化字段名自解释重要状态值带上文本注释。我甚至会在返回 JSON 里额外加一个summary字段直接把“给用户看的一句话结论”准备好模型直接拿来用就够了。6.4 技能列表太长请求超时工具描述占的 token 越多模型首字延迟越长。排查时先量化一下一次请求总共多少 token其中工具描述占了多少。如果超过三成就应当采取措施了。我的做法有两条描述尽量精简到“够用但不冗余”以及动态分场景注入。前者是日常优化后者是结构性解法两条都得做。6.5 常见问题速查表现象根因应对手段模型不调工具描述含糊或工具未注入检查注入层级重写描述调错工具多个工具边界重叠拆分职责统一命名参数乱传Schema 过宽枚举、格式约束、白名单过滤回复答非所问返回结果字段不明确扁平化 添加 summary 字段响应太慢工具列表 token 过大精简描述动态注入多轮丢失状态上下文截断保留工具结果历史截断策略优化写在最后的一个小技巧聊到最后分享一个我个人的习惯每个 Skill 我都会在描述里放一个“调用示例”。不是写给人看的示例而是写给模型看的——比如query_schedule的描述末尾加上一句“示例用户问‘周五下午有空吗’应传入 date周五对应日期, start_time14:00, end_time18:00”。这个小动作成本极低但实测能让模型在相似问题上少犯一半以上的错。模型是不擅长纯抽象推理的给它一个具体例子它照着抄的能力远比你想象中强。在 Skills 这条路上我踩过的坑比写出来的还多。但有一点是确定的凡是认真对待工具描述、参数约束和回归测试的项目技能调用准确率都能做到让人满意的水平凡是偷懒跳过这些的后面都在拿线上事故还债。这个内容后续还可以扩展的方向很多比如 Skill 之间的状态共享、复杂多步任务的图编排、技能的自学习演进——但不管怎么扩展先把今天这套基本功练扎实比什么都重要。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑