Agent技能体系设计:从裸工具到可编排技能层的完整实践
发布时间:2026/9/26 8:27:12来源:尧图网络
先交代一下背景。过去大半年我一直在做企业内部的知识库问答助手最初它就是一个能联网、能查库的聊天机器人但越往后越发现真正让 Agent 变得可用的不是接了多少个 API而是你手上那套技能体系设计得有多干净。所谓的 agent-skills不是给模型塞几十个接口而是把能力拆成一个个带描述、带参数约束、带权限边界、带错误处理的行为单元。这篇文章把我自己在项目里从零搭建技能层的完整思路、踩过的坑和实测数据一起写出来希望能帮到正在做类似架构的人。1. 技能化从给Agent塞工具到给Agent立规矩1.1 一次失败的工具调用让我决定重做技能层最早版本里我们直接给 Agent 挂了 27 个函数从查工单、读数据库、发通知到调用内部 API 全都有。结果线上跑了一个月问题全暴露了模型经常分不清查询某个字段和导出全部字段之间的差异参数填错率高得离谱更麻烦的是有些 API 是有副作用的比如发送通知和更新工单状态模型在不该触发的时候触发了用户直接被骚扰。那段时间我每天的工作就是看日志靠手工在黑名单里加限制规则越加越乱。后来我意识到问题不在模型的聪明程度而在我们给模型提供的能力描述太粗糙。工具只有一个函数名和几行参数说明模型只能靠猜。与其继续打补丁不如把整个能力层推翻做成一套真正意义上的技能系统。每个技能不再是一个孤零零的函数而是一个完整的、自描述的、可编排的能力单元。1.2 技能和工具到底差在哪严格来说工具Tool是技能的底层实现技能是工具在 Agent 语境下的完整包装。我后来在团队内部定了一个标准如果一样东西只有入参、出参、执行逻辑它只能叫函数只有当它同时具备名称、描述、参数约束、依赖声明、权限声明、超时策略、返回值规范、版本号时它才配叫技能。这个区分的价值在于工具是给工程师调用的技能是给模型阅读和理解的。代码里的函数名再清晰对模型来说也只是字符串但当它被包装成一段结构化的技能描述模型就能像人看说明书一样准确判断这个场景该不该用你、用你传什么、用完你会给我什么。我在实践中发现同样的底层函数从裸工具改造为完整技能之后模型的动作选择准确率提升了非常明显这个数据后面会详细讲。1.3 给 Agent 立规矩的三个层次技能化的本质是给 Agent 立规矩这个规矩分三个层次能力边界明确告诉 Agent 你能做什么。没注册的技能模型在提示词里根本看不到自然也不会调用这比事后拦截靠谱得多。行为方式明确告诉 Agent 你该怎么做。技能描述里写清楚适用场景、不适用场景、典型用法模型做决策时就不是掷骰子而是有据可依。后果控制技能体声明了超时、重试、副作用权限即使模型误判执行层也会兜底不会造成不可逆影响。一个常见误区是很多人觉得把技能描述写得越详细越好。其实不是描述太多反而会稀释关键信息。我见过有的团队把技能描述写到 600 字结果模型经常漏看关键的参数约束。正确的写法是像产品说明书——首句说清楚用途中段给典型场景末尾明确禁忌。下面这张表是我内部培训时常用的对比。对比维度裸工具Tool完整技能Skill核心内容函数签名、参数列表名称、描述、Schema、依赖、权限、超时等面向对象开发者大模型 开发者选择准确率依赖参数命名巧合依赖描述与场景匹配度可治理性基本不可控权限、配额、审计都可控可复用性跨场景复用难可组合、可编排、可共享2. 技能注册协议让模型一眼看懂的登记手册2.1 注册清单里到底该放什么我设计的技能注册清单核心是下面这一组字段。每个字段都是经过线上问题反推之后保留的缺一个都会在某个环节出问题。{ skill_id: issue_label_helper, version: 1.4.0, name: 智能打标签助手, description: 根据工单标题和正文的语义为工单推荐3-5个分类标签。仅在需要对工单做文本分类时使用不处理附件内容。, input_schema: { type: object, properties: { title: { type: string, description: 工单标题不超过200字 }, content: { type: string, description: 工单正文不超过8000字 } }, required: [title, content] }, output_schema: { type: array, items: {type: string}, description: 按置信度排序的标签列表最多5个 }, dependencies: [auth_token_reader], timeout_ms: 3000, permissions: [read:ticket, write:label], cost_hint: low }简单解释几个关键字段。description是给模型看的重要性仅次于 skill_id我单独在下一节讲。input_schema和output_schema是给模型看和给执行引擎校验用的必须同时存在很多团队只做输入校验不约束输出结果技能返回了模型看不懂的结构还要模型硬猜。dependencies和permissions是给执行引擎做安全校验用的避免一个技能悄悄读取它不该读的数据。cost_hint是我后来加的告诉调度器这个技能是便宜的猜测还是昂贵的推理方便做预算控制。2.2 描述即路由模型经常选错工具的真正原因如果你发现模型总在错误的场景下调用某个工具不要急着骂模型笨先检查 description 写得怎么样。我有一次排查模型该查知识库却去查工单系统的问题最后发现知识库工具的 description 里压根没写用户提问的业务知识问题优先使用本工具而工单工具的 description 里写了一句可以同时查询与工单相关的知识就这多出来的一句话导致模型几乎每次都误选。现在我的团队对 description 有硬性要求按这个模板写第一句这个技能完成什么任务务必包含明确的动词和名词。第二句在什么场景下优先使用。第三句什么场景下不要使用。第四句特殊注意事项比如仅处理文本不解析附件、返回结果非实时可能有 5 分钟延迟。举一个改前改后的例子改前获取热点资讯数据。改后根据用户输入的主题词从资讯平台获取最近 24 小时内的热点新闻列表返回标题、来源和发布时间。当用户询问最近有什么热点今天发生了什么大事时优先使用。如果用户需要的是财经行情类数据不要使用本技能。改完之后同样的场景下选错率直接下降了一大截。这说明模型不是不会选而是你给的信息不足以让它做出正确的路由决策。2.3 参数 Schema 的博弈填错参数本质上是一场沟通失败参数 Schema 是另一个重灾区。我见过很多人把属性名写得和内部数据库字段一样比如tkt_cust_id模型根本不理解这是什么结果就是反复填错、反复校验失败、反复重试。参数设计有一条原则属性名和描述都站在发起人的视角写而不是站在数据库视角写。比如查工单参数customer_id的描述不要写数据库外键 22而要写客户唯一编号用户在个人中心可以看到通常是一串 10 位数字。模型看到这个描述就知道该填什么了。另外要到把required字段降到最少。每多加一个必填参数模型填错的概率就高一分。非关键参数设为可选并且提供默认值。我在技能执行引擎里做了一个自动补值模块如果模型没有传某个可选参数就从default字段取值或者从上下文里自动抽取。这一步能把一次技能调用的失败率降低不少。这里还要说一个容易忽略的点返回值的结构也要让模型能看懂。很多技能返回一堆嵌套 JSON字段名混乱模型解析起来费劲后续推理的输出质量就下降。我在技能执行引擎里加了一个result_summarizer每次技能返回后不是把原始 JSON 直接丢给模型而是先经过一层摘要只保留模型真正需要的关键信息这样既能省 token又能减少模型被无效信息带偏的概率。3. 执行引擎与 ReAct 循环一次技能调用的完整生命周期3.1 模型、调度器、技能执行器三者如何协作光有注册表还不够技能要跑起来必须有一个可靠的对齐循环。我们用的是经典的 ReAct 模式的改良版但相比原始论文里的循环我在中间加了一个专门的调度器和校验器防止模型在 loop 里面来回折腾。整个执行流程大致是这样的while steps max_steps and not converged: prompt build_prompt(state, skill_registry.list_all_descriptions()) decision llm.complete(prompt) if decision.type call_skill: skill skill_registry.get(decision.skill_id) if skill is None: state.add_observation(错误所选技能不存在请从列表中选择) continue payload, error validate_arguments(decision.arguments, skill.input_schema) if error: state.add_observation(f参数校验失败{error}请修正后重试) continue if not check_permission(skill.permissions, context.actor): state.add_observation(错误当前用户无权限调用该技能) break result executor.run(skill, payload, trace_idtrace_id) state.add_observation(format_observation(skill, result)) elif decision.type final_answer: return decision.answer else: state.add_observation(错误无法理解你的指令请重新明确意图)这个循环里最关键的三个设计是注册表驱动提示词、参数校验前置拦截、结果摘要回填。模型看到的技能列表不是写死在提示词里的而是根据当前会话上下文做一次粗筛后的子集避免几百个技能描述一次性全塞进去。参数校验在前置层就拦截错误而不是让技能内部抛异常这样模型收到的错误信息更清晰下次重试时也更可能改正。3.2 超时、重试与最隐蔽的副作用技能执行一定会遇到超时。我一开始只设了全局 10 秒超时后来发现问题大了某个技能超时Agent 会判定为此路不通转而尝试别的路径但那个超时的技能其实已经在后台执行成功了这就导致了重复操作和脏数据。后来我在技能层做了一个双状态机制每个技能执行时状态分pending已下发、running执行中、succeeded成功、failed失败、timeout_unknown超时但结果未知。对于timeout_unknown不允许引擎直接重试除非技能自己声明了幂等idempotent: true否则必须先调用cancel接口或在业务上做好幂等校验再决定是否重试。我强烈建议每一个技能在注册时都要明确回答这个技能的操作可不可以重复执行第二次查询类技能天然幂等发通知、改状态这类技能就不是。我在技能注册表里专门加了一个execution_side_effect字段配合timeout_unknown状态能让 Agent 在超时后做出更合理的决策而不是傻傻地再调一次。3.3 Token 预算让 Agent 在断电之前回来ReAct 循环一个容易被低估的问题是 token 失控。一次复杂的任务可能要跑十几轮每轮都有技能描述、参数、结果摘要、中间推理累加起来很容易把上下文撑爆。我们刚开始内测时有用户反馈说Agent 聊着聊着就开始胡说八道查日志发现就是上下文太长早期的关键信息被挤掉了。解决思路是做一个三级预算单轮调用预算、单技能结果摘要预算、整体会话预算。每轮的决策 prompt 只放当轮最必要的技能描述控制在 1500 token 内技能返回结果后立即压缩为摘要控制在 500 token 内整体会话超过阈值就提示用户开启新会话或对历史做摘要合并。这一层做好之后不仅模型吐字稳定了响应速度也明显提升。因为上下文短了每次推理的耗时也就短了这对用户体验的影响是立竿见影的。4. 技能编排把单点能力串成业务闭环4.1 编排层如何把流程变清晰单技能解决的是某个动作的问题但真实业务往往需要连续好几个动作。比如一个新工单自动处置的场景要先识别工单分类再搜索历史相似方案再生成回复草稿最后视严重程度决定要不要通知相关负责人。如果让 Agent 自由发挥它可以靠循环把技能一个个调完但问题是自由发挥不可控——可能中间某个环节漏掉了可能顺序颠倒了也可能在没必要的地方多调了技能。所以我在技能层之上又加了一层轻量的编排层用一个简单的流程图描述语言把技能的调用顺序、分支条件和数据依赖显式声明出来。flow SkillFlow( name新工单自动处置, steps[ Step(classify, skillissue_classifier), Step(search, skillknowledge_search, if_classify.severity P1), Step(draft, skillreply_generator, using[classify, search]), Step(notify, skillim_notifier, if_classify.severity in (P1, P2)), ], )这种编排方式最大的好处是技能依然是原子的、可复用的但技能之间的连接关系由编排层控制不用每次都在提示词里解释先做什么再做什么。Agent 只需要在编排层给定的大框架里做局部决策比如确认某个 if 条件是否成立、选择某个步骤的输入参数这大大降低了决策难度。4.2 技能内部的技能复用有时候一个大技能内部天然是多个小技能的组合。比如跨部门周报生成这个技能内部要先拉取 git 提交记录、再查项目管理系统里的任务进度、最后把两份数据合并渲染成 Markdown。如果把这个大技能做成一个无法拆分的大块头它的复用性就差而且一旦某个中间环节变化整个技能都要改。我的做法是大技能作为组合技能存在内部通过子技能调用sub-skill invocation串联。组合技能在注册时声明它依赖哪些子技能执行引擎在调度到组合技能时会自动把它的执行栈推入子技能执行栈每个子技能内部依然有独立的超时和错误处理。这有点像写程序时的函数封装底层是原子技能上层是组合技能组合技能可以被更高的流程编排再次引用。层次清晰之后每层都能独立测试、独立替换不会出现动一个参数就牵连全局的尴尬。4.3 上下文传递每个技能只认自己该认的技能编排里最容易出问题的是上一技能的输出怎么变成下一技能的输入。一开始为了实现简单我会把完整上下文传给所有技能让模型自己去挑。结果经常发生这种情况技能 A 输出里有一个数量字段技能 B 需要的是总量模型看着差不多的语义就填了实际业务上差之毫厘谬以千里。后来我强制执行了一件事每个技能只能从编排上下文里读取它依赖的那个键不得读取全局上下文。编排层在生成下一个技能的参数时先按照using声明把前序输出里对应的字段翻译成新的参数。这样技能之间是解耦的上下文不会无限膨胀而且每个技能的执行结果都更可预测。在这个基础上我还加了一层字段来源追踪记录每个技能的输入参数来自于哪个上游输出的哪个字段。调试时能看到完整的数据血缘线上出问题排查起来非常省事这也是我最舍不得砍掉的一个功能。5. 实测数据与踩坑复盘哪些设计让我睡不好觉5.1 工具收敛后效果反而提升了写了这么多设计理念再说说实际项目里的数据。我们把原来裸暴露给 Agent 的 27 个工具收敛成 14 个技能其中 9 个原子技能、5 个组合技能同时补全了描述和参数 Schema。在同样的 200 条真实工单测试集上效果变化非常明显指标改造前裸工具改造后技能化动作选择准确率选对工具/技能68.5%91.0%参数填写的首次校验通过率42.0%76.5%单任务平均工具/技能调用次数4.8 次2.9 次需要人工干预的比例22.0%9.5%可以看出收敛技能数量并没有让 Agent 变笨反而因为每个技能都描述得足够清晰模型一次就能做出正确选择不需要反复试错。参数校验通过率提升更是立竿见影这直接省掉了大量无效调用和 token 消耗。5.2 五个最值钱的坑每一个都踩过第一个坑是描述里用了不一致的动词。比如同一个动作在技能 A 的描述里叫获取在技能 B 里叫获取到模型对这两个词的理解权重是不一样的可能导致在两个相似技能之间随机摇摆。后来我们引入了同义动作归一化所有技能描述里描述同一类动作的词必须统一并且由代码自动检查。第二个坑是技能粒度太粗。一开始有个处理工单技能包含修改状态、分配负责人、追加备注、关闭工单四件事参数 Schema 有十来个字段模型根本填不全。后来拆成四个独立技能每个技能的参数都降到 3 到 5 个准确率立刻上来了。技能粒度以一个动作只做一件事为原则宁多勿粗。第三个坑是技能描述里有绝对化用语。比如描述里写获取当前用户的所有信息模型就真的以为能拿到所有字段其实技能只返回了有限字段。后来我们改用工整的枚举说明写清楚返回字段包括 A、B、C不包含 D。第四个坑是重试导致的重复副作用。这个前面提到过超时后如果无脑重试发通知、改状态这类技能就会执行两次。深入了解后我们规定所有非幂等技能必须配合业务侧的唯一请求 ID 做去重否则不允许打开自动重试。第五个坑是技能评估只看成功不看过拟合。我们最初的人工评估只统计技能是否成功调用后来发现有些案例虽然调用成功了但 Agent 选的是次优技能结果也是错的。从那时起评估集从二元通过制改成了三级打分制正确技能且正确参数、正确技能但错误参数、错误技能。这个调整让我们在后续优化中能更精准地定位问题在路由还是填参。5.3 评估集让每一次技能改进都可量化技能系统的迭代非常依赖评估集。我整理了一套轻量但有效的评估流程不必引入复杂的评测框架只需要几个关键部分场景语料库把线上的真实用户问题按业务场景分桶每个桶至少 30 条覆盖常见诉求、模糊表述、边界场景。期望动作集每条语料标注期望调用的技能链和关键参数人工标定一次之后每次改动都能自动比对。回归机制每改技能描述或参数 Schema就全量跑一遍语料库产出准确率变化报告。线上抽检闭环线上日志中随机抽取 10% 的会话做人工抽检标注Agent 是否选了正确技能是否填了正确参数每周汇总。这套流程跑下来最大的价值不是发现 bug而是能说服团队里每一个人这次的改动到底有没有用。避免我感觉好像变好了这种无法验证的优化一切靠数据说话。6. 技能共享与多 Agent 协作下一步我要做的事6.1 技能仓库让跨团队复用成为可能技能系统成熟之后我下一步想做的是技能仓库skill registry hub。同一个公司内部不同业务线的 Agent 往往有很多相似技能比如获取用户信息发送通知查询订单状态。如果每个团队各自实现一套描述风格、参数 Schema、权限模型全都不一样统一治理就无从谈起。我目前正在验证一种组织方式把技能仓库分成基础技能库company-level和业务技能库team-level。基础技能由平台团队统一维护对所有 Agent 开放业务技能由各业务团队自行维护注册时声明可见范围。这样既保证了核心能力的统一描述又允许业务侧灵活扩展。技能一旦进入共享仓库就要有明确的版本管理和下线机制。我的思路是用语义化版本号每个技能发布后不可变变更必须生成新版本并在描述里注明变更点。Agent 默认绑定主版本explicitly 标注测试版本时才会用 beta channel这样不会因为某个技能更新引发全线故障。6.2 多 Agent 协作时的技能目录另一个值得深入的方向是多 Agent 协作。过去我们是一个 Agent 干所有事技能一多提示词就会被撑爆。更好的做法是多个专业 Agent 共享一个技能目录每个 Agent 只加载与自己职责相关的技能子集。比如一个客服主 Agent它不需要知道导出财务报表这个技能的存在而一个数据分析 Agent也不需要关心修改工单紧急度这类技能。这种场景下技能目录需要支持按标签检索和按 Agent 角色过滤。我在技能注册表里加了一个visible_to_roles字段执行引擎在构建提示词时会根据当前 Agent 的 role 过滤技能列表。这样不仅上下文更精简权限也更安全——每个 Agent 只能看到它该看到的技能出问题的概率自然就小了。关于多 Agent 之间的技能调用我发现一个值得注意的细节跨 Agent 的技能调用不应该直接暴露给模型自由路由而是应该通过明确的协作协议比如主 Agent 向副 Agent 发送任务描述副 Agent 自己决定用哪个技能完成否则模型容易在两个 Agent 之间来回踢皮球。这个经验让我很受用也推荐给准备做多 Agent 协作的团队。最后分享一个我个人的小经验。如果你现在也在做 Agent 技能化不用一上来就设计一个大而全的技能规范这样反而容易陷入过度设计。我的建议是先挑三个你最头疼的裸工具把它们完整包装成技能跑通注册、调度、校验、摘要、评估这条链路然后再扩展。技能系统是在真实问题的反复捶打中长出来的不是一次画图能画出来的。踩过坑才知道哪些字段是真的需要哪些只是看起来很美好。
网站建设高端定制企业官网