news 2026/9/26 23:06:23

构建可插拔技能系统:让LLM智能体高效编排工具调用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
构建可插拔技能系统:让LLM智能体高效编排工具调用

先交代一个背景:今年上半年我在做一个人机协作项目,核心思路是把大模型从“聊天窗口”里拽出来,让它真正去操作文件、遍历数据、调用外部API。结果发现一个非常尴尬的情况——模型能力很强,但落到具体任务上,它不知道该用哪段“手艺”。后来我花了大量时间把思路收拢到一件事上:给智能体搭一套可插拔的技能系统。这个项目我内部就叫agent-skills。如果你也在搞Agent、搞自动化工作流,或者单纯想让LLM少说废话多做实事,那这篇文章应该能帮你少踩几个大坑。

agent-skills本质上做的是这样一件事:把离散的工具调用、任务步骤、上下文处理逻辑全部封装成一个个独立的技能单元,由LLM感知环境后动态编排调用。这种方式解决的问题很直接——传统硬编码的自动化流程,换一个输入场景就崩,而基于技能系统的Agent能自己判断该用哪个技能、按什么顺序用。适合所有正在尝试把LLM接入真实业务系统的开发者,也适合那些已经跑通了RAG和对话机器人、正准备往下一层“做事”进阶的团队。

1. 项目整体设计与思路拆解

1.1 核心需求:为什么不能让LLM直接调函数

业内做Agent早期有个非常粗暴的路径:把所有能调用的函数一股脑塞进System Prompt,让模型自己选。这个方案Demo阶段跑得非常爽——什么天气查询、待办事项,看起来智能得不得了。一旦进入真实业务,马上就露馅了。

第一个问题是上下文膨胀。塞进去五十个函数定义,每个函数带上参数说明,光函数描述就能占掉两三千token,留给真实对话和推理的空间被严重压缩。第二个问题是函数之间的依赖关系表达不清楚。业务流程里存在明确的先后顺序:先查订单状态,再决定退款还是补发。这种链路靠“让模型自由发挥”是稳不住的,它总会找到一种你完全没想到的刁钻调用顺序。

agent-skills的定位就是在“裸函数调用”和“全自动规划”之间找一个折中:系统帮你把可复用的能力边界划清楚,模型只需要在边界内做选择和排序。

我当时定下的核心设计目标是四条:

  1. 新技能接入零代码改动——加新功能不需要改Agent主干代码。
  2. 运行时技能状态可视化——能随时看到当前Agent调了哪个技能、执行到哪一步、输入输出是什么。
  3. 技能间无缝衔接——技能A的输出能干净地成为技能B的输入,不搞复杂的格式转换。
  4. 失败可重试、可降级——单个技能调用失败不影响整个任务链路。

这套需求听起来很顺,但真正落地的时候,每一个目标背后都有一堆细节要处理。

1.2 方案选型:为什么选择“技能描述优先”而不是“代码优先”

设计之初我经历过一次特别痛苦的重构。最初版本我采用的是“代码优先”——每个技能就是一个Python类,里面定义了run()方法,技能之间的衔接靠写胶水代码。结果用了一个月,发现最大的瓶颈不是写技能,而是改技能。每一次接口微调,调用方就要跟着改,团队的沟通成本高得离谱。

后来我彻底转向了“描述优先”的思路。每个技能文件由两部分组成:自然语言编写的技能描述和可执行的代码实现。模型在决策时只读描述,不读代码;执行时才加载代码。

这个转变有本质区别。代码是给人看的,哪怕写注释写得再好,模型的语义理解依然会偏差。而自然语言描述,比如“此技能用于将原始用户反馈文本按照情感极性分类,返回JSON格式结果,字段包括sentiment、confidence”,模型的理解准确率明显高出一大截。描述里写清楚“何时该用”“何时不该用”,实际上是对模型做了一次隐式约束。

