Agent Skills实战:用技能中间层让AI Agent稳定执行真实任务

发布时间:2026/10/7 13:18:48
Agent Skills实战:用技能中间层让AI Agent稳定执行真实任务 最近半年我一直在琢磨一个事儿怎么让AI Agent不只是“能聊天”而是真的能稳定地“干活”。市面上的Agent框架一大堆但真正拿到自己的业务场景里跑总会遇到同一个坎——模型既当调度员又当操作员能力边界模糊、工具调用零散、上下文越攒越乱。后来我把思路换了个方向不再追求一个“全能Agent”而是把能力拆成一个个可独立开发、单独测试、按需组合的“技能包”也就是今天要聊的 agent-skills 这套设计思路。先说清楚这东西能做什么它本质上是在大模型和真实业务逻辑之间加了一层“技能中间层”。每个技能是一个自包含的模块负责一件具体的事比如查天气、算账、解析文档、调内部接口。Agent只需要理解“有哪些技能、分别在什么场景用”然后把任务分发给对应的技能去执行。这样一来模型负责思考和决策代码负责执行和校验各干各擅长的部分。这篇文章适合正在做Agent应用落地、被工具调用不稳定或系统难维护折磨的开发者也适合想把团队里的AI能力沉淀成可复用资产的AI产品经理。我会从设计思路、技能结构、运行时实现到排查技巧把整套方案拆开讲清楚代码可以直接拿去改。1. Agent Skills的核心设计思路1.1 为什么把能力拆成“技能”而不是直接写Prompt先回顾一下大多数团队做Agent的第一版方案把几十个工具的说明、用法、注意事项全塞进system prompt然后让模型自己“脑补”怎么调。这个方案有两个致命问题。第一Prompt是被模型“读到”的不是被代码“约束”的。模型对工具描述的理解存在歧义同一个工具在不同上下文中可能被解读成不同的行为出错后你根本没法在代码层拦截。第二所有工具共享一份上下文窗口工具越多单次请求消耗的token越多模型做意图判断的准确率反而下降。我见过一个项目接了30多个工具结果模型经常把“查余额”和“查流水”搞混因为两个工具描述里都出现了“账户信息”这几个字。技能化设计改变了这个局面。每个技能是一个独立的代码模块有明确的输入输出协议和校验逻辑。模型看到的不是几十段工具描述而是一个经过筛选的、紧凑的技能清单。任务进来后系统先做一轮技能路由把请求只分发给相关的两三个技能其他技能根本不进上下文。这既降低了模型的认知负担也把错误拦截在代码层。1.2 技能的“原子性”和“可编排性”是两回事做Agent Skills最容易犯的错是把一个技能写得太大。拿“订单处理”来说如果这个技能内部既包含查库存、又包含下单、还包含发送通知那它的边界就太宽了。模型一旦把这个技能用于某个子场景要么传一堆不需要的参数要么漏掉关键参数。正确做法是每个技能只做一件最小的事。查库存就是一个独立技能下单是另一个独立技能发通知再单独拆一个。每个技能的输入参数控制在5个以内能用枚举就用枚举。这样拆完之后技能之间通过编排器组合这就是“可编排性”的由来。你不再需要写一个“全能订单处理”技能而是在编排层定义一条链路先调查库存技能确认数量再调下单技能创建订单最后调发送通知技能告知结果。我把这套逻辑想明白后内部定了一条规矩如果一个技能内部出现了“如果……那么……”的分支流程就该拆成两个技能。分支逻辑放到编排层让模型或者规则引擎来做技能本身保持纯执行。1.3 技能选型背后的权衡代码优先还是模型优先在设计技能内部实现时会遇到一个选择技能逻辑到底用代码写还是用Prompt加模型推理写。我的答案是能用代码的地方绝不用模型。模型只做两件事从用户意图中抽取参数、在多个技能之间做路由选择。剩下的计算、查询、格式化全部用确定性代码完成。这不是技术洁癖而是成本考虑。一次模型推理调用在性能和费用上都比一次本地函数调用贵几个数量级。更重要的是可靠性同样一段逻辑代码跑100次结果都一样模型跑100次可能有2次偏差。技能内部的确定性越高整个Agent的稳定性就越好。1.4 与传统Function Calling方案的区别Function Calling是OpenAI最早推广的机制目前主流大模型基本都支持。它本质上是一个参数补全协议模型根据对话内容输出一个函数名和参数JSON。Agent Skills和它的区别在于层次和边界。Function Calling的每个函数是平铺的、单独调度的系统没有函数之间的组合关系。而且它只覆盖了“让模型正确输出调用意图”这一步函数执行结果的校验、重试、上下文摘要都要你自己另外实现。Agent Skills把函数升级成带有元信息、依赖声明、校验规则的完整模块还建议你做一个注册中心来管理这些模块。两者不是替代关系很多实现里Agent Skills最终也是通过Function Calling协议暴露给模型的。区别在于直接用Function Calling时你的业务代码会被大模型的调用方式牵着走而用Agent Skills时业务代码先在本地定义好能力边界再决定怎么暴露给模型。2. 技能的标准结构与元信息设计2.1 一个技能包的文件组织一个技能在代码仓库里应该是一个独立目录而不是一个散落的函数。我习惯的结构很简单每个技能目录包含三个文件skill.yaml描述文件写给模型看包含名称、描述、参数定义、返回格式、适用场景。__init__.py技能实现暴露一个统一的执行入口。tests/技能自己的单元测试。这个拆分是有意的。描述文件是给模型做决策用的实现文件是给系统执行用的测试文件是给开发者维护用的。三者分离之后你可以让产品经理维护描述文件工程师专注实现逻辑互不干扰。在yaml描述文件里至少要包含这些字段name: get_weather description: 根据城市名称获取实时天气信息 tags: [weather, query] parameters: type: object properties: city: type: string description: 城市名称例如“北京”“上海” unit: type: string enum: [celsius, fahrenheit] default: celsius required: [city] returns: type: object properties: temperature: { type: number } condition: { type: string } humidity: { type: number }2.2 描述文件是写给模型看的不是给人看的很多团队把描述文件写成工具文档风格像写API参考手册一样。“通过指定城市ID获取该城市当前天气情况数据”这种描述模型经常抓不住重点。模型做技能选择时依赖的是语义匹配不要写“该技能用于……”要直接写“在用户询问XX场景时使用”。一个好的描述应该包含三个部分。第一触发场景用户说什么话时该用这个技能。第二关键能力这个技能能提供什么确切结果。第三边界条件什么情况不该用这个技能。我拿一个实际例子对比一下。低质量描述“查询数据库中用户信息需要传入用户名”。高质量描述“当用户提供用户名或手机号并请求查看个人资料时使用此技能。禁止用于查询订单或余额信息这些由其他技能处理。”后面这句边界说明非常重要它能显著降低技能误调用的概率。2.3 参数Schema要限定模型的自由发挥空间参数定义这块不能图省事只写上字段名和类型。你不给例子模型就会自由发挥。比如一个日期参数你说“格式为字符串”模型可能生成“明天”“下周一”这种自然语言也可能生成“2025-02-30”这种不存在的日期。我的经验是每个参数都尽量加上描述、示例值和枚举约束。对于有明确格式要求的参数在描述里直接给一个完整例子date: type: string description: 查询日期格式YYYY-MM-DD例如2025-01-15。不允许使用“今天”“明天”等相对时间表达。此外在参数校验层做兜底。模型传进来的参数无论多离谱先过一遍JSON Schema校验不合格就直接返回错误信息让模型重新生成而不是用一个脏参数继续执行。2.4 技能执行协议输入、输出和错误码技能与外部世界的交互需要一个统一协议。我定义了三个标准输入必须是结构化JSON、输出必须是结构化JSON、错误必须返回明确的错误码加可读错误信息。结构化JSON输入保证每个技能都能独立测试。输出结构化JSON保证编排器能统一解析结果。错误码设计这块容易被忽略但实际使用中价值极高。我常用几个错误码错误码含义处理方式1001参数校验失败返回给模型要求重新生成参数1002外部依赖不可用触发降级策略1003业务规则校验未通过直接返回给用户不重试1004执行超时返回提示并记录日志有了这套协议编排器不需要关心技能内部逻辑只需要根据错误码做对应的路由决策。3. 技能运行时与注册中心实现3.1 注册中心与技能发现运行时核心是技能注册中心Registry。注册中心的职责是维护所有可用技能的索引提供按名称精确查找和按场景语义模糊搜索两种查询能力。精确查找用于编排器内部比如编排规则里已经写死“先调用get_weather再调用get_wind”那直接按名字拿。模糊搜索用于模型做路由决策前的候选集筛选输入是用户请求输出是相关性最高的几个技能。我实现了一个轻量的技能发现机制没有用向量数据库那么重的方案。先把每个技能的关键词和描述分词建立一个倒排索引查询时按关键词命中数和描述重合度打分。实测下来这个小方案在多技能场景下已经够用而且没有额外的服务依赖。3.2 一个可直接运行的SkillRegistration实现用Python写一个最小可用的注册中心核心逻辑也就几十行from dataclasses import dataclass, field from typing import Callable, Any import json dataclass class SkillMeta: name: str description: str tags: list[str] param_schema: dict enabled: bool True keywords: list[str] field(default_factorylist) dataclass class Skill: meta: SkillMeta executor: Callable[..., Any] class SkillRegistry: def __init__(self): self._skills: dict[str, Skill] {} self._index: dict[str, list[str]] {} def register(self, skill: Skill): self._skills[skill.meta.name] skill for keyword in skill.meta.tags skill.meta.keywords: self._index.setdefault(keyword, []).append(skill.meta.name) def get(self, name: str) - Skill | None: return self._skills.get(name) def search(self, query: str, top_k: int 3) - list[str]: scores {} for word in query.lower().split(): for skill_name in self._index.get(word, []): scores[skill_name] scores.get(skill_name, 0) 1 ranked sorted(scores.items(), keylambda x: -x[1]) return [name for name, _ in ranked[:top_k]]这个注册中心用起来很简单registry SkillRegistry() async def get_weather_executor(city: str, unit: str celsius): # 内部实现可以是真实API调用 return {temperature: 23, condition: sunny, unit: unit} skill Skill( metaSkillMeta( nameget_weather, description当用户询问天气时使用, tags[weather, query], keywords[天气, 气温, weather], param_schema{city: {type: string, required: True}} ), executorget_weather_executor ) registry.register(skill)3.3 执行器参数校验与运行时包装在执行器外面我会再包一层通用的校验壳。这个壳做的事是解析参数、按Schema校验、调用真正实现、捕获异常、统一输出。import jsonschema from functools import wraps def validate_params(schema: dict): def decorator(func): wraps(func) async def wrapper(*args, **kwargs): try: jsonschema.validate(kwargs, schema) except jsonschema.ValidationError as e: return {error_code: 1001, message: f参数校验失败: {e.message}} try: result await func(*args, **kwargs) return {error_code: 0, result: result} except TimeoutError: return {error_code: 1004, message: 执行超时} except Exception as e: return {error_code: 1002, message: f执行异常: {str(e)}} return wrapper return decorator有了这层壳技能实现者只需要专注写核心逻辑不需要在产品代码里到处塞try-catch和参数校验。3.4 编排器的两种模式规则编排与模型编排技能准备好了之后需要有一个编排器把技能串起来。编排器有两种工作模式对应不同的业务场景。规则编排适合流程固定的任务。比如“开票流程”严格分三步查询订单、校验信息、生成发票。这种情况下不需要模型参与决策直接写死调用顺序即可。优点是零模型开销、完全可控适合对准确性要求极高的场景。模型编排适合开放式的任务模型根据用户请求动态决定调用哪些技能。这种情况下系统先把用户请求送入注册中心搜索拿到候选技能列表然后把候选技能的描述、参数列表传给模型让模型输出意图。两种模式可以混合使用。我的习惯是把高频且固定的流程做成规则编排只在规则覆盖不了的开放式场景才走模型编排。这样整体稳定性和成本都能兼顾。3.5 上下文压缩技能结果太多时怎么办多个技能连续执行会产生大量中间结果如果全部塞进新的模型请求里上下文很快会爆炸。例如用户问“北京适合旅游吗”你可能需要依次调用天气技能、空气质量技能、交通拥堵技能三个技能返回的JSON都是半页内容。解决办法是在每轮技能执行结束后让编排器输出一段短摘要而不是保留完整输出。摘要可以是一个Prompt让模型完成也可以自己写规则截取关键字段。我倾向于先规则截取因为快且费用为零摘要信息不够时再补模型调用。4. 实操从零构建带Skills的Agent4.1 整体架构的三层拆分在实际项目中我建议把系统拆成三层入口层、编排层、执行层。入口层负责接收用户输入、做必要的格式化编排层负责决定调用哪些技能执行层就是一个个技能模块。入口层最好是一个无状态API方便接不同的前端入口。编排层可以独立部署为一个服务内部包含技能注册中心、路由逻辑和调用链跟踪。执行层的每个技能可以部署在同一个进程内也可以拆成独立微服务。进程内部署适合技能数量少、实现没有不同技术栈要求的场景。拆成微服务适合技能相互独立、有不同性能需求或使用不同语言的场景。初期阶段建议都放在同一个进程里等技能数量超过20个再考虑拆分。跨进程调用带来的网络开销和序列化成本在技能少于20个时不太划算。4.2 用Agent Skills框架接入大模型下面这段代码展示一个完整的Agent循环。它做的事是接收用户请求、搜索候选技能、让模型选择技能并抽取参数、执行技能、返回结果。import json async def run_agent_with_skills(user_input: str, registry: SkillRegistry): # step 1: 搜索候候选技能 candidates registry.search(user_input, top_k3) if not candidates: return {error: 没有找到合适的技能} # step 2: 把候选技能描述交给模型 skill_context {} for name in candidates: skill registry.get(name) skill_context[name] { description: skill.meta.description, parameters: skill.meta.param_schema } prompt f 用户请求: {user_input} 可用技能: {json.dumps(skill_context, ensure_asciiFalse)} 请返回一个JSON包含selected_skill和parameters两个字段。 # 这里调用你自己的模型接口返回一个结构化结果 llm_result await call_llm(prompt) # 假设返回的是完整JSON字符串 try: action json.loads(llm_result) skill_name action[selected_skill] params action[parameters] except (json.JSONDecodeError, KeyError): return {error: 模型输出格式不合法} # step 3: 执行技能 skill registry.get(skill_name) if skill is None: return {error: f技能 {skill_name} 不存在} return await skill.executor(**params)这段代码虽然短但已经把Agent Skills最核心的闭环跑通了。真正的生产实现里你还需要在里面加入对话历史管理、重试机制、异常后降级、日志记录等但这些都属于常规工程完善。4.3 六个实战技能的完整演示为了演示技能化的价值我用这个框架做了六个技能。技能如下获取天气、汇率换算、计算器、SQL查询、PDF转文本、关键词提取。实现都很简单重点看它们如何协同。我想强调一个实际经验做技能清单时尽可能把“其他团队已经做过的东西”包装成技能复用而不是每个项目从零实现。比如SQL查询技能内部封装了公司的统一数据访问层其他Agent项目直接注册同一个技能实例就行。4.4 技能编排的实际案例一个具体案例构建一个“差旅助理”技能编排。当用户说“帮我规划一下下周去上海出差”技能编排器需要做下面几步先调用技能A解析出差日期从这句话里抽出“下周一到周五”。再调用技能B查询列车时刻给定起点、终点、日期返回可选车次。同时可以调用技能C查询目的地天气提醒带伞还是带外套。三个技能可以并行调用等全部返回后汇总结果。模型在这里扮演两层角色第一层从用户请求中抽取参数第二层在多个候选执行策略中选择最优方案。整体比纯Prompt驱动的Agent稳定很多因为中间任何一步出错都可以定位到具体的技能模块排查。4.5 测试一个技能Sills的两种方法技能本身可以用常规单元测试覆盖这个方法的问题是只能验证输入输出对应关系无法验证描述文件是否让模型在意图匹配时表现良好。我推荐再补一个评测集收集约200条用户真实请求样本标注每个样本该命中哪个技能以及正确的参数值。然后建一个自动评测脚本逐个跑样本统计技能命中率和参数正确率。这个评测集的价值会随着时间累积越来越重要。每当你修改了某技能的描述文件跑一遍评测集就能立刻看出是提升了还是削弱了模型的命中率。这是一种回归测试和代码回归测试同样重要。5. 常见问题与排查技巧实录5.1 模型总是选错技能怎么办这是按出现频率排名第一的问题。排查思路是先区分是哪一层出了问题是注册中心搜索阶段就把正确技能排除了还是搜索到了但模型在路由时选错。注册中心搜索阶段排除问题通常出在关键词覆盖不全。比如技能描述里写的是“查询订单”用户说的是“看一下我买的东西到哪了”分词后完全匹配不上。修复方式是给技能加更宽泛的同义词关键词。模型在路由阶段选错问题通常出在技能描述之间区分度太低。比如“查询账户余额”和“查询账户明细”两个技能描述都用了“账户”和“查询”模型分不清楚。修复方式是强化边界描述在“查询账户余额”里明确写“只返回当前可用金额不包含历史流水”在另一个技能里明确写“返回历史交易列表非余额信息”。5.2 模型生成的参数不按照Schema来一种是类型错误比如把整数参数填成字符串。这种情况可以在校验层用宽松校验数字字符串自动转成数字。另一种是参数值幻觉比如用户没提供日期模型编造了一个。这种情况原则是宁可让校验失败重试也不要用幻觉参数执行。一个实测有效的技巧是在参数Schema里给每个数组或枚举字段设置空值选项当模型不确定时不强制生成而是无视该字段由技能内部提供默认行为。5.3 技能执行失败后如何自动重试和降级我会把所有外部依赖调用都设计成带超时和重试的版本。在技能实现里重试两次间隔1秒仍然失败就直接返回1002错误码。编排器收到1002后可以先尝试找同类技能覆盖。降级级别我分了三层。第一层找功能相似的备选技能。第二层把技能的结果改成模拟数据但明确告诉用户当前是测试数据。第三层直接告诉用户服务暂时不可用。降级逻辑尽量写在编排器层不要写在技能内部这样技能可以保持纯粹。5.4 上下文管理在技能对话中的坑长对话场景下连续执行多个技能前面的技能输出会在模型上下文里留存导致后面的路由请求上下文越来越大。我的做法是彻底隔离每一轮技能路由都用独立的Prompt不携带历史对话的完整内容只携带一个固定格式的“状态摘要”。状态摘要可以这样设计当前用户的核心意图、已经执行过的技能及结果摘要、还有哪些信息未知。这比把完整对话历史传给模型省token且意图更集中。5.5 Agent Skills的评测选址和回归基线最后建议每个团队在正式用Agent Skills时都建立一个“技能评测集”。我的做法是评测项数量通过标准单技能意图命中率200条样本 95%参数抽取正确率200条样本 90%多技能全链路成功率50条复杂任务 80%平均单轮响应耗时压测100次 3秒这个基线一旦建立每次修改任何一份描述文件先跑一套评测集再上线。实测下来很多微小的描述改动看起来智能了但评测命中率反而降了这类改动不上线。5.6 踩过几次坑之后的加分建议技能描述文件建议用中文写还是英文写我测试下来和国内大模型交互时中文描述效果更好和国外模型时英文更好。如果你同时对接多个模型可以在注册中心里为同一个技能存多份描述按模型类型自动切换。技能目录最好加入独立测试和环境配置能力。后续团队协作时一个协作工程师可以只在自己的技能目录里开发不需要看全局代码。6. 更多落地场景与能力扩展6.1 垂直领域的Agent Skill封装在实际行业中Agent Skills的价值会被放大因为领域知识正好可以沉淀在技能里。以金融场景为例合规检查技能、交易试算技能、报表生成技能都是相对独立的每一个都有自身的知识库和规则。通用大模型不会自带这些领域技能把领域知识固化到技能之后Agent的可控性和可审计性就很清晰。用技能设计的方式沉淀领域逻辑挺适合当成团队的一种知识资产。6.2 可视化技能编排与低代码平台技能之间的编排并不一定都是代码。现在很多低代码平台已经支持拖拽式的技能编排每个节点对应一个技能节点之间连线定义依赖关系。这种可视化编排非常适合业务人员直接参与。产品经理自己就能配上“先查库存再下单最后通知”的流程不需要等工程师写代码。6.3 技能复用与团队协作我把技能注册中心比作一个内部的应用商店每个技能都是一个可上架应用。某个团队的技能可以在申请后注册到另一个团队的中心使用。这个和微服务治理有些类似模块复用起来协作效率提升会很直观。技能需要明确的负责人和版本信息。一旦技能出了Bug或者需要变更你能迅速知道找谁也能做灰度对比。6.4 与模型能力的持续演进配合模型能力在快速变强Agent技能这种设计反而会越来越重要。模型从“会调用函数”到“会组合技能”这个演进过程中技能包作为稳定的业务逻辑边界可以把不同代的模型底座换掉而不影响上层业务。这个隔离价值一旦体验到就不太会离开这种设计了。对于刚开始尝试团队建议先选业务里最痛的一个流程拆成3-5个技能跑通之后再横向铺开。从我个人的实操体验上看agent-skills 这套思路最值得借鉴的地方不是某个具体的代码实现而是它所提倡的“把模型能力当成可替换的零件、把业务能力沉淀为可复用的技能”这个视角。模型迭代太快业务逻辑不能跟着模型一起变技能层恰好作为它们之间的缓冲垫。最初阶段可以不太在意工程完备性先把两三个技能跑通一次全链路那种“模型干思考的活、代码干执行的活”的掌控感很快就会建立起来。真正要花心思的永远是描述文件的质量和评测数据的积累。