ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

从工具堆砌到技能体系:智能体技能全生命周期管理实战

从工具堆砌到技能体系:智能体技能全生命周期管理实战 去年我做智能体项目第一版把所有工具函数硬编码在系统提示词里结果迭代到第三个月就彻底维护不动了新增一个工具要动一大片逻辑模型经常在相近功能里选错线上某次多个工具叠加调用时把脏数据写进了订单库查了半天都定位不到是哪个环节出的问题。后来我下定决心把整个架构从“工具堆砌”重构为“技能体系”也就是现在 agent-skills 这个项目想展示的东西——一套让智能体做得住、管得动、跑得稳的技能全生命周期管理方案定义、注册、加载、编排、隔离每一个环节都能独立去验证和迭代。这篇文章会把我这段时间踩过的坑和沉淀下来的做法完整写出来。如果你正在做 agent 相关开发尤其是那种“单技能 demo 跑得很顺多技能一起上就失控”的项目这篇内容可以直接拿来当参考。1. agent-skills 要回答的问题为什么通用大模型还不够很多团队做 agent 的第一步是“让大模型调用工具”也就是给模型一堆函数让它自己选。这个方案在工具数量只有两三个的时候很好用但一旦规模上来问题就开始集中出现。首先是 token 限制。把十几个工具的名称、描述、参数结构全部塞进上下文单次请求的 token 消耗会明显上升模型在超长上下文里对工具描述的注意力也会衰减。更严重的是语义歧义两个功能相近的工具如果描述写得不够明确模型很容易选错。第三个问题是最难处理的工具之间无法组合。订单查询工具返回的数据要经过一个判断逻辑才能交给退款工具去执行这个“流程”没办法通过简单塞工具描述表达给模型最终只能靠上层代码硬编码agent 也就退化成了普通接口调用。agent-skills 的核心思路是把每一个可复用的能力封装成“技能”而不是“工具”。技能不仅包含可执行代码还包含一套结构化的能力说明、触发条件、前置依赖和输出约束。技能之间是独立的可以被注册、被发现、被编排也能被统一治理。这样做的目标有三个。1.1 技能、工具、插件的差别到底在哪很多人问我技能和工具到底有什么区别。我自己的定义是这样工具是一个纯粹的“动作”比如“调用订单查询接口”“发送短信”而技能是一组带约束的“能力过程”它内部可以有多个步骤有前置条件有默认参数有错误处理。举个例子“查询订单”是一个工具“处理用户售后请求”是一个技能因为后者包含了查询订单、判断售后条件、生成退款单、通知用户等多个步骤。插件则是技能在宿主环境中的集成形态插件可以向外暴露一个或多个技能同时还要描述自己的依赖、权限、生命周期钩子。一句话概括工具是技能的组成部分插件是技能的载体技能是 agent 理解和调用的最小能力单元。1.2 agent-skills 的设计目标我自己做 agent-skills 的时候给自己定了四条必须满足的设计约束缺一条后面都会很痛苦。可发现性agent 在任意时刻都能知道“自己现在有哪些技能可以用”并且能按需加载而不是全部塞进上下文。可组合性技能之间可以通过简单的编排机制协作上一个技能的输出可以成为下一个技能的输入不需要为每个组合场景写硬编码。可治理性每个技能的执行都留有痕迹权限、成本、调用次数都要能被统一追踪和管控。可测试性技能任务清单、Skill 文件、执行代码相互分离可以针对单个技能做单元测试不依赖完整 agent 运行环境。2. 技能描述格式与能力边界先把“会什么”说清楚技能注册的第一步就是把一个技能的“能力边界”用结构化的方式描述清楚。描述的质量直接决定了模型能不能正确选到它也决定了后续动态发现和编排能不能做起来。2.1 技能清单的标准目录结构我在项目里用的目录结构非常简单每个技能一个文件夹内部固定放三个部分skills/ ├── order_query/ │ ├── SKILL.md │ ├── manifest.json │ └── main.py ├── refund_process/ │ ├── SKILL.md │ ├── manifest.json │ └── main.py └── logistics_track/ ├── SKILL.md ├── manifest.json └── main.pySKILL.md 是给模型看的能力说明。manifest.json 是给注册系统看的配置包含技能名、版本、依赖、权限级别、超时时间。main.py 是实际的执行逻辑。这三者分开而不是写在一起是我重构后最明智的决定之一。之前我把描述和执行逻辑放在同一个文件里结果改代码的时候没同步改描述模型看到的和实际执行的对不上选技能全靠猜。2.2 SKILL.md 到底要写什么一份能打的能力描述至少包含五块内容技能目的、适用场景、不适用场景、输入参数、输出格式。其中“不适用场景”是我特别要强调的因为没有它模型会在模棱两可的时候选错。这是我在业务里实际使用的一个 SKILL.md 片段供参考--- skill: order_query version: 1.2.0 description: 查用户的订单状态和基础信息。 when_to_use: 用户询问订单状态、物流状态、发货时间时使用。 when_not_to_use: 用户要求退款、退货或修改订单时改走 refund_process 技能。 input: order_id: type: string required: true description: 平台生成的订单号形如 SO2024... order_type: type: string required: false enum: [online, offline] default: online output: format: json fields: [order_id, status, items, create_time, update_time] --- # 技能说明 查询订单主状态和商品明细。只读操作不修改任何数据。注意 description 字段我写的是“查用户的订单状态和基础信息”而不是“对订单进行复杂聚合分析”。模型的选技能逻辑本质上是一个语义匹配任务描述越接近用户真实口语命中率越高。2.3 为什么不能只靠模型自己的理解能力有段时间我试图偷懒技能描述写得非常抽象总指望模型“聪明地”泛化。结果在实测里用户说“我的东西怎么还没到”模型去调了订单查询而不是物流跟踪用户说“我不想要了想退掉”模型去调了订单查询而不是退款处理。后来我总结出规律模型在选择技能时本质上是在做相似度匹配它会优先选择描述字面上与用户话术最接近的哪个技能。所以我在每个技能的 when_to_use 里开始加入用户可能的原始说法比如“我的东西怎么还没到”“货到哪了”“啥时候发货”这类口语化表达。这是一种非常有效的“提示工程”手段对技能系统同样适用。2.4 参数 Schema 不能“睁一只眼闭一只眼”很多人写参数模式时喜欢把字段标成 optional想着“反正模型能理解”。但这是我在 agent-skills 里纠正的第一个坏习惯技能入口的参数校验一定要严格有 enum 的必须写 enum有默认值的必须写 default。原因很简单技能参数校验是防止错误进入执行环节的最后一道闸门如果在这里放松错误会传导到下游整个编排链路。我开发的时候用 pydantic 做了入参校验并在 main.py 入口处强制校验from pydantic import BaseModel, Field class OrderQueryInput(BaseModel): order_id: str Field(..., min_length4, max_length32, patternr^SO\d$) order_type: str Field(online, enum[online, offline]) def run(raw_input: dict) - dict: validated OrderQueryInput(**raw_input) ...校验失败直接抛出 SkillsParameterError而不是把坏数据往业务流程里传。这一步后来在线上帮我挡住了非常多次参数拼接错误。3. 技能加载机制agent 怎么知道“自己现在能用什么”技能库一旦上了规模就不能采用“启动时全量加载”的方式。技能注册中心存在的意义是让 agent 在任意时刻都能按需获取候选技能而不是把全量清单都塞进模型上下文。3.1 技能注册中心与索引每次系统启动时agent-skills 会扫描 skills 目录读取每一个 manifest.json把技能的名称、别名、能力描述、参数模式、权限级别全部构建成一份技能索引放在注册中心的内存里。索引的最小存储单元是技能描述不是代码。注册中心对外暴露三个接口全量列表给运营后台和开发调试用。关键词检索从用户话术里提取关键词返回候选技能。语义匹配本地 embedding 计算相似度返回 top-k 候选技能。技能数量在几十个量级的时候关键词检索加规则兜底就足够了。只有技能数量超过一百个或者用户话术特别开放时才需要引入 embedding 召回。3.2 一次技能调用的完整触发流程技能调用的完整链路我把它串成一条线用户输入进来先做意图粗分类排除掉那些根本不需要技能的闲聊。系统从注册中心检索候选技能默认取 top-5避免把几十个技能描述全部丢给模型。把候选技能的描述拼进一个“技能选择”提示模板让模型从中选一个并按照参数 schema 提取入参。入参通过校验后进入技能执行器。执行器运行技能代码返回结构化结果同时写入执行日志。第五步往往被很多人忽略。但实际上在技能化改造前我根本不知道某个结果到底是哪个环节产生的技能执行日志是排查问题的地基。3.3 技能热加载与缓存策略开发时经常会面临一个场景技能代码改了一行还要重启整个 agent 才能生效这很影响迭代速度。后来我在注册中心里加了技能目录文件监听只要 SKILL.md 和 main.py 有改动就自动重新加载对应技能的热缓存不需要重启。这里的缓存粒度要分两层技能描述缓存SKILL.md 内容命中之后不再重新拼接。技能代码缓存main.py 的模块对象通过 importlib 重新加载。缓存失效用文件 mtime 去判断效果很直接几乎没有额外成本。4. 编排与上下文传递多个技能如何协作单个技能能解决简单问题agent 真正的价值在于把多个技能串成一条流程。但技能协作不是简单地在代码里一个接一个调用这里有两个核心问题需要解决上下文怎么传路由怎么定。4.1 编排引擎的基本模型我在 agent-skills 里采用的编排模型是子目标拆分 技能路由 结果合成。agent 先根据用户目标拆出若干子目标然后为每个子目标选择一个技能最后把多个技能的输出合并成最终结果。整个过程由一个编排器控制编排器本身不是一个技能而是一个调度层。class OrchestrationContext: def __init__(self, user_id: str): self.user_id user_id self.store {} def set(self, key: str, value: Any) - None: self.store[key] value def get(self, key: str, defaultNone) - Any: return self.store.get(key, default) def snapshot(self) - dict: return self.store.copy()每个技能执行时接收这个上下文读取自己需要的字段写入自己的产出。关键原则有一个技能只能写自己负责的命名空间不能随意覆盖别的技能写入的数据。我在实现时给上下文里每个字段加了一层 prefix比如 order_query 模块产出的数据都放在 order_query. 前缀下。4.2 路由策略什么时候该让模型选什么时候该写死路由是整个编排里最需要拿捏的地方。全让模型选不可控全写死又失去了 agent 的灵活性。我最后采用的混合策略是把路由分为规则无条件和语义路由两层。规则无条件路由适合那种逻辑非常明确的场景。比如用户申请退款且订单状态已发货那么必须先调物流跟踪确认是否签收再决定是否进入售后。这个流程是业务硬规则不能用模型去“悟”。语义路由适合开放场景。比如用户那句“我的东西怎么还没到”模型需要从订单查询、物流跟踪、异常登记三个候选中选一个这时候语义匹配比硬规则靠谱。我实现的伪代码如下async def dispatch(intent: str, context: OrchestrationContext) - SkillResult: if rule_router.has_rule(intent): return await rule_router.execute(intent, context) candidates await registry.search(intent, top_k5) chosen await llm_select_skill(candidates, context.user_intent) return await execute_skill(chosen, context)4.3 失败补偿与重试先想清楚“能不能重试”多个技能串行执行最怕中间某一个环节挂了。初版我图省事做了“失败重试三次”的逻辑。上线第二天就出问题了一个退款技能在第一次执行时已经创建了退款单但因为外部接口响应超时上层判定失败触发重试结果重复创建了三张退款单。从此我给自己立了一个规矩重试之前必须先判断操作是不是幂等的。查询类技能可以放心重试写操作类技能必须严格做幂等校验或者带上全局唯一的幂等键。退款、发消息、改状态这类操作幂等键就是业务订单号加一个随机前缀。def execute_with_retry(skill, context, max_retries3): for attempt in range(max_retries): try: return skill.run(context) except TransientError as e: if attempt max_retries - 1: time.sleep(2 ** attempt) continue raise重试的退避策略用指数退避基础间隔 2 秒三次重试就是 2 秒、4 秒、8 秒。这个策略在内部服务抖动时表现很稳也不会因为高频重试把下游接口打挂。5. 技能隔离与安全边界不能因为一个技能拖垮整个 agent技能系统与普通业务代码最大的不同在于技能可能来自不同团队甚至来自第三方。你没法保证别人写的代码不会死循环、不会占用过多内存、不会被外部接口卡住。5.1 沙箱与隔离的三种粒度我在 agent-skills 里把隔离方案按强度和成本分成三档可以根据技能来源选择方案隔离强度性能开销适用场景子进程执行器低低内部技能代码可控Docker 容器中中第三方技能动态加载独立微服务高高企业级核心技能强治理需求内部技能我优先选择子进程执行器把技能代码放到一个独立进程里跑主进程负责超时控制和结果回收。这样即使技能进程崩了agent 主进程也不会被拖垮。价格便宜、效果直接。import asyncio async def run_skill_in_subprocess(skill_run: str, payload: dict, timeout: int 30): proc await asyncio.create_subprocess_exec( python, skill_run, stdinasyncio.subprocess.PIPE, stdoutasyncio.subprocess.PIPE, stderrasyncio.subprocess.PIPE, ) try: stdout, stderr await asyncio.wait_for(proc.communicate(payload), timeouttimeout) except asyncio.TimeoutError: proc.kill() raise SkillsTimeoutError(skill_run) return stdout5.2 超时和资源限制是必选项不是可选项技能执行最常见的故障是外部 API 不返回。如果不对技能执行做统一超时控制一个技能卡住会影响整个 agent 的响应。在 agent-skills 里我把超时分成两级连接超时和总执行超时。连接超时控制每次外部调用的建连时间总执行超时控制整个技能的运行时间。资源限制方面子进程方案比较难做细粒度控制我通常依赖操作系统的容器技术。这里有一个很实用的经验在 Docker 方案里给每个技能容器设置 CPU 和内存上限会让异常技能的影响面大大缩小。下面是实际用的 docker-compose 片段services: skill-refund: image: skill-runtime:1.2.0 cpu_count: 1 mem_limit: 512m pids_limit: 128 read_only: true tmpfs: - /tmpread_only 表示文件系统只读等于从根上禁止技能代码随便写文件。5.3 敏感操作必须“过一道人”做完隔离还要考虑操作权限的边界。查询类技能风险低但退款、发送、修改状态这类操作一旦被模型误触发后果很严重。我设计了一个审批占位机制当技能声明自己的 permission 级别为 admin 或 involve_money 时执行器不会直接执行而是先返回一个待确认状态。agent 会先告知用户将执行什么操作、影响是什么得到用户明确确认后才真正调用执行函数。这一步在技术上不复杂但对实际业务的合规性帮助非常大。这么做会让 agent 的交互多一个来回但换来的是安全边界非常值。6. 实测下来的几个坑和建议最后把我在 agent-skills 开发和真实运行中踩到的最有价值的几个坑整理出来每个坑背后都对应一条增量改动可以直接参考。6.1 坑技能描述写得像“官方文档”一样抽象第一版技能描述我写的是“查询订单综合信息”看起来没什么问题但模型经常在多个查询类技能之间乱跳。后来我把描述改成“用户问订单啥时候发、现在到哪了、买了几件东西就用这个”模型命中率立刻上来了。技能描述不是给后端同学看的字段注释而是给模型看的语义信号。6.2 坑参数校验太宽松脏数据流入下游开发时为了方便模型调用我把技能参数全部设成 optional觉得模型总会自己填。结果技能 A 拿到的 order_id 是 None技能 B 拿到的 status 是非法枚举值编排链路开始大量传递垃圾数据。后来我强制所有技能入口做 pydantic 校验和枚举约束非法入参直接拒绝错误定位效率提升非常明显。6.3 坑重试造成了重复扣款前面已经提过重复创建退款单的问题。这里再补一个细节幂等键光有还不行幂等键要在技能代码里“先查旧单再建新单”不能直接把幂等键传给下游接口就算完事。很多外部接口对幂等键的支持并不一致甚至有些三方支付接口的重复提交处理是异步的所以业务侧先做一层状态查询兜底永远是安全的。6.4 坑日志太散出了问题不知道往哪查技能化之前代码散在各自调用方里出了错要看三四个服务日志。技能化改造后我统一在编排上下文里注入了一个 trace_id每个技能执行开始和结束时都会记录一行结构化日志包含 trace_id、技能名、入参摘要、出参摘要、耗时、错误信息。{ trace_id: trace-8f3a2c, skill: order_query, action: start, input: {order_id: SO2024...}, ts: 2025-01-12T10:22:31.123Z }有了这些日志任何一次异常技能调用都能在日志系统里按 trace_id 串起整条链路。排查问题的时效从小时级降到了分钟级这是我认为整个重构里最划算的一笔投入。6.5 后续还可以怎么扩展agent-skills 目前在这个项目里已经稳定跑过了几个迭代技能数量从最早的三五个增长到现在的四十多个agent 的选技能准确率稳定在较高水位排障链路也基本打通。我个人体会最深的一点是做 agent 和做传统后端有个很大的区别——传统后端重视模块接口的清晰度就够了而 agent 系统里的“接口”不只是给代码看的更是给模型看的。技能描述、参数约束、路由规则本质上全部是面向模型提示层的一部分需要像对待产品文案一样对待它们。基于这个经验如果你也在推进自己的 agent 项目我建议从最简单的部分开始先列一份现有能力的技能清单用 SKILL.md 的格式把每个能力的边界写清楚然后交给 agent 去选。你会发现许多看似复杂的编排问题会在把描述理清楚之后自动消失。
返回列表