另外我选了技能目录动态扫描的方式做注册发现。每加一个新的技能,只要把文件夹丢进指定目录,刷新后系统就会自动读取SKILL.md(技能描述文件)、校验代码文件完整性、把技能注入候选池。整个过程不需要重启服务,也不需要注册中心。这个设计网上很多人介绍过,但真正跑起来之后你会发现,目录扫描方案最重要的价值不是省事,而是让“技能”这个概念的边界在物理层面变得极其清晰——一个技能就是一个文件夹,所有相关的东西都在里面。

2. 核心细节解析与实操要点

2.1 技能描述文件的结构设计

如果你准备动手搭建,技能描述文件(我习惯命名为SKILL.md)是第一个要死磕的东西。它的质量直接决定了Agent选技能的正确率。我在实践中打磨出的结构是五段式:

--- name: user_feedback_classifier version: 1.2.0 description: > 将原始用户反馈文本分类为正向/负向/中性,并提取核心诉求关键词。 当需要分析用户情绪、评估服务满意度、从大量反馈中筛选负面案例时使用。 如果输入不是文本反馈,或需要多轮对话交互,不要使用本技能。 --- ## Input - 原始反馈文本(字符串) ## Output - JSON对象,fields: sentiment, keywords ## Examples Input: 你们的快递等了一个星期才到,太慢了 Output: {"sentiment": "negative", "keywords": ["物流慢", "配送时效"]}

看着简单,里面的门道不少。description字段的语气要写“命令式”而不是“说明式”。“请使用”“可以考虑”这类措辞会让模型在犹豫的时候倾向跳过技能调用。直接写“当...时使用”比写“本技能可以用于...”效果稳得多。

还有一点容易被忽略——负面提示一定要写。告诉模型“什么时候不要用”能过滤掉大量错误调用。我试过不写负面提示,结果在分类技能上,模型把闲聊的句子也丢进来跑一遍,白白浪费调用次数。

Examples字段是隐含的“锦囊”,你不能只给一个。我给每个技能至少配置三个不同形态的输入输出对,模型在Few-shot场景下的准确率会有肉眼可见的提升。实测数据:一个情绪分类技能,只有一个示例时准确率约78%,补到三个示例后稳定在86%以上。

2.2 技能内部分层:入口、校验、执行、回传

很多人第一次设计技能代码时,会把所有逻辑写在一个函数里,表面上“简单直接”,最后调试的时候欲哭无泪。我强烈推荐把技能内部拆成四层,哪怕技能逻辑再简单也保持这套结构:

  • 入口层(entry):统一接收外部传入的参数,做一层格式清洗,把各种可能的调用方式(JSON、命令行参数、函数调用)归一化成内部标准字典。
  • 校验层(validate):检查必填参数是否存在、类型是否正确、数值范围是否合理。校验失败要抛出明确的错误码,不要只返回“参数错误”四个字。
  • 执行层(run):真正干活的代码。这一层保持纯粹,不掺入与任务无关的逻辑。
  • 回传层(response):把执行结果整理成标准格式,附带执行时长、状态码、日志信息。这些元数据是后续编排判断是否重试的依据。

四层结构看起来多写了很多“废话”代码,但是复用性极佳。入口层和回传层往往可以抽象成公共基类。我在项目中做了一个BaseSkill类,新技能只需继承并实现validate和run两个方法,其余全部复用。一个熟练的工程师写一个新技能,从建目录到注册完成,基本能控制在半小时以内。

2.3 技能结果的统一规范

这是整个项目中最“不起眼”却是最关键的一个环节——输出规范。刚开始我允许各个技能自由定义返回结构,结果在技能B使用技能A的输出时,要做一堆兼容处理,痛苦不堪。后来我强制统一了输出结构:

{ "status": "success", "code": 0, "data": {}, "meta": { "skill_name": "user_feedback_classifier", "start_time": 1735000000, "elapsed_ms": 128, "retry_count": 0 }, "error": null }

这个规范一旦定下来,后续所有编排逻辑都轻松了。status字段决定是否走重试或降级链路,data里的内容可以做校验后再决定能否直接传给下一个技能,meta里的耗时数据还能用于后续的性能分析。

