agent-skills:大模型Agent技能设计与落地规范

发布时间:2026/10/14 9:56:29
agent-skills:大模型Agent技能设计与落地规范
聊个我们内部折腾了小半年的东西agent-skills。准确说这是一套关于大模型Agent技能的设计与落地规范不是什么炫技框架而是一堆踩坑记录沉淀出来的方法论。起因很简单团队上线了七八个自动化场景从工单分类到周报生成全都在各自维护一份工具代码结果模型时不时选错函数、参数改个名字就要联动改一大堆。后来我们把所有可复用的能力统一收编为“技能”用统一的Schema描述、统一的执行入口、统一的错误处理才把这个问题彻底摁住。这篇文章就把整个设计思路、实现过程、踩过的坑原原本本写出来适合正在做Agent类应用、被工具调用问题折磨过的朋友参考。1. Agent技能到底是什么从会聊天到会干活1.1 为什么我最终选择了“技能”这套抽象最早我们做Agent自动化时思路非常朴素写一堆函数把函数名和描述塞给模型让它自己选着调。前两个场景跑得很顺但到第四个、第五个场景上线时问题开始集中爆发。第一是重复。同一个“查询工单状态”的逻辑在客服机器人里有一份在周报生成器里又复制了一份后来运维场景里还要再拷贝一次。改了一处另外几处立刻失同步。第二是描述混乱。不同函数对同一个参数用了不同的叫法有的叫ticket_id有的叫work_order_no模型在意图判断时很容易串。第三是缺少边界。模型一旦传了不合法参数函数执行到一半抛异常整个对话流程就卡死了用户感知非常差。后来我看了一圈业内普遍做法发现成熟一点的方案基本都会谈“技能Skill”这个概念。它和“工具”最大的区别在于工具只定义“能干什么”技能还定义“什么时候用、参数怎么约束、前置条件是什么、失败了怎么处理”。换句话说技能是把工具、描述、约束、错误策略打包成一个完整单元。我们内部这个体系就叫 agent-skills所有场景复用同一套注册机制和目录约定新增能力不再靠复制代码而是注册一个技能就好。1.2 技能、工具、函数调用边界到底在哪很多刚接触这块的人会混淆三个词函数调用、工具、技能。我用一张表直接说清楚它们的差异。概念核心问题包含内容典型形态函数调用模型怎么请求执行函数名、入参出参契约API接口定义工具能调用什么外部能力函数、API、命令行、插件可执行单元技能什么场景用、怎么用得对描述、Schema、前置条件、错误策略、版本信息带元信息的工具包可以这样理解工具是“零件”技能是“带说明书的零件包”。模型本身不会凭空知道一个函数是干什么的它完全依赖你给的描述来做选择。描述越完整、约束越清晰模型选对技能的概率就越高。agent-skills 做的事情说白了就是把“说明书”标准化让每一份说明书的写法、字段、执行入口都一致这样无论是模型理解还是工程维护成本都能降下来。用生活里的例子类比你去便利店买“速冻水饺”包装上一定写着配料表、烹饪方法、保质期。如果没有这些信息你买回去可能不知道怎么煮。技能就是这个包装模型则是那个拿货的人——给它好包装它才敢放心拿。2. 技能设计的四个核心原则2.1 粒度控制一个技能只解决一件事技能粒度怎么定是我们在落地时争论最多的问题。一开始有人把“获取天气”做成技能也有人把“完整处理一份工单查状态、分配负责人、发通知、写结论”整个流程做成一个技能。最后发现两种极端都有问题。粒度过小比如把“从数据库读取一行记录”做成技能模型需要频繁调用多轮才能完成一个用户请求对话次数爆炸、延迟飙升而且组合出错的概率成倍增加。粒度过大比如把整个工单处理流程塞进一个技能参数会有十几个内部暗含了大量假设一旦某个环节变化就得整个改版。我后来定了一个简单标准一个技能应该在一次推理决策内可以被讲清楚描述文本控制在三到五句话以内。如果介绍这个技能时需要列举超过三个前置条件或者超过五个必填参数那大概率是粒度太粗了需要拆。反过来如果模型的上一句话还没说完、下一句话又要连续调用同一个技能两次才能完成一个目标那大概率是粒度过细需要合并。实际执行中我们采用“技能组合层”来解决流程类需求底层保持原子技能比如“查询工单”“分配负责人”“发送通知”各是一个技能上层再用编排层内部叫 Workflow把多个技能串起来。这种分层方式既能保证技能的复用性又能承载复杂流程是我们在十几个场景里验证下来最稳的结构。2.2 参数设计入口收敛出口规范技能的参数就是模型和现实世界之间的契约设计得不好后患无穷。我从实操中总结了三句话必填参数越少越好可枚举值必须用枚举输出结构要有Schema。第一个原则必填参数尽量精简。每次让模型多填一个参数就多一次理解偏差的可能。比如周报生成技能其实只需要“仓库路径”和“起始日期”两个核心参数仓库分支名、提交作者、是否包含空提交这些都可以做成可选参数并给默认值让模型在不确定时不传即可。第二个原则能用枚举就不要用自由文本。领域、状态、类型这些字段务必在Schema里列出候选值。模型对自由文本的“自由发挥”远超你的想象明明要求传statusresolved它可能传resolved也可能传已解决甚至传fixed。枚举约束能把这类问题直接消灭在定义阶段。第三个原则输出也得定Schema。很多人只约束入参不约束出参导致技能执行完返回一段自由文本模型还要二次解析。正确做法是技能执行完成后统一返回结构化结果比如状态码、业务数据、人类可读消息三个字段。下游无论是要继续推理还是落库、展示都能直接消费。2.3 描述文本是给模型看的不是给人看的这是我在 agent-skills 里感受最深的一点。写技能描述时团队里写代码习惯好的人描述都写得特别短比如“生成周报”。但模型不是靠读变量名理解语义的它需要你明确告诉它这个技能解决什么问题、在什么场景下触发、有哪些边界情况不要用它。我建议描述文本按这个结构写角色的定位 触发的意图场景 不适用场景 特殊注意点。举个例子一个“代码评审总结”技能描述可以写成“根据代码评审记录生成结构化总结适合在代码评审结束后使用当用户仅询问评审流程规范而非总结具体评审内容时不要调用该技能”。后半句的负向描述能明显降低模型乱调用的概率。另外有一个容易忽视的细节技能描述里的名词要和用户真实表达保持一致。用户大概率会说“查一下余额”而不是“调用AccountBalanceQuerySkill”。所以描述里一定要覆盖口语化的触发词我们团队内部要求每个技能至少写两个用户表达样例。实测下来有样例的技能触发准确率比没有样例的高两到三成这个差距非常可观。2.4 技能内部必须自带错误处理很多技能的首次实现都默认“参数合法、环境正常、外部服务不超时”。真实情况是参数出格、上游服务抖动、文件权限不对任何环节都可能挂。如果技能只管抛出异常模型拿到的就是一段堆栈信息既没法理解也没法给用户一个体面的答复。我们的做法是在每个技能内部统一套一层兜底逻辑捕获所有异常转成结构化的错误对象对象里至少包含code、message、retryable三个字段。比如文件不存在返回codeFILE_NOT_FOUND外部接口超时返回codeUPSTREAM_TIMEOUT, retryabletrue。模型看到retryabletrue时会主动选择重试或者换个姿势看到retryablefalse时会直接向用户解释失败原因。这里要特别提醒一个反直觉的坑不要把错误信息设置得过于“体贴”。我们曾经把底层报错全部翻译成“系统繁忙请稍后再试”结果模型拿到这个信息后完全无法判断下一步该干什么只能结束对话。正确做法是错误信息既要人类可读又要保留足够的机器可解析信息让模型有决策依据。3. 从0到1实现一个技能周报生成器完整实操3.1 技能目录与Schema约定空谈设计没用我直接用一个真实技能“周报生成器”演示完整落地过程。这个技能做的事情非常具体拉取指定Git仓库最近一周的提交记录按作者和功能模块聚合输出一份结构化周报。agent-skills 的目录约定是每个技能一个文件夹内部必须包含skill.yaml描述与参数Schema和implementation.py执行逻辑可选tests/目录。skills/ └── weekly_report_generator/ ├── skill.yaml ├── implementation.py └── tests/ ├── test_normal.py └── test_error.pyskill.yaml是核心模型能不能理解、调度层能不能正确传参全看这份文件。下面是我们用了一段时间后比较稳定的写法name: weekly_report_generator version: 1.0.0 description: | 根据代码仓库的提交记录生成结构化周报。 适合在用户要求“生成周报/本周工作/最近一周提交总结”时使用。 当用户只是询问某些代码的修改内容而不是要求汇总周报时不要调用该技能。 inputs: - name: repo_path type: string required: true description: 代码仓库的本地绝对路径 - name: since_date type: string required: false description: 统计起始日期格式 YYYY-MM-DD默认最近7天 default: - name: granularity type: string required: false enum: [author, module, day] default: author description: 汇总维度author表示按作者module表示按功能模块day表示按日期 outputs: type: object properties: code: { type: integer } summary: { type: string } items: { type: array }这里有几个字段单独解释一下。description里我特意放了触发场景的正例“生成周报/本周工作”和负例“只是询问修改内容时不要调用”这比单纯写“生成周报”要有效得多。granularity用枚举约束模型只能在三个值里选绝不允许自由发挥。since_date给了默认值语义模型不传时技能内部自己计算最近七天。3.2 核心实现代码与逐段解析接下来看implementation.py这个文件的结构在所有技能里是统一的接收ctx对象读取参数执行核心逻辑返回结构化结果。import subprocess from datetime import datetime, timedelta def execute(ctx): params ctx.params repo_path params[repo_path] granularity params.get(granularity, author) since params.get(since_date) or ( datetime.now() - timedelta(days7) ).strftime(%Y-%m-%d) try: log git_log(repo_path, since) except Exception as exc: return ctx.error(GIT_READ_FAILED, f读取仓库日志失败: {exc}, retryableTrue) if not log: return ctx.ok(最近一周没有任何提交记录, []) grouped aggregate(log, granularity) return ctx.ok(build_summary(log, grouped), grouped) def git_log(repo_path, since): cmd [ git, -C, repo_path, log, --since since, --prettyformat:%H|%an|%ad|%s, --dateshort, ] result subprocess.run(cmd, capture_outputTrue, textTrue, timeout15) if result.returncode ! 0: raise RuntimeError(result.stderr.strip()) return result.stdout.strip().splitlines() def aggregate(log, granularity): items [] for line in log: commit_hash, author, date, message line.split(|, 3) key {author: author, day: date, module: guess_module(message)}[granularity] items.append({key: key, message: message, hash: commit_hash}) return items这段代码有几个细节是刻意为之的。git_log里加了timeout15避免仓库过大时 git 命令无限挂起这是技能并发问题里最常见的隐患之一。execute函数包住所有可能抛异常的地方统一转成ctx.error返回结构化错误附带retryableTrue让模型决定是否重试。aggregate中key的取值完全依赖枚举映射即使granularity传了意外值也会因为 KeyError 被外层捕获不会污染数据。ctx.ok和ctx.error是 agent-skills 执行框架统一提供的包装方法。返回结构固定为{code: 0, summary: ..., items: [...]}或{code: 非0, error: {...}}。下游模型看到code0就知道技能执行成功直接基于summary和items生成用户可读的报告即可。3.3 本地调试与回归验证技能写完不能直接用必须在本地模拟模型调用来验证。我习惯写一套“迷你测试集”模拟不同用户表达确认技能能被正确触发、参数能正确映射。为这件事我写了一个十几行的调试脚本核心思路是把skill.yaml里的description和参数 Schema 拿给模型让它针对测试用例返回调用请求然后检查它是否选对了技能、传对了参数。test_cases [ {user: 帮我生成这周的周报, expect_skill: weekly_report_generator, expect_params: {repo_path: ...}}, {user: 最近七天大家提交了什么, expect_skill: weekly_report_generator}, {user: 某个提交为什么改了这些代码, expect_skill: None}, ]第三个用例是最重要的负例确保模型在用户只是询问具体代码修改时不会错误触发周报技能。我把这类负例看得比正例还重因为误触发造成的干扰比漏触发严重得多。整个调试过程其实就是不断调整description措辞直到正例负例全部通过。4. 上线后遇到的高频问题与排查实录4.1 模型就是不调用你的技能这是上线初期最让人头疼的问题技能写得很好人一用就知道该调但模型死活不触发。排查下来绝大多数原因是描述和真实用户表达错位。用户习惯说“整理一下本周的活”而我们的技能描述只写了“生成周报”模型自然匹配不上。解法就是回头扩充描述里的触发样例把用户可能说的各种口语化表达都写进去。注意不要只加同义替换要加不同句式的样例比如疑问句“这周都干了啥”、祈使句“帮我总结一下这周的工作”。另外一个容易被忽略的点技能名称本身也会影响触发建议名称用业务词汇而不是技术词汇weekly_report_generator就比repo_stats_aggregator更贴近用户心智。4.2 参数错位与类型混乱参数问题出现频率最高的是日期格式和枚举取值。模型有时把日期传成带时分秒的完整时间有时把枚举值传成同义的中文。这两种情况都在运行时才暴露调试成本很高。我们的解法是在框架层做两层防线。第一层严格 JSON Schema 校验类型不合法直接拒绝并提示模型重新传参。第二层运行时做一次轻量归一化比如日期统一转成YYYY-MM-DD枚举值转成小写并与候选值做模糊匹配。这两层配合下来参数错误导致的失败率从最初的百分之十几降到了个位数。4.3 并发与超时问题技能内部如果执行超过模型对话的超时时间整个会话就会被截断用户那边看到的是“对话中断”非常糟糕。周报生成这类技能遇到大仓库时git 命令可能跑几十秒远超对话容忍度。我建议所有技能默认设置内部超时并按“可重试”和“不可重试”分类处理。可重试的超时错误让模型提示用户稍后重试不可重试的直接返回错误说明。更进一步对耗时任务做异步化改造技能执行后立即返回{code: PENDING}完成后通过回调通知模型继续生成结果。这套异步方案虽然实现成本高一些但在生产环境里是值得投入的。4.4 技能改版引发的连锁故障技能上线后一定会迭代最常见的问题是新版本参数变化但旧会话还带着旧Schema在跑导致调用错位。我们踩过一次给周报生成器加了必填字段结果存量会话请求全部失败。现在的解决方案是版本管理。skill.yaml里的version字段必须语义化主版本号变化意味着参数契约变化调度层需要做新旧版本兼容次版本号变化只是描述或逻辑微调可以热更新。每次改版前还要跑一遍回归测试集确认正例负例不劣化。这套流程看起来繁琐但能避免你在凌晨两点被线上告警叫醒。5. 一些值得长期坚持的习惯最后分享几个我在 agent-skills 整个落地过程中最有体感的习惯不一定写在任何文档里但对长期维护非常关键。第一把“测试集”当资产而不是临时脚本。每上线一个新技能就把它的正例和负例补充进统一回归集。后面任何技能描述改版先跑一遍全量回归。模型选型变了也要重跑因为不同模型对同一份描述的敏感度差别很大。这套回归集是我们做技能优化的底气没有它后续所有调整都是在盲改。第二给技能执行加上全链路日志。每个技能执行完记录下模型传入的原始参数、技能返回结果、耗时、命中版本号。后续定位问题第一件事就是查这条日志链路。尤其要记录“模型没调用技能”的情况这种负日志往往比调用成功的日志更有价值能帮你发现描述盲区。第三克制技能的膨胀。技能数量变多之后模型的选择准确率会下降这是我们在实践中发现的规律。所以每隔一段时间我们就会合并同类的技能、清理低频技能。与其维护一百个细碎技能不如维护二十个设计良好的技能。少即是多这句话在Agent技能设计里同样成立。如果你也在做Agent相关的东西建议从最简单的一个场景开始把“技能”这套规范立起来而不是继续往工具列表里塞函数。一开始多花一点时间把描述、Schema、错误处理定好后面所有场景都能复用这套底层能力。这个改造带来的收益会比预想中大得多。