ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Agent Skills实战指南:从Function Calling到技能封装,构建可复用Agent能力

Agent Skills实战指南:从Function Calling到技能封装,构建可复用Agent能力 1. 项目概述Agent Skills到底在解决什么问题最近一直在折腾 Agent 相关的东西发现社区里冒出来一个高频词——agent-skills。可能很多朋友看到这个词的第一反应是这不就是把 Function Calling 换个名字再包装一遍吗刚开始我也是这么想的但实际拆解下来它解决的问题跟 Tool Calling 还真不完全是一回事。先说结论Agent Skills 更像是把大模型从“会调用工具”升级成“真正掌握某项工艺能力”的一套方法论。传统的 Function Call 是在模型需要的时候去外部拿数据、执行某个单一动作而 Skills 强调的是把一组相关的操作流程、判断逻辑、异常处理打包成一个可复用的能力单元。打个生活化的比方Function Call 像是给了厨师一把刀Skills 则是教会厨师一套完整的刀工体系配菜、切片、摆盘自成一体。这篇内容适合正在做 Agent 应用开发、被多步骤任务编排折磨得头疼的工程师也适合刚入门想搞明白“Skills 跟 Tools 到底是什么关系”的产品经理或研究者。接下来我会从概念拆解、设计思路、实操落地到坑点排查完整过一遍给你一套可以直接拿去用的参考框架。在动手之前我们需要先把 agent-skills 这个项目的核心边界画清楚。它不是一个具体的开源仓库名称也不是某一个框架的专有名词而是 2025 年以来 Agent 工程化落地过程中逐步沉淀出的一种实践范式。你可以把它理解为一套“技能封装规范 运行框架 组织方式”的组合体。这套组合体背后真正的技术趋势是大模型的能力边界已经不再由模型本身的参数量决定而是由它能编排多少外部能力来决定。当模型学会了“把复杂任务拆成步骤、为每一步匹配技能、用技能执行结果修正下一步计划”这套循环逻辑之后Agent 的落地能力会有一个量级上的提升。我见过不少团队在同一个模型底座上仅仅因为技能库组织得好业务效果就有肉眼可见的差距。2. 技术拆解Skill 的本质与技能库设计2.1 为什么需要把“技能”做成一等公民我们换个思路想如果让一个没有任何工具的大模型去完成“生成一份带图表的周报”它会怎么做大概率是凭记忆拼凑文字然后给你一段 Markdown 表格最后告诉你“图片我没办法直接生成”。这就是裸模型的天然局限它只会“说”不会“做”。加上 Function Calling 之后呢模型可以调用一个画图接口、一个查询接口、一个文件写入接口。表面上能力变强了但你很快会发现一个问题每次对话里模型都要重新理解“如何用这些接口组合出周报”。当你面对二三十个函数定义时模型的选择成本和幻觉率都会显著上升上下文也被大量无关的工具描述塞满。Agent Skills 的解法是把“与工具交互的复杂过程”再抽象一层不是给模型一堆零散的函数而是给模型一组封装好的、带明确输入输出的子能力模块。每个模块内部可以包含多个步骤、多轮工具调用、甚至有自己的状态管理。模型只需要决定“此时该调用哪个技能”而不需要关心“这个技能内部是怎么实现的”。我第一次在实践中真正感受到这个设计的好处是在做一个信息整合类 Agent 的时候。最开始直接铺了四十多个 Tools 给模型结果发现模型频繁选错工具、参数填错、甚至自己编造工具名。后来把工具按业务语义重组成十几个 Skills每个 Skill 内部封装好参数校验和流程编排模型的选择准确率一下就上来了。这件事让我彻底理解了一个道理模型不是能力不够而是我们喂给它的信息组织方式太原始了。2.2 技能库的核心结构从 Skill.md 到代码执行再看具体实现层。标准化的 agent-skills 实践里一个技能通常由两部分组成一份人类可读的技能说明文件通常叫 SKILL.md以及一份机器可执行的代码文件通常是 Python 脚本。这两者不是互相替代的关系而是互相配合的关系。SKILL.md 描述的是技能的语义——它解决什么问题、适用于什么场景、有哪些关键参数、输入输出长什么样、边界在哪里。这份文件是给大模型看的模型通过阅读这份描述来决定“当前任务是否适合调用这个技能”。所以这份文件的写法和以往写接口文档很不一样你不能只写“参数说明”你需要写“什么时候用、什么时候不能用、典型的输入输出长什么样”。代码文件则是技能的实际执行体。现在主流的做法是让技能能够写入一个临时文件然后由沙箱环境执行并返回结果。这样设计有几个好处一是自然语言指令的执行跟代码执行解耦了模型先生成代码再由运行环境确认执行二是可以加权限控制、资源限制、审计日志三是支持复杂的重试循环和异常处理逻辑。我记得自己在第一个技能里尝试把“网页抓取、正文清洗、摘要生成”串成一个完整流程。最开始我用 JSON 配置的方式来描述这个流程后来发现配置越写越复杂可读性和可维护性都在下降。换成了 SKILL.md Python 脚本的方案之后一切都清爽了很多。文档负责告诉模型“这个技能能干嘛”代码负责“真正把活干完”职责单一边界清晰。2.3 为什么选择 Python 作为技能实现语言市面上做 Agent Skills 的框架有不少有的用 TypeScript有的用 Python有的干脆是 DSL 配置文件。但主流实践几乎都倾向用 Python这不是偶然的。一方面AI 生态的核心库全是 Python 写的文件处理也好、HTTP 调用也好、数据清洗也好Python 都有成熟的方案不用重复造轮子。另一方面Python 的“胶水语言”特性非常适合做技能粘合层。一个技能内部可能要调用好几个外部服务还要处理不同的数据格式Python 可以非常轻量地把这些能力拼接起来。相比之下用 TypeScript 或 Java 来实现技能光是想清楚异步逻辑和类型定义就得花不少额外精力逼着开发者把大量时间花在了和业务无关的工程问题上。不过也要说一句公道话如果你的整个 Agent 体系已经比较重技能和框架是同构的那直接用同一种语言统一技术栈反而更合理。技能语言的选择本质上是架构决策的一部分不是独立的偏好问题。我个人的建议是尽量让技能的运行环境做到进程级隔离不管用什么语言技能跑在独立的 Python 进程里主 Agent 负责调度这样即使某个技能崩了也不会拖垮整个系统。2.4 模块化 Skills 的三个基础层从工程的视角看一个完整的 Agent Skills 体系至少包含三个层次。基础层是原子技能库。每个技能只做一件事比如“读取 PDF 某一页”“将 CSV 转成 JSON”“调用某个搜索 API 并整理结果”。这一层的技能要足够小、足够通用、边界清晰方便复用和组合。中间层是组合技能层。组合技能由多个原子技能拼接而成解决一个相对完整的问题。比如“生成销售周报”这个技能可能包含了数据查询、指标计算、表格生成、图片渲染四个原子步骤。上层是工作流编排层。这一层不再关注单个技能而是关注“当前正在执行的目标任务”根据中间状态动态地选择和编排技能。这一层通常由 Agent 的规划模块Planner来实现核心是决策逻辑和上下文管理。这三层的关系有点像盖房子原子技能是砖块组合技能是预制板工作流编排是施工图纸。大多数团队刚起步时只做了砖块层结果发现砌墙效率低下各干各的做得成熟的团队一定会把三层都想清楚并且为每一层设计好对应的调试和观测手段。3. 实操落地从零构建自己的 Agent Skills 技能库3.1 环境准备与基本框架选型纸上谈兵没意思直接上手搭一套技能库大家跟着操作就行。我这边以 Python 为基础栈来演示因为它是社区里最通用、最不容易出错的方案。首先需要一个基础环境。Python 版本建议 3.10 以上顺手装好虚拟环境工具。技能运行时要跟主 Agent 框架通信这里有两个选择一是直接用 FastAPI 或 Flask 起一个轻量的本地 HTTP 服务二是借助 Claude Agent SDK 这类具备 Skills 概念的框架。我自己的习惯是能少依赖就少依赖核心逻辑尽量自己写只在通信层用现成库这样排查问题的时候心智负担会小很多。准备的依赖库不用多核心是这几样fastapi用来起技能接口服务uvicorn做 ASGI 服务器pydantic做参数校验requests或httpx做外部 HTTP 调用loguru做日志。如果你还需要文件解析、数据库读取之类的能力每加一个技能的时候按需引入对应的库就好不用一次性装一堆。3.2 第一步任务拆解与技能提取这个环节最容易被忽略但恰恰是整个体系成不成功的分水岭。很多团队上来就直接写代码结果技能做出来一个能跑的“独苗”换个项目场景就完全复用不了。我建议先在便签上把你当前业务里最频繁的 10 个任务全部写下来然后逐个拆解任务内部的步骤。比如我做过的一个内容分析 Agent任务清单里有“分析某篇文章的情感倾向”“抽取文章里的关键实体”“把文章总结成 200 字以内的摘要”“把摘要翻译成英文”……如果一股脑把这些都做成技能那就又掉进“工具堆砌”的坑里了。正确的做法是把它们进一步抽象。你仔细看会发现“分析情感”和“抽取实体”本质上有很大的公共部分都是把文本预处理后送到大模型接口只是 Prompt 模板不同。这时候你就应该抽象出一个“文本智能处理”技能而不是搞两个独立的技能。技能提取的核心指标是可复用密度一段能力被复用的次数越多越值得被提成技能。3.3 第二步以“生成 SVG 图表”技能为例完整实现说一个我自己最近在做的案例让 Agent 具备直接生成 SVG 图表的能力。这个技能很有代表性因为它同时涉及代码生成、语法校验、文件输出三个环节能把技能开发的思路讲透。首先定义 SKILL.md。内容最关键的是“使用场景”和“边界条件”两段我把真实使用的版本简化了一个版本给大家参考# Skill: 生成SVG图表 ## 功能描述 根据用户提供的结构化数据生成对应的SVG矢量图表输出可直接嵌入HTML的可缩放图形。 ## 适用场景 - 用户需要数据可视化图形且对清晰度、缩放有要求 - 图表需要嵌入报告页面或邮件正文 - 用户提供的数据量级在10-1000个数据点之间 ## 不适用场景 - 数据量超大超过1000个点此时建议使用专业BI工具 - 需要动态交互的图表此时应使用ECharts等前端库 - 用户明确要求输出PNG/JPG图片此时应使用绘图库生成位图 ## 输入参数 - data: list[dict]图表数据格式[{label: A, value: 10}, ...] - chart_type: str图表类型可选值bar / line / pie - width: int画布宽度默认800 - height: int画布高度默认600 - title: str图表标题可选写这份文档的时候有个技巧不要用太多抽象的描述性语言尽量用具体的例子来帮助模型理解。模型读文档的能力跟人读文档的能力有着本质区别它更擅长从“例子”中总结规律而不是从抽象的“逻辑描述”中推理。所以每个参数最好是带着示例值再加上取值范围说明。看一下核心实现代码我简化了边界情况保留主干逻辑import json from typing import Optional, List, Dict import xml.etree.ElementTree as ET def _escape_xml(text: str) - str: 转义XML特殊字符防止SVG被破坏 return (text.replace(, amp;) .replace(, lt;) .replace(, gt;) .replace(, quot;) .replace(, apos;)) def generate_bar_chart(data: List[Dict], width: int, height: int, title: Optional[str]) - str: 生成柱状图SVG if not data: raise ValueError(数据不能为空) max_value max(item[value] for item in data) label_width 120 chart_width width - label_width padding_top 60 if title else 30 padding_bottom 60 bar_gap 20 bar_width (chart_width - bar_gap * (len(data) 1)) / len(data) plot_height height - padding_top - padding_bottom svg_parts [ fsvg xmlnshttp://www.w3.org/2000/svg width{width} height{height} viewBox0 0 {width} {height}, rect width100% height100% fill#ffffff/, ] if title: svg_parts.append( ftext x{width/2} y30 font-size18 font-weightbold text-anchormiddle{_escape_xml(title)}/text ) for i, item in enumerate(data): bar_height (item[value] / max_value) * plot_height if max_value 0 else 0 x label_width bar_gap i * (bar_width bar_gap) y padding_top plot_height - bar_height # 柱子 svg_parts.append( frect x{x:.1f} y{y:.1f} width{bar_width:.1f} height{bar_height:.1f} ffill#4C8BF5 rx3/ ) # 数值标签 svg_parts.append( ftext x{x bar_width/2:.1f} y{y - 8:.1f} font-size12 text-anchormiddle{item[value]}/text ) # x轴标签 svg_parts.append( ftext x{x bar_width/2:.1f} y{padding_top plot_height 25} font-size12 text-anchormiddle{_escape_xml(str(item[label]))}/text ) svg_parts.append(/svg) return .join(svg_parts) def render_svg_to_html(svg_content: str) - str: 把SVG包装进HTML方便直接预览 return f!DOCTYPE htmlhtmlheadmeta charsetutf-8/headbody{svg_content}/body/html这段代码的核心输出是标准的 SVG XML 字符串。为了安全我用了_escape_xml处理了文本内容避免注入问题。技术上关键点就是计算每个柱子的位置和高度其他图表的写法思路都类似就是生成不同的形状元素。然后是技能的执行入口。按 agent-skills 的规范我们要把“模型生成代码”和“代码执行”两段剥离开。模型先看到的是技能描述然后生成“调用某个 JSON 函数”的参数最后执行器来真正执行代码。下面是一个简化的执行器接口from enum import Enum from typing import Any, Dict class ChartType(str, Enum): BAR bar LINE line PIE pie def execute_svg_skill(params: Dict[str, Any]) - Dict[str, Any]: 技能执行统一入口解析参数并分派到具体实现 chart_type params.get(chart_type) data params.get(data) width int(params.get(width, 800)) height int(params.get(height, 600)) title params.get(title) if not isinstance(data, list) or len(data) 0: return {success: False, error: data 必须是包含 data 的列表且不能为空} try: if chart_type ChartType.BAR or chart_type ChartType.LINE: svg_content generate_bar_chart(data, width, height, title) elif chart_type ChartType.PIE: svg_content generate_pie_chart(data, width, height, title) else: return {success: False, error: f暂不支持的图表类型: {chart_type}} html_content render_svg_to_html(svg_content) return { success: True, svg: svg_content, html: html_content, format: svg, } except Exception as e: return {success: False, error: f技能执行失败: {str(e)}}这个执行入口承担一件很关键的事情把参数校验放在执行之前。大模型生成的参数常常有类型不匹配、缺字段、枚举值乱写的情况如果你直接拿这些参数去执行底层函数很容易弄出一堆莫名其妙的运行时错误。加上一层显式校验以后95% 的脏参数在进到核心逻辑之前就被挡住了。3.4 第三步技能组合与上下文工程单个技能写完之后真正难的是让多个技能配合起来完成一个复杂任务。还是拿 SVG 图表来说如果用户的需求是“帮我把这个 CSV 文件里每个月的销售额画成柱状图”那么 Agent 的完整流程是读取 CSV - 解析数据 - 生成图表 - 返回结果。这就不是一个技能能搞定的了需要三个技能配合。我的做法是在上一层设计一个路由模块它负责把用户的自然语言需求翻译成技能的调用序列。这个环节的技能拆解如下read_csv_to_list把 CSV 内容读成 JSON 列表data_transform按字段筛选、聚合、排序整理成图表需要的[{label: 1月, value: 12000}]格式svg_chart_gen就是上面写的生成 SVG 图表的技能markdown_output把生成的 SVG 包装成 Markdown 引用格式方便模型在回答中展示路由模块的核心逻辑是让大模型基于用户请求生成一个技能调用计划然后逐个执行并收集结果。这里我强烈建议一步到位把上下文管理做进去每次技能调用的输入输出都要以结构化的形式记录进上下文中这样后续技能可以基于前序技能的结果做进一步处理。很多人做 Agent Skills 做得一塌糊涂问题不在于技能定义不好而在于上下文管理一塌糊涂。上下文里堆了太多历史对话噪音导致模型在技能选择时压根提取不到关键信息。我常用的缓解手段是维护一个“精简状态摘要”每执行一步技能之后把当前任务状态压缩成一两句话的摘要替换掉原来高信息密度的完整输出这样模型的决策负担明显下降。3.5 第四步接口封装与模型接入技能库的最终形态要暴露给外部使用不能每次都在 Python 里硬编码调用。我现在习惯用一个 FastAPI 服务把所有技能包一层 RESTful API好处是后续不管前端界面、机器人还是别的 Agent 来调用走统一端口即可方便维护。一个简化的接口封装from fastapi import FastAPI, HTTPException from pydantic import BaseModel, Field from typing import Optional, List app FastAPI(titleAgent Skills Service, version1.0.0) class SkillRequest(BaseModel): skill_name: str Field(description技能名称) params: dict Field(description技能参数) class SkillResponse(BaseModel): success: bool result: dict app.post(/v1/skills/execute, response_modelSkillResponse) async def execute_skill(request: SkillRequest): 统一的技能执行接口 skill_name request.skill_name params request.params try: if skill_name svg_chart_gen: result execute_svg_skill(params) elif skill_name read_csv_to_list: result execute_read_csv(params) else: raise HTTPException(status_code404, detailf技能不存在: {skill_name}) return SkillResponse(successresult[success], resultresult) except Exception as e: return SkillResponse(successFalse, result{error: str(e)})这个服务跑起来以后第一件要做的事情是写一个“技能健康检查”脚本。不要等服务上线了才发现某个技能早就挂掉了我通常会在技能注册表里记下每个技能的“期望行为基准”然后写一个自动化测试套件每次改完代码跑一遍回归确保现有技能不会被后续修改意外破坏。4. 实战经验Agent Skills 的常见坑与排查实录4.1 技能边界模糊模型频繁选错或乱选这是实践中最常见也最让人抓狂的问题。技能一多模型就容易“选择困难”甚至逻辑混乱地调用明显不对的技能。我踩过最惨的一次坑是“生成图表”和“生成流程图”两个技能描述高度相似模型经常把柱状图的需求路由到流程图技能上然后输出一堆 Box 和 Arrow 的 JSON画面惨不忍睹。排查思路其实很清晰对比技能描述的区分度。如果两个技能的描述在很多场景下都能“套上”那不是模型的问题是技能边界设计的问题。解决办法是明确划清界限在 SKILL.md 的“适用场景”和“不适用场景”里做显式的负向约束。我后来用了一个更粗暴但有效的方法给每个技能定义“极端反例”。设计一些你绝对不希望由这个技能处理的输入案例在描述里写“当用户需要X时不应该使用本技能而应该使用YYY技能”。这个做法能大幅降低模型的误判率。因为大模型很多时候不是不知道选什么而是缺少足够的“排除信息”来帮忙判断。4.2 参数传递的上下文污染另一个高频问题发生在多技能组合时。前一个技能的输出里混入了大量无关字段直接被塞进下一个技能的输入参数里导致后一个技能被噪音干扰或者直接报错。我之前做“文档解析 - 实体抽取 - 知识库写入”这个链路的时候文档解析器的输出结构复杂包含了很多原始文本、分页信息等字段。实体抽取技能拿到的输入里有大量无关字段有几次甚至把原始文本当成实体知识库存进来一堆垃圾数据。排查下来核心是缺一层数据清洗。每个技能在开始执行前必须按照自己的参数定义严格做过滤只保留自己需要的字段其他一律丢弃。宁可多写几行数据处理的代码也不要让脏字段流向下游。4.3 技能执行的安全边界与资源限制技能是要执行代码的代码就一定有安全风险。如果你把技能做成“文本描述 代码执行”的模式尤其要小心。这里的安全不是指外部攻击这种远的事情而是指大模型生成的代码本身可能就是不安全的或者是有 bug 导致无限循环、占用大量内存、访问不该访问的路径。我给自己的技能执行环境定了几条铁律技能只能运行在受限用户账户下禁止 root 权限运行。技能没有网络访问权除非有业务上的明确需求需要通过环境变量显式开启。技能无法访问宿主机上的任意目录代码只能写入独立的工作目录。技能执行必须有超时和内存限制。除了安全边界可观测性同样重要。我给所有技能接入统一的日志记录每次执行记录技能名称、入参、出参、耗时、错误信息。没有这个日志体系排查线上问题会让你崩溃因为 Agent 系统天生就是异步的、多层的报错位置极不确定。4.4 一个典型的排查案例实录分享一个近期的排查案例。有段时间线上的“数据图表生成”技能经常超时进入沙箱环境一查发现模型生成的代码里用了一种不合理的循环方式它对 50 万条数据做嵌套循环去重时间复杂度直接爆炸。这不是技能本身有 bug而是大模型写代码时的“惯性思维”导致性能悲剧。这个问题只能通过“输入约束”和“执行环境限制”双管齐下来解决。输入约束是指出数据量不能超过某个阈值超出就走批量方案执行环境限制是设置 CPU 和内存配额超了直接杀掉进程让模型回去重新生成。第二次生成时模型看到数据集大小和运行环境限制的描述生成的代码就合理多了。这个案例说明在 Agent Skills 体系里技能的健壮性不能只靠测试用例来保证执行环境的强约束是兜底的关键。测试只能覆盖你想象中的场景而大模型可以创造你想象不到的输入。5. 技能设计的分层哲学70% 封装30% 临场说得更抽象一点。我用 Agent Skills 构建系统时一直在坚持一个比例70% 的技能要预先封装好30% 的技能要允许在对话过程中临时生成。GR00T 的一些设计思路也验证了这个方向。预先封装好的是高频、稳定、语义明确的技能这些技能需要打磨到工业级稳定。临时生成的是低频、个性化、上下文强依赖的技能这种技能的价值在于灵活性不追求完美。如果你试图把 100% 的技能都预先封装好大概率会在 60% 的进度时陷入“过度设计”的泥潭花费大量时间做建设但收益有限。这个比例可以理解为Agent 的能力预置技能库的深度 × 临场技能生成的宽度。只做预置技能Agent 会变得很“死板”只做临场生成Agent 会变得很“飘忽”。两者缺一不可平衡才是关键。我在实际使用中发现把临场生成技能的能力本身做成一个技能是一个不错的巧劲。这个“技能生成器”技能接收自然语言描述的需求输出符合规范的新技能源码自动注册到技能库。这样 Agent 在处理全新任务时能够自我进化而这个进化能力本身也是可控的。如果你做 Agent 产品强烈建议把这个“自我进化”的能力加到路线图里它带来的体验提升是巨大的。6. 给想上手 agent-skills 的人一些实在建议最后给不同阶段的读者一点建议。如果你刚接触 Agent 开发建议不要从大而全的技能框架开始。拿一个真实的业务任务手工把它拆成 3 到 5 个技能跑通全流程感受一下“技能封装”和“直接给工具”的区别。你只有在真实任务里体会过“有了技能之后模型选择变准了”才能真正理解这套方法论的价值。如果你已经有 Agent 系统在运行可以从“工具重构为技能”开始。盘点现有工具函数把相关的函数按业务语义聚合写一个测试集对比重构前后的效果。这种重构的效果通常是立竿见影的——单单是上下文长度因为减少了无关工具描述而大幅下降就能让你的模型走得更稳、更快、更便宜。如果你正在设计一个产品级的 Agent 系统我建议不要只考虑“技能定义”还要考虑“技能运营”包括技能版本管理、A/B 测试、调用统计、失败分析、质量评分、灰度上线等。技能库不是写完就完事的静态资产它更像是一个持续演进的产品。我见过太多团队死在“一次性开发完就不管了”这个坑里技能质量的衰退对 Agent 系统的影响是悄无声息的——技能出错率上升、选择混乱、用户体验下降而你往往很难定位到是哪个环节出了问题。在技术选型上如果你只有一个建议可以记住那就是“先跑通再抽象最后规模化”。不要上来就搞一个巨复杂的技能编排引擎先把三五个核心技能的“开发-测试-部署-观测”闭环跑起来找到自己团队在这个体系里最舒服的节奏然后再慢慢扩展。这套东西做到最后最大的收获可能不是技术上的而是思维上的。你会发现Agent 开发本质上不是在“写代码”而是在“设计一套让模型能自主调用代码的接口体系”。你越早接受这个思维转换你的 Agent 系统就越早脱离“玩具”阶段进入真正的工程化阶段。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进