关于输入输出格式,还有一个词要提醒:尽量用JSON不要用纯文本。模型解析纯文本的容错率太低了。有一次我图省事让一个技能返回“成功”或“失败”两个字符串,结果模型在某些边缘场景下把它理解成了“success”或“fail”,后面环节的匹配逻辑直接失效。统一用JSON结构,这个问题就消失了。

3. 实操过程与核心环节实现

3.1 搭建最小技能运行环境

下面进入实战环节。我以Python为例,演示一个最小可运行的agent-skills环境。

先建目录结构:

agent_skills/ ├── core/ │ ├── __init__.py │ ├── base.py # BaseSkill基类 │ ├── registry.py # 技能注册与发现 │ └── executor.py # 技能执行器 ├── skills/ │ ├── text_summarizer/ │ │ ├── SKILL.md │ │ └── skill.py │ └── sentiment_analyzer/ │ ├── SKILL.md │ └── skill.py ├── run.py # 启动入口 └── requirements.txt

core/base.py里定义技能基类。这里贴一个精简版,实际项目中我会把日志和异常处理做得更细:

# core/base.py import json import time from abc import ABC, abstractmethod from typing import Any, Dict class BaseSkill(ABC): name: str = "" version: str = "1.0.0" def __init__(self, raw_input: Dict[str, Any]): self.raw_input = raw_input self._standard_input = {} @abstractmethod def validate(self, params: Dict[str, Any]) -> Dict[str, Any]: """校验并清洗参数,返回标准字典""" pass @abstractmethod def run(self, params: Dict[str, Any]) -> Any: """核心执行逻辑""" pass def execute(self) -> Dict[str, Any]: start = time.time() try: self._standard_input = self.validate(self.raw_input) result = self.run(self._standard_input) return { "status": "success", "code": 0, "data": result, "meta": { "skill_name": self.name, "version": self.version, "start_time": start, "elapsed_ms": int((time.time() - start) * 1000), "retry_count": 0, }, "error": None, } except Exception as e: return { "status": "failed", "code": -1, "data": None, "meta": { "skill_name": self.name, "version": self.version, "start_time": start, "elapsed_ms": int((time.time() - start) * 1000), "retry_count": 0, }, "error": {"type": type(e).__name__, "message": str(e)}, }

core/registry.py负责扫描skills/目录下的所有技能。核心逻辑是读取每个子目录下的SKILL.md,解析YAML头部的name和description,把元信息注册到内存字典中。这里分享一个坑:技能目录名、yaml里的name、代码类名,三者尽量全部一致。我一开始没注意,结果导致日志里显示的名称和实际调起的类对不上,排查问题要多花不少时间。

3.2 多技能编排:让模型做决策的完整链路

技能准备好了,还需要一个“大脑”来调度。我采用的是经典的LLM循环模式,核心流程是:

用户请求 -> 任务分解 -> 选择技能 -> 构造参数 -> 技能执行 -> 观察结果 -> 判断任务是否完成

重点在于任务分解这一步。我设计了一个AgentLoop类,它维护当前任务状态,每次迭代做以下几个动作:

  1. 把用户请求、历史对话、当前可用的技能列表(只放名称和两行描述)组合成一次Prompt。
  2. 让LLM输出一段JSON格式的“计划”,内容包含skill_name、params、reason三个字段。
  3. 代码解析这段JSON,调用对应技能。
  4. 把执行结果追加到对话历史中,再次交给LLM,让它判断是继续执行还是终止。

有个参数很关键——技能列表可见长度。当系统里注册了几十个技能后,全量塞给模型不现实,我会在注册阶段给每个技能打标签(比如text_process、data_fetch、file_op),先让模型判断当前阶段需要哪一类标签,再把对应子集展开。这个“二级筛选”能把候选技能从几十个压缩到五六个,决策精度明显提升。

实操时还要注意,Prompt里给模型看的技能描述,务必提炼成一句话。SKILL.md里的详细内容不要直接灌给模型。比如:

可选技能: - text_summarizer: 把长文本压缩为要点摘要,输入文本,输出列表。 - sentiment_analyzer: 判断文本情感极性,返回JSON。

