资讯动态

agent-skills实战:从零搭建智能体技能系统,让大模型真正会干活

发布时间:2026/10/14 9:35:21 来源:尧图企业网站定制
1. 从“会聊”到“会做”agent-skills 到底在解决什么问题大模型能写诗、能编故事、能陪你聊哲学但真让它帮你把一份散乱的会议纪要整理成待办清单、把十几个 CSV 文件合并去重、再顺手发一封格式规范的邮件它就开始“手忙脚乱”了。这个落差就是agent-skills要填的坑。所谓 agent-skills直白点说就是给智能体Agent装备的一套“动手能力包”。模型本身是大脑负责理解意图、做决策而 skills 是手和脚负责真正去调用工具、执行操作、拿到结果。没有 skills 的 Agent就像一个知识渊博但四肢瘫痪的顾问只能动嘴有了 skills它才能变成一个能干活儿的助手。我最初接触这个概念是因为一个很具体的需求团队每天要处理大量重复性的数据整理工作人工做又慢又容易出错用传统脚本又不够灵活——数据格式一变脚本就废了。当时就想能不能让模型自己判断该用什么工具、按什么顺序执行这就是 agent-skills 的核心价值所在把“判断”交给模型把“执行”交给技能两者解耦各司其职。这套东西适合谁如果你写过一点 Python调过 API对 prompt engineering 有基本概念那就可以上手。不需要你是算法专家也不需要你从头训练模型。它更像是在现有模型能力之上搭一层“工具调度层”。对于做自动化流程、智能客服、数据分析助手、内容运营工具的人来说agent-skills 是一个能显著提升效率的切入点。接下来我会从设计思路、核心细节、实操过程、问题排查几个维度把我在实际项目中踩过的坑和总结的经验完整拆开讲。文章会比较长但每一段都是实操中验证过的你可以直接抄作业也可以根据自己的场景做调整。2. 整体设计思路为什么这样拆而不是那样做2.1 核心架构三层解耦各管各的agent-skills 的架构我习惯把它分成三层来看决策层由大模型负责。它接收用户输入理解意图决定“要不要调用技能”“调用哪个技能”“传什么参数”。技能层由一个个独立的 skill 组成。每个 skill 是一个封装好的函数或服务负责完成一件具体的事比如“读取文件”“调用搜索”“发送请求”“格式化输出”。执行层由运行时环境负责。它接收决策层的指令路由到对应的 skill执行后把结果返回给决策层形成闭环。为什么要这样拆因为如果把“判断”和“执行”揉在一起会出大问题。模型每次都要重新理解“怎么读文件”既浪费 token又容易出错。而拆开之后skill 是稳定的、可测试的、可复用的模型只需要知道“有这个技能可用”以及“什么时候用它”。我试过另一种做法把所有逻辑写在一个巨大的 prompt 里让模型直接输出操作步骤然后人工去执行。结果就是模型输出的步骤经常“看起来对但跑不通”——比如它会说“用 pandas 读取文件”但不会告诉你文件编码是 GBK 还是 UTF-8不会处理路径里的空格不会考虑文件不存在的情况。这些细节必须由 skill 来兜底。2.2 技能粒度多大算合适这是设计中最容易纠结的地方。技能拆得太细模型要调很多次效率低拆得太粗又失去了灵活性。我的经验是一个 skill 只做一件事但这件事要有完整的输入输出契约。比如“读取 CSV 文件”是一个 skill“清洗数据”是另一个“导出结果”是第三个。不要做一个“处理数据”的超级 skill因为它内部要处理太多分支模型很难准确描述需求。但也不能细到“打开文件”“读取一行”“关闭文件”这种程度。那样模型要编排几十步出错概率反而更高。判断标准很简单如果一个操作在业务上是一个完整的原子动作就把它封装成一个 skill。比如“发送邮件”是一个原子动作内部包含连接服务器、认证、构造邮件、发送、断开这些不需要模型操心。2.3 工具选型为什么用这套组合在具体技术选型上我最终用的是Python 函数注册机制 JSON Schema 描述。理由如下Python 生态最成熟几乎所有第三方服务都有现成的库封装 skill 成本最低。函数注册机制让 skill 的添加和移除变得极其简单新增一个技能只需要写一个函数、加一个装饰器。JSON Schema 用来描述每个 skill 的参数模型能直接读懂不需要额外训练。我没有用某些重型框架是因为它们抽象层太多出了问题很难排查。比如某个 skill 调用失败框架可能吞掉原始错误只给你一个“执行失败”的提示。而自己搭的这套错误信息是透明的堆栈是完整的调试起来快得多。另外skill 的描述文本非常关键。模型是根据描述来决定用哪个技能的所以描述要写得像“给同事交代任务”一样清楚。比如不要写“处理数据”而要写“读取指定路径的 CSV 文件返回前 5 行预览和总行数”。描述越具体模型选错的概率越低。3. 核心细节解析技能注册、参数校验与调用链路3.1 技能注册让模型“看见”有哪些能力技能注册的核心是把每个 skill 的元信息暴露给模型。元信息包括技能名称、功能描述、参数列表、每个参数的类型和是否必填。我用的方式是用装饰器把函数注册到一个全局 registry 里然后生成一份 JSON Schema 清单塞进模型的 system prompt 或者作为工具列表传入。这样模型每次决策时都能看到当前可用的所有技能。这里有个细节技能名称要用英文且尽量短、语义明确。比如read_csv、send_email、search_web。不要用do_something_with_file这种模糊命名。模型对英文命名的理解通常更稳定而且短名称在 token 消耗上更友好。还有一个坑技能数量不要一次性全暴露给模型。如果你有几十个技能全部塞进 prompt模型会“选择困难”而且 token 成本很高。我的做法是按场景分组每次只加载当前场景相关的技能。比如做数据分析时只加载文件读写、统计、绘图相关的技能做客服时只加载查询订单、发送通知、转人工的技能。3.2 参数校验别让模型“自由发挥”模型生成参数时经常会出现类型错误、缺少必填项、格式不对等问题。比如它可能把数字写成字符串把路径写成相对路径把日期写成“明天”这种自然语言。所以每个 skill 在执行前必须做严格的参数校验。我用的是 JSON Schema 校验 自定义校验函数两层。JSON Schema 负责类型、必填、枚举值这些基础校验自定义函数负责业务逻辑校验比如“文件路径必须存在”“日期不能早于今天”。校验失败时不要直接抛异常给用户而是把错误信息返回给模型让它重新生成参数。这形成了一个“自我修正”的循环。实测下来大部分参数错误都能在一到两次重试内解决。注意重试次数要设上限比如 3 次。否则模型可能陷入死循环反复生成同样的错误参数浪费时间和 token。3.3 调用链路从用户输入到结果返回完整的调用链路是这样的用户输入自然语言请求。决策层模型分析请求判断是否需要调用技能。如果需要模型输出技能名称和参数JSON 格式。运行时解析 JSON找到对应 skill执行参数校验。校验通过后执行 skill拿到结果。结果返回给模型模型判断是否还需要继续调用其他技能。如果不需要模型生成最终回复给用户。这个链路里第 6 步是关键。很多任务不是一次调用就能完成的比如“把文件 A 的数据合并到文件 B然后发邮件给某人”需要先读 A、再读 B、再合并、再发送。模型需要根据中间结果决定下一步做什么。我试过让模型一次性输出所有步骤然后批量执行。但问题是如果第一步的结果和预期不符后面的步骤就全错了。所以更稳妥的方式是“走一步看一步”每执行完一个 skill就把结果喂回给模型让它决定下一步。虽然多消耗一些 token但可靠性高很多。4. 实操过程从零搭一个可用的 agent-skills 系统4.1 环境准备与依赖安装先说一下我的环境Python 3.10主要依赖就几个——openai或者任何兼容的模型 SDK、jsonschema参数校验、pydantic数据模型。其他的按需安装比如处理 Excel 要openpyxl处理 CSV 用内置的csv模块就够了。pip install openai jsonschema pydantic如果你用的是其他模型服务把 SDK 换掉就行核心逻辑不变。我建议用虚拟环境避免依赖冲突。4.2 定义技能注册机制先写一个简单的注册器import json from typing import Callable, Any SKILL_REGISTRY {} def register_skill(name: str, description: str, parameters: dict): def decorator(func: Callable): SKILL_REGISTRY[name] { name: name, description: description, parameters: parameters, function: func } return func return decorator def get_skill_schemas(): return [ { name: skill[name], description: skill[description], parameters: skill[parameters] } for skill in SKILL_REGISTRY.values() ]这段代码很直白register_skill是一个装饰器把函数和它的元信息存进全局字典。get_skill_schemas返回给模型看的技能清单。4.3 实现几个基础技能先实现三个最常用的读文件、写文件、发送 HTTP 请求。import csv import requests register_skill( nameread_csv, description读取指定路径的 CSV 文件返回前 5 行预览和总行数, parameters{ type: object, properties: { path: {type: string, description: CSV 文件的绝对路径}, encoding: {type: string, description: 文件编码默认 utf-8, default: utf-8} }, required: [path] } ) def read_csv(path: str, encoding: str utf-8): with open(path, r, encodingencoding) as f: reader csv.reader(f) rows list(reader) return { total_rows: len(rows), preview: rows[:5] }写文件和 HTTP 请求类似这里不展开。重点说一下参数设计path是必填encoding有默认值。模型如果没传encoding就用默认的。这样既灵活又不会因为缺参数而失败。4.4 组装决策层与执行循环核心循环大概长这样def run_agent(user_input: str, max_turns: int 10): messages [ {role: system, content: 你是一个助手可以使用以下技能\n json.dumps(get_skill_schemas(), ensure_asciiFalse)}, {role: user, content: user_input} ] for turn in range(max_turns): response call_model(messages) if response.get(type) skill_call: skill_name response[name] params response[parameters] # 参数校验 skill SKILL_REGISTRY.get(skill_name) if not skill: messages.append({role: tool, content: f技能 {skill_name} 不存在}) continue try: validate_params(params, skill[parameters]) result skill[function](**params) messages.append({role: tool, content: json.dumps(result, ensure_asciiFalse)}) except Exception as e: messages.append({role: tool, content: f执行失败{str(e)}}) else: return response[content] return 达到最大轮次任务未完成这个循环的逻辑是模型每次回复要么是调用技能要么是最终答案。如果是调用技能就执行、把结果塞回消息历史继续下一轮。如果是最终答案就直接返回。max_turns是安全阀防止模型无限循环。我一般设 10 到 15大部分任务 3 到 5 轮就能完成。4.5 参数校验的实现from jsonschema import validate, ValidationError def validate_params(params: dict, schema: dict): try: validate(instanceparams, schemaschema) except ValidationError as e: raise ValueError(f参数校验失败{e.message})就这几行但非常关键。没有这层校验模型传个null进来skill 直接崩整个流程就断了。4.6 实际跑一个完整任务假设用户输入“帮我看看 data.csv 有多少行然后告诉我前几行长什么样。”模型第一轮会输出{ type: skill_call, name: read_csv, parameters: {path: /home/user/data.csv} }执行后返回{ total_rows: 1523, preview: [[id, name, date], [1, A, 2024-01-01], ...] }模型第二轮拿到结果生成最终回复“data.csv 共有 1523 行前 5 行包括 id、name、date 三列第一条记录是 id1nameAdate2024-01-01。”整个过程两轮完成耗时不到 3 秒。这就是 agent-skills 的威力模型负责理解和表达技能负责执行和返回各干各的效率极高。5. 常见问题与排查技巧实录5.1 模型选错技能怎么办这是最常见的问题。模型可能把“读取文件”理解成“写入文件”或者把“搜索”理解成“爬取”。排查思路先看技能描述是否足够清晰。描述模糊是选错的首要原因。再看技能数量是否太多。超过 15 个技能时模型的选择准确率会明显下降。最后看用户输入是否有歧义。如果用户说“处理一下那个文件”模型确实很难判断该用哪个技能。解决办法在 system prompt 里加一句“如果不确定用哪个技能先向用户确认”。另外可以在技能描述里加一些“反例”比如“这个技能只读不写不要用它来修改文件”。5.2 参数格式错误怎么处理模型经常把数字写成字符串把数组写成逗号分隔的字符串。比如它可能传{path: 123}而不是{path: 123}。我的做法是在参数校验失败时把具体的错误信息返回给模型比如“path 必须是字符串你传的是数字”。模型看到这个提示下一轮通常能改对。如果反复改不对就在 skill 内部做一次类型转换兜底。比如path str(path)。但这是最后手段不要一开始就做否则模型永远学不会正确格式。5.3 执行超时或卡死怎么办有些 skill 可能因为网络问题或数据量太大而卡住。我一般给每个 skill 加超时控制import signal def timeout_handler(signum, frame): raise TimeoutError(技能执行超时) signal.signal(signal.SIGALRM, timeout_handler) signal.alarm(30) # 30 秒超时 try: result skill[function](**params) finally: signal.alarm(0)30 秒是我根据经验设的阈值。大部分 skill 应该在 5 秒内完成超过 30 秒的基本可以判定为异常。5.4 常见问题速查表问题现象可能原因排查方法解决方案模型不调用技能直接回答技能描述没传对检查 system prompt 里是否包含技能清单确保技能 schema 正确传入调用技能但参数为空模型没理解参数含义查看参数描述是否清晰补充参数说明和示例技能执行报错参数类型不对或文件不存在查看错误堆栈加参数校验和友好错误提示循环多次不结束模型陷入重试死循环查看消息历史设 max_turns 上限结果不符合预期技能逻辑有 bug单独测试 skill 函数修复 skill 并加单元测试5.5 几个独家避坑技巧技巧一给技能加“使用示例”。在技能描述里加一句“例如read_csv(path/data/test.csv)”模型看到示例后参数格式错误的概率会大幅降低。技巧二错误信息要“可操作”。不要返回“执行失败”而要返回“文件 /data/test.csv 不存在请检查路径是否正确”。模型看到具体原因才能做出正确调整。技巧三定期清理不用的技能。项目迭代过程中会积累很多废弃技能。它们不仅占用 token还会干扰模型选择。我一般每个月 review 一次技能列表把三个月没调用过的技能下线。技巧四给技能加日志。每次调用记录技能名称、参数、耗时、结果状态。出问题时日志是最快的排查入口。我用的是简单的 logging 模块输出到文件按天切割。6. 技能扩展与性能优化让系统越跑越顺6.1 如何添加一个新技能添加技能的成本极低这是这套架构最大的优势。步骤就三步写一个 Python 函数实现具体逻辑。用register_skill装饰器注册填好名称、描述、参数 schema。重启服务新技能自动生效。比如要加一个“发送邮件”的技能import smtplib from email.mime.text import MIMEText register_skill( namesend_email, description发送纯文本邮件需要提供收件人、主题和正文, parameters{ type: object, properties: { to: {type: string, description: 收件人邮箱}, subject: {type: string, description: 邮件主题}, body: {type: string, description: 邮件正文} }, required: [to, subject, body] } ) def send_email(to: str, subject: str, body: str): msg MIMEText(body) msg[Subject] subject msg[To] to # 这里省略 SMTP 连接细节 return {status: sent, to: to}注册完模型就能在需要时调用它了。不需要改决策层代码不需要重新训练这就是解耦的好处。6.2 性能优化的三个方向方向一减少 token 消耗。技能描述尽量精简不要写废话。参数 schema 只保留必要字段。如果技能很多按场景分组加载不要一次性全塞进去。方向二并行执行无依赖的技能。如果模型一次输出了多个技能调用且它们之间没有依赖关系可以并行执行。比如同时读取三个文件没必要串行。我用concurrent.futures实现了一个简单的并行执行器耗时从 3 秒降到 1 秒左右。方向三缓存高频调用的结果。有些技能调用结果在短时间内不会变比如“获取当前汇率”“读取配置文件”。可以加一层缓存设置合理的过期时间。我用的是内存字典加 TTL简单有效。6.3 安全与权限控制技能执行意味着真实操作所以权限控制不能马虎。我的做法是每个技能标注风险等级低、中、高。高风险技能如删除文件、发送邮件、调用支付接口需要二次确认。所有技能调用记录审计日志包括谁调的、什么时候调的、参数是什么、结果如何。另外文件路径要做白名单限制不允许访问系统目录。HTTP 请求要限制目标域名防止模型被诱导去访问恶意地址。注意不要给模型开放“执行任意 shell 命令”的技能。这是极其危险的操作一旦模型被恶意输入诱导后果不堪设想。如果确实需要执行命令必须做严格的白名单校验。6.4 从单技能到技能编排当技能数量增多后模型可能需要编排多个技能完成复杂任务。比如“下载报表、解析数据、生成图表、发送邮件”这一串操作。我的做法是引入“技能链”的概念允许模型输出一个技能调用序列运行时按顺序执行前一个的输出作为后一个的输入。但这要求模型对技能之间的依赖关系有清晰理解。更稳妥的方式还是“走一步看一步”每步都让模型确认。虽然慢一点但可靠性高。对于确定性很强的流程可以预定义技能链模板模型只需要填充参数不需要自己编排顺序。7. 实际项目中的经验沉淀7.1 技能描述比技能实现更重要这是我踩过最大的坑。一开始我花大量时间优化 skill 的内部实现觉得只要功能强大模型就能用好。结果发现模型根本不知道这个技能能干什么因为描述写得太简略。后来我把每个技能的描述都当成“给新同事写交接文档”来写这个技能做什么、什么时候用、参数怎么填、有什么限制、举个例子。改完之后模型选错技能的概率下降了 70% 以上。7.2 错误处理要“温柔”模型不是人它看到“Error: NullPointerException”这种信息完全不知道该怎么办。但如果你返回“参数 path 不能为空请提供文件路径”它就能立刻修正。所以我在每个 skill 里都做了错误包装把技术性错误翻译成自然语言提示。这层包装看起来麻烦但极大提升了系统的自愈能力。7.3 不要追求“全自动”很多人做 agent 系统总想一步到位让模型全自动完成所有事情。但实际项目中完全自动化的风险很高一旦出错就是连锁反应。我的建议是关键节点加人工确认。比如发送邮件前让用户确认内容删除文件前让用户确认路径。这样既保留了自动化的效率又避免了不可逆的错误。7.4 持续迭代技能库agent-skills 不是一次搭好就完事的。业务在变需求在变技能库也要跟着变。我一般每两周 review 一次哪些技能调用频率最高能不能进一步优化哪些技能几乎没被调用是不是描述有问题还是根本不需要用户反馈了哪些新需求能不能封装成新技能这个迭代节奏能让系统始终保持活力而不是搭完就烂在那里。7.5 一个真实场景的完整复盘之前做过一个“周报助手”的项目用户输入“帮我生成本周周报”系统需要读取本周的日志文件、提取关键事件、按模板生成周报、发送给指定人员。最初的版本模型经常漏掉步骤或者把日志文件读错。后来我做了三件事把“读取日志”技能的描述改成“读取指定日期范围内的日志文件返回按时间排序的事件列表”并加了示例。在 system prompt 里明确写了周报生成的步骤顺序。发送邮件前加了人工确认环节。改完之后任务完成率从 60% 提升到 95% 以上。这个案例让我深刻体会到agent-skills 的效果不取决于模型多强而取决于技能设计得多清晰、流程编排得多合理。8. 关于未来扩展的一些个人想法这套架构目前跑得挺稳但我也在思考几个扩展方向。一个是技能的自描述能力让模型能自己发现新技能而不是靠人工注册。另一个是技能的自动组合模型能把多个简单技能组合成一个复杂技能减少调用轮次。还有一个是跨会话的技能记忆让模型记住用户偏好下次调用时自动填充常用参数。不过这些都是锦上添花。核心还是那句话把判断交给模型把执行交给技能把控制权留给人。这个原则不变系统就不会跑偏。如果你也在做类似的东西我的建议是从一个小场景开始先跑通一个技能再慢慢加。不要一上来就搞大而全那样很容易卡在细节里出不来。先让系统能干活再让它干得好。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