☰
AI Agent技能管理器:从碎片化到统一底座,支撑企业级Agent落地
2026/9/29 17:08:19 网站建设 项目流程

做过 AI Agent 项目的人应该都体会过这种感觉:模型选型不难,难的是把 Agent 真正接到业务里。而业务接入里排第一的痛点,就是技能/工具的维护。这个技能管理器,算是我从好几个 Agent 项目里“踩”出来的一个基础设施件——先说是什么:它把散落在代码、配置文件、甚至 Prompt 里的工具定义统一收进一个可视化平台,你在界面上注册技能、配参数、做调试、管理版本和权限,Agent 运行时按需拉取可用技能清单,两边通过一份注册协议对接。文章后面会完整讲数据模型、核心实现链路和多环境发布,适合正在从 0 到 1 搭建 AI Agent、或者想往企业级 Agent 中台方向走的团队参考。

我不会给你画一张宏伟的蓝图,只讲我已经落地的东西:为什么做、表怎么建、界面怎么组织、运行时怎么桥接、以及哪些地方踩坑踩得最痛。

1. 先从混乱现场说起:为什么我非要做一个统一管理器

1.1 三个项目、三套工具定义,谁维护谁崩溃

我手上有两个偏业务的 Agent 项目和一个人工智能平台内部的辅助 Agent。刚开始大家都没当回事,工具定义就是“往代码里加一个函数,再写一段描述”。等做到第三个月,问题开始集中爆发。

最典型的一个场景:同一个“查询订单”的能力,在 A 项目里是一个 Java 方法,通过 Spring AI 的@Tool注解暴露;在 B 项目里是一个 Python 函数,直接塞进了 OpenAI 风格的tools数组;在 C 项目里干脆是一段 Prompt 里描述的“伪工具”,让模型输出 JSON 走人工流程。三个项目三套定义,参数名不完全一样,描述语气也不一样。模型在 B 项目里经常把orderId当成order_id去传,就是因为在 A 项目的工具描述里写的是前者。

更要命的是没有人知道哪些技能在线上是开着的。有同事把函数删了,但函数的“僵尸定义”还留在配置里,模型一直尝试调用,报错信息又只回传到日志深处,普通排查根本看不到。我们还因为一个旧技能没下线,让 Agent 在某个时段反复调用一个已经指向测试环境的 HTTP 接口,产生了一堆脏数据。

这些都是典型的“技能碎片化”问题。技能定义分散、运行状态不可见、调用结果不可观测,统一管理就成了刚需,而不是什么加分项。

1.2 技能管理器到底管哪几件事

我做的这个可视化技能管理器,核心职责可以归纳成五条:

  • 技能注册与配置:技能的名称、描述、参数 Schema、执行目标(哪个接口/哪个函数/哪个 MCP 服务)、鉴权信息都集中维护,不散落在代码里。
  • 生命周期管理:每个技能有 draft、published、disabled、deprecated、retired 这些状态,对应“编辑中、可被 Agent 调用、紧急停用、已废弃、已移除”。
  • 可视化调试:直接在网页上构造参数、调用技能、查看返回结果,不用每次写测试代码。
  • 版本与发布:技能升级不是改代码上线,而是发布一个新版本,支持灰度、回滚。
  • 运行态观测:记录每一次技能调用,包括触发它的 Agent、会话、耗时、成功状态、错误信息,方便定位“模型为什么这么调”。

一句话总结:把技能从“程序员脑子里的隐式知识”变成“系统里显式管理的资产”。

1.3 什么样的团队真正需要它

说实话,如果你只是写一个技术 Demo,或者 Agent 总共就三个工具,那真没必要上这套东西,直接在代码里维护反而更高效。

但如果你遇到下面任一情况,可以考虑上统一管理:

  • 有多个 Agent,共享一批业务技能,但各自项目里的实现已经分叉;
  • 技能数量超过十五到二十个,靠人肉记忆已经记不清参数和状态;
  • 需要给非开发人员(比如运营、售前)配技能的查看或测试能力;
  • 技能涉及敏感接口,需要权限审批、调用审计;
  • 团队在往“Agent 中台”方向演进,希望能力可以复用、沉淀。

我属于最后一种情况,所以这个管理器的定位从一开始就不是“给单个项目用的工具”,而是“给多项目、多 Agent 共用的技能底座”。后面讲的很多设计,都是围绕这个定位展开的。