这样模型不会陷在细节里,选技能的速度和准确率都更好。

3.3 技能复合编排:支持条件分支和循环

单一技能调用只是地基,真实任务往往是“先做A,然后根据A的结果决定做B还是C”。我在agent-skills里实现了两种简单高效的复合编排方式:顺序编排和条件编排。

顺序编排实现起来最简单:在同一个AgentLoop里连续下达多个指令,每次指令指定一个技能。有一次用户需求是“把最新的销售报告发到钉钉群”。拆分后就是两个技能:file_reader读取报告文件,im_sender发送到群聊。我在循环里预先定义好了一个执行顺序模板,让LLM按照模板依次执行,出错的概率比完全自由发挥低得多。

条件编排稍微复杂一点,我用的是“技能返回值参与决策”的方式。例如需求是“从Excel里找出销量低于目标的商品,发邮件给销售负责人”。流程是:

Skill A: excel_reader 读取商品表 -> 得到商品清单 Skill B: sales_filter 过滤低销量商品 -> 得到目标商品列表 Skill C: mail_sender 发送邮件

其中B和C之间有个隐式约束:如果清单为空,不应该发邮件。我把这个约束放在B技能的输出校验里——当data为空数组时,返回一个特殊的status: "empty",AgentLoop收到这个状态后,会直接终止后续技能调用,反馈给用户“没有需要处理的商品”。这个设计让我避免了无数次“明明没有异常却把空邮件发出去”的尴尬。

3.4 执行中间件的妙用

技能系统做厚之后,我开始加入中间件(middleware)机制,灵感来源于Web框架的设计。现在每个技能的execute流程是这样的:

raw_input -> middleware_chain(校验、日志、限流、统计) -> BaseSkill.execute() -> middleware_chain(响应格式化) -> final_output

中间件解决了一个实际问题:很多横切需求不需要每个技能各自实现。比如权限控制、调用频控、参数脱敏、耗时统计,这些逻辑放在中间件里可以统一处理。我在系统里做了一个TimeCostMiddleware,只做一件事——在请求进入和响应返回时记录时间戳,汇入全局指标。上线半个月后看数据,发现file_reader这个技能平均耗时是其他技能的5倍,顺着这条线索优化了文件读取策略,整体性能直接翻倍。

如果你也准备加中间件,请务必遵循“职责单一”原则。一个中间件只干一件事,否则调试的时候多个中间件互相影响,排查成本极高。我踩过一次坑,把日志中间件和流量限制中间件写在了一起,流量超限时日志记录也挂了,问题定位花了一个下午。

4. 常见问题与排查技巧实录

4.1 困难之一:技能明明注册了,但LLM就是不用

这是最常见的问题。排查思路从三个层面展开:

第一层,确认技能描述与模型对场景的认知方式匹配。有一次我注册了一个regex_extractor技能,描述里大谈正则规则的强大,但模型在遇到“从文本里提取日期”的需求时,它觉得用代码技能就够了,直接自己生成了正则表达式。后来我把描述改成“当需要从非结构化文本中抽取标准格式数据时使用”,模型的调用率立马上来了。

第二层,检查历史输出里模型是否曾经“意图调用但参数构造错误”。很多时候模型选了技能但参数写错了,在用户视角就是“技能没用”。这个问题的根源在于SKILL.md里的参数说明不够详细,尤其是“边界条件”。比如时间范围参数,必须写明“采用Unix时间戳”,否则模型会给你传“2024-12-01 12:00:00”这种字符串。参数格式写清楚后,类似的问题减少了大半。

第三层,考虑候选技能之间的“语义打架”。两个技能描述过于接近时,模型会随机选一个,看起来就像是“某个技能不稳定”。解决方式是给描述增加明确的差异化信息。我处理过image_resize和image_compress两个技能的冲突,最后在描述里分别加上“调整尺寸常用于头像裁剪,压缩常用于减小文件体积”,模型就分得很精准。

4.2 困难之二:多技能串联时的“鸡同鸭讲”

