做智能体开发这一年多我最大的一个感受是真正拉开项目水平的往往不是模型选得有多新、Prompt写得有多花而是一堆不起眼的 skills 怎么设计、怎么组织、怎么复用。第一次接触“技能包”这个概念是因为一个特别具体的痛点当时我在做一个信息汇总 Demo同样一个“从网页里抽正文”的功能先写进了一个脚本再塞进了一段 Prompt最后又单独暴露成了一个工具接口。三个地方各写一份逻辑大同小异但换一个输入场景就要改三处改到最后自己都分不清哪份是新的。后来参考了主流的 Agent Skills 机制把这类能力统一封装成独立的技能包模型调用率、复用性、排查效率都明显上了一个台阶。这篇内容就围绕“skills”展开聊清楚技能包到底解决什么问题、内部结构长什么样、怎么从零开发一个可用的技能包、以及哪些坑是常规文档里不会写的。适合正在做智能体应用、或者想把自己手里的脚本升级成可复用能力的开发者参考。1. 内容整体设计与思路拆解1.1 从脚本到技能包工程化的分水岭先聊一个很多人没意识到的问题普通脚本和技能包差别到底在哪普通脚本解决的是“开发者自己怎么调用”。你把一串逻辑写进文件命令行跑一次输出结果完事。它的问题是换一个调用方、换一个业务场景、换一种输入格式这段逻辑就基本报废了。它绑定了“某个具体任务”而不是“某类能力”。技能包解决的是“模型怎么理解、怎么自动调度、怎么标准化复用”。它不仅包含执行逻辑还包含一份给模型看的“说明文档”——什么时候该用这个技能、参数怎么填、返回什么结构。模型读到用户的问题之后自己去判断“这件事应该调用哪个技能”然后把参数填好触发执行。我用一个生活化类比来解释。以前你带实习生给他一份事无巨细的文档他也能把事情做出来但换一个任务、换一种问法他就懵了。技能包相当于给实习生发了一套标准的“工作卡”岗位说明、输入单、操作SOP、所需工具。实习生看到任务先翻工作卡匹配上了就照着执行匹配不上就上报。这套机制稳定的原因不是因为实习生更聪明而是因为“该在什么场景做什么、怎么做、做完怎么交付”都被明确定义了。当时我做那个信息汇总 Demo 的时候逻辑全堆在一个超长 Prompt 里模型每次的判断都不稳定。拆成技能包之后每个功能模块边界清晰模型只需要做“选择”不需要做“猜测”效果自然就稳了。1.2 技能包的组成结构四层拆解一个标准技能包我习惯拆成四层来看。第一层是描述层Skill Description。这一层是写给模型看的不是写给开发者看的。它回答三个问题这个技能是什么、在什么场景下使用、典型输入长什么样。很多人的技能包不被调用80% 的问题出在这一层写得不够清楚。第二层是参数层Input Schema。这一层定义模型调用技能时需要填哪些字段。用 JSON Schema 来描述比如字段类型、是否必填、可选项枚举、默认值。参数层设计得好不好直接决定模型填参数的时候会不会出错。第三层是执行层Execution Logic。这一层是真正干活的代码。它接收模型填好的参数执行实际操作然后把结果按约定格式返回。执行层的重点是稳定性不是炫技。第四层是资源层Dependencies Permissions。这一层包括技能运行所需的依赖包、外部服务访问权限、超时配置、安全策略等。资源层很容易被忽略但生产环境出问题大半都出在这里。用一张表对比一下“普通函数”“API”“插件”“技能包”这四者的区别类型服务对象核心关注点典型问题普通函数开发者代码复用、逻辑封装换场景就要重写API应用与系统通信协议、鉴权、稳定性模型不知道怎么调插件应用平台功能扩展、界面集成太重不适合细粒度调度技能包模型与智能体可描述、可调度、可复用描述和参数设计难这个表格里最关键的一行是最后一行技能包的服务对象是模型。你和模型之间没有口头沟通的机会唯一的沟通渠道就是那层描述文本。很多人把技能包当成普通函数来写结果模型理解不了、触发不了问题就出在“服务对象搞错了”。1.3 设计取舍不是所有场景都该上技能包技能包虽好但不要过度设计。我自己见过一些项目明明是一个固定流程非要拆成五个技能包结果模型在技能之间来回跳上下文被撑爆调用链路复杂到没法排查。什么情况下才值得做技能包我一般用四个条件来判断功能边界是否清晰能不能用一句话说清楚“这个技能负责什么”。输入输出能否结构化能不能用 JSON Schema 描述清楚参数和返回结果。是否有跨场景复用需求同一个功能是否会被多个任务、多个智能体用到。是否需要模型自主调度用户的问题是否会动态变化需要模型自己决定是否调用。如果四个条件都满足才值得动手做技能包。如果只是固定流程、固定输入用 Prompt 脚本反而更直接。技能包的本质是一种工程化抽象抽象是有成本的——开发成本、调试成本、维护成本。判断什么时候该抽象比抽象本身更重要。2. 核心细节解析与实操要点2.1 描述层写给模型的使用手册描述层是整个技能包里最关键、也最容易被敷衍的部分。很多开发者写 description 就写一句话比如“Extract web content”。这在模型眼里等于什么都没说。模型看到一个技能它需要判断“这个问题是不是这个技能能解决的”判断依据就是这段描述。描述越模糊模型就越不敢调用。我总结了一个描述层的“三段式写法”。第一段这个技能是什么擅长做什么。要具体不要用空泛的动词。 第二段在什么场景下应该触发调用。这是最关键的一段的要写“用户提出哪类问题时你可以使用本技能”。 第三段典型输入示例。给一两个真实输入示例帮助模型理解参数格式。拿“网页正文抽取”举例差描述是Extract web content.好描述是Extract the main text content of a web page and convert it to clean Markdown format. Use this skill when the user provides a URL and asks to summarize, collect, save, or quote the page content. Typical input: https://example.com/article?id1, with optional output format markdown.注意“Use this skill when...”这个句式它是在主动告诉模型触发条件。这比单纯描述功能要有效得多因为模型的判断过程本质上是“问题特征匹配技能描述”你把触发条件写清楚了匹配成功率会大幅提升。2.2 参数协议JSON Schema 不是细枝末节参数层是第二个容易出问题的地方。模型不是人类它填参数的时候不会“灵活变通”它只会严格按照它理解到的 Schema 来填。Schema 设计得不好模型就会填错、漏填、编造字段。我常用的一套基础参数设计原则参数要扁平不要搞多层嵌套对象。嵌套层级越多模型出错率越高。必要的时候用枚举enum给模型指路。比如输出格式就写成 markdown、json、text 三个枚举值模型大概率会选对。能设默认值的就设默认值。比如超时时间、输出语言、返回数量这些参数设了默认值模型少填一个参数就少一次出错机会。required 字段要精简。我见过一个技能包required 里列了八个字段模型每次调用都要编两个理由说明为什么填不出来。把真正必需的字段控制在三四个以内效果会好很多。给一个实际的 JSON Schema 参考{ name: extract_web_page, description: Extract main text content from a web page and convert it to Markdown or JSON. Use when the user provides a URL and asks to summarize, save, or quote page content., parameters: { type: object, properties: { url: { type: string, description: The full URL of the web page, starting with http:// or https:// }, output_format: { type: string, enum: [markdown, json, text], default: markdown, description: The expected output format }, max_length: { type: integer, default: 3000, description: Maximum length of extracted text in characters } }, required: [url] } }这个 Schema 只有 url 是必填的output_format 和 max_length 都有默认值。模型只需要填一个 URL 就能调用成功出错率就低很多。2.3 执行层稳定性比功能炫技更重要执行层是真正跑代码的地方。很多人把执行层当成写业务逻辑的地方花大量时间优化算法结果忽略了最基础的稳定性问题——超时、异常、返回格式不统一。我在实际项目中遇到的执行层问题按频率排序是网络请求超时。第三方接口响应超过 30 秒模型那边等不到结果整个链路就断了。异常没有兜底。解析出错、字段缺失、编码异常直接抛 Exception返回给模型一堆堆栈信息模型也看不懂。返回结果格式不统一。有时返回字符串有时返回数组有时返回 JSON模型下游处理逻辑只能靠猜。执行层的一个好习惯是不管成功还是失败都返回结构化的结果。成功时返回规定好的数据结构失败时返回一个包含错误类型的 JSON而不是裸抛异常。def extract_web_page(url: str, output_format: str markdown, max_length: int 3000): try: # 这里是具体的抓取和解析逻辑 result fetch_and_parse(url, output_format, max_length) return { success: True, data: result } except TimeoutError as exc: return { success: False, error_type: timeout, reason: frequest timeout after 30s: {url}, retryable: True } except InvalidUrlError as exc: return { success: False, error_type: invalid_url, reason: fURL is malformed: {url}, retryable: False }这个模式的好处是模型拿到结果后可以通过 success 字段明确知道成功还是失败通过 error_type 判断是否可以重试或需要换参数。这种把“失败也变成可处理的信息”的思路才是稳定的技能包该有的执行层。3. 实操过程与核心环节实现3.1 实战从零开发一个“竞品页面结构化抽取”技能包理论讲了那么多我们来走一遍完整实操。这个场景来自一个市场调研智能体项目智能体需要根据用户提供的链接把竞品官网页面抽取成结构化信息——产品名称、核心卖点、定价信息、页面更新时间。第一步先拆需求。输入参数有两个页面 URL、需要抽取的字段列表fields。可选参数有一个超时时间。输出是一个结构化 JSON结构如下{ product_name: , selling_points: [], pricing: {}, last_updated: , raw_url: }第二步定义执行函数。核心逻辑不复杂请求页面、解析 HTML、提取指定字段import requests from bs4 import BeautifulSoup def extract_product_page(url: str, fields: list, timeout: int 20): try: resp requests.get(url, timeouttimeout, headers{ User-Agent: Mozilla/5.0 (compatible; AgentBot/1.0) }) resp.raise_for_status() except requests.Timeout: return {success: False, error_type: timeout, reason: ftimeout {timeout}s} except requests.HTTPError as exc: return {success: False, error_type: http_error, reason: str(exc)} soup BeautifulSoup(resp.text, html.parser) result {} if product_name in fields: h1 soup.find(h1) result[product_name] h1.get_text(stripTrue) if h1 else if selling_points in fields: items soup.select(.selling-point, .feature-item, [data-feature]) result[selling_points] [i.get_text(stripTrue) for i in items][:10] # pricing 和 last_updated 的解析逻辑可以按页面结构扩展 result[last_updated] find_meta_date(soup) result[raw_url] url return {success: True, data: result}第三步编写描述层和参数层。描述层沿用前面的“三段式”参数层设计成url字符串必填。fields字符串数组枚举可选值默认给全部字段。timeout整数默认 20一般不传给模型。给模型的描述是“Extract structured product information from a competitor product page. Use when the user provides a product URL and wants to know product name, selling points, pricing, or last updated time. Typical input: https://competitor.example.com/product/12345”第四步做本地验证。在接入模型之前先用一条固定 URL 直接调用函数确认执行层能跑通、返回结构符合预期。这一步很多人跳过结果是模型那边调了半天才发现是执行层本身的 bug。3.2 注册与加载让模型真正“看到”新技能技能包开发完了下一步是让模型能够发现并调用它。以常见的智能体框架为例技能包通常以“一个目录一个技能”的方式组织skills/ extract_product_page/ SKILL.md main.py requirements.txtSKILL.md 就是前面说的描述层和参数层main.py 是执行层requirements.txt 声明依赖。框架在启动时扫描 skills 目录读取每个 SKILL.md把技能列表注册给模型。模型每次收到用户消息时会先看到这个技能列表再决定调用哪一个。这个环节最常见的困惑是“为什么我加了技能模型好像没反应”。我建议养成一个好习惯把模型真正“看到”的上下文打印出来。确认模型收到的技能列表里确实包含新技能名。确认描述文本没有被截断。确认技能名和描述里的触发词和用户问题的表达方式对得上。比如用户说“帮我看看这个页面有什么卖点”你的技能描述里写的是“Use when the user provides a product URL and wants to know product name, selling points...”这个匹配度就没问题。如果你写的是“A function to parse HTML pages”模型大概率不会选它。调试期可以先把其他技能临时注释掉只留一个技能看模型是否能稳定触发。能触发再逐步把它加回技能列表里。逐个验证比一次全上然后猜问题要高效得多。3.3 多技能组合编排日历查询与会议纪要素材整理单个技能能跑通不算完生产场景里更常遇到的是多个技能组合起来完成一个完整任务。我做过一个日程管理智能体需要组合“查询日程”和“生成会议纪要”两个技能。流程是这样的用户说“我今天下午的会议帮我整理一下待办”。模型先调用“查询日程”技能拿到下午的会议列表。拿到会议列表后模型发现有一个会议有录音链接于是调用“生成会议纪要”技能把录音转成文字并提取待办事项。最后模型把待办事项汇总输出给用户。这个流程里的关键点有两个。第一个关键点是中间结果的上下文传递。模型把第一个技能返回的 JSON 结果放在上下文里再决定要不要调用第二个技能。所以每个技能的输出结构必须清晰。如果“查询日程”返回的是一大段 Markdown 表格模型解析起来就费劲后续步骤就容易断。第一代版本我让“查询日程”返回原始 JSON模型反而容易提取字段信息。第二个关键点是编排顺序不能写死。模型需要有一定的自由裁量权它可以根据上下文判断先调用哪个、跳过哪个。所以技能描述里不要写“必须先调用 A 再调用 B”而是让模型基于当前上下文自行判断。实测下来给模型自由裁量权比强制编排一个固定 DAG 的容错性高很多。因为真实用户输入千变万化固定编排很容易在某个环节卡住。4. 常见问题与排查技巧实录4.1 技能存在但模型一直不调用这是最让人头疼的问题。你辛辛苦苦写了技能包注册也成功了日志里能看到模型收到的技能列表里有它但模型就是不用。我排查过很多次最后归纳为三个主要原因。第一个原因是描述层触发条件写得模糊。模型的判断依据是描述文本和用户问题的语义匹配度。描述里写“Extract web content”用户说“帮我总结一下这个链接里讲了什么”虽然语义上相关但模型可能认为“总结”和“抽取”不是一件事。改成“Use when the user provides a URL and asks to summarize, collect, or save the page content”之后触发率明显上升。现象首要排查点次要排查点技能列表里有模型不调用描述里有“应当触发”的条件与其他技能描述高度重合调用率低时灵时不灵描述中的触发词覆盖不全模型上下文太长技能列表被截断调用报错参数明显不合理参数 Schema 有歧义描述和参数没有示例第二个原因是多个技能之间的描述重叠。我一开始在系统里同时挂了一个“网页正文抽取”技能和一个“网页结构化抽取”技能两个描述里都有“URL”“提取”等关键词。模型每次选哪个基本靠猜。解决方法是明确划定边界正文抽取侧重“总结、引用全文”结构化抽取侧重“提取产品名、价格等信息”。边界清晰之后模型的选择就稳定了。第三个原因是技能数量太多。当技能列表超过二十个模型在有限的上下文窗口里需要处理大量不相关的描述信息决策质量会下降甚至可能忘记后面那些技能的存在。解决方法是分组管理先让模型通过一个“路由器”技能决定去哪一组技能里找。这一步是后面要讲的技能库设计的基础。4.2 参数解析错乱Expected object but got string模型在调用技能时偶尔会把参数格式搞错。最常见的一个报错是“Expected object but got string”。日志显示模型传了一个 JSON 字符串而不是 JSON 对象。这种情况通常是因为模型把参数用 Markdown 代码块包裹起来了或者把整个参数体当成了字符串字段。应对这个问题我从两个方向解决。第一个方向是给模型更多示例。在参数 Schema 的 description 字段里我并不只写字段含义而是加上“示例值”。例如url: { type: string, description: Full URL of the web page. Example: https://example.com/article?id123 }模型看到示例往往会更规范地填参数。第二个方向是写一个容错解析器。在框架层统一处理模型传参时的常见问题检查参数是不是被 Markdown 代码块包裹有的话先剥掉检查参数是字符串还是对象是字符串就尝试二次 JSON.parse遇到字段名不一致就做一层映射。这个容错层不复杂但能实打实减少不少线上报错。4.3 运行环境与安全边界技能包本质上是让模型触发一段代码执行这就意味着外部输入会直接进入你的执行环境。我在这方面吃过一次亏当时写了一个“下载并解析文件”的技能没有限制参数里的 URL 域结果模型根据用户输入请求了一个内网地址幸好当时环境隔离做得好没有造成实际影响。从那之后我严格遵守几条安全底线。第一所有外部输入参数都要做校验。URL 只允许 http/https 协议路径参数必须规范化禁止包含“..”等特殊符号。第二技能包运行环境要隔离不要直接在主进程里跑未知代码或抓取逻辑。第三日志不要打印完整的外部参数尤其是 URL、Token、密钥这些信息脱敏之后再记录。第四给技能设置超时和重试次数防止一个慢请求拖垮整个智能体。5. 技能包的扩展方向与工程化沉淀5.1 从单个技能到技能库单个技能能稳定运行之后下一步是把它放进一个可维护的技能库体系里。这个阶段要考虑的不再是“怎么写好一个技能”而是“怎么管好一百个技能”。我用的管理方法有三个原则。第一是命名规范。每个技能包的命名要能看出功能边界比如 extract_web_page、query_calendar、generate_meeting_notes避免用 ambiguous 的名称比如 utils、helper。第二是版本管理。技能包也要有版本号改动后要更新描述、记录变更点。模型调用某个技能时日志里要能追溯到具体版本。第三是白名单机制。不同的智能体应用加载不同的技能集合。比如面向市场调研的智能体只加载竞品分析和行业信息相关技能不加载会议纪要技能。这样可以减少模型的选择负担从源头避免技能间冲突。5.2 回归评估与观测技能包上线之后没有观测就等于摸黑运行。我建议维护一份标准回归样本集每个技能准备三到五条典型的输入问题比如“抽取这个链接的产品卖点”“总结这个页面内容”。每次改完代码后跑一遍回归集对比调用成功率、返回结果是否符合预期。观测指标方面我重点关注四个调用成功率模型成功触发技能并拿到 success 返回的比例。参数校验失败率模型传参不合法导致执行失败的比例。单次调用耗时从模型发起调用到返回结果的时长。返回数据质量抽取结果的完整性、字段非空率。这些指标不一定要做得很复杂先记录到一个日志表里每周扫一眼就能发现问题。比如某个技能的调用成功率突然从 90% 掉到 60%大概率是最近改过描述或参数 Schema 导致的回归回滚到上一个版本就行。5.3 多技能协作中的数据流设计最后聊一个进阶话题多个技能协作时数据怎么在技能之间流动。最基础的做法是让模型当“数据搬运工”。模型把技能 A 的返回结果读进上下文再填进技能 B 的调用参数里。这个方式简单但有两个问题一是模型搬运长文本容易出错二是中间结果占用大量上下文窗口影响模型整体表现。更工程化的做法是定义一个统一的中间数据格式。比如会议纪要技能的输出统一成一个“事件 时间 负责人 待办事项”的结构日历查询技能的输出也统一成同样的结构。两个技能只要都遵守同一个结构模型在中间环节只需要做字段映射不需要理解两种完全不同的输出结构。我做日程管理智能体时把会议纪要的待办和日历查询的事件格式统一之后整个编排流程一下子清爽了很多。再进一步还可以给技能加“降级策略”。比如“网页正文抽取”技能超时了就自动降级到“链接摘要生成”技能用模型直接读取链接内容生成摘要。用户无感知但任务没断。这种降级逻辑需要事先定义好主备关系并且备选技能的返回格式要和主技能保持一致。我自己踩过几次坑之后的体会是技能包设计不要一上来就追求“大而全”。先把一个边界清晰的小功能做成标准技能包跑通一轮完整的调用链路再慢慢往外扩展。这个过程中最有价值的部分不是代码本身而是你对自己功能边界的理解。每分出一个技能包其实都是在逼自己回答一个问题这件事最核心、最稳定、最值得复用的部分到底是什么。后续还可以往更有意思的方向探索比如给技能包增加缓存层、支持结果复用、用 DAG 编排代替模型自由调度。不过那些就是另一个话题了。先把手里这一个技能包做好、调顺、沉淀下来你会发现在智能体工程化这条路上这一步迈得比想象中更关键。