2. 数据模型设计:技能、工具、能力三层关系不能省

2.1 一张表硬塞所有技能,后期一定改哭

最早我图省事,设计了一张大宽表:字段有技能名、描述、参数、接口地址、超时时间、负责人、状态……所有东西塞一起。用了一周就发现问题。

问题是“技能”和“工具”被混在了一起。比如“查天气”是一项能力,但我有两个查天气的工具:一个是连接第三方天气 API,一个是查内部积累的历史天气库。模型应该优先用内部库,因为更稳定。可如果一张表只存一条技能记录,就没法表达“一个技能背后有多个候选执行通道,按优先级/策略路由”这件事。

后来我参考了企业服务里常见的建模思路,把概念拆成了三层:

  • 能力(Capability):业务上抽象的能力,描述的是“能做什么”,比如“查天气”“查订单”“发工单”。
  • 技能(Skill):能力对外暴露的配置化单元,包含给模型看的描述和参数 Schema,一个技能绑定一个或多个工具。
  • 工具(Tool):实际执行的东西,可以是 HTTP API、进程内函数、MCP 服务、数据库查询等。

对应到界面上,运营同学看到的是“技能”,开发同学维护的是“工具”,模型调用的是“技能的参数化实例”。这个分层看着抽象,但真能帮你理清后面所有的事情:权限可以挂在技能上,灰度可以按技能版本走,故障排查可以精确到工具通道。

2.2 技能定义表与版本表

按照上面的分层,我最终定了两张核心表。

第一张是技能主表,存相对稳定的元数据:

CREATE TABLE skill_definition ( id BIGINT PRIMARY KEY AUTO_INCREMENT, skill_code VARCHAR(64) NOT NULL UNIQUE COMMENT '技能唯一编码,模型调用时使用', name VARCHAR(128) NOT NULL COMMENT '展示名称', description TEXT NOT NULL COMMENT '给 LLM 看的自然语言描述', capability_id BIGINT NOT NULL COMMENT '所属能力域', owner VARCHAR(64) COMMENT '负责人', skill_group VARCHAR(64) COMMENT '分组标签,如: order, weather, misc', current_version VARCHAR(16) COMMENT '当前生效版本', status VARCHAR(16) NOT NULL DEFAULT 'DRAFT', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_group (skill_group), KEY idx_status (status) );

第二张是技能版本表,存每次发布的快照:

CREATE TABLE skill_version ( id BIGINT PRIMARY KEY AUTO_INCREMENT, skill_id BIGINT NOT NULL, version VARCHAR(16) NOT NULL COMMENT '语义化版本,如 1.2.0', params_schema JSON NOT NULL COMMENT 'JSON Schema 参数定义', tool_bindings JSON NOT NULL COMMENT '绑定的工具及路由策略', prompt_hint TEXT COMMENT '额外的调用约束提示', created_by VARCHAR(64), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_skill_version (skill_id, version) );

为什么版本要单独一张表?因为技能一旦被线上 Agent 引用,就不可能“原地修改”。你改了参数 Schema,正在跑的会话可能还带着旧参数来调用,会直接出兼容性问题。版本化之后,发布就是一个“新增记录 + 切换 current_version 指针”的动作,回滚就是把指针指回旧版本,非常干净。

tool_bindings我特意存成 JSON,而不是单独建关联表,是因为路由策略本身是结构化的:一个技能可能绑定主工具和降级工具,每个工具还有权重和条件。JSON 存起来灵活,查询的时候反正也是整体读出来反序列化。

2.3 参数 Schema:JSON Schema 比函数签名更适合做底座

参数定义这个点,很多人会直接写“函数签名”,比如 Java 里定义好参数类型就完事。但一旦涉及到可视化表单渲染、参数校验、以及给模型生成准确的调用说明,函数签名就远远不够了。

我采用的是JSON Schema作为唯一真相源。比如“查天气”这个技能的参数定义:

{ "type": "object", "properties": { "city": { "type": "string", "description": "城市名,如:北京、上海" }, "days": { "type": "integer", "description": "未来几天,1 到 15", "minimum": 1, "maximum": 15, "default": 3 } }, "required": ["city"] }

这个 Schema 有三个用处:

  • 给模型生成parameters字段时直接透传,模型能准确理解每个参数的含义;
  • 给前端表格渲染时,JSON Schema 可以自动生成表单控件,比如enum渲染成下拉框、string + format=date渲染成日期选择器;
  • 给运行时做参数校验,非法参数在进入工具通道前就被拦截,减少脏调用。

