去年我在给某智能体框架写自定义插件时第一次接触 AI Skill 这个概念。当时我以为 Skill 就是一段提示词告诉大模型“你会做文本分析”它就会了。结果第一次跑就翻车——模型确实调用了 Skill但传进来的参数是整段聊天历史直接把我的分析程序干崩了。后来我才彻底想明白AI Skill 不是提示词而是一套能被智能体稳定调用的“可执行能力包”。它需要规范的目录结构、清晰的参数契约、独立可跑的代码缺一环都会在线上出问题。这篇文章我就用“文本洞察”这个 Skill 当例子从需求拆解到规范目录再到可运行代码和调试兜底手把手带你把整个过程跑一遍。内容不偏理论都是能直接落地的东西。1. 先搞清楚 Skill 到底是什么不是一段提示词而是一份目录契约1.1 我的定义Skill 元信息 运行逻辑 调用契约我见过很多新手把 AI Skill 理解成“给大模型加一段人设词”这其实是用错了方向。提示词是让模型“自由发挥”而 Skill 是让模型“调用一段固定逻辑”。打个比方你让一个人算 500 个字他可能会数错但你把计算器递给他他按一下就不会错。AI Skill 就是那个“计算器”它的核心价值是把不稳定的自由发挥变成稳定的可执行能力。我习惯把 Skill 拆成三件套组成部分作用对应文件元信息告诉智能体“这个技能叫什么、什么时候该用、参数是什么”manifest.json / SKILL.md运行逻辑真正干活的那段代码接收输入并返回结果handler.py / main.py调用契约输入输出的 JSON 结构定义让双方的沟通有据可依manifest 中的 parameters 字段 代码中的解析逻辑这三件套缺一个都会出问题。没有元信息智能体根本不知道何时调用你没有运行逻辑Skill 就是个空壳没有调用契约模型传的参数五花八门你的程序根本没法稳定处理。还有一个容易被忽略的点Skill 不一定非要联网。很多人一听到“给 AI 加技能”就以为是调外部 API。其实最简单的 Skill 完全可以是一个纯本地函数比如文本处理、格式转换、计算、文件整理。先跑通一个本地 Skill再往外部 API 扩展是性价比最高的学习路径。1.2 一个最小 Skill 的目录长什么样我见过不同平台对 Skill 目录的要求略有差异但核心结构大同小异。下面这个算是最小可用版本text_insight/ ├── manifest.json # 元信息名称、描述、入口、参数说明 ├── handler.py # 核心逻辑接收 JSON返回 JSON ├── requirements.txt # 依赖声明本示例可以为空 └── examples/ └── sample_call.json # 一个示例调用方便自己和模型理解为什么要这样分因为智能体运行时通常会先扫描目录读取 manifest 得到技能描述再根据描述决定是否调用。入口文件handler.py会被运行时拉起并通过标准输入输出传递 JSON。examples 目录则像“操作说明书”当你将来忘了这个 Skill 怎么用翻一眼示例就能想起来甚至可以直接把示例展示给模型看减少错误调用。很多刚入门的朋友会把所有内容塞进一个文件目录结构混乱。一旦 Skill 多起来你根本记不住哪个文件是干嘛的。规范目录的真正价值不是“好看”而是让智能体的自动发现机制能稳定工作让你自己也能在半年后无障碍维护。1.3 为什么“规范目录”比代码本身更重要代码写得再漂亮如果目录不规范智能体找不到入口一切都白搭。我调试过一个同事写的“技能包”逻辑没问题但他把入口函数放在了一个深层嵌套的模块里平台死活扫不到入口文件折腾了两天才发现是 manifest 里的 entrypoint 路径写错了。规范的目录至少带来三个可量化收益第一可发现性。运行时扫描目录时能在固定位置找到元信息不用你写一堆啰嗦的配置。每个平台的机制虽有差异但“入口明确”是所有平台的共同要求。第二可迁移性。当你想把一个 Skill 从一个平台搬到另一个平台只要目录结构规整改造的成本通常很低。我经常是先写好一套本地 Skill再按目标平台的细节微调 manifest 字段半小时内就能完成迁移。第三可测试性。入口文件独立之后你就能在终端里直接喂 JSON 给 handler.py不依赖任何平台 UI跑通了再挂上去排查问题快得多。2. 手写第一个 Skill需求拆解与目录骨架搭建2.1 选一个难度适中的需求文本洞察我选择的第一个 Skill 是“文本洞察”输入一段文字输出它的字数、句数、高频关键词和一句简易摘要。为什么选这个因为它有三个人畜无害的特点不依赖外部网络不申请密钥没有任何权限风险结果可验证字数对不对、关键词合理不合理你一眼就能看出来它覆盖了“参数接收、校验、处理、返回结构化结果”这条最完整的链路你能把通用方法论学会。另一个很重要的设计环节是“定义能力边界”。我这个 Skill 只做统计和摘要不负责情感分析不负责改写润色。为什么要主动限制因为如果你把太多能力塞进一个 SkillAI 更容易误用。比如用户只是想算字数模型可能因为 description 里出现了“分析”两个字就把整段文字交给你做情感分类结果返回了一堆没用的字段。能力边界越清晰误调用越少。功能清单我最后定为四件事统计字符数统计句子数提取高频关键词默认 5 个生成一句话摘要。有了清单目录和代码就有依据了。很多人写代码前不写功能清单写到一半发现逻辑不清就是因为“边界”没有提前画出来。这里的边界不只是“做什么”还包括“不做什么”。2.2 搭目录骨架我习惯在终端里先建目录再逐个创建文件mkdir -p text_insight/examples cd text_insight touch manifest.json handler.py requirements.txt examples/sample_call.jsonrequirements.txt 当前可以为空因为我们的 handler.py 只用 Python 标准库不需要 pip 安装任何依赖。这也是我推荐新手从本地 Skill 入手的原因少一个依赖就少一个环境变量少一堆后续排障的麻烦。如果你要写一个涉及网络请求的 Skill再往 requirements.txt 里加诸如requests这样的依赖就好。依赖越少运行时环境越稳定。2.3 写 manifest 元信息把“什么时候用”说清楚manifest 是 Skill 的身份证也是智能体做“路由决策”的依据。我见过太多人把 description 写得像广告词比如“这是一个功能强大的文本分析工具”结果模型完全不知道什么时候该调用它。正确写法是描述清楚“用户在什么场景下需要这个能力”同时注明“什么情况下不要用”。下面是我这个 Skill 的 manifest.json{ name: text-insight, version: 1.0.0, description: 当用户提供一段文字文章、笔记、评论、日志等并希望获得字数统计、句子数统计、高频关键词或摘要时使用。将输入文本作为 text 参数传入。不适用于文本生成、翻译或情感分析。, entrypoint: handler.py, parameters: { text: { type: string, description: 需要分析的原始文本UTF-8 编码建议不超过 10000 字。, required: true }, max_keywords: { type: integer, description: 最多返回几个关键词默认 5。, required: false, default: 5 } } }description 中“不适用于文本生成、翻译或情感分析”这句话非常关键。它是在给模型画红线当用户说“帮我写一段介绍”时模型就不会错误地调用你这个分析工具。参数定义也要尽量明确。比如 text 字段我写了“建议不超过 10000 字”这就给了模型一个尺寸预期。“required”标记则告诉模型缺了 text 就不要调用否则会返回错误。看似很小的字段在真实调度中能省下大量调试时间。你也可以把 manifest 中补充 author 或 description 字段但对我而言上述五个字段已经覆盖了最小可行范围。不要在最小目录里堆过多无效字段否则反而会让运行时解析出错。3. 可运行核心逻辑让 AI 的请求变成真实函数调用3.1 为什么入口选择 stdin/stdout JSON我见过很多 Skill 框架入口五花八门有的用命令行参数有的直接暴露一个 Python 函数。我自己的经验是入口越“无聊”越好最好让运行时只需要做一件事——把 JSON 字符串通过标准输入喂给进程再从标准输出读取 JSON 结果。用 stdin/stdout 的好处是不限制 Skill 的实现语言Python、Node、Shell 都能跑方便本地调试你在命令行里echo一段 JSON 就能测试避免“运行时直接 import 你的 Python 模块”这种强耦合想换成别的语言也不受影响。所以你下面的 handler.py 会设计成读sys.stdin的内容解析 JSON执行任务再把结果打印到sys.stdout。就是这么朴素。3.2 核心代码逐段拆解我把完整代码贴在下面你可以直接存成 handler.py#!/usr/bin/env python3 文本洞察 Skill读取 stdin JSON分析后输出 JSON。 import json import re import sys from collections import Counter STOP_WORDS set(的了是在我和有我不就在也人都一上很到说要去你会着没好看对打吧吗啊这那.strip()) def log(msg: str): 调试日志统一走 stderr避免污染 stdout 的协议输出。 print(f[text-insight] {msg}, filesys.stderr) def split_sentences(text: str): 按中英文句末标点切分句子。 sentences [s.strip() for s in re.split(r[。!?;\n], text) if s.strip()] return sentences def extract_keywords(text: str, max_keywords: int): 提取关键词先用连续汉字切片再做简单停用词过滤和频率统计。 tokens re.findall(r[\u4e00-\u9fa5]{2,}, text) words [] for token in tokens: if len(token) 4: words.append(token) else: # 简化处理按二字切分长词便于统计高频片段 words.extend([token[i:i 2] for i in range(0, len(token) - 1, 2)]) counter Counter(w for w in words if w not in STOP_WORDS and len(w) 1) return [w for w, _ in counter.most_common(max_keywords)] def build_summary(text: str, keywords, max_len: int 120): 生成简易摘要给句子按关键词覆盖数打分选得分最高的两句拼接。 keyword_set set(keywords) scored_sentences [] sentences split_sentences(text) for index, sentence in enumerate(sentences): score sum(1 for kw in keyword_set if kw in sentence) # 前两个句子略微加权让摘要贴近开头 if index 2: score 0.5 scored_sentences.append((score, sentence)) scored_sentences.sort(keylambda x: x[0], reverseTrue) summary .join(sentence for _, sentence in scored_sentences[:2]) if len(summary) max_len: summary summary[:max_len] ... return summary def run(raw: str) - str: 统一入口接收 stdin 原始字符串返回 JSON 字符串。 try: payload json.loads(raw) if not isinstance(payload, dict): return json.dumps({error: 请求必须是 JSON 对象}, ensure_asciiFalse) text payload.get(text, ) if not isinstance(text, str) or not text.strip(): return json.dumps({error: text 不能为空且必须是字符串}, ensure_asciiFalse) max_keywords int(payload.get(max_keywords, 5)) if max_keywords 0: max_keywords 5 text text.strip() keywords extract_keywords(text, max_keywords) result { char_count: len(text), sentence_count: len(split_sentences(text)), keywords: keywords, summary: build_summary(text, keywords) } return json.dumps(result, ensure_asciiFalse, indent2) except Exception as exc: return json.dumps({error: f处理失败: {exc}}, ensure_asciiFalse) if __name__ __main__: input_data sys.stdin.read() log(f收到调用, 原始输入长度: {len(input_data)}) print(run(input_data))这段代码看起来不复杂但每个函数都承担了一个明确的职责。split_sentences负责把文本切块。正则里的[。!?;\n]覆盖了中英文常用句末标点基本够用。真实场景如果遇到引号嵌套可能需要更复杂的分句逻辑但作为第一个 Skill这个版本刚刚好。extract_keywords是简化版关键词提取先找连续汉字串再把中文长串按二元切片最后用停用词表过滤掉“的了是在”这种无意义字。这个方法不算精确但它稳定、无依赖、可解释。你可以把它理解成“先跑通再优化”的思路后续如果要更准确可以替换成分词库或调用算法接口。build_summary的思路很有意思它不直接截取开头而是给每个句子按“覆盖了多少关键词”打分选分数最高的两句拼成摘要。这样做的好处是摘要能覆盖全文的核心信息而不是简单粗暴地取前几十个字。我在本地测试时发现这种基于关键词覆盖的规则摘要对付观点性文本已经表现得不错。run是统一入口。它负责三件事解析 JSON、校验参数、返回结构化错误。所有异常都被捕获并转成 JSON 错误消息而不是让进程崩溃。这非常关键因为 AI 平台调用 Skill 时最讨厌的就是进程直接挂掉连错误原因都拿不到。3.3 本地跑通与结果验证写好代码后立刻在终端里做一次冒烟测试cd text_insight echo {text: AI Skill 是一种给智能体补充能力的方式。它强调规范和可复用。我在本地试了一下效果还不错。} | python3 handler.py正常情况下你会看到类似下面的输出{ char_count: 48, sentence_count: 3, keywords: [ 智能体, 补充, 规范, 复用 ], summary: AI Skill 是一种给智能体补充能力的方式。它强调规范和可复用。 }具体数值可能因 Python 版本和文本内容略有差异但字段结构一定是稳定的。这一步跑通说明你的 Skill 核心逻辑没问题。我特别提醒一句这里返回的char_count是 Python 的len(text)它统计的是 Unicode 字符数不是字节数。如果你的输入包含 emoji 或特殊符号这个数字的含义要提前想清楚。产品需求如果要求“字数按字节算”你得在文档里写明避免和前端展示对不上。4. 调试与安全兜底真正能丢到生产环境的 Skill 长什么样4.1 爱捣乱的模型参数校验必须自己做很多人第一次写完 Skill觉得“能跑就行”。但等到接上大模型却发现模型比你想象中更“皮”。它会传空字符串会把 text 传成数组会把 max_keywords 传成字符串数字甚至会在 JSON 外面包一层 Markdown。所以run函数里的防御式校验绝不是多余的。我要强调的是不要把json.loads(raw)的异常吞掉。它必须返回给调用方让平台知道“输入内容不是合法 JSON”不要相信payload.get(text)拿到的类型。大模型可能传一个数字也可能传一个嵌套对象必须用isinstance(text, str)校验不要对max_keywords做无脑强制转换。如果用户传了 0会影响结果最好做个兜底。代码里还有一处很重要的兜底整个run函数外层套了try...except...。你千万不要觉得这是用来“隐藏错误”的它是用来“把错误变成信息”的。平台拿到一个格式良好的{error: ...}比收到一段 Python traceback 好处理得多。另外针对超长文本我强烈建议在正式版本里加一个长度上限。比如在run函数的开头对text做截断或直接返回错误MAX_TEXT_LENGTH 10000 if len(text) MAX_TEXT_LENGTH: return json.dumps({error: ftext 长度超过 {MAX_TEXT_LENGTH}}, ensure_asciiFalse)这个限制不仅保护你的程序也保护平台资源。要不然模型偶尔抽风把整本小说传进来你的 Skill 可能要算到天荒地老。4.2 日志与超时不影响协议输出的保命技巧这是 Skill 开发里最容易踩的坑没有之一。stdout 是协议通道你打印的所有调试信息如果都走print()就会把返回的 JSON 污染掉。平台那边解析失败你还不知道问题出在哪。我代码里的做法是单独写一个log()函数把调试日志打到sys.stderr。这样你在终端测试时能看到日志但python3 handler.py的重定向输出里只会保留干净 JSON。这是命令行程序开发中的老规矩标准输出留给机器标准错误留给人类。如果你要接入外部 API超时设置也要跟上。我用过很多新手写的 Skill调第三方接口时不设timeout结果平台侧等待 30 秒直接杀进程页面只显示“超时”两个字根本不知道是哪一步卡住。正确姿势是给所有网络请求设置 5~10 秒超时并在代码里捕获超时异常返回一个可读的错误消息。外部依赖越强越要做降级比如这次请求失败下次走备用接口或者直接返回“服务暂时不可用”。4.3 最小测试清单上平台前先自测五条用例我建议每个 Skill 在接入平台前都准备下面这张最小测试清单。不用做得很复杂但至少要覆盖“正常、边界、违法”这三类情况用例输入内容期望行为正常输入{text: 今天天气不错适合出去走走。}返回字数、句数、关键词、摘要空字符串{text: }返回{error: text 不能为空...}字段缺失{}返回{error: text 不能为空...}类型错误{text: 12345}返回{error: text 必须是字符串}超长文本text 长度超过上限返回明确长度错误这张表看起来简单但它能帮你提前消灭 80% 的线上问题。你可以把每个用例手动echo进 handler.py 跑一遍也可以在本地写一个小 shell 脚本批量测。我自己的习惯是在examples/sample_call.json里保留一个正常调用样例和错误样例。这样以后平台需要示例或者你需要自己复盘打开 examples 目录就一目了然。5. 完整示例与扩展建议照抄一个能上手的“文本洞察” Skill5.1 完整文件清单与最终整合方式现在整合一下你的text_insight目录应该是这样text_insight/ ├── manifest.json ├── handler.py ├── requirements.txt └── examples/ └── sample_call.jsonmanifest.json 用第 2.3 节的内容handler.py 用第 3.2 节的完整代码requirements.txt 保持空文件sample_call.json 放一个示例{ text: AI Skill 是一种给智能体补充能力的方式。它强调规范和可复用。, max_keywords: 3 }到这一步你已经有了一套可以本地运行的完整 Skill。接平台时只需要在平台后台上传text_insight目录或者把 manifest.json 和 handler.py 分别填写到对应配置项里即可。绝大多数智能体平台的“自定义工具/技能”入口本质上都是读取你提供的元信息再按约定调用入口脚本。只要你本地跑通了平台侧通常不会有太大问题。如果平台要求使用不同的元信息格式比如 YAML 而不是 JSON你也不必慌。字段含义基本是相通的name 对应技能名description 对应调用描述parameters 对应参数定义entrypoint 对应入口文件。你只需做一个格式翻译核心处理逻辑一行都不用动。5.2 从文本洞察扩展到真实业务 Skill文本洞察跑通之后你可以试着往两个方向扩展。第一个方向是“增加分析维度”。比如在返回结果中加入情感倾向积极、中性、消极或者识别文本中的数字、日期、专有名词。每加一个维度就在 manifest 里同步增加对应的输出说明让模型知道这个 Skill 的“能力上限”变高了。第二个方向是“替换业务场景”。举个例子把输入从“任意文本”改成“开发日志”返回结果改成“错误级别、涉及模块、时间戳、责任人”你就得到了一个日志分析 Skill。再把输入改成“商品评论”返回结果改成“评分、优点、缺点、购买建议”你又得到了一个评论洞察 Skill。改代码并不难难的是把 manifest 的 description 和业务场景对齐。我写业务 Skill 时会先让产品同事描述“用户在什么情况下会用到”再把这句话翻译成 description。服务内部逻辑越通用description 越要具体因为模型是根据 description 做调用的。如果 Skill 要接入外部 API记住两个原则密钥从环境变量读不硬编码在代码里请求外层必须 try/except超时和权限错误都要转成 JSON。一个不设防的 Skill 接入生产环境就像把大门敞开给别人调用风险很大。5.3 我自己反复用的一套开发流程习惯最后分享一套我实践了很久的流程虽然简单但每次都能帮我少踩坑。第一步先把期望的输出样例写出来放到 examples 里。不要一上来就写代码。我在项目里是“先定义结果再写实现”效果比直接撸代码好得多。第二步用固定 JSON 在本地把 handler 跑通。这一步确保的是“逻辑正确”。你可以在终端里反复改输入观察输出直到满意为止。第三步再写 manifest。很多人本末倒置先写描述再写代码结果代码和描述对不上。先有代码再根据代码的真实行为写 description描述才准确。第四步接平台后先小范围试用并观察调用记录。关注点只有一个模型在你预期的情况下调用它了吗如果发现它在不该调用的时候调用了八成是 description 里有歧义词马上改。第五步每次改动代码或描述都同步更新 examples 和 manifest 中的 version 字段。这样即使线上出问题也能快速回溯。这套流程我用到现在最大的体会是Skill 难不在代码难在让大模型在正确的时机想起你。规范目录和可运行代码是地基而准确描述和详尽示例才是让 AI 真正“会用”你的关键。如果你也想动手写自己的第一个 Skill建议从现在这个文本洞察开始完整跑通一次。当你在终端看到那一行标准 JSON 输出时你就已经越过新手阶段了。