
做个事情之前先说个真实翻车现场。上个月我给一个客服方向的 Agent 排演流程按理说它手里已经有了好几个写好的 Skill能查订单、能退换货、能算运费跑起来却三连跪先是把查订单的入参填成了物流单号接着在需要调用退换货 Skill 的时候死活不肯调最后直接把一段不该给用户看的内部排错信息当成话术发了出去。排演结束一个同事说了一句让我印象特别深的话你这不叫给 Agent 配了 Skill你是往它背包里塞了一堆没贴标签的工具。这话点醒了我。真正的问题不是有没有 Skill而是有没有一套让 Skill 能被正确挑选、正确调用、正确交接的工程工作流。市面上谈 Skills 的文章大多在讲怎么写一个 Skill写个名字、写段描述、写个入参出参完事。可一旦放到生产环境Agent 会面对几十个 Skill、几百种输入、天上一脚地上一脚的用户表述零散写出来的 Skill 跟工程化配出来的 Skill表现差距是断崖式的。这篇就聊聊我给 AI Agent 配 Skills 时沉淀出来的一套工作流从规则文件、API 封装、编排策略到质量保障和团队协作。适合手上已经跑过几个 Agent Demo、正准备往生产环境推的开发者也适合刚入行但不想走弯路的人。我不会给你一套花架子理论全是从项目里磨出来的做法、踩过的坑、以及最后保留下来的套路。该给代码的地方给代码该给理由的地方给理由咱们按工程的逻辑来。1. 从会写 Skill到能上生产卡点到底在哪1.1 一个典型的 Demo 翻车现场先复盘一下我开头说的那次翻车。那个客服 Agent 其实组装得很用心Skill 文档写得也算完整每个 Skill 都有 name、description、parameters字段类型和枚举值也都标了。按理说模型调用工具不会太难现场却连续出状况。第一个状况查订单这个 Skill它的参数其实定义得很清楚要求传入order_id或customer_id但模型在用户只报了手机号的情况下自作聪明地把手机号填进了一个不在 schema 里的字段phoneSkill 执行器一校验就报错Agent 当场懵住。第二个状况用户问的是我东西坏了想换这属于退换货流程但上下文里刚好有一条历史消息提到过订单Agent 就一直在查订单 Skill 里打转压根没意识到该触发另一个 Skill。第三个状况最要命某个 Skill 内部报了个 500 错误返回了一段长长的堆栈跟踪模型把堆栈里的英文技术名词原样输出给了用户。事后复盘三个状况的根源其实不完全是模型不够聪明而是我的工程工作流没做到位。参数校验没做过归一化错误信息没做用户安全过滤Skill 之间的边界太模糊导致模型在模糊语义下选错了工具。这些坑单写一个好 Skill 文档是躲不掉的。只有当我把 Skills 当成一套有目录、有规则、有接口约束、有监控的工程系统来对待的时候问题才开始成批消失。1.2 我们真正需要的不是一个 Skill而是一套工作流给 Agent 配 Skill这个说法听起来像是在做加法好像写几个函数、挂几个 markdown 文档Agent 就变强了。实际你把它当成一个软件系统来看Agent 是执行者Skill 是操作手册你要交付的不是手册文本而是让 Agent 在你给定的边界里稳定运行的一套机制。机制包含四个层面语义层Skill 名字、描述、参数定义是否足够清晰让模型能准确理解这个工具是干嘛的什么时候该用什么情况下不要用。执行层Skill 内部代码是否健壮入参校验是否严格错误是否被安全地翻译成模型能理解的信息。编排层当多个 Skill 需要串行、并行、分支、重试时由谁决定下一步调用哪个依据是什么。质量层改动一个 Skill 之后如何保证其他 Skill 不被带崩如何量化Agent 工作流变好还是变坏。这四层合起来才是我说的工程工作流。单一 Skill 解决的是一件事工作流解决的是让 Agent 稳定做好一连串事。后者的复杂度高一个量级但也是真正把 Agent 从玩具变成生产力工具的分水岭。后面每一节我都会按这四层展开。2. 先把门票和规则文件设计清楚Skills 的骨架先说结论我后来给每个 Skill 都配了三种文件——规则文件、门票文件、清单文件分别对应这个 Skill 该遵守什么纪律这道门允许谁进、拒绝谁这个 Skill 到底有哪些能力可供调用。三者加在一起才算一个完整的工程化 Skill。2.1 用规则文件把系统提示词盘活很多人写 Skill 只写一层配置给模型的 description。但模型看到的那段 description 能承载的信息量非常有限你不可能在 200 字描述里塞下完整业务规则。把规则外置成一个文件按需注入系统提示词是我做过的最有效的一件事。举个例子。我有一个用来处理发票的 Skill如果只在 description 里写用于处理发票相关操作模型根本不知道发票抬头和税号必须匹配、专票和普票不能混用、金额超过某个阈值需要二次确认。你把这些规则写进一个RULES.md在 Skill 被触发时由执行器把它注入到系统提示词里模型在真实执行前就能读到约束。规则文件的内容编写有一条铁律只写约束不写废话。别在里面讲背景故事、讲动机、讲为了更好的用户体验模型读到这些不会变聪明只会稀释重点。我自己的格式固定为三类必须做的明确写出在任何情况下都必须执行的步骤比如调用开票接口前必须先校验税号格式。禁止做的写清楚红线比如禁止将内部 API 错误信息直接透传给用户。条件分支以 if-then 形式给出判断规则帮模型省去自己推理的成本比如如果发票金额超过 5000则必须进入人工审核节点。有次我把规则文件写得太长将近三千字结果模型在执行简单任务时反而变迟钝因为它在系统提示词里要记住的东西太多。后来我把规则文件按场景拆成多份每次只注入和当前 Skill 相关的部分效果立刻回来了。所以规则文件不是越全越好而是该出现的规则必须出现不该出现的绝对不出现。2.2 门票是谁的角色扮演门票是我自己取的名字原意是入场券。它的作用是给 Skill 定义一个明确的调用边界只允许符合条件的人或上下文进入。具体来说门票文件定义三个字段triggers哪些用户意图或关键词会让系统考虑这个 Skill。preconditions调用前必须成立的条件比如必须已登录必须存在有效订单号。guardrails拒绝调用的规则比如当用户情绪为强烈投诉时不得直接触发退款需转人工。门票存在的意义是减少模型在意图不明确时的自由发挥。我们做过一次 A/B 对比同样的客服 Agent加了门票机制之后模型错误调用 Skill 的比例降低了约 40%。原因不复杂模型在犹豫时你给它的不是一个看起来相关的工具而是一组明确的判断条件。它可以先查门票发现前置条件不满足就主动走别的流程而不是硬调用然后执行失败。但要注意门票不是死的。用户意图千变万化你不能指望三个关键词就覆盖所有情况。我通常把门票设计成可放行和可拒绝两套条件放行条件写得宽一点召回优先拒绝条件写得严一点精准拦截。这样宁可让模型多考虑某个 Skill也不要在用户真正需要的时候把它漏掉。2.3 清单文件不让人猜的功能入口清单文件是给人看的也是给被测系统看的。它定义这个 Skill 对外暴露的功能入口有哪些、输入输出是什么、版本号是多少。说白了这就是 Skill 的 API 文档。清单文件里我最看重的是两样东西候选 Skill 列表和参数示例。模型选择 Skill 时实际上是拿用户的输入去匹配每一个 Skill 的描述如果你的描述太泛它就会把所有 Skill 都当作候选如果太窄它又可能漏掉正确选项。参数示例则是给模型抄作业的我见过太多模型因为不清楚日期格式、不清楚金额单位把参数填得千奇百怪。每写一个参数类型我至少配两个合法示例和一个非法示例模型看到示例后填错率肉眼可见地下降。清单文件要配合一个总目录文件使用。总目录文件是给模型的一页纸上面列着目前系统里有这几个 Skill分别负责什么、什么情况用哪个模型在做工具选择前先读总目录再点进清单文件看细节。这个先总后分的结构比一股脑把所有 Skill 配置全部塞给模型要清爽得多也降低了上下文长度超限的风险。3. 把 Skill 变成可组合的原子能力API 封装与权限边界你写出来的 Skill 如果只能被当前这个 Agent 用那它只是临时代码。工程化工作流要求每个 Skill 像乐高块一样可以被组合、被替换、被独立测试。做到这一点的前提是把 Skill 的执行逻辑从和 Agent 强耦合的脚本改造成有明确输入输出契约的内部 API。3.1 内部 API 的归一化先统一 Schema再谈其他刚开始搞 Skills 那会儿我们的代码库里每个 Skill 都有自己的入参风格。有的用 JSON 对象有的用扁平字符串有的把鉴权信息塞在参数里。结果编排层想统一调用发现根本没法下手每接一个新 Skill 就得多写一段 adapter。后来我定了一条规矩所有 Skill 一律暴露统一格式的 JSON Schema 接口。入参只允许在params对象里声明出参统一包一层{ status, data, error }。status 取值为success、fail、error其中fail表示业务上合法但没有达成目标比如该订单不支持退款error表示系统异常。这个区分很重要模型看到fail会知道换个策略再来一次看到error则不该盲目重试。归一化带来的收益是立竿见影的。编排层只需要学会读一种返回格式技能加载器可以自动完成参数校验测试框架也能用同一套断言逻辑去测所有 Skill。如果你现在面对的是一堆参差不齐的旧 Skill值得花一个迭代周期专门做归整长痛不如短痛。3.2 访问控制Skill 不是拿到就能跑的原子弹很多人误以为Skill 让模型能自主调用的能力越强越好。我的经验恰恰相反Skill 的权限边界应该按照最小够用原则来划。比如一个数据分析 Agent它可能有一个跑 SQL 查询的 Skill还有一个读取数据字典的 Skill。如果把数据字典直接允许写权限模型就可能在推理过程中误改表结构。我会在 Skill 的服务层做一道显式的权限校验不只依赖模型的自觉。每个 Skill 在执行前先走一层鉴权确认被调用的操作read / write / admin在本次会话的作用域内。模型是概率系统它的判断不可能 100% 可控所以你必须在执行器这一层加上硬边界。比如有一个 Skill 能调用外部第三方接口我会要求它必须经过一个代理通道通道里统一做超时控制、接口限量、敏感字段脱敏。Agent 永远拿不到第三方接口的原始凭证只拿到一次性的短期令牌。这套执行器兜底的设计比寄希望于模型不乱来要靠谱得多。3.3 可观测性每个 Skill 都要能回答三个问题生产环境里Skill 一旦出错你要能马上回答三个问题哪个 Skill 被调用了调用时传了什么关键参数返回了什么结果如果答不上来那你基本只能靠猜。我给每个 Skill 都做了统一的 trace 埋点。埋点字段固定包括skill_name、request_id、params_hash、status_code、latency_ms、error_type。这些字段会在 Agent 运行时的 trace 链路里串起来和一个完整的会话 ID 绑定。将来做问题复现只需要拿 request_id 就能查全链路。有一次线上 Agent 行为异常用户说了一句我要改绑银行卡Agent 不停重复调用一个身份核验 Skill把用户当复读机一样反复提问。如果当时没有这套埋点我根本不知道是哪个环节在死循环。后来看时间线发现是身份核验 Skill 返回了fail编排层收到fail后没有走转人工分支而是直接重试于是 Agent 就卡在循环里。这暴露的是编排问题但它是在可观测性完善之后才被迅速定位到的。4. 编排层真正把多个 Skill 串成一条工作流有了原子能力和清晰接口下一步就是编排。编排层要回答的核心问题是Agent 在什么条件下、按什么顺序、调用哪些 Skill以及调用失败后怎么办。没有编排Skills 再多也是一盘散沙。4.1 委托分发 vs 硬路由让 Agent 自己认领任务我在项目里试过两种编排思路一种叫硬路由一种叫委托分发。硬路由是你在代码里用 if-else 写死意图到 Skill 的映射比如用户提到退款就走退款 Skill。好处是行为完全可控坏处是语言表达稍一变化就会漏接。委托分发是让 Agent 根据用户的输入自己从候选 Skill 总目录里认领任务。这是更聪明的方案因为模型对语义的理解比一堆关键词规则强。但完全放任也不行所以我做了一个折中先跑一次轻量的意图分类提醒可以很粗糙甚至用规则加模型混合圈定 2-3 个候选 Skill然后让模型在这 2-3 个里面做最终选择。这比让模型面对几十个 Skill 做开放式选择要稳得多。候选缩圈可以用一个很朴素的实现给每个 Skill 打上业务域标签比如订单域支付域账号域先根据用户输入做一次域判断再在该域内放行候选 Skill。我记得最早那版连模型分类器都没用就是靠关键词先粗分效果也还行。关键是别让模型做大海捞针式的选择它的选择空间越小正确率越高。4.2 分支与重试不把错误甩给下一环Skill 的执行结果不只有成功和失败还有条件不满足需要补信息需要人工介入等等中间态。编排层必须明确处理这些状态否则 Agent 就会陷入重试-失败-再重试的死循环甚至把错误一路甩给后续环节。我总结了一套状态处理经验表返回状态含义编排层处理策略success成功正常进入下一步或结束fail业务上不可行停止当前流程向用户解释或转其他 Skillerror系统异常报告错误禁止无脑重试最多做一次带退避的重试need_input参数缺失或需澄清生成追问问题向用户收集更多信息重点说fail和error的区别。fail是这个单子不能这么干比如用户要退款但订单已关闭重试一万次也没用。error是系统出岔子了比如接口超时这种时候短暂重试是有意义的。很多 Agent 出问题就是因为把这两者混为一谈该放弃时疯狂重试该重试时直接放弃。把状态语义理清编排层的每个分支就有了明确依据。分支之后还要考虑上下文接管。一个 Skill 走到一半因为缺信息停下了下一次用户补充信息后Agent 是要从头走还是从停下来的节点继续纯靠模型记忆不可靠我的做法是把流程快照存成一个结构化对象里面记录当前处于第几个 Skill、已收集到哪些字段、还缺哪些字段。下一次用户输入进来编排层拿快照去匹配就知道该续跑还是该换路。4.3 一个简短的编排示例把步骤讲透如果上面的概念太抽象我放一个简化版编排算法的伪代码它足以支撑一个中型客服工作流async function runPipeline(session, userMessage): # 1. 读取会话快照决定是续跑还是新任务 snapshot loadSnapshot(session.id) if snapshot is None: candidates narrowCandidates(userMessage) # 先做意向粗分 skill routeByLLM(candidates, userMessage) # 再在缩圈内选最终 Skill else: skill snapshot.current_skill skill fillMissingParams(snapshot, userMessage) # 2. 执行 Skill并处理状态 result executeSkill(skill, session, userMessage) if result.status need_input: saveSnapshot(session.id, {current_skill: skill, missing: result.missing}) return buildClarifyPrompt(result.missing) elif result.status fail: clearSnapshot(session.id) return handoffToManual(result.reason) elif result.status error: retry canRetry(result, session.retry_count) if retry: return retrySkill(skill) clearSnapshot(session.id) return reportSystemError() else: clearSnapshot(session.id) next_skill decideNextByFlow(skill, result.data) if next_skill: return runPipelineWithSkill(session, next_skill) return buildFinalReply(result.data)这段代码的精髓不在具体函数实现而在它表达的工作流心智模型每个环节都清楚自己处于什么状态失败时知道往哪走成功时知道下一步是谁。实际工程里你还会加超时控制、并发控制和人工兜底但骨架就是这样。5. 质量建设给工作流上体检仪而不是事后道歉Skills 工作流一旦跑起来你会发现它跟传统软件有个很大的不同同一个输入模型每次输出的内容不完全一样。这导致你没法用跑一次通过了来验证质量。质量建设是工程工作流的最后一块拼图也是很多人都欠着的。5.1 离线评测集把关键路径圈起来我做的第一件事是建立一个离线评测集里面放几十上百条典型用户请求每条都标注好期望触发哪个 Skill、期望哪些字段被正确填充、期望最终回复里包含什么。评测集就是 Agent 的单元测试。构建评测集有技巧不能只放标准问法。我会按四类收集正常清晰表达、模糊省略表达、含干扰信息的表达、边界条件表达。模糊表达这类特别重要比如用户说那个单子我不要了没有提到退款取消订单任何关键词但期望是触发取消订单 Skill。你不把这些写进评测集模型大概率永远学不会在这些表达下正确选路。有了评测集之后每次改 Skill 描述、改规则文件、改编排参数都拿评测集完整跑一遍。我们内部管这个叫回归跑。别嫌慢这套东西救过我太多次有一次只是把某个 Skill 的 description 多加了几个字结果让十多个模糊表达全部跑到错误分支去了没有回归跑根本发现不了。5.2 影子模式与金丝雀让线上帮你改错离线评测集的局限在于你覆盖不了所有真实请求。所以我会用影子模式做补充。影子模式的思路是线上用户请求照常走旧流程但同时把同一份输入送给新版本工作流跑一遍只记录结果不真正产生影响。过一段时间对比新旧版本在Skill 选择正确率错误率有效回答率上的差异达标后再切换线上。这个过程要小心延迟和成本。影子模式相当于把一次请求跑两遍费用翻倍是正常开销。我会抽一小部分流量做影子比如 10%而不是全部。如果影子模式表现良好再走金丝雀发布让新版本只覆盖 5% 的线上流量跑半天没问题再逐步放大。这套发布节奏听起来繁琐但在 Agent 这种输出不可完全确定的系统面前是保住口碑的基本盘。我见过有人跳过这些直接全量上结果线上用户被同一个 bug 折磨了一天第二天才回滚影响极差。5.3 回归指标不需要玄学质量建设最怕感觉变好了这种模糊结论。我给自己定了几条硬指标每次版本迭代都看这些数字Skill 选择准确率期望 Skill 与实际触发 Skill 一致的请求占比。这是全局最重要的指标。参数填充完整率调用 Skill 时必填字段的填充正确比例。零回复 / 错误回复率Agent 出现无意义回复、直接将错误暴露给用户的比例。无效重试率同一会话中同一 Skill 连续失败两次以上的次数占比。人工介入率用户请求最终需要转人工的比例。降低它能直接说明工作流变强了。这些指标不用在每次迭代里全部提升但至少不能明显回退。我个人的底线是Skill 选择准确率低于 90% 的新版本不发布。低于这个数用户体感上的智障时刻会密集到让人崩溃。6. 演进路径从单体 Skill 到团队级 Kit最后一块聊聊 Skill 工作流在团队和项目规模变大之后怎么演进。一个两个人维护的 Agent 项目和一个十个人维护的 Agent 项目面临的问题是截然不同的。但这块的经验我想留给真正开始做工程化建设的人。6.1 复用、版本、跨团队协作当团队里有多个业务线都在写 Skills你会碰到一个经典问题同一个能力A 组写了一份B 组又写了一份只是参数结构不同维护成本翻倍。解法是建立共享的 Skills Kit——把通用的能力身份鉴权、订单查询、消息通知、支付状态等抽成一个基础套件各业务线只做差异化部分。Kit 必须带版本号。我们吃过一次亏A 团队升级了一个共享 Skill 的内部逻辑忘记同步版本描述B 团队的 Agent 在不知情的情况下调用了新逻辑结果某个旧流程直接崩了。后来规定共享 Skill 的任何变更必须走三个步骤升级版本号、更新 changelog、通知所有使用方回归验证。跨团队协作的另一条经验是把Skill 的测试用例当作和代码同等重要的交付物。当你从别的团队接手一个 Skill第一件事不是看它怎么实现而是跑它的评测集看它的边界在哪。有评测集你才敢放心组合没评测集你拿到的只是一个黑盒出了问题只能互相甩锅。6.2 性能预算和依赖治理Skill 越多Agent 需要考虑的空间就越大响应延迟也会上升。建立性能预算是必要的。我自己的粗略经验候选 Skill 数量超过 30 个时模型单次选路的成功率会出现肉眼可见的下降。所以除了总目录之外我会在编排层做一层索引不让模型直接面对全部 Skill而是根据用户聚类先过滤到 10 个以内。依赖治理同样重要。Skill 之间如果出现 A 调 B、B 调 A 的循环依赖轻则超时重则死循环。我规定 Skill 之间的调用必须经过编排层不允许 Skill 内部直接调用另一个 Skill这样依赖关系就保持为可审计的树形结构。一旦出现环编排层的依赖图能直接把它检查出来。做依赖图时可以用简单的静态分析脚本扫描代码里声明的依赖关系即可。遇到跨 Skill 引用就提醒开发者走编排层做显式编排不要偷偷绕过。6.3 我自己踩过的三个教训写到这收尾前分享三个我这里真金白银踩出来的坑。第一个别在 Skill 描述里堆砌高级词汇。我曾为了让一个文档类 Skill 看起来更专业写了一大段语义增强知识抽取结构化推理之类的词结果模型反而把它当成了一个万能的总结工具什么任务都往它身上招呼。Skill 描述的风格应该是平实、具体、可验证。描述里的每一个形容词都要问自己它能让模型更准确判断什么时候用这个 Skill 吗不能就删掉。第二个别让 Skill 的报错太诚实。系统错误堆栈对你来说是信息对用户来说是灾难对模型来说是可以原样转述的上下文。所以 Skill 执行器的出口必须做统一的错误翻译层把内部错误转成一句模型能理解、用户能接受、又不泄露敏感信息的话。这个翻译层是我的工作流里永远不省的模块。第三个别把评测集变成一次性工程。我最早建评测集的时候花了一天整理了一百条用例用完就扔在仓库角落再也没更新过。后来线上出现了大量新的用户表达方式评测集完全覆盖不到等于形同虚设。现在我把评测集的更新和维护当成日常工作每个月至少加一批新用例来源就是线上真实请求里那些表现不佳又值得修的样本。评测集只有在不断生长时才有价值。如果只让我留一条经验那就是Agent 的能力上限由模型决定但能力的下限由工程工作流决定。你配不配一套好的 Skills 工作流直接决定了用户遇到的是偶尔聪明的玩具还是稳定可靠的工具。多花时间在工程化上你的 Agent 迟早会回报你。