news 2026/10/7 17:29:36

从巨石到技能层:Agent架构的技能编排与工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从巨石到技能层:Agent架构的技能编排与工程实践

去年年初开始,我们的后端团队在慢慢把业务往 agent 架构上迁移。最早一批 agent 的代码写出来之后,很快就遇到了一个很典型的问题:每个 agent 都把自己要做的事、要调的接口、要处理的异常全揉在 prompt 和 print 语句里,一个月之后再看,一个 agent 就是一个没人敢动的巨石。后来我们参考了社区里 agent-skills 这个方向的设计思路,把技能从 agent 的主逻辑里抽出来,做成了一个独立的、可注册、可编排的技能层。这套改造做完之后,效果非常明显——新需求从两三天压缩到半天,老 agent 的 bug 率也降了一大截。

这篇内容就是围绕 agent-skills 这个主题,聊聊我们是怎么从混乱走向结构化的,包括技能层为什么值得单独做、技能描述符该怎么设计、运行时编排有哪些坑,以及我踩过的几个比较典型的故障。

1. 为什么 agent 需要独立的技能层

1.1 把技能从 prompt 里解放出来

很多入门级的 agent 会把“做什么”和“怎么做”全部写进 system prompt。比如一个客服 agent,prompt 里写着“当用户问退款时,调用 refund_api 并传入 order_id”。这种方式在小规模 demo 里跑得通,一旦技能数量超过十个,prompt 就会变得臃肿不堪,而且每加一个技能就得调整 prompt,token 消耗增加,模型的理解精度反而下降。

agent-skills 的核心思路是把技能从 prompt 里抽出来,变成可以独立注册、独立调用、独立维护的代码模块。每个技能有自己的描述、参数定义、执行逻辑和返回值规范,agent 不再靠“记住”技能,而是靠“发现”技能。

这个转变的实质是:把模型从“记忆者”变成“决策者”。模型只需要根据用户意图去匹配技能,而技能的具体实现细节由代码完成。这样 prompt 会大幅缩短,模型的稳定性会提高,技能本身也可以像普通代码一样做单元测试。

1.2 为什么技能层要独立成模块

在我们的实践中发现,技能层如果不独立,通常会面临三个问题:

  • 职责混乱:技能逻辑和 agent 的对话逻辑耦合在一起,改技能可能影响对话行为,改对话行为也可能误伤技能。
  • 复用困难:两个 agent 可能都需要查询订单状态的技能,但代码写在各自的逻辑里,没法直接共享。
  • 测试成本高:技能的输入输出没有统一规范,测试只能端到端跑,无法对单个技能单独验证。

技能层独立之后,这些问题基本迎刃而解。技能模块不关心对话上下文,只关心输入参数和输出结果,你可以像测试普通函数一样测试它。同时,多个 agent 可以共享同一个技能注册表,按需加载,避免了重复开发。

1.3 技能与工具、插件的边界

这里需要澄清一个很容易混淆的概念:skill、tool 和 plugin 到底有什么区别。

在我们拆解 agent-skills 的过程中,给这三者划了一条比较清晰的边界:

  • Tool(工具):最基础的原子操作,比如“发送HTTP请求”“读写文件”“执行SQL查询”,它们不包含业务语义。
  • Skill(技能):在工具基础上封装了一层业务语义,比如“查询订单状态”“生成退款单”“计算运费”,它们通常需要组合多个工具,并且包含一定的业务规则。
  • Plugin(插件):是技能的集合,通常对应一个完整的功能域,比如“订单管理插件”包含了查询、退款、修改地址等多个技能。

所以 agent-skills 关注的是中间这一层:如何描述一个技能,如何让 agent 理解并调用它,如何编排多个技能完成复杂任务。

2. 技能描述符的设计与调度机制

2.1 技能描述符:agent 理解技能的桥梁

agent 要正确调用技能,必须理解技能是干什么的、需要什么参数、返回什么结果。我们把这份描述性信息称之为技能描述符(Skill Descriptor)。