技能A输出的是一个Python列表,技能B期望的输入是字符串数组;技能A输出的字段名是phone_number,技能B认为应该叫mobile。这种问题在我项目中反复出现。

根源在于每个技能的开发者习惯不同。我的解决办法是建了一份数据契约文档,把所有技能之间可能传递的通用数据实体统一命名和类型。比如“用户手机号”这个字段,全局统一叫user_phone,类型为string。“商品销量”统一叫sales_count,类型为int。任何新技能在设计输入输出时,先查这份文档,有的字段必须复用,不允许另起炉灶。

如果在串联链路上仍然发现格式不对,我会在AgentLoop里加一个轻量的“字段映射”步骤——在执行下一个技能前,用一个小模型将前序输出转换成目标技能需要的输入结构。注意这一步要加在“参数构造”时做,不要侵入技能内部。

4.3 困难之三:技能执行失败后如何优雅降级

真实环境里,技能执行失败是常态。最怕的是一失败整个任务就停摆。我在系统里定了三种处理策略:

  1. 直接重试:适用于网络抖动、临时性的第三方API超时。重试三次,间隔递增。
  2. 降级替换:如果某个技能挂了,尝试调用功能相似的备用技能。比如gpt4_text_summarizer挂了,自动切换到local_model_summarizer,虽然效果差点,但流程不中断。
  3. 部分成功:当任务拆分成多个步骤,后面的步骤失败时,保留前面已经完成的结果,把失败信息作为“部分失败”返回给用户。

这三种策略的实现都离不开前面说的统一返回结构。status字段为failed时,AgentLoop会根据错误类型和重试次数自动决定走哪条路径。实测下来,加了降级策略之后,长任务的最终成功率从74%提升到了92%。

4.4 常见问题速查表

现象直接原因解决动作
技能被注册但从未被调用SKILL.md描述与用户需求匹配度低精简描述、增强触发条件、加负面提示
模型选了技能但参数一次次出错参数说明不完整、类型不明确在SKILL.md中为每个参数写类型、范围、示例值
串联流程总是中间断裂下游技能无法解析上游输出结构统一数据契约文档,必要时加字段映射层
技能执行很慢大量同步IO、缺少缓存加缓存中间件、切换异步执行
技能逻辑没问题但结果错误输入参数在校验层被静默修改校验层只做检查,不主动改值,或记录修改日志
多技能同时运行时内存溢出技能加载方式过重改为按需加载,执行完释放资源

5. 实践心得与进阶建议

5.1 从零到一落地的最小路径

如果你准备在团队内搭建类似系统,我建议按下面的顺序推进,不要一上来就追求完美。

第一步,先把技能注册和动态扫描做出来。这步工作量不大,但能让你直观感受“技能目录化”带来的便利。第二步,只写两三个技能,比如文件读取、API调用、常用文本处理。第三步,接入LLM循环,让模型可以自主调用这一小撮技能。跑通这个最小闭环后,再逐步补充中间件、编排逻辑、降级策略。

很多人一开始就设计“技能市场”“技能版本控制”“技能自动化测试”,这些以后可以做,但初期会严重拖慢进度。先把链路跑通,再谈工程化。这也是为什么我在项目早期版本里只保留了最必要的三个中间件:日志、耗时统计、异常捕获。

5.2 关于技能设计的几条原则

经过几个月的实战,我提炼出几条“踩坑换来的原则”,分享给你:

原则一:技能的粒度宁可细不要粗。我曾经把“读取文件并解析内容提取关键信息”做成了一个技能,看起来效率高,实际上稍微换一种文件格式整个技能就废了。拆成file_reader、content_parser、info_extractor三个技能后,每个技能的适用范围更清晰,复用率也高得多。

原则二:技能描述不是越详细越好。描述的核心是“让模型在恰当的时机调用它”,而不是“让模型了解它的实现细节”。如果一个技能描述超过300字,很可能是因为你把不该写的东西写进去了。我通常会把实现细节放在代码注释里,描述文件的description只保留触发条件和输入输出概要。

