资讯动态

Agent Skill体系设计:把Agent从玩具变工具的关键实践

发布时间:2026/10/13 7:59:39 来源:尧图企业网站定制
1. Skill体系为什么是Agent从“玩具”走向“工具”的分水岭去年团队做内部运营助手时我花了大量时间调Prompt、调模型温度、堆few-shot示例最后的效果始终停留在“能聊天、能写点文案”的水平但一涉及“帮我查一下某店铺的库存”“跟第三方系统对个账”这类具体执行任务它就彻底卡壳。后来复盘才明白问题不在模型聪明不聪明而在于Agent压根没有一套组织“能力”的方式——它什么都会一点但什么都落不了地。“agent-skills”要解决的就是这件事。它把Agent从“被动应答”变成“主动执行”每次行动不再靠Prompt里塞一堆“你是个能做XXX的助手”这种空话而是由一组结构化的、可检索、可调用的技能模块来驱动。技能模块本身封装了任务描述、执行函数、入参出参、错误处理Agent只需要判断当前场景该调哪个skill然后像调用函数一样把它跑起来。我做第一版Skill框架时的核心启发是Agent的可靠性不取决于模型的推理上限而取决于有没有把“能力边界”定义清楚。模型可以自由发挥的部分越多翻车概率越高相反只要把决策收敛到“选哪个技能、传什么参数”整体稳定性能大幅上升。这篇文章不聊概念直接用完整的项目代码、注册机制、调度逻辑和排查案例把一套可落地的Skill体系拆开讲。2. 技能定义的两层结构声明信息和执行细节为什么要分家2.1 Skill的Schema设计让模型用最少的推理成本做对选择模型在执行Agent任务时本质上做两件事第一判断当前场景应该调用哪个技能第二把自然语言需求映射成技能要的结构化参数。第一件事如果做不好后面全白搭。所以Skill的声明信息需要设计成“便于模型检索”的形式而不是“便于人阅读”的形式。以我当时开发的数据库查询技能为例最开始的Schema只有名称和描述{ name: query_database, description: 查询数据库 }实测基本不可用——模型并不知道“查库存”和“查订单”是否属于同一个技能也不知道哪些表能查、结果返回格式是什么。后来我把Schema改成了分层的结构效果立刻改善{ name: query_mysql, description: 用于查询业务MySQL数据库支持按条件筛选、聚合统计和分页。适合查找订单、库存、用户信息等结构化数据。当用户提到查询、统计、报表时优先考虑该技能。, triggers: [查询, 统计, 有多少, 明细, 报表], input_schema: { type: object, properties: { sql: { type: string, description: 合法的MySQL查询语句 }, limit: { type: integer, description: 最大返回行数默认50 } }, required: [sql] }, output_schema: { type: object, properties: { columns: { type: array }, rows: { type: array } } } }设计的关键是描述字段的写法要面向“决策”而不是面向“解释”你写的是给模型看的决策锚点不是给人看的API文档。描述里必须包含“什么时候用、什么时候不用、输出是什么样”三个维度的信息。为了让模型准确匹配用户意图还要给它几个trigger示例词帮助它快速激活匹配逻辑。运营同事后来反馈“这个Agent终于知道什么时候该查库、什么时候该调接口了”就是因为描述里把决策条件勾勒清楚了。2.2 执行函数与声明分离业务逻辑不该被模型输出牵着走Schema只是“脸面”真正干活的是执行函数。在agent-skills项目的早期版本我把函数实现直接写在Agent流程里每加一个需求就要改主逻辑后来这些代码全都重构掉了。教训是技能的调用要像插线板一样即插即用而不是像芯片一样焊死在主板上。我最终采用的方案是每个技能一个独立Python文件统一暴露execute(params)接口然后通过注册表统一管理# skills/query_mysql.py from typing import Any, Dict import pymysql # 保持与声明信息同名便于自动关联 RUNTIME { name: query_mysql, version: 1.0.0, author: internal-team, } def execute(params: Dict[str, Any], context: Dict[str, Any]) - Dict[str, Any]: sql params[sql].strip() if not sql.lower().startswith((select, show)): raise ValueError(仅支持查询语句) limit params.get(limit, 50) conn context[mysql_conn] try: with conn.cursor() as cursor: cursor.execute(sql f LIMIT {limit}) columns [desc[0] for desc in cursor.description] rows cursor.fetchall() return {columns: columns, rows: [list(r) for r in rows]} finally: # 连接归还连接池 pass为什么要求统一execute(params, context)这个签名两个原因一是调度器可以无差别遍历所有技能不需要为每个技能写特殊分支二是context参数可以把共享依赖数据库连接、HTTP会话、日志器透传进去避免每个技能自己初始化连接导致资源失控。我见过一些半吊子框架执行函数各写各的签名调度层判断条件越堆越多最后维护成本高到只能推翻重来。3. 注册表与调度器的运行机制模型只做选择题不做填空题3.1 注册表的设计合并声明信息、运行时信息和统计信息一个Skill体系运转起来至少需要三个层面的信息声明信息给模型看的、运行时信息给调度器用的、统计信息给优化者参考的。很多人在第一步就把代码写死了——声明信息散落在各个文件里运行时信息散落在主流程里统计数据完全没有。我在agent-skills项目中用了一个SkillRegistry类来做统一管理# core/registry.py import importlib import json from typing import Dict, List, Optional class SkillRegistry: def __init__(self): self._skills: Dict[str, Dict] {} def register_from_dir(self, package_name: str skills): # 扫描skills目录下的所有模块 import pkgutil import skills for mod_info in pkgutil.iter_modules(skills.__path__): try: module importlib.import_module(f{package_name}.{mod_info.name}) # 优先读取同目录下的skill.json充当声明信息 schema_path module.__file__.replace(py, json) schema json.load(open(schema_path, encodingutf-8)) # 确保execute函数存在且可调用 assert callable(getattr(module, execute)) self._skills[schema[name]] { schema: schema, runtime: getattr(module, RUNTIME, {}), execute: module.execute, stats: { invoke_count: 0, success_count: 0, error_count: 0, } } except Exception as e: print(f技能加载失败: {mod_info.name}: {e}) def get_skill(self, name: str) - Optional[Dict]: return self._skills.get(name) def list_schemas(self) - List[Dict]: return [v[schema] for v in self._skills.values()]注册时schema.json自动和被注册的模块关联通过命名一致性来减少手动配置。技能加载失败的兜底逻辑也很有用——某个技能语法出错时不影响其他技能正常加载这在实际迭代时是保命设计。这里有个很容易踩的坑千万不要在注册阶段就实例化数据库连接或外部服务客户端。注册表的职责是登记“技能存在”而不是“技能已经具备运行条件”。运行时依赖的延迟初始化能让系统启动速度更快也避免一个服务挂掉拖垮全部技能。比如查库存的技能依赖MySQL连接但如果部署环境暂时没配MySQL技能照样应该注册成功只是执行时报错而已。3.2 调度器的两步走召回候选技能再用逻辑约束做裁决调度器是Agent头脑里最核心的“决策层”我实际调试后发现用两步策略最稳妥。第一步是候选召回把所有技能的Schema名称、描述、triggers合并成一份索引让模型基于用户输入先选出3-5个候选技能。这一步不追求精确追求的是不可漏掉正确答案。我一开始让模型直接返回最匹配的一个技能结果用户说“帮我看看这个月的退款情况”时模型总是选“query_mysql”而不是“query_refund_stats”因为后者在语义上更匹配但描述权重没调对。改成召回多个候选后这个问题的容错空间大很多。第二步是裁决和参数抽取从候选技能里选出最合适的一个同时生成结构化的参数JSON。这一步的关键是“让模型做选择题而不是填空题”——不要在同一个环节同时承担“选技能”和“写参数”两个任务。示例def dispatch(self, user_input: str) - DispatchResult: candidates self.recall(user_input) # 第1步输出技能名字列表 if not candidates: return DispatchResult.fail(no_matching_skill) chosen self.verify_and_extract(candidates, user_input) # 第2步选一个并抽参数 if chosen is None: return DispatchResult.fail(ambiguous_intent) return self.invoke(chosen)verify_and_extract决定参数是否齐全。参数不全时会走一轮“追问填充”而不是直接报错# 执行示例 user_input 查一下昨天的订单数 extracted {sql: SELECT COUNT(*) FROM orders WHERE created_at ?, limit: 1}调度器还会做参数合法性的预处理——比如SQL技能会检查语句前缀HTTP技能的URL要用allowlist过滤路径参数做正则校验。模型生成的任何内容都不可信它只是产生了意图真正的执行安全必须由代码保证。如果两个技能都声称能处理同一类问题怎么办我的实践是优先选择description中关键词和用户输入重合度更高的其次选历史上成功调用率高的最后兜底策略是选择执行成本更低的。这里用了一个非常小的函数def disambiguate(self, candidate_names: List[str], user_input: str) - str: def score(name: str) - float: schema self.registry.get_skill(name)[schema] overlap sum(1 for t in schema.get(triggers, []) if t in user_input) # 加权兼顾历史成功率 stats self.registry.get_skill(name)[stats] success_rate stats[success_count] / max(stats[invoke_count], 1) return overlap * 0.6 success_rate * 0.4 return max(candidate_names, keyscore)这个函数看起来简单但它把语义匹配和历史数据融合进了关键决策环节比单纯靠模型输出可靠得多。3.3 技能上下文传递让无状态的Skill协作起来单个技能能完成的任务有限真实场景多半需要多技能串行协作先查用户信息再查用户订单最后根据订单金额触发优惠策略。为了支持这种链路我在框架里引入了一个AgentContext对象它贯穿每次请求始终dataclass class AgentContext: user_id: str session_id: str vars: Dict[str, Any] logs: List[str] field(default_factorylist) def set_var(self, key: str, value: Any): self.vars[key] value def get_var(self, key: str, defaultNone): return self.vars.get(key, default)每个技能执行完后可以把中间结果写入vars后续技能可以直接读取。例如A技能查完用户的会员等级后写入ctx.set_var(member_level, gold)B技能根据会员等级计算折扣。这套机制避免了大模型上下文窗口被中间结果塞满同时让多技能的数据交换有迹可循——日志里可以完整回放ctx.vars的变化。4. 技能调用链路与错误处理当你依赖的第三方接口返回了一坨屎4.1 标准调用链从用户输入到最终响应的完整路径技能真正跑起来时最怕的不是模型选错而是“模型选对了但技能执行过程中炸了”。我把一次完整的技能调用拆成六个环节意图解析从用户输入中提取目标技能和参数。前置校验确认输入参数格式、业务权限和资源可用性。执行调用技能内部跑实际逻辑。结果封装把执行结果标准化成统一输出结构。后置校验检查输出是否符合Schema声明的结构。日志与统计记录耗时、成功失败状态和关键错误信息。其中第4步最容易被人忽略。很多Agent框架让技能随便返回Python对象导致后置环节完全没法判断结果好坏。我在agent-skills项目中规定了所有技能返回统一格式{ code: 0, # 0成功非0业务错误码 message: ok, # 人类可读描述 data: { ... } # 实际业务数据 }也定义了中间结果的结构方便技能内部边执行边记录进度。有了这个统一封装调度器才能做到“成功继续、失败降级”的通用处理。4.2 错误处理的三个层次模型层、技能层、系统层错误处理要分三层来想每一层负责堵住不同类型的错误模型层处理的是“模型生成错误”——参数类型不对、JSON格式坏掉、技能名超出注册范围。解决方式非常直接把模型返回的JSON字符串用严格解析器解析失败就重试一次重试时把错误信息作为反馈一起给模型。比如意图解析返回的结果是{skill: query_mysql, params: {sql: SELECT * FROM orders WHERE status pending, limit: 50}}如果limit字段不存在或类型不对就重新抽参并额外提醒模型“limit必须是整数”。实测下来两次重试能恢复九成以上的解析错误。技能层处理的是“业务逻辑错误”——数据库查询超时、第三方接口返回500、文件不存在。这层的关键是用异常捕获包裹技能执行体并区分可重试和不可重试的错误def safe_execute(skill, params, context): try: result skill[execute](params, context) return SkillResult.ok(result) except TimeoutError as e: return SkillResult.retryable(f执行超时: {e}) except PermissionError as e: return SkillResult.fatal(f权限不足: {e}) except Exception as e: return SkillResult.fatal(f未知错误: {e})系统层处理的是“框架错误”——调度器自身异常、注册表信息缺失、资源没传进context。这层的处理就简单了直接在入口捕获并返回固定的兜底话术例如“抱歉我现在无法处理这个请求请稍后再试”。三层错误各自的返给用户的提示语也不一样模型层的问题可以提示“我理解得不太对请换个说法再试试”技能层的问题提示“服务暂时繁忙请稍后再试”系统层的问题提示“内部逻辑出现异常已记录日志”。4.3 日志和统计知道哪个技能在拖后腿比加更多新技能更重要做Skill框架时排除“谁在拖后腿”比“谁在干活”重要得多。我的日志规范里每个技能在调用开始和结束时都要记录结构化信息{ event: skill_invoked, skill: query_mysql, session_id: s-001, params_summary: SELECT COUNT(*) FROM orders, ts: 2024-06-01T10:00:00Z }结束日志额外记录latency_ms和status。统计模块里我最关注三个指标技能调用成功率成功数/总调用数技能平均响应耗时意图解析的置信度分布如果经常出现“低置信但调用成功”说明描述信息要优化有一次我发现query_refund_stats技能调用成功率只有65%查日志后发现大量超时来自底层报表服务在每月日期边界的性能问题。后续在技能描述里加了一句“如果当月数据量高于万级建议走异步接口”成功率立刻回升到88%。这就是统计驱动的优化路径——没有统计数据这类问题会被埋在日常噪音里。5. 手动扩展Skill模块从零到一新增一个“天气查询”技能5.1 新增技能的最小文件集一个py文件加一个json文件用最小示例演示扩展流程。假设我现在要新增一个天气查询技能目录结构是agent-skills/ ├── core/ │ ├── registry.py │ └── dispatcher.py ├── skills/ │ ├── query_weather.py │ └── query_weather.jsonquery_weather.json是声明信息写清楚何时使用、何时不用{ name: query_weather, description: 查询指定城市当前天气和未来几天的预报。当用户询问天气、气温、降雨、出行是否带伞时使用。不用于查询历史气候数据。, triggers: [天气, 气温, 下雨, 带伞, 要不要带伞], input_schema: { type: object, properties: { city: { type: string, description: 城市中文名如北京、上海 }, days: { type: integer, description: 预报天数1-7默认1 } }, required: [city] }, output_schema: { type: object, properties: { city: { type: string }, current: { type: object }, forecast: { type: array } } } }query_weather.py是执行实现。真实业务中会请求气象服务API这里用一个读取mock数据的函数说明流程# skills/query_weather.py from typing import Any, Dict import time RUNTIME {name: query_weather, version: 1.0.0} def execute(params: Dict[str, Any], context: Dict[str, Any]) - Dict[str, Any]: city params[city].strip() days min(int(params.get(days, 1)), 7) # 从注册表读取的context里拿到HTTP会话 http context[http_session] api_key context[config][weather_api_key] try: resp http.get( https://api.weather.example.com/v1/forecast, params{city: city, days: days, key: api_key}, timeout5 ) resp.raise_for_status() return resp.json() except Exception as e: # 实践里可用fallback mock数据省去外部依赖 return { city: city, current: {temp_c: 22, condition: 晴}, forecast: [], _fallback: True, _error_note: str(e) }然后别忘记了在技能目录下做一个自动发现配置这个文件可以放在skills目录的__init__.py里为空让pkgutil.iter_modules能扫到子模块。5.2 验证技能注册与调用的快速脚本新增完技能后直接用这个脚本验证python -c from core.registry import SkillRegistry r SkillRegistry() r.register_from_dir() assert r.get_skill(query_weather) is not None print(技能注册成功:, r.list_schemas()) 接着跑一个简单地端到端调用from core.registry import SkillRegistry from core.dispatcher import Dispatcher from core.context import AgentContext registry SkillRegistry() registry.register_from_dir() dispatcher Dispatcher(registry) ctx AgentContext(user_idtest, session_ids-001) result dispatcher.dispatch(上海明天会下雨吗我需要决定要不要带伞) print(命中技能:, result.skill_name) print(参数:, result.params) print(执行结果:, result.result)跑通后再回看我的调试经验新增技能最容易出的问题就是description没写清楚触发场景导致模型选错或者input_schema的字段描述与用户表述对不上。比如天气这个例子如果city写成“城市拼音”模型就会把“上海”转成shanghai和业务API预期不一致。别嫌这些细节琐碎Agent的准确率就是被这些细碎规则一点一点夯实的。6. 与主流Agent编排方案的对比为什么我没有只用LangChain等现成框架6.1 Agent Skills、工具调用和原生Function Call的差异分析在落地的过程中有同事问我现成的Agent框架不是也有工具调用机制吗为什么还要自己搞一套Skill框架这个质疑很有道理做技术选型确实不该重复造轮子。我梳理过三种主流方案之间的差异维度现成框架的工具调用机制模型侧函数调用能力自建Skill框架抽象层次每个工具独立缺统一生命周期模型生成函数名与参数但不管执行技能支持注册、调度、协作、统计路由方式需在框架内写路由逻辑完全依赖模型判断模型筛候选 规则裁决错误处理各写各的无标准三层统一错误体系扩展性中等框架结构限制多强但缺乏约束高且可控现成框架的“工具调用”是为了打通Demo用的它默认你把所有工具都平等列出让模型自己选。这在技能数量少时没问题但技能超过二三十个后模型会频繁“犹豫”——召回的质量开始明显下降速度也变慢。模型侧的函数调用虽然抽象层次更高但执行过程完全不受控你没法在中间注入业务校验、权限检查、审计日志。自建Skill框架的核心价值是把决策和执行的边界画得清清楚楚适合中大型团队做长期沉淀。如果只是随便跑个Agent Demo不需要自建框架。6.2 什么情况下值得搭建自建Skill体系根据团队情况我自己总结的适用场景和不适用的场景如下值得搭自建体系的情况技能数量多于20个且还在持续增长。多个技能共享同样的数据库、权限系统或基础服务。需要严格审计每次Agent行为的合规性金融、医疗、政务类场景尤其如此。需要精细化统计模型选技能的错误模式以持续优化。团队有较强的开发人力能接受一个月内没有“炫酷Demo”而在打地基。不要用自建体系的情况只是做一次Prompt实验或概念验证两三天就交付。技能数量5个以内每个技能互不相关直接给模型列出来就行。团队没有专职开发主要靠配置化平台交付Agent。从迭代效率看自建Skill框架更像搭积木的底盘前期慢后期越来越顺。现成框架是拿现成的乐高套件上手快但拼到复杂结构时会发现缺专用件。这是取舍问题没有绝对的好坏。6.3 我选择自建体系的真实原因业务约束与审计需求压倒了开发便利性复盘那次内部运营助手项目真正促使我下定决心自建Skill框架的不只是技术洁癖而是两条业务约束第一业务方要求每次Agent调用第三方服务必须走统一权限网关并且有完整的链路追踪。现成框架的工具调用点分散在各业务代码里改造起来牵一发动全身。自建体系天然有一个context一次请求的所有调用日志、参数摘要、响应状态全部集中在一条链路上审计时只需要查一个索引。第二技能需要支持“灰度发布”。新版本技能上线后一部分流量走新逻辑另一部分走旧逻辑跑几天对比大盘成功率。现成框架不支持技能级别的路由权重配置但这个需求在僵雪SimClass框架里只是一个字段的事——注册表里每个技能加一个traffic_weight{ name: query_orders, traffic_weight: { v1: 0.5, v2: 0.5 } }调度器根据权重概率选版本日志区分version字段统计模块自动拆分对比。7. 打磨Skill体系的三个月从翻车现场到稳定交付的复盘7.1 最严重的错误把“技能粒度”设计得太粗第一个月最大的败笔是某一个技能做成了“全能查询”名字叫query_business_data既能查订单又能查库存还能查用户画像。模型倒是永远不会选错可问题是入参极其复杂需要同时传data_type、filters、group_by、time_range一长串参数里任何一项生成错了结果就全错。这个问题花了一周时间重构才解决最终拆成了7个各自聚焦的单技能查订单、查库存、查用户、查退款、查优惠券、查营销活动、查渠道报表。拆完之后发现参数复杂度大幅下降模型选技能也很少出错了。技能粒度的判断标准很朴素如果这个技能的入参超过5个且大部分是必填说明粒度太粗需要拆。另一个经验是“技能要有业务语义而不是数据源语义”。你别做“MySQL查询”这种以数据源划分的技能要做“订单查询”“库存查询”这种以业务场景划分的技能。前者看似灵活但因为覆盖太广最后只能回到“让用户/模型自己写SQL”的危险设计里。7.2 描述文案的AB测试同样的技能不同的说法成功率差距明显有一天技术群里讨论“模型幻觉”我通过对比实验发现技能描述就像某种“幻觉解药”。同一个查询技能A版描述是“查询订单数据支持各种条件筛选和聚合”B版描述是“用于查询订单基础信息与状态。当用户询问订单金额、购买时间、商品明细、物流状态时使用。不支持营销活动数据分析。”B版在50次实测中的准确命中率比A版高了22个百分点。描述写得好不好直接决定模型在“选没选对技能”这一步的准确率。之后我建立了一个小本本写描述时反复提醒自己用“当……时”句式列出典型使用场景。明确写出“不用于”什么场景负面清单比正面清单更能防止误用。如果两个技能容易混淆在描述中显式区分。经过几轮迭代技能描述文案已经像产品文案一样被版本化了。7.3 上下文窗口的矛盾既想让模型多理解又不想让Prompt太胖自建框架时会不断往调度提示词里塞技能描述最终导致一个尴尬问题技能超过30个后光把全部schema塞进上下文就占了几千token模型处理速度变慢且选错率上升。我当时想了很多办法方案一动态裁剪。先根据用户输入做一个粗召回只把粗召回到的3-5个技能的详细schema放进上下文其他技能只保留名字。这个方案效果不错本质是“先粗筛再细读”。粗筛用的是轻量关键词匹配或者一个便宜的小模型嵌入检索不需要大模型参与。方案二技能分组与命名空间。把技能拆成订单域、用户域、营销域等分组调度先选域再选技能。域选择可以直接用规则判断比如用户输入“订单”“支付”“退款”就归到订单域。有了分组之后每个域的schema列表都很短模型的选择压力大幅降低。方案三重要技能固定放前冷技能靠召回触达。高频技能直接写在系统提示词靠前位置长尾技能只按需注入。这三种手段加起来确保了即便技能数量再多核心决策上下文始终控制在合理范围内。7.4 灰度策略里的技能版本管理让“改坏一个地方”不至于整体下线技能不是写了就能永远不变业务规则会变、第三方接口会变、数据口径会变。我的技能版本管理方案很简单但也实用每个技能模块里带有RUNTIME版本号注册表里保存多个历史版本的执行函数调度器根据“当前流量配置”决定走哪个版本。做版本切换时一个非常实用的工具是“影子模式”新技能版本上线后它和旧版本同时执行但新版本结果不入库、不发给用户只用于对比。跑几天如果新版本的成功率、平均耗时、错误类型都比旧版本好再把流量切过去。这个机制听起来很重但在现有框架里实现只要几十行代码——调度器里加一个dry_run的上下文标记s技能执行完不影响主流程即可。说实话Skill体系的搭建没有太多玄学核心就是“定义清晰 边界明确 机制可控”。如果此刻你手头正有一个Agent项目做得磕磕绊绊我建议先别急着换模型或调Prompt而是花一个下午盘点现有技能画出它们的触发场景和参数约束。很可能你会发现真正的问题不在模型笨而在于你根本没有给模型一套可以“按图索骥”的完整地图。对我来说agent-skills带给团队的最大收益不是代码多精巧而是让每个参与者从此按同一套标准定义、实现、评估Agent的能力边界。

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

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

免费获取报价 →
↑