ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Agent技能层设计指南:从协议到上线的完整实践

Agent技能层设计指南:从协议到上线的完整实践 我接触过不少做Agent项目的团队聊到最后大家都不约而同地卡在同一个问题上模型本身已经很聪明了可一旦让它去做事情——查个库存、发封邮件、改个工单——它就手足无措。过去我的解决方式是往System Prompt里堆工具说明堆到后面Prompt比业务代码还长模型开始把无关工具也翻出来调用调用参数还传错。后来我彻底转向了一套专门的技能层来管这件事也就是大家常说的agent-skills。这篇文章就是我实践这套思路的完整记录从技能协议设计到上线后排查再到技能库的持续演化希望对正在做或准备做Agent的你有实际参考价值。1. 为什么我强烈建议每个Agent项目都单独建一套skills层1.1 从聊天模型到干活模型的那道坎先说一个反直觉的结论大语言模型能不能把事儿办成难度不在模型推理而在它身边有没有一套趁手的技能。ChatGPT刚火的时候大家觉得只要模型够强啥都能干。可真把Agent丢到业务流程里才发现模型只能做表达层的工作——理解意图、生成文本、拆解步骤。真正落地的那一哆嗦永远要落在某个具体动作上调一个API、写一条数据库记录、上传一个文件。这个最后一公里就是技能层的分内事。我见过很多团队把工具函数直接写死在Agent主逻辑里。工具一多主逻辑变成一坨巨大的if-else或switch-case每次加一个工具都要动核心代码测试回归成本高得吓人。更麻烦的是模型的工具选择空间和提示词里的描述质量强耦合工具描述写得含糊模型就瞎猜一猜就错。这就像给一个人装了无数只手却不告诉他每只手到底能握什么。1.2 技能不是函数是Agent身上可插拔的肌肉我在自己的项目里把技能定义成这样一个东西它是Agent能够执行的一个明确动作单元并且围绕这个动作单元系统性地打包了触发条件、输入输出协议、执行逻辑、错误处理和说明文档。普通函数解决的问题是这行代码怎么算技能解决的问题是Agent在什么情况下应该调用什么动作调完之后怎么把结果并入对话上下文。如果做个类比函数是工具箱里的单个扳手技能则是在什么工况下用扳手、怎么用、拧坏了找谁修的一整套操作手册。Agent有了技能层就不再是一个会背百科全书的话痨而是一个知道自己手里有电钻、电钻用在什么场景、用完会放回原位的工人。我自己早期犯过的错是直接把一个类里的public方法全部注册成技能。结果模型面对几十个粒度参差不齐的方法完全不知道该选哪个——有GetUserById又有GetUserByName还有GetUserDetailedInfo。对模型来说选择成本极高。技能层要做的第一件事就是用业务语义去重新包装这些函数把三个接口收敛成一个查询用户信息的技能并配好参数映射关系。1.3 什么时候你该开始建技能库有人问我是不是一开始就要设计一个大而全的技能框架我的答案是否定的。项目刚跑通Demo的阶段只有两三个工具写在Agent主逻辑里完全没问题。但当你遇到下面三个信号自我介绍就该认真搭技能库了工具数量超过10个模型选错工具的频次明显上升或者提示词里塞工具描述已经塞到影响主任务指令。同样的工具要在多个Agent或任务流里复用比如查天气既要在闲聊Agent里用又要在出行规划Agent里用。开始有外部系统接入需求——你要调别人的API、别人也要挂你的技能这时候没有一个标准协议协作成本会指数级上升。我见过的务实做法是从第二个信号出现时就动手搭一个极简框架——一个技能描述文件加一个注册机制。别一上来就上复杂的编排引擎、状态机、图数据库先用最朴素的字典列表跑起来把协议订好后面迁移也容易。2. 技能协议先定契约再写技能2.1 一个技能描述文件的结构长什么样技能层的第一块地基是技能描述文件。这个文件是给模型看的说明书也是给执行引擎看的规格书。我习惯用JSON格式兼顾人类可读和机器解析。一个最小可用的技能描述大概长这样{ name: query_user_info, description: 根据用户ID或手机号查询用户的基本信息包括昵称、等级、注册时间。当用户询问我的账号信息或需要用户ID之外的资料时使用。, version: 1.2.0, author: team-core, tags: [user, read-only], input_schema: { type: object, properties: { user_id: {type: string, description: 用户唯一标识优先使用}, phone: {type: string, description: 手机号仅当user_id缺失时使用} }, required: [] }, output_schema: { type: object, properties: { nickname: {type: string}, level: {type: integer}, registered_at: {type: string} } }, execution: { timeout_ms: 3000, retry_count: 2, backend_endpoint: internal://user-service/get-info }, error_policy: { on_failure: return_error_message, fallback_skill: query_user_info_via_admin } }这个文件里最关键的三块是description、input_schema和error_policy。description决定了模型的选择正确性input_schema决定了调用正确性error_policy决定了系统在失败时能否体面地处理问题。2.2 入参出参设计里的三个原则入参和出参的设计是我踩过最多坑的地方。整理下来就三条原则第一字段名用业务术语不用底层字段名。模型不懂usr_id_str是什么但能理解user_id。我们后台字段叫u_id技能层一定要包一层映射把它暴露成user_id。这不是讨好模型而是减少模型传参出错的最直接手段。第二入参要宽容出参要克制。入参设计上允许冗余的查询条件让模型有多条路径能凑齐参数。比如查询用户既可以接受ID也可以接受手机号查询订单可以传订单号、也可以传用户ID加时间范围。出参设计上则相反只给最精炼、最相关的字段其余全部丢弃或者放在一个单独的raw_data字段里。模型上下文窗口有限给太多无关字段等于拉低它对关键信息的注意力。第三所有数值型参数必须写明单位和边界。这个听起来很基础但出错率极高。一个timeout_seconds上层模型可能传1以为是分钟可能传60以为单位是毫秒。在description里写明单位秒允许范围1-60模型的出错率会直线下降。# 我常用的一段入参校验逻辑处理模型多传、漏传参数的问题 def normalize_parameters(skill_schema, model_params): cleaned {} for key, spec in skill_schema[properties].items(): if key in model_params and model_params[key] is not None: cleaned[key] model_params[key] # 如果必填参数缺失返回可读的错误提示而不是直接抛异常 missing [k for k in skill_schema.get(required, []) if k not in cleaned] if missing: return {success: False, error: {code: MISSING_REQUIRED_PARAMS, fields: missing}} return {success: True, params: cleaned}2.3 为什么我不用纯自然语言描述来调用技能有一段时间行业里流行用纯自然语言让Agent自己生成调用代码也就是不定义结构化schema只给模型一段话描述让它自由发挥。这个概念很性感但工程上非常难落地。模型生成的代码在语法上能跑可是一旦涉及URL拼接、鉴权头、枚举值它的出错率就会显著上升而且错误是随机分布的——同一个问题这次参数写对了下次就写错你甚至没法做系统的单元测试。所以我的原则是**自然语言只用于让模型选择技能绝不让它生成调用细节。**选择走语义匹配具体执行细节统统交给预定义好的schema和机器生成代码。这样哪怕模型开篇把技能选错了至少它调用技能时传参是安全的不会造成数据污染这类更严重的后果。3. 我的技能库是从这四类技能开始的技能设计没有标准答案但我在多个项目里沉淀出了一套分类法。按能力性质技能库里的技能基本可以分成四类每一类在库里的写法、更新频率、故障模式都完全不同。3.1 原子工具技能把手伸进外部系统原子工具技能是最常见、也是最好理解的一类它的本质是对外部API或内部服务的一次封装。查询订单、发送短信、创建工单、修改库存都属此类。这类技能的特点是一个技能对应一个明确的外部动作输入输出可预期粒度最小。写这类技能时我最在意的是两件事一是封装要薄二是错误信息要完整。封装薄的意思是技能层不要夹带私货不要在技能里内置业务规则比如查询后自动判断金额是否超限这种逻辑应该放在决策层而不是技能层否则技能就没法复用了。错误信息要完整的意思是当外部API返回非预期结果时技能必须把状态码、响应体、可能的原因一并吐回来让上层决策能够据此判断是重试、换技能还是直接向用户道歉。# 原子技能的错误处理示例把外部错误翻译成Agent能理解的信息 def run(self, params): try: resp requests.post(END_POINT, jsonparams, timeout3) resp.raise_for_status() return {success: True, data: resp.json()} except HTTPError as e: return { success: False, error: { code: fHTTP_{e.response.status_code}, message: e.response.text[:500], suggestion: 可能是参数无效或服务端异常可尝试修正参数后重试 } }3.2 检索增强技能让Agent有临时记忆第二类技能是检索增强技能。它的作用是给Agent提供一个翻资料的能力入口也就是把RAG检索增强生成包装成一个技能。很多人以为RAG是单独一个模块不该算技能。但站在Agent的角度从一个知识库检索资料和从一个数据库查记录行为模式完全一样——都是输入一个query、返回一段相关的内容块。把它技能化之后一个Agent可以同时挂上公司政策知识库产品文档库客户历史工单库等多个检索技能由模型根据问题内容决定去哪个库里翻。这类技能的设计要点在于query改写和TopK设置。用户的一个原始问题往往很长含大量与检索无关的寒暄直接拿去做向量检索效果很差。我会在技能内部先做一个query改写小步骤把问题抽成几个关键词组合再执行检索。TopK一般不要给太大3到5个就够了给多了反而会让上下文里塞满不相关内容。3.3 编排决策技能它决定了任务往哪走第三类技能比较特殊叫编排决策技能。它不直接触达外部系统而是负责决定下一步调用什么。在简单的Agent架构里这个决策由模型的主推理链路完成不单独设技能。但当你的Agent开始面对复杂任务比如帮客户制定一份包含机票、酒店、日程的出行计划你就需要把一些常用的决策流程固化成技能。我用过一个很典型的编排技能叫travel_planner。它的执行逻辑不是简单调一个API而是先调用查询航班的技能、再调用查询酒店的技能、再调用生成日程的技能并按照给定顺序把结果汇总到一份报告里。这实质上是用代码固化了一条任务流水线。好处是这类任务不再每次消耗大量模型推理token且执行路径稳定可测。坏处是它牺牲了一定灵活性。我的做法是只把已被验证过的高频路径固化成编排技能低频的、需要创造性拆解的任务依然让模型自由规划。3.4 状态维护技能让多轮对话不失忆第四类技能很容易被忽略却是支撑Agent体验的关键就是状态维护技能。一个Agent在跑任务的过程中往往需要记住刚才做到哪一步了、已经拿到了哪些信息、还有哪些没拿到。多数Agent框架的做法是把对话历史全部塞给模型让它自己从中取出状态但对话一长这个办法就会失效。所以我把状态管理也做成了技能。比如有一个update_task_progress技能专门记录当前任务的阶段标记和关键变量还有一个get_task_progress技能供多轮对话开始时恢复状态。这相当于给Agent配了一本工作笔记。我实测过加了状态维护技能之后Agent在执行一个多步骤、跨多轮对话的复杂任务时丢步骤的频次下降非常明显。4. Agent技能上线后的五个致命坑4.1 技能清单越来越长匹配却越来越不准技能库第一个致命坑就是随着技能数量增长模型选错技能的频率不降反升。你以为是模型推理能力不够其实是技能之间的描述互扰。当技能A的描述里出现了技能B的关键词模型就很容易混淆。举个例子我有一个查询天气技能description里写了适合在安排出行时使用结果另一个建议穿搭技能也写了适合在出行时参考——模型碰到周末要不要去爬山这种问题时可能会选错。解决办法有两个。第一个办法是技能隔离就是检查所有技能的description确保每个技能的关键特征词是独特的、不与其他技能共享。第二个办法是分组路由当技能超过20个就要引入技能分组概念。比如先让模型在出行类、信息查询类、订单处理类、系统管理类这四组中做一次粗选再在选定组内部做细选。粗选可以用非常简单的分类器成本低效果却好得惊人。4.2 授权与权限边界技能不该全知全能技能一定要做权限管理这是我在一次线上事故后学到的血泪教训。当时我们的Agent有一个删除项目的技能设计得功能正常、参数清楚唯独漏了权限校验。于是在一次对话中模型误判了用户的删除意图直接把一个未完成的方案项目删了。数据恢复花了大半天从那之后我把技能权限列成了架构上的硬性要求。我现在会在每个技能的执行层加上一个统一的鉴权前置步骤。步骤里检查两个维度一是这个技能当前用户有没有权限调用二是调用者Agent自己有没有执行级别。这两个维度必须同时通过才放行。技能网关的鉴权逻辑长这样def enforce_skill_permission(user_ctx, skill_name, agent_level): skill get_skill(skill_name) if not skill.allow_list.get(user_ctx.role, False): return {success: False, error: {code: PERMISSION_DENIED, message: 当前用户无权调用此技能}} if agent_level not in skill.agent_level_allowed: return {success: False, error: {code: AGENT_LEVEL_DENIED, message: 当前Agent等级不允许执行此操作}} return {success: True}这个鉴权既保护了外部系统的资源安全也保护了Agent自己不被用户诱导去做越权操作。初学者往往忽略后者但模型越狱攻击里有一大类就是通过诱导Agent调用危险技能来实现的。4.3 超时与重试机制外部调用出问题时的正确反应凡是连接外部系统的技能一定会遇到超时、限流、短暂不可用这类问题。很多技能第一次上线时只处理了成功路径没有认真设计失败路径导致Agent在外部系统抖动时像断线木偶一样反复报错。我总结了一套三级超时策略。第一级每个技能内部设置一个合理的超时时间超过即中断外部调用。第二级技能返回一个结构化错误其中包含建议重试还是不要重试的明确标记。第三级Agent决策层根据错误标记决定是否重试以及是否切换到备用技能。这里特别注意错误标记为幂等可重试的操作比如查询类可以重试非幂等操作比如创建订单、扣款绝对不能盲目重试否则会造成重复扣款、重复下单。4.4 技能间的隐性依赖你以为解耦了其实没有技能看似是独立部署的但它们在业务层面往往存在隐性依赖。最典型的是数据依赖——技能B需要技能A的输出作为输入。比如生成报销单技能依赖查询订单技能的返回值如果查询订单改了输出字段名报销单技能就会静默失败。我处理这个问题的方案有两个一是强制在技能描述文件里声明依赖关系用depends_on字段标注让执行引擎在启动时做依赖检查二是给关键输出加一层契约测试每当上游技能变更自动跑一遍下游技能的测试用例不合格就阻断上线。这个机制帮我拦截过至少三次因字段改名导致的线上故障。4.5 回滚与兼容技能升级伤害了存量任务最后一个坑和版本管理有关。技能升级很容易改个逻辑重新发布就行但存量任务怎么办如果用户正在执行一个多轮任务任务流中间调用的技能版本突然变了轻则行为不一致重则直接执行失败。我的经验是技能在执行引擎里一定要支持多版本共存。当一个任务开始执行时就锁定当前技能版本号后续调用都走这个版本不跟随最新发布。新任务才使用最新版本。这和我们平时做数据库迁移的向后兼容思路完全一致。如果某个技能的非兼容变更无法避免那就要提供迁移脚本在任务开始前检测旧版本任务并进行提示。{ skill: create_refund, task_ref: task_20250321_xyz, locked_version: 1.1.0, current_version: 1.2.0, migration_status: REQUIRED, migration_action: confirm_with_user_before_continue }5. 让技能库持续进化的四件事技能库不是建一次就完事的静态资产它会随着业务变化不断膨胀、腐化、重组。我把维护技能库的日常工作也做成了固定节奏定期复盘。具体来说我会做四件事。5.1 用失败追踪反向敲打技能设计我会把所有技能失败的调用记录汇总到一张大表里每周分析一次看失败的模式是什么。是模型选错技能、参数缺失、还是外部系统错误选错技能说明description有问题参数缺失说明input_schema设计得不友好外部系统错误说明要梳理限流或服务稳定性。这些失败信号就是技能设计最直接的反馈比任何代码评审都管用。我特别看重归因准确率这个指标。就是在汇总失败记录时引擎要把到底是哪一步失败标记清楚不能只笼统地记一个技能执行失败。做到这一步后面的优化才有针对性。5.2 给技能打分不是所有技能都值得保留技能和代码一样会有坏味道。有些技能当初建起来是因为某个临时需求之后再也没有被用到有些技能描述和其他技能高度重叠成了干扰源。我维护了一份技能评分表从调用次数、失败率、语义冗余度、维护成本四个维度给每个技能打分定期清退低分技能。这里有一个容易忽视的点删除技能比新增技能更影响系统稳定性因为如果有存量Agent或存量任务还在引用删除会直接导致执行失败。所以我的清退流程一定是先停用三天观察线上是否有报错再彻底删除。停用期间引用它的旧任务会走fallback逻辑直到确认安全。5.3 组合竞技场测试两三个技能叠起来的效果单个技能好用不代表组合起来好用。我见过不少Agent项目单个技能通过全部测试可真叠加起来就出现各种怪问题——A技能的出参接不上B技能的入参、两个技能都尝试处理同一个操作导致冲突、模型在两个技能之间反复横跳。我设计了一个组合竞技场它本质上是一组预定义的多技能关联测试场景每次技能库有新增或变更就自动跑一遍。比如给定一个帮用户预订酒店的目标同时允许模型调用搜索酒店查询余额创建订单三个技能跑完检查预订链路是否顺畅。这个场景覆盖成本不高但拦截组合问题的效果很好比单测有价值得多。5.4 技能文档与代码同源别让Agent读到一份旧说明书最后一个经验关乎技能库的长期维护——文档和代码必须同源。技能的description文件就是给Agent看的唯一文档如果它和真实执行逻辑不一致Agent就会照着错误的说明书干活。这个问题在团队协作时尤其严重新同学改完执行逻辑忘了同步更新描述文件模型就在新功能上用旧描述行为开始飘。我在代码工程里做了一条CI检查规则描述文件里的input_schema和代码里的参数校验函数必须严格匹配一旦不一致CI直接报红不允许合并。靠这个规则我堵住了至少五次说明和实现分家的潜在事故。很多做Agent的朋友问我说你到底是怎么把Agent的可靠性做上去的。我思来想去答案其实不在模型选择也不在Prompt技巧而在于这个不起眼的技能层设计。把每一个能力都当作一个严肃的、有契约、有版本、有权限、有失败预案的技能来对待Agent才不会像一个什么都懂但什么都不敢干的实习生而会像一个知道自己的工具边界、知道出了问题该找谁、也知道自己的权限禁区的靠谱老员工。这一层的设计投入会随着你的技能数量增长从边际成本变成边际收益——这也是我在多个项目里反复验证过的事。
返回列表