1. 为什么我要把定时任务和模型调用拆开
OpenClaw 本身是个很称手的任务编排工具,cron 表达式、依赖关系、失败重试这些它都管。但真把它跑起来做 AI 定时任务,你会发现一个尴尬的现实:任务调度是稳的,模型调用是飘的。今天用这家 Key,明天那家限流,后天某家接口改字段,你的定时任务半夜三点挂掉,第二天早上才发现。
我试过最原始的做法,把 API Key 硬编码在任务脚本里。结果就是每换一次模型供应商,就要翻一遍所有任务文件,改完还要重新部署。更麻烦的是,不同任务用不同模型——写作用一个、数据抽取用一个、摘要生成又用一个——Key 散落在各处,根本没法统一管理。
所以这篇的核心思路是:让 OpenClaw 只管调度和编排,让 TaoToken 只管模型通道。两者通过一个统一的 Base URL 和 Key 对接,任务配置里不再出现任何具体供应商的名字。这样你换模型、加模型、调参数,都只动一个地方。
适合谁看?如果你已经在用 OpenClaw 跑一些自动化脚本,想给它加上 AI 能力;或者你手上有一堆定时任务需要调用大模型,但被 Key 管理搞得头大,这篇就是给你写的。整套链路我会从任务定义、调度触发、模型调用到结果校验完整走一遍,配置片段可以直接复制。
先说清楚一个概念,避免后面混淆。OpenClaw 里的"定时任务"本质是一个 CronTask 对象,它有三个关键属性:schedule(什么时候跑)、action(跑什么)、params(跑的时候带什么参数)。我们要做的,就是把 action 里原本直接调某家 API 的逻辑,换成调 TaoToken 的统一通道。这样任务本身不需要知道背后是哪个模型。
2. TaoToken 统一 Key 接入前的准备
在动手改任务之前,先把通道这件事理清楚。TaoToken 在这里扮演的角色是"模型调用的统一入口",它对外暴露一个兼容 OpenAI 格式的 API 地址,你拿一个 Key 就能访问它支持的多个模型。对 OpenClaw 来说,它只需要知道三件事:Base URL 是什么、Key 是什么、Model ID 填什么。
第一步是拿 Key。访问 https://taotoken.net/api-keys 这个页面,登录后创建一个新的 API Key。建议按用途命名,比如openclaw-cron,这样以后在控制台看用量的时候能一眼区分是哪个系统在调。Key 创建后只显示一次,复制下来存到安全的地方,后面配置里要用。
第二步是确认 Base URL。TaoToken 的 API 入口是https://taotoken.net/api,注意这个地址后面不加任何路径后缀,OpenClaw 或 SDK 会自动拼接/v1/chat/completions这类端点。如果你用的是 OpenAI 兼容的客户端,Base URL 就填这个。
第三步是选 Model ID。这个取决于你任务的实际需求。写作类任务可以用通用对话模型,数据抽取类任务可以用响应更快的轻量模型。具体有哪些 Model ID 可用,在 https://taotoken.net/doc 的文档页有完整列表。我建议先在 https://taotoken.net/models 用对话界面手动试几个模型,确认输出质量符合预期,再写进任务配置。
这里有个容易踩的坑:很多人会把 Base URL 写成https://taotoken.net/api/v1,然后客户端又自动拼一次/v1,结果变成/api/v1/v1/chat/completions,直接 404。记住,Base URL 就是https://taotoken.net/api,不要自己加/v1。
环境变量这块也建议提前设好,不要硬编码在任务文件里。在 OpenClaw 的运行环境里加两个变量:
export TAOTOKEN_API_KEY="sk-你的Key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"这样任务配置里引用os.environ["TAOTOKEN_API_KEY"]就行,换 Key 不用改代码。如果你是用 Docker 跑 OpenClaw,就在docker-compose.yml的 environment 段里加这两行。
还有一点,如果你打算长期跑多个定时任务,建议直接上 Coding Plan,它在用量和并发上比按次调用更划算,具体可以看 https://taotoken.net/coding-plan。对于每天要跑几十次模型调用的任务系统来说,这个账很好算。
3. 可复制的 OpenClaw 任务配置片段
现在进入正题,把 TaoToken 接进 OpenClaw 的任务配置。我按"任务定义 → 调度触发 → 模型调用"三层来拆,每一层都给可复制的片段。
先看任务定义。OpenClaw 的 CronTask 支持用 Python 对象方式定义,也支持用 JSON/TOML 配置文件。我推荐后者,因为配置和代码分离,改任务不用动主程序。下面是一个 TOML 格式的任务定义,放在tasks/daily_ai_summary.toml:
[task] name = "daily_ai_summary" schedule = "0 9 * * *" timezone = "Asia/Shanghai" action = "ai_summarize" timeout = 300 max_retries = 3 [task.params] source = "inbox" model = "gpt-4o-mini" prompt_template = "把以下内容总结成三条要点,每条不超过30字:\n{content}" output_channel = "feishu" [task.llm] base_url = "https://taotoken.net/api" api_key_env = "TAOTOKEN_API_KEY" model_id = "gpt-4o-mini" temperature = 0.3 max_tokens = 800这里的关键是[task.llm]这一段。它把模型调用的三个要素——Base URL、Key 来源、Model ID——集中在一个地方。任务的其他部分完全不关心背后是哪家模型。你要换模型,只改model_id这一行。
对应的 Python 加载逻辑大概长这样:
import os import toml from openclaw import CronTask from openai import OpenAI def load_task(path): cfg = toml.load(path) llm = cfg["task"]["llm"] client = OpenAI( base_url=llm["base_url"], api_key=os.environ[llm["api_key_env"]], ) return CronTask( name=cfg["task"]["name"], schedule=cfg["task"]["schedule"], timezone=cfg["task"]["timezone"], action=lambda params: ai_summarize(client, llm, params), params=cfg["task"]["params"], timeout=cfg["task"]["timeout"], max_retries=cfg["task"]["max_retries"], ) def ai_summarize(client, llm, params): content = fetch_content(params["source"]) prompt = params["prompt_template"].format(content=content) resp = client.chat.completions.create( model=llm["model_id"], messages=[{"role": "user", "content": prompt}], temperature=llm["temperature"], max_tokens=llm["max_tokens"], ) summary = resp.choices[0].message.content send_to_channel(params["output_channel"], summary) return {"summary": summary, "model": llm["model_id"]}注意ai_summarize的返回值。我特意把model也返回了,这样任务执行记录里能看出这次跑的是哪个模型,排查问题时很有用。
如果你更喜欢用 JSON 配置,等价写法是这样:
{ "task": { "name": "daily_ai_summary", "schedule": "0 9 * * *", "timezone": "Asia/Shanghai", "action": "ai_summarize", "llm": { "base_url": "https://taotoken.net/api", "api_key_env": "TAOTOKEN_API_KEY", "model_id": "gpt-4o-mini", "temperature": 0.3, "max_tokens": 800 } } }调度触发这块,OpenClaw 用的是标准 cron 表达式。0 9 * * *是每天 9 点,0 */2 * * *是每两小时,0 10 * * 1是每周一 10 点。时区一定要显式指定,不然服务器在 UTC 环境下,你的"早上 9 点"会变成下午 5 点。这个坑我在多个项目里都见过。
任务依赖用depends_on字段。比如"生成日报"依赖"收集数据",就在日报任务里加depends_on = ["collect_data"]。OpenClaw 会保证依赖任务先完成,再触发当前任务。如果依赖任务失败,当前任务默认不执行,除非你设run_on_dependency_failure = true。
4. 验证请求与成功结果
配置写完,别急着挂到生产调度上。先手动触发一次,确认整条链路通。OpenClaw 提供了run_now方法,可以在不等待 cron 时间的情况下立即执行任务:
task = load_task("tasks/daily_ai_summary.toml") result = task.run_now() print(result)如果一切正常,你会看到类似这样的输出:
{ "status": "success", "task": "daily_ai_summary", "started_at": "2025-03-20T09:00:01+08:00", "duration": 2.34, "result": { "summary": "1. 项目进度正常\n2. 下周需评审设计稿\n3. 服务器扩容已完成", "model": "gpt-4o-mini" } }看到status: success和result.summary有内容,说明模型调用成功了。如果summary是空的,或者status是failed,往下看排障那节。
更严谨的验证方式是直接打一次 API,排除 OpenClaw 层面的干扰。用 curl 测:
curl https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o-mini", "messages": [{"role": "user", "content": "回复OK两个字"}], "max_tokens": 10 }'正常返回里会有choices[0].message.content等于OK。如果这一步就失败,那问题在 Key 或网络,跟 OpenClaw 无关。
验证通过后,把任务注册到调度器:
from openclaw import Scheduler scheduler = Scheduler() scheduler.register(load_task("tasks/daily_ai_summary.toml")) scheduler.start()然后查一下任务列表,确认它被正确加载:
openclaw "显示所有定时任务"输出里应该能看到daily_ai_summary,状态是enabled,下次执行时间是明天 9 点。到这一步,一个可观测的定时任务闭环就跑通了。
结果校验这块,我建议在任务里加一个轻量的断言。比如摘要任务,如果返回内容长度小于 10 个字符,就认为异常,触发告警:
if len(summary) < 10: raise ValueError(f"摘要过短,可能模型异常: {summary}")这样任务状态会变成failed,你在执行历史里一眼就能看到,不用去翻日志。
5. 本篇常见错误排查
跑不通的时候,对照下面这几个真实报错看。
401 Unauthorized。这个最常见,九成是 Key 的问题。先确认环境变量TAOTOKEN_API_KEY真的被加载了,在 Python 里print(os.environ.get("TAOTOKEN_API_KEY"))看一下。如果打印出来是None,说明环境变量没设对,或者 Docker 容器里没传进去。如果 Key 有值但还是 401,检查一下 Key 有没有多余的空格或换行,复制的时候很容易带上。
local proxy failed / connection refused。这个报错说明请求根本没发出去,卡在本地网络层。检查 Base URL 是不是写成了https://taotoken.net/api/v1,多出来的/v1会导致路径拼接错误。另外确认你的运行环境能正常访问外网,有些内网服务器需要配置出口规则。
reading choices 报错 / KeyError: 'choices'。这个通常意味着返回的 JSON 结构和你预期的不一样。可能是模型名写错了,返回了一个错误对象而不是正常的 completion 响应。先把原始响应打出来看:
resp = client.chat.completions.create(...) print(resp.model_dump_json(indent=2))如果里面是{"error": {"message": "model not found"}},那就是 Model ID 填错了,去文档页核对一下正确的 ID。
OAuth / token expired。如果你用的是某些需要 OAuth 的客户端,可能会遇到这个。TaoToken 的 API Key 是长期有效的,不存在 OAuth 刷新问题。遇到这个报错,基本是客户端配置里混进了别的认证方式,检查一下是不是同时配了api_key和oauth_token,把后者删掉。
任务执行了但没输出。检查output_channel对应的发送函数有没有抛异常。有时候模型调用成功了,但发飞书消息失败,整个任务被标记为 failed。把发送逻辑用 try/except 包起来,发送失败不影响任务主流程,只记日志。
时区不对,任务在错误的时间跑。确认timezone字段写的是Asia/Shanghai,并且服务器时间本身是准的。可以用date命令看一下系统时间。如果服务器是 UTC,而任务没设时区,那 9 点的任务会在北京时间 17 点跑。
CC Switch / Cline MCP / Codex auth.json 相关。如果你在 OpenClaw 之外还用了这些工具,注意它们的配置是独立的。CC Switch 里配的 Base URL 和 Key 不会自动同步到 OpenClaw。每个工具都要单独配一遍,三件套(Base URL + Key + Model ID)缺一不可。Codex 的auth.json里如果写了别的供应商地址,也会导致请求走错通道。
排查的通用思路是:先 curl 测 API,通了再测 OpenClaw 任务,任务通了再挂调度。一层一层来,不要跳步。
6. 把定时任务跑稳的几个实用动作
最后说几个让这套系统长期稳定运行的动作,都是实际跑下来觉得有用的。
第一,给每个任务加执行日志落盘。OpenClaw 默认的日志在内存里,重启就没了。在任务 action 里加一行写文件:
import json, datetime with open(f"logs/{task.name}.jsonl", "a") as f: f.write(json.dumps({ "ts": datetime.datetime.now().isoformat(), "result": result, }, ensure_ascii=False) + "\n")这样出问题的时候有据可查,也能统计每个任务的调用量和耗时。
第二,模型调用加超时和重试。OpenClaw 的任务级 timeout 是一层保护,但模型调用本身也应该设超时。OpenAI 客户端支持timeout参数:
client = OpenAI( base_url=llm["base_url"], api_key=os.environ[llm["api_key_env"]], timeout=60.0, max_retries=2, )这样单次调用最多等 60 秒,失败自动重试 2 次,不会因为某次网络抖动把整个任务卡死。
第三,定期检查 Key 的用量。在 https://taotoken.net/console 能看到每个 Key 的调用量和费用。如果某个任务的用量突然暴涨,可能是 prompt 写得太长,或者任务被重复触发了。早点发现能省不少钱。
第四,任务命名要有规律。daily_、weekly_、hourly_前缀,加上功能描述,比如daily_ai_summary、weekly_report_gen。这样在任务列表里一眼能看出哪些是同类,批量操作的时候不容易误伤。
第五,新任务先 dry run。OpenClaw 支持dry_run模式,任务会走完所有逻辑但不实际发送消息、不写数据库。新任务上线前先 dry run 几次,确认输出符合预期,再切到正式模式。
这套东西跑顺之后,你会发现加一个新 AI 定时任务就是写一个 TOML 文件的事。模型通道统一在 TaoToken 这边,任务编排统一在 OpenClaw 这边,两边各管各的,互不干扰。