1. 为什么 Codex Skills 需要 click 与 rich 这套组合
如果你正在做 Codex Skills 开发,大概率会遇到一个很具体的问题:技能写完了,但调用体验很粗糙。参数靠位置传,报错靠 traceback,输出是一坨没有结构的字符串。Agent 拿到这种输出,要么解析失败,要么得写一堆正则去猜。我自己在接第一个查询类 Skill 的时候,就因为输出格式不稳定,来回改了三四版解析逻辑。
click 解决的是“契约”问题。它把命令的参数、类型、默认值、可选范围全部声明清楚,Agent 在调用前就能从 help 文本里读到结构化信息。rich 解决的是“呈现”问题。表格、面板、高亮、进度条,这些在终端里看起来是美化,但对 Agent 来说更重要的是输出边界清晰、字段对齐、状态可读。
这篇文章面向的是已经在写 Codex Skills、或者准备把本地脚本封装成 Agent 可调用工具的开发者。我会用一个数据查询 Skill 作为主线,从 click 的参数定义讲到 rich 的表格渲染,再把它封装成 Skill 函数,最后用 TaoToken 的统一 Key 通道把模型调用接进来,给出可复制的 config.toml 和 settings.json 骨架,以及逐步验证动作。整套流程跑通之后,你手里会有一个既能被人用、也能被 Agent 调的 CLI 技能。
2. TaoToken 前置:统一 Key 与 API 通道准备
在把 Skill 接到模型之前,需要先有一个稳定的调用通道。TaoToken 在这里的角色是统一 Key 和 API 入口,让你不用在多个模型供应商之间来回切换配置。官网地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 入口是 https://taotoken.net/api 。
你需要先拿到一个 API Key。进入控制台创建 Key 的页面在 https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite ,创建之后复制保存,后面配置里会用到。如果你还没决定用哪个模型,可以先去模型对话页面试一下 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite ,确认通道可用再写进配置。
对于长期做编码和 Agent 的场景,Coding Plan 会更合适,入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,配置字段有疑问的时候对照文档查。
这里要强调一点:TaoToken 是统一的 API 通道,不是让你绕过任何合规流程的工具。你拿到的 Key 就是正常调用凭证,配置方式跟标准 API 客户端一致。
3. 可复制配置:config.toml 与 settings.json 骨架
Codex Skills 的配置通常分两层:一层是模型通道配置,一层是 Skill 运行时的设置。下面给出两个可直接复制的骨架。
3.1 config.toml 模型通道配置
# config.toml # Codex Skills 模型通道配置骨架 [provider] name = "taotoken" base_url = "https://taotoken.net/api" api_key_env = "TAOTOKEN_API_KEY" # 从环境变量读取,不要硬编码 [model] default = "claude-sonnet-4-20250514" fallback = "gpt-4o-mini" max_tokens = 4096 temperature = 0.2 [skill] name = "data_search" entry = "skill.py" timeout_seconds = 30 retry = 2 [output] format = "json" # Agent 模式用 json,终端模式用 table pretty = true关键点:api_key_env指向环境变量,不要把 Key 写进文件。base_url用 API 入口,不带任何多余路径。temperature在 Skill 场景建议调低,保证输出稳定。
3.2 settings.json Skill 运行时设置
{ "skill_name": "data_search", "version": "1.0.0", "runtime": { "python": "3.11", "dependencies": ["click>=8.1", "rich>=13.0", "httpx>=0.27"] }, "parameters": { "query": { "type": "string", "required": true }, "limit": { "type": "integer", "default": 10, "max": 1000 }, "format": { "type": "string", "enum": ["json", "csv", "table"], "default": "table" }, "sort_by": { "type": "string", "enum": ["id", "name", "score"], "default": "id" } }, "channel": { "provider": "taotoken", "base_url": "https://taotoken.net/api", "api_key_env": "TAOTOKEN_API_KEY" } }这两个文件的分工是:config.toml 管通道和模型,settings.json 管 Skill 自身的参数契约。Agent 读取 settings.json 就能知道这个 Skill 接受什么参数、返回什么格式。
3.3 环境变量设置
# Linux / macOS export TAOTOKEN_API_KEY="你的Key" # Windows PowerShell $env:TAOTOKEN_API_KEY="你的Key"设置完之后用echo $TAOTOKEN_API_KEY确认能读到。这一步没做对,后面所有调用都会 401。
4. click 参数契约与 rich 输出实现
4.1 click 定义命令契约
click 的核心是装饰器风格。下面这段定义了一个带子命令的 CLI,参数类型和可选范围都声明清楚。
import click OUTPUT_FORMATS = ["json", "csv", "table"] SORT_FIELDS = ["id", "name", "score"] @click.group() def cli(): """Codex Skill CLI: 数据查询技能入口。""" pass @cli.command() @click.argument("query", type=click.STRING, required=True) @click.option("--limit", "-n", type=click.INT, default=10, help="返回结果最大数量。") @click.option("--format", "-f", type=click.Choice(OUTPUT_FORMATS), default="table", help="输出格式。") @click.option("--sort-by", "-s", type=click.Choice(SORT_FIELDS), default="id", help="排序字段。") @click.option("--skill-mode", is_flag=True, default=False, help="Skill 模式,仅输出结构化数据。") def search(query, limit, format, sort_by, skill_mode): """执行数据搜索。""" result = data_search_skill(query, limit, format, sort_by) render_output(result, format, skill_mode)click.Choice这一层很关键。Agent 传了非法值,click 会在进入业务逻辑之前就拦截并返回明确错误,不需要你在函数里写 if-else 校验。
4.2 rich 渲染表格与面板
rich 的 Console 会自动检测终端能力。表格用Table,提示用Panel,状态字段用Text上色。
from rich.console import Console from rich.table import Table from rich.panel import Panel from rich.text import Text console = Console() def render_table(data): table = Table(title="搜索结果", show_header=True, header_style="bold magenta") table.add_column("ID", style="dim", width=5) table.add_column("Name", style="cyan") table.add_column("Score", justify="right", style="green") table.add_column("Status", justify="center") for row in data: color = "green" if row["status"] == "Active" else "yellow" if row["status"] == "Pending" else "red" table.add_row(str(row["id"]), row["name"], f"{row['score']:.1f}", Text(row["status"], style=color)) console.print(table) console.print(Panel(f"共 {len(data)} 条结果", style="bold blue"))Text嵌入单元格做条件着色,比整行着色更精确。Panel用来做结果汇总,Agent 解析时也能通过面板文本快速拿到总数。
4.3 Skill 函数与渲染分离
Skill 函数只负责返回结构化数据,渲染交给 CLI 层。这样同一套逻辑既能给人用,也能给 Agent 用。
def data_search_skill(query, limit=10, format="table", sort_by="id"): results = get_mock_data() if query: results = [r for r in results if query.lower() in r["name"].lower()] results.sort(key=lambda x: x.get(sort_by, 0), reverse=True) results = results[:limit] if format == "json": import json return json.dumps(results, indent=2, ensure_ascii=False) elif format == "csv": import io, csv buf = io.StringIO() writer = csv.DictWriter(buf, fieldnames=results[0].keys()) writer.writeheader() writer.writerows(results) return buf.getvalue() else: return {"type": "table", "data": results}注意 table 格式返回的是字典,由 CLI 层决定怎么渲染。Agent 模式下走 json 分支,直接拿到字符串。
5. 验证请求与成功结果
配置和代码都就位之后,按下面步骤逐步验证。
5.1 验证通道连通
先用一个最小请求确认 Key 和 base_url 可用。
curl -s https://taotoken.net/api/v1/models \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ | head -c 500返回模型列表说明通道正常。如果返回 401,检查环境变量是否生效;返回 404,检查 base_url 是否写成了带多余路径的地址。
5.2 验证 CLI 参数解析
python skill.py search "Alice" --limit 3 --format table预期看到一张带颜色的表格,Status 列 Active 为绿色,Pending 为黄色。如果参数传了非法 format,click 会直接报错并列出可选值。
5.3 验证 Skill 模式输出
python skill.py search "Alice" --format json --skill-mode预期输出纯 JSON,没有 rich 的 ANSI 转义码。Agent 拿到这个字符串可以直接json.loads。
5.4 验证模型调用链路
在 Skill 里加一段调用模型的逻辑,用 TaoToken 通道。
import httpx, os def call_model(prompt): resp = httpx.post( "https://taotoken.net/api/v1/chat/completions", headers={"Authorization": f"Bearer {os.environ['TAOTOKEN_API_KEY']}"}, json={ "model": "claude-sonnet-4-20250514", "messages": [{"role": "user", "content": prompt}], "max_tokens": 512 }, timeout=30 ) resp.raise_for_status() return resp.json()["choices"][0]["message"]["content"]跑一次call_model("用一句话说明 click 的作用"),能拿到正常回复就说明整条链路通了。
6. 本篇常见错排查
报错一:click.exceptions.BadParameter。这是 Choice 类型拦截了非法输入。检查 Agent 传参是否在 enum 范围内。如果 Agent 经常传错,把 enum 写进 settings.json 的 parameters 里,让 Agent 在调用前就能读到约束。
报错二:rich 输出带 ANSI 转义码导致 JSON 解析失败。原因是 Skill 模式下走了 table 分支。检查--skill-mode是否生效,以及 format 是否强制为 json。最稳妥的做法是在 Skill 函数里判断 skill_mode,直接返回纯字符串。
报错三:401 Unauthorized。Key 没读到或者写错了。先echo $TAOTOKEN_API_KEY确认,再检查 config.toml 里的api_key_env名称是否和实际环境变量一致。注意不要有多余空格。
报错四:httpx.ConnectTimeout。通道地址写错或者网络不通。确认 base_url 是https://taotoken.net/api,不要带/v1之外的路径。超时时间在 config.toml 的timeout_seconds里调大。
报错五:表格列宽错乱。rich 的 Table 在窄终端下会自动换行。如果 Agent 解析表格文本,建议改用 json 格式,不要解析渲染后的表格字符串。
报错六:ModuleNotFoundError: No module named 'click'。依赖没装。按 settings.json 里的 dependencies 执行pip install click rich httpx。
排障过程中如果需要确认 Key 状态,去 https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 看 Key 是否有效。接入细节对照 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。
7. 把 Skill 接到 Codex 工作流
Skill 跑通之后,下一步是让它进入日常编码流程。如果你主要用 Claude Code 做开发,可以参考 https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claude_code&utm_campaign=rewrite 里的接入方式,把 Skill 作为工具挂进去。
长期做编码和 Agent 的话,Coding Plan 的额度模型更适合持续调用,入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。模型选择上如果拿不准,先去 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 对比几个模型的输出风格,再写进 config.toml 的 default 字段。
一个实用技巧:把 settings.json 里的 parameters 直接喂给 Agent 作为工具描述,Agent 就能在调用前知道每个参数的类型和范围,减少无效调用。这比在 prompt 里用自然语言描述参数要可靠得多。