要注意的是,给模型看的描述不能太干。description字段里我会补充业务含义,比如“用户口中的‘今明两天’对应 days=2,不要拆成两次调用”。这些约束写在参数 Schema 里,比写在系统提示词里更稳定,因为它会跟着技能版本走。

2.4 技能生命周期状态机

状态看起来是小事,但如果不设计好,会出现“模型调用了一个废弃技能”的事故。我定义的状态流是这样的:

  • DRAFT:编辑中,不会出现在 Agent 的工具清单里;
  • PUBLISHED:可以正常被 Agent 发现和调用;
  • DISABLED:紧急停用,不展示给模型,但保留数据和版本;
  • DEPRECATED:已废弃,仍然可调用但会记录告警,并提示负责人迁移;
  • RETIRED:彻底移除,历史调用日志仍然保留。

这里最关键的是DISABLED 和 DEPRECATED 的区别。DISABLED 是“立刻止血”,一般用于出故障时,比如接口 5xx 飙高、返回数据异常,先停用再说。DEPRECATED 是“计划内淘汰”,可以给一段过渡期,让依赖它的 Agent 配置逐步迁移。

所有状态变更我都要求写审计日志,记录“谁、在什么时候、把哪个技能从什么状态改成了什么状态”。这个不是为了找麻烦,而是技能一多,总有开发以为是别人改错了,审计日志能帮你省掉大量扯皮时间。

3. 可视化界面怎么设计:技能库、详情页、运行态三块屏

3.1 技能库视图:搜索、分组、组合

管理器的首页就是一个技能库,类似手机应用商店的列表。左侧是分组筛选,右侧是技能卡片。每张卡片上显示技能名、简短描述、状态、当前版本、最近调用成功率。

分组维度我同时支持skill_group标签和capability_id能力域。前者适合开发同学按业务模块找技能,后者适合老板视角看清能力全景。搜索则是对name、description、skill_code做全文检索,方便在技能多了以后快速定位。

这里有一个小设计容易被忽略:技能卡片上的描述必须展示给“人”看和给“模型”看的两种版本。给模型看的是完整版,包含参数约束和业务场景;给运维同学看的是摘要版,突出负责人、成功率和最后发布时间。人机读的信息结构不一样,一开始共用一套描述,导致运维经常看不出来“这个技能到底是谁的”。

技能库还支持“组合收藏”。运营同学可以把“查天气 + 查空气质量 + 穿衣建议”绑定成一套组合技能,本质上是生成一个新的聚合技能,内部编排依次调用子技能。这个功能我先做的是硬编码版本,后续演进可以对接工作流引擎,但初级版本用组合列表也够用。

3.2 技能详情页:配置、测试、监控三合一

点进一个技能,详情页分三个 Tab。

第一个 Tab 是配置。左侧显示基础信息,右侧是参数 Schema 编辑器。我做得比较简化的做法是:提供一个 JSON 编辑器,同时在下方自动渲染出表单预览。你改 Schema,表单实时变,确认无误后再保存草稿。

第二个 Tab 是测试。这里可以选定环境(测试环境还是预发环境),填好参数,点击调用,页面直接展示原始返回、耗时、状态码。对 HTTP 类工具,还能看到实际发送的请求头和请求体;对 MCP 类工具,能看到服务端返回的结构化内容。

这个测试沙箱最大的价值,是让非后端同学也能验证技能。以前运营想确认“这个技能参数传什么能返回正确结果”,得找开发写脚本;现在直接在界面上点,问题就闭环了。我甚至给测试 Tab 加了一个“以指定角色身份调用”的功能,用于模拟不同权限用户调用技能时返回数据的差异。

第三个 Tab 是监控。展示近 7 天调用量、成功率、平均耗时曲线,下面是最近的调用日志列表。这部分数据来自我后面会提到的skill_call_log表,界面只是做一个聚合查询。

3.3 运行态视图:把 Agent 的调用行为“回放”出来

这是我觉得整个管理器最值钱的一块界面:会话级调用回放。

简单说,当用户和 Agent 对话时,系统会记录:用户说了什么、模型思考过程(如果有)、模型调用了哪个技能、参数是什么、技能返回了什么、模型又怎么把结果组织成回复。管理器的运行态视图把这些信息按时间线串起来,运维可以按agent_id + session_id搜索一整条链路。

