
1. Agent Skills到底解决什么问题先聊个现象。过去一年多做大模型应用的朋友应该都有同感把大模型接入业务系统真正难的不是让它能说话而是让它会办事。早期大家靠写超长System Prompt把所有工具说明、约束规则、调用方式一股脑塞进去结果模型要么记不住要么在工具多的时候选错。后来有了Function Calling模型终于知道该调哪个API了但函数本质上是扁平的、无状态的它只管一次调用不管任务上下文。再后来有了Agent框架把思维链、规划、工具调用串起来效果好了很多但工程复杂度也上来了——每个Agent内部塞满了难以复用的逻辑。Agent Skills要解决的就是这个夹层问题在底层的原子工具和上层的复杂Agent之间加一层可复用的、自包含的、面向任务的能力单元。一个Skill不是一次API调用而是一个遇到某类任务时Agent可以翻阅并调用的小型解决方案。比如帮用户做一份Excel销售报表是一个Skill它内部可能包含读取数据、清洗格式、计算汇总、生成图表、导出文件五六个步骤但对Agent和用户来说它就是一个整体能力。我在实际项目里的体感是Skills最合适的定位不是又一个框架而是一种组织代码和Prompt的方式。它把你散落在Agent主循环里的那些if-else逻辑、专用工具调用序列、异常处理策略收敛成一个一个带着说明文档的模块。Agent拿到用户请求后先在自己的技能库里检索找到匹配项加载它执行它然后返回结果。技能库可以像插件一样动态增减甚至在Agent运行过程中自己把多个基础Skill编排成新的Skill这就是另一个进阶话题了。适合读这篇文章的人我认为有三类第一类是正在用LangChain之类的框架做Agent应用但发现复用和调试越来越痛苦的工程师第二类是业务系统想接入大模型能力需要一套给模型配工具方案的架构师第三类是刚接触Agent开发、想从零搭一套清晰可扩展的智能体能力层的初学者。不管是哪一类我下面讲的设计思路和代码基本都能直接拿来改。先说清楚我不会推荐某个具体商业产品而是带你手写一套足够轻量、完全可控的agent-skills机制你会真正理解它内部在做什么。2. 技能库的核心设计从接口规范到注册机制2.1 一个Skill到底由哪些部分组成先说抽象层面的设计。如果你搜Agent Skills相关的资料会发现各家实现细节千差万别但抽掉外壳技能模块几乎都由五部分组成技能元信息名称、唯一标识、版本等身份信息。这部分是给Agent做检索用的。技能描述一段面向LLM的自然语言说明书包括这个技能是干什么的、什么场景下应该触发它、什么场景下不应该碰它。描述的质量直接决定Agent能不能正确选中技能。参数契约定义这个技能需要哪些入参、每个参数的类型约束和取值范围通常用JSON Schema风格描述。参数契约是Agent填充参数时的唯一依据。执行体真正干活的代码或工具调用序列可以是一个Python函数也可以是一串调用其他API的步骤。执行体内部要处理错误、重试、校验等逻辑。示例与边界一到两个少样本示例展示用户这样问时参数应该怎样填以及边界条件说明比如数据量超过一万行时建议走批处理。这五个部分缺一个都不完整。我见过很多初版实现只写前三项结果测试阶段发现Agent虽然选对了技能、填对了参数但执行体里遇到意外情况就直接崩溃因为没写错误处理的逻辑。所以执行体的自包含性是硬要求。2.2 技能、工具和API的边界到底在哪这个图景里最让人混淆的就是Skill、Tool、API这三者的区别。我自己的理解是这样的API是最底层的原子能力比如查询用户信息、发送邮件Tool是把API或函数包上一层可供模型调用的外壳参数Schema描述本质还是一个动作Skill则是面向任务的、可能编排多个Tool或动作的能力单元它有自己的状态、异常处理策略和产出物。举个例子。一个Tool可能是get_weather(city)一个Skill可能是plan_travel(出发地, 目的地, 日期)。后者内部可能要查天气、查交通方式、查酒店价格、计算总预算还要把结果组装成行程表。对上层Agent来说它不需要关心plan_travel内部调了哪几个API只需要知道它能帮用户做行程规划就行。这样分层的好处非常明显Agent的主循环代码会变得非常干净。它只需要做三件事理解意图、检索匹配技能、执行并汇报结果。所有的领域逻辑都被封装在技能内部新增一个业务能力时不用动主循环往技能库里注册一个新Skill就行。这对项目后续的扩展和维护来说是质的提升。2.3 技能注册表让Agent能找到你要提供给它的一切能力有了单个Skill的抽象下一个问题是这些技能集中放在哪、怎么被检索。我推荐用一个轻量级的注册表Registry模式本质上就是一个内存里的字典key是技能名value是技能对象。注册表负责三件事登记把技能挂进来、索引按名称/描述/标签建索引方便检索、路由给定一个意图查询返回最佳匹配技能列表。注册表还可以承担一个很实际的功能技能过滤。不同场景下Agent能看到的技能子集不一样。比如给客服机器人注册了100个技能但实际处理退款咨询时只需要其中15个那就在检索前先用上下文把候选范围缩小。这听起来像是优化细节但实际作用非常大——候选技能越少模型选错的可能性就越低响应延迟也会明显下降。3. 实操手搭一套最小可用的agent-skills系统3.1 基础数据结构和注册逻辑直接上代码。我用Python写一个最小实现不依赖任何Agent框架这样你能看清底层机制。from dataclasses import dataclass, field from typing import Callable, Any, Dict, List, Optional dataclass class SkillSpec: name: str description: str parameters_schema: dict handler: Callable[..., Any] version: str 1.0.0 examples: List[dict] field(default_factorylist) tags: List[str] field(default_factorylist) def render_manifest(self) - str: 把技能信息渲染成一段给LLM看的文本用于触发判断和参数抽取。 lines [ f技能名称: {self.name}, f技能描述: {self.description}, 参数说明:, ] for prop, meta in self.parameters_schema.get(properties, {}).items(): required prop in self.parameters_schema.get(required, []) lines.append( f- {prop} ({meta.get(type, string)}, f{必填 if required else 可选}): {meta.get(description, )} ) if self.examples: lines.append(调用示例:) for ex in self.examples: lines.append(f - {ex}) return \n.join(lines)这里最关键的字段是description和parameters_schema。在训练LLM选择工具的实践中这两个字段几乎决定了选型准确率。描述要让模型一眼看懂何时该用参数Schema要让模型不靠猜就能把参数填完整。后面我会专门讲描述怎么写这里是数据结构基础。注册表实现如下class SkillRegistry: def __init__(self): self._skills: Dict[str, SkillSpec] {} self._tags_index: Dict[str, List[str]] {} def register(self, skill: SkillSpec) - None: if skill.name in self._skills: raise ValueError(f技能 {skill.name} 已存在请检查命名冲突) self._skills[skill.name] skill for tag in skill.tags: self._tags_index.setdefault(tag, []).append(skill.name) def unregister(self, name: str) - None: if name not in self._skills: return skill self._skills.pop(name) for tag in skill.tags: self._tags_index[tag].remove(name) if not self._tags_index[tag]: del self._tags_index[tag] def get(self, name: str) - Optional[SkillSpec]: return self._skills.get(name) def list_all(self) - List[SkillSpec]: return list(self._skills.values()) def filter_by_tags(self, tags: List[str]) - List[SkillSpec]: merged set() for tag in tags: merged.update(self._tags_index.get(tag, [])) return [self._skills[name] for name in merged if name in self._skills]注册时检查命名冲突是非常必要的。我踩过一个真实的坑两个不同模块的开发者都注册了一个叫search的技能结果后注册的静默覆盖了前者Agent调用的其实是另一个模块的实现排查了很久才发现。所以注册表里这种Fail-Fast设计能帮你在开发期就暴露问题。3.2 参数Schema用JSON Schema约束让模型不靠猜填参数参数Schema这块我直接用JSON Schema子集不用自定义格式。原因很简单LLM对JSON Schema的熟悉程度最高各家Function Calling的生态也兼容这个格式。下面是一个查询订单状态的技能参数定义order_status_schema { type: object, properties: { order_id: { type: string, description: 用户提供的订单号通常是以OR开头的12位字符串, }, customer_phone: { type: string, description: 用户手机号用于身份核验有时可以从对话上下文获取, pattern: ^1[3-9]\\d{9}$, }, include_history: { type: boolean, description: 是否返回完整的历史流转记录默认false只返回最新状态, default: False, }, }, required: [order_id], }这里有几个容易被忽略的细节。第一description里不只写参数含义还要写从哪获取——是从用户原话里抽、还是从对话历史里找、还是取默认值。第二能加正则的就加正则能加default就加default这能显著降低模型填空时的自由度。第三required里只放真正必要的参数可选项全部给出默认值。你给模型越多必填项模型就越容易编造一个值去填所以尽量用默认值减少必填项是提升参数准确率的关键。3.3 一个具体技能的执行体查询订单状态执行体我建议采用输入严格、输出干净的策略。内部可以做任何事但对外暴露的返回值必须是结构化的、可被上层直接消费的import json import re from datetime import datetime def query_order_status(order_id: str, customer_phone: str , include_history: bool False) - dict: # 技能内部可以有完整的业务逻辑 if not re.fullmatch(rOR\d{10}, order_id): return { status: fail, code: INVALID_ORDER_ID, message: f订单号格式不正确: {order_id}, } # 模拟业务系统查询 mock_db { OR20241015001: { order_id: OR20241015001, phone: 13800000000, latest_status: 已发货, timeline: [ {time: 2024-10-15 10:23:00, status: 已付款}, {time: 2024-10-16 14:30:00, status: 商家已发货}, ], } } record mock_db.get(order_id) if record is None: return {status: fail, code: ORDER_NOT_FOUND, message: 查无此订单请核对订单号后重试} if customer_phone and record[phone] ! customer_phone: return {status: fail, code: PHONE_MISMATCH, message: 手机号与订单信息不一致无法展示详情} result { status: success, order_id: order_id, latest_status: record[latest_status], query_time: datetime.now().isoformat(), } if include_history: result[timeline] record[timeline] return result看到这里你会发现执行体本身和普通业务函数没什么区别这是对的。执行体不需要感知我是被LLM调用的它只需要是一个健壮的业务函数。真正让它变成技能的是外面包的那层描述、参数契约和注册行为。4. 技能运行时的关键环节意图解析、匹配与执行4.1 Agent主循环简化到极致的编排逻辑要让Agent用上技能库需要在每一次用户请求中完成四个步骤解析意图、候选技能生成、参数填充/调用、结果整理。一个不依赖重型框架的主循环可以长这样class SkillAgent: def __init__(self, registry: SkillRegistry, llm_fn): self.registry registry self.llm_fn llm_fn def _select_skills(self, user_input: str, context: dict) - List[SkillSpec]: # 1. 先用规则粗筛如标签过滤 candidates self.registry.list_all() # 2. 把技能列表渲染成紧凑文本交给LLM做匹配 manifests [] for skill in candidates: manifests.append(skill.render_manifest()) prompt ( 用户请求如下\n f{user_input}\n\n 以下是当前可用的技能清单\n \n.join(manifests) \n\n请选择最合适的技能名称最多选3个只输出技能名用逗号分隔。 如没有合适的技能输出 NONE。 ) response self.llm_fn(prompt) if response.strip() NONE: return [] selected_names [n.strip() for n in response.split(,) if n.strip()] return [self.registry.get(n) for n in selected_names if self.registry.get(n)] def _fill_and_run(self, skill: SkillSpec, user_input: str, context: dict) - dict: # 让LLM根据用户输入和参数Schema生成参数JSON param_prompt ( f技能 {skill.name} 的参数Schema如下\n f{json.dumps(skill.parameters_schema, ensure_asciiFalse, indent2)}\n f根据用户请求和上下文填充参数只输出JSON不要输出解释\n{user_input} ) param_raw self.llm_fn(param_prompt) try: params json.loads(param_raw) except json.JSONDecodeError: # 从LLM输出中用正则提取JSON块作为兜底手段 match re.search(r\{.*\}, param_raw, re.S) if not match: return {status: fail, code: PARAM_PARSE_ERROR, message: 无法解析模型生成的参数} params json.loads(match.group(0)) try: return skill.handler(**params) except TypeError as e: return {status: fail, code: HANDLER_CALL_ERROR, message: f参数不匹配: {e}}这里有个非常实用的设计细节把技能选择路由和参数填充分成两次LLM调用。之前我尝试过一次调用同时输出技能选择和参数模型写出来的JSON结构经常出错。拆成两步后每一步的输出格式都极其简单模型的成功率大幅提升。代价是多一次LLM调用延迟增加一些但现在模型的响应速度完全能接受。4.2 技能匹配准确率提升从靠运气到靠机制我在实践中总结出三个让技能匹配更稳的做法。第一个是描述里主动写清负面触发场景。比如天气查询技能描述里不仅要写当用户询问天气、气温、降雨概率、空气质量时使用本技能还要写当用户询问昨天的气温历史或未来两周气温趋势时不要使用本技能。模型特别容易根据关键词过度联想气温趋势和天气看似相关但实际需要的是另一个数据分析技能。负面场景写清楚了误触发率能降好几个百分点。第二个是用示例锚定参数格式。每次注册技能时把典型调用示例写在技能对象里。这样在渲染技能清单时模型能看到用户问查一下OR20241015001走到哪了时参数填{order_id: OR20241015001}它的模仿能力会帮它把类似问题抽成同样结构。示例数量不用多每条技能两三个就够。第三个是候选技能列表的动态压缩。我上面代码里是有一步用规则过滤候选集的这一步千万别省。比如当前对话发生在订单中心页面那可以直接把候选技能限定为订单相关标签。这既减少LLM的决策负担也能防止跨领域技能串味。实测下来把候选从30个减到5个以内选型准确率能提升10%到15%。4.3 给执行体包一层安全毯统一做错误捕获有一件事我第一次做Agent项目时没做后来吃了很多苦头就是执行体的异常必须全部在技能层消化。原因很简单如果技能抛异常上层要么拿到一个stack trace要么整个Agent流程崩溃用户会得到一段莫名其妙的系统错误。正确的做法是执行体内部捕获所有预期内异常返回统一结构技能调用入口再包一层兜底捕获防止未预期异常穿透到Agent层。def safe_execute(skill: SkillSpec, params: dict) - dict: try: result skill.handler(**params) if isinstance(result, dict): return result return {status: success, data: result} except Exception as e: # 兜底捕获避免Agent流程直接崩溃 return { status: fail, code: UNEXPECTED_ERROR, message: f{skill.name} 执行异常: {str(e)}, }这一段代码的价值在于所有技能失败都会转成一个干净的、Agent能理解的字典。Agent拿到这个字典后可以根据code字段决定下一步——比如ORDER_NOT_FOUND就回复用户查无此单UNEXPECTED_ERROR就回系统暂时开小差请稍后重试。这样用户永远看不到底层异常体验会好非常多。5. 常见问题与排查实录5.1 技能匹配不准明明是天气问题Agent却选了订机票技能这是最常遇到的问题。我排查的思路一般分三步。第一步检查描述长短和风格。如果技能描述写得像产品需求文档满篇本模块旨在...模型很难识别出触发条件。描述要口语化、直白像给实习生交代活。第二步检查候选集是否过大。如果一次给模型三四十个技能它选错的概率天然就高优先用标签或上下文把候选缩小。第三步加few-shot示例。我会把最近一次匹配失误的真实case做成反例写进技能描述的负面场景里下次模型就会避开。如果三步都做了还是不准就要考虑是不是两个技能的能力边界确实重叠了。比如既有一个查天气技能又有一个查空气质量技能用户问今天适合跑步吗模型不知道该选哪个这种情况下可以在高层做一个运动建议技能编排底层两个技能比强迫模型在底层做选择更可靠。5.2 参数填充错误模型把用户手机号误填成订单号在Parameter填充阶段模型把参数糊弄过去是第二大痛点。表面看是模型笨实际是Schema设计的问题。按照我3.2节说的每个参数描述里要写清楚数据类型格式示例获取来源给出明确的提取路径。另外我发现给参数加pattern约束能显著减少格式错误而给参数加上错误的示例比如明明期望字符串却在description里给出数字示例也会误导模型。所以每次新加技能我会用二三十条测试输入去跑一遍参数填充凡是出错率高的字段就在描述里补充更多的正反示例来喂给模型相当于一次离线优化。5.3 执行体偶发崩溃技能用得好好的突然报错技能上线后不定时崩溃十有八九是外部依赖不稳定。我的建议是执行体所有IO部分都要设置超时和降级逻辑。比如查订单状态的技能内部调用了外部订单中心的接口该接口偶尔会慢或超时那在执行体里需要把对外调用包上超时控制和兜底返回。同时我强烈建议对每次技能调用的耗时、参数、返回值做日志埋点。这些日志是后续优化的数据基础——哪些技能调用最多、哪些参数经常解析失败、哪些技能耗时最长全都能从日志里看出来。5.4 常见问题速查表问题可能原因快速解法Agent选择了错误技能描述未写清楚触发场景候选集过大重写描述增加负面触发条件利用标签预过滤参数老是填错Schema描述模糊缺格式约束每个参数写清类型、示例、来源增加正则约束执行体频繁抛异常依赖不稳无兜底逻辑执行体加超时、重试和统一错误返回技能库里技能越多越混乱缺少分类和检索策略用标签、命名空间组织按场景动态过滤多个技能功能重叠能力边界定义不清在更高层做编排而不是让模型硬选6. 把技能描述写成对模型友好的说明书技能描述是很容易被忽视、但性价比最高的优化点。我写技能描述有一个固定的模板每次写都往里套当用户{触发场景描述}时使用本技能。 输入要求{一句话说明需要什么信息}。 输出形式{一句话说明会给用户返回什么}。 不要在本技能处理{负面场景列表}。举个例子一个生成周报技能的描述可以这样写当用户提出把本周工作情况整理成周报、需要发给主管或归档时使用本技能。用户可能提供零散的工作事项、已完成任务列表或项目进展描述本技能会将内容按本周完成、正在进行、风险与阻塞、下周计划四段结构整理成Markdown格式文档并输出可直接复制的周报文本。请勿在用户要求日报、月报或仅需简单提醒事项时使用本技能。描述里信息密度很足触发词周报、边界词日报、月报不归我管、输入容忍范围零散描述也能处理、输出格式Markdown四段结构。这种描述对模型来说就像按钮上贴了清晰的标签它不需要猜。还有个实战技巧写完描述之后一定拿它跑一次离线评测。我习惯准备二三十条每类技能的典型问题跑一遍选择准确率低于95%就继续调描述。准确率提升往往不在大改而在细节词。7. 从工具到技能我踩过坑后的重新理解最后再说点我自己的理解。最早我做Agent项目时把所有能力做成一个大工具箱结果模型经常拿错工具。后来开始按业务场景拆分但发现拆太细又导致技能数量爆炸维护成本上升。直到把技能视为一个带文档、带契约、带异常处理的完整模块才算找到了平衡点每个技能有明确的责任边界对外暴露的接口简单内部复杂度可控。给正在搭技能体系的开发者几个实在建议第一无论用什么框架技能库的注册和检索逻辑最好是独立的模块不要和Agent主流程耦合死因为你会频繁调整检索策略。第二给技能做版本管理哪怕只是简单的version字段因为技能行为的变化会直接影响Agent的表现版本字段能帮你在回归时定位问题。第三每次Agent选了错技能或填错参数一定要把case记录下来专门用它来改进描述或Schema这种持续优化比换更大的模型还管用。我自己的项目里技能库从最早的7个技能长到了现在的40多个Agent主循环几乎没有改过新业务全都是通过加技能扩展的。这个架构带来的长期收益在我实际开发中比一切花哨的Agent编排都更实在。