news 2026/10/30 11:49:22

Agent技能扩展机制实战:从工具调用到技能编排的完整链路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Agent技能扩展机制实战:从工具调用到技能编排的完整链路

1. 项目概述

先说点实际的:我最近在维护一套自研的 Agent 技能扩展机制,内部就叫 agent-skills。

做 Agent 应用的人应该都有同感:模型本身的能力边界是固定的,真正让 Agent “能干成事”的,是把外部能力一块块挂上去。挂上去的这一块块能力,你可以叫工具、插件、扩展,但我和团队最终统一叫 skill——技能。为什么叫 skill?因为这个词更贴近产品化的表达:模型不只是能调用一个函数,而是像人一样掌握了一项“本事”。

agent-skills 解决的是三个问题:第一,技能怎么定义,让模型知道什么时候该用什么技能;第二,技能怎么注册和调度,让几十上百个技能共存不冲突;第三,技能怎么安全地执行,不让一次调用把整个系统搞崩。

如果你正在做 Agent 编排、正在把 LLM 接入业务系统,或者在思考“为什么我的 Agent 总是调用错工具”,这篇内容值得看完。我会把定义、注册、编排、执行、评测、排障这整条链路拆开讲,也会给出一些可以直接抄走的代码片段和踩坑记录。

2. 为什么必须把“技能”抽成独立层

2.1 不抽离技能层,你会在第 20 个工具时崩溃

很多项目一开始是从“给模型塞两三个 function”起步的。这个阶段确实不需要什么技能机制,直接在 prompt 里写清楚就行。但当工具数量超过 10 个,问题就来了:

  • prompt 里工具描述占的 token 越来越多,模型注意力被分散,误调用率上升;
  • 工具之间的调用关系没法表达,A 工具的结果需要 B 工具处理后才能用,只能靠模型自由发挥;
  • 权限、限流、审计这些横切逻辑无处安放,每个工具各自实现一份,代码重复严重;
  • 模型生成的参数对不对,全靠运气,没有统一校验层,线上很容易出事。

agent-skills 的核心理念是把“单次工具调用”提升为“技能生命周期管理”。一个技能不是一段函数,而是一个完整单元:它有自己的描述元数据、输入输出契约、执行策略、依赖关系、版本信息、运行环境和权限声明。模型只是触发了这个技能,真正的逻辑由技能运行时接管。

2.2 技能、工具、工作流,三者到底是什么关系

这三个词经常被混用,但在我这边的设计里,它们是不同层级的抽象:

  • 工具(tool):最细粒度的可执行单元,比如“调一个 API”“查一张表”“发一条消息”。工具不关心业务语境,只完成单一动作。
  • 技能(skill):面向任务的组合能力,可以是一个工具封装了 prompt 策略,也可以是多个工具的编排结果。技能有明确的触发条件和输出承诺。比如“周报生成”技能,内部可能包含“读取消息记录”“提取工作事项”“格式化输出”三个步骤。
  • 工作流(workflow):面向业务的固定流程,技能之间的连接关系是预定义的,弱化了模型的实时决策。

打个比方:工具是螺丝刀,技能是“换灯泡”的操作手册(里面用到螺丝刀、验电笔、新灯泡),工作流是物业检修的标准作业流程。agent-skills 主要管中间层,它要足够灵活,让模型能动态挑选技能,又要足够规范,不至于把底层工具暴露给模型乱调。

这个分层带来的直接好处是:模型需要做的决策变简单了。它面对的不是 50 个零散 function,而是 10 个语义清晰的技能,每个技能内部又有自己的兜底策略。决策面窄了,准确率自然上去。

3. 技能定义:写清楚“什么时候用”比写“怎么实现”更重要

3.1 技能清单的必备字段和扩展字段

我们的技能定义走的是 JSON 描述 + 代码实现的分离模式。描述部分负责“让模型理解”,代码部分负责“真正干活”。一个完整的技能定义包含以下字段:

必需字段:

{ "name": "create_meeting", "description": "在会议室系统中创建一场新会议,支持设定时间、参会人、会议主题。当用户提出开会、预约会议室、安排会议等意图时使用。", "parameters": { "type": "object", "properties": { "topic": { "type": "string", "description": "会议主题" }, "start_time": { "type": "string", "format": "date-time", "description": "会议开始时间" }, "duration_minutes": { "type": "integer", "minimum": 15, "maximum": 180 }, "attendee_emails": { "type": "array", "items": { "type": "string" }, "description": "参会人邮箱列表" } }, "required": ["topic", "start_time"] } }

扩展字段会根据实际场景追加:

{ "version": "1.2.0", "owner": "platform-team", "tags": ["meeting", "schedule"], "depends_on": ["auth_token", "calendar_room_check"], "timeout_ms": 5000, "auth_scopes": ["calendar:write"], "rate_limit": { "window_seconds": 60, "max_calls": 30 }, "semantic_trigger": "用户需要安排会议、查看日程、协调多人时间" }

version 和 owner 是工程化必备,否则技能演进一段时间后,没人知道这个技能现在是谁在维护。depends_on 声明依赖项,运行时校验依赖是否就绪。auth_scopes 做权限收敛,避免一个技能拥有过大权限。

3.2 描述即门面:同一个函数,不同的 prompt 写法,命中率差一倍

我做过一个对比实验:同一个“查询订单”函数,写了三种不同的描述,让同一个模型在 50 个测试问题上做技能调用。

  • 描述 A:“查询订单”。
  • 描述 B:“根据订单号或用户手机号查询订单详情。当用户想了解订单状态、物流信息、购买记录时使用。在调用前必须从原始对话中提取订单号,若缺少订单号应先询问用户。”
  • 描述 C:在描述 B 基础上增加了“绝不在没有订单号时猜测订单号,绝不用订单列表接口代替详情接口”。

实验结果:描述 A 的调用准确率只有 41%,频繁被用于“查库存”“开发票”等场景;描述 B 的准确率到了 78%;描述 C 能达到 89%,但代价是描述变长,token 占用多了。这个实验让我形成了一个原则:技能描述里“触发场景”和“禁用的边界条件”必须同时写清楚。只说什么时候用,不说什么时候不用,模型就会扩大技能的适用范围。

另外,description 的写法有很多细节:

  • 用动词开头,描述动作和结果,不用名词堆砌;
  • 把参数的含义解释清楚,尤其是枚举值、单位、格式;
  • 参数需要从对话中提取时,明确提示模型需要前置条件;
  • 如果有返回结构,用一两句话说明返回内容,方便模型判断下一步。

4. 技能注册与语义路由:为什么你的 Agent 总是选错技能

4.1 注册中心的职责不止是“存一份列表”

很多人理解的注册中心就是一个 Map:name 到 handler 的映射。但实际运转下来,注册中心要做的事情至少有五件:

第一,索引维护。技能数量上百后,不可能每次请求都把全部技能描述塞给模型,注册中心需要维护一个可检索的索引,按 tag、按语义向量、按最近使用热度加速查找。

第二,描述压缩。同一领域有多个技能时,要能生成“技能族描述”,先让模型选领域,再选具体技能。比如用户说“帮我改下周三的会”,先命中“会议管理”领域,再在领域内选择 update_meeting 而不是 create_meeting。

第三,版本冲突检测。两个技能 name 相同但版本不同,注册中心要能识别,并决定是否允许共存。我们的策略是只允许一个共存版本,其余作为灰度候选。

第四,依赖完整性检查。注册新技能时,检查 depends_on 里声明的依赖是否都存在。经常有人漏注册依赖,结果运行时报错。

第五,心跳与健康状态。每个技能上报自己的执行成功率、平均耗时、异常次数。超过阈值自动摘除,避免模型一直调到一个已经损坏的技能上。

4.2 语义路由的实现方案:不是所有技能都要进 prompt

当技能数量超过 20 个,把所有技能的 description 全塞进 prompt 会让模型决策质量明显下降。我们的路由策略是三层递进:

第一层:硬规则过滤。根据当前对话的上下文硬条件过滤技能。比如用户当前在“订单售后”流程中,那么只保留售后相关的技能。这一层用确定的规则,不经过模型,零延迟,负责把明显不该出现的技能踢掉。