为什么要做这个?因为 Agent 的很多问题根本不在于“代码写错”,而在于“模型调用错了技能”或者“技能返回错了数据”。没有时间线回放,你看到的就是一句“Agent 回答错误”,完全无从查起。有了回放,你能马上看出来:哦,模型把city参数传成了“海淀区”而不是“北京”,而技能服务端只支持市级查询,所以返回了空结果。

回放视图我建议还要展示“模型为什么这么调”的证据。比如把模型当时的完整原文(包括工具调用前后的内容)存下来,这样排障时能区分是提示词问题、参数 Schema 问题还是技能服务端问题,而不是靠猜。

4. 核心实现链路:怎么把界面配置变成 Agent 能调用的工具

4.1 注册与存储:开箱即用的表结构

前端的注册操作,后端本质就是往skill_definition和skill_version里插数据。但有几个隐藏逻辑要做:

  • 技能创建时自动生成skill_code,规范是域名_动作_对象,比如order_query_detail、weather_query_city。skill_code不能随便让用户填,否则容易重名,或者出现不规范的命名,模型在理解工具时也会受影响。
  • 保存草稿时,新版本记录status=DRAFT,不会影响当前线上版本。
  • 发布时,把 DRAFT 版本置为PUBLISHED,同时更新skill_definition.current_version,并在发布记录表里写一条流水。

存储我用的是 MySQL,原因是我们团队的运维体系已经很成熟,RDS 全家桶都在,没必要为了技能管理单独引入一套新的存储组件。如果你的场景是纯本地的单人工具,用 SQLite 或者 JSON 文件其实也可以,但只要有多人协作,数据库是底线——文件的并发写入和审计能力都太弱了。

4.2 运行时桥接:生成 OpenAI 风格 tools 和 Spring AI ToolCallback

技能管理器本身只是一个管理面,真正要让 Agent 用起来,必须有一条“运行时桥接链路”。我这里同时接了两种主流的 Agent 运行时。

第一种是面向 OpenAI 兼容接口的 Python 侧。在 Agent 启动或者会话开始时,从管理器拉取已发布技能清单,转成tools数组:

def fetch_published_tools(registry_base_url, agent_id): resp = requests.get( f"{registry_base_url}/api/v1/agents/{agent_id}/effective-skills", timeout=3 ) resp.raise_for_status() skills = resp.json()["data"] tools = [] for skill in skills: tools.append({ "type": "function", "function": { "name": skill["skill_code"], "description": skill["description"], "parameters": skill["params_schema"], } }) return tools

注意这里我加了一个关键参数agent_id。也就是说,不是发布的所有技能都会推给所有 Agent,而是按 Agent 的授权范围返回“effective skills”。这个在 5.1 会细说。

第二种是 Java/Spring AI 侧。Spring AI 里你既可以用@Tool注解,也可以用ToolCallback接口。技能管理器对接 Spring AI 时,我是在启动阶段动态构造ToolCallback列表:

@Configuration public class SkillToolAutoConfiguration { @Bean public ToolCallback skillRegistryToolCallback( SkillRegistryClient registryClient, SkillInvocationService invocationService) { return new ToolCallback() { @Override public String getToolName() { return "dynamic_skill_bridge"; } @Override public String getDescription() { return "动态技能桥接,按技能编码调用统一技能管理器中的注册技能"; } @Override public JsonSchema getToolSchema() { return JsonSchema.builder() .name("dynamic_skill_bridge") .description("统一技能调用入口") .build(); } @Override public String call(String toolInput) { SkillInvokeRequest request = JsonUtils.parse(toolInput, SkillInvokeRequest.class); return invocationService.invoke(request.getSkillCode(), request.getParams()); } }; } }

有同学会问:为什么不直接给 Spring AI 生成多个ToolCallback,一个技能一个?我的建议是:对于运行时动态注册的技能,最好通过一个统一 Bridge 接入,而不是注册成 N 个 Bean。原因很简单:技能管理器里的技能是动态增删的,Bean 列表是启动时固定的,两者不同步。用一个 Bridge 工具,里面根据skill_code路由到对应执行通道,就解决了动态性问题。代价是模型看到的是一个“万能工具”,描述必须写清楚“这是一个技能调度器,具体能力见参数里的 skill_code,可选值包括……”,牺牲一点工具语义换取架构上的灵活性,实测是划算的。

