Agent技能系统设计:从零搭建可复用的智能体技能库

发布时间:2026/10/28 16:30:10
Agent技能系统设计:从零搭建可复用的智能体技能库 手头这个 agent-skills 项目从能跑通到稳定上线前后折腾了快一个月。简单说agent-skills 就是给智能体预装的一批可复用能力查订单、算指标、生成周报、同步外部系统……它们不埋伏在大段提示词里而是以带名字、带参数、带返回结构的“技能包”形式存在让大模型在任务过程中按需挑选、调用、组合。这套设计解决的核心问题是智能体不能每次都被逼着从零推理完整动作流程而是能直接调用已经验证过的专业动作把通用智能和专用能力分开。写这篇文章是想把设计思路、最小可运行代码、以及上线后踩过的坑一次讲清楚。如果你是正在做 Agent 功能的工程师或者想沉淀技能库但还没找到落地路径的同学应该能直接拿去参考。不过在聊代码之前得先把一个基本问题说透技能到底是什么。我最早也把它当成花哨的“插件”直到第一次在真实项目里翻车才彻底改过来。1. 先搞懂技能层为什么普通工具函数不够用1.1 一次真实翻车没有技能系统的 Agent 有多难用第一版智能体没有技能系统所有动作都写在提示词里告诉模型“你可以执行步骤一、步骤二、步骤三”模型每走一步输出一段 JSON action代码再去解析执行。听起来很灵活实际用起来全是坑。第一个坑是动作不固定。同一个“查询订单”动作模型一周里生成过 12 种字段写法有叫 order_id 的有叫 id 的还有叫 orderNumber 的。解析层只能把所有可能的字段名都兼容一遍每加一个新场景解析规则就膨胀一次。第二个坑是流程脆弱。一次客服对话里模型要生成“查询订单”的 JSON紧接着还要生成“确认订单状态”的 JSON只要中间某个字段写错整条链路就断。当时的日志显示超过三成的失败发生在动作解析环节也就是模型想做的事情是对的但动作描述是错的。第三个坑是 token 消耗同一个取数步骤在每段会话里重复生成平均每个会话多消耗七八百 token成本高还不稳定。后来我把这些动作收敛成固定接口模型不再“生成动作”而是“选择动作”。这就是 agent-skills 最早的原型。那次重构让我明白一件事通用模型擅长的是做决策不是每次都从头发明操作过程。1.2 Skills、Tools、Plugin 的边界到底在哪网上聊 Agent 概念时Tool、Skill、Plugin 经常混着用我一开始也分不清。后来按自己的工程口径做了区分。Tool 是原子能力比如 send_email、query_order_status一个函数对应一件事没有内部流程。Skill 是行为单元内部可能组合多个 Tool比如“生成月度复盘报告”需要拉数据、算指标、生成摘要、发邮件这四步拼在一起才算一个完整技能。Plugin 更像一种业务集成形态通常包含多个 Skill还会带配置界面、权限模型和生命周期管理。用表格看更清楚维度ToolSkillPlugin粒度原子操作复合行为单元业务功能包触发方式模型直接调用模型按意图调用按场景安装/启用典型例子发送单封邮件生成并发送周报CRM 集成套件是否关心业务语义不关心必须关心天然绑定业务域我的观点是名字怎么叫不重要但“原子层”和“行为层”必须分开。如果直接把 Tool 当 Skill 用模型在复杂任务里就会把决策压力全扛在自己身上组合动作时反复出错。如果反过来把太大太重的流程塞进一个 Skill模型又很难判断什么时候该触发它。Skill 卡在中间这一层是最值得投入精力的。1.3 我坚持的三条设计原则原子性、可发现性、可组合性定义技能之前我先定了三条原则后面所有设计都围绕它们展开。原子性是指一个技能只做一件完整的事。“生成周报”和“发送周报”必须拆开不能合并成一个“生成并发送周报”的技能。原因很现实用户可能只想生成不想发送合并之后模型会犹豫是否触发。判断原子性的简单办法是看技能名字里的动词能不能再拆能拆就拆。可发现性是指技能对模型而言是“找得到”的。模型不是靠代码指针发现技能而是靠名字和描述。描述写得含糊技能就等于不存在。这就像超市货架商品再好标签写错顾客也拿不到。可组合性是指一个技能的输出可以被另一个技能继续消费。如果每个技能返回值格式都不一样编排层只能写一堆 if-else 来适配组合能力基本为零。为什么是这三条因为我观察到一个规律技能系统的大部分失败落在触发错误、参数错误、结果无法继续传递这三类问题上。三条原则恰好各自对症。后面写代码、写描述、做编排时凡是不符合任一原则的做法我都会再想一遍是否值得。2. 一个技能系统的三层设计定义、注册、执行2.1 技能定义层名字、描述、参数契约这样写才不翻车技能定义层是最容易糊弄、也最容易返工的部分。一个技能至少要有 name、description、parameters、func 四样东西前三样是给大模型看的func 是真正干活的。名字的规范是动词开头、语义唯一。query_order_status 比 order 好generate_daily_report 比 report 好。要特别警惕近义命名比如 query_order 和 fetch_order 同时存在模型会随机选一个而且你很难在测试里复现这种随机性。描述是重头戏。我的模板是三层什么场景用、什么场景不用、依赖什么前置信息。写一个好的示例{ name: query_order_status, description: 当用户询问订单状态或物流信息时使用。必须提供订单号 order_id如果用户只给了手机号请先调用 lookup_customer_order 获取订单号不要直接调用本技能。, parameters: { type: object, properties: { order_id: { type: string, description: 订单号格式如 SO2024001 } }, required: [order_id] } }这段描述里写了两条关键信息一是触发条件二是反例直接告诉模型什么情况别来。很多技能描述只写“查询订单状态”模型在用户只是抱怨物流慢的时候也会莫名其妙触发一次。参数契约我直接用 JSON Schema。经验是 required 字段越少越好能设枚举就设枚举。一个查询接口如果让模型填十几个筛选条件模型一定会幻觉出几个不存在的值。正确做法是只暴露高频参数低频参数合并成一个 options 对象需要时由更上层的路由技能决定怎么给。返回结构也在这里定下来。我统一用 status、data、error、meta 四段式任何技能都返回这个结构。status 只有 ok、empty、error 三种empty 表示“逻辑正确但没有结果”error 表示异常。data 是真正要输出的内容error 是给模型看的错误描述meta 里放耗时、来源这些旁路信息。之所以不直接裸返回结果是为了让编排层和模型都能快速判断下一步该干什么。2.2 注册层从字典表到技能发现技能定义好了需要一个地方存它们这就是注册层。最小实现是一个注册表类内部用 dict 按名字存技能提供 register、get、all 三个方法。注册表真正要解决的是“哪些技能该暴露给模型”的问题。工具列表会占用上下文窗口技能数量少的时候全量暴露没问题技能一多模型就开始挑花眼。我自己的经验阈值是 30 个以内可以直接进提示词超过 30 个就要做技能发现。技能发现有两个层次。第一层是场景分组把技能按业务域分成客服、运营、数据分析等组每个会话只挂载相关组。第二层是向量检索把技能 name 和 description 一起做 embedding运行时拿用户任务向量召回最相似的 Top-K 个技能。实测下来Top-K 取 8 到 10 效果比较稳召回太少模型没得选召回太多又回到“选择困难”的问题。注册表还应该存些旁路元数据比如 version、author、tags、usage_count。这些字段不会直接发给模型但对后续的版本管理和调用统计很有用。注意不要把所有技能都做成全局可见能做白名单就做白名单避免模型在非相关场景里乱翻。2.3 执行层把技能塞进 Agent 主循环执行层是最容易写崩的一层。标准做法是 ReAct 循环模型思考决定调用某个技能执行器执行技能把结果作为 observation 返回模型看到结果后继续推理直到完成任务。循环代码不复杂但有两个细节必须注意。第一技能内部异常不能直接抛出要捕获后转成结构化错误返回。模型的能力之一是从错误信息里自愈你把异常吞掉、把 error 字段塞回对话模型往往能自己修正参数再试一次如果你直接让整个 Agent 崩溃用户看到的就是一句冷冰冰的“系统错误”。第二必须设超时。一个技能如果内部调用了外部 HTTP 接口可能会卡住几秒甚至几十秒。执行器要单独给技能调用加 timeout超时结果也按 error 返回。否则一个技能卡住整个循环的空转轮次就没了。执行层还要区分不同模型的能力差异。有些模型原生支持 function calling可以直接传 tools有些模型不支持只能用文本形式把技能列表塞进 system prompt。后者的提示词里要写明严格格式比如“只允许输出以下动作的 JSON”否则模型会自由发挥。3. 手写一套最小可用的智能体技能系统3.1 技能类定义十行代码搭建基础骨架先定义一个最朴素的 Skill 数据类把设计层的字段固化下来# skill.py from dataclasses import dataclass from typing import Callable, Any, Optional dataclass class Skill: name: str description: str parameters: dict func: Callable[..., Any] version: str 1.0.0 author: str platformparameters 这里存的是 JSON Schema 字典func 是真正执行的回调函数。之所以用参数对象而不是普通函数签名是因为要直接生成模型可读的工具描述而 function calling 的标准输入就是 JSON Schema。定义两个最简单的技能做示例一个是查询订单一个是计算订单金额汇总。查询订单函数内部可以对接数据库这里只做占位演示def query_order_status(order_id: str) - dict: # 真实项目里这里会查数据库或调用订单服务 if not order_id.startswith(SO): return {status: error, data: None, error: 订单号格式不正确, meta: {}} return { status: ok, data: {order_id: order_id, state: shipped}, error: None, meta: {source: order_service}, }注意返回值里我用的是规范四段式这样后续编排和模型读取都不需要额外适配。3.2 注册中心与 LLM 工具描述生成注册中心的核心能力是把 Skill 对象转成大模型能理解的 tools 格式class SkillRegistry: def __init__(self): self._skills {} def register(self, skill: Skill) - None: if skill.name in self._skills: raise ValueError(fduplicated skill: {skill.name}) self._skills[skill.name] skill def get(self, name: str) - Skill: return self._skills.get(name) def all(self) - list[Skill]: # 按名字排序保证生成工具列表的顺序稳定 return [self._skills[k] for k in sorted(self._skills)] def for_llm_tools(self) - list[dict]: return [ { type: function, function: { name: s.name, description: s.description, parameters: s.parameters, }, } for s in self.all() ]代码里有一个我踩过的坑遍历 dict 时如果不排序工具列表顺序每次可能不同。模型对列表前面的工具更敏感顺序不稳定会导致同样的任务有时触发这个技能、有时触发那个技能。一次修复方式是按技能名排序简单有效。另外一个细节是注册时不允许重名。重名往往是复制粘贴造成的如果不拦截后面排查问题时你会面对两个不同行为却同名技能完全无法定位。3.3 Agent 主循环让模型学会按需调用主循环是整个系统的引擎逻辑只有几步但每一步都有讲究def run_agent(user_task: str, registry: SkillRegistry, llm, max_steps: int 8): messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: user_task}, ] for step in range(max_steps): response llm.chat( messagesmessages, toolsregistry.for_llm_tools(), tool_choiceauto, ) if not response.tool_calls: return response.content messages.append(response) for call in response.tool_calls: skill registry.get(call.function.name) try: result skill.func(**json.loads(call.function.arguments or {})) observation {status: ok, data: result, error: None, meta: {}} except Exception as exc: observation {status: error, data: None, error: str(exc), meta: {}} messages.append({ role: tool, tool_call_id: call.id, content: json.dumps(observation, ensure_asciiFalse), }) return 已达最大轮次任务未能完成这段代码最关键的思路是把所有信息都通过 messages 传回给模型让它自己消化。出错信息也不例外我故意把异常字符串放回 observation模型通常会在下一轮修正参数重新调用。自己亲眼见过模型在拿到“订单号格式不正确”后主动把传入的 id 改成标准 SO 前缀并重试。如果你把异常直接吞掉模型就失去了一次“自愈”的机会。max_steps 必须设上限。我线上常用 8 到 10太大会让无意义的循环持续很久太小会让复杂任务完不成。另外跟模型约定好完成用户任务后不要做多余动作直接输出结论。这样可以避免模型在已得出答案后又忍不住调用一个不相关的技能。3.4 技能评测只跑通不算数很多项目做到“能跑通”就停了这等于没做质量保障。我的做法是三层评测。第一层是函数级测试直接对技能函数做单元测试验证输入输出是否稳定。第二层是接口级测试模拟真实参数给 LLM 的 function calling API检查模型是否在正确场景下触发了正确技能。第三层是任务级评测准备十来条典型用户请求记录每次调用的技能名、参数、是否完成任务。一个最简单的评测集可以这样记录测试任务期望触发技能实际触发是否正确参数是否正确任务完成用户查询订单 SO2024001 的状态query_order_status是是是用户抱怨物流太慢但没给订单号不应触发技能未触发无是用户要求做本周订单金额汇总generate_order_summary是是是每个模型版本升级后这个评测集都要完整跑一遍。我遇到过模型版本更新后同一个描述下技能触发率从 90% 掉到 60% 的情况如果没有回归评测问题会直接漏到线上。4. 技能编排与组合从单一技能到完整工作流4.1 三种最基本的编排模式顺序、并行、条件路由单个技能能解决单点问题但真实任务往往是流程。编排最常用的有三种模式顺序执行、并行执行、条件路由。顺序执行也叫 pipeline适合步骤之间有依赖关系的场景。比如“生成运营日报”必须先拉销售数据再聚合统计最后生成摘要每一步的输出是下一步的输入。实现时不要写死每一步而是给编排器一个技能序列由上一技能返回结果决定下一技能的参数。并行执行适合互不依赖的动作。比如用户问“我的订单 SO2024001 发货了吗同时最近三个月有没有售后记录”查询物流和查询售后是两个独立技能可以并发执行再合并结果。并发能显著缩短响应时间但要注意共享状态别被并发写坏我一般只做只读并行。条件路由是让决策逻辑和动作逻辑分离。用一个 router 技能根据输入内容判断走哪条分支。比如客服工单进来先做意图分类再决定调用“自动退款技能”“人工介入技能”还是“发送安抚话术技能”。路由本身也可以是大模型决策但动作仍然落到具体技能上。这个模式能避免把一堆 if-else 写在主循环里。4.2 数据流与状态技能之间的信息怎么传技能编排最容易被忽略的是数据流设计。几种编排模式跑起来后技能 A 的输出如何变成技能 B 的输入是决定可组合性的关键。我建议在编排层维护一个 skill_result_pool按技能名和调用序号存储上次结果。比如订单查询技能输出了 data 里的订单对象下一步要算金额汇总就从 pool 里取 order_id不需要让模型重新生成一遍参数。还一个容易犯的错把超大结果直接塞回 messages 传给模型。比如拉取全量订单明细后如果直接把几百行记录塞进上下文模型会浪费大量 token还容易看漏关键信息。正确做法是存到会话级内存对象里只给模型返回一个引用 ID 或者更精简的摘要。这就像中央厨房备菜备好的菜放在工作台厨师只需要知道“菜已备好、编号是 03”不用每次重新读一遍菜谱。记录状态时别忘了清理。会话结束后 pool 要释放否则长期运行的内存会越来越大。我当时就是没做清理跑了三天后内存占用接近翻倍查了半天才发现是历史技能结果全堆在内存里。4.3 技能调技能高阶组合的甜区与坑技能可以调用其他技能。实现上很简单把 registry 传进技能函数里内部按名字取子技能执行def generate_order_report(order_id: str, registry: SkillRegistry) - dict: order registry.get(query_order_status).func(order_id) if order[status] ! ok: return order summary registry.get(calc_order_summary).func(order[data]) return summary这个能力很强可以把一条高频稳定路径固化成新技能。比如“生成订单报告”内部包含查询订单、计算汇总、格式化三个子技能外层模型只需要调用 generate_order_report 一个入口。但这里有两个大坑。第一个是组合深度失控。技能调技能再调技能一旦超过两层错误定位会非常痛苦。我的经验是内部组合最多两层超过两层就重构把更深层逻辑直接揉进底层技能里。第二个是隐式依赖。如果技能 B 依赖技能 A 先被调用但编排器没有显式保证顺序结果就会不稳定。解决方法是把这种依赖关系写进技能描述或者在编排层显式校验前置条件。组合也不是越多越好。过度封装会让模型变成“被动的字典查找”它不知道内部发生了什么一旦用户需求稍微偏离既定流程模型就不知道该怎么调整了。我现在只在子流程足够稳定时才封装组合技能新场景里更多的还是让模型在原子技能上自由编排。5. 常见问题与排查实录我踩过的四个大坑5.1 模型死活不调用技能先查这三样技能列表都传进去了模型就是无视直接凭记忆回答问题这是最常见的问题。排查时我按以下顺序查。第一个是描述不清晰。技能描述里没有触发场景的关键词模型判断不出来。比如一个负责查询会员积分的技能描述只写“查询积分 ”模型在其他话题下就会忽略它。改成“当用户询问会员积分、积分抵扣、积分过期时间时使用”触发率立刻上升。第二个是工具列表过长。技能超过几十个时模型对后面的技能基本视而不见甚至整个 tools 参数被截断。解决办法是按场景分组挂载或做向量召回。第三个是缺少调用示例。有些模型对“什么时候该调用工具”的理解比较弱给出一个完整的用户提问加工具调用示例比在描述里反复强调有效得多。我在 system prompt 里固定放一组示例对话实测触发率和参数准确率都有明显提升。还有一类特殊情况模型本身不支持 function calling这时要把工具转成文本格式塞进提示词。如果用了不支持 tool calling 的模型却仍传 tools 参数API 不会报错但模型完全不会触发。5.2 技能被错误调用名称撞车与参数幻觉技能被误触发比不触发还难修。最常见的成因是两个技能名字或描述太接近。比如 query_order_status 和 query_after_sale_record描述里都提到“订单”模型在用户问“订单什么时候到”时可能错误触发售后记录查询。解法是名字差异拉大描述里写清楚各自专属场景必要时加否定语句“如果用户询问物流进度请使用 query_order_status不要使用本技能。”参数幻觉则是模型在调用时自动补全了用户没提供的信息。最危险的情况是缺 required 参数时模型凭空填一个看似合理的 order_id。解法是执行层先做参数校验和 required 字段严格比对缺失就返回参数错误。还可以用枚举约束可选值把模型发挥的空间压缩到安全范围。另一个我现在才补上的防线是语义校验。比如 order_id 有固定 SO 前缀哪怕模型填了完整字段格式不对也要进去查一遍查不到就返回 empty 而不是 ok。绝不在空结果上返回成功状态。5.3 “假成功”执行成功但结果是错的假成功比报错隐蔽得多。技能函数返回 statusok模型基于这个结果写出一段漂亮但错误的结论用户察觉不到却会被错误数据坑到。典型场景是报表分析。拉当月销售数据结果表里为空但函数返回的是空列表外层模型继续写“本月销售额为零建议加强推广”。解决方案是定义完备状态把“无数据”和“数据正常”分开查询没拉到任何行就返回 statusempty模型看到 empty 就不会生成空洞结论。我在所有查询类技能里都强制加了这个判断空结果必须显式表达出来。另一个防御手段是结果断言。技能返回前用几行代码校验结果是否符合预期比如金额汇总必须大于等于零、图表数据长度不能为空不满足就抛异常转成 error。我把断言当成技能的“出厂质检”宁可让模型看到错误也不能让它看到假象。5.4 失控循环Agent 在一个技能里打转Agent 在同一个技能上反复调用每次参数几乎一样结果也一样白白消耗 token 和轮次。这类问题在高自由度编排里很容易出现。一个真实案例模型反复调用查询技能十几次每次传的订单号都一样返回也相同但模型就是不肯往下走。原因不是模型蠢而是返回信息没有给它带来“新东西”它不知道下一步该做啥只能原地重复。我的修复方案分两步。第一步是重复动作检测记录最近 N 步的“技能名 参数哈希”发现完全重复时在 tool message 里追加一句话“你已经用相同参数调用过该技能结果为 X请不要再重复调用。”模型看到这句话后通常会改变策略。第二步是提供“收尾技能”给模型一个 stop_agent 技能允许它在觉得任务完成时主动结束循环。模型有能力收尾之后就不需要靠无限调用外部技能来拖延决策了。5.5 问题速查表症状可能原因排查与解法模型不调用技能描述含糊、列表过长、缺少示例重写描述、场景分组、向量召回、补充调用示例模型调用错技能名称相似、描述范围重叠差异化命名、描述加否定场景、执行层加白名单技能成功但结果错误空结果未显式表达、无校验增加 empty 状态、返回值断言、语义校验Agent 重复调用同一技能信息不足、模型在兜圈子重复动作检测、提供 stop_agent 技能、压缩上下文工具列表顺序不稳定dict 遍历无序按技能名排序生成 tools6. 真正落地后我沉淀下来的工程习惯系统上线容易长期稳定运行难。这里分享几个我后来一直坚持的工程习惯能帮你少走很多弯路。技能一定要做版本管理。注册表里每个技能带 version每次行为变化都要升版本并在 changelog 里记一笔。不要小看这件事一个技能描述被悄悄改过模型触发率可能浮动十几个百分点如果没有版本记录连改动来源都查不到。模型对技能描述的依赖程度远超预期描述即行为。衡量技能价值时别只看“跑通次数”要看调用率、参数错误率、空结果率。我在执行层加了埋点每个技能被调用时都会记录会话 ID、动作名、参数摘要、返回状态、耗时。日志格式类似这样{session_id: s_123, action: query_order_status, params: {order_id: SO2024001}, status: ok, latency_ms: 120}这些日志是排查问题的第一手材料。有一次线上反馈订单状态显示错误我直接按会话 ID 拉出整条技能调用链几分钟就定位到是某个子服务返回字段变更而不是模型出了问题。日志里尽量不要记录敏感业务数据参数可以做哈希或脱敏保护用户隐私这件事怎么强调都不为过。最后说一点我个人体会。做 agent-skills 最难的其实不是写代码而是定义技能边界。封装过头模型变成被动查字典的工具决策能力被架空封装太细模型又要花大量轮次在组合动作上频繁出错。拿捏这个度没有捷径只能靠持续观察真实会话记录看模型在哪些场景触发了不该触发的技能、又在哪些场景迟迟不触发。每次优化技能描述和边界后再跑一遍评测集确认效果。如果让我给新人一个建议我会说从二三个高频技能开始跑稳了再扩不要一开始就铺一个大而全的技能库。技能库的膨胀速度远比你想象中快而每一个失效技能都是要还的技术债。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询