第二层:向量召回。把用户问题转成 embedding,与技能描述的 embedding 做相似度计算,召回 top-K 候选。向量模型用的是一套通用中文 embedding,并没有针对技能库做微调,但配合第一层的过滤,效果已经可用。

第三层:LLM 精排。候选技能控制在 5 个以内后,再把它们的 description 拼进 prompt,让模型做最终选择。这一步由于候选少,准确率比较稳定。

用一个表格简单列一下各层的作用:

层级输入输出耗时作用
硬规则过滤用户输入 + 会话状态候选技能子集1ms 以内踢掉语义为主
向量召回用户输入top-K 技能10~30ms缩小候选范围
LLM 精排候选技能描述最终选中技能300~800ms完成精确决策

这套三层方案跑下来的整体命中率比单层 LLM 选择高了不少,失败案例主要出在语义召回阶段:用户表达过于口语化,或者技能描述里的关键词和用户问法完全不一致。后面章节会讲怎么针对这个问题做优化。

5. 技能执行闭环:从参数生成到结果回填

5.1 参数校验:模型给出来的参数必须过“合同”

技能被选中只是开始,真正的坑在参数生成环节。模型通过 function calling 模式生成参数 JSON,这个 JSON 的长相基本决定于它在 prompt 里看到的 schema。但模型是不可靠的,经常出现这几类问题:

  • 缺失必填参数:required 里声明了必填,但模型生成的 JSON 里就是没有;
  • 格式错误:时间写成了“明天上午”,而不是 ISO 格式,数值类型写成了字符串;
  • 凭空捏造:用户没提供过的信息,模型会用默认值或编造值填进去;
  • 语义正确但值错误:start_time 写对了格式,但把“下周三”换算错了日期。

我们的解法是:参数校验分两层。第一层用 JSON Schema 校验器做硬校验,类型、必填、范围、格式全走标准校验。第二层做语义校验,针对特定字段写校验规则,比如时间字段必须晚于当前时间、邮箱格式必须符合规范、ID 必须存在。

硬校验可以快速拦下一批明显错误,语义校验负责拦截“格式正确但内容不可信”的参数。

实现上,校验器逻辑大致如下:

def validate_skill_params(skill_name, params): schema = get_skill_schema(skill_name) # 第一层:硬校验 errors = validate_json_schema(schema, params) if errors: return {"valid": False, "errors": errors, "stage": "schema"} # 第二层:语义校验钩子 custom_validators = get_semantic_validators(skill_name) for field, validator in custom_validators.items(): if field in params: field_error = validator(params[field], context=get_current_context()) if field_error: return {"valid": False, "errors": field_error, "stage": "semantic"} return {"valid": True, "errors": []}

校验失败时,不直接报错让用户重来,而是把校验错误信息反馈回模型,让模型重新生成参数。这招效果非常明显,一次纠错成功率在 60% 以上,两次纠错能达到 90%。“校验失败 - 带着错误信息回去重新生成”是一个低成本高回报的闭环设计。

5.2 执行与错误归一化:同一套错误语言,模型才明白怎么处理

技能具体执行代码五花八门,但我们要求每个技能返回统一的执行结果结构:

{ "success": true, "data": { ... }, "error": null }

失败时:

{ "success": false, "data": null, "error": { "code": "CALENDAR_CONFLICT", "message": "所选时间段已有其他会议", "retryable": false, "suggestion": "请向用户说明该时间段冲突,建议重新询问可用时间" } }

这里最关键的是 retryable 和 suggestion 两个字段。retryable 告诉 Agent 这个错误是否值得重试,比如限流错误可以重试,库存不足不能重试。suggestion 给出了下一步动作的建议,模型可以直接把这个建议翻译成用户话术。

错误码要语义化,不要用 HTTP 状态码代替。HTTP 500 无法告诉模型“库存不足”和“价格变了”有什么区别,但 SKU_OUT_OF_STOCK 和 PRICE_CHANGED 能让模型精准决策。

执行超时统一由技能运行时兜底。每个技能有自己的超时阈值,默认 5 秒,超时后任务转入异步等待或者返回“稍后重试”。不允许技能内部开无界 goroutine/线程,超时控制必须在运行时层面强制。

5.3 上下文与结果回填:一旦用过,就要让模型记得住

