1. 从“ponytail”这个标题说起:它到底是什么
第一次看到“ponytail”这个词,很多人脑子里蹦出来的画面是发型——马尾辫。但在技术圈和工具链语境里,ponytail 早就不是发型那么简单了。最近一段时间,ponytail skill、ponytail 插件、插件 ponytail 如何使用这几个词频繁出现在搜索框里,说明有一批人正在接触或者试图搞懂这个东西。我前后花了差不多两周时间,把 ponytail 相关的插件形态、技能封装方式、实际使用场景摸了一遍,踩了一些坑,也总结出了一些真正能落地的经验。
先把话说清楚:ponytail 在当前的技术语境下,通常指的是一种轻量级的任务编排与技能封装机制,它以一个可插拔的插件形态存在,核心能力是把零散的操作步骤、工具调用、数据处理逻辑打包成一个可复用的“技能单元”。你可以把它理解成一个“任务胶囊”——把一段固定流程装进去,下次遇到同样的场景,直接调用这个胶囊就行,不用重新写一遍。它解决的问题很具体:重复性操作的标准化和复用。适合谁来参考?如果你平时需要处理大量重复性的流程化工作,比如数据清洗、文件批量处理、接口联调、内容生成流水线,那 ponytail 这套思路值得花时间研究。如果你只是偶尔做一两次手动操作,那暂时用不上,不用硬套。
我最初接触 ponytail 是因为一个批量处理的需求:手头有几百个结构类似的文本文件,需要按固定规则提取字段、做格式转换、再输出成统一格式。手动做了一遍之后觉得太蠢了,就开始找有没有现成的编排工具。试了几个方案之后,ponytail 的插件化思路最符合我的预期——它不要求你重写整个流程,而是让你把每一步封装成独立的 skill,然后像搭积木一样串起来。这个设计哲学很关键,后面我会详细拆解。
2. ponytail 的核心设计思路与方案选型
2.1 为什么是“技能封装”而不是“脚本堆叠”
大部分人处理重复任务的第一反应是写脚本。Shell 脚本、Python 脚本、批处理文件,哪个顺手用哪个。这没什么问题,但脚本堆叠到一定数量之后会暴露几个硬伤:第一,脚本之间的依赖关系不透明,A 脚本改了输出格式,B 脚本可能就挂了;第二,复用粒度太粗,一个脚本往往绑定了特定的输入路径和输出路径,换个环境就得改;第三,调试成本高,一个长脚本跑到一半出错,你得从头排查。
ponytail 的 skill 封装机制就是冲着这几个痛点来的。它把每个操作步骤定义为一个独立的 skill,每个 skill 有明确的输入契约和输出契约。你调用一个 skill 的时候,只需要关心它要什么、它给什么,不需要关心它内部怎么实现的。这个思路和微服务架构里的“服务边界”概念很像——每个服务只负责一件事,服务之间通过明确定义的接口通信。
我实际用下来的感受是:当你把流程拆成 5 到 8 个 skill 之后,整个流程的可维护性会有质的提升。因为每个 skill 都可以单独测试、单独替换、单独优化。比如我那个文本处理流程,拆成了“文件扫描 skill”“编码检测 skill”“字段提取 skill”“格式转换 skill”“输出写入 skill”五个部分。后来发现编码检测那一步用了一个不太准的库,直接换掉那个 skill 就行,其他四个完全不受影响。如果是一整条脚本,改一处就得重新跑全流程验证。
2.2 插件化架构带来的灵活性
ponytail 以插件形态运行,这意味着它不绑定特定的宿主环境。你可以把它挂到编辑器里、挂到命令行工具里、挂到自动化平台里。这个设计的好处是:你不需要为了用 ponytail 而改变现有的工作环境。我平时主力用 VS Code 写代码,ponytail 插件直接装在编辑器里,写代码的时候顺手就能调用 skill,不用切终端。
插件化还有一个隐性优势:生态复用。因为 skill 是独立封装的,理论上你可以把别人写好的 skill 直接拿过来用,也可以把自己写的 skill 分享出去。我目前用到的几个 skill 就是从社区里扒下来的,改了几个参数就能适配自己的场景。当然,这里要注意版本兼容问题——不同版本的 ponytail 插件对 skill 的接口定义可能有差异,拿别人的 skill 之前先确认一下版本号。
2.3 和其他方案的核心差异
市面上做任务编排的工具不少,有偏可视化的,有偏代码的,有偏云端的。ponytail 的定位比较特殊:它既不是纯可视化拖拽,也不是纯代码编排,而是介于两者之间——用配置文件定义 skill,用插件来执行。这个定位决定了它的适用边界。
| 对比维度 | ponytail | 纯脚本方案 | 可视化编排工具 |
|---|---|---|---|
| 学习成本 | 中等 | 低 | 低 |
| 复用粒度 | 细(skill 级) | 粗(脚本级) | 中(节点级) |
| 版本管理 | 友好(配置文件可 diff) | 一般 | 较差(配置藏在数据库) |
| 调试体验 | 好(单 skill 可测) | 一般 | 一般 |
| 环境依赖 | 低(插件形态) | 极低 | 高(需要平台) |
| 适合场景 | 流程化重复任务 | 一次性任务 | 非技术人员使用 |
我选 ponytail 的核心原因是:我的任务既有重复性,又需要频繁调整参数,而且我希望整个流程的配置能进 Git 做版本管理。可视化工具虽然上手快,但配置不好做 diff,改了什么全靠记忆。纯脚本方案虽然灵活,但复用粒度太粗。ponytail 刚好卡在我需要的那个位置上。
3. ponytail 插件的安装与基础配置
3.1 安装前的环境确认
在装 ponytail 插件之前,有几件事必须先确认,不然装完了跑不起来会很浪费时间。第一,确认你的宿主环境版本。ponytail 插件对宿主版本有最低要求,版本太低会直接报兼容性错误。第二,确认运行时依赖。ponytail 本身是插件,但它依赖的 skill 执行引擎可能需要额外的运行时,比如 Node.js 或者 Python 的某个版本。第三,确认权限。有些宿主环境对插件的文件系统访问权限有限制,如果你的 skill 需要读写本地文件,得提前把权限开好。
我踩过的坑:第一次装的时候没注意宿主版本,装完了插件面板能打开,但一执行 skill 就报“engine not found”。排查了半天才发现是宿主版本低于插件要求的最低版本。所以这一步别跳过,花两分钟确认一下,省得后面折腾。
3.2 插件安装的两种方式
ponytail 插件的安装方式取决于你的宿主环境。常见的有两种:
方式一:通过插件市场安装。如果你的宿主环境有内置的插件市场,直接在市场里搜“ponytail”,找到对应的插件,点安装就行。这种方式最省事,插件市场会自动处理版本匹配和依赖安装。安装完成后通常需要重启宿主环境,让插件生效。
方式二:手动安装。如果插件市场里没有,或者你需要装特定版本,就得手动来。步骤是:下载插件的安装包,解压到宿主环境的插件目录,然后在配置文件里注册插件路径。手动安装的好处是你可以控制版本,坏处是依赖得自己装。
# 以命令行宿主为例,手动安装 ponytail 插件的典型流程 # 1. 创建插件目录 mkdir -p ~/.host/plugins/ponytail # 2. 解压插件包到该目录 tar -xzf ponytail-plugin-v2.3.1.tar.gz -C ~/.host/plugins/ponytail # 3. 安装运行时依赖 cd ~/.host/plugins/ponytail && npm install --production # 4. 在宿主配置文件中注册插件 # 编辑 ~/.host/config.json,在 plugins 数组中加入插件路径安装完成后,验证插件是否生效:打开宿主环境的命令面板,输入“ponytail”,如果能看到相关命令列表,说明插件已经加载成功。如果看不到,检查宿主日志里的插件加载记录,通常会有明确的错误提示。
3.3 初始化配置的关键参数
插件装好之后,需要做一次初始化配置。ponytail 的配置文件通常是一个 JSON 或 YAML 文件,放在宿主环境的配置目录下。核心参数有这几个:
- skill_root:skill 的存放根目录。所有 skill 定义文件都放在这个目录下,ponytail 会自动扫描并加载。
- runtime:skill 执行时使用的运行时。可以是内置运行时,也可以指定外部运行时路径。
- timeout:单个 skill 执行的超时时间,单位秒。默认通常是 30 秒,如果你的 skill 涉及大量 IO 操作,建议调到 120 秒以上。
- log_level:日志级别。调试阶段建议设为 debug,稳定运行后改成 warn 或 error,避免日志文件膨胀。
- max_concurrent:最大并发 skill 数。如果你的流程里有多个独立 skill 可以并行执行,调大这个值能显著缩短总耗时。但要注意宿主环境的资源限制,别调太大把内存跑满了。
{ "ponytail": { "skill_root": "./skills", "runtime": "builtin", "timeout": 120, "log_level": "debug", "max_concurrent": 4 } }提示:初始化配置改完之后,一定要重启宿主环境或者重新加载插件,配置才会生效。我遇到过改完配置没重启,排查了半天以为配置写错了,结果只是没生效。
4. ponytail skill 的编写与实操要点
4.1 skill 的基本结构
一个 ponytail skill 本质上是一个描述文件加一个执行体。描述文件定义 skill 的元信息(名称、版本、输入参数、输出格式),执行体是实际干活的代码。描述文件通常用 YAML 或 JSON 写,执行体可以用 JavaScript、Python 或者 shell。
先看一个最简单的 skill 描述文件长什么样:
# skills/text-extract/skill.yaml name: text-extract version: 1.0.0 description: 从文本文件中提取指定字段 inputs: - name: file_path type: string required: true description: 待处理的文本文件路径 - name: pattern type: string required: true description: 提取字段的正则表达式 outputs: - name: extracted type: array description: 提取到的字段列表 runtime: python entry: extract.py这个描述文件告诉 ponytail:这个 skill 叫 text-extract,需要一个文件路径和一个正则表达式作为输入,输出是一个数组。执行时调用同目录下的 extract.py。
对应的执行体:
# skills/text-extract/extract.py import re import json import sys def main(): # ponytail 通过环境变量或标准输入传递参数 params = json.loads(sys.stdin.read()) file_path = params["file_path"] pattern = params["pattern"] with open(file_path, "r", encoding="utf-8") as f: content = f.read() matches = re.findall(pattern, content) # 输出结果到标准输出 print(json.dumps({"extracted": matches})) if __name__ == "__main__": main()这个结构看起来简单,但有几个关键点需要注意。第一,输入参数的传递方式。ponytail 通常通过标准输入传递 JSON 格式的参数,执行体需要解析这个 JSON。第二,输出格式。执行体必须输出 JSON 格式的结果,ponytail 会解析这个 JSON 并传递给下一个 skill。第三,错误处理。如果执行体抛异常,ponytail 会捕获并记录,但你需要确保异常信息足够清晰,方便排查。
4.2 输入输出契约的设计原则
写 skill 最容易犯的错误是输入输出契约设计得太随意。我见过有人把 skill 的输入定义成一个“大对象”,里面塞了十几个字段,其中一半是可选的。这种设计看起来灵活,实际上很难维护——调用方不知道哪些字段必须传,skill 内部也不知道哪些字段一定有值。
我的经验是:一个 skill 的输入参数不要超过 5 个,必填参数不要超过 3 个。如果超过这个数,说明这个 skill 的职责太宽了,应该拆成多个 skill。输出也一样,尽量保持输出结构简单、扁平,不要嵌套太深。
另一个原则是:输入输出都用强类型。不要用“any”或者“object”这种模糊类型。字符串就是字符串,数组就是数组,布尔就是布尔。强类型的好处是 ponytail 可以在执行前做参数校验,参数不对直接报错,不用等到执行体跑到一半才发现。
4.3 实操:从零写一个可用的 skill
我拿一个实际场景来演示:批量重命名文件。需求是:给定一个目录,把目录下所有 .txt 文件按“日期_序号.txt”的格式重命名。
第一步,定义 skill 描述:
# skills/batch-rename/skill.yaml name: batch-rename version: 1.0.0 description: 批量重命名目录下的文本文件 inputs: - name: dir_path type: string required: true description: 目标目录路径 - name: prefix type: string required: false default: "file" description: 重命名前缀 outputs: - name: renamed_count type: number description: 成功重命名的文件数量 - name: renamed_files type: array description: 重命名后的文件列表 runtime: python entry: rename.py第二步,写执行体:
# skills/batch-rename/rename.py import os import json import sys from datetime import datetime def main(): params = json.loads(sys.stdin.read()) dir_path = params["dir_path"] prefix = params.get("prefix", "file") if not os.path.isdir(dir_path): print(json.dumps({"error": f"目录不存在: {dir_path}"})) sys.exit(1) txt_files = sorted([f for f in os.listdir(dir_path) if f.endswith(".txt")]) today = datetime.now().strftime("%Y%m%d") renamed = [] for idx, filename in enumerate(txt_files, start=1): old_path = os.path.join(dir_path, filename) new_name = f"{prefix}_{today}_{idx:03d}.txt" new_path = os.path.join(dir_path, new_name) os.rename(old_path, new_path) renamed.append(new_name) print(json.dumps({ "renamed_count": len(renamed), "renamed_files": renamed })) if __name__ == "__main__": main()第三步,测试 skill。ponytail 通常提供一个测试命令,可以直接调用 skill 并传入参数:
# 通过 ponytail 命令行测试 skill ponytail run batch-rename --input '{"dir_path": "./test_data", "prefix": "report"}'如果输出是预期的 JSON 结果,说明 skill 写对了。如果报错,根据错误信息调整。
注意:写 skill 的时候,一定要处理边界情况。比如目录不存在、目录为空、文件没有写权限。这些情况在实际运行中很常见,如果不处理,skill 会直接崩溃,整个流程就断了。
4.4 skill 的版本管理与复用
skill 写多了之后,版本管理就成了问题。我的做法是:每个 skill 单独一个目录,目录名就是 skill 名,目录下放 skill.yaml 和执行体文件。所有 skill 目录统一放在 skill_root 下。版本号写在 skill.yaml 的 version 字段里,每次修改 skill 就递增版本号。
复用方面,如果多个流程用到同一个 skill,直接引用同一个 skill 目录就行,不需要复制。ponytail 加载 skill 的时候是按名称加载的,只要 skill_root 下有这个 skill,任何流程都可以调用。
如果要把 skill 分享给别人,把整个 skill 目录打包发过去就行。对方解压到自己的 skill_root 下,ponytail 重新加载后就能用。但要注意依赖问题——如果你的 skill 依赖了某个第三方库,得在 skill.yaml 里声明依赖,或者在 README 里写清楚。
5. ponytail 流程编排与执行
5.1 流程定义文件的写法
单个 skill 只能完成一个步骤,真正的价值在于把多个 skill 串成流程。ponytail 的流程定义文件通常是一个 YAML 文件,描述 skill 之间的调用顺序和数据传递关系。
# flows/text-pipeline.yaml name: text-pipeline version: 1.0.0 description: 文本文件批量处理流程 steps: - id: scan skill: file-scan inputs: dir_path: "{{ input.dir_path }}" extension: ".txt" - id: extract skill: text-extract inputs: file_path: "{{ scan.files }}" pattern: "{{ input.pattern }}" foreach: "{{ scan.files }}" - id: rename skill: batch-rename inputs: dir_path: "{{ input.dir_path }}" prefix: "{{ input.prefix }}" depends_on: - extract这个流程定义了三步:先扫描目录下的 .txt 文件,然后对每个文件执行字段提取,最后批量重命名。几个关键语法:
{{ input.xxx }}:引用流程的输入参数。{{ step_id.output_field }}:引用某个步骤的输出。foreach:对数组中的每个元素执行一次 skill。depends_on:声明步骤之间的依赖关系,ponytail 会根据依赖关系决定执行顺序。
5.2 数据在 skill 之间的传递
数据传递是流程编排里最容易出问题的地方。ponytail 的数据传递遵循“输出即输入”的原则:前一个 skill 的输出 JSON 会作为后一个 skill 的输入来源。但实际使用中,往往需要做字段映射和格式转换。
比如上面的例子中,scan skill 输出的是一个文件列表,extract skill 需要的是单个文件路径。这里用 foreach 来处理:ponytail 会自动遍历文件列表,每次把一个文件路径传给 extract skill。
如果字段名不匹配,可以在 inputs 里做映射:
inputs: file_path: "{{ scan.files }}" # scan 输出的 files 字段映射到 extract 的 file_path 输入如果需要对数据进行转换,ponytail 通常支持简单的表达式,比如字符串拼接、数组过滤等。但复杂的转换逻辑建议单独写一个 skill 来做,不要在流程定义里写太复杂的表达式,不然可读性会很差。
5.3 执行流程与查看结果
流程定义好之后,通过 ponytail 命令执行:
ponytail flow run text-pipeline --input '{"dir_path": "./data", "pattern": "\\d{4}-\\d{2}-\\d{2}", "prefix": "processed"}'执行过程中,ponytail 会输出每个步骤的执行状态。如果某个步骤失败,流程会暂停并输出错误信息。你可以根据错误信息定位问题,修复后重新执行。ponytail 通常支持断点续跑,已经成功的步骤不会重复执行。
执行完成后,可以查看流程的完整执行报告:
ponytail flow report text-pipeline --last报告里会包含每个步骤的耗时、输入输出摘要、是否有警告或错误。这个报告对排查问题很有帮助,尤其是当流程步骤比较多的时候。
5.4 并发执行与性能优化
ponytail 支持 skill 的并发执行。如果流程中有多个没有依赖关系的步骤,ponytail 会自动并行执行它们。比如上面例子中,如果 extract 和 rename 没有依赖关系,它们可以同时跑。
但并发不是越多越好。我实测下来,并发数设为 CPU 核心数的 1.5 倍左右比较合适。设太高会导致上下文切换开销增大,反而变慢。如果 skill 涉及大量磁盘 IO,并发数可以适当调高,因为 IO 等待时间可以重叠。
另外,foreach 循环里的 skill 执行也可以并发。如果你的 skill 是纯计算型的,并发执行能显著缩短总耗时。但如果是 IO 密集型的,并发太高可能会导致磁盘瓶颈。
6. 常见问题与排查技巧实录
6.1 skill 加载失败怎么办
这是最常见的问题。表现是:ponytail 启动后,执行流程时报“skill not found”。排查步骤:
- 确认 skill 目录在 skill_root 下,且目录名和 skill.yaml 里的 name 字段一致。
- 确认 skill.yaml 格式正确,没有 YAML 语法错误。可以用在线 YAML 校验工具检查。
- 确认执行体文件存在,且路径和 skill.yaml 里的 entry 字段一致。
- 查看 ponytail 日志,通常会有具体的加载失败原因。
我遇到过一次是因为 skill.yaml 里用了 Tab 缩进,YAML 不支持 Tab,只支持空格。改成空格就好了。这种问题很隐蔽,因为文件看起来没问题,但解析就是失败。
6.2 参数传递错误的排查
参数传递错误的表现是:skill 执行时报“missing required parameter”或者参数值不对。排查思路:
- 检查流程定义里的 inputs 映射是否正确。
- 检查前一个 skill 的输出字段名是否和当前 skill 的输入字段名匹配。
- 在 skill 执行体里打印接收到的参数,确认参数确实传进来了。
我习惯在 skill 执行体的开头加一行日志,把接收到的参数打印出来。这样一旦参数有问题,一眼就能看出来。
6.3 超时与资源限制问题
如果 skill 执行时间超过 timeout 设置,ponytail 会强制终止 skill 并报超时错误。解决办法:
- 调大 timeout 值。
- 优化 skill 执行效率,比如减少不必要的 IO 操作、使用更高效的算法。
- 如果 skill 确实需要很长时间,考虑拆成多个 skill 分步执行。
资源限制方面,如果宿主环境对内存或 CPU 有限制,skill 执行时可能会被系统杀掉。这种情况需要查看系统日志,确认是否是资源不足导致的。
6.4 常见问题速查表
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| skill not found | skill 目录不在 skill_root 下 | 移动 skill 目录到正确位置 |
| YAML 解析错误 | 使用了 Tab 缩进 | 全部改成空格缩进 |
| 参数缺失 | inputs 映射字段名不匹配 | 检查字段名,确保一致 |
| 超时 | skill 执行时间过长 | 调大 timeout 或优化 skill |
| 输出格式错误 | 执行体没有输出合法 JSON | 检查执行体的输出逻辑 |
| 并发冲突 | 多个 skill 同时写同一文件 | 加锁或改为串行执行 |
| 版本不兼容 | skill 和插件版本不匹配 | 升级插件或降级 skill |
6.5 几个实用的避坑技巧
技巧一:skill 要幂等。同一个 skill 执行多次,结果应该是一样的。这样即使流程中断后重跑,也不会产生副作用。比如重命名 skill,如果文件已经重命名过了,再次执行应该跳过而不是报错。
技巧二:日志要详细。skill 执行体里多打日志,尤其是关键步骤的前后。日志级别用 debug,稳定后再调高。日志是排查问题的第一手资料。
技巧三:先单测再串联。每个 skill 写完后,先用 ponytail 的单 skill 测试命令跑一遍,确认输入输出都正确,再放到流程里串联。这样出问题时容易定位是哪个 skill 的问题。
技巧四:流程定义要版本化。流程定义文件也放进 Git 管理,每次修改都提交。这样出问题可以回滚到上一个可用版本。
技巧五:不要在一个 skill 里做太多事。一个 skill 只做一件事,做精做透。如果发现一个 skill 的代码超过 200 行,大概率应该拆分了。
7. ponytail 的扩展玩法与进阶思路
7.1 把 skill 做成可配置的模板
skill 写多了之后,会发现很多 skill 的结构是类似的,只是参数不同。这时候可以把 skill 做成模板,通过参数来定制行为。比如“文件处理”类的 skill,可以抽象出一个通用模板,支持不同的文件格式、不同的处理规则。
ponytail 本身支持 skill 继承和参数覆盖。你可以在 skill.yaml 里定义一个基础 skill,然后在另一个 skill 里继承它并覆盖部分参数。这个机制可以减少重复代码,提高 skill 的复用率。
7.2 和外部工具链的集成
ponytail 的 skill 执行体可以是任意可执行程序,这意味着它可以和几乎任何外部工具集成。比如:
- 调用 ffmpeg 处理视频文件。
- 调用 pandoc 转换文档格式。
- 调用数据库客户端执行 SQL。
- 调用 HTTP 接口获取数据。
集成的关键是:把外部工具的调用封装成 skill,处理好输入输出格式转换。外部工具的输出通常是文本或文件,skill 需要把它转换成 JSON 格式,才能被 ponytail 流程消费。
7.3 定时触发与事件驱动
ponytail 流程可以通过定时任务触发,也可以通过事件触发。定时触发比较简单,用系统的 cron 或者任务计划程序定时执行 ponytail 命令就行。事件触发需要宿主环境支持事件机制,比如文件变化事件、消息队列事件等。
我目前用的是一个混合模式:定时扫描目录,发现有新文件就触发处理流程。这样既不需要实时监听,又能保证文件在合理时间内被处理。
7.4 监控与告警
流程跑在生产环境里,监控和告警是必须的。ponytail 的执行报告可以输出成 JSON 格式,方便接入监控系统。我通常会把每次流程执行的结果写入一个日志文件,然后用一个简单的脚本分析日志,发现异常就发通知。
监控的关键指标:流程成功率、平均执行时间、失败步骤分布。如果成功率下降或者执行时间突然变长,说明有问题需要排查。
8. 我个人的实操体会
ponytail 这套东西,刚上手的时候会觉得有点绕——为什么要定义 skill、为什么要写流程文件、为什么不直接写个脚本。但用了一段时间之后,我越来越觉得这个设计是有道理的。它强迫你把流程拆解清楚,把每个步骤的输入输出定义明白。这个过程本身就是一种梳理,很多隐藏的问题在定义阶段就会暴露出来。
我最大的体会是:skill 的粒度控制是关键。粒度太粗,复用性差;粒度太细,流程文件会变得很复杂。我的经验是,一个 skill 对应一个“不可再分的业务动作”。比如“读取文件”是一个动作,“解析 JSON”是一个动作,“提取字段”是一个动作。这三个动作可以合成一个 skill,也可以拆成三个。怎么选?看复用频率。如果“读取文件”这个动作在多个流程里都会用到,就单独拆出来;如果只在特定流程里用,就合在一起。
另一个体会是:不要追求一步到位。我一开始想把整个流程设计得很完美,结果花了很多时间在调整 skill 的接口定义上。后来我换了个思路:先跑通,再优化。先用最粗糙的方式把流程串起来,能跑就行。跑通之后,再根据实际使用中暴露的问题逐步优化 skill 的接口和实现。这样迭代速度更快,也不会因为过度设计而浪费时间。
最后分享一个小技巧:给 skill 写文档。每个 skill 目录下放一个 README.md,写清楚这个 skill 做什么、输入输出是什么、有什么注意事项。过一段时间回头看,没有文档的 skill 你根本想不起来它是干嘛的。文档不用很长,几句话就行,但一定要写。