ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

从零搭建Agent技能体系:注册、调度与生命周期管理实战

2026/9/24 23:27:50 拓冰建站 浏览量
从零搭建Agent技能体系:注册、调度与生命周期管理实战 最近在折腾 Agent 应用时我越发觉得一个被反复低估、却又决定成败的关键模块就是技能体系设计。项目代号agent-skills听起来像是一个简单的工具集合但实际上它是在解决一个非常具体且头疼的问题如何让 Agent 稳定、可靠、不抽风地完成真实业务动作。今天这篇文章我就把从零搭建 Agent 技能体系的完整思路、踩坑记录和最终落地方案整理出来分享给正在做 Agent 应用、或者准备让 Agent 接手实际任务的开发者。内容会比较长但都是实打实的项目经验和可复用的代码逻辑不是概念科普。1. 技能体系的设计思路先别急着写代码把边界想清楚做 Agent 应用最怕什么最怕一上来就写一堆工具函数然后让大模型随便调用。前期看起来没问题一旦技能数量超过十个模型开始乱选工具、参数传错、上下文被工具日志撑爆的场景会越来越频繁。agent-skills项目的核心出发点就是给 Agent 的技能调用做一套清晰、可控、可维护的标准化方案。1.1 为什么需要“技能”这一层抽象在项目初期我们也是传统的 function calling 路子把所有可执行操作直接映射成大模型 API 里的 functions 列表。这种方式在 demo 阶段没有问题但进入真实业务后暴露了三个致命问题。第一函数命名和描述完全依赖开发者的临时发挥。A 同学写的函数叫get_user_infoB 同学写的函数叫fetch_customer_data功能高度重叠。大模型面对两个描述含糊、功能相近的函数时经常选错。第二参数结构不统一。有的函数用 snake_case有的用 camelCase有的直接用一长串 JSON 字符串作为参数。大模型虽然能容忍一定程度的不规范但输出错误参数的频率会显著上升。第三函数直接裸露给大模型存在严重的安全隐患。如果不加控制地允许 Agent 调用一切函数一个 prompt injection 就可能让 Agent 执行危险操作。技能Skill概念的引入就是在这三者之间建立一道中间层。每个技能是一个自包含的执行单元它有明确的名称、清晰的描述、标准化的参数 Schema以及经过封装的内部实现。大模型只面对技能层看不到底层函数的具体实现细节。这就像给 Agent 配了一本标准化的操作手册而不是让它直接面对一堆底层 API 文档。1.2 技能与工具、工作流之间的边界很多人会混淆技能、工具和工作流这三个概念。我见过不少项目把三者揉在一起最后弄出一个四不像的调度系统。在实际设计中我习惯用三个问题来界定它们。技能是最小的可复用动作单元。它回答的问题是谁、在什么条件下、执行了什么操作、得到什么结果。比如“根据用户ID查询订单列表”就是一个技能。工具更偏底层指具体的 API 或函数调用。一个技能可以封装多个工具调用。工作流则是一系列技能的有序组合解决的是一个复杂的业务场景。比如“用户下单流程”可能由“查询库存”、“创建订单”、“发起支付”、“发送通知”四个技能按顺序组成。理解这个边界后面做调度策略会轻松很多。agent-skills的设计里技能是调度器直接面对的可操作对象每个技能内部可以自由调用多个工具但工具本身不暴露给大模型。工作流则在技能之上做编排可以用显式定义也可以让大模型动态规划。1.3 设计原则简单、可控、可观测经过三个版本的迭代我总结出技能体系设计的四条核心原则后续所有代码和配置都围绕这四条展开。第一简单至上。技能对外暴露的接口不能超过三个参数如果超过就说明技能粒度太粗需要拆分。第二明确定义。每个技能的描述必须包含场景、输入、输出、失败条件四个要素缺一不可。第三最小权限。技能只能访问它完成任务所必需的资源和数据不允许共享 Session、全局变量或者跨技能的状态。第四全程可观测。每个技能的调用开始、结束、异常都必须有日志和指标记录不能有黑盒操作。这四条原则看似简单但真正贯彻到代码里需要持续自律。尤其是第二条描述写得好不好直接影响大模型的选择准确率这个我们在后文会详细展开。2. 技能注册与发现机制让 Agent 在正确的时间找到正确的技能技能设计好之后下一步就是解决“怎么被找到”的问题。这一步是整个技能体系的骨架。如果技能无法被 Agent 高效、准确地发现和选中那再强的执行能力也发挥不出来。2.1 技能注册表一切的起点在agent-skills中所有技能在启动时都注册到一个统一的内存注册表中。注册时的关键信息包括技能名、技能描述、参数 Schema、版本号、权限级别和处理器引用。我用一个简单的 Python 字典结构来存储# skill_registry.py from dataclasses import dataclass, field from typing import Any, Callable, Optional dataclass class SkillMeta: name: str description: str parameters_schema: dict version: str 1.0.0 enabled: bool True timeout_seconds: float 10.0 required_permission: str user handler: Optional[Callable[..., Any]] None class SkillRegistry: def __init__(self): self._skills: dict[str, SkillMeta] {} def register(self, meta: SkillMeta): if meta.name in self._skills: raise ValueError(fSkill {meta.name} already registered) self._skills[meta.name] meta def unregister(self, name: str): self._skills.pop(name, None) def get(self, name: str) - Optional[SkillMeta]: return self._skills.get(name) def list_skills(self, enabled_only: bool True): if enabled_only: return [s for s in self._skills.values() if s.enabled] return list(self._skills.values())注册表使用单一数据源避免多处维护导致的同步问题。启动时通过装饰器自动注册后续动态加载技能包也只操作这个注册表。这就像一个公司的通讯录每个技能是员工注册表就是 HR 系统调度器就是行政部门要找人的时候先查通讯录。2.2 技能描述如何写才能让模型少犯浑技能描述是直接喂给大模型的中文文本它的质量直接决定了意图识别的准确率。我在实战中总结出一个公式技能描述 功能定义 适用场景 输入说明 典型触发条件举一个反例和正例的对比。反例查询用户信息输入用户ID返回用户基本信息。这个描述非常模糊大模型不知道什么时候该用这个技能。是用户直接问“我的账号余额”时调用还是骂骂咧咧说“你们平台怎么回事”时调用没有边界。正例根据用户ID查询用户基本信息包含姓名、手机号、注册时间、会员等级和最近登录时间。 适用于用户主动询问个人资料、客服需要核验用户身份、运营查看用户画像等场景。 输入参数为user_id字符串类型必填格式为u_开头加数字例如u_12345。 当用户问我什么时候注册的查一下我的手机号或者客服说帮我查一下这个用户时优先考虑调用此技能。正例包含了具体的字段名和示例值大模型对具体示例的敏感性远高于抽象描述。强烈建议每一个技能描述都带一个参数示例值这能显著提升参数填充的正确率。不过描述也不宜过长大模型上下文有限最佳长度在 100 到 200 个汉字之间。2.3 技能分组与命名别只靠扁平列表当技能数量超过 30 个时扁平列表会让大模型的注意力分散。在一些复杂 Agent 场景中模型会在无关技能组间来回试探导致响应延迟。我的解决思路是增加分组层。每个技能有一个category字段同时注册表维护一个分组索引。调度时先通过一次意图粗分类确定技能组再在组内精确定位具体技能。# category: user_info, order, payment, product, logistics, customer_service group_index: dict[str, list[str]] {} def build_group_index(self): self.group_index.clear() for skill in self._skills.values(): self.group_index.setdefault(skill.category, []).append(skill.name) def list_skills_by_category(self, category: str): names self.group_index.get(category, []) return [self._skills[n] for n in names if self._skills[n].enabled]这里对技能分组枚举做了严格限定而不是让开发者随意填。分组数量控制在 6 到 8 个以内每个组内技能数量不超过 10 个。这样大模型每次只面对一小撮候选技能选择准确率能有明显提升。3. 调度链路的三大关键环节从意图到参数再到执行技能注册好了描述写得再完美最终还是要靠调度链路来真正执行。这一节是整个系统的大脑和神经网络涉及大模型调用、参数映射和执行安全三个方面。我在这个环节调试的时间最久也发现了一些系统性规律。3.1 初步意图识别与候选技能召回在把全部技能灌给大模型之前先进一次快速分类。我用轻量分类模型或者纯规则先把用户 query 归入某个技能组这样大模型调用时只需携带组内技能列表。这种方式既能减少 token 消耗又能集中大模型注意力。规则召回的逻辑很简单每个技能定义一组高频触发词。比如用户 query 里出现“密码”“登录不上”“账号锁了”就会优先召回账号安全相关技能。不过这里要注意单纯靠关键词会误伤所以召回只是候选集生成不代表最终选择。最终决策权依然在大模型手里。我采用两层召回第一层是粗粒度的规则召回第二层是把规则召回结果和 query 的语义向量放进一个轻量模型里做排序取 top-K 个技能进入大模型候选列表。K 控制在 5 到 8 个。这个做法的好处是大幅降低大模型在无关技能间犹豫不决的概率。# skill_router.py def recall_skills(query_text: str, registry: SkillRegistry, top_k: int 5): scores {} for skill in registry.list_skills(): if skill.category ! classify_intent(query_text): continue score rule_match_score(query_text, skill.trigger_keywords) if score 0: scores[skill.name] score ranked sorted(scores.items(), keylambda x: x[1], reverseTrue)[:top_k] return [registry.get(name) for name, _ in ranked]classify_intent函数理想情况下是语义模型但小项目里用规则命中也能完成大部分工作。实际调优时我会对照日志重点看哪些用户 query 经常落在错误分组里再用热词修正。3.2 参数提取与校验堵住最常见的报错来源如果说技能选错是偶发问题那参数错误就是高频问题。大模型返回的参数格式不稳定即使我们在 Schema 里定义了类型和必填项输出还是可能出幺蛾子比如缺 key、整数被转成字符串、枚举值写错等等。我在参数处理上做了三道防线。第一道防线定义严格的参数 Schema靠 Pydantic 或 JSON Schema 做强校验。每个技能的参数定义都必须声明类型、是否必填、枚举范围和默认值。第二道防线对必填参数做缺失补偿。如果大模型漏掉了必填参数调用层不直接返回错误而是向大模型回传一个缺失参数指令让它补充。第三道防线类型自动转换。比如在技能里定义user_id是字符串但大模型返回了一个数字自动转成字符串。参数校验是伪代码化的工具真实运行时我强烈建议使用 Pydantic# skill_params.py from pydantic import BaseModel, Field class GetUserInfoParams(BaseModel): user_id: str Field( ..., patternr^u_\d$, description用户ID格式为u_加数字例如u_12345 ) include_orders: bool Field( False, description是否同时返回最近的订单记录默认不返回 )注意这里给user_id加了正则校验这是一条很关键的设计在参数里面就写明格式要求当大模型返回的内容匹配不到正则时立刻重新请求补参或纠错而不是等技能执行到一半才报错。3.3 技能执行超时、重试与安全拦截技能执行器是整个调度链路的最后一棒。它负责真正调用技能处理器并且在调用前后做一系列安全和控制动作。执行前要检查技能是否启用、当前对话上下文是否具有技能要求的权限级别。执行中要设置超时时间因为大模型经常超时。不同技能的执行时间差别很大查询类技能 2 秒超时文件处理类技能可能 30 秒写操作类技能我建议严格限制在 5 秒以内避免不可控操作占用资源。有了超时保护之后还要考虑重试策略。不是所有技能都适合无脑重试。查询类技能遇到超时可以重试一次但扣款或发消息这种操作类技能除非实现幂等否则禁止自动重试宁可报错人工介入也不能重复执行。执行后要立刻做结果封装和日志记录。结果统一包装成一个SkillResult对象包含状态码、数据或错误信息、执行耗时。日志除了记录数据还要埋点记录这个技能是哪次对话、哪条意图触发的这些数据对后续做评估集非常有用。# skill_executor.py import time import logging from concurrent.futures import ThreadPoolExecutor, TimeoutError logger logging.getLogger(agent.skill) class SkillExecutor: def __init__(self): self._pool ThreadPoolExecutor(max_workers8) def execute(self, skill: SkillMeta, params: dict): if not skill.enabled: return SkillResult.error(skill_disabled) start time.monotonic() future self._pool.submit(skill.handler, **params) try: result future.result(timeoutskill.timeout_seconds) elapsed round((time.monotonic() - start) * 1000, 2) logger.info(fskill{skill.name} statusok elapsed_ms{elapsed}) return SkillResult.success(result, elapsed) except TimeoutError: logger.error(fskill{skill.name} statustimeout) return SkillResult.error(timeout) except Exception as exc: logger.exception(fskill{skill.name} statuserror exc{exc}) return SkillResult.error(str(exc))这段代码默认多线程执行技能函数。要注意的是ThreadPoolExecutor虽然共享进程内变量但技能内部如果操作了共享状态并发时还是可能出安全问题所以技能内部的所有可变状态都应该是它自己的局部变量不要用全局变量。4. 技能的生命周期管理从开发到废弃的全链路闭环技能体系一旦跑起来就不会是一成不变的。新业务要加新技能旧技能要调整描述偶尔还要下架不用的技能。这一节讲的是技能上线之后的管理问题也是实际运营中工作量最大的部分却被很多项目忽略。4.1 开发态本地调试与沙箱联调我在本地开发技能时用了一套很轻量的 CLI 工具可以单独调用某个技能而不需要启动整个 Agent。输入一个 JSON 格式的参数就能直接看到返回结果。这套调试流程能省下大量时间因为大模型链路走一次至少三五秒而直接本地调试毫秒级返回。调试完之后再做一次端到端联调确认技能在真实 Agent 调度中能被正确选中、正确传参、正确返回。联调时我习惯先让 Agent 用自然语言描述场景确认它能选对技能。比如输入“帮我查下订单 zx123456 的物流”看模型是否选中track_order参数是否传成order_idzx123456。这一步不光是验证技能本身也是在验证技能描述写得是否清晰。4.2 测试态用评估集把回归问题挡在门外技能多了之后最常见的问题是改 A 技能导致 B 技能调用出错。为了挡住回归我在项目里构建了一个小型评估集。每个技能至少准备 10 条测试用例覆盖正常场景、边界场景和异常场景。评估集的数据来自两部分一部分是历史上真实用户跟 Agent 的对话另一部分是测试时伪造的边界 query。每条用例标注了期望选中的技能和期望参数。这样每次更新技能描述或者调整注册表逻辑都能跑一遍评估集快速定位影响范围。评估集格式如下[ { query: 我要取消昨天下的订单 87654321, expected_skill: cancel_order, expected_params: {order_id: 87654321}, context: user_intentafter_sales }, { query: 最近有没有新出的轻薄本, expected_skill: search_product, expected_params: {category: laptop, sort: newest} } ]执行评估时主要看两个指标技能选择准确率Correct Skill Selection和参数填充准确率Parameter Accuracy。我在实践中发现技能描述精修一次选择准确率能提升 10 到 15 个百分点有时候比换大模型更见效。4.3 灰度发布与快速回滚技能发布的灰度策略我参考了常规服务发布方式但没有做得太重。更低成本的做法是给技能加一个版本字段新版本技能先在内部对话渠道跑一天收集日志确认调用成功率高于 95%、平均耗时无明显增加才全量开放。回滚操作要做到秒级。在注册表里version字段和handler紧密绑定一旦发现异常直接修改注册表把 handler 指回上一个稳定版本。整个操作不需要发布服务。另外还有一个很重要的技巧技能下架前先置为enabledfalse不要直接删除。这样已产生的会话还能拿到上下文调度器也知道这个技能暂时不可用而不是直接报错找不到。观察一周没有异常后再彻底删除注册表项。5. 常见问题与排查技巧把线上踩过的坑一次说清楚技能体系上了生产之后会遇到各种各样的问题。这一节整理出我在agent-skills项目里真实处理过的几类高频问题每一条都有排查思路和解决方案。5.1 大模型选错技能排查思路是什么选错技能是最常见的线上问题。用户说“我要退货”Agent 却调用了“查询订单”技能。遇到这种情况先从三个层面排查。先看技能描述是否准确尤其查描述里有没有覆盖用户常用的口头表达。再查召回环节在日志里确认用户 query 是否被正确归组了如果归到了错误的技能组那就是分类逻辑问题。最后检查候选技能列表如果候选列表里有多个功能相近的技能大模型确实容易选错这种情况要合并技能或者补充区分性描述。我的经验是大多数人卡在第二层。特别是意图分类规则里如果只写了“退货”这样一个关键词用户说“我不想要了”根本匹配不上。把触发词扩充成“退货、退款、不想要、申请售后”等常见变体后问题立刻缓解。5.2 技能参数永远填错怎么办当大模型总是把“当前时间”理解成“下单时间”或者把金额单位搞混这就说明参数 Schema 的定义不够严谨。我排查这类问题的顺序是先看有没有给参数加正则再看有没有给参数加默认值接着看技能描述里有没有给参数示例。这里分享一个特别管用的做法在参数 Schema 里加examples字段把期望的正确值写进去。大模型对示例的敏感度远高于规则描述。比如金额字段写清楚“单位是元最多两位小数例如 199.90”传错的概率会大幅下降。如果参数结构很复杂大模型经常把嵌套 JSON 搞错还有一个更彻底的方案把复杂参数转换成简单参数。比如把地址拆成省、市、区三个独立参数不要用一个整串的 address 对象。5.3 上下文太长被截断导致 Agent 失忆技能日志和返回结果如果全塞进对话历史上下文很快就会爆。我在早期犯过这个错每次技能调用结束把完整 JSON 返回给大模型结果第五轮对话之后历史里全是技能日志。对策是设置三个摘要策略。技能执行成功的只记录“已调用XX技能结果状态成功”不记录完整返回值。技能执行失败的才记录错误信息。当上下文超过预算时把最早几轮的技能日志压缩成一句摘要。这套策略上线后对话轮数从平均 6 轮提升到了 15 轮以上。5.4 技能并发执行导致数据安全问题怎么规避这个问题主要出现在技能内部共享了全局状态的情况下。我在代码审查时发现一个团队把订单缓存放在模块级字典里两个 Agent 线程同时写数据结果数据错乱。排查这类问题有一个笨办法代码里搜索模块级 mutable 对象一旦发现就要重构。重构的方向是让技能函数变成纯函数所有可变数据都通过参数传入、返回值传出来。如果确实需要跨技能共享数据应该显式地通过一个外部存储层而不是隐式共享内存。另外对写操作类技能启动一个独立的串行队列避免并发冲突。下面把这几类问题整理成速查表方便真踩坑时快速定位问题现象排查路径常用解法调用错误技能描述 → 召回分类 → 候选列表重写描述、扩充触发词、技能分组拆分参数缺失Schema → 正则 → 描述示例加默认值、加正则、增加 examples 字段上下文过长日志长度 → 历史轮次结果摘要、裁剪最早日志、屏蔽细节并发数据错乱全局状态 → 共享引用纯函数改造、串行队列、外置存储技能超时平均耗时 → 超时设置异步化、超时时间调整、服务端优化5.5 一个值得长期投资的资产把失败案例沉淀成评估集最后分享一个让系统越用越稳的方法每次线上出现选错技能、参数报错都把案例截取下来添加到评估集里。一周后你会有一批非常宝贵的负面样本它们比任何手写的测试用例都更能反映真实用户的行为。我维护了一个regression/目录按时间存放失败案例。修改技能描述后跑一遍整个评估集确保这次修复没有影响其他案例。这个习惯坚持一段时间后技能调用的整体准确率会稳步提升而且每次修改都有数据支撑不用靠感觉。我现在最深的体会是Agent 技能体系不是一个一次性的开发任务而是一个需要持续运营的工程。注册表是骨架描述是灵魂评估集是保障。想让 Agent 靠谱这三样东西缺一不可。