一个合格的技能描述符至少应包含以下几项:

  • skill_id:技能唯一标识,agent 通过它引用技能。
  • name:人类可读的名称,便于日志排查。
  • description:一段简洁的描述,说明该技能适用的场景,这部分会被注入 prompt 供模型匹配。
  • parameters:JSON Schema 格式的参数定义,包括字段名、类型、是否必填、描述。
  • returns:返回值规范,说明成功和失败时各自返回什么结构。

我们的实践经验是:description 写得好不好,直接影响模型选技能的准确率。描述要突出“什么场景用这个技能”“和别的技能的区别在哪里”,而不是泛泛地说“这是查询订单状态的技能”。同时,描述里应写清楚典型的使用条件,比如“仅当用户提供订单号时使用”,这样模型在参数缺失时就会先追问用户,而不是直接报错。

字段说明示例
skill_id唯一标识order.query_status
name可读名称查询订单状态
description匹配用描述根据订单号查询订单当前物流与支付状态,仅当用户已提供订单号时调用
parametersJSON Schema{ "order_id": { "type": "string", "required": true } }
returns返回值规范{ "status": "shipped" }

2.2 全局技能注册表与动态加载

有了技能描述符之后,下一步就是让这些技能可被发现、可被加载。我们用了“注册表 + 动态加载”的模型。

注册表是一个全局的字典结构,key 是 skill_id,value 是技能对象。启动时,我们扫描所有技能目录,将技能注册进去;运行时,agent 根据模型匹配结果从注册表取出技能并执行。

动态加载需要考虑版本问题。我们的做法是每次发布新技能时不覆盖旧版本,而是以版本号区分,注册表中同时保留多个版本,agent 默认调用最新稳定版,如果需要回滚可以通过配置指定版本。

# registry.py class SkillRegistry: def __init__(self): self._skills = {} def register(self, skill): if skill.skill_id in self._skills: raise ValueError(f"skill {skill.skill_id} already registered") self._skills[skill.skill_id] = skill def get(self, skill_id): if skill_id not in self._skills: raise KeyError(f"skill {skill_id} not found") return self._skills[skill_id] def list_skills(self): return [ { "skill_id": s.skill_id, "name": s.name, "description": s.description, "parameters": s.parameters, } for s in self._skills.values() ]

2.3 技能调度:匹配、鉴权与执行

调度是 agent 调用技能的核心链路,我们将其拆为三步:

第一步是匹配。模型基于用户输入和技能描述符的 description 做语义匹配,输出要调用的 skill_id。这里需要约束模型只能从注册表已有的 skill_id 中选,避免模型编造不存在的技能。实践中我们用函数调用(function calling)来实现这一约束,模型返回的调用参数会经过 schema 校验。

第二步是鉴权。不是所有技能所有用户都有权限调用。我们的做法是为技能配置权限标签,比如“仅管理员”“仅内部系统”,调度层根据当前会话的权限上下文决定是否放行。

第三步是执行。执行时把模型解析出的参数传给技能函数。行内有一个超时控制和重试机制,超时时间默认设为 10 秒,重试次数默认 2 次,可调的参数都写在配置中心里。

3. 实战:从零实现一套 agent-skills 体系

3.1 技能基类的抽象设计

为了让技能代码保持统一风格,我们定义了一个基础抽象类 BaseSkill。所有技能继承这个类,并实现必需的接口,这样调度层就可以用统一的方式调用所有技能。

# base_skill.py from abc import ABC, abstractmethod from typing import Any, Dict class BaseSkill(ABC): skill_id: str name: str description: str parameters: Dict[str, Any] permissions: list[str] = [] @abstractmethod def execute(self, params: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]: """执行技能逻辑,返回结构化结果""" pass

execute 接收两个参数:params 是模型解析出的参数,context 是运行上下文,包括用户身份、会话 ID 等。返回值统一用字典结构,包含 status、data、message 三个字段,方便调度层统一处理成功与失败。

3.2 按目录组织技能,实现自动注册

我们采用目录即模块的组织方式:每个技能一个目录,目录名就是 skill_id,目录内包含 main.py(实现逻辑)、schema.json(参数定义)、description.txt(描述文本)。启动时框架自动扫描所有技能目录并注册。

skills/ ├── order_query_status/ │ ├── main.py │ ├── schema.json │ └── description.txt ├── order_refund/ │ ├── main.py │ ├── schema.json │ └── description.txt └── logistics_trace/ ├── main.py ├── schema.json └── description.txt