原则三:先跑通再优化。技能系统的调优最好是“线上真实数据驱动”。与其在开发环境反复测试,不如让Agent在保护模式下跑真实任务,把每一次技能调用的决策记录、执行结果、耗时全部落盘,然后定期复盘。我从这些日志里发现最多的三类问题是:描述歧义、参数边界不清、技能选择冲突。

5.3 后续可以怎么扩展

这个系统的边界其实很宽。目前我在探索的方向包括:给技能加上“学习”能力——当某个技能经常失败时,自动修改描述或参数示例;技能的“市场”化——不同团队贡献自己的技能包,通过独立的技能管理API互相交流;以及基于历史调用数据优化模型决策的Prompt模板。

有一点我得坦白说:写技能代码的时候,最大的工作量其实不在写功能本身,而是在“让另一个模型能准确理解并正确使用这个功能”。这一点如果处理好了,整个Agent系统的体验会顺滑很多,如果处理不好,再强的模型也会被粗糙的技能描述拖后腿。

如果你现在正准备构建自己的Agent技能库,我的建议是从一个真实的业务场景切入,选三五个最常用的操作做成技能,跑通一整个任务链路,再回头来打磨描述和编排策略。毕竟,技能系统只有被实际调用、真实反馈,才能不断变得好用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 23:06:14

VideoRAG实战:从视频到结构化笔记与知识库问答的完整指南

看视频做笔记这件事,我坚持了快十年,从早期的纯手抄,到后来用截图加文字,再到用各种笔记软件,说实话一直都觉得不够痛快。尤其是遇到课程视频、技术分享、播客长视频,动不动一个多小时,有效信息…

作者头像 李华
网站建设 2026/9/26 23:06:02

WordPress什么值得买主题最新v多少钱?避坑指南与源码实战

WordPress什么值得买主题最新v多少钱?避坑指南与源码实战 域名解析失败?服务器超时?别慌,先别急着掏钱买主题。很多新手卡在“域名服务器搞不懂”这一步,其实只要理清DNS记录和服务器环境,WordPress什么值得买主题最新v多少钱的问题就能迎刃而解。我见过太多人因为环境没配好,导致花几百块买…

作者头像 李华
网站建设 2026/9/26 23:05:52

网站开发交流必看:服务器被黑前,先搞懂这5个防护多少钱

网站开发交流必看:服务器被黑前,先搞懂这5个防护多少钱 域名解析乱套、服务器响应超时、后台登录页直接白屏,这些是不是让你抓狂?很多做项目的朋友一遇到技术问题,第一反应就是找外包问 多少钱…

作者头像 李华
网站建设 2026/9/26 23:05:45

MATLAB五模型融合实现奥运奖牌预测:从特征工程到因果推断

很多人问过我,体育赛事的奖牌预测是不是个“玄学”问题。说实话,如果只用一种模型、拍脑袋选特征,那确实跟算命差不多。但我这次把这事当成一个正经的量化研究来做:用MATLAB搭了一套完整的预测链路,把CNN神经网络、逻辑…

作者头像 李华
网站建设 2026/9/26 23:05:44

世界搜索引擎公司排名怎么选?10年老兵教你避开流量陷阱

世界搜索引擎公司排名怎么选?10年老兵教你避开流量陷阱 网站做好了没人访问,是不是让你抓狂?别急着骂推广费烧得冤,先看看你的站点在 世界搜索引擎公司排名 里排第几。很多老板问我: 怎么选 才能让流量真正进来?今天不整虚的,直接拆解主流搜索引擎的底层逻辑,帮你把官网从“隐形人”变成“吸铁石”。…

作者头像 李华
网站建设 2026/9/26 23:05:41

瑞美4.79客户端单机版LIS部署避坑指南:从数据库连接到加密狗

简介:瑞美4.79客户端是一款面向医疗机构实验室的专业LIS系统客户端,单机版设计免去注册机激活环节,适合检验科人员、实验室技术员以及医疗信息化维护者在独立环境中部署使用。软件覆盖样本采集、检测、结果分析、报告生成、数据存储等完整流程…

作者头像 李华