让Agent稳定干活:技能注册表与参数契约设计

📅 发布时间:2026/10/13 18:55:22
让Agent稳定干活:技能注册表与参数契约设计
1. 项目整体设计为什么我给 Agent 装了一套“技能包”如果你最近在折腾大语言模型应用大概率已经被“Agent”这个概念轰炸过一轮了。但真正上手之后你会发现Agent 的骨架搭起来并不难——无非是模型、记忆、工具调用三板斧难的是让这个 Agent 在真实任务里稳定地干活而不是聊着聊着就开始胡说八道。我做的这个“agent-skills”项目本质上就是为了解决这个问题给 Agent 预装一套结构化的“技能包”让它知道自己能干什么、该怎么干、干到一半遇到问题该找谁。这套设计不是从零发明的而是把工程上常见的“模块化 注册表 参数契约”的思路搬到了 Agent 场景里。先说结论把技能做成可注册、可发现、可校验的独立模块Agent 的稳定性会有质的提升。如果你正在做客服机器人、自动化工作流、代码助手或者任何需要调用外部工具的 LLM 应用这套思路可以直接复用。1.1 从 Prompt 模板到技能抽象Agent 开发的核心痛点早期做 Agent 的时候我是踩过坑的。最早的方案特别原始——把所有工具的描述、调用方式、注意事项全部塞进 system prompt然后用 few-shot 示例引导模型调用。小规模跑通没问题但工具一多就出乱子。举一个真实的例子。我做过一个内部运维助手一开始挂了 12 个工具查日志、查指标、发通知、查工单、执行脚本、查配置、查权限……这些工具的完整说明加起来已经能撑满半个上下文窗口。结果模型经常出现三种问题该调“查指标”的时候调了“查日志”需要传时间参数的时候把参数格式写错两个工具都能满足需求时选择完全随机。这说明一个核心问题大模型本身并不天然理解“工具边界”。它只能根据描述去猜而描述一旦含糊猜测就会出错。技能抽象就是为了解决这个问题的——把工具的调用约定、适用场景、参数约束全部结构化让模型在做选择时有据可依而不是靠“感觉”。1.2 技能系统的边界划分什么该进技能库什么不该进在设计技能库之前我花了大量时间思考一个问题到底什么样的能力应该封装成一个“技能”我的标准很简单满足以下条件就值得做职责单一一个技能只做一件事比如“查询用户订单状态”不要做“查询订单 退款 催单”这种复合技能。参数可契约化技能的输入输出能明确描述有清晰的参数结构。可复用同一个技能能在多个流程里被反复使用而不是只服务于一个特定场景。反之一些能力就不适合做成技能——比如需要长上下文理解的推理任务、依赖多轮对话才能确定意图的事情、以及包含大量主观判断的决策类工作。这类任务强行做成技能反而会让 Agent 的调度变得笨重。另外还有一条边界值得注意技能和插件的区别。插件是完整的功能模块技能则是插件内部的一个可执行动作。拿浏览器插件举例插件是“网页数据采集器”技能则是“翻到下一页”、“提取当前页面的标题”、“点击确定按钮”——是更细粒度的能力单元。1.3 技术选型参考主流框架的成熟经验我的方案选型上没有标新立异基本是站在巨人的肩膀上做的。先对比了市面上的主流思路方案方向优点缺点适合场景全量工具说明注入 Prompt实现简单上下文浪费大、易混淆工具数量 5 的原型验证Function Calling 原生支持结构规范、生态成熟灵活度受平台限制快速接入标准模型能力自建技能注册表 动态加载可控性强、扩展灵活开发工作量大中大型项目、跨模型复用我最终选了第三种自建一套轻量级的技能注册和调度机制。原因在于我的目标不是做一个 Demo而是一个能长期演进、多模型复用的框架。技能注册表的好处是模型的部署平台可以随时切换比如从 GPT 切到开源的国产模型技能库本身不去依赖某个具体平台的消息格式这样整套工具不会被厂商锁定。实操心得关于模型选型如果你做的 Agent 相对简单任务类型固定、工具数量不超过 8 个直接用 Function Calling 就够了别折腾自建框架。等你真正开始做跨场景的复杂 Agent 时再考虑自研技能层不迟。我这个项目就是在工具数量爆炸到 20 多个之后才下决心重构的。2. 核心细节解析技能描述、参数契约与上下文管理框架定了之后真正的工作量其实在细节设计上。“agent-skills”这套体系看起来就是“存了一堆函数声明”实际上这里有三个最核心的机制直接决定了 Agent 的命中率和准确性。2.1 技能描述Skill Descriptor决定模型能否选对技能描述是最容易被忽略但最关键的环节——这是模型判断“什么时候该调用什么技能”的主要依据。写得好不好直接决定选择命中率。我的技能描述包含四个部分技能名称name、适用场景description、参数定义parameters、输出说明returns。前面两个好理解后面两个需要展开讲讲。参数定义我直接用 JSON Schema 格式不做过度设计。原因是 JSON Schema 在代码侧容易校验在模型侧也容易理解。描述里最关键的是每个参数的description字段——相信我模型的参数填充准确性和你对参数的描述措辞呈强相关。举个对比模糊写法start_time: 起始时间推荐写法start_time: 查询起始时间格式为 yyyy-MM-dd HH:mm:ss如果不传则默认当天零点必须早于 end_time同样一个参数第二种写法的填充错误率下降了大概六成。原因很简单模型不知道该填什么的时候它会从描述里推断。描述越具体推断越容易命中。输出说明我通常写成一段自然语言描述告诉模型这个技能会返回什么样的数据。这一点的价值在于如果技能返回了模型预期之外的数据它会自己判断要不要换个技能重试。技能描述是一个典型的“磨刀不误砍柴工”的事情。我花了大量时间打磨每个技能的描述特别是对边界情况的限制说明。比如提示技能描述中必须写明“什么时候不该调用”。例如查询类技能要注明“仅提供查询不支持修改操作如果用户表达的是修改需求请调用更新类技能”。这类负向描述能显著减少误调用概率。2.2 技能注册与发现机制不把所有技能塞进 Prompt技能数量一旦超过 10 个全部塞进 Prompt 就不现实了。我采用的是注册表 动态检索的方案核心逻辑是系统启动时所有技能模块通过装饰器或注册函数登记到全局注册表。当用户请求到达时先经过一个“技能预选器”根据用户请求和技能描述做一次粗筛选出最可能相关的 3~5 个技能。只把预选出的技能描述注入到本次调用的上下文中。这个设计借鉴了信息检索里的粗排-精排思路。粗筛阶段可以用 embedding 相似度计算也可以写简单的关键词匹配规则。我的实操经验是初期先用规则做粗筛就够用后续再逐步替换成向量检索效果更好。对动态检索而言关键点是技能数量别硬往上限顶。如果某个用户请求命中了超过 8 个技能大概率是技能粒度太粗应该拆。另外预选器本身需要保证召回率优先——宁可多选几个无关的也别漏掉真正需要的。漏选的代价远大于误选。2.3 技能执行体与依赖注入的设计技能描述只是“说明书”真正干活的是技能的执行函数。这一层我的核心设计是依赖注入。每个技能的执行函数只负责接收标准化的输入参数并返回标准化的输出结构至于外部依赖比如 HTTP 客户端、数据库连接、Redis 实例全部通过注入方式传入。这样设计有几个好处可测试性单测时可以直接注入 mock 对象不用真的连数据库。可替换性同一个技能可以接不同的数据源改配置就行。上下文隔离技能之间不会共享可变的内部状态减少互相干扰。看一个具体的例子。假设我有一个“查询用户账户余额”的技能skill_register def get_balance(user_id: str, currency: str CNY) - dict: 查询指定用户的钱包余额。 当用户询问“我还有多少钱”“余额是多少”时使用。 client container.resolve(payment_client) try: data client.get_balance(user_id, currency) return {code: 0, data: data, message: ok} except PaymentServiceError as e: return {code: 1001, data: None, message: f查询余额失败: {str(e)}}你注意到区别了吗函数内部不直接import一个全局客户端而是从container中解析。这就是依赖注入的实际应用。好处体现在测试的时候——我可以轻松换一个 mock 客户端。2.4 技能调用的上下文管理机制Agent 的一个天然弱点是“记性不好”。技能调用过程中产生的中间结果、历史对话摘要、外部数据都需要统一管理。我把上下文分为三层会话级上下文记录用户与 Agent 的多轮对话摘要保存在内存或 Redis 中。任务级上下文当前任务执行过程中技能之间传递的数据比如“用户查询了订单 A又要求退款”需要把订单信息从上下文里捞出来接力。技能级上下文单个技能执行时的输入输出历史主要用于排错和日志审计。上下文管理的核心原则是“能用结构化数据就不用自然语言”。技能执行完返回的数据我会把它结构化存起来而不是把模型生成的回复原文塞进上下文。这样后续技能读取时不需要再从一大段文字里做信息抽取——既能节省 Token又能降低出错率。这个设计我在前期吃过亏。最早为了省事直接把 Agent 的完整回复作为上下文传递结果第二个技能需要订单号时模型要从已经生成过的自然语言里“猜测”订单号。后来改成结构化传递之后这个环节的错误率直线下降。3. 实操过程从零实现一个轻量级 agent-skills 库理论说了一大堆下面直接上实操。我会用一段可运行的 Python 示例代码带你走完从定义技能到跑通全程的关键步骤。3.1 定义技能基类和注册装饰器第一步要做的是技能的基类和注册机制。基类定义了技能的基础属性装饰器负责把技能登记到全局注册表。from dataclasses import dataclass, field from typing import Callable, Dict, Any, List import inspect import json dataclass class Skill: name: str description: str parameters: Dict[str, Any] required: List[str] returns_schema: Dict[str, Any] field(default_factorydict) handler: Callable None tags: List[str] field(default_factorylist) class SkillRegistry: def __init__(self): self._registry: Dict[str, Skill] {} def register(self, skill: Skill) - None: if skill.name in self._registry: raise ValueError(f技能 {skill.name} 已经被注册过了) self._registry[skill.name] skill def get(self, name: str) - Skill: return self._registry.get(name) def all(self) - List[Skill]: return list(self._registry.values()) # 全局注册表 registry SkillRegistry() def skill( name: str, description: str, parameters: Dict[str, Any], required: List[str], returns_schema: Dict[str, Any] None, tags: List[str] None, ): def decorator(func): skill_obj Skill( namename, descriptiondescription, parametersparameters, requiredrequired, returns_schemareturns_schema or {}, handlerfunc, tagstags or [], ) registry.register(skill_obj) return func return decorator这段代码里有两个细节值得注意。一个是注册时检查重名——技能名称重复是一个隐藏的深坑如果两个技能同名后面的会把前面的覆盖掉排错时非常难发现。另一个是required和parameters单独拆开因为模型可能只填了有效参数的一部分这给后续的参数校验提供了条件。3.2 技能预选器从全量技能中选出最相关的几个注册表建好了接下来实现预选器。这个模块的职责是输入用户请求输出该请求最可能用到的技能子集。前期默认用关键词匹配加入少量规则。def pre_select_skills(user_query: str, max_skills: int 5) - List[Skill]: 根据用户请求粗选技能早期版本不需要复杂算法。 candidates [] query_lower user_query.lower() for item in registry.all(): score 0 # 匹配技能名称 if item.name.lower() in query_lower: score 10 # 匹配描述中的关键词简单分词后交叉计数 desc_words set(item.description.split( )) for word in desc_words: if word.lower() in query_lower: score 1 if score 0: candidates.append((score, item)) candidates.sort(keylambda x: x[0], reverseTrue) return [item for _, item in candidates[:max_skills]]这个粗选器很粗糙但对启动阶段已经足够。它的价值在于把候选集缩小为后续精排减少计算压力。你完全可以把这段逻辑替换成向量检索——先对全量技能做 embedding 预计算再用用户请求的 embedding 做相似度 top-k。前期用规则后期上向量这样的演进路径不会浪费你的工作量。我当时先用规则跑了两个星期积累了足够的真实用户请求和对应的技能命中情况。之后把这些历史数据作为训练集去调向量检索效果比直接凭空调要好得多。3.3 模型调用与参数提取预选出技能子集后下一步是交给模型做“选择和参数填充”。我用的方式是构建一个紧凑的 system prompt只包含候选中技能的描述。def build_call_prompt(user_query: str, skills: List[Skill]) - str: system_parts [ 你是一个智能助手请根据用户请求选择最合适的技能并提取参数。, 只从给定的技能列表中选择不要编造不存在的技能或参数。, , 可用的技能如下, ] for i, item in enumerate(skills): system_parts.append(f[{i 1}] {item.name}) system_parts.append(f描述{item.description}) system_parts.append(f参数{json.dumps(item.parameters, ensure_asciiFalse)}) system_parts.append(f必填参数{item.required}) system_parts.append() system_parts.append(请以 JSON 格式输出格式为 {\skill\: \技能名\, \arguments\: {参数名: 值}}。) return \n.join(system_parts)这里有一个特别重要的设计不给模型提供技能描述之外的任何工具调用方式。也就是说模型只能从列出的候选里选不能自己“发明”新函数。这能最大化避免模型脑补不存在的技能。填充参数时还需要做一轮后处理校验。模型输出的 JSON 不一定完全符合 Schema——最常见的问题是缺少必填项、参数名大小写写错、数字被写成字符串。我用一段简单的校验函数兜底def validate_arguments(skill: Skill, arguments: dict) - dict: errors [] for req in skill.required: if req not in arguments or arguments[req] in (None, , []): errors.append(f缺少必填参数: {req}) # 类型转换schema 里声明为 number 的参数允许从字符串转 for key, value in arguments.items(): if key in skill.parameters: schema_type skill.parameters[key].get(type) if schema_type number and isinstance(value, str): try: arguments[key] float(value) except ValueError: errors.append(f参数 {key} 无法转换为数字) if errors: raise ValueError(; .join(errors)) return arguments这段校验脚本看起来很简单但线上的价值非常大。模型输出的格式五花八门而且同一个技能在不同模型上输出风格的差异也很大。没有这层校验后续的执行函数会经常因为拿到奇怪的数据类型而崩溃。3.4 技能执行与结果合成参数校验通过后进入执行阶段。执行逻辑很简单就是调用注册好的 handler 函数。但这里有一个细节容易被忽略技能返回值要不要经过二次加工才给模型生成回复。我的方案是技能返回值先做结构化存放生成一个“执行摘要”给模型而不是把完整原始数据全扔给模型。举例来说查询订单接口可能返回一个包含大量字段的 JSON但用户实际上只关心“订单是否已发货、预计什么时候到”。因此执行摘要要把关键信息提取出来def run_skill(skill_name: str, arguments: dict) - dict: skill_obj registry.get(skill_name) if not skill_obj: return {code: 404, data: None, message: f技能 {skill_name} 不存在} try: validated validate_arguments(skill_obj, arguments) result skill_obj.handler(**validated) # 结构化存结果统一包装成固定格式 return { code: 0, data: result, message: 成功, skill: skill_name, arguments: validated, } except Exception as e: return {code: 500, data: None, message: f执行技能 {skill_name} 出错: {str(e)}}统一的返回结构非常重要。模型通过 code 字段判断这个技能是否执行成功如果返回 500它可以自主决定换个技能尝试而不是强行基于错误信息编一套回复出来。这个设计直接遏制了模型“死鸭子嘴硬”的行为。3.5 多技能串联让 Agent 学会使用“工具链”单个技能能做的事情有限真正的 Agent 需要能串联多个技能完成复杂流程。我在“agent-skills”里做了一层简单的编排机制——让模型在返回技能选择的同时可以附带声明“下一步计划”。例如用户请求“查询最近一周的订单并把总金额统计一下”这需要两个技能配合订单查询 金额汇总。我的编排处理方式是循环把用户请求交给模型模型选择技能 A订单查询并填充参数。执行技能 A把结果放入上下文。把上下文和原始请求再次交给模型模型看到订单数据后选择技能 B汇总并填充参数。执行技能 B把结果交给模型生成最终回复。这段循环本质上就是 ReAct 模式的简化版。每一步执行后模型都能看到“技能返回了真实数据”从而更准确地决定下一步动作。这个模式我在测试中发现比让模型一次规划完整步骤要稳定得多。注意循环不是死循环。每次调用都会带上累计步数限制超过 6 步还没有拿到用户想要的最终结果我会让 Agent 主动承认“这个问题太复杂了建议分步处理”而不是无限套娃。这一步在线上非常必要——某些用户请求很容易触发无终止的循环调用。4. 常见问题与排查技巧实录这块进入正题。我在真实使用这套机制时遇到的典型问题还不少这里挑四个最有代表性的讲讲每一个都附了排查思路和解决方案。4.1 技能选择命中率偏低模型老选错技能这是最开始遇到最多的问题。现象是工具 A 和工具 B 功能相近用户说“帮我看看订单到哪了”模型有多大几率选了“查询订单物流”而不是“查询订单状态”。排查思路是检查技能描述是否足够区分。我最初把两个技能的描述都写在同一个粒度上——“查询订单信息”这一看就知道模型根本没法学。后来我把描述改成查询订单物流当用户需要了解包裹运输进度、快递轨迹、配送到达时间时使用。查询订单状态当用户需要了解订单当前所处阶段、是否已支付、是否已发货等业务状态时使用。改完之后命中率提升了三成还多。描述的区别度就是技能之间的边界。别怕写长重要的是让模型看见明确的“什么情况用哪个”的信号。如果改了描述还是选不对还有一个技巧是给相似技能加负向描述。比如在物流技能里加一句“如果用户只关心订单是否支付请勿使用此技能。”这种写法对模型约束效果极好。4.2 参数提取错误模型把用户没有给的信息“脑补”进去另一种高频问题是参数幻觉。比如用户说“查一下订单 12345”模型自作主张把start_time也填了昨天。这种情况很让人头疼因为参数本身看起来合法但完全不是用户的意图。我的处理是用“可选参数的默认值策略”。所有非必填参数都不要求模型主动填充而是靠执行函数侧取默认值# 技能函数内部做兜底 def get_order_detail(order_id: str, start_time: str None, end_time: str None): if start_time is None: start_time today_zero() if end_time is None: end_time now()这样模型只需要填它确定的信息不确定的可以留给默认值避免凭空捏造。同时在描述里明确标注“用户没有提到的参数一律不要填。”这一句约束在多个模型上实测下来降低参数幻觉的效果相当显著。4.3 技能返回数据格式不统一模型读不懂返回内容这是在技能数量上去之后才暴露的问题。不同技能由不同的人开发返回格式各写各的——有的返回{status: ok}有的返回{success: True}还有的返回裸字符串。模型拿到这些五花八门的返回结果后特别容易迷失方向。解决的办法就是我在第 3.4 节提到的统一返回包装。所有技能的外部表现都走同一个函数强制执行统一格式。这一步不能省否则技能越多模型越容易“精神分裂”。另外还要注意一个细节返回给模型之前要做一个“面向用户的摘要”。有些技能返回了大量日志型数据几十条流水模型不会去逐条计算它可能会忽略关键数字。更好的做法是技能内部直接把“本次调用最核心的结果”提取成 1~2 句话连同结构化数据一起返回。这相当于给模型划了重点。4.4 多技能串联时上下文互相污染多技能串联之后发现一个有意思的问题完成第一步查询之后第二步汇总时模型把第一步返回的原始信息又听进去了产生幻觉。比如“订单物流显示已签收为什么你汇总的时候还说在运输中”。原因是上下文里保留了多轮技能调用的原始返回模型容易把最早的结果当成当前事实。解决办法是每次技能执行完成后只保留当前技能的执行摘要和最终结果旧技能的详细返回在进入下一轮之前就压掉。用术语说这是“上下文压缩”。实现上不复杂——上一轮结果做摘要后用一个 short-term memory slot 保存当新步骤开始时将其从模型上下文中移除。这需要一个独立的上下文管理器来调度而不是把所有内容一股脑塞进一个地方。class ContextManager: def __init__(self): self.current_skill_result {} self.original_user_query self.history_summary [] def compress(self): # 把上一次技能结果压缩为一行摘要 if self.current_skill_result: brief ( f上一技能 {self.current_skill_result[skill]} 已执行完成、 f结果为{json.dumps(self.current_skill_result[data], ensure_asciiFalse)[:200]} ) self.history_summary.append(brief) self.current_skill_result {}压缩之后模型每次看到的上下文焦点都集中在当前这一步不会被历史数据带偏。5. 进阶扩展技能版本、权限与资源控制基础版本跑通之后你会慢慢碰到一些更深层的问题——技能要迭代要控制访问权限要避免资源损耗。这些内容可能不是 MVP 阶段必须的但长期运营时绕不开。5.1 技能版本管理与灰度策略技能描述和实现会持续优化。问题在于新版本和老版本不能简单覆盖——万一新版本描述反而变差了会影响线上所有请求。我采用的做法是给技能注册表加上版本字段每次修改都以新版本注册由调度器决定使用哪个版本skill( namequery_order, version2.0, description查询订单基础信息。当用户询问订单状态、金额、支付信息时使用。, ) def query_order_v2(order_id: str): # 新的实现 pass配合一个简单的灰度开关按流量比例把一部分请求路由到新版本。这个设计参考了后端的灰度发布思路在不中断服务的情况下验证技能改动效果。实测下来很有用——有一次我把某个技能的描述改得更具体了结果整体表现反而变差因为有旧版本顶住了流量问题才没有扩大。5.2 技能权限控制与审计日志如果你的 Agent 要接入真实业务系统权限控制不可回避。不能让模型根据用户一句话就触发管理员操作。我设计了一个轻量的权限层技能注册时声明permission_level比如read、write、admin。会话建立时绑定用户的角色和权限列表。调度器在调用技能前先检查权限不满足则拒绝执行并告知用户“你没有权限执行这个操作”。审计日志同样重要。每次技能调用都会记录用户 ID、会话 ID、技能名、参数、返回结果、模型名、Token 消耗。这个日志不仅用于追溯问题还是分析哪些技能被频繁调用、哪个技能经常出错的基础数据源。没有日志就没有优化依据。5.3 技能超时、重试与降级处理技能执行体是真实的外部调用大概率会遇到超时、限流、第三方服务宕机等情况。线上运行时间长了你会发现外部接口不稳定才是常态。给执行层加上超时控制和重试机制非常关键from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type retry( stopstop_after_attempt(3), waitwait_exponential(multiplier1, min2, max10), retryretry_if_exception_type(TimeoutError), ) def call_external_api(url, payload): # 外部HTTP调用 pass重试之外我还给技能设计了降级路径。比如“发送邮件通知”这个技能如果邮件服务挂了可以降级为“把通知内容写入消息队列等待恢复”。降级逻辑写在技能内部不用让模型知道——对模型来说它只看到调用成功。安全提示降级方案不能无条件使用。涉及到支付扣款、资源删除等敏感操作一律不允许自动降级宁可直接失败也不能用模糊的策略绕过去。这个原则要写死在框架里不能依赖技能开发者自觉。6. 我在实际项目里踩过的坑最后分享几个真正困扰过我的细节问题这些坑在文档里很难找到答案但线上踩到之后会特别费时间。6.1 Prompt 里技能过多导致模型“注意力稀释”早期我为了求稳预选器选了 8 个技能进 Prompt。结果模型的表现反而比只用 3 个技能更差。原因很好理解——当可选范围变大模型在每个选项上的“注意力权重”会被稀释决策质量下降。实测下来5 个技能以内表现比较稳定超过 5 个就会出现选择犹豫和错误率上升的趋势。所以预选器的max_skills参数我最终定在 5没有再调高过。宁可召回率略低一点也要保证选择质量。6.2 技能函数不该偷懒写长注释我见过一个技能函数内部注释写得像小作文结果这些注释全被带到了 Prompt 里。你以为注释只给程序员看不prompt 拼接时如果直接把函数体当描述用注释也会变成模型“参考信息”的一部分轻则浪费 Token重则给模型传递错误信号。所以技能的对外描述和函数的内部注释一定要隔离。对外描述放在description字段里精写函数内部的注释只服务开发者自己。6.3 模型输出 JSON 的容错技巧模型输出极不稳定。同一个模型上午输出的 JSON 是干净的下午就可能在 JSON 前后加一段“好的以下是结果”。我用了一招容错解析def safe_json_loads(text: str) - dict: text text.strip() # 去掉可能的 markdown 代码块包裹 if text.startswith(): text text.strip() if text.startswith(json): text text[4:] # 找到第一个 { 和最后一个 } 之间的区间 start text.find({) end text.rfind(}) if start -1 or end -1: raise ValueError(未找到 JSON 对象) return json.loads(text[start:end 1])这个方法不保证百分百纠正但能解决九成以上的格式抖动。核心思路是截取最像 JSON 的子串再解析而不是直接把整段文本扔给json.loads。6.4 技能注册表的测试策略技能数量多了之后注册表本身的正确性也可能出问题。比如某个技能描述里写的关键词和另一个技能完全重复或者某个必填参数的 Schema 写错了。我建议写一套自动化的“注册表自检”脚本在每次部署时执行检查技能名称唯一性。检查所有必填参数是否在 parameters 里都有定义。检查技能描述是否超过长度限制。检查返回 Schema 是否合法 JSON。这个过程花不了多少时间但能杜绝一批低级错误。特别是多人协作开发技能时自检脚本几乎是唯一能守住质量底线的工具。写在最后的个人体会这套“agent-skills”从最早的纯 Prompt 方案演化到现在的技能注册表加动态调度结构中间改了大概三次版本。每次重构的触发点都不是“想用新技术”而是线上数据告诉我哪里卡住了——一开始是工具多了模型分不清后来是参数乱填再到后来是多技能串联互相污染。每一个问题都是先观察数据、再涉及方案的顺序推进不是拍脑袋提前设计的。如果你想在自己的项目里复刻这套方案我建议不要一上来就想着“全量技能 完美调度 复杂上下文”。先挑两三个最核心的技能跑通整个链路——注册、预选、调用、校验、返回包装——然后再逐步加技能。这个节奏会顺畅很多。另外技能的迭代是一个持续过程。我的技能描述平均每隔几周就会根据真实用户请求调整一次。模型在变用户在变技能描述也永远没有“最终版”。保持迭代心态在这条路上比什么都重要。