4.3 三种执行通道:HTTP、进程内函数、MCP

SkillInvocationService真正执行技能时,按tool_bindings里配置的通道类型分发:

public InvokeResult invoke(String skillCode, JsonNode params, InvokeContext ctx) { SkillVersion version = skillRegistry.getEffectiveVersion(skillCode, ctx.agentId()); ToolBinding binding = chooseBinding(version.getToolBindings(), ctx); return switch (binding.getType()) { case HTTP -> invokeHttp(binding, params, ctx); case FUNCTION -> invokeBean(binding, params, ctx); case MCP -> invokeMcp(binding, params, ctx); }; }
  • HTTP 通道:最常用。把参数映射到请求体模板,自动带上鉴权 Header,支持超时和重试。这里有一个细节:URL 里经常有路径参数,比如/api/order/{orderId},所以绑定配置里要写清楚参数映射规则,而不是简单地把整个参数对象 POST 出去。
  • 进程内函数通道:主要用于同 JVM 里的存量方法。通过 Spring 的 Bean 工厂按名字查找目标 Bean,再反射调用。适合技能和业务代码同仓库、需要低延迟的场景。
  • MCP 通道:针对已经用 MCP Server 暴露的能力。我们的做法是让技能管理器充当 MCP Client,把模型请求翻译成 MCPtools/call,再拿结果返回。这块的好处是技能能力可以被 MCP 生态复用,坏处是需要额外维护 MCP 连接的生命周期。

通道选择还有一个“降级路由”:主通道失败、且错误类型是超时或 5xx 时,自动切到备用通道。比如“查天气”主通道是内部库,降级通道是第三方 API。降级次数要打点记录,方便后续判断备用通道的服务质量。

4.4 测试沙箱与密钥脱敏

测试沙箱本质上是一条“只读优先 + 环境隔离”的调用链路。我的实现里做了三件事:

  • 测试调用默认注入X-Skill-Test: true头,服务端识别后不写真实业务数据,企业微信、短信这类有副作用的能力在测试环境直接打桩返回。
  • 测试调用记录单独打标,不混入生产调用日志,避免污染成功率统计。
  • 密钥脱敏:配置文件里的 API Key、Token 在前端一律只显示后四位,编辑时也不能回显明文,只能覆盖。这个坑我是踩过的——有一次把第三方密钥明文展示在测试日志里,导致它出现在截图和日志采集系统里,最后只能紧急更换密钥。

好的测试沙箱,本质上是对生产调用链路的一个“影子副本”。它不需要完美,但一定要让用户无副作用地验证技能逻辑,否则大家还是会回到“写脚本调试”的老路。

5. 多 Agent 多环境:从单机工具走向 Agent 中台的几个关键设计

5.1 技能分组与最小权限

当你有多个 Agent,比如“客服 Agent”“运维助手 Agent”“购课推荐 Agent”,就不能让它们共享同一份技能清单了,否则客服 Agent 可能会调用内部数据管理工具,风险很大。

我的实现是引入agent_skill_binding关联表,记录“哪些 Agent 可以访问哪些技能分组”。技能分组按两个维度切:

  • 业务域:订单域、用户域、内容域、基础设施域;
  • 安全等级:公开、内部、敏感、高危。

Agent 在拉取有效技能时,管理器会先算出“业务域交集”,再检查“安全等级上限”,两者都通过的技能才会出现在effective-skills接口里。这个逻辑同样适用于人——你可以给某个运营账号分配“只能查看和测试天气预报技能”的权限,但不能让它停用支付类技能。

权限模型我用的就是最简单的 RBAC + 技能分组绑定,没有上 ABAC,因为初期用不到属性级的复杂规则。原则是:先能用,再完善,别一开始就把权限系统做成庞然大物。

5.2 版本灰度与快速回滚

技能发布一旦变成“线上变更”,就应该当作一次小型的发布工程来看。我给技能版本发布加了三种灰度策略:

  • 白名单灰度:指定部分 Agent 使用新版本,其他 Agent 继续用旧版本;
  • 比例灰度:新版本承载百分之多少的调用流量,按agent_id哈希取模做分流;
  • 全量发布:所有 Agent 生效。

实现上,effective-skills接口返回的是“版本指纹”,每次发布都会生成一个新的指纹。Agent 启动时如果发现指纹变了,就重新拉取技能清单。这样做的好处是不需要每次发布都重启 Agent,只需要周期性刷新。

