最近在折腾 Agent 工具链的时候,我一直觉得 Claude Code 和 Codex CLI 这类终端 Agent 的能力上限其实不在模型本身,而在它们能“摸到”多少外部工具。模型再聪明,如果只能靠内置的几板斧干活,遇到一些特定场景照样抓瞎。我自己的做法是:把常用的外部工具封装成 Skill,让 Agent 在合适的时机自动调用,效率直接翻倍。这篇就把我在实际项目中把 Google Antigravity 这类外部工具封装进 Claude Code / Codex 的完整思路和踩坑记录整理出来,给想折腾同样事情的开发者一个可以直接抄作业的参考。
先说清楚这套方案能解决什么问题。我日常有大量需要调用外部工具的场景,但直接让 Agent 用自然语言去“描述”调用过程,一是容易让模型自由发挥导致参数传错,二是 Agent 内置的权限和工具列表根本覆盖不到这些第三方能力。把工具封装成 Skill,相当于给 Agent 配了一份结构化说明书加一个可执行的脚本入口,它读到合适的上下文就会自动调起这个技能,整个调用过程变成可预期、可复现的操作。适合谁看?如果你正在用 Claude Code 或 Codex 做自动化任务,嫌它们内置工具不够用,又不想上重型 MCP 服务器,这套外部调用 Skill 的封装路线很值得花十分钟了解一下。
1. 把外部工具封装成 Skill 的核心逻辑:为什么这样做能突破边界
1.1 Agent 内置工具能力的短板在哪里
我先说一个比较反直觉的观察:很多人在用 Claude Code 的时候,默认它是“什么都能干”的,但实际上终端 Agent 的内置工具集非常克制,大致就分几类:文件读写、执行命令、搜索替换、跑测试,顶多再来个网页抓取。你让它“调用一下 Antigravity 的能力”,它要么不知道这个词是什么,要么就按照自己的理解胡编一个命令出来,反正最后大概率是跑不通的。
这并不是模型笨,而是模型天生不具备“调用你没教过它的工具”的能力。你想让 Agent 稳定地使用某个外部工具,本质上要干两件事:第一,让模型知道在什么场景下该用这个工具;第二,让它知道怎么用、参数怎么传、结果怎么解析。Skill 机制就是干这个的——它把工具的用法写成了模型能读懂的自然语言说明书,再配上真正可以执行的脚本,Agent 读到技能描述后就能按图索骥。
1.2 Skill 与 MCP、插件机制的本质区别
可能有人会问:已有 MCP(Model Context Protocol)了,为什么还要搞 Skill?我的看法是两者定位完全不同。MCP 更像一个实时连接的外部服务总线,它把工具注册到模型上下文里,适合高频、多工具、需要动态发现的场景;但 MCP 的搭建成本不低,每个工具都要写一个 server,还要处理鉴权、传输协议这些底层细节。
Skill 则轻得多。它本质上就是一个目录加一个 Markdown 文件加若干可执行脚本,Agent 按需去读描述、执行命令、拿到结果。不需要常驻服务、不需要网络协议,本地文件系统就能跑。对于“某个项目里的专用小工具”“公司内部的命令行能力”“某个特定的外部 API 封装”这类低频但稳定的场景,Skill 的性价比远高于 MCP。我自己现在的基本判断是:工具多且杂,上 MCP;工具少而专,封装 Skill,简单直接。
2. Skill 的目录结构与 SKILL.md 规范:动手前先吃透这套约定
2.1 标准目录结构长什么样
不管是 Claude Code 还是 Codex,对 Skill 目录的约定其实大同小异。核心就是一个文件夹加一个主描述文件,外部脚本放在同目录或子目录里。我建议按下面这种结构组织:
skills/ └── antigravity-tool/ ├── SKILL.md └── scripts/ ├── run.py └── config.json目录名必须是小写字母加连字符,不要出现空格、大写和特殊符号。这一点很关键,因为 Agent 在解析技能名称时会用它做标识符,命名不规范轻则匹配不到,重则整个技能被忽略。SKILL.md是灵魂文件,Agent 能不能正确理解技能用途、什么时候用、怎么用,全看这个文件写得清不清楚。脚本文件放在子目录里,保持整洁。
2.2 SKILL.md 里的 Frontmatter 字段怎么填
SKILL.md的头部是 YAML 格式的 Frontmatter,这部分是 Agent 用来做技能匹配和触发决策的关键信息。我自己常用的字段就三个,简单有效:
--- name: antigravity-tool description: 仅在用户明确要求执行 Antigravity 相关操作时使用。将外部指令转换为结构化任务并提交执行。 ---name必须和目录名一致。description里最容易犯的错是写得太泛,比如“一个有用的工具”,这种描述在 Agent 眼里等于没有。有效的写法应该包含触发条件和能力边界:什么时候用、能干什么、什么时候不要用。我一般在 description 里面直接写“仅当……时使用”,这能明显减少模型瞎调用的概率。
2.3 正文部分要写给“阅读理解能力强但没常识”的读者
把 SKILL.md 的正文想象成写给一个非常聪明但对你这个工具一无所知的实习生看。它不关心你的工具有多厉害,只关心三件事:怎么调用、参数是什么、输出长什么样。
正文我通常分四块写。第一块是 Overview,两句话讲清工具做什么;第二块是 Usage,明确命令行调用方式,最好带一两个真实示例;第三块是 Parameters,把关键参数的意义说清楚,有枚举值的一定要列出来;第四块是 Output Format,告诉 Agent 期望的输出长什么样,尤其是错误输出。这四块写扎实了,模型在执行的时候基本不会跑偏。
3. 从零封装一个外部工具 Skill:完整示例与参数约定
3.1 设计一个典型的外部工具调用脚本
光讲规范有点虚,我直接用一个具体例子来演示。假设我们有这样一个场景:需要让 Agent 能够调用某个云端服务的 API 来执行任务提交和状态查询。我们把它封装成 Skill,脚本先按 JSON 格式接收参数。
#!/usr/bin/env python3 """Antigravity-like 外部工具封装脚本(示例)""" import json import os import sys import urllib.request def load_config(): cfg_path = os.path.join(os.path.dirname(__file__), "config.json") with open(cfg_path, "r", encoding="utf-8") as f: return json.load(f) def submit_task(prompt: str, params: dict | None = None): cfg = load_config() payload = json.dumps({ "prompt": prompt, "params": params or {}, "source": "agent-skill", }).encode("utf-8") req = urllib.request.Request( cfg["api_endpoint"], data=payload, headers={"Content-Type": "application/json", "Authorization": f'Bearer {cfg["api_key"]}'}, method="POST", ) with urllib.request.urlopen(req, timeout=30) as resp: return json.loads(resp.read().decode("utf-8")) def main(): if len(sys.argv) < 2: print("Usage: run.py <json-args>", file=sys.stderr) sys.exit(1) args = json.loads(sys.argv[1]) action = args.get("action", "submit") if action == "submit": result = submit_task(args["prompt"], args.get("params")) else: result = {"error": f"unknown action: {action}"} print(json.dumps(result)) if __name__ == "__main__": main()这个脚本本身不复杂,但有几个设计点是刻意的:参数通过单个 JSON 字符串传入,避免 shell 转义噩梦;配置信息放独立的config.json,不让密钥写死在代码里;输出统一走 stdout 且是 JSON 格式,方便 Agent 解析。密钥放配置文件的思路,实际上绕过了把敏感信息暴露在命令行历史里的问题。
3.2 让 SKILL.md 覆盖“模型最容易犯错的三个地方”
脚本写完了,SKILL.md 才是一半工夫。我直接给一份我用下来觉得比较稳妥的模板:
--- name: antigravity-tool description: 仅当用户要求提交任务到 Antigravity 或查询任务状态时使用。用于外部任务处理、批量执行等场景。日常对话禁止使用。 --- # Antigravity 外部工具 ## Overview 通过命令行封装向 Antigravity 服务提交任务,并获取执行结果。 ## Usage python3 scripts/run.py '{"action": "submit", "prompt": "...", "params": {...}}' ## Parameters - action: 必填,'submit' 表示提交新任务 - prompt: 必填,任务内容描述 - params: 可选,额外参数对象 ## Output Format 成功时输出 JSON,包含 task_id 和 status 字段。 失败时输出 JSON,包含 error 字段。模型最容易错的地方有三个:一是把参数拼成平铺的命令行参数而不是 JSON 对象,二是在非相关对话里也自作主张调用技能,三是拿到错误输出后不能正确解读。我的对策分别是对应上述模板里的 Usage 示例、description 里的严格触发条件、Output Format 里的错误格式说明。第一次让模型跑通之后,后面就会稳定很多。
3.3 触发词与描述的关系:不是靠“魔法词”
有一种谣言说 Skill 靠触发词生效,只要在对话里提到某个词就会自动触发。实际机制没那么玄乎——Agent 是把description的内容嵌入上下文,然后由模型基于用户输入判断是否调用。触发词只是描述的附属品,不是开关。所以与其去凑一堆触发词,不如把 description 写精准。让模型自己判断“这个用户请求是不是匹配这个技能”,比任何硬编码的词表都可靠。
4. Codex CLI 与 Claude Code 在 Skill 加载上的差异与适配
4.1 两边加载机制完全不同
我自己同时在用 Codex CLI 和 Claude Code,顺手对比了一下两个平台对 Skill 的加载方式,差异比想象中大。Claude Code 是启动时扫描固定的skills目录,把每个项目的SKILL.md的描述信息注入系统提示,模型在对话过程中随时能感知到这些技能的存在。只要目录放对位置、描述写得合格,几乎零配置就能跑起来。
Codex 的思路稍有不同,它用的是 agents 目录定制化配置的方案,Skill 文件更像“追加到系统提示里的补充说明”。这意味着 Codex 侧对 Skill 的加载不像 Claude Code 那么自动,往往需要把说明文件放到指定目录,再配合配置文件显式引用。我第一次在 Codex 里折腾时,把文件放在项目根目录下没生效,后来放到全局配置的 agents 目录下才可以。
4.2 全局技能与项目级技能如何取舍
这个取舍直接关系到你的使用体验。全局技能的好处是任何项目下都能用,不用重复配置;坏处是 All Skills 都会挤占上下文窗口,描述写得太长或技能数量太多,反而干扰模型判断。项目级技能则精准得多——只有在这个项目里才会看到,描述可以写得针对性强,上下文开销几乎为零。
我现在是混合策略:高频通用能力(代码格式化、依赖检查这类)放全局;跟特定业务绑定的外部工具封装,放对应项目的.claude/skills或 Codex 的 agents 配置目录下。这样既保障了通用性,又不会让每个项目的上下文都被一堆无关技能塞满。
4.3 路径引用要防止“换个目录就失效”的坑
用相对路径引用脚本是最稳妥的,但在 Agent 场景下有个坑:Agent 的工作目录不一定是 Skill 目录所在位置。比如它在项目根目录执行命令,把脚本路径写成scripts/run.py,就可能找不到文件。我的解决办法是脚本内不管当前目录在哪,先定位自身所在目录再拼路径,像示例代码里os.path.dirname(__file__)那行,这样不管从哪个 CWD 发起调用都能稳定执行。这个细节我第一次没注意,结果 Agent 换个运行目录就报 file not found,排查了半天才意识到是路径解析的问题。
5. 调试、踩坑与实践技巧:让 Agent 真正稳定调用你的 Skill
5.1 先当普通 CLI 跑通,再交给 Agent
我在封装 Skill 过程中最大的体会是:一定要先把脚本当成普通命令行工具完整测一遍,再写 SKILL.md 交给 Agent 使用。如果脚本本身对参数校验不严、输出格式不统一,Agent 拿到手后只能靠猜,那出问题就是必然的。我的测试顺序是:用固定参数跑几次确认输出稳定;故意传错参数看报错信息是否能被结构化解读;再用不同输入验证没有隐藏状态残留。这轮测试做完,才轮到 Agent 上场。
5.2 stdout 和 stderr 的区分是 Agent 稳定性的分水岭
模型解析 Agent 工具调用结果时,主要看 stdout。如果你的脚本把日志和结果混着往 stdout 输出,Agent 就会在解析时卡壳——它分不清哪一行是真正的返回数据。我现在的强制规范是:程序日志和错误提示一律走 stderr,只有最终的结构化结果走 stdout。凡是结果输出,严格保持 JSON 格式,不附带任何解释性文字。这个习惯帮我在后续使用中省掉了大量返工。
5.3 超时处理与错误信息的多语言问题
外部工具调用网络接口时,超时是一个高频问题。我的建议是脚本内部必须设置请求超时,同时捕获超时异常并输出成 JSON 错误信息,而不是让脚本直接崩掉吐一堆 traceback。长 traceback 对 Agent 来说可读性很差,等于没给任何有效提示。
错误信息本身也值得注意:能给英文就别给中文,不是崇洋媚外,而是模型对英文错误信息的语义理解通常更稳定,这也是我在实际对比测试中的明显感受。更重要的是,错误输出里要带合理的行动建议字段,比如"retryable": true,Agent 看到这个就知道可以自动重试一次而不是傻乎乎地直接宣告失败。
5.4 查看调用痕迹,反向优化 Skill 描述
Skill 封装得好不好,不能只看跑通没跑通,还要看 Agent 是不是按预期的方式去调用。Claude Code 的 verbose 日志模式可以看到 Agent 每次调用工具时的完整思考片段和参数。我发现了一个非常实用的循环:每次任务跑完,翻一下日志里 Agent 调用 Skill 前后的“推理文本”,就能看出它有没有误解描述里的某个表述,然后针对性地修改 SKILL.md 的描述,再重复验证。这么迭代两三轮,技能调用的准确率提升非常明显。
6. 多个 Skill 的组合编排:从“能调用”到“会配合”
6.1 用前置技能完成参数准备
单个 Skill 能解决的问题,规模始终有限。真正让工作流智能化的是多个 Skill 的组合调用。举个例子,我封装了一个提交外部任务的能力,但任务内容经常需要先做数据分析和格式整理。那么数据预处理就可以是另一个 Skill,Agent 的判断逻辑是:先调用预处理技能生成规范化数据,再把结果作为参数传给外部提交技能。这个过程在 SKILL.md 里并不需要强行描述“你应该跟其他技能配合”,只要每个技能的职责边界清晰,模型会自己编排顺序。
6.2 技能间避免重复描述
多个技能并存时,一个很容易踩的坑是在每个 SKILL.md 里重复描述基础概念。比如两个技能都定义了一遍“什么是外部任务”,会导致模型在选择使用哪个技能时产生困惑。我的方式是每个技能只描述它自己的职责,基础概念只放其中一个技能的 Overview 里,另一个技能在描述中直接写“如果输入是任务提交场景,优先考虑另一个技能”之类的引路说明。技能间的边界越干净,模型的选择越准确。
6.3 控制技能数量防止上下文爆炸
技能不是越多越好。每个技能的 description 都会占用上下文预算,如果装了十几个技能,每个 description 都很长,Agent 光读完这些描述就要消耗不少 token。我现在控制在三到五个关键技能,个别低频场景的技能宁愿在需要时用自然语言让 Agent 现学普通的命令行操作,也不轻易常驻。上下文干净了,模型对每个技能的注意力和调用准确率都会明显上升。
7. 一步步操作:从零搭建并验证你的第一个外部调用 Skill
7.1 搭建步骤拆解
最后把整个流程串成一个可以照着做的清单。我以 Claude Code 为例,Codex 用户只需要把目录路径替换成自己的 agents 目录即可。
- 建目录:
mkdir -p ~/.claude/skills/my-tool/scripts,目录名想好再建,尽量不要改。 - 把封装好的脚本放进去,确认用
python3 scripts/xxx.py '{}'能正常跑通。 - 写
SKILL.md,Frontmatter 和正文按上面说的四段式结构来。 - 在 Claude Code 里新建对话,不做任何特殊提示,直接用自然语言描述需求。
- 观察是否自动触发;如果没触发,先看日志,再改 description 的表述,反复调整。
7.2 验证通过的标准是什么
一个 Skill 是否真正封装成功,我有一套自己的验收标准,不是“跑通一次”就算完。第一,连续三次在相似场景下都能正确触发,不触发就算失败;第二,参数从传参到输入解析没有任何手动干预;第三,异常场景能自动感知并返回结构化错误,而不是让 Agent 胡编结果。这三点都过关,我才敢把这个 Skill 放进常用列表里。如果你的 Skill 只能在自己手动指定脚本路径时跑通,说明描述和 Agent 的认知还没有对齐,还得继续打磨。
我自己在实际操作中有两个深刻的体会。其一,Skill 封装的本质是降低模型的决策成本——所有的约定、路径、参数、输出结构你都提前定好,模型只需要做选择和翻译,这种稳定感是天然 Prompt 完全给不了的。其二,这些机制比很多人想象中要简单得多,它不神秘也不复杂,核心就是“一个清晰的说明书加一个能跑的脚本”,剩下的全看你愿不愿意花那二十分钟把它写好。另外分享一个小技巧:Skill 目录里的脚本最好每次更新后都顺手跑一次冒烟测试,确认改动没影响 agent 调用路径,否则一个不经意的路径改动,会让后面所有对话里隐藏的调用全部悄悄失效。