技能执行完之后,结果要写回对话上下文,供后续技能或模型继续推理使用。这里有个容易忽略的问题:直接把完整返回塞进上下文,会带来 token 膨胀和敏感信息泄漏风险。

我们的做法是:每个技能声明一个 result_summary 字段,说明返回结果中哪些信息应该保留在上下文中、哪些应该丢弃。比如“查询订单”技能返回了订单号、金额、地址、手机号,但上下文中只需要保留订单号和金额,手机号和地址直接截断。

"result_summary": { "keep_fields": ["order_id", "total_amount", "status"], "drop_fields": ["receiver_phone", "receiver_address"] }

这不仅是省钱的问题,更是安全边界。Agent 的上下文会被用于后续所有推理,如果一个查询技能返回了用户隐私字段,这些字段可能在下一次调用另一个无关技能时被模型“无意”带入参数中。做了结果回填裁剪之后,这类隐私漂移问题基本消失了。

6. 技能编排与依赖管理:单点技能好用,组合起来才是 Agent

6.1 串行编排和并行编排的决策成本

单个技能能搞定的事,根本不叫 Agent。真正的 Agent 场景是一连串技能配合,比如“明天下午三点和某人开会,先查下双方空档,再找会议室,最后创建邀请”。

这种多技能协作的编排方式,我目前用的是两层设计:

第一层是预定义工作流。当业务流程固定时,直接声明一个 step 列表,按顺序执行。比如上面那个开会场景,固定步骤就是“查空档→找会议室→建会议”,中间不需要模型做额外决策。

{ "name": "arrange_meeting_workflow", "steps": [ { "skill": "check_attendee_availability", "map_params": { "user_ids": "attendees" } }, { "skill": "find_free_room", "depends_on_result": "step_0", "map_params": { "time_range": "step_0.free_slots[0]" } }, { "skill": "create_meeting", "depends_on_result": "step_1", "map_params": { "room_id": "step_1.room_id" } } ] }

第二层是自由编排。当业务流程不固定时,只给模型一个可用技能列表和当前目标,让模型自己决定调用顺序、何时终止。自由编排的难点在于防止模型陷入循环:同一个技能被反复调用、参数完全一样,或者两个技能互相调用永远不结束。

目前较稳的防护手段是设置执行深度上限(比如最多 8 个技能调用)和节点去重(同技能同参数组合只允许执行一次,重复触发直接返回上一次结果并提醒模型换方案)。这两条是基础红线,能拦掉 90% 的循环类事故。

6.2 参数映射:上一个技能的返回值怎么变成下一个技能的入参

自由编排里有一个特别容易被低估的细节:上一个技能返回值的结构,和下一个技能需要的入参结构,几乎永远不一致。查空档返回的是“该用户有空的时间段列表”,找会议室需要的是“时间段区间”,中间必须有字段映射逻辑。

我们在技能定义中增加了output_schema,配合运行时做自动映射:

{ "output_schema": { "type": "object", "properties": { "free_slots": { "type": "array", "items": { "type": "object", "properties": { "start": { "type": "string" }, "end": { "type": "string" } } } } } } }

下一个技能声明入参时可以用表达式引用上一步结果。运行时根据依赖关系解析这些表达式,完成自动映射。表达式解析要能处理嵌套路径,比如step_0.free_slots[0].start这种写法。

这个机制听起来简单,但实际上解决了编排中一个巨大的隐性成本:不用每个步骤之间都写胶水代码。没有它,每加一个技能组合,就要手写一段转换函数,技能越多组合越多,维护成本爆炸。

6.3 技能依赖不只有数据,还有环境和状态

除了数据依赖,技能之间还有环境依赖和状态依赖。环境依赖指某个技能必须在特定运行时里执行,状态依赖指技能执行的结果会改变系统状态,后续技能要能感知。

举一个踩过坑的例子:一个“发优惠券”技能,依赖用户是否完成过“实名认证”。代码上,两个技能没有任何数据传递关系,但发券时校验了认证标记。如果编排引擎只做数据依赖处理,不会发现这条隐式依赖。

我们的解决办法是在技能定义里加一个 effects 声明,表示执行该技能会对哪些业务状态产生影响:

"effects": [ { "target": "user.verified", "action": "set", "value": true } ]