自动注册的扫描逻辑很简单:遍历 skills 目录,找到每个包含 main.py 的子目录,用 importlib 动态导入并实例化,然后塞进注册表。这里有个值得注意的坑:动态导入的模块名不能重复,我们建议以技能目录名作为模块名的一部分,比如skills.order_query_status.main。

3.3 技能描述符注入 prompt 的格式设计

技能描述符如何注入 prompt,直接影响模型匹配的准确率。我们尝试过两种方式:一是全部注入 system prompt,二是只注入技能列表,需要时再展开详情。实践下来,第二种方式效果更好。

我们采用的做法是:把技能列表压缩成一行摘要注入系统消息,摘要格式为“skill_id: 简短描述”,模型通过摘要缩小候选范围,再通过 function calling 的 schema 完成参数绑定。

可用技能列表: - order.query_status: 查询订单状态,需要订单号 - order.refund: 发起订单退款,需要订单号和退款原因 - logistics.trace: 查询物流轨迹,需要运单号

这种方式既削减了 token 消耗,又保证了模型能掌握技能的全貌。模型只负责输出 skill_id 和参数,不负责拼接逻辑,大大降低了出错的概率。

3.4 技能的编排与组合

当单个技能无法满足用户需求时,我们需要把多个技能编排起来。比如用户问“我的订单到哪了,顺便退款”,这需要先查询订单号对应的运单号,再查询物流轨迹,最后发起退款。

我们实现了一个简单的编排引擎,支持顺序执行和有条件执行。顺序执行就是按 skill_id 列表依次调用的流水线,前一个技能的输出可以作为后一个技能的输入映射。有条件执行则是根据某个技能返回的字段决定是否执行下一个技能。

# pipeline.py class SkillPipeline: def __init__(self, steps): self.steps = steps async def run(self, initial_params, context): current_params = initial_params for step in self.steps: skill = registry.get(step.skill_id) result = await skill.execute(current_params, context) if step.condition and not step.condition(result): return result current_params = step.output_mapper(result, current_params) return current_params

这个引擎看似简单,但把技能之间的依赖关系显式化了。每条流水线定义在配置文件里,新业务只需要新增配置和技能代码,不需要改主逻辑。

3.5 技能执行的超时、重试与降级

技能执行过程中,超时和失败是最常见的问题。我们为每个技能配置了超时时间、重试次数和降级策略,这些配置项统一放在配置中心,支持热更新。

超时时间的设置需要根据技能类型区分:内部函数调用通常 3 秒足够,外部 HTTP 调用建议放宽到 10 秒。我们最初统一用 5 秒,结果外部接口稍慢就触发超时,之后改为分技能配置,问题就消失了。

重试要特别注意幂等性。查询类技能可以安全重试,但退款、下单等写操作,如果重试前没有做幂等检查,很容易重复提交。我们要求所有写操作类技能在参数中带上 idempotency_key,服务端根据这个 key 去重。

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

4.1 模型乱编 skill_id

上线初期,模型偶尔会输出一个注册表里不存在的 skill_id,对话直接崩溃。排查后发现原因在于 prompt 里技能列表格式太松散,没有强调必须从列表中选择。

修复方案有两个:一是结构化约束,用 function calling 的方式让模型只能从给定的函数列表中选择;二是增加校验,调度层拿模型返回的 skill_id 去注册表检查,找不到就返回一条清晰提示,让模型重新选择。

我们最终两层都做了,效果很好。现在即便模型输出了非法 ID,调度层也会优雅地提示“技能不存在,请从以下列表中选择”,而不是直接抛异常。

4.2 参数校验不一致

另一个高频问题:技能内部的参数校验和描述符里的 JSON Schema 校验不一致,导致描述符说订单号必填,代码里却允许空值;或者反过来,模型按 Schema 传了参数,代码却因为类型不匹配报错。

我们的解决办法是强制代码执行前统一走 Schema 校验。所有技能在 execute 开头先校验 params 是否符合 schema.json 定义,不符合就返回参数错误。这样代码里就不用再重复写参数检查逻辑,也保证了两处的校验规则永远一致。

