
做AI Agent半年多我踩过最大的坑就是模型明明能听懂人话却干不好实事。后来才想明白缺的不是聪明的大脑而是一套结构化的技能体系——也就是项目标题里写的agent-skills。这个东西现在成了我所有Agent项目的底座。它不解决“模型会不会思考”的问题只解决一件事让Agent真正“会干活”。这篇东西没有理论绕弯子全是实操经验适合正在搭Agent应用、被模型乱调用工具气得头疼的开发者。你不需要懂强化学习也不需要啃几百页论文照着这套思路去整理你的技能库Agent的稳定性能肉眼可见地提升。1. 整体设计与思路拆解1.1 先说清楚agent-skills 到底解决了什么问题我在最早做Agent的时候思路特别简单粗暴把一堆工具函数丢给模型然后在Prompt里写“你可以调用这些工具”完事。结果非常惨淡——模型经常在错误的场景调用错误的工具有时候把参数传得乱七八糟更有意思的是它会在不需要调用工具的时候强行调用。后来我翻了一些开源项目发现老手们几乎都在做同一件事对工具做一层“技能化”封装。也就是说不直接给模型一堆零散的函数而是给模型一套经过设计的、有明确使用边界的“技能卡”。agent-skills 核心要解决三个问题上下文过载工具太多塞进Prompt里既费token又让模型注意力分散。技能化之后把工具按场景分组按需加载。调用不可控没有任何约束时模型调用工具的随机性太强。技能体系给模型画了一条“什么场景用什么技能”的路。复用困难没有统一的封装换个项目所有工具都要重写。技能化之后技能本身是独立的跟具体业务解耦。一句话agent-skills 的本质是在模型和工具之间加了一层“行为准则层”让大模型不再胡乱伸手而是按规矩办事。1.2 为什么不能只靠 Prompt 硬堆很多人的第一反应是我多写几句Prompt不就行了我试过真的不行。Prompt 是自然语言大模型对自然语言的解读本身就有随机性。你写“请谨慎调用工具”它可能理解成“尽量少调用”你写“只在必要时调用”它可能觉得“用户问天气也算必要”。而且 Prompt 越长模型对关键指令的注意力就越会被稀释。实测下来超过一定长度后模型对工具调用的准确率是下降的不是上升的。技能化的做法是变“劝说”为“约束”。我不需要告诉模型“你要谨慎”而是把工具包装成一个带“触发条件”和“参数约束”的结构化对象。模型面对的不是一段模糊的文字指示而是一组明确的规则。结构化信息的约束力远强于自然语言这是 agent-skills 架构的第一性原理。1.3 关键词“agent-skills”在项目里的具体落点那 agent-skills 在我们的实际项目里到底长什么样展开之前我先给一个全局的技术分层技能层Skill Layer单个技能的定义与实现包含触发条件、参数Schema、执行函数。注册中心Skill Registry所有技能的元数据仓库负责技能发现和按需加载。编排层Orchestration根据用户输入和上下文选择合适的技能并组织调用顺序。执行层Execution真正跑技能的Python函数或外部API调用。这套架构的好处是每一层都能独立测试和迭代。我最初把所有逻辑塞在一起的时候出了一个bug根本不知道是模型选错了技能还是执行函数出了问题。分层之后每一层的职责边界清楚了排查效率翻倍。2. 核心细节解析与实操要点2.1 技能定义一份技能卡需要包含哪些字段技能不是简单地把函数名丢给模型一份标准的技能卡我建议包含以下核心字段name技能的唯一标识简洁、语义化。description一段给模型看的自然语言描述说明“这个技能是做什么的”、“在什么场景下用”。trigger_conditions触发条件明确告诉模型“当出现哪些情况时必须调用这个技能”。parameters参数Schema定义入参的名称、类型、必填性、取值范围。output_schema输出Schema定义返回结果的结构。examples几个典型案例直接示范“什么输入该调用”、“调用的参数长什么样”。这里有个很容易被忽略的点description 的写法。很多人的description写得过于泛化比如“查询天气信息的工具”。模型完全不知道什么时候该用它。我后来改成“当用户询问任何地理位置的当前天气或未来天气预报时使用此技能。如果用户没有提供地理位置先询问位置再调用”。改完之后调用准确率一下提升了。2.2 参数约束用结构化约束代替自然语言解释参数定义是最容易出问题的环节。大模型在生成参数时经常出现以下情况把字符串传成JSON对象把日期格式从 YYYY-MM-DD 传成 2024年3月5日漏传必填参数解决办法是严格使用 JSON Schema 定义参数而且不要嫌麻烦。每个参数都定义 type、description、enum可枚举、minimum/maximum数值范围、pattern正则。比如日期参数我会在Pattern里写死格式约束模型在生成参数时看到正则绝大多数情况下会遵循。这比在description里写一百遍“请使用YYYY-MM-DD格式”管用得多。另外我强烈建议给所有参数加一个required标记对于非必填参数默认值也要在Schema里写清楚。模型是最会偷懒的你不在Schema层面约束它它就自己发挥。2.3 技能发现如何从“全量加载”到“按需加载”早期我把30个工具全塞进Prompt效果差且烧钱。后来改成技能注册中心加路由的方式。我的做法是先把所有技能的 description 存进一个向量数据库用户输入进来先做向量检索找出Top5相关技能只把这5个技能的技能卡拼进Prompt。这个思路有点像RAG但它检索的是“能力描述”而不是“知识片段”。实现上不复杂用任何embedding模型都可以。关键是检索条件要考虑两点用户输入的字面意思。对话历史的意图比如用户之前一直在查天气现在问“明天呢”这时候要继续命中天气技能。这个按需加载机制上线后Prompt长度平均减少了70%调用准确率提升了20个百分点。token成本也降了一大截。3. 实操过程与核心环节实现3.1 最小可用的技能系统一把梭的底座代码我直接给一套最简实现用Python写跑通整个流程大概需要两百行代码。这个系统不复杂但完整覆盖技能定义、注册、发现、调用四个环节。首先定义技能基类from typing import Any, Callable, Dict, Optional import json class Skill: def __init__( self, name: str, description: str, trigger_conditions: str, parameters_schema: dict, output_schema: dict, examples: list[dict], handler: Callable[..., Any], ): self.name name self.description description self.trigger_conditions trigger_conditions self.parameters_schema parameters_schema self.output_schema output_schema self.examples examples self.handler handler def get_skill_card(self) - str: 生成最终拼进 Prompt 的技能卡 return f ### {self.name} 描述: {self.description} 触发条件: {self.trigger_conditions} 参数定义(JSON Schema): {json.dumps(self.parameters_schema, ensure_asciiFalse, indent2)} 示例: {json.dumps(self.examples, ensure_asciiFalse, indent2)} 然后是注册中心class SkillRegistry: def __init__(self): self._skills: Dict[str, Skill] {} def register(self, skill: Skill): self._skills[skill.name] skill def get(self, name: str) - Optional[Skill]: return self._skills.get(name) def list_all(self) - list[Skill]: return list(self._skills.values())有了这两个类你就可以注册技能了。我建议把所有技能放在一个独立的skills/目录里每个技能一个文件用装饰器注册from skill_registry import registry from skill import Skill def weather_handler(city: str, date: str): # 这里写你的业务逻辑 return {city: city, date: date, weather: 晴} weather_skill Skill( namequery_weather, description查询指定城市在指定日期的天气情况, trigger_conditions当用户直接或间接询问天气时使用。注意城市和日期缺一不可若用户未提供必须反问。, parameters_schema{ type: object, properties: { city: {type: string, description: 城市名如 北京}, date: {type: string, pattern: ^\\d{4}-\\d{2}-\\d{2}$, description: 日期格式 YYYY-MM-DD} }, required: [city, date] }, output_schema{ type: object, properties: { city: {type: string}, date: {type: string}, weather: {type: string} } }, examples[ {input: 北京明天天气如何, function_call: {name: query_weather, arguments: {city: 北京, date: 2025-03-08}}} ], handlerweather_handler ) registry.register(weather_skill)3.2 模型函数调用Function Calling的正确接入方式技能注册好了接下来要接大模型。以OpenAI格式为例需要把技能卡转换成函数调用格式def skill_to_function(skill: Skill) - dict: return { type: function, function: { name: skill.name, description: skill.description \n触发条件: skill.trigger_conditions, parameters: skill.parameters_schema } }然后每次请求时按检索结果动态拼装from openai import OpenAI client OpenAI() def agent_run(user_input: str, registry: SkillRegistry, top_k: int 5): # 简化版直接全量匹配。生产环境建议优先做向量检索 skills registry.list_all() messages [{role: system, content: 你是一个智能助手请根据场景选择合适的技能。技能列表如下: }] for skill in skills: messages.append({role: system, content: skill.get_skill_card()}) messages.append({role: user, content: user_input}) response client.chat.completions.create( modelgpt-4o, messagesmessages, tools[skill_to_function(skill) for skill in skills], tool_choiceauto, ) return response注意一个细节技能卡放在system message里tools里放函数的结构化定义。这两个东西要对应让模型既能读懂技能的语义说明又能按结构化信息去生成调用参数。模型返回之后如果response.choices[0].message.tool_calls不为空就解析并执行def execute_tool_calls(response, registry: SkillRegistry): message response.choices[0].message if not message.tool_calls: return None results [] for tool_call in message.tool_calls: skill_name tool_call.function.name arguments json.loads(tool_call.function.arguments) skill registry.get(skill_name) if not skill: results.append({error: funknown skill: {skill_name}}) continue # 校验参数 from jsonschema import validate, ValidationError try: validate(instancearguments, schemaskill.parameters_schema) except ValidationError as e: results.append({error: str(e)}) continue result skill.handler(**arguments) results.append({skill: skill_name, result: result}) return results执行完之后把结果作为tool消息回传给模型让它生成最终回复。这步别漏了。3.3 技能编排让Agent具备多步操作能力单个技能调用只是基础。真正复杂的场景是连环调用比如“帮我订一杯拿铁送到公司然后通知我同事”。这个流程涉及查询咖啡店技能、下单技能、查询公司地址技能、发送消息技能。模型需要自己规划调用顺序。我经验是不要让模型一次性生成多步计划再执行而是让它一步一步来。每走一步把当前状态反馈给模型由它决定下一步调用什么。原因有两个模型的一次性计划通常很理想化一旦执行中某一步失败整个计划就废了。一步一步调用每一步的错误信息都能回传给模型模型可以即时调整策略。下面是逐步执行的伪代码def agent_loop(user_input: str, registry: SkillRegistry, max_steps: int 5): messages [{role: user, content: user_input}] for step in range(max_steps): response client.chat.completions.create( modelgpt-4o, messagesmessages, tools[skill_to_function(skill) for skill in registry.list_all()], tool_choiceauto, ) message response.choices[0].message messages.append(message) if not message.tool_calls: break for tool_call in message.tool_calls: skill registry.get(tool_call.function.name) arguments json.loads(tool_call.function.arguments) result skill.handler(**arguments) messages.append({ role: tool, tool_call_id: tool_call.id, content: json.dumps(result, ensure_asciiFalse) }) return messages[-1].content建议给循环加一个max_steps上限别让Agent无限空转。我见过模型在一个循环里因为一个错误参数反复调用同一个技能七次最后把API额度烧完了。3.4 技能库的组织结构按场景域拆分技能一多目录结构就很重要。不整理到最后你会疯掉的。我自己现在用的组织方式skills/ ├── common/ # 通用技能时间、日期、计算等 ├── weather/ # 天气相关 ├── communication/ # 邮件、消息等 ├── ecommerce/ # 商品查询、下单、物流 ├── enterprise/ # 内部系统对接 └── experimental/ # 未稳定发布的实验技能每个目录下至少有__init__.py技能注册入口xxx_skill.py技能实现test_xxx.py离线测试脚本技能不是写完就完事了它是会迭代的。我要求所有技能必须带测试不然半年后技能库维护起来会想哭。4. 常见问题与排查技巧实录4.1 模型死活不调用技能怎么办这个问题太常见了。模型宁可自己瞎编答案也不愿意调用工具。遇到这种情况优先检查三件事技能描述里有没有明确触发条件。没写触发条件模型不敢用。示例够不够。给一个正例不如给“输入期望调用期望参数”三个元素的完整示例。system prompt 有没有提醒。在系统提示词里加一句“当用户需求在你能力之外但存在对应技能时必须先调用技能”。另外一个隐蔽的原因模型版本太老。有些旧模型的函数调用能力非常弱换个新版本模型问题直接消失。4.2 参数幻觉问题模型编造不存在的参数值比如用户的输入是“查询明天的天气”模型调用技能的时候把城市名填成“明天”。这就是参数幻觉。解决思路是收紧Schema 增加反问日期参数用正则限定\d{4}-\d{2}-\d{2}模糊匹配就过不了校验。城市参数如果业务上支持枚举就用enum限定。更狠的做法是在触发条件里写明“如果用户提供的参数不完整不要调用技能直接回复反问句”。参数校验不通过时不要静默失败要把校验错误信息以tool消息的形式回传给模型让它“重新读题、重新填参数”。很多模型看到校验错误后会自动意识到自己传错了重新生成正确参数。4.3 技能调用循环空转与死锁模型规划错了可能陷入一个循环调技能A失败重试还是失败又调技能A再失败。我看过最夸张的一次一个Agent在五分钟内调用了同一个不存在的技能名十几次。两个对策全局步数上限前面代码里的max_steps强制终止。重试次数上限记录每个技能的连续失败次数超过2次就切换策略比如直接调一个兜底回复技能不再让模型自动决策。4.4 技能编排顺序混乱有些场景有严格的先后顺序比如必须“先登录后下单”“先查库存后加购物车”。模型有时候会打乱顺序。我建议在技能定义里除了“触发条件”再增加一个“前置条件”字段requires: [auth_login]编排层在读技能定义时发现前置技能未执行可以把前置技能的技能卡高优先级插入Prompt引导模型先做前置调用。这本质上是状态感知的编排实现起来不难但效果提升很明显。4.5 技能冲突多个技能都命中同一个用户输入怎么办比如用户说“帮我看看明天怎么安排”既可能触发“日程查询”技能也可能触发“天气查询”技能。模型在两个技能之间摇摆不定有时候会两个都调用浪费上下文。我给每个技能定义了一个priority字段冲突的时候路由逻辑先看优先级再看语义相似度。比如日程查询的优先级标记为5天气查询标记为3模型会倾向选日程查询。这也避免了一部分随机性。5. 技能评估与持续迭代5.1 建一个回归测试集比什么都重要技能改了一次不知道有没有改坏别的地方这是最痛苦的事情。我后来专门建了一个evaluation/目录里面放了几百条测试用例干净的输入应该精确命中一个技能模糊的输入应该触发反问多技能输入应该触发多步编排边界输入缺参数、格式错误、极端值每次迭代技能库先跑一遍回归测试。准确率低于阈值就不允许发布。我强烈建议把评估集的结果指标和技能库版本一起记录下来用表格形式维护技能名测试数调用准确率参数合法率平均延迟备注query_weather8696.5%98.8%312ms正常send_email3488.2%94.1%284ms描述待优化数据会告诉你哪个技能需要打磨不用靠感觉。5.2 技能版本管理与发布策略技能也有版本。我现在用的是非常简单但有效的机制技能注册表里每个技能带version字段发布新版本时旧版本不删除保留在历史版本列表里。线上系统固定引用某个版本新版本先在实验环境跑评估集指标达标再切流量。这个机制帮了我大忙。有一次我改了一个日期处理技能自测没问题上线后用户反馈说是非颠倒切回旧版本问题立刻消失。5.3 从“工具”到“技能”的思维转变最后聊一个观念上的东西。我见过很多人做Agent始终停留在“给模型加工具”的思维。这个思维没有错但它缺少一个关键维度工具是死的技能是活的。工具只描述“我能做什么”技能描述的是一个完整的决策单元什么时候用、怎么用、参数如何组合、失败了怎么兜底。这中间多出来的“使用边界”和“决策逻辑”才是Agent稳定性的真正来源。我现在的习惯是每接一个新需求先不写代码而是先写一页“技能说明书”把触发条件、参数约束、边界场景想清楚再动手实现。前期花点时间后面省十倍调试时间。关于 agent-skills 这套体系我暂时就分享到这里。这不算什么高深理论但这套方法论已经让我手上的Agent项目从“花架子”变成了“真能用”。你如果正在搭自己的技能库我唯一的建议是不要先贪多先把三五个核心技能的描述和参数打磨到极致跑通流程再慢慢扩充。技能库越大管理成本越高前期的结构设计越要用心。