你有没有遇到过这种情况给大模型接了一个插件工具结果它死活不用或者用了但参数传得乱七八糟最后输出的结果还不如直接问它。这不是模型笨是你的技能设计有问题。我在这块踩了不少坑之后逐渐总结出一套比较靠谱的Agent技能封装与管理方法也就是这次想聊的agent-skills这个方向。先明确一点Agent Skills本质上解决的是怎么让大模型稳定地使用外部能力这件事。它把工具调用从随手塞进Prompt升级成一套标准化、可注册、可编排、可评估的技能系统。这套东西适合谁适合正在做Agent应用开发的工程师、想给LLM接入内部系统的后端同学以及所有被模型不会用工具折磨过的人。下面我从前期的设计思路到核心代码实现再到实际踩坑的记录完整拆一遍。1. 项目定位与整体设计思路拆解1.1 从会聊天到会干活Agent Skills到底解决什么问题大语言模型本身只是一个文本生成器。你问它帮我看下这周的销售数据并总结异常它只能给你一份说得头头是道、但实际数据全是编的回复。要让模型真正干活就必须给它接上外部工具查询数据库、调用API、操作文件、发送消息。这一步大多数人都做过了但我见过太多团队把工具定义写得极其随意——函数名乱起、参数说明含糊、返回值结构千奇百怪。结果就是模型在调用时频繁选错工具、漏传参数整个Agent形同虚设。这个项目的出发点就很朴素把模型能用什么技能这件事当成一个正经的系统来设计而不是临时拼凑。什么叫正经第一技能的定义必须结构化有统一的元信息规范第二技能的注册和发现要自动化不能每次加一个工具就改一堆代码第三技能之间要有可组合性能从简单技能编排成复杂技能第四技能的使用效果要能评估、可观测出了问题能找到原因。我把这套思路命名为技能层思维。LLM是大脑技能层就是神经系统——大脑发出指令神经把指令准确传递给手脚。如果神经传导有问题大脑再聪明也做不了事。1.2 一套合格的技能系统该具备哪些能力我实践下来一个成熟的Agent技能系统至少要覆盖五个方面技能定义标准化每个技能都包含名称、描述、参数Schema、执行函数、返回结果规范。这是地基定义不规范后面全乱。技能注册自动化开发人员只需要按约定写一个函数加一段声明系统启动时自动扫描加载不用手工维护一份工具清单。技能选择准确化系统要能让LLM在合适的场景选到合适的技能。这依赖命名的直觉性和描述的详细程度。技能编排可组合单个技能解决单点问题多个技能组合起来才能完成复杂任务比如检索资料→撰写摘要→翻译成英文。技能效果可评估每次调用都要有日志记录模型选择了哪个技能、传了什么参数、执行结果如何。没有日志出了问题你只能靠猜。这五条如果我早点想明白前几个项目能少走两三个月的弯路。尤其是可评估这一点很多人忽略觉得能跑就行但Agent应用和传统程序的本质区别就在于它的行为有随机性不观测就无法迭代。2. 核心细节解析与方案选型2.1 技能定义格式怎么让模型看得懂你的工具这是整个技能系统里最关键、也最容易被低估的一环。很多时候模型不调用工具不是模型笨是你给技能写的描述太抽象了。一个标准的技能定义通常包含四部分名称name、描述description、参数parameters、实现implementation。我见过不少半路出家的方案只写函数名和参数列表完全不写描述或者描述就一句话查询用户信息。这种描述丢给LLM它根本不知道什么时候该用你。举个例子假设你要写一个根据城市名查询天气的技能。一个不合格的描述是获取天气信息合格的描述应该类似查询指定城市当前天气情况。当用户询问某地天气、气温、降水、风力时使用。输入应为中文城市名或城市名加市如北京、上海市。若用户未指定城市请先询问用户。看见了没你要告诉模型这个技能是干什么的、什么场景下用、输入长什么样、输入缺失时怎么办。参数定义方面我强烈建议用Pydantic或者JSON Schema来约束别手写。比如from pydantic import BaseModel, Field class WeatherInput(BaseModel): city: str Field(description城市名如北京、上海, example杭州) days: int Field(default1, ge1, le7, description预报天数1-7之间)这个过程实际上是把你脑子里的业务逻辑翻译成大模型能理解的语言。翻译得好不好直接决定技能调用成功率的上下限。2.2 技能注册与发现机制新增一个技能要几步技能系统用起来顺不顺就看加新技能的成本。最原始的方案是改一个巨大的tools列表把新函数加进去。但这种做法在技能超过20个以后就开始失控命名冲突、参数风格不一、维护成本飙升。我采用的是目录扫描 装饰器注册的组合。每个技能一个文件夹里面是独立的Python模块模块里用装饰器声明技能。系统启动时自动扫描技能目录收集所有标记过的函数生成结构化清单。整个过程新增技能只需要两步创建一个py文件写实现函数并加上装饰器。# skills/weather.py from agent_skills import register register( namequery_weather, description查询指定城市当前天气情况。当用户问及天气、气温、降水、风力时使用。, input_schemaWeatherInput ) def query_weather(city: str, days: int 1) - dict: # 调用天气API返回结构化数据 return {city: city, days: days, data: ...}这个设计最大的好处是约定大于配置。团队成员不需要理解框架内部的加载逻辑只要按约定写文件就行。系统自动把这些技能统一编入索引送给LLM做Function Calling。目录结构本身就是技能库的目录页可维护性比单文件堆函数高一个量级。2.3 技术选型站在巨人的肩膀上还是自己造轮子关于底层实现现在已经有非常成熟的方案没必要从零搞。OpenAI的Function Calling、Anthropic的Tool Use、各大框架自带的工具机制本质上都是在做同一件事把结构化工具声明传给模型让模型按JSON格式输出调用指令然后你接管执行。我在项目中最终采用了OpenAI Function Calling 自研注册与编排层的组合。为什么不全用现成框架因为LangChain这类框架的工具定义机制虽然好用但在技能编排和权限控制方面还是偏薄而且每次更新都可能引入breaking changes对生产环境不够友好。自研一层薄薄的封装底层调用还是走官方SDK风险和可控性都更理想。还得提一下MCPModel Context Protocol这个方向如果你做的是跨团队、跨系统的技能共享MCP标准会很有价值。但如果你只是自己产品里的Agent在用前期不必上MCP直接Function Calling更轻量、调试也方便。我建议先把本地技能系统跑通再考虑协议标准化一步到位反而容易卡住。3. 实操过程与核心环节实现3.1 从零搭一个最小可用的技能注册器下面我就把整套系统的核心代码拆开讲这部分你完全可以照着抄去搭自己的底座。首先定义技能的数据结构。我用attrs或者Pydantic都可以这里用Pydantic示范因为它自带Schema生成能力方便对接Function Calling。# agent_skills/base.py from typing import Callable, Any, Optional from pydantic import BaseModel, create_model class SkillDefinition(BaseModel): name: str description: str parameters_schema: dict implementation: Callable[..., Any]这里有个细节parameters_schema直接存dict而不是存Pydantic模型类这是为了让模型工具定义和实际函数签名解耦。因为模型那边需要的是JSON Schema格式而函数那边用的是Python类型两者可以不一致靠执行前做一次参数校验来对齐。然后是注册器。注册器维护一个全局的技能表提供register装饰器和获取工具列表的方法# agent_skills/registry.py import inspect from typing import Dict, List from .base import SkillDefinition _skills: Dict[str, SkillDefinition] {} def register(name: str, description: str, input_schema: type[BaseModel]): def decorator(func): _skills[name] SkillDefinition( namename, descriptiondescription, parameters_schemainput_schema.model_json_schema(), implementationfunc, ) return func return decorator def get_skill(name: str) - SkillDefinition: return _skills[name] def list_skills() - List[SkillDefinition]: return list(_skills.values()) def to_openai_functions() - List[dict]: return [ { type: function, function: { name: skill.name, description: skill.description, parameters: skill.parameters_schema, } } for skill in _skills.values() ]这里有一个很实用的点model_json_schema()是Pydantic v2的方法会自动把WeatherInput这种模型转成OpenAI需要的JSON Schema格式。你不需要手写任何JSON结构省去了一大堆坑。最后一个关键环节是执行函数时的参数校验。LLM输出的参数有时候类型不对有时候多传了没定义的字段直接调用函数会爆TypeError。所以执行时要先做一层校验# agent_skills/executor.py from pydantic import ValidationError import inspect def execute_skill(name: str, arguments: dict) - dict: skill get_skill(name) # 用输入模型校验参数 input_model infer_input_model(skill) validated_args input_model.model_validate(arguments) # 只传入函数签名中需要的参数 func skill.implementation sig inspect.signature(func) filtered {k: v for k, v in validated_args.model_dump().items() if k in sig.parameters} return func(**filtered)这个infer_input_model的实现是从注册时的参数Schema反推一个Pydantic模型或者更简单一点在注册的时候直接把模型类存下来。代码不复杂但这一步能拦下大量运行时错误属于性价比极高的投入。3.2 把技能接入LLM调用循环核心系统写完了接下来就是把它接到大模型上。这里以OpenAI SDK为例展示完整的调用流程import json from openai import OpenAI client OpenAI(api_key你的key) messages [{role: user, content: 杭州明天天气怎么样}] functions to_openai_functions() resp client.chat.completions.create( modelgpt-4o-mini, messagesmessages, toolsfunctions, tool_choiceauto, ) choice resp.choices[0] msg choice.message # 第一步模型决定要不要调用技能 if msg.tool_calls: for tool_call in msg.tool_calls: fn_name tool_call.function.name args json.loads(tool_call.function.arguments) result execute_skill(fn_name, args) # 把执行结果送回给模型让它基于结果生成对用户友好的回答 messages.append(msg) messages.append({ role: tool, tool_call_id: tool_call.id, content: json.dumps(result, ensure_asciiFalse) }) seq_resp client.chat.completions.create( modelgpt-4o-mini, messagesmessages, toolsfunctions, ) print(seq_resp.choices[0].message.content)这一步我要提醒一个非常容易犯的错误很多人把工具执行结果拼成字符串拼回消息却不保留tool_call_id的对应关系。OpenAI要求每一条role: tool的消息必须关联一个tool_call_id如果对应不上就会报错。上面的代码用tool_call.id来关联这是官方推荐的做法不要省。另外tool_choice参数的设置也有讲究。auto表示让模型自己决定是否调用工具required表示强制调用一个或多个工具none禁止调用。如果你做的是指令明确的任务比如把这段话翻译成英文并发送可以用required提高稳定性如果是开放式对话用auto更自然避免模型在不需要工具的时候也强行调。3.3 多技能编排把简单技能组合成复杂能力单技能调用只解决了查个天气、发个消息这类单点问题。实际业务场景往往需要串联多个技能。举个例子用户说帮我找一下上周的销售报告提炼出三个关键问题然后用英文发到团队群里。这至少涉及三个技能检索报告、生成摘要、发送消息。技能编排有两种思路。一种是LLM自主编排让主Agent自己决定调用顺序每调用完一个就把结果传回给它让它判断下一步。这种方式灵活但不可控适合探索性任务。另一种是预设流水线把固定的多技能流程写成编排逻辑用代码控制顺序LLM只负责中间某一环的输入输出处理。这种方式稳定适合经常重复的业务流。工程实践中我倾向于混合主流程是预设的任务边界清晰但每个环节内的具体参数由LLM补全。比如上面那个例子代码会首先调用文档检索技能拿到报告全文后模型需要从全文里提取关键问题这时候再发生一次技能调用生成摘要。编排逻辑看起来像这样steps [ {skill: search_document, prompt_template: 查找用户说的报告}, {skill: summarize, prompt_template: 提炼关键问题不超过3条}, {skill: send_message, prompt_template: 把摘要发到指定群} ] context {} for step in steps: skill get_skill(step[skill]) # 根据prompt_template和历史context填充参数 args llm_generate_arguments(skill, step[prompt_template], context) result execute_skill(step[skill], args) context[step[skill]] result看到没有技能编排的核心不是调用本身而是上下文管理。每一步输出的结果要能被下一步看到才能形成完整的流水线。我在自研框架里专门设计了一个context对象每一步的技能输出都会写入其中下一步的参数生成可以直接引用。这个设计做得好复杂流程就能自动跑通做得不好就会出现第二步不知道第一步查到了什么的尴尬。4. 常见问题与排查技巧实录4.1 模型就是看不见你的技能这是我最常被问到的问题技能定义好了函数列表也传了但模型死活不调用。排查思路其实就三步。第一步检查技能描述是否足够具体。我见过一个案例技能名字叫process_data描述是处理数据。这谁能懂改成处理用户上传的Excel文件支持数据清洗、去重、格式转换。当用户提到处理表格、清理数据、导入Excel时使用调用率立刻翻了一倍。名字要直观描述要包含触发场景和同义词。第二步检查技能数量是否过多。模型在一个对话里能感知到的工具是有限的。我用GPT-4o系列实测超过15到20个技能时模型选择准确率显著下降。这时就要做技能分组。比如把查询类技能合并成一个内部用参数区分具体查什么而不是拆成十个独立技能。第三步检查参数Schema是否合理。如果模型看不懂参数格式它宁可不调用。比如你让它在arguments里传一个ISO格式日期字符串不如直接给它一个Python的date类型加描述它更容易生成正确值。4.2 LLM传参总是出错参数问题是最让人崩溃的坑。模型经常会把数字传成字符串、把数组传成逗号分隔的文本、漏传必填字段。我总结了几条有效的缓解手段。一是尽可能减少必填参数。以天气查询为例如果查询天数不传就默认1天那就不要设为必填给一个默认值。每次模型要填的必填项越少出错概率越低。二是给参数加枚举约束。凡是取值范围有限的参数比如排序方式只有asc和desc都写成枚举。这比在描述里写只能是asc或desc有效得多因为Schema层面的约束模型会严格遵守。三是在描述里给出具体示例。参数描述里写城市名如北京、上海比你写目标城市名称好太多。这是对模型最直接的教学。我自己的经验法则是一个技能的定义中描述文本的占比应该超过50%。如果描述得不够详细说明你对这个技能的边界和使用场景还没想清楚。4.3 技能执行成功了但模型总结得稀烂还有一种情况是工具调用很顺利结果数据也拿到了但模型最终给用户的回复质量很差比如把数字念错了、忽略了关键字段、用自己的幻觉补充了结果里没有的信息。这时候问题大概率出在工具返回值的格式设计上。模型不是直接读你的dict它只是把dict序列化成JSON字符串塞进上下文。如果你的返回结构嵌套很深字段名不直观模型很难从中提取到重点最后只能根据自身知识瞎编。我的做法是给工具返回值做平面化处理。返回dict尽量是两层以内字段名语义化并在返回体顶部增加一个summary字段预置一段人类可读的摘要。比如{ summary: 杭州明天多云最高气温25度最低18度东南风3级。, details: {...} }这样模型即使后面的细节没细看也能直接引用summary来组织回答。这算是我总结出的一个比较巧妙的小技巧在很多场景下都有效。4.4 一些实操中的避坑清单最后整理一份清单都是我自己实际项目里一条条填坑填出来的经验技能返回值不要包含超长原始文本5000字以上的报告直接全文丢回给模型会严重拉低它的注意力。先做摘要或分块。技能执行要做超时控制网络API的调用尤其如此否则一个技能卡住整个对话流程就卡死了。技能执行日志建议记录触发时间、模型选择的参数JSON、执行结果、耗时。这样后续要优化Prompt或排查问题都有据可查。技能命名注意加业务前缀避免冲突比如crm_get_user和bi_get_user比裸叫get_user安全得多。多个技能返回相似内容时可以在描述里显式说明本技能与XX技能的区别帮助模型做区分。不要盲目相信模型的tool_choice输出。重要操作如发邮件、删数据在执行前要做二次人工确认这是Agent落地的底线。整个agent-skills系统做下来我最深的感受就是它不是一个写完就结束的模块而是一个需要持续迭代的基础设施。技能的描述、参数、编排方式都要随着使用数据不断修正。每隔一段时间回看技能调用日志你会发现新的调整空间——有些技能从来没人触发该删有些技能老是选错描述该改有些场景需要新技能该加。把它当成一个活系统来养Agent才能越用越顺手。