def execute(self, params, context): errors = validate_schema(params, self.parameters) if errors: return {"status": "error", "message": f"参数校验失败: {errors}", "data": None} # 业务逻辑

4.3 技能会话状态丢失

第三个值得一提的问题是状态管理的陷阱。我们的订单技能需要登录态,最初把登录态存在技能内部,结果换一个会话就丢了。排查之后发现,技能应该是无状态的,所有状态必须放在 context 里由调度层维护。

我们把技能接口约束为无状态后,技能的复用性大幅提升。状态信息(用户身份、会话、租户 ID)统一从 context 传入,技能内部不维护任何会话数据。

4.4 技能冲突与版本回滚

多个技能同时依赖同一个底层服务时,很容易出现版本冲突。比如查询订单技能和查询物流技能都依赖订单服务 SDK,升级 SDK 后物流技能暂时不兼容。

目前我们的做法是技能与 SDK 版本一起打包,每个技能独立声明依赖。上线新技能前跑一套自动测试,如果某个技能的依赖与全局配置冲突,就暂时保留旧版本,等依赖更新后再切换。


我在实际把 agent-skills 这套体系落地到生产环境之后最深的体会是:技能层解决的不是“能不能调用”的问题,而是“能不能规模化管理”的问题。单个 agent 手写技能调用没问题,但当你有二十个 agent、上百个技能的时候,没有统一的注册、调度、校验和版本管理,系统迟早会在某个半夜被一个参数错误打崩。把技能当成一等公民来设计,短时间看好像多写了一些基础设施代码,但后面每次新增功能、每次排查线上问题,你都会发现这套投入是值得的。

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

CSS层叠机制不只有优先级:从特异性到@layer的完整规则

CSS 的层叠,光听名字像是个概念游戏,可实践里它是真真切切决定生死的。我这几年面试前端,几乎必问一个问题:样式被覆盖时,你第一步打开 DevTools 看什么?能脱口而出"看被划掉那条规则的来源、重要性、…

作者头像 李华
网站建设 2026/10/7 17:28:28

JavaWeb美食网站项目实战:从部署调试到课设答辩

简介:基于JavaWeb的美食网站设计与实现项目源码,面向JavaWeb初学者及课程设计人群。项目围绕美食信息浏览、食谱检索、心得分享等场景,采用Servlet、JSP、JDBC实现用户注册登录、菜谱分类、搜索、评论等模块。压缩包约50.34MB,源码…

作者头像 李华
网站建设 2026/10/7 17:28:18

claude-mem:为Claude对话打造持久化记忆管理方案

Claude 用多了之后,最大的痛点其实是"失忆"。这话不是我随便说的——你开一个新终端,Claude 就完全不记得上一个会话里聊到哪了,哪怕你在同一个项目目录下反复调试同一个 bug,每次都得从头交代背景。我自己因为这事浪费…

作者头像 李华
网站建设 2026/10/7 17:26:45

impeccable:基于npx的零安装Playwright离线安装工具

1. 项目概述:一个被误读的 CLI 工具名,以及它背后真实的工程逻辑“impeccable”这个词最近在开发者社区里频繁出现,但几乎没人能说清楚它到底是什么——它既不是 npm 上下载量破百万的明星包,也不是某家大厂开源的框架核心库。我第…

作者头像 李华
网站建设 2026/10/7 17:26:09

eFuse+STM32G474:嵌入式电源路径保护设计实践指南

搞嵌入式硬件,最怕看到的一种画面就是:负载侧短路,PCB走线烧断发黑,保险丝却没动静。我之前做一块12V输入的工业控制板,就吃过这种亏——不是保险丝质量差,而是普通保险丝的熔断特性和短路热累积根本不匹配…

作者头像 李华
网站建设 2026/10/7 17:24:35

HERA:面向智能体主动拒止能力的执行框架‑环境协同演化框架

HERA:面向智能体主动拒止能力的执行框架‑环境协同演化框架 原文网页:https://arxiv.org/html/2610.06563v1 PDF链接:https://arxiv.org/pdf/2610.06563v1 arXiv编号:arXiv:2610.06563v1 [cs.AI] 摘要 大语言模型工具智能体已经可…

作者头像 李华