2026/10/28 6:24:38

agent-skills:从工具堆砌到技能体系的LLM Agent稳定方案

agent-skills:从工具堆砌到技能体系的LLM Agent稳定方案 最早做 Agent 的时候我总以为核心是“把大模型接进来给几个工具让它自己跑”。等真把一个项目跑起来才发现工具一多Agent 就变成了脱缰野马调用错参数、反复试探、上下文漂移最后在一堆散装函数里迷失方向。后来我把思路从“给 Agent 一堆工具”调整成“给 Agent 一套技能体系”整个系统才真正稳定下来。这就是我这篇要聊的 agent-skills一套面向 LLM Agent 的技能封装与调度方案。它不是某个特定框架的独门秘笈而是一种通用做法把原子操作按业务场景封装成可注册、可校验、可编排的技能单元让模型像人一样“会什么就调什么”。不管你是刚入门 LangChain还是已经在自研 Agent 框架这篇都适配我会直接把设计思路、代码骨架和踩坑记录摊开讲。1. 为什么需要技能体系而不是工具堆砌1.1 从“工具”到“技能”到底差在哪很多人觉得工具就是技能Agent 能调用search()、send_email()、create_ticket()这不就是会技能了吗表面上看是的但实际跑起来会发现一个很尴尬的情况模型永远在猜“我现在该用哪个函数”而不是“我现在该完成哪个目标”。举例来说用户说“帮我把这周的销售数据发邮件给领导”如果你只给了query_database()和send_email()两个原子工具模型要先想“是不是得先查库再取数再写正文再发送”每一步它都在决策任何一步想歪了后面全歪。而如果把“汇总最近七天的销售数据并发送邮件”封装成一个技能模型的任务就从四步决策变成了一步确认调用这个技能传两个参数“收件人地址”和“是否包含图表”。人也是一样的你问一个实习生“会写周报吗”他说会不是因为他记得print()每一行而是因为他知道“写周报”这件事背后包含哪几个动作。Agent 技能体系就是为了让模型也拥有这种“组合记忆”。那什么才算技能我认为至少有三个特征第一它面向目标而不是面向函数技能名就是人类能理解的动作描述第二它内部可以有多个步骤步骤之间是既定流程而非临时决策第三它有明确的输入边界和输出边界参数经过 schema 校验不会跑着跑着凭空冒出额外副作用。工具是“手”技能是“驾驶技术”两者差距在于是否具备稳定的动作序列。1.2 Agent 技能库要解决的核心问题我在实际项目里遇到的痛感非常具体。第一是决策开销太大每调用一次工具模型上下文就要多一次思考痕迹工具越多模型越容易犹豫技能封装后一个复杂流程对模型来说只占一个“函数槽”上下文消耗显著下降。第二是错误率原子工具没有前置条件检查和参数范围限制模型经常传负数日期、空字符串、不存在的表名而技能可以在入口处把参数拦截住。第三是流程不可控散装工具之间谁先谁后完全由模型临场发挥同一句话换个说法它会走不同路径没法做灰度验证技能库把流程固化下来行为可预期、可回滚。还有一个容易被忽略的问题权限。一个系统里往往有查询类工具、写入类工具、管理类工具如果全部暴露给模型风险极高。技能层可以充当权限边界比如“只读技能包”和“写入技能包”分开装载模型在不同场景下只看到当前会话允许多调用的技能。这一点在我做企业内部分级 Agent 时尤其重要不是所有模型都该有能力执行删除命令。技能库解决的核心问题是“让 Agent 更接近一个专业助手而不是一个万能遥控器”。万能遥控器每个键都是裸露的用户得自己组合专业助手则能把“打开空调”“调温度”“切换模式”打包成“我冷了开制热”。对 LLM 而言技能就是它理解世界的最小业务单位。1.3 一套技能库应该长什么样我理想中的技能库分四层接入层、注册层、执行层、观测层。接入层负责把外部能力数据库、API、文件系统、脚本转换成标准动作注册层维护一份技能清单每个技能包含名称、描述、参数 schema、依赖条件和超时设置执行层负责任务调度、并发控制、错误捕获和重试观测层记录每次调用的输入、输出、耗时、Token 消耗为后续优化提供数据。这四层缺一不可只做注册不执行是花架子只做执行不观测是盲人摸象。从数据模型上看技能库的核心是“技能描述符”。我一般用 Pydantic 模型定义包括name、description、parameters、handler、permission五个字段。name和description给模型看用于意图匹配parameters给校验器用确保入参合法handler是真正的可执行代码句柄permission是运行时权限标记。注册之后技能库对外只暴露两个方法list_skills()和execute_skill(name, params)前者给模型做选择后者做实际执行。内部再怎么复杂外面只留这两个入口模型的认知负担就会小很多。我见过有人把技能定义堆成一个大 JSON几十个函数塞在同一个文件里最后模型没法区分“查询本月订单”和“查询本月订单明细”有什么区别。所以设计时一定要把“描述”当成一等公民来对待后续我会专门讲描述怎么写得让模型一眼就懂。现在先记住这条原则技能库是给模型配备的“能力目录”目录编得好不好直接影响模型用得准不准。2. 技能封装注册、描述与参数规范2.1 最小的技能定义长什么样先给一个最小可用的技能定义示例我习惯用 Python 加 Pydantic 来写from pydantic import BaseModel, Field from typing import Literal, Optional class SalesReportParams(BaseModel): recipient_email: str Field(description收件人邮箱必须是合法邮箱格式) include_chart: bool Field(defaultTrue, description是否附带趋势图) period: Literal[7d, 30d] Field(default7d, description统计周期) class SalesReportSkill: name: str generate_sales_report description: str ( 当用户需要把销售数据汇总并以邮件形式发送时使用。 适用场景发送周报、月报、销售趋势汇总。 ) params: type[BaseModel] SalesReportParams permission: str write这段代码里没有真正的“逻辑”它只是描述符。重点是把name写成一个动宾短语description写清楚“什么时候用、能做什么”参数用Field(description...)给足语义。很多初学者把参数描述写得很糊比如period只写“周期”模型根本不知道填7d还是30d有了 Literal 约束和示例说明才能减少错误。有了描述符之后真正的函数体可以单独写注册时再绑定def _send_sales_report(params: SalesReportParams, context: dict): query_result load_sales_data(params.period) chart_path render_trend_chart(query_result) if params.include_chart else None send_email(params.recipient_email, 销售周报, build_body(query_result), chart_path) return {status: ok, sent_to: params.recipient_email}为什么要拆成描述符和函数体因为描述符是给模型和校验器看的函数体是给执行器看的两者职责不同。模型不需要知道函数体内部调用了哪些接口它只需要知道“调这个技能传这几个参数能得到什么结果”。2.2 描述怎么写 LLM 才不误解描述是整个技能库里最容易被低估的部分。同样一个技能描述写得好模型能在第一轮就正确选到写不好模型会反复在其他技能之间横跳。我总结了几条实践经验。第一开头直接说要“触发条件”不要绕弯子。例如“当用户提到订单退款、售后处理、退费申请时使用”比“这是一个退款处理技能”有效得多。模型做意图识别本质是在做文本匹配触发条件越贴近用户的真实表达命中率越高。第二明确写出“不要什么时候用”。给正面的触发说明加一句反例比如“注意仅用于已支付订单未支付订单请调用 remind_payment”能大幅减少误召。第三描述里避免写流程细节只写最终效果。模型不需要知道函数内部先查表还是先调接口写多了反而干扰它判断它只看输出目标和适用边界。还可以做一个描述质量的自检把技能描述盖住函数名拿给另一个同事看问他“如果用户说 X你会觉得这个技能能用吗”如果他能答对说明描述合格。不要用大模型去生成那种“优美的废话”比如“本技能用于高效地实现销售报表的自动化生成与邮件分发”这种句子信息量很低真正有用的信息是“什么时候触发”和“输入什么”。2.3 参数 Schema 决定调用成败参数 Schema 是技能库防呆设计的关键。你不约束参数模型就敢传recipient_email老板、period上周五一旦进入函数体各种隐错就来了。所以参数设计一定要做到三点类型收敛、范围收敛、默认值兜底。类型收敛用 Pydantic 的严格类型或者 Literal 做。例如period用 Literal 限定成只能选7d或30d模型传了last_week就会在校验阶段被拦下不会漏到函数体里。范围收敛用 Field 描述加gt、lt这类校验器比如导出条数如果限制le10000模型就不会尝试拉全表。默认值兜底则要保证“不传也能跑”不是每个参数都要必填给合理的默认值能显著降低模型的决策压力。我还要强调一个容易踩的坑参数数量不要超过五个。模型在做 Function Calling 的时候参数越多生成正确 JSON 的概率越低五个以上参数的错误率明显上升。如果某个技能需要七个参数那就把技能拆成两级外层技能收三四个参数内层通过配置上下文补齐剩余字段。这就像给人填表表太长没人想填Agent 也一样。校验失败的日志也别只记录错误要把完整的调用参数、技能名称、模型返回原文都记录下来。我排错时最依赖的信息就是“模型当时到底传了什么值”只有看到原始值才能判断是模型理解错了还是我们的参数描述有歧义。这条经验帮我省了至少一周的排错时间。3. 实操手写一个轻量 Agent 技能库3.1 注册中心的设计技能库的心脏是一个注册中心。它的职责是把所有技能描述符集中管理提供查询列表并在执行时定位到对应的 handler。我用一个非常简单的类来实现class SkillRegistry: def __init__(self): self._skills: dict[str, dict] {} def register(self, skill_cls): descriptor skill_cls() if descriptor.name in self._skills: raise ValueError(fDuplicate skill name: {descriptor.name}) self._skills[descriptor.name] { descriptor: descriptor, handler: descriptor.run, params_model: descriptor.params, } return skill_cls def list_skills(self, permission_maskNone): result [] for name, item in self._skills.items(): desc item[descriptor] if permission_mask and desc.permission ! permission_mask: continue result.append(desc.to_schema()) return result def execute(self, name: str, params: dict): if name not in self._skills: raise KeyError(fUnknown skill: {name}) item self._skills[name] validated item[params_model].model_validate(params) return item[handler](validated)注册顺序需要注意技能之间如果有依赖关系比如generate_sales_report依赖load_sales_data那么load_sales_data应该注册为内部服务而不是独立技能。否则模型可能会跳过外层技能直接调用内层函数流程控就失效了。我的做法是内外分离对模型可见的只有“可执行技能”内部函数统一放在services目录不注册到公开列表里。这个注册中心只有几十行但它撑起了整个技能体系。你可以把它扩展到支持 YAML 配置加载、技能分组、条件注册等能力但核心逻辑不变先注册后查询再执行。不要一开始就设计成微服务架构技能库的瓶颈从来不是注册中心的性能而是技能的语义质量。3.2 让 LLM 能发现技能Function Calling 接入注册中心只是对 Python 程序可见要让 LLM 知道有哪些技能必须把它们翻译成模型接口能识别的 JSON Schema。以 OpenAI Function Calling 为例我们的list_skills()返回的每个技能都要转换成如下格式def build_openai_function_schema(skill_schema: dict) - dict: params skill_schema[params_model].model_json_schema() return { type: function, function: { name: skill_schema[name], description: skill_schema[description], parameters: params, }, }这样接入之后模型会在每次会话开始时看到一份“可调用技能清单”。如果清单里有 30 个技能模型每次决策都要把这 30 个函数的描述全部塞进上下文既费 Token 又容易互相干扰。我建议做一个粗粒度的“意图路由”先让模型判断用户请求属于哪个域销售、售后、人事再只加载该域的技能子集项。技能库的分组能力在这里就派上用场了每个技能声明一个domain字段路由层根据领域标签过滤。还有一个细节OpenAI 的函数名不能随意包含特殊字符注册技能时如果用的是中文短语务必转换成英文或拼音否则接口直接报错。我自己习惯用下划线英文命名保持verb_noun结构比如send_email、create_task。描述字段用中文写没问题函数名保持 ASCII 更稳。接入之后别急着上线先做一轮“邀请测试”拿十几条真实用户话术观察模型第一轮选中的技能是否和人工判断一致。准确率低于 80% 就回头改描述不要通过加大系统提示词硬掰。系统提示词是全局的为单个技能加说明会污染所有技能的选择效果。3.3 执行与错误处理执行层最常见的需求是超时控制、并发限制和错误重试。模型可能同时请求两三个技能但有些技能涉及外部 API并发量上来后容易把下游打崩有些技能是幂等的可以反复重试有些技能是非幂等的重试会造成重复写数据。我通常在技能描述符里增加两个字段max_concurrency和retryable。class BaseSkill: max_concurrency: int 1 retryable: bool False timeout_seconds: int 30执行器在线程池或者 asyncio 中运行技能时先检查当前技能并发数是否达到上限达到就排队等待执行时用asyncio.wait_for做超时如果超时且retryableTrue退避重试最多两次。这个机制看起来简单但能挡住很多线上事故。我见过因为模型循环调用同一个写接口把测试环境数据库灌成重复数据的情况就是因为没有加幂等控制和并发限制。错误返回也要规范化。技能执行失败时不要只返回异常堆栈给模型模型看到一堆英文异常不会处理反而会硬着头皮重试。要返回结构化错误信息包括错误码、用户可理解的错误描述、可能的修正方向{ error: INVALID_PARAMS, message: 收件人邮箱格式不正确请提供合法邮箱地址, suggestion: 检查邮箱地址是否包含 符号和域名后缀 }模型拿到这类错误信息下一步才能做出正确修正。记住一个原则执行结果不是给程序员看的是给模型看的。模型同时也是你的“用户”错误信息写得越像人能看懂的提示模型恢复得就越快。4. 常见问题与排查技巧实录4.1 模型反复改错技能参数这是我在接入 Function Calling 后遇到的第一个高发问题。现象是模型已经选了正确的技能但是在生成参数时不断报错或者来回修改。排查时我先看日志里的原始参数发现很多值都是“语义正确但格式不对”比如把7d写成7 days把邮箱写成领导xx。后来我把参数描述的每个字段都附上了示例值错误率立刻下降了一半。但如果遇到模型死循环调用同一个技能可能是结果返回格式没让模型“满意”。比如某个查询技能返回了空数组模型会认为查询不够彻底反复换参数重试。解决办法是在结果里明确写一句“当前条件无数据如需扩大范围请让用户确认”直接把模型的下一步引导到“询问用户”而不是“自动重试”。还有一个隐藏坑技能描述里如果出现“如果失败请重试”这种字眼模型会非常听话地重试千万别在描述里给这类暗示。最后说一个硬规则校验层的错误次数要统计同一个技能在单次会话里连续失败超过三次就强制切换到“人工确认模式”不再让模型自动重试。这个护栏能避免很多无意义的 Token 消耗。4.2 技能之间的上下文传递丢失技能库做大了以后模型经常需要先执行search_customer拿到 customer_id再调用create_order去下单。问题来了第二次调用时模型是应该把 customer_id 作为参数直接传给第二个技能还是技能库自动从上下文里取我建议直接传递而不是依赖隐式状态。每个技能都应该是无状态的纯函数上下文只保留在会话层技能之间不共享内存变量。具体的做法是把前一个技能的关键输出结构化成context_variables放到 Agent 的 Running Context 中模型看到了也会把它填到下一个技能参数里。你需要检查的是技能的返回值结构是否足够清晰。例如search_customer返回{customer_id: 12345, name: 张三}模型一看就知道拿customer_id去填下一个参数如果返回的是{data: [{id: 12345, name: 张三}]}模型可能理解不了应该拿哪个字段。所以技能输出的字段命名也要贴近业务直觉和参数 schema 的字段名保持一致能极大减少传递错误。还有一类上下文漂移发生在长时间会话中前五轮已经查过用户所在城市第十轮模型调用天气技能时又去问用户要城市。这种时候可以给技能增加infer_from_context的能力让它可以读取会话记忆中的前置字段。但前提是会话记忆的结构化程度要高不能是一堆聊天记录堆在那里。4.3 沙箱环境与依赖隔离技能库涉及的第三方依赖五花八门有的技能要连数据库有的技能要调内部 HTTP API有的技能要跑一段动态生成的代码。全部塞进同一个 Python 进程迟早会因为依赖版本冲突或者异常进程拖垮整个 Agent。我在早期吃过一次亏某个技能里引入的库把全局 logging 配置改了结果所有后续技能日志全丢了排查了很久才发现。之后的做法是常规技能用进程级隔离通过multiprocessing或者asyncio.subprocess执行高风险技能比如执行用户提供的代码、写入文件、操作远程环境放到 Docker 容器里给只读文件系统、禁用网络或者限制出网地址。写操作类的高危技能还会加一层审批流转模型对接人的地方生成一个请求人点确认后才真正执行而不是模型自己完成全链路。如果你暂时不具备 Docker 环境至少要在函数装饰器上加一个requires_sandboxTrue标记日志里单独记录这类技能的调用方便事后再审计。沙箱不是越严越好而是和技能权限匹配只读查询不需要开 Docker写操作必须有隔离这是成本和安全之间的取舍。4.4 技能版本管理技能不是写一次就永久不变的。我维护技能库期间改过各种东西描述措辞、参数默认值、内部调用的 API 地址、依赖版本。刚开始没做版本管理出过一次事故线上 Agent 突然开始用新接口调用方式和旧数据格式不兼容整个任务链路断了一整天。痛定思痛之后我给每个技能增加了一个version字段注册中心只保留最新版本但执行日志和观测系统里会记录当时的版本号和技能源码 hash。如果你需要同时稳定运行多个独立技能的“你感觉不错”的版本可以通过技能包进行版本化发布。技能包本质上是一个固定版本的技能集合统一标记release_tag比如v1.2.0。Agent 启动时指定加载某个发布包不回源到最新代码这样就能避免“昨天还好好的今天突然变了”的问题。每次更新发布包之前我会跑一遍回归用例抽二十条历史话术对比新旧包在技能选中的结果差异差异超过预期就回滚。版本管理这件事听起来很重其实落地很轻就是在注册表里加一列版本号、一个发布清单。不做的话出了问题根本没法快速还原。5. 一点经验谈开头我说过把工具堆砌变成技能体系之后Agent 的可靠性会上一个台阶。实际操作中我最大的感受是技能库设计好坏不取决于代码写得有多花哨而取决于“模型对你的技能目录有没有稳定的第一印象”。描述写得清楚、参数约束到位、执行结果结构化这三件事做到位了模型就像一个受过良好培训的新员工指哪打哪。相反哪怕底层能力再强模型面对一份混乱的技能清单也会手足无措。最后分享一个细节每次技能库上线前我会准备一个 mock 的模型调用记录把用户最常说的二十句话一条条读给技能清单“听”看模型会不会选对。这步不花太多时间却能把绝大部分问题暴露在上线之前。如果你正在搭自己的 agent-skills我建议你从一门最常用的小业务技能开始把它做得足够扎实再逐步往外扩展而不是一开始就铺一大堆技能。技能库是长出来的不是规划出来的。