编排引擎在调度前检查所有技能的前置状态条件。这样虽然不能发现所有隐式依赖,但至少把显式声明过的状态依赖纳入调度把关,避免大批量事故。

7. 实测效果与评测体系:技能质量的量化标尺

7.1 三大核心指标:命中率、完成率、返工率

技能体系不能只凭感觉优化,必须有评测指标。我们建立了一套相对轻量的评测机制,重点看三个数字:

  • 技能命中率:给定一组测试问题,路由引擎最终选中的技能与人工标注技能一致的比例。低于 60% 说明路由链路或描述质量出了问题。
  • 任务完成率:Agent 完整走完流程后,用户目标是否达成。这个指标由自动化检查脚本或人工复核判定,不能只靠模型自评。
  • 技能返工率:在一次任务中,同一个技能被调用次数超过 2 次的比例。返工率高通常意味着参数生成失败或错误信息没有正确传回模型。

每个技能单独维护这三个指标的滚动数据,按周对比。

之前上线的“用户意图识别优化”和“技能描述重写”双管齐下,命中率从 51% 提到了 79%,返工率从 24% 降到了 11%。描述重写的效果尤其明显——“触发条件写清楚,禁用条件也写清楚”,这两句话做到位,命中率低不到哪去。

7.2 评测集怎么建:别只用真实对话,要用“边缘对话”

评测集如果只从真实用户对话里抽样,会严重偏向高频场景,漏掉边缘场景。我们的评测集包含三类来源:

第一类,真实用户对话脱敏后按技能语义聚类采样。用于验证日常主流程的技能命中。

第二类,人工构造的边缘问题。比如“帮我查下上周的订单但是现在不记得订单号”(缺参场景)、“我想退掉昨天买的所有东西”(多技能协作场景)、“不用了谢谢”(无意图场景)。

第三类,Bad Case 回流。线上出现了错误的调用、返工、报错,人工筛选后作为回归用例塞进评测集。

评测集建好之后,每一次技能描述修改、参数 schema 改动、路由逻辑升级,都要全量回归。回归通过后,才允许上线。没有评测集的情况下改技能描述,等于闭着眼睛开车。

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

8.1 模型就是不用某个技能,怎么排查

如果你的 Agent 始终绕开某个技能不用,先别急着怀疑模型能力,按下面几个方向逐一排查:

  • 看描述是不是太窄。只写了具体功能,没写触发场景,模型就不知道该什么时候用。把用户可能怎么表达这件事的意图也写进去。
  • 看描述是不是太长。超过 200 字的描述,模型在前面看得到,在后面有时就忽略了。控制在 150 字内比较稳妥。
  • 看参数 schema 是不是太难生成。required 字段过多、枚举值语义模糊,模型可能因为“生成参数困难”而放弃调用。
  • 看技能名是否容易混淆。技能名像check这种太宽泛的,和别的技能容易打架,改成check_inventory_by_sku这样的明确命名帮助更大。

8.2 参数幻觉严重怎么办

参数幻觉就是模型生成了用户根本没提供的参数值。比如用户只说“帮我订个蛋糕”,模型自动填了“8 寸”“奶油口味”,这就是幻觉。

处理思路有三步:第一步,schema 里把可选参数尽量去掉或者设默认值,减少模型自由发挥空间;第二步,增加缺失参数追问指令,prompt 里明确“用户未提供的参数不得瞎猜,必须追问”;第三步,执行前加语义校验,对可疑字段进行质疑确认,比如时间、地点、数量这几个字段必须有明确信息来源,否则触发反问。

做了这三步之后,参数幻觉类问题明显减少,但没法完全消除。关键是要建立兜底:校验失败时,模型有两个选择,追问用户或者改用合理默认值,但绝不能用编造值直接执行。

8.3 技能多了之后注册表越来越乱,怎么治理

技能上到 50 个之后,注册表就开始出现各种乱象:同名技能不同版本共存、描述陈旧与实现不符、废弃技能占着资源没下线、技能之间互相依赖形成环。