回滚路径也必须通畅。出问题的时候,我只要在后台把current_version指回上一个版本,策略立即生效。因为之前的所有历史版本都留存在skill_version表里,所以回滚一点也不慌。我遇到过最严重的一次是版本 2.0.0 新增了一个参数必填项,但部分 Agent 场景没有这个参数,导致调用成功率骤降。当时就是靠一秒回滚到 1.9.2 止血的,然后花时间补了兼容逻辑再重新发版。

5.3 和 Spring AI / Spring Cloud 的集成姿势

我们内部有不少业务是用 Spring Cloud 微服务架构跑的,Agent 服务也是其中的一个节点。技能管理器和这类平台的集成,我总结成两句话:配置走配置中心,技能走独立服务,Agent 只做拉取和缓存。

  • 配置中心的application.yml里只放技能管理器地址、拉取间隔、本地缓存容量三个配置项,不直接放技能定义。
  • Agent 启动后第一件事就是调到技能管理器拉取有效技能并缓存到本地内存;每隔 30 秒做一次增量刷新。
  • 调用失败时,Agent 会尝试从本地缓存兜底,而不是每次都远程拉技能定义,保证主链路可用性。

这里有一个很实用的建议:Agent 侧一定要做技能清单的本地缓存,并且把“缓存时间戳”暴露成监控指标。因为你永远不知道技能管理器服务会不会抖动,如果 Agent 每次对话都实时拉技能,一次网络抖动就会让整个对话失败,这是线上事故,不是理论问题。

用 Spring AI 的话,官方支持ToolCallbackProvider方式批量注册工具。我建议把技能管理器拉取到的技能映射成一个ToolCallbackProvider的 Bean,而不要散落着手动写@Tool。这样对上层业务代码几乎无侵入,新增技能也不需要改 Agent 的代码。

5.4 多智能体协作时的技能路由

最后到了多智能体场景。前段时间我们搭了一个“代码协助开发”的多智能体系统,里面有规划 Agent、编码 Agent、审查 Agent。三个 Agent 理论上都算“代码类技能”的使用者,但它们各自需要的技能很不一样:

  • 规划 Agent 需要的是读取仓库结构、解析需求、拆解任务;
  • 编码 Agent 需要的是读写文件、执行测试、调用编译工具;
  • 审查 Agent 需要的是静态检查、依赖分析、生成审查意见。

如果让三个 Agent 共享全部技能,不仅容易让它们互相踩到对方的上下文,还会浪费模型在每一轮对话里读取工具描述用的 Token。

我在管理器里做的方案是:定义agent_profile,里面声明该 Agent 的定位和不适合调用的技能类型,再配合上面的agent_skill_binding。规划 Agent 拉取技能时,管理器会过滤掉“写文件”“执行测试”这类偏向执行的技能。这个过滤动作不是写在 Agent 代码里的,而是技能管理器根据统一策略在接口层做掉的,这样后续新增 Agent 就不再需要动技能层。

从这里其实就能看出,统一技能管理器最终会变成一个轻量级的“Agent 能力中台”:所有 Agent 的能力从一个地方获取,权限、版本、路由都在这层收敛,团队新接一个 Agent 时不用再从零维护工具定义。

6. 踩坑实录:热更新、超时熔断、技能冲突

6.1 进程内 Java 技能的热更新没那么简单

最开始我把“进程内函数”当成首选技能类型,想着动态改参数、改路由就能热更新。但实测下来,进程内函数有一个绕不过去的坎:JVM 的类已经加载了,你改了方法的实现,不重启就根本不会生效。

我一度想用自定义类加载器做热更新,把技能方法打成独立 JAR,加载到独立 ClassLoader 里。做了一个 Demo 版本,能用,但引入的问题更多:类加载器泄漏、依赖冲突、调试困难,最后还是拆了。

后来我调整了策略:进程内函数只承接“稳定且轻量”的技能,比如内部查询类函数;凡是可能频繁改动的技能,一律走 HTTP 通道,部署成独立的小服务,这样发布和回滚天然就由服务治理体系接管。这个教训让我明白,技能管理器的职责边界是“管理和路由”,不要强行去解决运行时热部署,那是另一个层面的事。

6.2 超时、限流、熔断缺一不可

