Agent Skills 实战指南:让大模型从工具调用走向技能编排
Agent Skills 这词我盯了很久。之前在和团队做复杂任务自动化的时候最大的痛点就是模型单次推理能力再强面对多步骤、跨系统、需要决策分支的真实业务场景照样抓瞎。直到我们把思路从“让模型直接完成整个任务”切换成“给模型一套可复用、可组合、可验证的原子技能”整个系统的稳定性和可维护性才真正上了一个台阶。这篇文章我想以实际项目的角度把 Agent Skills 从概念拆解到落地实现完整聊一遍。不管你是刚开始接触智能体开发还是已经在生产环境里被 prompt 和 function call 折磨得够呛这篇都值得花几分钟读完。不光告诉你怎么做更重要的是讲清楚为什么这么做。1. 内容整体设计与思路拆解1.1 从“万能模型”到“技能拼装”的认知转变不管是早期的 ReAct 模式还是后来的 Function Calling本质上都是一种“让模型自己去调用工具”的尝试。但实际跑过几个项目之后你会很明显地感觉到当任务链条变长模型会在中途迷失工具参数一多就开始乱传甚至在关键步骤上“自以为是”地跳过验证环节。Agent Skills 这套思路的核心变化是把“模型的推理能力”和“任务的执行能力”做了一次彻底分离。模型依然负责理解用户意图、拆解目标、规划步骤但它不再需要自己去揣摩“某个系统该怎么操作”而是直接调用一个已经封装好的 Skill。这个 Skill 内部是确定性代码输入什么、输出什么、有哪些约束、需要哪些权限全部是预先定义好的。这样做带来的直接好处有三个可控性大幅提升。模型只做决策和编排不碰具体的执行逻辑行为边界清晰不会被带偏。可测试性变强。每个 Skill 都是一个独立模块可以单独写测试用例出问题能快速定位。复用性真正落地。同一个 Skill 可以在不同 Agent、不同业务流程里反复使用不再需要重复写 prompt。我见过很多团队在这个问题上绕了很大的弯路。一开始拼命调 prompt试图让模型变得更“聪明”结果发现上下文稍长一点效果就急剧下降。后来尝试把所有业务逻辑写进 function calling结果函数数量一多注释和参数描述全成了负担。最后切到 Skills 的组织方式之后才算是找到了一个让模型和执行层各司其职的平衡点。1.2 为什么这套设计适合当下的技术阶段当前大语言模型的能力边界很有意思语义理解、常识推理、多轮对话这些已经强到让人惊叹但精确计算、状态管理、外部系统交互这些仍然极其脆弱。一个典型的例子是模型能把需求理解得很到位但让它记住会话里第三轮提到的订单号并且准确用于最后一轮查询可能就翻车了。Agent Skills 的设计正好卡在这个分界线上。凡是需要确定性、需要状态追踪、需要和外部系统对接的事情全部下沉到代码层解决凡是需要理解、判断、生成的事情留给模型解决。这样既顺应了模型能力的优势区间又用工程手段弥补了模型天然的短板。从成本角度看这个方案也更划算。模型推理按 token 计费长上下文和复杂推理都意味着更高的开销。如果能把“调用某个技能”压成一个极短的结构化动作而不是把整个接口文档塞进 prompt单次调用的成本可以压缩一个量级。我在压测的时候测过同样一个订单查询任务纯 prompt 实现平均要消耗两千多个 token切到 Skill 调用之后只需要四百多。从迭代效率来看这套设计也有明显优势。业务逻辑一旦发生变化只需要修改对应的 Skill 实现模型侧的规划逻辑几乎不用动。相比传统“改需求等于重新调 prompt”的噩梦这种模式下的迭代速度堪称降维打击。1.3 核心概念Skill 到底是什么想理解 Agent Skills先要把 Skill 这个抽象单位定义清楚。在我的项目里一个 Skill 包含四个基本组成部分能力描述。用自然语言说明这个技能是做什么的、适合什么场景、输入输出大概是什么样。这部分是为了让模型能够准确判断“什么时候该调用它”本质上是一份给模型看的“岗位说明书”。执行逻辑。实际运行的代码可以是单个函数也可以是一段完整流程。这是 Skill 真正干活的引擎需要保证确定性、鲁棒性和可观测性。参数协议。定义调用该技能需要哪些输入参数、每个参数的类型和约束条件。这个协议越严格模型传错参数的概率就越低。验证机制。调用之后如何确认执行结果是否符合预期。没有验证步骤的 Skill 就像没有体检的打工人表面上无事发生实际上可能已经出了问题。这四个部分缺一不可任何一块设计不到位整个 Skill 的可信度都会大打折扣。2. 核心细节解析与实操要点2.1 Skill 的粒度控制多大算合适这里我直接给出实战结论Skill 的粒度决定整个 Agent 系统的成败而且绝大多数人第一次都会把 Skill 设计得太大。我最初犯过的错误是把“处理订单全流程”做成一个 Skill。听起来很美好模型只需要说一句“帮我处理订单”就完事了但实际跑起来会发现参数列表有十几项模型根本不知道哪些是必填哪些是选填不同的异常分支也全部埋在内部代码里出错了很难向上反馈。后来我做的调整是把整个流程拆成若干个更小的 Skill查订单、修改订单状态、生成物流单、发送通知、计算费用。每个 Skill 只负责一件具体的事情参数限制在 3 到 5 个执行逻辑控制在几十行以内。模型在规划的时候不再是“调一个大黑盒”而是像搭积木一样把多个小 Skill 编排成一条清晰的链路。什么样的粒度算合适我在实践里总结出一个判断标准如果一个 Skill 的描述需要写超过三句话或者调用参数超过五个大概率就是粒度太大了。把它拆成两个或三个更小的技能整体的可控性会明显好很多。2.2 参数协议设计的工程细节参数协议是整个 Skill 与模型交互的接口面这里的设计质量直接决定了模型能不能准确调用。我踩过的坑和总结出来的经验主要集中在几个方面。参数名的语义要极其明确。比如 status 这种名字就没有 order_status 好前者在模型的语义空间里太模糊。参数名最好能自解释让模型通过名字就能判断该往里面填什么内容。我还见过用拼音缩写做参数名的项目模型果然不负众望地开始乱填。类型约束要严格。我在代码里用 Pydantic 做参数校验之后传参错误率下降了大概六成。模型从本质上说只是个文本生成器你告诉它“quantity 参数必须是 integer”它在生成 JSON 的时候仍然可能给你一个字符串。如果入口处有强制校验错误就会在第一时间被拦截反馈也清晰得多。默认值要减少模型的负担。那些在大多数场景下都不会变的参数直接给默认值就好不要让模型去猜。比如查询近三个月订单三个月这个时间窗口直接写成默认值比让模型自己推断时间范围要靠谱得多。2.3 Skill 的描述编写给模型的说明书要怎么写Skill 描述这段文本在很多人看来就是写几句话的事实际上它的重要性不亚于执行代码本身。因为决定“这个 Skill 何时被调用”的不是代码逻辑而是这段描述给模型带来的语义匹配效果。我现在的写法遵循一个固定模板先说明这个技能能做什么再说清楚什么场景下适合用、什么场景下绝对不要用然后列出典型的使用示例。负面的排除信息很关键它能有效防止模型在错误的场景里调用错误的技能。比如一个“查天气”的 Skill描述里明确写了“不要用于查询历史气象数据”模型就不会把历史数据查天气的请求错误路由到当前这个技能上。描述里还应该包含边界说明。如果某个 Skill 只能处理特定类型的数据或者只适用于特定地区的业务这些约束必须写清楚。模型不会自己去“推理”这些隐藏的前提条件你不写就等于没有。2.4 执行层的鲁棒性你最需要重视的环节很多 Agent 项目开发时一切正常一上生产环境就各种问题最核心的原因就是执行层写得太脆弱。所谓执行层就是模型中控之外的确定性代码部分包括 Skill 内部逻辑、外部接口调用、数据读写等。我要求团队在写 Skill 执行逻辑的时候遵循几条铁律必须处理超时。所有外部 HTTP 调用都要设置超时时间默认五秒线上系统根本不给你无限等待的机会。必须处理异常。每个 Skill 内部要有 try-catch出错时要返回结构化的错误信息而不是让异常穿透到模型层。必须记录日志。每次 Skill 调用的入参、出参、耗时、错误信息全部要留痕这是后续排查问题和评测调优的基础。必须保证幂等性。大多数 Skill 可能会被模型重复调用执行两次的结果必须和一次相同否则容易出现重复下单、重复扣款这种事故。这几条看起来是工程常识但在 Agent 开发的热潮里很多人骨子里还是把它当成“写个 demo 给模型调用”工程标准一降再降最后线上报错的时候全都懵了。3. 实操过程与核心环节实现3.1 完整构建一个 Skill从需求到上线拿一个实际的案例来演示整个构建流程。假设当前业务里有一个需求需要 Agent 能查询某个客户账户下的所有有效订单。按照前面的原则第一步先把 Skill 的边界划清楚它只做“查询”不做“修改”。修改订单状态是另一个 Skill 的职责。这样划分是为了让每个技能都保持单一职责避免模型调用链路上产生歧义。第二步定义参数协议。需要一个客户标识和订单状态过滤条件。客户标识用字符串数组支持一次查多个客户状态过滤用一个可选的枚举值默认返回全部有效订单。第三步编写能力描述。描述里明确说明这个技能适用于当用户询问“我的订单有哪些”“某个订单发货了没”这类查询场景。同时写明不能用于什么场景比如不能用于下单、不能用于修改地址、不能用于退款操作。第四步实现执行逻辑。代码层先去缓存查一遍命中就直接返回没命中再去数据库查查完顺手更新一下缓存。整个函数控制在一百行以内入参校验放在第一步数据库查询统一走预编译语句。第五步写测试用例。至少覆盖正常场景、空数据场景、入参错误场景、数据库异常场景。测试通过之后这个 Skill 才算真正上线。3.2 技能注册表让模型知道你有什么牌有了 Skill 之后还要解决一个问题在任意一轮对话里模型怎么知道当前有哪些技能可用。这靠的不是把全部 Skill 描述塞进上下文那是原始的笨办法。正确做法是维护一个技能注册表动态决定每轮对话向模型暴露哪些技能。技能注册表本质上是一个索引记录每个 Skill 的名称、触发条件、适用场景和依赖关系。在模型收到用户消息之后先做一个快速的意图分类把命中的候选技能列表传给模型而不是把几百个技能全量塞给它。这种动态暴露机制还能自然处理一个问题技能太多会稀释模型的注意力。根据实测数据暴露给模型的候选技能数量控制在 5 到 10 个时调用准确率最高。技能少于 3 个时模型容易“没得选”多于 15 个时它就开始犯迷糊了。注册表加上意图分类器就是把技能数量限制在最合理范围的关键。3.3 模型编排层规划、调用、反馈的循环整个 Agent 的工作流可以简化成一个循环理解用户需求拆解为任务序列为每个子任务选择合适的技能执行技能根据执行结果判断是否完成或需要调整。这个循环说起来简单实际上每一环都有很多细节。规划环节最容易出的问题是模型把“期望的最终状态”和“当前需要执行的动作”搞混。比如用户说“帮我看看昨天晚上为什么没扣款成功”模型直接调用了“查询账户余额”技能而不是按顺序执行“查询交易流水”再“分析失败原因”。要解决这个问题除了在 prompt 中强调“一步一步来”更有效的方式是让每个技能的描述里带上明确的前置场景比如查询流水技能描述中写明“在处理支付失败类问题时请首先调用本技能获取流水事实”。调用环节要重点控制的是模型的“编造倾向”。模型时不时会脑补出根本不存在的参数值或者凭空捏造一个不存在的技能。应对方法就是前面强调的严格校验 技能注册表白名单。凡是入口校验通过不了的一律拒绝凡是白名单里没有的技能一律视为无效调用。反馈环节是很多初版实现里容易遗漏的。模型调用完技能之后需要把结构化的执行结果转换回自然语言再回答用户这个过程不是简单的拼接。比较稳妥的做法是先让模型基于技能返回的数据做总结再在返回给用户之前加一层模板校验防止模型在加工信息的时候引入幻觉。3.4 一个可运行的技能调用协议示例下面给出一段简化但完整可参考的调用协议实现用 Python 伪代码形式展示核心逻辑。from typing import Dict, Any import json class SkillBase: name: str description: str parameters: Dict[str, Any] def execute(self, **kwargs) - Dict[str, Any]: raise NotImplementedError def run_skill(skill: SkillBase, params: Dict[str, Any]) - Dict[str, Any]: # 入参校验 for field, spec in skill.parameters.items(): if field not in params and spec.get(required): return { status: error, code: MISSING_PARAM, message: f缺失必填参数: {field} } if field in params and not _check_type(params[field], spec.get(type)): return { status: error, code: WRONG_TYPE, message: f参数 {field} 类型错误期望 {spec.get(type)} } # 执行技能 try: result skill.execute(**params) return { status: success, result: result, trace_id: _generate_trace_id() } except TimeoutError: return {status: error, code: TIMEOUT, message: 调用上游服务超时} except Exception as e: return {status: error, code: INTERNAL_ERROR, message: str(e)}这段代码演示的是一个标准化的技能执行入口。实际项目中可以在这个基础上加日志埋点、耗时统计、错误聚合但这些是工程优化层面的事核心流程就是校验、执行、反馈三件事。4. 常见问题与排查技巧实录4.1 模型反复调用同一个错误技能这个现象很典型。现象是无论你怎么调整描述模型就是坚持把一个查询类需求路由到一个计算类技能上。我排查这个问题的经验是先不要急着改 prompt先去翻技能注册表里这两个技能的语义是否出现了重叠。很多时候是因为两个技能的核心关键词撞了模型在语义匹配时更偏向描述更长的那一个。解决办法也比较直接给两个技能分别加上明确的“绝对不要用”边界。比如查询类技能写明“请不要使用本技能进行任何形式的聚合或统计计算”计算类技能写明“本技能仅在需要数学运算时使用查询请调用查询技能”。边界一划清楚模型的路由命中率很快就上来了。4.2 技能调用成功但执行结果明显错误这种问题最迷惑人因为链路从头到尾看起来都是通的模型正常规划、正常调用、技能正常返回。但返回的数据和用户的问题牛头不对马嘴。后来我加了详细的日志链路才发现根因出在技能内部的过滤条件上。比如用户问“最近有哪些退款订单”模型调用了订单查询技能传了退款状态但技能内部实现里默认加上了一个“最近三个月”的时间过滤条件而用户的真实意图是查全部退款。数据被静默过滤了返回结果自然就偏了。这个问题的核心教训是技能内部不要自作主张地添加默认过滤条件除非这些条件在描述里明确写明。任何隐含逻辑都有可能成为“模型预期”和“实际结果”之间的断层。4.3 参数携带了几个月前的旧状态在对上下文敏感的对话场景里还会遇到一个更隐蔽的问题模型调用了技能但参数里携带的关联信息是几轮之前的状态。比如用户先问“A 订单多少钱”又问了“那它的物流呢”模型在查物流的时候把 A 订单的 ID 带对了但把收货地址带成了上一轮别的订单里的地址。这类问题的根源在于长对话场景下的上下文状态管理。模型需要维护一个“当前讨论对象”的内部指代一旦跳转多个主题指代就容易冲突。我在工程上的解决方案是在规划层增加一个显式的“实体状态表”跟踪当前对话涉及的主要实体及其属性在模型生成技能调用参数时强制从状态表里取对应值而不是依赖模型的隐式记忆。4.4 常见问题速查表现象可能原因排查方向模型频繁选错技能技能描述语义重叠检查注册表补充边界说明调用参数频繁报错参数命名不语义化、类型约束不严重设计参数协议加强入口校验结果对但用户不满意技能内部有隐含过滤或默认逻辑审查执行代码去除未声明的过滤条件长对话中调错对象上下文实体指代混乱引入实体状态表强制参数取值生产环境偶发超时执行层缺少超时控制和重试增加超时、熔断和重试机制模型编造不存在的技能开放集调用改为白名单模式未注册即拒绝4.5 关于评测怎么衡量 Skill 体系做得好不好最后聊一个经常被忽视、但极其重要的环节评测。没有评测体系你永远无法知道改动一个 Skill 描述是在变好还是变坏。我在项目里建立了一套基于回放日志的评测管线。采集真实用户的历史对话标注好消息里对应的技能调用序列然后每次改动之后把同样的问题重新跑一遍对比技能调用准确率、参数准确率、任务完成率三个核心指标。这个评测机制做起来之后团队对 Skill 的迭代终于有了实感。哪些改动有效、哪些改动是心理安慰跑一遍数据全清楚了。以前调 prompt 纯粹靠感觉现在每次改动都有数据兜底这个转变对项目的长期健康发展至关重要。我强烈建议任何做 Agent 相关工作的团队项目启动的第一周就把评测管线立起来拖得越久补课的代价越大。Agent Skills 这条路方向是对的。它没有把所有希望押在模型推理能力上而是用工程化的方式把模型的能力框在一个可控范围内发挥。如果你正在做 Agent 开发不妨从这个思路切入重构你现有的工具调用体系跑一段时间感受一下其中的差距。