1. 一套 .env把 Claude Slides 的出稿流程从聊天窗口搬到命令行Anthropic 把 Claude Chat 和 Claude Cowork 合并成了统一入口未来几周会向 Pro / Max 用户分批推送同一批上线的还有 Claude Docs 和 Claude Slides —— 和 Claude Design 一样这两者都能直接从统一聊天界面点进去。作为天天写脚本的人我看到这条消息的第一反应不是去点一下试试而是季度复盘要出 60 页手点不现实。于是我把这套活儿拆成了命令行流程先到 TaoToken 官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_slides_env_intro 拿一个 Key把请求 Base URL 固定为https://taotoken.net/api再把 Key、Base URL、模型名三样东西全部塞进.env脚本自己负责把 brief 变成一页页 deck 结构最后回到统一界面里粘贴成幻灯片。整套东西跑通之后单页出稿从十几分钟降到一分钟以内批量出稿只要改一行 manifest。这篇文章不聊行业判断只写能跟做的部分.env怎么写、生成命令怎么敲、单页和批量的差别在哪、报错怎么定位。文中所有 Key 都用YOUR_API_KEY占位请勿把我这里的占位符直接提交到仓库。2..env与目录结构Key、Base URL、模型名一次固定2.1 先拿 Key再写配置顺序不能反。先打开 TaoToken 官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_slides_get_key 在控制台创建 API Key复制出来放在手边。接着在项目根目录建三个文件.env、.env.example、.gitignore。.env放真实值.env.example放字段名.gitignore保证.env永远不进版本库。推荐的目录结构长这样slides-pipeline/ ├── .env ├── .env.example ├── .gitignore ├── briefs/ │ ├── 2025q4-review.json │ └── series-llm-basics.json ├── out/ │ ├── singles/ │ └── batch/ ├── manifest.jsonl └── scripts/ ├── gen_slide.py ├── gen_batch.py └── validate_deck.py2.2.env内容# .env —— 真实值不要提交到仓库 ANTHROPIC_BASE_URLhttps://taotoken.net/api ANTHROPIC_API_KEYYOUR_API_KEY # 模型 ID 请以控制台里实际可用的为准填错会直接返回模型不存在 SLIDES_MODELclaude-sonnet-4-5 # 出稿参数 SLIDES_MAX_TOKENS4000 SLIDES_TIMEOUT120 SLIDES_CONCURRENCY3# .env.example —— 只留字段名可以进仓库 ANTHROPIC_BASE_URL ANTHROPIC_API_KEY SLIDES_MODEL SLIDES_MAX_TOKENS SLIDES_TIMEOUT SLIDES_CONCURRENCY# .gitignore .env out/ __pycache__/这里有两个容易踩的点。第一ANTHROPIC_BASE_URL后面不要加/v1客户端会自己拼路径多写一层就变成/v1/v1/messages返回 404。第二.env里的值不要加引号很多加载库会把引号一起读进去导致请求头里出现带引号的 Key直接 401。2.3 冒烟测试先确认链路通再写业务脚本写完.env不要急着写生成逻辑先用一条最小请求确认链路。把请求体写成文件避免 shell 引号地狱cat /tmp/smoke.json EOF { model: claude-sonnet-4-5, max_tokens: 64, messages: [ {role: user, content: 只回复两个字连通} ] } EOF set -a source .env set a curl -sS ${ANTHROPIC_BASE_URL}/v1/messages \ -H x-api-key: ${ANTHROPIC_API_KEY} \ -H anthropic-version: 2023-06-01 \ -H content-type: application/json \ -d /tmp/smoke.json返回体里能拿到content数组和usage字段就说明 Key、Base URL、模型名三者对上了。如果这里就失败后面所有脚本都没必要调先把这一条打通过。3. 单页出稿一条命令从 brief 到 deck 结构统一界面里的 Claude Slides 是给人点的脚本要产出的是它的原料。我的做法是脚本生成一份结构化的 deck JSON标题页、章节页、要点页、数据页、总结页你把它粘进统一界面的 Slides 入口或者在本地渲染成 Markdown 再贴。如果后续 Slides 开放独立接口只需要换掉 endpoint结构层完全不用动。3.1 brief 文件长什么样{ title: 2025 Q4 平台稳定性复盘, audience: 研发管理层, duration_min: 20, sections: [ 季度可用性数据, 三次 P1 事故复盘, 2026 Q1 改进项 ], constraints: [ 每页不超过 5 个要点, 数据页必须给出对比口径, 禁用营销式形容词 ] }把 brief 和 prompt 分开是为了让同一份 brief 能反复重跑改了 prompt 不用改输入。3.2 生成脚本# scripts/gen_slide.py import json import os import sys from pathlib import Path import requests from dotenv import load_dotenv load_dotenv() BASE_URL os.environ[ANTHROPIC_BASE_URL].rstrip(/) API_KEY os.environ[ANTHROPIC_API_KEY] MODEL os.environ[SLIDES_MODEL] MAX_TOKENS int(os.environ.get(SLIDES_MAX_TOKENS, 4000)) TIMEOUT int(os.environ.get(SLIDES_TIMEOUT, 120)) SYSTEM_PROMPT 你是一名演示文稿结构工程师。 只输出一个合法 JSON 对象不要输出解释文字不要包裹 markdown 代码块。 JSON 结构为 { deck_title: string, slides: [ {type: title|section|bullets|data|summary, title: string, bullets: [string], speaker_notes: string} ] } 要求每个 bullets 元素不超过 40 字data 类型的 slides 必须包含 table 字段值为 {headers: [...], rows: [[...]]}。 def build_prompt(brief: dict) - str: lines [ f演示主题{brief[title]}, f目标听众{brief[audience]}, f预计时长{brief[duration_min]} 分钟, 章节顺序 、.join(brief[sections]), 硬性约束 .join(brief.get(constraints, [])), 请按上面给的结构输出完整 deck JSON。, ] return \n.join(lines) def call_model(prompt: str) - str: resp requests.post( f{BASE_URL}/v1/messages, headers{ x-api-key: API_KEY, anthropic-version: 2023-06-01, content-type: application/json, }, json{ model: MODEL, max_tokens: MAX_TOKENS, system: SYSTEM_PROMPT, messages: [{role: user, content: prompt}], }, timeoutTIMEOUT, ) resp.raise_for_status() payload resp.json() return .join( block.get(text, ) for block in payload.get(content, []) if block.get(type) text ) def strip_fence(text: str) - str: text text.strip() if text.startswith(): text text.split(\n, 1)[1] text text.rsplit(, 1)[0] return text.strip() def main() - int: brief_path Path(sys.argv[1]) out_path Path(sys.argv[2]) brief json.loads(brief_path.read_text(encodingutf-8)) raw strip_fence(call_model(build_prompt(brief))) deck json.loads(raw) # 解析失败直接抛错方便定位 out_path.parent.mkdir(parentsTrue, exist_okTrue) out_path.write_text( json.dumps(deck, ensure_asciiFalse, indent2), encodingutf-8 ) print(f[ok] {len(deck[slides])} slides - {out_path}) return 0 if __name__ __main__: raise SystemExit(main())3.3 运行命令pip install requests python-dotenv python scripts/gen_slide.py \ briefs/2025q4-review.json \ out/singles/2025q4-review.deck.json结构校验单独写一个脚本跟生成脚本解耦# scripts/validate_deck.py import json import sys ALLOWED {title, section, bullets, data, summary} deck json.load(open(sys.argv[1], encodingutf-8)) problems [] for i, slide in enumerate(deck.get(slides, [])): if slide.get(type) not in ALLOWED: problems.append(fslide[{i}] 未知类型: {slide.get(type)}) if len(slide.get(bullets, [])) 5: problems.append(fslide[{i}] 要点超过 5 条) if slide.get(type) data and table not in slide: problems.append(fslide[{i}] data 页缺少 table) print(OK if not problems else \n.join(problems)) sys.exit(1 if problems else 0)python scripts/validate_deck.py out/singles/2025q4-review.deck.json单页跑通之后你会发现真正的价值不是省了那几分钟而是 prompt 变成了可版本管理的资产约束写在 brief 里输出结构写在 system prompt 里两者都可以进 git。4. 批量出稿manifest、并发与断点续跑4.1 manifest 用 JSONL批量场景下我不用一个大 JSON而用 JSONL一行一个任务方便追加、方便grep、失败重跑时也容易定位。{id:2025q4-review,brief:briefs/2025q4-review.json,out:out/batch/2025q4-review.deck.json} {id:series-llm-basics,brief:briefs/series-llm-basics.json,out:out/batch/series-llm-basics.deck.json} {id:oncall-handbook,brief:briefs/oncall-handbook.json,out:out/batch/oncall-handbook.deck.json}4.2 批量脚本# scripts/gen_batch.py import json import os import time from concurrent.futures import ThreadPoolExecutor, as_completed from pathlib import Path import requests from dotenv import load_dotenv load_dotenv() BASE_URL os.environ[ANTHROPIC_BASE_URL].rstrip(/) API_KEY os.environ[ANTHROPIC_API_KEY] MODEL os.environ[SLIDES_MODEL] CONCURRENCY int(os.environ.get(SLIDES_CONCURRENCY, 3)) MAX_RETRY 3 SYSTEM_PROMPT Path(prompts/deck_system.txt).read_text(encodingutf-8) def run_one(task: dict) - tuple[str, str]: out_path Path(task[out]) if out_path.exists(): return task[id], skipped brief json.loads(Path(task[brief]).read_text(encodingutf-8)) prompt json.dumps(brief, ensure_asciiFalse) last_err for attempt in range(1, MAX_RETRY 1): try: resp requests.post( f{BASE_URL}/v1/messages, headers{ x-api-key: API_KEY, anthropic-version: 2023-06-01, content-type: application/json, }, json{ model: MODEL, max_tokens: 4000, system: SYSTEM_PROMPT, messages: [{role: user, content: prompt}], }, timeout180, ) if resp.status_code in (429, 500, 502, 503, 504): raise RuntimeError(fretryable {resp.status_code}) resp.raise_for_status() text .join( b.get(text, ) for b in resp.json().get(content, []) if b.get(type) text ).strip() if text.startswith(): text text.split(\n, 1)[1].rsplit(, 1)[0].strip() out_path.parent.mkdir(parentsTrue, exist_okTrue) out_path.write_text(text, encodingutf-8) return task[id], ok except Exception as exc: # noqa: BLE001 last_err str(exc) time.sleep(2 ** attempt) return task[id], ffailed: {last_err} def main() - None: tasks [ json.loads(line) for line in Path(manifest.jsonl).read_text(encodingutf-8).splitlines() if line.strip() ] with ThreadPoolExecutor(max_workersCONCURRENCY) as pool: futures [pool.submit(run_one, t) for t in tasks] for fut in as_completed(futures): tid, status fut.result() print(f{tid}\t{status}) if __name__ __main__: main()python scripts/gen_batch.py断点续跑就靠out_path.exists()那三行已经出过的任务直接跳过失败的任务删掉产物再跑一次即可。并发数不要一上来就拉满先用SLIDES_CONCURRENCY2跑一遍看平均耗时和失败率再决定要不要加到 4 或 5。4.3 单页与批量出稿对照维度单页出稿批量出稿入口手动传 brief 路径manifest.jsonl驱动命令gen_slide.pygen_batch.py并发1环境变量控制建议 2~5失败处理当场看 traceback重试 失败任务清单断点续跑无必要产物存在即跳过产物单个 deck JSON多个 deck JSON典型场景调 prompt、改结构季度复盘、系列课程、客户交付主要成本试错时间并发与配额管理一句话总结单页出稿是调试工具批量出稿是生产工具。不要跳过单页那一步直接上批量否则一个 prompt 缺陷会被复制成几十份垃圾产物。5. 同一套变量接进 Claude Code、Codex 与 CC Switch脚本跑通之后编辑器里的工具链也该统一到同一个 Base URL否则你会同时维护两套 Key排查问题时很难判断是哪一层出的错。5.1 Claude Codesettings.jsonClaude Code 走的是 Anthropic 协议所以用ANTHROPIC_*这一组变量写进用户级配置文件{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_AUTH_TOKEN: YOUR_API_KEY, ANTHROPIC_MODEL: claude-sonnet-4-5 } }保存后新开一个终端窗口让配置生效。如果你更习惯用环境变量在 shell 的 rc 文件里导出同样三行即可效果等价。注意ANTHROPIC_BASE_URL依然只写到/api这一层。5.2 Codexconfig.tomlCodex 用的是另一套协议和另一套字段千万不要把ANTHROPIC_*抄过去那样配置根本不会被读取。配置文件一般在~/.codex/config.tomlmodel claude-sonnet-4-5 model_provider taotoken [model_providers.taotoken] name TaoToken base_url https://taotoken.net/api/v1 env_key TAOTOKEN_API_KEY wire_api chat对应的环境变量单独一个export TAOTOKEN_API_KEYYOUR_API_KEY两套配置的差异记住一句话就够了Claude Code 认ANTHROPIC_*Codex 认TAOTOKEN_API_KEY加config.toml里的 provider 段字段名不能混用。5.3 CC Switch 三件套如果你在多个供应商之间来回切用 CC Switch 这类配置切换工具会省事。切换时真正需要同步改的其实就三项Base URL统一填https://taotoken.net/apiClaude Code 语境Codex 语境下按它自己的路径要求补/v1。API Key控制台里新建的那把占位符YOUR_API_KEY。默认模型 ID与.env中的SLIDES_MODEL保持一致。把这三项绑定成一份 profile切供应商时整份替换就不会出现Key 换了但 Base URL 还是旧的这种最难查的组合错误。新建 Key 的入口在 https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_slides_switch 如果只是想先看看模型列表和对话效果可以从 https://taotoken.net/models/detail/chat?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_slides_switch 进去试。6. 报错对照表与排障顺序批量出稿最怕的不是报错而是同样的报错出现十几次还不知道从哪查。下面这张表是我自己踩过之后整理的排查顺序从上往下。现象常见原因处理方式401 UnauthorizedKey 带引号、空格或读的是旧.env打印len(API_KEY)确认长度重新source .env404 Not FoundBase URL 多写了一层/v1改回https://taotoken.net/api让客户端拼路径400 model not foundSLIDES_MODEL与可用模型 ID 不一致对照控制台模型列表逐字核对429 Too Many Requests并发过高把SLIDES_CONCURRENCY降到 2并保留指数退避响应被截断、JSON 解析失败max_tokens不够调大SLIDES_MAX_TOKENS或在 prompt 里限制页数单页正常、批量全挂共用 session 或线程安全问题每个线程独立构造请求不复用可变对象偶发超时单次请求时长超过客户端阈值设timeout并把批次拆小几个补充经验第一JSON 解析失败不要用try/except静默兜底直接把原始文本落盘到out/failed/看一眼是被截断还是模型多写了一段解释。大多数情况是max_tokens卡在了 JSON 中间。第二批量失败先看失败任务的分布。如果失败集中在某几个 brief 上那是输入问题如果均匀分布那是并发或配额问题。这两类的处理方式完全不同。第三任何情况下都不要把 Key 打到日志里。加日志时只记录长度或者 hash 前几位print调试完记得删掉。7. 把流程固化下来从单页试跑到成批交付回到开头那个场景Anthropic 把 Claude Chat 和 Claude Cowork 收到同一个入口Docs 和 Slides 变成了聊天界面里的两个按钮对普通用户是便利对写脚本的人是接口边界的变化。能复用的策略是把生成什么和怎么调用拆开——brief 描述生成什么.env加脚本描述怎么调用。这样无论上层界面怎么合并、怎么改名你的批处理管道都不用重写。落地顺序建议按这个来先到 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_slides_final 创建 Key把.env写完整用第 2 节的 curl 命令做冒烟测试确认链路通用第 3 节的单页脚本调 prompt直到结构稳定再上第 4 节的批量脚本从并发 2 起步最后把 Claude Code 或 Codex 的配置统一到同一个 Base URL 和同一个 Key 上。如果你们团队要长期跑这类内容生产任务可以看看 Coding Plan 的额度模型是否匹配你的批量节奏https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_slides_final 。需要新 Key 时走 https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_slides_final 。Claude Code 侧的完整配置说明在 https://taotoken.net/doc/ClaudeCodeAnthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_slides_final 想先零成本验证对话链路就直接开 https://taotoken.net/models/detail/chat?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_slides_final 发一条消息试试。把.env、brief 文件和脚本一起提交到仓库记得忽略.env本身你的幻灯片生产线就有了版本历史。下次再出季度复盘改的是数据不是流程。