从描述到治理:Agent技能设计的完整实操指南
发布时间:2026/9/23 4:43:10 锦皓数字建站

1. 先搞清楚Agent的技能到底是什么1.1 技能不是普通函数差的不是语法做Agent开发有一段时间的人应该都有同感把一个大模型接上工具让它能查天气、发邮件、查数据库这事儿本身不难。真正难的是让模型在合适的场景下、用正确的方式、把一系列动作组合起来完成一个完整目标。这就是“agent-skills”或者说Agent技能体系要解决的核心问题。我理解的技能不是单个工具函数而是把“模型的一个能力片段”做了标准化封装。比如“查询订单状态”是一个技能“根据用户情绪调整客服话术”也是一个技能。前者需要调用接口拿数据后者可能只依赖模型自身的推理能力。从模型视角来看技能就是它可以去调用的一组能力每个能力有名字、有描述、有入参、有出参、有执行逻辑还有触发它的前置条件。很多人容易把技能和Function Calling混为一谈。我的看法是Function Calling是底层协议技能是在这个协议之上设计出来的一层语义封装。打个比方Function Calling相当于给你一把电钻技能则是告诉你“在什么位置、用什么角度、打多深的孔”。没有电钻不行但只有电钻也干不了装修的活儿。另外一个常见的误区是觉得“模型能力强了技能设计可以随便一点”。实测下来恰恰相反。模型越强它越会主动调用看起来可用的工具哪怕你的技能描述写得含糊它也会凭着上下文猜。猜对了皆大欢喜猜错了排查起来非常痛苦。所以我写这篇东西的核心观点就一句话技能设计是一等公民前期多花一小时后期省下十小时。1.2 我把技能拆成了三层来理解在动手做技能之前我习惯把它拆成三层语义层、执行层、治理层。这三层各管各的事但缺一不可。语义层解决的是“模型知不知道这个技能是干嘛的”。这一层包括技能名称、描述信息、参数说明、适用场景、典型示例。模型接受到用户请求后先通过语义层来挑选应该调用哪个技能。所以这层做得不好后面执行层写得再漂亮也没用——模型根本不会选到它。执行层解决的是“技能被选中后怎么稳定地把事情做完”。这一层包括实际的代码逻辑、API调用、数据处理、异常处理、超时重试等。执行层是工程师的主战场也最容易让人上头一上来就写各种复杂逻辑反而忽略了语义层。治理层解决的是“多个技能之间如何协作冲突如何规避权限如何管控”。比如某个技能依赖用户登录态某个技能只允许特定角色调用某个技能执行时需要审批确认。这些如果不在设计阶段想清楚上线之后一定会被各种边界情况找上门。把技能拆成这三层之后你就会发现写一个技能本身不难难的是一套技能体系能不能互相配合。后面我会按这个框架展开讲每一个环节都配合实操案例。2. 技能设计里最容易被忽视的一环描述质量2.1 一份合格技能描述应有的四个部分很多团队在给技能写描述时就是一句话查询订单状态。这种描述信息量太低模型遇到相似场景时根本分不清该选哪个技能。我自己在实践里总结了一套描述模板分为四个部分技能职责、触发场景、执行约束和典型示例。技能职责就是一句话说明这个技能做什么最好包含核心动词和对象。比如“查询订单当前物流状态和预计送达时间”就比“查询订单”清晰得多。触发场景要写清楚“什么情况下用户应该用这个技能”。比如“当用户询问包裹走到哪里了、什么时候能到、物流是否异常时应使用此技能”。这一步很关键因为用户不会直接说“我要调用查询物流技能”他会说“我东西怎么还没到”你要让模型能识别出这句话背后的意图。执行约束写的是调用这个技能时需要满足的条件比如是否要求用户已登录、是否需要传订单号、接口的超时时间、是否有调用频率限制。这些信息模型自己是推不出来的全靠写在描述里。典型示例则是给模型提供的参考样本让它学习到“这个技能对应的用户问法长什么样”。示例越多越多样化模型选择的准确率就越高这个动作几乎没有任何副作用。2.2 怎么写触发场景让模型选得对我在调试时会反复看日志发现模型选错技能的原因大部分都出在触发场景写得不对。写触发场景最容易犯的毛病是写得太抽象。比如“处理用户关于物流的问题”这种描述模型看了等于没看。正确做法是写具体问法最好能覆盖正面问法、侧面问法、情绪化问法三类。以下面这个“查询订单物流”技能为例正面问法“我的包裹到哪了”“物流更新了吗”“什么时候能送到”侧面问法“我买的手机怎么还没发货”“上周下单的东西现在到哪儿了”情绪化问法“我的快递是不是丢了”“怎么好几天没动静了”把这些问法写进触发场景里模型在遇到新表达时就能通过语义相似度正确路由。虽然大模型有很强的泛化能力但你给它越明确的参考样本它的选择稳定性就越高。这个事没有太多玄学就是数据质量和覆盖面的问题。参数说明这一块也值得多说一句。不同模型对参数schema的理解方式不同但共同点是参数名称和描述一定要贴合业务语义。比如传查询起始日期的参数命名成“start_date”然后描述里写“用户想要查询的起始日期格式为YYYY-MM-DD”比只写“start日期”要有用得多。最好把可选项的枚举值也列出来减少模型自由发挥的空间。3. 从零开始定义一个可用技能完整实操3.1 先想清楚技能边界动手写代码之前我建议先在文档里把这个技能的设计案写清楚。以“查快递物流”这个技能为例我会先定义它的边界它只负责查物流信息不负责退货申请也不负责修改收货地址。边界不清晰的技能后期很容易被模型误调。定义技能边界时我会同时列一个问题清单这个技能的输入最少需要哪些信息哪些字段是必填哪些可以自动获取用户问得模糊时是主动澄清还是采用默认值技能执行失败时返回给用户的话术应该是什么这个清单一口气列完后面写代码的速度会快很多。还要想清楚技能的执行方式是同步执行还是异步执行。如果是同步执行接口响应时间必须控制在模型等待范围内如果是异步执行就要设计好任务轮询或回调机制。很多Agent卡顿问题根源就是技能设计成了同步但实际执行耗时太长模型侧等待超时。我自己的习惯是超过3秒才能出结果的优先拆成异步任务先给用户一个“正在查询”的反馈再通过轮询把结果补回来。这个体验设计对用户感知影响很大。3.2 代码实现和配置要点以TypeScript为例我会先在项目里创建一个技能文件比如skills/queryLogistics.ts。代码结构分为四个部分技能元信息、参数校验、业务执行、结果格式化。每部分都单独拆开后续扩展和维护都会方便很多。// skills/queryLogistics.ts export const queryLogisticsSkill { name: query_logistics, description: 查询订单物流状态和预计送达时间, triggers: [ 用户询问包裹位置、物流进度、预计送达时间, 用户反馈快递长时间未更新或疑似丢失, 用户询问发货状态或物流异常原因, ], parameters: { type: object, properties: { order_id: { type: string, description: 用户的订单号必须是纯数字字符串, }, }, required: [order_id], }, async execute(params: { order_id: string }) { // 参数校验 if (!/^\d{6,20}$/.test(params.order_id)) { return { success: false, message: 订单号格式不正确请确认后重试 }; } // 业务执行 const logisticsInfo await this.queryLogisticsFromAPI(params.order_id); // 结果格式化 if (logisticsInfo.completed) { return { success: true, data: { status: delivered, summary: 您的包裹已于${logisticsInfo.deliveredAt}签收, detail: logisticsInfo.trace, }, }; } return { success: true, data: { status: in_transit, summary: 包裹正在运输途中预计${logisticsInfo.eta}送达, detail: logisticsInfo.trace, }, }; }, };这段代码只做了一件事把技能的四层逻辑隔离开。参数校验放在最前面不让脏数据进入业务逻辑业务执行只负责对接外部API结果格式化负责把第三方接口返回的复杂结构翻译成用户能看懂的平实语言。第三个环节很容易被忽略但它直接影响用户对Agent的信任感——用户要的是“你的包裹预计明天下午送达”不是一串JSON。关于结果格式化我的原则是如果技能本身可以做就不要让模型再去加工。很多人习惯把第三方数据原样丢给模型让模型生成最终话术。这样做可行但会带来两个问题一是模型可能编造数据二是额外增加一次模型调用成本和延迟都上去了。技能层直接输出格式化结果只是在必要情况下让模型做润色这样最稳定。3.3 本地测试与效果验证技能写完之后不能只在正常流程里测一遍就完事。我会在本地做三轮测试正常路径测试、边界参数测试、意图干扰测试。正常路径测试就是给一个合法订单号看链路通不通边界参数测试是故意传空字符串、传字母、传不存在的订单号看技能能不能优雅地返回错误信息意图干扰测试是最容易被人忽略的我会准备一批和订单查询相近但实际不同的问法比如“帮我取消订单”“我要退货”看模型会不会错误地选到这个技能上。这一轮测试下来会发现不少问题。最常见的是参数schema定义太宽松导致模型把缺失值填成空字符串还有触发场景和类似技能重叠导致的误路由。发现问题后回到设计文档去改描述和参数定义迭代两三轮之后技能的稳定性才会明显提升。我在实践中还会用一些自动化的方式来做回归测试把历史对话样本整理成测试集每次修改技能描述之后跑一遍看选择准确率和执行成功率有没有下降。这听起来像是要做一套测试平台其实用简单的脚本加CSV就能跑起来# test_skill_routing.py import json test_cases [ {user_input: 我的快递到哪了, expected_skill: query_logistics}, {user_input: 这个订单还能改地址吗, expected_skill: update_address}, {user_input: 我要退货, expected_skill: return_apply}, # ... 更多历史对话样本 ] # 调用模型工具选择接口对比选择结果 def run_routing_test(model_client): total len(test_cases) correct 0 for case in test_cases: selected model_client.select_skill(case[user_input]) if selected case[expected_skill]: correct 1 else: print(fFAIL: {case[user_input]} - {selected} (expected {case[expected_skill]})) print(fAccuracy: {correct}/{total})如此反复技能描述的迭代就有了数据支撑不是靠拍脑袋改。4. 常见问题与排查技巧实录4.1 技能列表越加越多模型反而越选越乱这是我踩过最大的坑。技能从5个加到30个的时候模型的选择准确率不升反降。原因很好理解技能越多描述之间的语义重叠概率越高模型面对的决策空间越大。解决思路有两个收敛技能粒度和建立技能导航机制。收敛技能粒度就是把语义相似、执行逻辑相近的技能合并成一个。比如“查询订单物流”和“查询订单状态”如果在一个业务体系里语义高度重叠就直接合并。宁可一个技能内部多做几个分支也不要在顶层让模型自己去判断“用户到底要物流还是要状态”。建立技能导航机制则是提供一个顶层的“skill router”技能先让模型判断用户意图属于哪一类再分发给子技能。这相当于把一次大决策拆成两次小决策准确率会高很多。还有一个经验当技能数量超过15个时建议把所有技能名称和一句话描述生成一份索引交给模型先做“粗筛”再详细参考候选技能的完整描述。这种两段式选择能有效降低干扰。4.2 参数类型写对了模型还是传错值出现这种情况大概率是描述里没给足候选值或格式示例。比如一个参数定义成string类型描述只写“订单号”模型就有一定概率把用户输入的“我的订单”这种口语化表达原封不动地传进来。解决方式是把描述写具体明确值的来源、格式、必需长度、示例值。如果是枚举值就把枚举列表写进描述里如果是需要从历史记录里获取的就明确写成“根据用户上下文中的订单号字段获取不要猜测或编造”。参数层面的另一个坑是必填参数太多。本来一句话就能查到结果的事技能却要求用户提供登录态、订单号、验证码三个参数。模型只能去反问用户对话体验一下就差了。我的原则是尽量让参数可从上下文推导必填参数只保留真正必要的能自动获取的就不让用户填。4.3 工具调用无响应或超时Agent长时间不回复多半不是模型的问题而是技能执行卡住了。排查时我会先看日志确认技能是否真的被触发其次确认技能内部是否有第三方接口调用接口是否超时再看整个链路是否有重试机制。第三方接口超时是常见根源。我习惯给所有外部调用都加上超时和降级逻辑async function queryLogisticsFromAPI(orderId: string) { const controller new AbortController(); const timeout setTimeout(() controller.abort(), 3000); try { const response await fetch(https://api.example.com/logistics/${orderId}, { signal: controller.signal, }); return await response.json(); } catch (error) { // 降级处理返回缓存数据或友好错误提示 return { completed: false, trace: 物流信息暂时无法获取请稍后再试 }; } finally { clearTimeout(timeout); } }超时时间、重试次数、降级策略这些在设计期就要定好不能等上线了再补。另外如果多个技能会并发执行也要关注并发限制。很多API提供商对单账号有QPS限制技能层不做限流的话调用一多就会触发限流报错用户看到的反馈就是“服务暂时不可用”。4.4 技能组合起来就容易失控怎么办单个技能测试没问题一旦让Agent在一个任务里串联多个技能就开始出乱子。常见情况是模型先调了A技能拿到结果后又调了A技能一次然后才想起该调B技能。或者A技能的执行结果被B技能拿去当参数但格式对不上导致B技能报错。这个问题的根源在于技能之间的依赖关系没有设计好。我用的一个有效办法是在技能执行结果里明确标注哪些字段可以被其他技能使用以及它们的格式规范。相当于在技能之间定义了一份“数据契约”。另外一个更实用的办法把频繁组合的技能做编排。比如“查询物流推送客服反馈”这两个技能组合频次很高就直接做一个复合技能内部串行调用两个服务减少模型做决策的环节。这个做法牺牲了一些灵活性但换来了稳定性和响应速度在业务需求明确时非常值得。4.5 最后说权限与安全技能越做越多权限控制就必须跟上。我经历过一次安全事故一个技能本应只读数据但由于权限位配错了用户通过对话绕了一圈间接触发了一个写操作。那次之后我要求所有技能在元信息里强制声明权限级别。只读技能数据查询、信息获取写操作技能必须经过二次确认高危操作技能需要额外鉴权甚至审批流程模型在执行技能时其实并不知道操作的高危等级它只是按指令行动。所以技能设计者必须把安全兜底做在代码里不能指望模型自己能识别“这个操作是不是该停一下”。在技能描述里写清楚约束的同时执行层对关键操作也要有强制校验形成双保险。我现在对技能体系的感受是它不是一个静态的代码仓库而是一套持续迭代的能力系统。技能描述、参数定义、触发场景、权限策略都需要跟随业务变化不断优化。把技能当成一份长期维护的知识资产来运营而不是用完就丢的工具函数这个Agent才会越用越顺手。基于我自己的项目经验这样的认知转变比多写几个技能本身更有价值。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。