1. 长任务跑满 200 小时,先看心跳这一层
把一个社区热度很高的国产开源 AI Agent 挂到服务器上跑长任务,最典型的翻车方式并不是模型答错,而是跑到第几十个小时,日志里突然出现一行stream closed或者httpx.ReadTimeout,然后整个任务停在半路,进程还在,但再也不会往前走一步。最近很多人在讨论"让 AI Agent 稳定跑满 200 小时不掉线"这类话题,讨论焦点大多集中在 Agent 自身的推理循环、上下文压缩和工具调用上,但真正让链路断掉的往往是更底层的一件事:心跳检测缺失。Agent 与模型服务之间是长连接(HTTP streaming / SSE),只要一段时间没有任何数据帧经过,中间的负载均衡、反向代理、NAT 网关就会判定这条连接"闲置",然后单方面把它掐掉。如果 Agent 客户端没有定时发心跳、没有设置读超时、没有把会话状态落盘,那这条链路一断就得从头再来。本文要解决的就是这个场景:把模型的供应商入口统一切到 TaoToken 官网,拿到 Key 后把 Base URL 填成https://taotoken.net/api,然后用一份可复制的心跳 + 断点续跑配置,让长任务在被切流之后能把请求续上,而不是重启重来。
先说清楚"掉线"到底掉在哪一层,这决定了你后面该改配置还是该改代码:
- 传输层掉线:TCP 空闲被回收,表现为
connection reset by peer、RemoteDisconnected、stream closed。这一层跟模型无关,跟有没有心跳帧强相关。 - 应用层卡死:连接还在,但客户端
read()一直阻塞,没有超时保护。表现是进程活着、CPU 为 0、日志停在某一行不动,这类最容易被误判成"模型在长思考"。 - 鉴权层中断:跑到中途返回 401 / 403,多半是 Key 失效、配额耗尽或者切了别的供应商,跟心跳无关,但症状看起来一样"突然不动了"。
- 限流层中断:429 密集出现,客户端没做指数退避,重试风暴把剩余配额打光。
长任务排查的顺序应该是:先从时间戳上确认掉线时刻,再看掉线前 3 分钟有没有"数据帧间隔异常拉长"的迹象,最后再判断是传输问题还是鉴权问题。下面这套流程,就是围绕"让请求续上"来组织的。
2. 在 TaoToken 取 Key 与 Base URL:写入前先做一次连通性探针
所有配置都建立在两个常量之上:一把可用的 Key,和一个固定的 Base URL。Key 不需要在代码里硬编码,也不要在写了 Key 之后才发现地址填错。正确顺序是:先去 TaoToken 官网 完成注册并拿到 Key,再把它写进本机环境变量。这个顺序很重要——很多"跑到一半掉线"的案例,根因其实是 Key 被写进了某个临时脚本,重启后被覆盖成了空值。
Base URL 固定填:
https://taotoken.net/api注意这个地址是给工具配置用的,不要额外拼 UTM 参数,也不要自己加/v1之类的后缀去试探,按文档给的填即可。模型名、可用上下文长度这类信息,以控制台里的模型列表为准,不要凭记忆写。
拿到 Key 之后,先用一条最小请求把链路跑通,确认三件事:地址对、Key 有效、返回体是标准流式结构。
export TAOTOKEN_API_KEY="YOUR_API_KEY" curl -sS -N \ -X POST "https://taotoken.net/api/v1/chat/completions" \ -H "Authorization: Bearer ${TAOTOKEN_API_KEY}" \ -H "Content-Type: application/json" \ -H "Accept: text/event-stream" \ -d '{ "model": "<按控制台模型列表填写>", "stream": true, "messages": [{"role": "user", "content": "ping"}] }' | head -n 20四个观察点:
- 有没有
data:开头的行连续返回。如果只返回一个完整 JSON 体,说明流式没打开。 - 首字节时间(TTFB)是多少。长任务里这个值决定了心跳间隔该设多长。
- HTTP 状态码是不是 200。401 说明 Key 没生效,可能是环境变量没
export或者复制时带了空格。 - 结束后连接是不是被正常关闭。如果 curl 挂住不退出,说明你在客户端侧也缺少读超时。
这一步别跳过。长任务排障最耗时的部分,就是分不清"是供应商侧断的"还是"自己代码断的"。有了这条基准命令,后面出问题时可以直接复现对比。
3. Claude Code 侧:settings.json 与 ANTHROPIC_* 的正确写法
Claude Code 的供应商切换走的是ANTHROPIC_*这一组变量,配置文件放在用户目录下的settings.json。这里要强调的是:ANTHROPIC_*只属于 Claude Code 这一侧,不要把它套到 Codex 上,两条链路的变量体系是分开的,混用会出现"看起来配了但请求打到了默认地址"这种极难排查的现象。
~/.claude/settings.json参考配置:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_AUTH_TOKEN": "YOUR_API_KEY", "ANTHROPIC_MODEL": "<按控制台模型列表填写>", "ANTHROPIC_SMALL_FAST_MODEL": "<按控制台模型列表填写>", "API_TIMEOUT_MS": "600000" } }几个和长任务稳定性直接相关的点:
ANTHROPIC_BASE_URL指向 TaoToken 的 Base URL,这是所有请求的出口。ANTHROPIC_AUTH_TOKEN放 Key。生产环境建议不要写死在 JSON 里,改成从系统密钥管理或 CI 注入,JSON 中留占位符。API_TIMEOUT_MS是单次请求的容忍上限。默认值偏短,长任务里一个大型工具调用或长上下文压缩就可能超过它,导致"没掉线但一直重试"。把它放宽到 10 分钟是一个常见起点,但不要无限大——太大等于取消了超时保护,连接被对端悄悄切断时你会一直等下去。- 如果配置文件里同时存在旧的
ANTHROPIC_API_KEY,建议清理掉,避免和ANTHROPIC_AUTH_TOKEN两个来源打架。
如果不想改全局配置,也可以用环境变量方式临时拉起:
export ANTHROPIC_BASE_URL="https://taotoken.net/api" export ANTHROPIC_AUTH_TOKEN="YOUR_API_KEY" export API_TIMEOUT_MS="600000" claude --print "读取当前目录结构并输出摘要"验证配置是否真的生效,不要只看它跑起来了——跑起来了也可能还在用旧地址。用一个必然超长输出的请求去观察日志,确认请求确实发往了你配置的地址,并且长时间空闲时没有立刻断开。
4. Codex 侧:config.toml 里的 provider 段与心跳超时
Codex 用的是config.toml,走的是model_providers这套结构,和 Claude Code 完全不同。把 TaoToken 作为自定义 provider 加进去:
# ~/.codex/config.toml model = "<按控制台模型列表填写>" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY" wire_api = "chat" request_max_retries = 6 stream_max_retries = 6 stream_idle_timeout_ms = 300000这里的stream_idle_timeout_ms就是本篇的关键参数。它定义的是"流式响应空闲多久算断"。设得太小,模型在长思考时会被误判成断线并触发重试;设得太大,真正被网关掐掉时你要等很久才发现。经验区间是 180000 到 300000 毫秒,配合外层的心跳脚本一起用。
stream_max_retries和request_max_retries决定断流后的重试次数。Codex 侧的自动重试是好事,但前提是重试要能接上上下文。如果每次重试都从头开始(重新提交完整历史),长任务里会迅速累积 Token 消耗,钱花了、进度却归零。
key 的注入方式:
export TAOTOKEN_API_KEY="YOUR_API_KEY" codex exec "扫描仓库并生成模块依赖说明"再次强调:Codex 侧不要写ANTHROPIC_*,Claude Code 侧也不要写env_key这一套。两边的配置文件分开放,切换时只动其中一个。
5. CC Switch 三件套:一份 profile 在两边复用
同时用 Claude Code 和 Codex 的人,通常会在多个供应商之间来回切。手动改两个配置文件很容易改漏,改用 CC Switch 把配置收敛成"三件套":
第一件:供应商条目(Provider Profile)
在 CC Switch 里新增一个条目,字段只填三样——名称TaoToken、Base URLhttps://taotoken.net/api、KeyYOUR_API_KEY。名称只作标识,不要影响请求路径。
第二件:目标工具映射
同一个供应商条目分别映射到 Claude Code 和 Codex 两条链路。映射时注意字段落点不同:落到 Claude Code 的是ANTHROPIC_BASE_URL/ANTHROPIC_AUTH_TOKEN,落到 Codex 的是base_url/env_key。这一步是整套方案里最容易出错的地方。
第三件:切换动作与校验
每次切换后立刻做一次校验,而不是等到跑长任务时才发现不对:
# 切换后确认当前生效的地址 echo "BASE_URL=${ANTHROPIC_BASE_URL:-$CODEX_BASE_URL}" echo "KEY_SET=$([ -n "${TAOTOKEN_API_KEY}" ] && echo yes || echo no)"KEY_SET=no是长任务掉线的头号嫌疑。很多"跑了 40 小时突然 401"的案例,本质是某个终端会话没有继承到环境变量,而不是 Key 本身失效。
用 CC Switch 的另一个好处是:出问题时可以一条命令切回上一个能跑通的 profile,快速区分"是 TaoToken 侧的问题"还是"是我这次改的配置的问题"。这对长任务排障的价值,比省下来的那点手动改配置的时间大得多。
6. 启动命令:让长任务在心跳缺失时也能续上
配置对了,接下来是启动方式。长任务不能用交互式终端直接跑,SSH 一断进程就跟着走。
# 1) 注入 Key export TAOTOKEN_API_KEY="YOUR_API_KEY" # 2) 开一个独立会话 tmux new -s agent-longrun # 3) 在会话内启动,把日志按天落盘 nohup python -m your_agent \ --provider taotoken \ --heartbeat-interval 30 \ --checkpoint-dir ./ckpt \ --resume-from-latest \ > "logs/agent-$(date +%Y%m%d).log" 2>&1 & # 4) 记录 PID,方便看门狗判断 echo $! > ./agent.pid四个参数对应四件事:
--heartbeat-interval 30:每 30 秒发一次心跳。这个值的上限由中间设备的最短空闲超时决定,下限由你的配额和日志噪声决定。30 秒是长任务里的常见选择。--checkpoint-dir ./ckpt:会话状态落盘。没有它,重连等于重来。--resume-from-latest:重启后从最近一个检查点继续,而不是从第 0 步。- 日志按天切分:排障时你需要能按时间定位到"掉线那一刻",单文件几十 GB 的日志是灾难。
如果 Agent 项目本身没有暴露心跳参数,就在外层补一个保活循环。下面的看门狗脚本做三件事:进程不在了就拉起、日志 10 分钟没更新就重启、重启时带上续跑参数。
#!/usr/bin/env bash set -euo pipefail LOG="logs/agent-$(date +%Y%m%d).log" STALL_SECONDS=600 CMD=(python -m your_agent --provider taotoken --checkpoint-dir ./ckpt --resume-from-latest) while true; do if [ -f ./agent.pid ] && kill -0 "$(cat ./agent.pid)" 2>/dev/null; then last_ts=$(stat -c %Y "$LOG" 2>/dev/null || echo 0) now_ts=$(date +%s) if [ $((now_ts - last_ts)) -gt "$STALL_SECONDS" ]; then echo "[watchdog] log stalled $((now_ts - last_ts))s, restarting" | tee -a "$LOG" kill "$(cat ./agent.pid)" 2>/dev/null || true sleep 5 else sleep 30 continue fi fi nohup "${CMD[@]}" >> "$LOG" 2>&1 & echo $! > ./agent.pid echo "[watchdog] started pid=$(cat ./agent.pid)" | tee -a "$LOG" sleep 30 done这个脚本的关键在STALL_SECONDS。设得太小,模型在长思考时会被误杀;设得太大,掉线后你要干等。600 秒配合stream_idle_timeout_ms = 300000是一个合理的组合:内层超时负责触发重试,外层看门狗负责兜底重启。
7. 掉线前后日志对照表
下面是一份脱敏后的日志对照,把"症状—判读—处置"三列对齐。排障时可以拿你自己的日志逐行比对。
| 时间 | 日志片段(脱敏) | 判读 | 处置 |
|---|---|---|---|
| 00:00:03 | provider=taotoken base_url=https://taotoken.net/api | 出口地址正确 | 无需处理 |
| 00:30:11 | [stream] chunk=128 latency=780ms | 数据帧间隔正常 | 记录基线 |
| 41:09:52 | [stream] chunk=0 idle=165s | 空闲时间异常拉长 | 检查心跳是否在发 |
| 41:12:30 | [stream] chunk=0 idle=320s | 超过空闲阈值 | 对齐心跳间隔与超时 |
| 41:12:41 | httpx.ReadTimeout/stream closed | 读超时触发,链路被切 | 见下一行处置 |
| 41:12:41 | retry=1 backoff=2s | 有重试,但退避过短 | 改为指数退避并设上限 |
| 41:12:43 | resume from checkpoint step=1832 | 断点续跑生效 | 保持该配置 |
| 41:12:45 | [stream] chunk=96 latency=1400ms | 请求已续上 | 观察 10 分钟稳定性 |
| 41:20:00 | usage in=..., out=... | 用量正常增长 | 核对控制台用量曲线 |
| 41:35:12 | 401 unauthorized | 鉴权中断,非心跳问题 | 检查 Key 是否被覆盖或失效 |
| 41:35:13 | retry storm: 12 calls/s | 无退避的重试风暴 | 加限速,避免打光配额 |
从这张表里能读出三个结论:
第一,掉线前一定有征兆。idle从 165 秒涨到 320 秒的过程,就是心跳缺失在起作用。如果你只盯"最后那行报错",永远定位不到根因。
第二,续跑能力比重试次数重要。resume from checkpoint step=1832这一行,决定了这次中断的代价是"重跑 1 步"还是"重跑 41 小时"。重试次数再多,不能续跑也没意义。
第三,401 和流超时要分开看。两者的表象都是"任务停住了",但一个要改 Key、一个要改超时与心跳。混在一起排查,容易把配置越改越乱。
8. 200 小时长任务的 Token 消耗与稳定性排查清单
把上面所有内容收敛成一份可执行的检查清单。长任务跑到中途出问题,按顺序走一遍,通常 10 分钟内能定位。
接入层
- [ ] Base URL 是否为
https://taotoken.net/api,有没有被某个旧配置覆盖 - [ ] Key 是否通过环境变量注入,
echo检查是否为空 - [ ] Claude Code 用的是
ANTHROPIC_*,Codex 用的是config.toml的model_providers,两边没有混用 - [ ] 模型名以控制台列表为准,没有凭记忆手写
心跳层
- [ ] 客户端是否有固定间隔的心跳(典型 30 秒)
- [ ]
stream_idle_timeout_ms是否大于心跳间隔的 3 倍以上 - [ ]
API_TIMEOUT_MS是否足以覆盖最长的单次工具调用 - [ ] 是否有外层的 stall 看门狗,兜住"进程活着但不前进"的情况
续跑层
- [ ] 会话状态是否落盘,检查点间隔是多少
- [ ] 重启时是否带
--resume-from-latest这类续跑参数 - [ ] 重试是否是指数退避且有上限,避免重试风暴
用量层
- [ ] 长任务开始前确认配额余量,按小时折算预估消耗
- [ ] 日志里定期打印
usage字段,和模型对话控制台的曲线做交叉验证 - [ ] 上下文压缩触发频率是否异常,压缩过于频繁通常是任务设计问题而不是连接问题
观测层
- [ ] 日志按天切分,时间戳统一时区
- [ ] 关键节点(重连、续跑、鉴权失败)有独立可 grep 的标记
- [ ] 保留最近 24 小时原始日志,方便回溯"第一次异常"是哪一秒
这套清单里,真正决定能不能跑满 200 小时的是心跳 + 续跑这两块,其余都是配套。心跳解决"连接不会被误判闲置",续跑解决"真断了也不重来"。
9. 从一次配置到长期跑稳
回到最初的问题:心跳检测缺失,AI Agent 跑满 200 小时时请求怎么续上。答案分三层——在传输层补心跳,让长连接不会因为空闲被判死;在应用层补超时与退避,让异常能被感知、能被收敛;在状态层补检查点与续跑,让中断的代价被控制在一步之内。供应商入口只是这三层里最容易被替换的一环:把 Base URL 统一成https://taotoken.net/api,Key 只注入一次,剩下的稳定性工作全在客户端。
如果你还没开始,建议按这个顺序走一遍,每一步都能立刻验证:
- 先到 TaoToken 官网 完成注册并领取 Key;
- 在模型对话里发一条流式请求,确认心跳与超时行为符合预期;
- 长任务场景建议配 Coding Plan,先把配额这一层的不确定性去掉;
- 到 API Keys 生成正式 Key,按上面的方式注入环境变量,不要写进仓库;
- Claude Code 的具体字段含义与更多参数,对照 Claude Code 文档 再核一遍。
配好之后,先跑一个 6 小时的任务做灰度,重点看两件事:日志里idle有没有异常拉长的趋势,以及续跑有没有真的从检查点接上。这两件事都正常,再把时长拉到 200 小时。长任务的稳定性从来不是靠一次调参调出来的,而是靠"每一次中断都能被解释、被记录、被续上"积累出来的。