尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Agent技能库实战:让大模型从“会想”到“会做”

Agent技能库实战:让大模型从“会想”到“会做” 做AI应用开发这两年我越来越觉得一件事Agent的能力边界很大程度上不是模型决定的而是“技能库”决定的。就拿“agent-skills”这个项目来说它本质上是在解决一个非常现实的问题——模型只会“想”不会“做”而技能库就是让模型能“做”的那双手。我最早意识到这个问题是发现单纯靠提示词堆砌根本没法让大模型稳定完成复杂的多步操作比如调接口、读写文件、查数据库。每次效果都像开盲盒。后来我把这些能力拆成一个个可复用、可注册、可调度的skill组件整套系统才真正变得可控、可维护、可扩展。这篇文章我不打算整那些花里胡哨的概念就围绕agent-skills这个项目讲讲我实际搭建技能库时的完整思路、踩过的坑、以及一套能直接落地的做法。不管你是刚接触Agent开发的新手还是已经在做智能体应用的老手只要你需要让大模型真正“干活”这篇文章应该能给你不少参考。1. 为什么我会把Agent能力拆成一张技能清单1.1 agent-skills到底解决什么问题先说一个我自己的体会。早期我做一个内部问答机器人模型用的是当时的主流大模型提示词写了几千字把各种工具说明、参数格式、调用规范全塞进上下文里。结果是什么呢效果极不稳定同一个问题今天回答得挺好明天换了模型版本或者上下文稍微长一点模型就开始“幻觉”自己编造参数去调用工具。后来我把工具改成“技能”的思路每个技能像一张独立的卡片有自己的名字、描述、参数校验规则、执行逻辑模型只需要在规划阶段选择技能然后按技能定义去填充参数剩下的交给代码去执行。agent-skills的核心就是把Agent的能力从“模型自由发挥”变成“模型按目录选择”。模型不再需要知道每个工具内部是怎么实现的它只需要知道有什么技能可以用、每个技能是干什么的、需要什么参数。这样做的直接好处有三个稳定模型不需要记忆复杂的调用细节按技能描述来就行效果大幅提升。可维护每个技能独立迭代改一个不影响其他不像几千字提示词那样牵一发动全身。可扩展新能力接入就是新增一个技能文件不改核心逻辑就能让Agent学会新操作。当时我做完这个重构之后最大的感受是之前我是在跟模型“讲道理”让它理解我的意图现在我是给模型“摆货架”让它按需取用。后者显然靠谱得多。1.2 技能库和“提示词堆砌”的本质差别很多人会问我把工具说明写在系统提示词里不一样吗为什么非要搞一个技能库这个问题我在项目初期也纠结过。我自己对比过两种方式的差异写在这里供大家参考。提示词堆砌的最大问题是“静态的”。你写在提示词里的东西模型每次调用都要重新理解一遍而且如果工具多了提示词越来越长模型的注意力就会被稀释。打个比方你让一个新手去仓库找工具你把仓库所有工具的说明书全塞给他他反而不知道该用哪一个。而技能库的思路是“动态的”Agent先通过规划模块分析任务确定需要哪些技能再去技能库“检索”这些技能的定义最后才执行。也就是说技能库是按需加载的不是一股脑全塞给模型。另外还有一个很实际的差别——参数校验。提示词方式下模型生成的工具调用参数经常格式错误你只能在代码里做各种容错非常痛苦。技能库方式下每个技能自带参数校验逻辑参数不对直接报错返回让模型自己重新生成。这个机制带来的稳定性提升是我后来最看重的一点。所以在agent-skills的设计里我一开始就确定了核心原则技能注册表是独立的技能执行是独立的Agent只负责规划和决策不直接触碰执行细节。这条分层逻辑是整个项目的地基。2. 技能定义规范与设计原则2.1 一张技能卡片该包含哪些字段agent-skills里最基础的单位就是“技能卡片”。我开始的时候犯过一个错误把技能描述写得太简短结果模型经常误用。后来我把技能卡片的字段不断固化最终形成了一套比较稳定的规范。每个技能至少包含以下字段id技能唯一标识比如fetch_webpage、search_docs、execute_sql。name技能名称要口语化让模型一看就懂比如“抓取网页内容”。description技能描述说明这个技能的功能、适用场景、限制条件。描述写得好不好直接决定模型会不会选错技能。parameters参数声明包括每个参数的名称、类型、是否必填、含义、取值范围。executor技能执行入口指向实际的处理函数。required_permissions技能执行所需的权限或资源标识。timeout技能执行的超时时间防止某个技能卡死拖垮整个任务。这里我想重点说description怎么写。我实测下来有个技巧描述里要写清楚“什么时候用”和“什么时候不用”。比如一个搜索技能你如果只写“搜索信息”模型在遇到“计算11等于几”的时候也可能去调它。但如果你写“当需要获取实时外部信息时使用对于数学计算、逻辑推理等不需要外部信息的问题不要使用”模型误判的概率就会大幅下降。描述里的“正例反例”写法是我后来所有技能描述的默认风格。2.2 技能粒度怎么拿捏才合适技能粒度是另一个特别容易出问题的地方。我一开始把技能拆得特别碎比如“读取文件”“写入文件”“追加文件”“删除文件”全都独立成技能。结果模型在规划的时候经常犹豫不决不知道应该选哪个。后来我把它们合并成一个file_operations技能用操作类型来区分子动作模型的选择负担就小多了。反过来如果一个技能太大比如把整个数据分析流程都塞到一个技能里模型虽然容易选但执行时无法在中间步骤做调整灵活性就很差。我现在的经验是按“原子操作”来划分技能粒度。也就是说一个技能应该完成一个不可再拆的业务动作而不是一条完整的业务链。比如“查天气”是一个技能“根据天气推荐穿搭并生成邮件”就应该拆成“查天气”“生成推荐”“发送邮件”三个技能由Agent在规划时串联。判断粒度是否合适有个简单的办法如果一个技能描述里出现了“然后”“接着”“最后”这类词说明它大概率拆得太粗了。正常的技能描述应该是单动作的比如“将文本翻译为英文”“提取文本中的关键实体”而不是“先读取文件然后清洗数据最后生成报表”。2.3 技能入库存放目录结构建议技能库工程化之后目录结构一定要从一开始就规划好。我之前见过不少项目技能文件随便放name乱起到最后根本没法维护。我目前在agent-skills里用的结构是这样skills/ ├── registry.json # 技能注册表记录所有技能元信息 ├── builtin/ # 内置基础技能 │ ├── fetch_web.py │ ├── file_operations.py │ └── sql_executor.py ├── domain/ # 业务领域技能 │ ├── ecommerce/ │ │ ├── order_query.py │ │ └── refund_apply.py │ └── finance/ │ └── report_generator.py └── shared/ ├── validators.py # 公共参数校验工具 └── logger.py # 统一日志registry.json是技能注册表的核心里面记录每个技能对应的执行模块路径、启用状态、版本号。启动时系统加载注册表而不是全盘扫描目录这样在技能数量多的时候启动速度也快得多。后来我甚至把注册表放到配置中心里支持热更新技能上线不用重启主服务。3. 实操过程从零搭建一个可运行的技能库3.1 环境准备与基础依赖这部分我就按最通用的方案来讲。agent-skills项目我用的技术栈是Python因为AI生态的Python支持最成熟。基础依赖需要这些Python 3.10以上建议3.11性能更好。FastAPI或者Flask用来暴露Agent服务接口。Pydantic做参数校验配合类型声明非常舒服。你选用的模型SDK比如OpenAI SDK、或各大模型的Python包。安装命令很简单创建一个虚拟环境然后pip安装就行。我不建议在全局环境里搞项目依赖冲突太烦人了。3.2 技能注册与调度核心代码接下来是核心部分技能注册表和调度器。我先写一个最简版本的技能注册表# skill_registry.py from typing import Dict, Type, Optional class SkillBase: 所有技能的基类 id: str name: str description: str parameters: list [] async def execute(self, **kwargs): raise NotImplementedError class SkillRegistry: 技能注册表 def __init__(self): self._skills: Dict[str, Type[SkillBase]] {} def register(self, skill_cls: Type[SkillBase]): 注册一个技能 skill skill_cls() self._skills[skill.id] skill return skill_cls def get_skill(self, skill_id: str) - Optional[SkillBase]: return self._skills.get(skill_id) def list_skill_descriptions(self) - list: 返回所有技能的描述信息供模型选择 return [ { id: s.id, name: s.name, description: s.description, parameters: s.parameters, } for s in self._skills.values() ] registry SkillRegistry()这段代码很直白关键在list_skill_descriptions这个方法。这个方法的输出会被喂给模型作为它在规划阶段“看到的菜单”。所以这里的description质量基本决定了模型选择技能的准确率。然后看一个具体的技能怎么实现。比如一个“获取网页内容”的技能# skills/builtin/fetch_web.py import aiohttp from skill_registry import SkillBase, registry class FetchWebpageSkill(SkillBase): id fetch_webpage name 获取网页内容 description ( 当需要读取某个URL页面的文本内容时使用。 适用于获取公开网页、博客、新闻文章等。 注意不适用于需要登录才能访问的页面也不适用于文件下载。 ) parameters [ { name: url, type: string, required: True, description: 要抓取的网页完整地址必须包含http(s)://前缀, }, { name: max_length, type: integer, required: False, description: 返回内容的最大字符数默认5000, }, ] async def execute(self, url: str, max_length: int 5000): # 这里加个简单的参数校验 if not url.startswith((http://, https://)): return {success: False, error: URL格式不合法必须包含http(s)://前缀} async with aiohttp.ClientSession() as session: async with session.get(url, timeout10) as resp: text await resp.text() return {success: True, content: text[:max_length]} # 注册技能 registry.register(FetchWebpageSkill)这里有个细节要注意参数校验放在execute内部做而不是依赖框架。原因很简单当模型调用技能时参数是从模型返回的JSON里解析出来的可能格式不对、类型不对、甚至多传了不认识的参数。所以每个技能自己得对参数负责不能假设输入一定是合法的。3.3 让Agent在规划阶段感知技能技能注册表有了具体技能有了接下来就是最关键的一环怎么让Agent知道有哪些技能并且能正确调用。我目前用的是ReAct模式来搭这个循环。简单来说就是给模型一个系统提示词里面塞入技能菜单来自registry.list_skill_descriptions()然后让模型在每一步决定“调用哪个技能、传什么参数”执行完把结果反馈给模型模型再决定下一步做什么。核心流程的伪代码大致是这样的async def run_agent(task: str): messages [{role: system, content: build_system_prompt()}] messages.append({role: user, content: task}) for _ in range(MAX_STEPS): response await llm.chat(messages) if response.has_tool_call: skill_id response.tool_call.function.name args json.loads(response.tool_call.function.arguments) skill registry.get_skill(skill_id) result await skill.execute(**args) messages.append(response.message) messages.append({ role: tool, tool_call_id: response.tool_call.id, content: json.dumps(result, ensure_asciiFalse), }) else: return response.content return 任务步骤超过上限循环结束build_system_prompt里就是把技能菜单拼进提示词同时加一个“使用规则”优先选择最匹配任务的技能。如果当前技能需要前置操作先调用前置技能。如果技能执行失败根据错误信息调整参数后重试最多重试两次。如果任务无法用现有技能完成直接说明“无法完成”不要编造结果。3.4 一个完整的任务执行示例我拿一个实际的例子来说明整套流程跑起来是什么样。假设任务是“查一下今天的实时新闻然后把标题整理成列表。”第一步Agent收到任务看到技能菜单里有fetch_webpage获取网页内容、search_web搜索网页、generate_report生成报告等技能。它选择调用search_web参数是查询关键词“今日实时新闻”。第二步search_web返回搜索结果通常是几条新闻链接和摘要。第三步Agent选择调用fetch_webpage参数是某条新闻链接获取正文内容。第四步Agent从正文里抽取关键信息直接生成标题列表作为最终回复。这个链路本身并不复杂但每一步Agent的选择是否准确取决于两个因素技能描述是否清晰、执行结果反馈是否完整。如果某个技能返回的错误信息太模糊比如就一句“执行失败”模型根本无从调整。所以我在设计技能返回值时也定了规范必须返回结构化结果成功时返回数据失败时返回错误码和错误说明。这样Agent才能基于反馈做下一步决策。4. 常见问题与排查技巧实录4.1 技能总是不被调用怎么办这是我被问过最多的问题。模型明明看到了技能菜单却总是自己凭脑子里的知识回答不去调用技能。我排查这个问题时一般按顺序检查三件事第一技能描述是否足够“诱人”。如果你的技能描述写得跟文档似的模型觉得自己的知识就能搞定它当然不调。解决办法是在描述里强调“如果XX场景请务必使用本技能”并且给出不使用会造成什么后果。第二系统提示词里是否限制了模型必须使用技能。我现在的提示词会明确要求“你的所有回答必须基于技能执行结果如果技能执行不成功必须重试或明确告知用户无法完成不能直接凭记忆回答”。这句话非常重要它把模型的默认行为从“自由发挥”扭转为“先行动再回答”。第三模型是否在“尝试调用”但代码没接住。这种情况经常出现在SDK版本升级、函数调用字段名变化时。建议在调用模型的代码里打日志把模型完整返回内容打出来一眼就能看出模型到底有没有发起工具调用。4.2 参数传错、类型匹配不上模型在填充参数的时候偶尔会把数字写成字符串或者对象写成数组。比如日期参数传成“今天”数字传成“2024-01-01的销售数据”。这类问题很普遍我的处理策略是“校验失败后把错误消息反馈给模型重试”而不是在代码里强行转换。具体做法是每个技能执行前用Pydantic做一次严格校验。校验失败的报错信息里要包含具体是哪个参数、期望什么类型、实际传了什么值。然后把这个报错返回给模型让模型参考报错信息修正参数后重试。因为模型本身有很强的理解能力看到报了“expected int, got str”这种信息往往下一次就能修正。如果你发现模型反复在一个参数上报错那多半是参数描述写得不够清楚。比如一个技能需要“日期”参数你不能只写“date”要写清楚格式“YYYY-MM-DD例如2024-06-01”。参数描述越具体模型传错的概率越低。4.3 技能多了之后调度越来越慢技能数量从几个增长到几十个之后你会发现模型决策时间明显变长而且开始频繁在相近技能之间犹豫。这个问题的根源在于你把所有技能一股脑全塞给模型让它在几十个选项里做选择这本身就是一件困难的事。我的解决方案是引入“技能分组”和“预筛选”。在把技能菜单喂给模型之前先用一个轻量级的检索模块根据任务内容召回最相关的5到10个技能再让模型在这几步里选。这个轻量级检索模块可以是一个简单的关键词匹配也可以是一个小的embedding检索。实测下来用embedding召回效果更好而且模型决策的准确率也更高了因为干扰项变少了。4.4 技能调试的日志打法技能调试非常依赖日志但日志怎么写是有讲究的。我现在的日志标准是Agent执行的每一步都留痕。具体包括模型认为下一步需要调用什么技能、为什么模型自己的推理内容、传入的参数是什么、技能返回的结果是什么、模型根据结果做了什么判断。把这些串起来基本就能完整还原一次任务执行过程。我之前遇到过一个诡异的问题技能单测都通过但放在Agent流程里就频繁失败。后来靠日志才发现模型在一次请求里连续调用了两个技能而我在代码里只处理了第一个工具调用第二个被静默吞掉了。日志一打出来这个问题的原因马上浮出水面。所以如果你觉得Agent行为难以理解先把日志打全别急着改代码。5. 工具选型解析与后续扩展5.1 技能库到底放在哪里关于技能库的存储我试过几种方案。初期技能少直接Python字典在内存里注册就够了简单直接。但我很快发现两个问题一是重启丢失二是团队协作时技能定义分散在各处缺少统一版本管理。于是我把技能定义迁移到YAML文件里和代码分开存放。每个YAML文件描述一个技能的元信息包括id、描述、参数schema、执行模块路径。系统启动时从YAML文件加载注册信息动态注册到运行时的注册表。这样技能的定义和实现解耦非开发人员也可以参与维护技能描述。再往后技能数量到了上百个我开始用数据库存技能元信息配合管理后台在线编辑、审核、发布。不过这个阶段对个人项目和中小团队来说有点重大部分场景YAML文件加Git版本控制就够了。5.2 从技能库到技能生态最后聊点长远的。agent-skills这个项目的思路本质上就是把Agent能力“产品化”。技能不再是一个个零散的函数而是可以被复用、被共享、被组合的模块。我最近在做的扩展方向有两个一是基于性能反馈对技能做自动评估和排序模型优先选择历史上成功率高的技能这个思路在技能很多时特别有用二是技能参数模板化把一组固定参数保存成常用模板用户不用每次重新描述直接模板化调用。另外我还加了一个“技能自省”机制。每个技能执行失败后会把失败原因记录到技能元数据里。如果某个技能连续失败超过阈值系统会降低它的优先级或者自动通知维护者。这个机制说白了就是给技能库加了一点自我体检的能力让整个系统在运行过程中越来越稳定而不是越跑越乱。回到agent-skills这个项目本身我确实踩过不少坑也推翻过几次重做。但最终沉淀下来的这套“技能注册表加规划调度”的设计让我后续所有Agent应用开发都轻松了一大截。你不需要一开始就把系统做得特别复杂先从一个技能字典加一个for循环开始跑通之后再逐步加功能。技能库真正的好处是在你迭代到第三四个功能时会越来越明显——那种“加新功能不用动旧逻辑”的爽快感是真的会上瘾的。
返回列表