ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

AI Agent技能系统设计:从技能注册到动态路由的实战指南

AI Agent技能系统设计:从技能注册到动态路由的实战指南 在接触了大量 Agent 项目之后我越来越确信一件事决定一个智能体能走多远的不是模型的聪明程度而是它的技能层。模型再强技能系统一团糟落地的时候照样四处漏风。我们在生产环境遇到过很多次这种状况——Agent 在 demo 里表现得像个老员工一上真实业务就频繁调用错误的工具、在无关技能之间反复横跳甚至把上下文撑爆直接报错。排到最后问题的根子几乎都出在一个地方agent-skills 没有设计好技能之间毫无边界和章法。这篇文章我想把这些实操中的思考和踩坑记录整理出来给正在构建智能体技能系统的朋友做一个参考。1. 先搞清楚一件事Skills 不是会调用的函数而是 Agent 的能力骨架很多人会把 agent-skills 理解成一个简单的函数列表给模型一些带描述的 JSON Schema让它按需调用完事。这种理解在玩具项目里没问题但一旦进入真实业务场景它就会成为最大的隐患。1.1 为什么功能函数和技能不是一个概念函数是代码层面的东西形如def get_weather(city: str) - str: ...它只关心输入和输出。但技能是一个完整的能力单元它至少包含五个维度触发条件什么情况下模型应该使用这个技能什么情况下绝对不该用输入约束参数的取值范围、必填项、默认值以及模型理解模糊时的兜底策略执行策略调用外部 API 时是否需要重试和超时控制是否需要二次确认副作用说明这个技能会不会修改数据、扣费、发消息如果有副作用模型决策时要不要额外谨慎错误语义失败时该怎么反馈给模型是返回系统错误还是返回查询范围过窄请扩大时间窗口这五个维度真正决定了 Agent 的行为边界。一个函数只是机械地计算而技能则参与了模型的前置决策——它可以告诉模型什么场景该出手什么场景该闭嘴。我见过最典型的反面案例是把一个发送营销短信的功能直接暴露给模型没有在技能层做任何边界约束。结果模型在用户问我这个月话费怎么这么高的时候居然调用营销短信技能给用户发了一堆促销消息。功能函数没有任何责任但技能的决策权在模型手里技能边界不清模型的判断就会失控。1.2 技能层是模型推理和外部世界的翻译器再往深一层看agent-skills 实际上承担了一个翻译角色LLM 对世界的理解是建立在文本和语义空间里的而外部系统需要的是精确的结构化指令。技能就是这个翻译器。举个具体的例子。用户问帮我看看北京明天适合穿什么你事件设计和提供的能力可能包括{ name: query_weather, description: 用于查询世界主要城市的实时天气和未来三天的温度、降水、风力状况, parameters: { type: object, properties: { city: {type: string, description: 城市中文名如北京、上海、广州}, date: {type: string, description: 查询日期格式 YYYY-MM-DD默认今天}, metric: {type: string, enum: [temperature, precipitation, wind]} }, required: [city] } }模型看到的不只是几个参数而是这段描述里隐含的一个承诺这个技能能理解明天这种相对时间表达并把它转成具体的日期值能理解适合穿什么需要的是温度和降水的组合信息。所以技能描述写得越贴近真实能力边界模型的调用成功率就越高。如果技能系统只是机械地把函数签名翻译成 JSON 塞给模型相当于给了一个没有操作说明的复杂机器——模型能力强还能猜一猜弱一点的直接原地崩溃。技能层做得好的项目通常都会花很大精力打磨 description 字段把它当成和模型交互的接口协议来对待而不是顺手填一句话。1.3 技能是动态的不是静态的折旧品还有一个很容易被忽略的视角Agent 的技能不应该是一成不变的。人也是这样新任务出现时学新技能技能过时时就要淘汰。如果你的技能系统设计成每次新增能力都要改代码重新部署那就等于把 Agent 锁死在了开发迭代周期里。真正可用的技能系统应该把技能的注册、更新、启停做成运行时能力。也就是说运营一个 Agent 更像在管理一支团队技能库是团队的作战手册而不仅仅是几段代码的集合。你随时要能加技能、改技能描述、下架有问题的技能而这些操作不能影响整体的稳定性。这些思考直接决定了我在搭建技能系统时选什么样的架构。接下来详细说说落地过程。2. 技能系统怎么设计才稳注册中心、路由策略和上下文隔离2.1 零散技能就是一场灾难先建一个注册中心无论你是用 LangChain、OpenAI Function Calling还是自研的 Agent 框架第一步永远是统一管理技能清单。我在项目里称之为技能注册中心Skill Registry它是一个拥有完整元数据的能力目录而不是一堆散落各处的 Python 函数。注册中心的核心数据结构我一般推荐这样设计字段类型作用skill_namestring全局唯一技能名如query_user_ordersdisplay_namestring面向用户的展示名如查询历史订单descriptionstring给模型看的详细能力说明必须写清楚边界input_schemaobject入参 JSON Schema含类型、范围、必填output_schemaobject出参结构方便模型理解返回结果permission_levelint0 无副作用1 只读外部数据2 会修改数据3 涉及资金/隐私需二次确认idempotentbool是否幂等决定重试策略retry_policyobject超时时间、重试次数、降级方案statusstringactive / paused / deprecatedcreated_atdatetime注册时间updated_atdatetime最后一次修改时间这个注册表解决了三个痛点技能可见每添加一个技能都必须经过注册流程任何人不能绕过注册直接拼一个 function 进去。这样整个系统的能力边界是一目了然的。描述一致同一个技能不会出现在多个地方避免不同插件模块里各写一份割裂的描述。权责清晰每次修改都有记录可查生产环境出问题了能快速定位是哪个技能在哪个时间段变过。我见过太多没有注册中心的项目技能散落在不同模块里有的写在 prompt 里有的写进工具列表有的干脆塞在业务代码的 if-else 里。这种项目前期跑得快后期维护就是噩梦。模型行为稍微一变你都说不清楚它是被哪份技能描述影响的。2.2 路由策略给模型一份精简但完整的技能候选集解决了技能管理问题之后下一个挑战是把技能送到模型面前的方式。很多人一开始会把全部技能一股脑塞进每条请求里这在技能数量超过 20 个之后就会出问题入参 token 消耗巨大每条请求都要背负所有技能的描述技能之间相互干扰模型在太长的候选列表里更容易产生幻觉调用所以我最终的方案是动态路由。具体拆分两层粗选层Retriever用 embedding 关键词匹配从技能注册中心里筛出与用户意图最相关的 Top 5~8 个技能作为候选集。精排层模型决策把候选集的全量描述交给模型模型基于完整描述精准选择要调用的技能和参数。粗选层其实可以直接用文本相似度实现。核心代码逻辑大致是from sentence_transformers import SentenceTransformer import numpy as np model SentenceTransformer(paraphrase-multilingual-MiniLM-L12-v2) def retrieve_skills(query, skill_docs, top_k5): query_embedding model.encode([query]) skill_embeddings model.encode(skill_docs) scores cosine_similarity(query_embedding, skill_embeddings) top_indices np.argsort(scores[0])[::-1][:top_k] return [skill_docs[i] for i in top_indices]这里有个容易被忽视的细节技能描述文档skill_docs的生成方式。我建议不只把 description 字段做 embedding而是把名字、描述、输入参数说明、典型使用场景拼接成一份连续的文本再编码。这样语义匹配的空间更加丰富模型知道技能的真实使用边界而不是每次都在一个干燥的字段里捞信息。动态路由的效果非常明显。在我们的实测里技能数量从 15 个扩展到 60 个之后每条请求的 token 消耗几乎没变而模型的选择准确率反而因为干扰减少而上升了几个点。这就是典型的少即是多。2.3 上下文隔离技能信息不能污染对话记忆技能系统的另一个大坑是技能执行结果和对话记忆搅在一起。很多初学者会直接把 function_call 的结果当作普通消息 append 到 messages 里下一次对话又把这些内容全部发给模型。这会导致两个问题token 浪费严重一次技能返回的长文本会被反复携带哪怕下一次对话根本用不上语义干扰模型会把技能返回的过程细节当成用户意图的一部分严重影响后续推理后来我把上下文做了严格隔离。技能调用的上下文分成三层层级内容生命周期短期执行上下文当前技能调用的入参、原始返回结果、错误信息本次调用结束即销毁会话摘要层技能执行结果经过压缩后的结论摘要比如已查询用户上个月的订单总额为 342 元会话存在期间可用长期记忆层需要跨会话保留的关键事实比如用户偏好、常用地址持久化存储这三层的分工非常明确模型做决策时看到的永远是压缩后的摘要和长期记忆而不是一次技能调用产生的原始海量输出。这就要求技能返回结果的设计也和普通函数不同——技能不仅要返回原始数据还要生成一段适合沉淀进记忆的摘要文本。我在技能开发规范里定了一条规则每个返回结构化数据的技能必须额外提供一个memory_summary字段。这个字段是给模型看的、经过提炼的自然语言结论其他原始数据只用于本次展示和短时赋值。这样设计之后长会话中的模型表现稳定了很多。之前那种聊了二十轮之后模型开始胡言乱语的情况大量减少。原因很简单模型的上下文窗口终于有了清晰的结构技能产生的噪音没有在对话空间里无限累积。3. 我踩过的三个坑幻觉调用、命名冲突、上下文膨胀搞技术的人都清楚文档里写出的架构和真实运行的效果永远是两回事。技能系统真正上线、被各种真实流量打过之后那些隐蔽的问题才会暴露出来。这一节把这三个印象很深的坑还原一下也许能帮你少走几个月弯路。3.1 幻觉调用模型调用了根本不存在的技能第一次遇到这个问题时的场景我记得很清楚。生产环境日志里突然出现了大量 404 报错排查后发现模型在某几轮对话中调用了一个叫query_refund_status的技能但我们注册中心里根本没有这个技能只有一个外形相似度极高的query_order_detail。当时的第一反应是怀疑 embedding 路由出了问题但回查发现路由是正常的——候选集里根本没有query_refund_status。问题出在模型自身它在生成 function_call 的时候根据用户描述我退款到哪一步了自己脑补了一个技能名而不是从候选技能列表里做选择。这个坑的根子在于技能信息进入模型的方式不够闭环。当你把技能用 Function Calling 格式传给模型时模型理论上只能在给定列表里选名字但如果你用的是 Prompt JSON 输出或者某些宽松的 Agent 框架模型就有机会发挥创造性。解决方案分成三层层层加固第一层路由结果不仅要给模型看还要在系统层做一次硬校验。模型返回的function_name必须存在于当前请求路出的候选技能集合中否则视为无效调用强制进入纠错流程。第二层纠错流程里把候选技能的名称列表和简短说明再给模型看一次明确告诉它只能选这些。通常第二轮的准确率就回到正常水平了。第三层给模型明确加一条系统提示如果没有任何技能能解决用户的问题直接告诉用户做不到不要尝试编造一个不存在的能力。最终的效果立竿见影。这也让我意识到agent-skills 系统必须有两个层面的保障一个是模型决策层的智能另一个是执行层的硬校验。两者缺一不可绝不能把所有信任都放在模型的判断上。3.2 命名冲突两个技能语义重叠谁该被调用第二次大规模踩坑发生在技能数量增长到 40 个左右的时候。当时我们有两个技能一个叫get_warehouse_stock另一个叫query_supplier_inventory。产品经理的角度它们是有明显区别的前者查自家仓的实时库存后者查上游供应商的可供量。但对一个语言模型来说这两个描述几乎是一样的东西——库存查询。结果就是模型的选择概率几乎随机。有些对话里它选了前者有些对话里它选了后者客户看着明明是同一个问题两次得到的数据却完全对不上。更麻烦的是由于两个技能的返回结构不完全一致下游解析逻辑经常报错。排查到最后我并不想直接删掉其中一个技能因为它们的业务逻辑确实不同。于是我换了个思路重构两个技能的描述让它们的边界彻底互斥。新的描述写法是这样的第一个技能重点强调查询自有仓库的实时库存水位数据源为 WMS 系统单位为箱第二个技能重点强调查询外部供应商未来 30 天的承诺供货量数据源为供应商协同平台单位为件同时我在描述里主动加入该技能不适用于...的负向边界条件。这一点被太多人忽视了——正向描述管用但负向排除同样关键。当一个技能和其他技能语义相近时明确写上这个技能不会做什么能显著降低模型的混淆概率。这个优化上线后我们把两个技能的混淆错误率从接近 30% 降到了 5% 以下。从那以后每新增一个技能我都会先做一件事把新技能和所有存量技能的 embedding 两两计算相似度凡是相似度超过阈值的必须给出充分的差异理由否则不允许注册。这个机制成了技能注册中心的准入必备流程。3.3 上下文膨胀技能说明书太长把思路都堵住了第三个坑比较隐蔽是随着技能描述精修一起出现的。前面说描述写得越详细越好这是对的但它有一个隐藏代价token 消耗。我们曾经把一个技能的描述写到接近 500 字因为它太重要了我们想尽办法让模型理解它。结果每条路由后的候选技能集是 6 个技能光技能说明书就占了快 3000 token。加上历史对话和系统提示一次请求动辄 6000 到 8000 token。这在本地调试时完全看不出来但到了线上、并发量一上来成本和延迟全炸了。做了一番优化之后我的原则变成了描述信息分层投放。路由匹配阶段使用详细的技能全描述保证检索准确模型决策阶段使用精简版描述只保留能力边界和关键约束控制在 80 到 150 字之间模型调用的 schema 参数详解通过参数本身的 description 字段承载不再额外堆在技能总描述里这样一来模型看到的信息密度反而更高了token 消耗也降回到每次请求 3000 token 以内。最关键的经验是给模型看的技能描述不是一个越详细越好的文档而是一份在一定字数限制内的精准情报。信息浓缩度是设计出来的不是写出来的。4. 一个可落地的轻量技能编排参考实现理论说了不少这里给一份我在项目中反复用过的最小可运行实现。它不依赖任何重型框架用纯 Python 就能跑通适合中小型项目直接套用也适合作为理解 agent-skills 内部机制的起点。4.1 技能注册与 Schema 生成核心思路是用装饰器把普通函数变成可注册的技能并且自动生成 OpenAPI 风格的 Schema。from typing import Callable, Dict, Any import inspect import json class SkillRegistry: def __init__(self): self._skills {} def register(self, nameNone, description, permission_level0): def decorator(func): skill_name name or func.__name__ schema self._generate_schema(func) self._skills[skill_name] { name: skill_name, description: description, function: func, input_schema: schema, permission_level: permission_level, status: active } return func return decorator def _generate_schema(self, func: Callable) - Dict[str, Any]: sig inspect.signature(func) properties {} required [] for name, param in sig.parameters.items(): if param.annotation is not inspect.Parameter.empty: properties[name] {type: self._map_type(param.annotation)} else: properties[name] {type: string} if param.default is inspect.Parameter.empty: required.append(name) return { type: object, properties: properties, required: required } def get_skill(self, name): skill self._skills.get(name) if not skill or skill[status] ! active: return None return skill def all_skills(self): return [ {name: s[name], description: s[description], input_schema: s[input_schema], permission_level: s[permission_level]} for s in self._skills.values() if s[status] active ] registry SkillRegistry() registry.register(description查询指定城市的当前温度和天气状况城市名使用中文) def get_weather(city: str, unit: str celsius): 模拟天气服务 return {city: city, temperature: 25, unit: unit} registry.register(description计算两个数字之间的数学运算支持加、减、乘、除) def calculator(expression: str): 模拟计算器 return {result: eval(expression)}这段代码很简略但已经把注册中心最核心的骨架搭出来了。想直接当生产用还差很多东西比如参数校验、鉴权、监控、链路追踪但作为理解技术原理的起点足够了。4.2 动态候选路由的实现思路路由层的实现可以严格按照前面说的方法来。先把所有已注册技能的描述文档提前生成并编码每次请求进来后对用户 query 做 embedding 和余弦相似度排序取 Top-K。import numpy as np def build_skill_docs(registry): docs {} for skill in registry.all_skills(): doc f{skill[name]}: {skill[description]}, params: {json.dumps(skill[input_schema], ensure_asciiFalse)} docs[skill[name]] doc return docs # ... 假设 emb_model 是一个加载好的 SentenceTransformer 模型 # skill_embeddings 是提前算好的 {skill_name: np.ndarray} def route_skills(query, docs, embeddings, top_k5): q_emb emb_model.encode([query])[0] scored [] for name, doc in docs.items(): score cosine_similarity(q_emb, embeddings[name]) scored.append((name, score)) scored.sort(keylambda x: x[1], reverseTrue) return [name for name, _ in scored[:top_k]]这一步其实很便宜单条查询的 embedding 时间大约几十毫秒完全扛得住常见并发。如果你想更快还可以用向量数据库做 ANN 检索但项目早期没必要整这些花活内存计算完全够用。4.3 执行链路里的硬校验模型返回的 function_call 里带着一个技能名和入参。我这里做的第一件事不是马上去执行函数而是做三重检查def execute_skill_call(registry, skill_name, arguments, allowed_skills): # 第一重技能是否存在于全量注册表 skill registry.get_skill(skill_name) if not skill: return {error: skill_not_found, message: f技能 {skill_name} 不存在或已停用} # 第二重技能是否在当前请求允许的候选集合内 if skill_name not in allowed_skills: return {error: skill_not_allowed, message: f技能 {skill_name} 不在本次调用允许列表中} # 第三重权限检查 if skill[permission_level] 2: need_confirmation True # 参数校验 try: validated_args validate_args(skill[input_schema], arguments) except SchemaValidationError as e: return {error: schema_validation_failed, message: str(e)} return skill[function](**validated_args)这套写法最大的收益不是功能而是把错误返回当成一等公民。模型调用失败、校验不过、参数不对都能返回一个结构化的错误对象。这个对象会被交给语料摘要层最终变成给模型的一句话反馈比如查询失败原因是参数缺失缺少城市名。请向用户确认后再试。这比直接抛一个 TypeError 出去让模型原地发懵强太多了。4.4 如何给模型构造最终输入模型最终看到的是一个精简过的技能候选集。我一般把格式拼成下面这样的消息块{ role: system, content: 你是一个智能助理。以下是你当前可以调用的工具列表。只能调用列表中的工具如果你认为没有任何工具可以解决问题直接告知用户。\n\n 工具1: get_weather\n描述: 查询指定城市的当前温度和天气状况城市名使用中文\n参数: {\type\:\object\,\properties\:{\city\:{\type\:\string\}},\required\:[\city\]}\n\n 工具2: calculator\n描述: 计算两个数字之间的数学运算支持加、减、乘、除\n参数: {\type\:\object\,\properties\:{\expression\:{\type\:\string\}},\required\:[\expression\]} }注意这里的描述长度已经被我压缩过了。精简版描述是一个单独维护的字段不是从注册中心的长描述里直接截断。截断会产生语义残缺单独维护才能保证信息完整但密度高。这一步很琐碎但对生产效果影响极大。5. 想让技能系统能用三年这几个进阶方向得考虑如果只是跑 demo前面四章的内容已经够了。但真实世界的 Agent 是要长期演进、不断叠加新能力的所以技能系统在设计之初就要预留几个进阶方向。5.1 组合技能让 Agent 学会编排而不是每次现跑我的观察是单一技能能解决的问题是有限的真正的价值在于让 Agent 有能力把多个技能组合成一条完整的工作流。比如用户想查一下上个月的订单里有没有出口的订单这个需求本身可能需要三个技能query_orders、query_region_filter、format_report。组合技能不是硬编码工作流而是把组合链路也抽象成一个新的技能。我倾向于用计划-执行-总结三层来组织计划层模型先拆解用户任务判断需要哪些原子技能执行层路由系统根据拆解结果逐一调用原子技能每一步都返回结构化结果总结层把多技能的中间结果汇聚成面向用户的最终答案做了组合技能之后我体会到最大的区别在于Agent 的行为开始变得有层次感。用户的问题不再直接映射到单个函数而是先被理解成业务目标再被翻译成形如先查订单、再过滤地区、再统计金额的执行计划。这种可能会让远离业务的模型更稳定因为它不需要从用户话语凭空跳到一个函数而是通过中间规划缓冲了一层。5.2 技能加缓存高频技能不能每次都不计代价地执行有一个很容易被忽视的事实Agent 技能的调用成本往往不在模型 API 上而在技能本身调用的下游服务上。比如查一个报表底层可能关联数据库查询、数据仓库调度、甚至第三方接口扣费。如果用户问了同一个问题两遍Agent 老老实实把完整链路再走一遍那就是纯浪费。我给高频技能设计了一个轻量缓存方案缓存键由技能名、入参 JSON 的规范化排序、以及上下文指纹三部分组成。TTL 按技能类型不同而不同——查天气可以缓存 10 分钟查用户账户余额最多缓存 30 秒。from functools import lru_cache import hashlib import json def build_cache_key(skill_name, args, ttl_signature): raw json.dumps(args, sort_keysTrue, ensure_asciiFalse) payload f{skill_name}|{raw}|{ttl_signature}.encode() return hashlib.md5(payload).hexdigest()这里的关键不是缓存库怎么选而是缓存的内容形态。缓存的应该是技能的最终结构化结果附带记录缓存命中时的上下文摘要这样即使缓存过期了也能追溯它曾经产生的对话影响。让技能系统有可观测性比给技能增加更多功能更重要。5.3 技能自省让 Agent 动态认识自己的能力边界目前大多数技能系统是外挂式的——注册中心有什么Agent 就会什么。但更理想的状态是技能系统能主动感知自身的能力盲区并在合适的时机给出反馈。我把它称作技能自省Skill Introspection。实现方式不复杂就是给模型一个特殊的list_my_skills技能它允许模型在对话中途检索当前可用的技能列表甚至能查询某个技能的详细描述。当用户提出一个不在现有候选集里的需求时模型可以通过自省确认我的技能库里没有查汇率的能力然后主动告知用户。这比模型基于不完整信息硬猜一个技能去调用要强得多。这个设计也大大减轻了路由的负担。因为有些用户意图本身就是模糊的可能牵扯多个技能与其靠 embedding 一次猜准不如让模型自己通过自省技能做二次确认。实测下来自省机制在处理长尾需求、跨领域问题时的准确率提升非常明显。代价是多了 1 到 2 轮系统内部的交互延迟但换来的是更可靠的决策质量这个买卖划算。5.4 可观测性没有 trace就别谈调优最后一条也是我个人的血泪教训技能系统必须从第一天就埋好可观测性点而不是等功能跑挂了再去补。我通常要求每一个技能的调用生命周期都记录四类数据数据类型关键字段用途路由轨迹query embedding 向量、候选技能 Top-K 分数分析路由偏差决策记录模型最终选择了哪个技能、输出了什么参数分析模型选择逻辑执行明细技能执行耗时、下游状态码、返回体大小定位性能瓶颈纠错日志硬校验拦截了哪些非法调用、纠错后是否成功评估系统加固效果有了这些数据调优就变成了查数据、看分布、下结论的过程而不是拍脑袋。我们经常在 trace 里发现某类业务场景下模型反复选了错误技能从而倒逼出技能描述的重写方向——这比凭空猜模型怎么了靠谱得多。这条实践贯穿始终技能系统的每一次优化都必须有数据支撑。没有 trace 的 Agent 项目就像不带仪表盘的飞行器看着在飞其实随时可能失速。关于 agent-skills我目前的思考和实践大体就是这些。回头再看所有踩过的坑其实都指向同一个核心认知技能系统不是模型的附属品而是决定 Agent 行为质量的骨架工程。把技能注册、路由、隔离、校验和可观测性这五件事想透了Agent 的稳定性和可扩展性自然就上来了。
返回列表