
1. 为什么 scan.py 一挂后台就“变傻”长期运行 Agent 的真实障碍很多人第一次把 OpenClaw Agent 挂到后台都会遇到同一个尴尬前台对话时它挺聪明一旦脱离终端、交给系统调度行为就开始漂移。要么重复提醒同一件事要么干脆沉默好几天要么进程还在但状态全丢。你搜“OpenClaw Agent 长期运行 scan.py 状态保持”这类词大概率就是卡在这一步。问题的根子不在模型而在工程结构。一个只能被“手动触发一次”的 Agent本质上是个高级聊天框你问它答你不问它不动它没有自己的时间轴也没有跨次调用的记忆。而长期运行要求三件事同时成立——任务能被调度、状态能被保持、行为边界能被约束。缺任何一条它都跑不长。我拿 scan.py 做切入点是因为它足够小、足够安全又足够典型。它只读文件元数据、只输出 JSON没有任何副作用。但正是这种“只测量、不判断”的脚本最容易暴露长期运行的三个坑第一状态丢失。scan.py 每次运行都是全新进程它不知道上一次扫描是什么时候、上次有没有提醒过。如果调度器每 10 分钟拉一次它就会每 10 分钟重新判断一次结果要么疯狂提醒要么因为阈值没到永远沉默。第二判断与执行混在一起。很多人的写法是if file_count 100: notify()把阈值判断直接写进脚本。这在一次性任务里没问题但长期运行时阈值是死的场景是活的。用户正在集中下载资料的那两天文件数暴涨脚本会误报用户出差一周没碰电脑文件数没变脚本又该提醒却不提醒。第三权限没有收口。长期运行的 Agent 最怕的不是“做错”而是“有能力做错”。如果 Skill 允许它 move、rename、delete那你根本不敢让它无人值守。失控成本一旦高过收益这个 Agent 就永远只能停在 Demo 阶段。所以这一篇的目标很明确把 scan.py 从一个“被调用的脚本”改造成一个“可被长期调度、状态可保持、权限受约束”的 OpenClaw Skill。改造完成后你会得到一个能常驻、能沉默、能在该提醒时才提醒的任务执行体。下面所有配置和代码都可以直接复制路径与原文保持一致。2. TaoToken 前置给长期运行 Agent 一条稳定的模型通道长期运行的 Agent 和一次性对话最大的区别是它对模型通道的稳定性要求高一个量级。你手动聊天时接口偶尔抖一下无所谓重发就行但 Agent 在后台每 10 分钟调一次模型通道不稳就会导致判断时有时无状态机直接乱掉。所以在上 Skill 之前先把模型通道固定下来。TaoToken 在这里的角色是统一 Key 和 API 通道。你不需要在 Skill 里硬编码某一家模型的地址而是通过一个 Base URL 加一个 Key让 Agent 的每次判断请求都走同一条路。官网入口在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 注意 API 这条不带 UTM 参数配置时直接填。具体操作分三步。第一步进控制台创建 Key地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 创建后先复制保存页面刷新后不再完整显示。第二步如果你要跑的是编码类或 Agent 类长期任务建议直接看 Coding Plan地址 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 它面向的就是这种需要持续调用的场景。第三步把 Key 写进环境变量不要写进 Skill 文件里。export TAOTOKEN_API_KEYsk-你的Key export TAOTOKEN_BASE_URLhttps://taotoken.net/api这里有个我踩过的坑很多人把 Key 直接写进 SKILL.md 或 policy.yaml然后提交到 Git。长期运行的 Agent 往往部署在服务器上一旦仓库泄露Key 就跟着泄露。正确做法是 Skill 只引用环境变量名真实值由运行环境注入。模型 ID 的选择上长期运行场景优先选稳定、响应快的型号不要一味追最大参数。因为 Agent 每次判断的输入只是一段结构化 JSON输出只是一个布尔值加一句理由任务很轻。你可以先在模型对话页面 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 里试几个型号看哪个在“是否提醒”这类判断上更稳再固定下来写进配置。通道固定之后Skill 里的模型调用就变成了一件确定的事Base URL 固定、Key 从环境变量读、Model ID 固定。这样无论调度器什么时候拉起 Agent它拿到的判断能力都是一致的。这一步看起来简单但它是长期运行能不能成立的地基。地基不稳后面状态保持做得再好也白搭。3. 可复制配置SKILL.md、scan.py 与 policy.yaml 三件套这一节是整篇的核心给你一套可以直接落地的文件结构。目录保持和原文一致skills/downloads-reminder/ ├── SKILL.md ├── tools/ │ └── scan.py └── rules/ └── policy.yaml先看 SKILL.md。它的本质不是功能说明书而是权限清单。长期运行的 Agent权限必须写死在技能定义里让调度器每次拉起它时都拿到同一套边界。--- name: downloads-reminder description: Decide whether to remind user to clean Downloads directory metadata: openclaw: skillKey: downloads-reminder baseUrl: ${TAOTOKEN_BASE_URL} apiKey: ${TAOTOKEN_API_KEY} model: your-stable-model-id --- ## Purpose This Skill decides whether the user should be reminded to clean Downloads. It does NOT organize, move, rename, or delete any file. ## Safety Rules (Hard Constraints) 1. Never modify any file or directory 2. Never delete or overwrite files 3. Never operate outside the given Downloads path 4. Never perform destructive actions 5. Only read filesystem metadata 6. Only produce a reminder decision ## Allowed Tools Only one tool is permitted: tools/scan.py ## How to Act 1. Call scan.py to collect directory statistics 2. Evaluate the result together with rules/policy.yaml 3. Decide whether a reminder is necessary 4. Output a structured decision ## Decision Output Format json { notify: true, reason: Explain why a reminder is or is not necessary }注意 metadata 里我把 baseUrl、apiKey、model 三件套都写全了。这是长期运行的关键Base URL 指向 https://taotoken.net/api Key 从环境变量注入Model ID 固定。任何一项缺失Agent 在后台调用时都会失败。 接着是 scan.py。它的设计原则只有一句话脚本负责测量世界判断交给模型。所以它只输出事实不做任何阈值判断。 python #!/usr/bin/env python3 from __future__ import annotations from pathlib import Path import argparse import json import sys import time def scan_downloads(path: Path): now time.time() files [f for f in path.iterdir() if f.is_file()] return { file_count: len(files), new_24h: sum(1 for f in files if now - f.stat().st_mtime 86400), old_30d: sum(1 for f in files if now - f.stat().st_mtime 30 * 86400), total_size_mb: round( sum(f.stat().st_size for f in files) / 1024 / 1024, 2 ), } def _json_print(obj: dict) - None: print(json.dumps(obj, ensure_asciiFalse)) def main(argv: list[str]) - int: parser argparse.ArgumentParser( descriptionScan Downloads directory and output stats as JSON. ) parser.add_argument( --path, typestr, defaultstr(Path.home() / Downloads), helpDirectory to scan (default: ~/Downloads), ) args parser.parse_args(argv) target Path(args.path).expanduser().resolve() if not target.exists(): _json_print({error: fpath not found: {str(target)}}) return 2 if not target.is_dir(): _json_print({error: fnot a directory: {str(target)}}) return 2 try: result scan_downloads(target) _json_print(result) return 0 except PermissionError as e: _json_print({error: permission denied, detail: str(e), path: str(target)}) return 3 except Exception as e: _json_print({error: scan failed, detail: str(e), path: str(target)}) return 1 if __name__ __main__: raise SystemExit(main(sys.argv[1:]))这个脚本的工程要点很清晰只读文件元数据、输出结构化 JSON、不做判断、不产生副作用。它具备长期运行资格的根本原因就在这里——无论跑多少次世界都不会被它改变。最后是 policy.yaml。规则不是大脑只是护栏作用是防止 Agent 在明显不该提醒的时候乱说话。notify: min_file_count: 80 min_old_ratio: 0.6 max_new_24h: 5 min_total_size_mb: 500这四个参数的含义分别是文件总数低于 80 不提醒30 天以上旧文件占比低于 60% 不提醒最近 24 小时新增超过 5 个不提醒说明用户正在集中下载别打断总体积低于 500MB 不提醒。它们是 guardrails不是决策逻辑本身。真正的判断由模型结合这些护栏和 scan.py 的输出完成。三件套齐了之后Skill 的调用链就固定了调度器拉起 Skill → Skill 调 scan.py 拿事实 → 模型结合 policy.yaml 做判断 → 输出 notify 布尔值和理由。整条链路无副作用、可重复、可无人值守。4. 验证请求一次持续运行的成功结果长什么样配置写完必须验证它真的能长期跑而不是只在手动触发时正常。验证分两层先验证单次调用链路通再验证持续调度下状态稳定。先做单次验证。直接跑 scan.py确认它输出的是干净的 JSONpython3 skills/downloads-reminder/tools/scan.py --path ~/Downloads正常输出类似{file_count: 132, new_24h: 2, old_30d: 98, total_size_mb: 1240.5}如果这里报path not found或permission denied先别急着上调度器把路径和权限问题解决掉。长期运行最怕的就是带着小错误上线跑一晚上全是失败日志。单次通了之后用模型对话页面做一次判断验证。把 scan.py 的输出和 policy.yaml 的规则一起喂给模型问它“现在要不要提醒用户清理 Downloads为什么”。地址在 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。正常应该返回类似{ notify: true, reason: 文件总数 132 超过阈值 8030 天以上旧文件 98 个占比约 74% 超过 60%最近 24 小时仅新增 2 个说明用户没有在集中下载属于长期堆积建议提醒清理。 }注意这个理由的措辞它引用的是事实和护栏而不是“我觉得该清理了”。这就是 Agent 和脚本的分界线——脚本只会if file_count 100Agent 会结合新增量判断“现在提醒会不会打断你”。接下来是持续运行验证。用 cron 每 30 分钟拉一次观察一天的行为*/30 * * * * cd /path/to/skills /usr/bin/python3 -c import subprocess,json; outsubprocess.check_output([python3,downloads-reminder/tools/scan.py]); print(out.decode()) /var/log/downloads-reminder.log 21跑一天后看日志你应该观察到三种状态文件少时沉默、文件多但新增多时沉默、文件多且新增少时提醒。如果日志里出现连续多次相同提醒说明状态保持没做好需要加一个“上次提醒时间”的记录避免重复打扰。成功结果的标准不是“它提醒了”而是“它在该沉默的时候沉默了”。我实测下来一个健康的长期运行 Agent绝大多数调度周期都是无输出的。你翻日志会发现它一天可能只判断出一次该提醒其余时间都在安静地测量世界。这种“大部分时间什么都不做”的能力恰恰是它能被长期信任的原因。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth长期运行场景的报错和一次性对话不太一样因为错误会被调度器放大。下面按真实报错逐个排查。401 Unauthorized。最常见的原因是 Key 没注入到运行环境。cron 的环境变量和你终端里的不一样你在.bashrc里 export 的 Keycron 根本读不到。解决方法是把环境变量写进 cron 任务本身或者用一个 wrapper 脚本先 source 再执行。检查命令env | grep TAOTOKEN如果 cron 里为空就在 crontab 顶部加TAOTOKEN_API_KEYsk-你的Key TAOTOKEN_BASE_URLhttps://taotoken.net/apilocal proxy failed。这个报错通常出现在 Base URL 配置错误时。检查 SKILL.md 里的 baseUrl 是不是写成了带路径的完整地址正确值应该是 https://taotoken.net/api 不要多加/v1之类的后缀。另外确认运行环境能正常解析这个域名长期运行的服务器如果 DNS 不稳也会间歇性报这个错。reading choices 相关报错。这类错误一般出现在模型返回格式不符合预期时。长期运行场景下模型偶尔返回非 JSON 内容Skill 解析就会失败。解决方法是在 Skill 里加一层容错如果解析失败记录原始返回并跳过本次判断不要中断整个调度。可以在 scan.py 之外加一个轻量 wrapper捕获解析异常后输出{notify: false, reason: parse failed, skip}保证调度不中断。OAuth 相关报错。如果你用的是 Claude Code 或类似需要 OAuth 的客户端接入报错往往出在认证方式不匹配。这类场景建议直接走 API Key 方式而不是 OAuth。配置三件套时确认Base URL 填 https://taotoken.net/api Key 用控制台创建的 KeyModel ID 填你验证过的型号。三件套缺一不可尤其是 Model ID很多人只填了前两个结果调用时找不到模型。排查顺序建议固定下来先看 Key 是否注入再看 Base URL 是否正确再看 Model ID 是否存在最后看返回格式是否能解析。按这个顺序走90% 的长期运行报错都能定位。排障过程中如果需要重新生成 Key去 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 接入细节看文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。6. 从 scan.py 到常驻任务体把判断权交出去之后改造完成后你手里这个 downloads-reminder 已经不是脚本了。它有三个脚本永远给不了的能力能持续运行、能在无人看管时做判断、能在该沉默的时候保持沉默。我建议你下一步做两件事。第一给它加一个轻量的状态文件记录上次提醒时间避免同一问题在短时间内重复提醒。这个状态文件只写时间戳不碰任何用户文件依然满足只读原则。第二把调度周期从 30 分钟拉长到 2 小时观察一周。长期运行的价值不在频率高而在稳定。频率太高反而会变成打扰。如果你想把这类常驻 Agent 跑得更稳长期编码和 Agent 场景可以直接用 Coding Plan地址 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 它面向的就是这种需要持续调用的任务体。通道固定、Key 统一、Model ID 固定剩下的就是让调度器安静地跑。最后留一个判断标准给你当你发现自己已经好几天没想过这个 Agent但它还在默默帮你盯着 Downloads你就知道它真正跑起来了。这时候它不再是一个高级聊天框而是一个你可以放心交给系统长期运行的任务执行体。