这两年做Agent项目的开发者应该都有一个共同感受模型本身的能力进步很快但真正让你在真实业务里翻车的往往不是模型不会答而是它不知道在什么时候该干什么。我见过太多Demo级的Agent演示的时候行云流水一上真实数据就频繁出状况——要么该调工具的时候完全在胡编要么把一个查询技能反复调用到超时。后来我把整个项目从那种“大Prompt套小Prompt”的写法重构为一套 agent-skills 体系问题才真正收敛。所谓Agent Skills就是把Agent需要的外部能力——查数据库、调接口、跑脚本、读文件——封装成一个个有明确描述、有参数约束、有独立执行逻辑的技能单元让模型像人翻开工具箱一样知道自己该用哪把扳手、怎么用、用完怎么收。这套思路适合所有正在做Agent应用的人参考不管你是打算做客服机器人、数据分析助手还是自动化运维代理把技能层的设计吃透Agent的稳定性会有质的提升。1. 为什么Agent项目必须有技能层1.1 从对话能力到执行能力的关键一跃纯对话模型本质上是一个“文本预测器”它的强项是生成连贯、合理的文字。可真实业务里没人只需要文字你需要的是它帮你把订单状态查出来、把报表生成出来、把服务重启掉。这一步跨越靠提示词是补不上的。你可以让模型“假装”执行一个操作但它没有真的去连数据库、真的去调API。Agent Skills做的事情就是把“模型想做的事”和“系统能做的事”之间打通。技能层就像一个中间翻译官模型输出结构化的调用意图技能框架负责把意图翻译成真实的外部调用再把结果回填给模型继续推理。我在项目里最直观的感受是没有技能层的时候Agent的逻辑全埋在对话历史里上下文稍微长一点模型就开始自说自话。有了技能层之后交互变成了一个清晰的循环观察结果、决定调用哪个技能、执行、再观察。这个循环让Agent的行为变得可预测、可追踪、可回滚。1.2 技能、工具、函数调用先分清术语很多人第一次接触这个概念时会被三个词搞混Function Calling、Tools、Skills。我按自己的理解做个区分函数调用Function Calling是模型侧的能力指模型在生成文本的同时输出一个结构化的调用请求本质上是一种输出格式约束。工具Tools是函数调用的具体落地通常指一个可以被调用的外部功能比如“查询天气”的HTTP接口。技能Skills则是比工具更高一层的抽象它不只是一个孤立的函数而是“描述 参数约束 执行逻辑 可能的多步操作”的完整封装。举个例子一个“获取用户最近订单”的功能如果只是一个工具它就是一个API。但如果把它做成技能它会包含触发条件的描述、参数的格式校验、内部是否要分页拉取、异常时怎么降级甚至还有调用频率限制。工具解决“能不能调”技能解决“调得对不对、稳不稳”。这个区分不是咬文嚼字而是项目演进到中期必然会遇到的架构问题。我早期把所有能力一股脑塞进工具列表里二十几个工具全部平铺给模型结果模型频繁选错、参数拼错。后来按技能的方式重新组织每个技能自带边界和说明误调率立刻降了一个量级。1.3 技能层要解决的四个核心问题概括下来Agent Skills要解决的就是四个问题这四点也是你评审一个技能设计好不好的检查清单第一可发现性。模型面对一堆技能时能不能从描述里准确判断“这件事该由哪个技能负责”。可发现性差的典型表现是模型面对一个明明已经存在的技能仍然选择自己编答案或者随机挑一个不相关的技能调用。第二可约束性。技能的参数必须能被校验、被约束。真实场景里用户说“查一下最近的数据”模型可能传一个缺省的时间范围如果没有合理的默认值和校验技能就会跑出错误结果。第三可观测性。每一次技能调用都要留下痕迹调了哪个技能、传了什么参数、结果是什么、耗时多久、是否失败。没有这一步出问题时你连从哪里排查都不知道。第四可维护性。技能是会演化的接口会变、逻辑会调。技能层如果和业务代码强耦合改一处就要重新发布整个应用。好的技能设计应该是插拔式的加一个技能只是加一个目录的事。2. 技能定义写清楚“是什么”比写“怎么做”更重要2.1 技能描述是模型做选择题的题干技能定义的第一道关卡是description。很多人觉得description随便写两句就行把精力全放在执行逻辑上这是本末倒置。模型选择技能时本质上是在做一道阅读理解题题干就是每个技能的name和description。题干写得含糊模型再聪明也会答错。我自己写描述时有一个笨办法先把技能的使用场景列出来然后反推描述。比如做一个“查询订单状态”的技能不会只写“查询订单状态”而是会写成“当用户询问某个订单的当前进度、物流状态、签收情况或要求查看订单详情时使用此技能。需要传入订单号。如果用户没有提供订单号先通过‘查找订单’技能获取订单号”。你注意这里有几个关键点一是写清楚了触发场景“询问进度、物流、签收、详情”都是可能触发的话术变体二是写清楚了前置依赖“如果没有订单号先去调另一个技能”。这种描述才能让模型在模糊场景里做出正确选择。还有一个经验描述里要明说“什么时候不要用”。比如“此技能仅用于已支付订单未支付订单请使用‘订单创建’技能”。负面约束对模型纠偏的作用往往比正面描述更大。2.2 参数设计遵循最小必要原则参数Schema是技能最容易过度设计的地方。我见过有人把一个技能参数设计成十几个字段各种嵌套结构看起来很完善实际用起来模型根本填不对。参数设计的第一原则是能少就少。模型不是表单填写工具它面对的是千变万化的自然语言表达。你让它在一次调用里同时填出六个必填字段它大概率会瞎猜。正确的做法是把参数拆细或者让技能内部自己补全。举个例子我之前做数据查询技能时最初设计了数据源、指标、维度、时间范围、过滤条件五个参数。实际跑下来模型经常把指标和维度搞混。后来我改成两个参数query用户的原始查询语句和limit返回条数上限让技能内部用规则去解析指标和维度。改动之后成功率明显提升而且技能的可测试性也变好了。另外每个参数一定要给清晰的description和合理的默认值。模型对“string”类型的理解是模糊的你要告诉它这个字符串应该是什么格式。比如时间范围参数描述里写“接受格式为YYYY-MM-DD的日期或短语如‘最近7天’”模型才能正确映射。2.3 反面案例一个糟糕的技能定义长什么样为了让你避坑我直接放一个我早期写过的反面教材。当时我要做一个“查天气”的技能定义是这样的{ name: get_weather, description: 查询天气, parameters: { type: object, properties: { city: { type: string }, date: { type: string } } } }这个定义有三个问题。第一description“查询天气”完全没有触发场景用户问“明天出门要带伞吗”模型可能觉得这不是在问天气于是自己编一段话。第二city参数没有说明支持的格式用户说“我家这边”模型不知道怎么填。第三date参数没有默认值如果用户没提日期模型要么不调用要么瞎填。改完之后是这样{ name: get_weather, description: 查询某个城市在指定日期的天气情况包括温度、降水概率、风力等。当用户询问天气、气温、是否需要带伞、适不适合出行时使用。若用户未指定城市需先通过定位技能获取城市。, parameters: { type: object, properties: { city: { type: string, description: 城市名称如‘北京’、‘上海’若为县级市请带上省份前缀以消除歧义 }, date: { type: string, description: 查询日期格式YYYY-MM-DD缺省时默认今天 } }, required: [city] } }这个例子看起来很基础但我确实见过太多团队栽在这一步。技能定义的每一个字符都会影响模型在真实对话里的决策质量。3. 注册与调度技能多了之后怎么不打架3.1 技能注册表的结构设计当技能数量超过十个你就需要一个注册表来统一管理。注册表不是简单的列表它至少要承担三件事登记每个技能的元信息、维护技能的启停状态、提供按场景过滤的能力。我的做法是把注册表设计成一个独立的配置模块核心是一个字典key为技能名value包含以下字段技能描述传给模型的那一段文本参数Schema用于校验和生成调用提示执行函数实际的处理逻辑启用状态上线、灰度、下线标签用于按场景做过滤比如“只读”“写操作”“高权限”这种设计带来的最大好处是你可以按对话场景动态决定把哪些技能暴露给模型。比如客服场景只需要查询类技能就把所有写操作技能从注册表里摘掉这样模型根本不可能调用到不该用的东西。注册表还应该支持热更新。技能迭代不应该要求整个服务重启。把这个机制想清楚后续技能从十几个扩到几十个时会省很多事。3.2 调度策略模型自主选择与规则路由的取舍技能调度有两种极端路线。一种是完全交给模型自主选择所有技能一股脑暴露让模型自己判断。另一种是完全规则化用意图识别模型或关键词匹配来硬路由。我的实践结论是两者要结合而且有个先后顺序先用规则做粗筛再用模型做精排。规则粗筛的好处是稳定和廉价。很多场景其实不需要模型来判断。比如用户消息里有“订单号”字样并且提到了“物流”或“发货”可以直接锁定订单查询技能。粗筛能大幅减少模型的决策负担也降低了误调用概率。模型精排的价值在于处理模糊表达。用户说“我那个东西怎么还没到”没有订单号也没有明确的“物流”词这时候规则就失灵了需要模型理解语义后选技能。这个双层调度还有一个附加好处你可以给每层加日志。粗筛命中了什么规则、模型最终选了哪个技能、置信度是多少全部记录下来。出问题的时候顺着日志就能定位是哪一层决策错了。3.3 权限与安全边界技能层天然是权限控制的最佳落点。每个技能在注册表里就应该绑定一个权限级别。我的惯例是分三级只读级查数据、读文件、检索文档操作级修改配置、发送消息、更新记录高危级删除数据、执行代码、管理账号高危级技能的调用必须满足额外条件用户明确确认、双重提醒、操作审计日志。我曾经在项目里放过一个“执行SQL”的技能一开始没有加任何保护结果模型在一个测试环境里把一张表清了。数据能恢复但那次事故之后我把所有写操作技能都加上了二次确认机制。安全边界的另一个要点是技能自身的健壮性。技能的执行逻辑里必须有超时控制、异常捕获和输入校验。模型传来的参数是不可信的这一点必须刻在脑子里。哪怕只是把参数直接拼进SQL都可能产生注入问题。技能内部要做白名单校验而不是黑名单过滤。4. 实战从0到1搭建一套可用的技能框架4.1 项目目录与分层结构理论讲再多不如直接看一套能跑的骨架。我会用一个最小化的Python框架来演示目标不是造轮子而是让你看懂技能框架的核心脉络然后能快速移植到你自己的项目里。我习惯把技能框架分成三层协议层、调度层、执行层。协议层定义技能的元数据格式、参数Schema、调用结果的统一返回结构调度层负责从注册表里筛选技能、调用模型做决策、解析模型返回的调用意图执行层真正跑技能逻辑并对执行结果做后处理目录结构大致是这样agent_skills/ ├── registry.py # 技能注册表 ├── dispatcher.py # 调度器规则粗筛 模型决策 ├── executor.py # 执行器调用技能逻辑统一处理异常 ├── schema.py # 协议层技能元数据、参数校验 ├── skills/ │ ├── order_query.py # 具体技能订单查询 │ ├── weather.py # 具体技能天气查询 │ └── common.py # 技能共用工具 └── tests/ ├── test_registry.py └── test_skills.py这个分层的好处是新增一个技能时你只需要在skills目录加一个文件然后在注册表里登记一行不需要动调度器和执行器。4.2 核心代码骨架注册、校验与执行先看协议层的技能基类定义。我用的方式很朴素每个技能文件里定义一个类类属性就是技能的元信息。# schema.py from dataclasses import dataclass, field from typing import Callable, Any dataclass class SkillMeta: name: str description: str parameters: dict required: list field(default_factorylist) permission: str read # read / write / high enabled: bool True class BaseSkill: meta: SkillMeta def execute(self, **kwargs) - dict: raise NotImplementedError def validate(self, kwargs: dict) - tuple[bool, str]: # 参数校验检查必填、类型、枚举值 for key in self.meta.required: if key not in kwargs or kwargs[key] in (None, ): return False, f缺少必填参数: {key} return True, 然后是注册表它的作用是把技能实例按名字登记起来并提供按权限和启停状态的过滤能力。# registry.py class SkillRegistry: def __init__(self): self._skills {} def register(self, skill: BaseSkill): meta skill.meta self._skills[meta.name] { skill: skill, meta: meta, } def list_for_model(self, permission: str None): # 返回给模型看的技能描述列表按权限过滤 result [] for name, item in self._skills.items(): meta item[meta] if not meta.enabled: continue if permission and meta.permission ! permission: continue result.append({ name: meta.name, description: meta.description, parameters: meta.parameters, }) return result def get(self, name: str): return self._skills.get(name)执行器的核心逻辑是统一做三层处理先校验参数再执行技能最后格式化返回结果。所有技能抛出的异常都在这一层被拦截转成模型能理解的错误描述。# executor.py import traceback class SkillExecutor: def __init__(self, registry: SkillRegistry): self.registry registry def execute(self, name: str, arguments: dict) - dict: item self.registry.get(name) if not item: return {success: False, error: f技能不存在: {name}} skill item[skill] ok, msg skill.validate(arguments) if not ok: return {success: False, error: msg} try: result skill.execute(**arguments) return {success: True, result: result} except Exception: traceback.print_exc() return {success: False, error: 技能执行异常请稍后重试}这是一套极简但能跑的骨架。你在自己项目里复用时可以往里面加缓存、加日志、加埋点但核心的“校验—执行—异常收敛”这个闭环一定要保留。4.3 评测先跑通再跑快技能框架搭完之后别急着接业务先做评测。我给技能层配的评测集基本都是真实对话里收集来的用户原话然后人工标注了“正确技能”和“正确参数”。评测指标我主要看三个召回率该调用技能的场景里模型正确调用了多少精确率所有调用里技能选择正确的比例参数准确率技能选对了但参数是否填对这三个指标分开统计会非常有价值。如果召回率低说明技能描述不够清晰模型没意识到该调用如果召回率还行但精确率低说明技能之间有语义重叠需要调整描述边界如果前两者都好但参数准确率低问题出在参数Schema设计。评测集不用一开始就追求大但一定要覆盖每个技能的正例、反例和边界案例。比如订单查询技能的正例是“我的订单到哪了”反例是“帮我推荐一款手机”边界案例是“查一下昨天和前天我买的东西到没到”。5. 常见问题与排查技巧实录5.1 模型就是不调用技能怎么办这是最常遇到的头号问题技能明明存在描述也写了模型却视而不见直接凭记忆编答案。排查顺序我建议这么走第一步确认技能是否真的暴露给了模型。检查注册表过滤逻辑有没有误伤权限级别是不是匹配。我就踩过这个坑某个技能权限设置成了write而会话上下文默认传的是read权限技能直接被过滤掉了模型当然不会调用。第二步检查技能描述是否和其他技能或系统Prompt存在冲突。有时候系统Prompt里写了“你是一个知识渊博的助手能回答任何问题”模型的自信心爆棚自然不愿意调用工具。解决办法是在系统Prompt里明确“遇到数据类问题必须调用技能禁止自行编造”。第三步给几个示范样例。在少样本的时候模型很难理解“什么时候该调用”你可以提供几个对话示例明确展示用户问什么样的问题时你要输出调用请求。这个办法往往比反复改描述更有效。5.2 乱调用、重复调用怎么压下去和“不调用”相反的场景是“过度调用”。模型一上来就把所有相关技能挨个调用一遍或者同一个技能在一个回合内连续触发五六次。针对连续重复调用我在执行器里加了一个循环护栏记录每个技能在单轮对话里的调用次数超过阈值就强制停止并且把“已超限”的信息回给模型。阈值按技能类型设不同值查询类可以放宽写操作类必须收紧。针对“扫射式调用”本质问题是技能之间边界不清。模型觉得订单查询和订单列表都能回答同一个问题就会两个都调。这时候要做的不是改Prompt而是调整技能描述明确区分“订单查询”是查单个订单详情“订单列表”是拉多个订单的汇总。边界清晰了误调用自然减少。5.3 参数幻觉模型传出不存在的值参数幻觉是另一个高频故障技能选对了但参数填得离谱。比如日期参数传一个“2024-13-45”或者城市参数传一个不存在的城市名。这类问题靠提示词解决不了必须在技能内部做校验兜底。我给每条参数都配了解析器例如日期参数会先尝试解析失败就回退到默认值的合理范围。城市参数则走一个在线匹配接口匹配不到就明确让用户提供正确名称而不是将错就错。还有一个经验让模型先澄清再调用。当用户提供的参数信息不足时模型应该先追问而不是强行调用。这个行为需要在System Prompt里明确写“当必填参数缺失或存在明显歧义时先向用户确认不要尝试猜测”。配合参数校验能把参数幻觉压到很低的比例。5.4 超时、并发与状态残留技能层的稳定性问题很多不是模型的问题而是基础设施的问题。外部接口慢、偶发超时、并发执行时共享变量被污染这些坑每个都足以让Agent在线上翻车。我的处理方式是所有外部调用一律设置超时默认给8秒超过就返回一个标准错误。技能执行尽量无状态不在类属性里存中间变量。如果需要跨步骤共享数据显式通过参数传递不要藏在全局变量里。还有一个容易忽略的点是并发下的日志追踪。多轮对话并发时日志都混在一起排查问题非常痛苦。我在每次请求的入口生成一个trace_id贯穿整个技能调用链日志全部带上这个ID。有了这个ID之后排查线上问题的时间至少缩短一半。6. 收尾一点个人体会技能层的设计没有标准答案但它有一些放之四海皆准的道理描述写清楚比模型选得准更重要参数少比参数全更重要可观测性比功能丰富更重要。我踩过最大的坑就是一开始把Agent的稳定性押在模型的临场发挥上结果反复被打脸。把Agent的能力收敛到技能体系里之后每一次行为都有了依据和记录系统才真正变得可治理。如果你现在正在做一个Agent项目我建议先把技能层单独抽出来做不要和业务代码混在一起。哪怕只花一天时间搭一个最小框架后续迭代的收益也远远大于这一天。什么时候你发现自己改一行技能代码不需要重新发整个服务技能层的架构就算立住了。最后再分享一个小技巧技能的description迭代之后记得重新跑一遍评测集。很多团队改完描述不回归测试结果修复了一个场景的技能选择弄坏了另外三个。有了评测集这种“安全网”你才敢大胆去调描述、调参数、调调度策略而不是在线上赌运气。