技能调用比普通接口调用更让人头疼的是:模型会并行调用多个技能,或者在一个会话里高频连续调用同一个技能。如果不做治理,外部接口很容易直接打满。

我后来给每条调用链路都加了三个指标:

  • 超时:默认 5 秒,HTTP 通道可单独配置;
  • 限流:按技能维度做令牌桶,比如“查天气”每秒最多 50 次;
  • 熔断:连续 10 次调用超时或 5xx,自动熔断 30 秒,并把该技能标记为DEGRADED。

熔断后管理者要能收到告警,界面上技能卡片也会变色。我见过太多项目只做了超时,没做熔断,结果一个慢接口把整个 Agent 的响应时间全部拖垮,用户等三十秒才收到一个报错。

6.3 技能冲突和参数歧义问题

技能一多,新的问题出现了:模型分不清该调哪个技能。比如我们有“查询订单物流”和“查询售后进度”,描述都写得差不多,参数都有orderId。模型经常把售后问题调成物流查询,返回“暂无物流信息”,用户自然不满意。

排查下来,根因有两个:

  • 描述太相似,缺少区分关键词;
  • 参数名和含义有重叠,模型不知道该按哪个语义走。

解决方案是给技能管理器加了一个“相似技能冲突检测”。发布新技能时,后台会计算新技能描述和现有已发布技能描述的向量相似度,超过阈值就提示“这个技能与 xxx 可能冲突,请确认是否要补充差异说明”。同时会在配置界面强制要求填写“本技能不适用于哪些场景”的负面描述。这个负向约束很管用,模型在选择工具时对这种排除性信息非常敏感,能明显减少错误调用。

6.4 小心 Agent 的“幻觉调用”

最后一个坑,也是我认为最重要的:技能管理器只是提供了工具,但没有能力阻止模型乱用工具。尤其是当技能描述写得不够严谨时,模型会“创造”出不存在的参数值,或者在一个不该调技能的场景去调技能。

我的经验是,除了参数 Schema 要严格之外,必须在技能描述里写清楚“什么时候不要调用我”。比如“查天气”技能的描述最后一定要加一句“仅在用户明确询问天气时调用,日常闲聊请勿调用”。这句看起来简单,但它能显著减少模型在寒暄开场白里就莫名其妙调技能的情况。

还有一个更深的体会:工具清单越长,模型选择出错的概率越高。统一管理的价值之一,恰恰是让你有能力控制“每次会话里 Agent 到底能看到多少个技能”。我建议默认不要一次性把所有技能全塞给模型,而是按意图预筛出 5 到 8 个候选技能,再让模型选择。技能管理器完全可以承担这个预筛职责,因为它知道每个技能的历史调用分布和意图关键词。

7. 如果让我重做一次,这些地方我会先改

说点我自己回头看觉得可以少走弯路的地方。第一,权限系统不应该等到“出事了”才补。上面讲的agent_skill_binding其实是我在第二个版本才加的,第一个版本对外只做了“所有人都可以发布和停用技能”的权限,结果有人在测试环境把生产环境正在用的技能停掉了,触发了一次不小的线上失误。权限这件事,最好从第一天就至少做到“管理端和调用端分离”。

第二,技能描述的质量应该纳入代码评审。很多同学会觉得描述只是“写一段文字而已”,但实际上它是模型决策的直接依据。我现在要求每次技能发布,描述变更必须经过“对模型调用效果抽样对比”的检查,哪怕只是人工看一眼 10 条真实会话记录,也比不看强得多。

第三,调用日志要早点开始存。skill_call_log这张表我是在做运行态回放时才补的,结果历史数据已经丢了,很多早期的调用异常无法回溯。如果你准备做技能管理器,我建议第一次上线时就记录调用日志,哪怕先只记录skill_code、agent_id、params、result、latency_ms、status这几个字段,后面再做回放和监控就有数据基础了。这比事后追数据的成本低太多。

这个可视化技能管理器做到现在,我们的 Agent 已经有二十多个技能在统一管理下运行,线上调用成功率稳定在 98% 以上,新增一个业务技能从原来的一到两天缩短到半小时以内。每次看到运营同学在界面上自己把新技能测试通过、不需要再打扰开发,我都觉得当初把这块基础件做出来是对的。如果你也在做 AI Agent,正好被技能碎片化烦得不行,不妨从一个最小的注册表加一个网页列表开始,先跑通再逐步加权限、加版本、加回放,这套东西没有想象中那么高不可攀。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询