
让Agent的手真正可控我维护 agent-skills 技能库这段时间的实践总结最近大半年我一直在折腾一件事把各种杂七杂八的 Agent 能力沉淀成一个内部代号叫 agent-skills 的技能库。起因是手上有好几个项目都在做智能体做着做着发现一个问题——模型选型、Prompt 调优这类事反而不是最费时间的真正的痛点是每个项目都要重新接一遍工具、重新理解一遍模型到底该怎么调用这些能力。这个技能库解决的是 Agent 开发里最容易被低估的一层模型的手。也就是工具调用、技能编排、执行反馈、观测评估这整条链路。如果你也在做大模型应用尤其是那种需要模型调用多个外部系统、处理真实业务的 Agent这篇文章应该能帮你少走不少弯路。我会把我这段时间的设计思路、实现细节、踩过的坑以及技能库怎么和评测数据形成闭环一次性讲清楚。1. 为什么说Agent开发最耗时的是能力层而非模型本身1.1 从Chatbot到Agent差的不是模型是工程很多人以为 Agent 就是对话模型 调接口但实际从 Chatbot 进化到生产级 Agent中间隔着的不是模型能力而是工程化程度。我见过好几个团队把单轮对话的 Demo 跑通后就兴冲冲去做多轮 Agent结果第一周就卡住了模型不知道怎么从十来个工具里挑出正确的那一个调完工具返回了非预期结果模型也没法优雅恢复更别提并发场景下技能间的状态互相污染这种低级但致命的 bug。这里有个很根本的差距。单轮 Chatbot 是模型生成文本Agent 是模型做出决策并执行。一旦涉及执行你就得操心三件事模型能不能找到正确的技能参数能不能填对执行失败以后模型能不能自己兜底。这三件事没有一件是模型自己可以解决的全部要靠外面这层工程代码来托底。我自己总结过一张表标出了三层应用对工程能力的要求差异层级核心任务对技能层的要求最常踩的坑单轮回答生成文本基本没有幻觉、事实性错误带工具调用一次调用返回结果能选对工具、填对参数工具太少还好一多就乱生产级 Agent多步规划、状态管理、失败恢复技能注册/路由/观测/评估全要技能调用不可控、无法复盘到第三层的时候技能就不是一堆函数了它本质上是模型和环境之间的一个契约层。这个契约层如果做得乱再强的模型也发挥不出来。1.2 技能库的本质把能力的使用规则显式化那为什么不能每个项目各自接一遍工具当然可以成本低的时候没问题。但当你有几十个技能、多个 Agent 共用你就会发现缺一个统一的抽象层。agent-skills 想做的事情就一句话把每一个能力封装成模型看得懂、系统跑得稳、后期可观测的标准技能单元。说得再直白一点就好比你把家里所有螺丝刀、扳手、电钻从抽屉里倒出来然后按照用途、规格、使用场景分门别类贴好标签放进工具墙而不是每次要用的时候翻半天。传统工具调用就是抽屉里乱放螺丝刀——函数散落在代码各处参数定义只有程序员自己看得懂模型能不能正确调用全靠蒙。而技能库则是贴上标签的工具墙——每个技能有标准化的描述、参数校验、执行规则、失败出口模型只要会读菜单就能正确点菜。这个抽象层带来的直接好处是新项目接入时不用从零开始理解每个接口的怪癖技能库本身就把这个技能是干什么的、什么时候用、怎么用、失败了会怎样全部描述清楚了。2. 技能体系怎么设计先分清楚哪些该沉淀成技能2.1 技能的三级分类原子技能、复合技能、策略技能刚开始做技能库的时候我犯过一个错误把所有能力都平铺成一长串函数列表结果模型一面对 30 多个技能就开始选择困难。后来我把技能分成了三层类别定义典型例子编排方式原子技能只做一件事不可再拆发邮件、查天气、创建工单模型直接调用复合技能多个原子技能的固定序列会议纪要归档解析录音→生成纪要→建文档→发通知内部编排对外是一个技能策略技能根据条件动态决定怎么执行优先走成本低的通知渠道由模型决策内部路由这个分类的前两个都好理解重点说下策略技能。它是我在实际项目中被迫拆出来的。最初我把通知用户做成一个技能参数里有渠道、模板、夜间免打扰开关结果模型经常传错参数比如明明应该走站内信却传了短信渠道。后来我把发通知拆成了策略层模型只需要说告诉用户他的审核通过了策略技能会根据通知类型、用户偏好、当前时段自动选渠道。这里的关键原则是凡是可以通过规则确定的事情不要交给模型来解决。模型的强项是理解模糊意图弱项是按固定规则做选择。策略技能的设计就是把这个边界画清楚。2.2 技能描述是给模型看的菜单不是API文档这是我在整个技能库里收获最大的一点认知。技能描述不是给人看的程序注释而是给模型看的推荐内容。同一个技能用两种写法模型调用的准确率可以差出 20 个百分点以上。我的经验是一份有效的技能描述只需要包含四项触发场景、入参说明、约束条件、一个典型请求示例。像下面这样{ name: create_review_ticket, description: 当用户反馈内容需要人工审核处理时创建审核工单。适合纠纷、举报、异常内容上报等场景。不要在用户仅询问进度时使用。, parameters: { type: string, 可选值: content_report | payment_dispute, reason: string, 必填描述具体问题, urgency: string, 可选值: low | high, attachments: array, 可选相关文件ID列表 }, examples: [ {type: payment_dispute, reason: 用户A反馈扣款成功但服务未开通} ] }注意我加了不要这种负向描述。实测下来负向描述比正向描述更能提升路由准确率模型经常会因为语义相近而误调用一个明确的否定约束能拦掉相当多的误判。另外描述不要太长。后面我会专门讲这个坑但这里先说结论描述超过 150 字模型对它的注意力反而下降路由准确率不升反降。宁可精简一点把最关键的触发场景说清楚。2.3 执行层注册中心 意图路由 结果标准化技能库的运行时我分成了三个组件注册中心所有技能启动时向这里登记元信息包括 schema、版本号、健康检查接口。新技能上线就是一次注册不需要改动既有代码。意图路由把模型给出的调用意向映射到具体的技能和执行参数。刚开始可以直接依赖模型原生的 function calling技能少的时候完全够用但当技能数量超过 20 个我建议在路由层做一次预筛比如按用户请求里的关键词先过滤掉明显不相关的技能再让模型从剩下的候选里选。这个优化能把 token 消耗降 30% 以上。结果标准化所有技能执行完统一包装成固定的返回结构这是我强烈建议的规范{ status: success | business_error | system_error, data: {}, error: {code: EMPTY_RESULT, message: 查询无结果}, context: {request_id: req_123, retry_count: 2} }为什么强制统一返回结构因为模型在执行完技能后要根据返回结果决定下一步动作。如果返回结构五花八门模型就不知道是成功了还是失败了更不知道失败了该怎么处理。有了这套标准结构模型看到status: system_error就知道应该向用户解释故障而不是假装成功看到EMPTY_RESULT就知道应该换个思路而不是原地重试。3. 从零实现一个会议纪要归档技能3.1 定义技能Schema每个字段都要有为什么跑通全链路最好的方式就是完整做一个技能。我挑会议纪要归档来说因为这个技能跨了内部编排、文档写入、消息通知三个系统覆盖了不少典型问题。先定义技能 schema{ name: meeting_minutes_archive, description: 将会议录音或文字稿归档为结构化纪要并存入知识库归档完成后通知相关人员。适用于会议结束后需要沉淀结论和待办事项的场景。, parameters: { transcript: string, 必填会议完整文字稿或录音转写文本, title: string, 必填会议名称, participants: array, 选填参会人邮箱列表, notify: boolean, 默认true是否发送归档通知 }, required_skills: [transcript_to_summary, knowledge_base_write, send_message] }注意我加了required_skills字段这表示这是一个复合技能。调用它的时候内部引擎会依次执行三个原子技能任何一步失败都能精准定位。设计入参的时候有一个容易被忽略的点入参要尽量贴近用户的表达习惯而不是贴近底层系统的输入格式。比如录音转写文本是一个用户自然能提供的字段如果你把字段设计成audio_file_id那就强制模型去理解文件系统凭空增加了模型出错的空间。3.2 实现执行函数异常出口比主流程更重要执行函数我建议遵循一个原则主流程写简单异常出口写详细。因为主流程就是按顺序调三个技能但每个技能的异常都要有对应的恢复策略。# 技能执行函数骨架 def execute_meeting_minutes_archive(ctx, params): transcript params[transcript] title params[title] # 第一步生成结构化纪要失败可重试1次 summary call_skill(transcript_to_summary, {text: transcript, format: structured}) if summary.status system_error: summary call_skill(transcript_to_summary, {text: transcript, format: structured}, retryTrue) if summary.status system_error: return error_result(SUMMARY_GEN_FAILED, 纪要生成服务暂时不可用) # 第二步写入知识库失败则降级为保存到草稿区 doc_id call_skill(knowledge_base_write, {title: f[纪要] {title}, content: summary.data}) if doc_id.status ! success: doc_id call_skill(knowledge_base_write, {title: f[草稿] {title}, content: summary.data, folder: draft}) # 第三步通知失败不阻塞主流程 if params.get(notify): call_skill(send_message, {to: params.get(participants, []), content: f{title} 纪要已归档文档ID: {doc_id.data}}) return success_result({doc_id: doc_id.data})这里我做了三件值得细说的事第一纪要生成失败可以重试一次因为这类服务偶发超时概率不低重试成本低收益高。知识库写入失败则降级到草稿区保证用户至少有一个可访问的产物。通知失败干脆不阻塞主流程。这里体现了一个原则每个异常出口都必须明确回答一个问题——用户感知应该是什么。纪要生成彻底失败告诉用户稍后再试只是通知没发出去主流程照常成功顶多日志记一笔。第二内层技能调用统一走call_skill函数它负责超时控制、重试策略、日志埋点。避免每个技能自己写一遍超时和重试逻辑。第三返回值统一用success_result/error_result包装这就是前面说过的结果标准化在实际代码里的具体落地。3.3 注册技能并验证模型能正确触发技能实现完需要在注册中心登记。这一步的目的是让模型在下一轮对话前就能看到这个新技能# 注册中心登记 registry.register( namemeeting_minutes_archive, version1.0.0, handlerexecute_meeting_minutes_archive, schemameeting_minutes_archive_schema, timeout_seconds30, health_checklambda: knowledge_base.health() ok )注册完成之后我习惯用一组固定用例跑一次回归确认三件事模型能否从模糊的用户表达中识别出该用这个技能参数能否正确抽取异常场景下模型能否看懂错误码并给出合适的用户话术。我常用的一个测试用例是这样用户输入: 下午的产品评审会开完了你帮我把结论和待办整理一下存起来再提醒一下老李他们期望的模型行为选中meeting_minutes_archivetitle抽成产品评审会participants抽成老李他们的邮箱列表。如果模型在抽取participants时卡住了大概率是 schema 里的描述不够明确这时我会把participants字段说明改成参会人的邮箱地址列表如果没有明确邮箱可用姓名查找通讯录。跑通这一个技能之后后面的技能就都是同样的套路了。我后来总结出一个写技能的检查清单schema 描述的触发场景是不是够明确有没有负向约束异常出口有没有覆盖系统错误和业务空结果返回结构是不是标准的注册中心里有没有健康检查这五条全过技能就算合格了。4. 上线后踩过的三个真实坑4.1 技能描述越长模型反而越不会调用这个坑最出乎我意料。初版技能库上线前我担心模型看不懂技能于是把描述写得很详细甚至把底层接口的字段都抄进去了。结果上线后模型调用准确率掉了一大截。后来我做了个对比实验把同一个技能分别用 200 字和 80 字的描述各跑 50 个测试用例结果如下描述长度正确触发率误触发率平均决策耗时200字含实现细节76%18%1.9s80字只含触发场景约束92%7%1.2s原因也不复杂模型的注意力窗口就那么大描述里全是底层实现细节真正决定什么时候该用这个技能的关键信息反而被稀释了。技能描述是给模型看的菜单不是给人看的接口文档——底层怎么实现模型不需要知道。我后来定了一条铁律技能描述里禁止出现代码、字段级细节、内部架构信息只保留触发场景、入参一句话说明、负向约束、一个典型示例。这条规矩救了我后面所有新技能。4.2 复合技能里的共享状态并行时崩了做会议纪要归档的时候transcript_to_summary会把生成的临时纪要写到共享缓存里之后knowledge_base_write会读这个缓存去取内容。单个请求没问题但一旦多个会话同时跑归档缓存里的数据就互相覆盖了经常出现纪要内容是上一个会议的。说实话这个问题的根子是我自己的设计偷懒了技能之间共享状态却没有做隔离。修的时候我做了两层改造。第一层是会话级隔离每个请求的request_id独立命名空间写缓存时 key 带上这个 ID读的时候只读自己的 key。第二层是防止复合技能内部不同原子技能之间的数据传递我不再用共享缓存而是改成显式传参——上一步的返回值直接作为下一步的入参传进去完全去掉隐式状态。改造完之后我意识到一个更本质的原则复合技能的原子技能之间只允许通过函数参数传数不允许通过外部存储传数。状态一隐式必出鬼。这个改动顺手解决了一系列之前没暴露的并发问题也让我后面排查问题变得极其简单。4.3 模型编造参数技能层必须自己守门上线一个月左右我发现日志里有一批奇怪的失败记录技能调用了但参数明显是编的。比如用户压根没提文件名模型却给title传了个默认标题文件 v2最终版类似的话。模型在不确定参数值的时候倾向于自己合理脑补一个值填上而不是说这个参数我不知道。针对这个问题我在执行层加了一道守门逻辑入参不是模型说什么就是什么而是先过一遍校验器。校验规则很简单必填字段缺失直接返回MISSING_PARAM错误码同时把缺失字段名放进错误信息里字段值的枚举不在可选范围内返回INVALID_VALUE并且把合法的可选值列表返回给模型完全不确定的值允许技能层返回一个人工确认待补充状态由外层向用户询问。def validate_params(schema, raw_params): missing [f for f in schema[required] if f not in raw_params] if missing: return error_result(MISSING_PARAM, f缺少必要参数: {missing}) for field, rule in schema.get(field_rules, {}).items(): if field in raw_params: if enum in rule and raw_params[field] not in rule[enum]: return error_result(INVALID_VALUE, f字段 {field} 可选值应为: {rule[enum]}) return success_result(raw_params)最关键的一点是错误码的设计要让模型能看懂并且能据以修正。MISSING_PARAM和INVALID_VALUE配合上明确的修复提示之后模型第二轮就能补对参数。实测下来加上这道校验后技能调用的成功率从 82% 提到了 95% 以上。5. 让技能库活起来数据反馈与迭代闭环5.1 日志字段怎么设计才能支撑复盘技能库上线只是开始真正重要的是让它在日常使用里不断变好。而变好的前提是有数据。我见过很多团队做完 Agent 就完事了调用日志打了几十万条但从来不看出了问题只能靠猜。这等于白做。我设计的技能日志记录了这么几个维度谁在哪个会话里、用了哪个版本的技能、传了什么参数、花了多长时间和多少 token、结果如何、模型决策过程是什么。每次调用都落一条记录后面分析问题全靠它。比如排查为什么审核类工单创建变少了先看调用量趋势再看触发模型的决策轨迹很可能是某次 Prompt 调整导致路由偏好变了。没有这些字段你只能对着空屏幕干瞪眼。我的建议是日志宁可多记录一些也别省运行期多记的字段是排查问题时的救命稻草。5.2 用三个指标决定技能的去留我完整跑了一个月之后总结出对技能库最有用的三个指标第一个是技能调用量看这个技能在真实场景被需要的频率。调用量极低的技能要么是描述写得太隐蔽模型找不到要么是业务本身不需要得优化或者下线。第二个是执行成功率指的是技能跑完且返回success的比例。成功率低说明技能本身不稳定。要注意区分系统错误服务挂了和业务错误查无结果前者修程序后者改描述。第三个是模型纠错率也就是第一次调用失败后模型根据错误码自行修正并第二次成功的占比。这个指标直接反映技能错误信息设计得好不好也反映技能和模型的配合状态。我每周会拉一次这三个指标对着列表把问题技能抽出来处理。这套循环跑起来之后技能库才算是有了生命才敢说它是可维护的。5.3 技能也要版本化和灰度最后一个容易踩的坑技能更新直接改动线上逻辑一旦改挂了连带所有调用这个技能的 Agent 一起遭殃。后来我把技能库也做成了版本化发布和业务应用一样走灰度流程。比如会议纪要技能从1.0.0升到1.1.0我会在注册中心同时保留两个版本路由层按一定的比例切流量新版本跑几天数据正常了再全量切换。一次发版后再回滚成本高且容易出二次故障。技能灰度虽然不能完全避免问题但至少能把爆炸半径控制在 10% 的流量内。我维护 agent-skills 这段时间最深的感受是Agent 能不能做好大概率不取决于你选的模型而是取决于模型外面这层能力层是否稳、是否可控、是否可观测。技能库不是一个有没有的问题而是一个能不能持续变好的问题。里面每个技能都在和真实业务对话也在和模型的能力边界互相磨合。如果你也在搭类似的技能层建议一开始就把日志和评估指标做进去别等技术债攒到不可收拾的时候再补。