治理措施有这几条:

  • 上线自动化检查:每次提交技能定义时,自动跑 schema 检查、依赖完整性检查、描述长度检查、重复 name 检查;
  • 建立技能所有权制度:每个技能必须有 owner 字段,owner 超过 3 个月没更新就推送提醒;
  • 定期做技能下线:按调用频率排序,低于阈值的技能进入“待废弃”状态,观察 2 周后自动下线;
  • 环依赖检测:注册时用拓扑排序检测依赖环,存在环直接拒绝注册。

8.4 并发调用同一个技能,资源争抢问题

多个用户同时触发同一个有状态技能时,会出现资源争抢。比如同一个会议室被两个人同时创建预订,或者同一个优惠券模板被并发发放导致超发。

处理方式从代码层面和编排层面双管齐下:

代码层面,每个技能按业务语义定义资源锁的粒度。比如创建会议的 lock key 是 room_id + time_slot,发券的 lock key 是 activity_id。锁的粒度不能太粗,太粗会拖垮整体吞吐;也不能太细,太细锁不住。

编排层面,增加全局并发窗口限制。同技能同一秒内允许的最大并发数可配置,超出的请求排队或直接拒绝。注意被拒绝时要把“稍后重试”的信号返回给模型。

9. 写在最后的经验和下一步打算

agent-skills 这套机制跑了半年,我最深的体会是:技能体系的瓶颈通常不在模型能力,而在工程化细节。描述写得潦草、参数校验缺失、依赖管理混乱,这些才是拖垮 Agent 质量的元凶。

如果让我给刚开始做 Agent 技能化的开发者一个建议,我会说:先别急着实现花哨的自动编排,先把“技能定义、注册、校验、结果回填”这四件事做扎实,跑通一个最小闭环之后,再扩展编排和路由。基础不牢,后面每一个新技能上线都会变成一次灾难。

下一步我在探索两个方向:一是技能间的上下文传递更细粒度化,按需保留、按需遗忘,而不是一个大 Context 从头带到尾;二是让技能描述可以基于线上失败案例自动迭代,减少人工维护描述的成本。这个方向上如果跑出了不错的结果,会再来分享。

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

Lasso回归在Matlab中的实现与应用技巧

1. Lasso回归在数据预测中的独特价值 第一次接触Lasso回归是在处理一个客户流失预测项目时。当时数据集有200多个特征,但大多数都是冗余或相关性很弱的变量。传统线性回归不仅计算量大,过拟合问题也特别严重。直到尝试了Lasso,才发现它简直是…

作者头像 李华
网站建设 2026/10/23 10:05:31

金融科技Java笔试题核心考点拆解:集合、并发、JVM与Spring

简介:2020兴业数金Java笔试题.docx是一份针对兴业银行旗下兴业数金Java在线笔试整理的问答式复习资料,适合备战银行金融科技岗位Java笔试的求职者使用。文档以知识点问答形式呈现,重点覆盖Java方法重载、JDK提供的javadoc/jar/jconsole等辅助…

作者头像 李华
网站建设 2026/10/24 23:02:16

数据结构与算法习题答案docx实战:版本核对到高效复习指南

简介:《数据结构与算法分析(Java语言描述)》(第三版)习题答案文档,面向以Java语言学习数据结构与算法的学生,尤其适合备考期末、考研复试或课后自测。整体为1个docx文档,压缩包约1.5…

作者头像 李华
网站建设 2026/10/29 14:02:56

零成本搭建个人AI API聚合网关:2026年保姆级实战教程

个人开发者想低成本用上多家大模型,有个经典玩法:用开源项目 One-API 搭一个自己的聚合网关,把多家厂商的免费额度统一成一个 OpenAI 格式接口,换模型不改代码。本文是 2026 年版的保姆级教程,从网关选型一路讲到运维进阶。网关选型:为什么是 One-API 社区里三个主流方案各有千…

作者头像 李华
网站建设 2026/10/26 15:29:39

微电网多时间尺度调度优化MATLAB实现与工程实践

1. 项目背景与核心价值 微电网调度领域正面临一个关键挑战:如何协调不同响应速度的能源设备在分钟级到小时级时间尺度上的协同运行。传统单层调度模型往往难以兼顾光伏发电的秒级波动和燃气轮机的分钟级启停特性,这正是多时间尺度滚动优化技术的用武之地…

作者头像 李华