资讯动态

AI Agent技能体系实战:从工具调用到技能编排的工程化之路

发布时间:2026/10/14 9:46:49 来源:尧图企业网站定制
我大概从去年开始就把手上几个智能体项目里的“工具调用”全部重构成了一套统一技能体系。前前后后折腾了三个版本最终留下内部代号为 agent-skills 的这套框架。这篇文章不打算从理论定义讲起只想把我在技能注册、路由、编排、安全、评估这些环节踩过的坑以及最后沉淀下来的做法原原本本写出来。如果你正在做一个需要接很多真实能力、又不想被杂乱工具函数拖死的 AI Agent 项目这篇应该能帮你省掉不少试错时间。1. 从工具碎片化到技能化我为什么做了 agent-skills1.1 智能体不是“有一个模型就行”最初做客服助手的时候我以为只要选一个足够聪明的模型再塞给它一批工具函数就算大功告成。本地测试阶段只有五六个函数模型调用得很准我当时还挺得意。等工具数量慢慢涨到二十多个问题就出来了函数描述互相挤占上下文窗口模型经常在“查订单”和“查物流”之间反复横跳同一个功能换一种问法就调用失败。那段时间我每天做的事就是往 prompt 里追加“请严格按照如下函数定义调用”结果越补越乱。后来我慢慢意识到一个关键区别函数是写给代码用的技能是写给模型用的。表面上都是“模型能调用的东西”背后其实是完全不同的抽象层级。函数关心输入输出类型、异常处理、返回值结构技能关心的是“这个能力在什么场景下该出现、调用参数怎么表达、失败之后怎么办”。所谓 agent-skills就是把这层抽象单独拎出来做成一套独立于具体 Agent 业务的技能编排系统。技能成为组织单位每个技能封装一个可复用的能力比如查订单、发短信、做计算、调知识库。模型不直接面对一堆零散函数而是面对一组“有语义、有边界、有权限”的技能。1.2 现有工具调用方案的三个痛点在我决定自己搭之前也尝试过直接复用常见工具调用方案但始终绕不开三个痛点。可发现性差工具列表一长模型根本不知道有哪些工具可用即使知道也不会主动去调用不熟悉的工具。就像给一个新人发了一份五百行的 API 文档他大概率只会用自己见过的那几个接口。可配置性弱业务上经常需要临时调整某个能力的超时时间、权限范围、备用接口传统函数调用方案里这些逻辑散落在代码各处的 if/else 和装饰器里改动一次要重新走发布流程。不可观测模型到底选了哪个函数、参数解析成了什么、执行成功还是失败、花掉多少 token传统方案很少把这些信息结构化沉淀下来。没有观测数据就很难判断“模型选错工具”是描述问题还是底模问题。对比维度传统函数列表agent-skills组织单位函数技能使用时信息函数名参数场景描述参数权限兜底策略可观测性基本无全链路日志与指标迭代模式改代码发布技能独立上线/灰度/回滚模型友好度数量一多就迷茫路由机制先行筛掉无关技能这三个痛点叠加之后我决定做一个自己的技能中间层。接下来的内容会以 agent-skills 的最终实现为线索分成注册、路由、编排、安全、性能、评估几个部分逐个讲。2. agent-skills 的技能抽象与注册机制2.1 一个技能到底应该包含哪些信息技能的抽象设计是整个体系的底座我反复改过很多遍。最初只放技能名和函数引用后来发现完全不够。一个真正能被生产环境使用的技能至少需要以下几个字段。字段作用我的建议skill_id全局唯一标识用“领域.动作.对象”的方式命名例如order.queryname展示名中文短词让模型和人都能一眼看懂description详细说明包含触发场景、主要用途、参数含义、返回结果示例parameters_schema参数 JSON Schema严格描述参数类型、枚举、格式、是否必填executor执行器本地函数、HTTP 调用、子 Agent 任选其一timeout超时时间防止慢依赖拖垮整个流程permission最小权限声明例如order:read、order:writefallback兜底策略失败时返回什么或者降级到哪个技能这里尤其要强调description和parameters_schema这两个字段后面模型能不能走对门基本都靠它们撑着。description写得好不好不是文笔问题而是数据质量问题。一句话如果只说“查订单”模型在“查询订单”“修改订单”“取消订单”之间就分不清但如果写成“根据用户提供的订单号查询订单当前状态适合在用户询问订单进度、物流状态、发货时间时使用”模型就能比较准确地把它和“修改订单”区分开。2.2 装饰器式注册与自动装配注册机制我见过几种做法有人用配置文件登记有人扫描目录规则。最后我选的是装饰器式注册加启动时自动装配因为它在代码层面足够直观又能保留程序化的灵活性。举个例子一个订单查询技能长这样。skill.register( name查询订单, description根据订单号查询订单当前状态适合在用户询问订单进度、物流状态、发货时间时使用, parameters{ type: object, properties: { order_id: { type: string, description: 订单号通常为数字串, pattern: ^\\d{10,20}$ } }, required: [order_id] }, timeout10, permissionorder:read, fallback{default_message: 暂时无法查到订单信息请稍后再试} ) def query_order(order_id: str) - dict: return order_repo.get_by_id(order_id)装饰器内部做的事情很简单把函数对象、元信息、参数定义打包成一个Skill对象注册到全局技能仓库里。应用启动时管理器会扫描所有被skill.register标记的函数做一个完整性和唯一性校验然后构建路由索引和描述索引。你可能会问为什么不用 YAML 配置文件我试过确实配置更集中但两个问题很难解决一是参数校验逻辑很难在 YAML 里表达清楚二是技能代码和技能配置容易出现在不同仓库改一个技能要跨两个项目跑发布。装饰器把配置和实现放在同一个文件里至少对于“功能型技能”来说维护成本最低。纯配置化的方式更适合“流程型技能”这一点我会在编排部分再说。2.3 技能描述的质量决定了模型能否走对门技能描述是这个系统里最容易被低估的环节。一个技能上线后你的路由向量检索、模型最终选型、没命中时的日志分析全都依赖这段文本。描述写得不到位后面所有的权重调参都是白费。我自己总结了一套技能描述模板不一定符合所有场景但可以作为起步参照第一句说明“是什么”例如“根据订单号查询订单状态”。第二句说明“适合在什么情况下使用”把用户可能出现的表述变体写进去。第三句说明“不适合在什么情况下使用”帮模型排除歧义。最后一句给出一个调用结果的示例让模型知道返回什么、能解决什么。比如订单查询的完整描述可以是根据订单号查询订单状态。适合在用户询问“我的订单到哪了”“订单发货了吗”“订单处理中是什么情况”等场景使用。不适合用于查询物流轨迹、修改订单信息。返回字段包括 status、delivery_time、logistics_company 等。这个描述其实就是路由索引的原始素材。注册时我会用 embedding 模型把它转成向量同时拼接一份精简版用于模型决策。描述越结构化后面的检索和重排就越省力气。3. 技能路由不让大模型在几百个技能里大海捞针3.1 传统工具列表的问题如果你只接十几个技能把全部技能描述直接塞给模型可能还行。但一旦技能超过五十个上下文会被技能描述占掉一大半模型的选择准确率也会明显下降而且每次请求都要为“不相关的技能”多花 token。更麻烦的是有些技能之间的描述本来就长得像模型容易把 A 技能当成 B 技能调用。我一度试图通过压缩描述文本、缩短字段说明来缓解但效果有限。后来想明白根本解法不是让模型从全集里挑而是先做一个“预筛”用非模型手段把候选集缩小到 5 到 10 个再让模型在这几个里面做真正需要“理解能力”的最终选择。3.2 基于向量召回的两级路由agent-skills 最终采用了两级路由核心思路是“先粗筛再精选”。第一级用稀疏向量和稠密向量结合的方式召回候选技能。所谓稠密向量就是把技能描述和用户 query 分别 embedding计算余弦相似度所谓稀疏向量是抽取关键词和实体做字面匹配。两者合并打分能兼顾语义匹配和字面匹配。比如用户问“帮我看看我昨天买的东西发货没”语义上应该能召回物流查询字面上“发货”也能帮召回相关技能。第二级是“模型精排”。召回阶段我并不是直接把完整技能描述给大模型而是把候选技能压缩成一套轻量路由表每行只包含技能 ID、一句话描述、适用场景关键词。模型只需要在这份精简表里做选择题上下文压力小得多准确率也明显提升。这里有个关键设计每个技能要维护两套文本。一套是前面说的完整描述用于向量检索和技能说明另一套是路由用精简描述通常控制在 40 字以内。不要试图用一套文本应付两种场景完整描述太长模型精排时容易注意力涣散精简描述太短向量召回时又容易丢失语义。3.3 候选技能重排与阈值设计向量召回不是终点我在候选列表进入模型精排之前还会加一层规则重排。重排规则包括但不限于权限过滤用户/会话没有权限的技能直接踢出候选。参数预检query 里如果已经解析出明确的订单号就给order.*加权重。历史偏好同一个用户在历史会话里频繁使用某技能可以对它做小幅加权。热度惩罚最近五分钟内该技能错误调用率明显升高临时降权。阈值也很重要。如果只是按相似度取 topK某些完全不相关但文本上有点相似的技能会混进候选。我给相似度设了一个下限低于 0.25 的技能一律不进精排。这个阈值不是拍脑袋定的是通过一段时间的日志统计对比“正确召回的技能”和“误召回技能”的相似度分布后确定的。每个领域的分布不一样建议你上线后把相似度分数落库跑一周再调。4. 技能编排从单技能调用到可组合的工作流4.1 链式调用与数据流转单技能能解决的事情根本不需要编排。但真实场景里用户往往一句话要同时触发多个动作。比如“帮我把这个订单取消然后重新下一单”这里就涉及到查订单、取消订单、创建订单三个技能的前后协作。agent-skills 的编排层做的是“技能 DAG”也就是把多个技能节点连接成一个有向无环图节点是技能边是数据依赖。执行时从入度为 0 的节点开始一个节点跑完输出映射成下游节点的输入逐层推进。比如一个典型的售后流程可以描述为用户提问 - 查询用户身份 - 查询订单列表 - 定位目标订单 - 判断订单状态 - 执行对应售后操作。在这个模型里状态流转是个核心问题。我不会把每个技能的所有输出都原样放进上下文而是维护一个结构化的状态对象每个技能声明自己“读取哪些字段、写入哪些字段”。下游技能只从状态对象里取自己需要的部分。这样既能防止无关数据污染大模型的判断也能减少 token 消耗。4.2 分支、重试与兜底策略实际跑起来你会发现简单链路根本不够。用户的问题千奇百怪订单查询可能返回空、物流接口可能超时、用户权限可能不足每一个失败分支都得有明确行为。不要指望大模型在失败时“临场发挥”那会让行为完全不可控。我在技能 DAG 里定义了几类节点行为条件分支根据上游技能输出或状态字段值决定下一个执行哪个技能。重试节点针对外部 API 超时或瞬时错误最多重试两次间隔递增。兜底节点如果主技能失败执行一个替代技能或返回固定文案。终止节点正常结束或失败终止时统一封装响应。举个例子查询物流失败后兜底策略不是重新查一遍而是降级为显示“订单状态更新于昨天”。这个兜底动作本身也是一个技能拥有完整的注册信息和评估记录。因为兜底逻辑一旦写死就无法灰度、无法观测后续想优化会很被动。4.3 编排配置化别把逻辑写死在代码里一开始我把 DAG 结构写死在 Python 流程里后来运营同学想调整某个判断条件需要我改代码、发版、回测效率太低。第二次重构时我把编排流程改成了 JSON/YAML 配置DAG 定义和技能实现解耦。一个简单的配置示例大概长这样nodes: - id: verify_user skill: user.verify next: get_orders - id: get_orders skill: order.list next: find_target_order - id: find_target_order skill: order.slot next: check_status - id: check_status skill: order.check next: branch_by_status - id: branch_by_status type: condition conditions: - field: order.status if: cancellable next: order.cancel - field: order.status if: pending_payment next: order.pay default: terminal.fallback配置化之后流程变更变成改文件、走 review、灰度发布不再需要打扰服务端发版。不过要提醒一句配置化不等于无代码它只是把“流程可变的部分”和“技能实现的部分”分开。你对每个节点的输入输出字段还是要做 schema 约束否则配置写错一个字段名运行半天才发现所有节点都在取空值。5. 安全边界、成本控制与可观测性5.1 技能不是“能跑就行”权限与沙箱一个技能能跑通不代表它可以上线。尤其是那些会修改数据、发送消息、调用第三方付费接口的技能必须有明确的权限边界。agent-skills 里每个技能都声明了permission比如order:read表示只读order:write表示写操作。路由和编排引擎在执行前都会做权限检查和当前会话的用户身份匹配不匹配的直接拒绝并记录审计日志。沙箱方面我把执行器分成了三类本地函数、远程 API、子 Agent。本地函数跑在进程内受超时和异常隔离保护远程 API 统一走网关由网关做限流和重试子 Agent 是相对重量级的执行方式每一步还需要额外的 token 预算控制。我没有去做进程级容器沙箱原因是大多数技能只需要逻辑隔离不需要系统隔离。如果你的技能包括用户上传脚本、执行任意代码那种那另当别论应该上更严格的隔离方案。还有一点容易被忽略技能的输出很可能包含敏感信息。比如查询订单技能返回了收货地址和手机号如果原样拼进回复就可能把隐私内容暴露给另一个用户。所以 agent-skills 在技能输出层内置了脱敏组件默认把手机号、邮箱、身份证号等字段打码只有明确声明“需要明文”且拥有对应权限的技能才允许输出原始数据。5.2 调用配额和预算熔断技能上线之后成本控制是随时可能亮红灯的问题。大模型调用贵外部 API 也贵如果一个技能因为描述写得模糊被模型反复错误调用预算会瞬间烧掉一大截。我的做法是给每个技能配置了三层计数器请求量、错误量、估算费用。按时间窗口统计超过阈值就触发熔断在短时间内自动禁用该技能。比如某个技能五分钟内错误调用率超过 60%或者单小时调用费用超过预设值执行引擎会直接把它从候选中摘掉并返回一个“技能暂不可用”的兜底结果。这个能力特别适合灰度期间保护线上服务。新技能刚上线时我一般会把熔断阈值调得比较保守等错误率稳定之后再放宽。否则一个参数 Schema 写错的技能可能在几个小时内让团队把整个月的 API 预算都花光。5.3 全链路追踪与效果回捞没有观测就没有优化。agent-skills 从第一版开始就把全链路日志作为一等公民记录的信息包括完整用户请求、候选技能列表、每个候选的相似度分数、模型最终选中的技能 ID、调用参数、执行结果、延迟、token 消耗、错误信息、熔断状态。我会给每次请求生成一个trace_id贯穿 Agent 入口、技能路由、DAG 执行、外部 API 调用。出现任何线上问题先按 trace_id 找回放再判断是路由错了、参数错了、技能本身错了还是外部依赖错了。这个问题定位链路比任何调试工具都管用。这些日志还有一个附加值它们就是技能评估和模型调优的天然数据集。每次请求都是“用户在真实场景下想完成某件事”的样本哪怕模型选错、参数解析失败也是宝贵的负样本。我后面会讲怎么用这些数据做持续评估前提是日志数据从第一天就开始沉淀别等到需要时再补。6. 实测中的性能瓶颈与优化6.1 技能描述太长把上下文塞爆了我第一次全量接入二十个技能时发现每轮对话仅技能描述就占掉两三千 token还不算 DAG 节点之间的状态输出。用户稍微多聊几句上下文很容易逼近模型窗口回答质量直线下降。优化思路有两波。第一波是把每个技能的完整描述换成精简版也就是我之前提到的“路由描述”从一两百字压到三四十字。只保留“技能名 一句话场景 关键参数”上下文占用立刻少了一半多。第二波是把 DAG 执行过程中的中间状态尽量从对话上下文里剔除改用结构化状态对象传递。只有最终需要呈现给用户的信息才拼进回复模板。这两步做完同一个任务的 token 消耗大概下降了百分之四十五模型选型准确率不降反升。原因也简单上下文干净了模型的注意力更集中。6.2 并发执行时的连接池与锁DAG 编排不是纯串行。比如一个订单相关的流程可能需要同时查订单、查库存、查物流这三个节点互不依赖可以并行执行。只做串行的话一次用户请求的延迟会是所有技能延迟之和体验很差。并行改造时最容易踩的坑是连接池和线程安全问题。我一开始直接在每个技能函数里用requests.get并发一上来就报连接超时因为底层连接没有复用。后来统一改成使用一个共享会话池连接池大小按并发上限配置才稳定下来。如果你用的是异步框架也要注意技能函数是不是阻塞型 API把阻塞调用丢进事件循环会导致整个 Agent 卡顿。另外一个坑是“共享状态被并发覆盖”。DAG 里下游并行节点同时往状态对象里写字段如果没加锁字段可能被后执行的节点覆盖。我的方案是明确每个节点写入的字段范围执行引擎对写操作加细粒度锁并且下游节点只能读取上游已声明的字段不允许隐式依赖不在边上的状态。6.3 缓存触发结果时要小心的坑为了降本增效我给一部分只读技能加了结果缓存。比如查询天气、查询汇率、查询订单状态这类高频读操作在时效要求内直接返回缓存结果能省掉大部分模型重推理和外部 API 调用。但缓存技能结果有几个坑。第一它不是所有技能都适用写操作、随机结果、强时效数据绝对不能缓存。第二缓存 key 不能只拼参数还要拼“权限上下文”否则一个用户请求返回另一个用户的数据就出大事故。第三过期时间要按业务场景定订单状态可能几分钟内就有变化缓存 TTL 设成五分钟就是灾难。我还吃过一次亏给查询订单技能加了缓存结果人工客服修改订单状态后用户端仍然看到旧缓存。后来给状态变更类操作加了“缓存失效事件”技能内部主动删除相关 key才解决。这个机制在 agent-skills 里叫“缓存联动”执行写操作技能时可以声明自己会失效哪些读操作的缓存。7. 技能质量评估与迭代闭环7.1 给每个技能建需求-用例-回归用例技能上线不是终点而是评估的起点。我给每个技能维护了一个“用例集”里面至少包括三类样本用户说得很明确的标准案例、用户说得比较模糊但语义指向本技能的案例、容易混淆的边界案例。比如查询订单技能标准案例是“帮我查一下订单 123456789”模糊案例是“我那笔付款怎么回事”混淆案例是“我要改收货地址”。每次技能描述、参数 Schema 或路由权重发生变化我都会用这套用例集做回归。回归统计四个指标路由命中率正确技能是否进入候选列表并最终被选中。参数解析成功率模型填写的参数是否符合 Schema。执行成功率技能本身调用外部依赖是否成功。回复准确率最终回复是否满足用户意图。这四个指标全部落库按版本对比。只要出现明显回退我就知道改坏了什么可以直接回滚。7.2 用 LLM 打分替代人工验收用例集如果只有几十条人工可以慢慢标。但真实线上跑一周就会有几千条请求日志人工根本看不过来。我后来搭了一个“自动裁判”流程把一个请求的原始输入、候选技能列表、选中技能、参数、最终回复一起喂给一个独立评估模型让它按一定标准打分并给出简短理由。裁判 prompt 的核心标准是目标是否达成、技能选择是否合理、参数是否准确、回复是否简洁自然。裁判模型的输出不需要绝对精确主要是为了把“明显错误”的样本筛出来。比如技能选错了、参数乱编、回复答非所问这些一眼就能看出的问题自动打分就能覆盖。人工只需要每天抽检一部分裁判认为“不确定”或者“失败”的样本用来校准裁判的严苛程度。我自己的经验是裁判模型不要和业务主模型用同一个否则会在同样的盲区上犯错。用另一个厂商的模型或者完全不同的提示词体系反而能提供更独立的视角。7.3 灰度发布与灰度禁用技能体系必须支持独立上线、独立回滚。一个技能有问题不能影响整个 Agent 的主流程。agent-skills 里每个技能都有一个状态字段禁用、白名单、灰度、全量。灰度期间我可以按用户 ID 尾号或按流量百分比放量比如先放 10% 流量观察错误率和用户反馈。一旦发现某个技能错误率飙升最快止损动作不是改代码而是直接把它标记为“禁用”。禁用状态在路由引擎里即时生效所有请求都不会再召回这个技能直到修复并重新灰度。这个机制在线上救过我很多次比临时改代码发布快好几个数量级。8. 我踩过的几个典型坑8.1 参数 Schema 写得太宽松这个坑我踩得特别深。最开始给查询订单技能写 Schema只定义了一个order_id字符串也没加格式校验。结果模型在用户说“查一下我的订单”时居然把“我的订单”四个字当成了订单号传进去。执行器肯定不会命中我就收到一堆报错。后来我把 Schema 写得非常严格加上了数字串长度限制、常见前缀还给了两个格式示例。模型再遇到模糊输入时倾向于先通过另一个“提取订单号技能”做解析再用正式查询技能调用成功率立刻上了一个台阶。参数 Schema 不是写给别人看的是写给模型看的它越严格模型越不容易乱来。8.2 技能间隐式依赖害我半夜回滚有一次我给订单状态技能加了一个“预计送达时间”字段上线当天没问题第二天早上发现售后流程里的另一个技能开始拿不到数据。排查了很久才发现那个技能内部偷偷读取了订单状态字段里的某个非契约字段上游没有提供所以取到了空值。这就是技能间隐式依赖造成的。解决方法是把“字段契约”显式化。每个技能在注册时声明自己读取哪些字段、写入哪些字段执行引擎在 DAG 运行前做字段依赖校验。如果一个技能读取的字段不在上游输出里直接报配置错误而不是等到运行时才暴露。现在所有技能之间的数据交换都必须走显式字段映射不允许隐式共享全局对象。8.3 一些实际操作建议如果你也要做类似的东西我的第一条建议是先把“日志和回放能力”做好因为后面所有优化都靠它。别等技能数量多了再补日志那时候你已经丢掉大量宝贵样本。第二路由、编排、安全这些机制不要一开始就全部铺开。技能数量没超过三十个时简单的“完整列表 模型自选”可能更实用。过度设计会消耗你的迭代速度。我在第一版就上了全套路由和沙箱结果大部分时间不是在解决业务问题而是在调框架本身。第三技能描述要当作产品文案对待每个字都可能影响线上效果。不要写完就上线至少用回归用例跑一遍再放量。这套 agent-skills 体系到现在还在持续迭代但核心思路已经稳定把能力抽象成技能用路由缩小决策范围用编排组合复杂流程用权限和熔断守住边界用日志和评估形成闭环。如果你也在做类似的事情希望这些经验能帮你少踩几个坑。最后再多说一句任何 Agent 框架都只是辅助真正决定体验的依然是你对用户需求的理解和对每个技能的打磨。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