
1. 为什么你的 OpenClaw 心跳总是不触发很多人第一次接触 OpenClaw 的 HEARTBEAT.md 时都会有一个疑问我明明在文件里写了任务为什么 Agent 从来不主动执行我试过把任务写得非常详细结果每次心跳回来只有一句 HEARTBEAT_OK什么也没干。这个问题的根源在于HEARTBEAT.md 在 OpenClaw 的整个上下文注入体系里是一个非常特殊的存在。它不是普通的项目上下文文件而是唯一一个被标记为「动态上下文文件」的 bootstrap 文件。这个标记决定了它被注入 system prompt 的位置、被哪些运行模式加载、以及在什么条件下会被整个跳过。如果你不理解这套机制就会陷入两个极端要么写了一大堆任务但从不执行要么每次心跳都在烧 token 却什么也没检查。这篇内容会从源码结构出发把 HEARTBEAT.md 的加载、注入、触发、过滤、跳过这几条链路拆开讲清楚然后给你一份可以直接复制使用的配置片段最后逐项验证心跳是否真的在工作。适合谁看已经在本地跑 OpenClaw、想让 Agent 定时做巡检邮件、日历、项目状态、数据管道的人以及被心跳不触发、心跳烧 token、心跳结果被上下文污染这三类问题卡住的人。核心检索词就是 HEARTBEAT.md 源码分析与配置指南下面所有步骤都围绕它展开。先说结论HEARTBEAT.md 的本质是一份「巡逻路线图」它只负责列出检查什么、多久检查一次而「怎么执行、什么时候该说话」这些行为规范由 AGENTS.md 的 Heartbeats 章节定义。两者分工不清是心跳失效最常见的原因。2. HEARTBEAT.md 在 OpenClaw 里的加载与注入链路要理解心跳为什么不触发必须先搞清楚 HEARTBEAT.md 从磁盘到 system prompt 的完整路径。OpenClaw 的 workspace 里有一组 bootstrap 文件默认包括 AGENTS.md、SOUL.md、IDENTITY.md、USER.md、TOOLS.md、MEMORY.md以及 HEARTBEAT.md。前六个是静态上下文只有 HEARTBEAT.md 被单独标记为动态。在 workspace 注册阶段源码里定义了默认文件名常量DEFAULT_HEARTBEAT_FILENAME HEARTBEAT.md位置在 workspace.ts 第 31 行附近。真正决定它特殊性的是 system-prompt.ts 第 54 行的那行代码const DYNAMIC_CONTEXT_FILE_BASENAMES new Set([heartbeat.md]);这个 Set 里只有 heartbeat.md 一个成员。也就是说在所有 bootstrap 文件里只有它会被当作「频繁变化」的内容处理。2.1 缓存边界之上与之下OpenClaw 在拼装 system prompt 时会把上下文文件分成两组。system-prompt.ts 第 900 到 940 行附近的逻辑是先过滤出非动态文件再过滤出动态文件然后分别构建两个 section。const stableContextFiles orderedContextFiles.filter( (file) !isDynamicContextFile(file.path) ); const dynamicContextFiles orderedContextFiles.filter( (file) isDynamicContextFile(file.path) ); lines.push(...buildProjectContextSection({ files: stableContextFiles, heading: # Project Context, dynamic: false, })); lines.push(SYSTEM_PROMPT_CACHE_BOUNDARY); lines.push(...buildProjectContextSection({ files: dynamicContextFiles, heading: stableContextFiles.length 0 ? # Dynamic Project Context : # Project Context, dynamic: true, }));静态文件放在# Project Context里位于缓存边界线之上HEARTBEAT.md 放在# Dynamic Project Context里位于缓存边界线之下。AI 最终看到的 system prompt 结构大致是这样# Project Context The following project context files have been loaded: If SOUL.md is present, embody its persona and tone... ## AGENTS.md [内容] ## SOUL.md [内容] ... !-- OPENCLAW_CACHE_BOUNDARY -- # Dynamic Project Context The following frequently-changing project context files are kept below the cache boundary when possible: ## HEARTBEAT.md [HEARTBEAT.md 内容]为什么要把 HEARTBEAT.md 放在缓存边界之下因为心跳任务清单可能每次心跳前都被修改如果它放在边界之上任何一次改动都会让整个 system prompt 的缓存失效成本会非常高。放在边界之下Anthropic 等模型的 prompt caching 可以复用静态部分只重新计算动态部分。这是 OpenClaw 在成本控制上的一个关键设计。2.2 心跳触发时到底发生了什么心跳的默认提示词定义在 auto-reply/heartbeat.ts 第 30 到 31 行附近export const HEARTBEAT_PROMPT Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.; export const DEFAULT_HEARTBEAT_EVERY 30m;整个流程是定时器每 30 分钟触发一次生成上面这条心跳消息AI 读取 HEARTBEAT.md 检查任务列表有事就执行并回复结果没事就回复 HEARTBEAT_OK。注意提示词里那句「Do not infer or repeat old tasks from prior chats」这是为了防止 AI 把历史对话里的旧任务重新翻出来执行。2.3 空内容检测为什么你的心跳被跳过了这是最容易被忽略、也最常导致「心跳不触发」的机制。heartbeat.ts 第 51 到 107 行有一个isHeartbeatContentEffectivelyEmpty函数它会逐行判断 HEARTBEAT.md 是否「实际上是空的」export function isHeartbeatContentEffectivelyEmpty(content) { for (const line of lines) { const trimmed line.trim(); if (!trimmed) continue; if (/^#(\s|$)/.test(trimmed)) continue; // 跳过标题 if (/^[-*]\s*(\[[\sXx]?\]\s*)?$/.test(trimmed)) continue; // 跳过空列表项 if (/^[A-Za-z0-9_-]*$/.test(trimmed)) continue; // 跳过代码围栏 return false; // 有实际内容 } return true; // 全部是空结构 }如果你的 HEARTBEAT.md 只有标题、空列表项、代码围栏系统会判定它「实际上为空」直接跳过这次心跳 API 调用。这个优化能省 token但也会让新手误以为心跳坏了。所以第一件要检查的事就是你的文件里有没有真正的任务行。2.4 心跳过滤与子 Agent 隔离心跳消息在对话历史里会被 heartbeat-filter.ts 处理。心跳响应对user 发 HEARTBEAT_PROMPTassistant 回 HEARTBEAT_OK在发送给 AI 前可能被过滤掉避免上下文被大量 HEARTBEAT_OK 污染。另一个关键点是白名单。源码里的MINIMAL_BOOTSTRAP_ALLOWLIST包含 AGENTS.md、TOOLS.md、SOUL.md、IDENTITY.md、USER.md但不包含 HEARTBEAT.md。同时 bootstrap-files.ts 里有额外过滤function applyContextModeFilter(params) { if (contextMode ! lightweight) return params.files; if (runKind heartbeat) { return params.files.filter((file) file.name HEARTBEAT.md); } return []; }含义很明确普通主会话会注入 HEARTBEAT.md心跳 turn 是 lightweight 模式只注入 HEARTBEAT.md子 agent 和 cron 不注入 HEARTBEAT.md。所以如果你指望子 agent 去执行心跳任务那是不会发生的。3. 可复制的 HEARTBEAT.md 配置片段与心跳参数理解了链路之后配置就有的放矢了。下面这份 HEARTBEAT.md 可以直接复制到你的 workspace 根目录按需改任务名和间隔。# HEARTBEAT.md ## Periodic Checks - email-check: Check for urgent unread emails (every 4h) - calendar-check: Check next 24-48h events (every 8h) - weather-check: Check weather if human might go out (twice daily) ## Conditional Tasks - project-monitor: Check git status of active projects (daily) ->agents: defaults: heartbeat: every: 30m # 心跳间隔默认 30 分钟 prompt: Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK. ackMaxChars: 300 # 心跳响应最大字符数every支持30m、1h、2h这类写法。ackMaxChars限制心跳回复的长度防止 AI 在心跳里写长篇大论。如果你想让心跳更频繁比如做数据管道监控可以改成10m但要留意 token 消耗。3.2 任务优先级标记当任务变多时用优先级标记帮 AI 排序- [URGENT] email-check: Check urgent unread emails (every 2h) - [NORMAL] calendar-check: Check upcoming events (every 6h) - [LOW] memory-review: Review and consolidate memories (weekly)3.3 跨任务状态追踪AGENTS.md 里建议用memory/heartbeat-state.json记录上次检查时间。你可以在 HEARTBEAT.md 里显式引用它## State Tracking - Use memory/heartbeat-state.json to track last check times - Update the state file after each check completes这样 AI 在心跳时能读到上次检查时间避免重复执行或漏执行。3.4 与 AGENTS.md 的分工HEARTBEAT.md 只列任务行为规范放 AGENTS.md。AGENTS.md 的 Heartbeats 章节可以这样写## Heartbeats - Be Proactive! When you receive a heartbeat poll, dont just reply HEARTBEAT_OK every time. - Things to check (rotate through these, 2-4 times per day): - Emails - Any urgent unread messages? - Calendar - Upcoming events in next 24-48h? - Weather - Relevant if your human might go out? - Track your checks in memory/heartbeat-state.json分工清楚了心跳才会既不过度打扰也不漏掉重要检查。4. 验证心跳是否真的在工作配置写完不代表心跳就在跑。下面这套验证动作可以逐项确认。4.1 确认文件非空先确认 HEARTBEAT.md 不会被空内容检测跳过。在 workspace 根目录执行grep -vE ^\s*(#|[-*]\s*(\[[\sXx]?\]\s*)?$|[A-Za-z0-9_-]*$)?\s*$ HEARTBEAT.md如果这条命令有输出说明文件里有实际任务行不会被跳过。如果没有任何输出说明你的文件全是空结构心跳会被优化掉。4.2 确认注入位置启动 OpenClaw 后查看 system prompt 的拼装结果。你可以在日志里搜索Dynamic Project Context和OPENCLAW_CACHE_BOUNDARYgrep -n Dynamic Project Context\|OPENCLAW_CACHE_BOUNDARY openclaw.log正常情况下OPENCLAW_CACHE_BOUNDARY应该出现在# Project Context之后、# Dynamic Project Context之前。如果 HEARTBEAT.md 出现在边界之上说明动态文件标记没生效需要检查版本。4.3 手动触发一次心跳把every临时改成1m等一分钟看日志里有没有心跳消息grep -n HEARTBEAT_PROMPT\|HEARTBEAT_OK openclaw.log如果看到 HEARTBEAT_PROMPT 发出但回复是 HEARTBEAT_OK说明心跳在跑但任务没到期。如果连 HEARTBEAT_PROMPT 都没有说明定时器没触发检查every配置是否被覆盖。4.4 确认任务真的被执行在 HEARTBEAT.md 里放一条一定会触发的任务比如- test-check: Reply with the current timestamp (every 1m)等一分钟后看日志如果 AI 回复了时间戳而不是 HEARTBEAT_OK说明任务执行链路是通的。验证完记得删掉这条测试任务。4.5 确认子 Agent 不执行心跳如果你有子 agent 或 cron 任务确认它们没有加载 HEARTBEAT.md。在子 agent 的日志里搜索HEARTBEAT.md正常情况下不应该出现。如果出现了说明 lightweight 模式过滤没生效。5. 心跳失效的常见报错与排查心跳出问题时报错往往不在 HEARTBEAT.md 本身而在上下文注入或模型调用环节。下面按真实报错逐项排查。5.1 401 Unauthorized心跳触发时如果返回 401说明模型调用的凭证有问题。检查你的 API Key 是否配置正确以及 Base URL 是否指向正确的端点。如果你用的是 TaoToken 这类聚合入口确认 Key 和 Base URL 是配套的。排查命令curl -s -o /dev/null -w %{http_code} \ -H Authorization: Bearer $API_KEY \ https://taotoken.net/api/v1/models返回 200 说明 Key 有效返回 401 说明 Key 或 Base URL 有问题。5.2 local proxy failed这个报错通常出现在本地代理层。心跳请求经过本地代理时如果代理进程没起来或端口不对就会报 local proxy failed。检查代理进程是否在运行以及配置里的端口是否和实际监听端口一致。注意不要使用任何不合规的网络工具这里指的是 OpenClaw 自身的本地转发组件。5.3 reading choices 报错如果日志里出现reading choices相关的错误通常是模型返回结构不符合预期。心跳请求的响应被解析时如果 choices 字段缺失或格式不对就会报这个错。检查你的模型 ID 是否正确以及ackMaxChars是否设置得过小导致响应被截断。5.4 OAuth 相关报错如果你用的是需要 OAuth 的模型服务心跳触发时可能因为 token 过期报 OAuth 错误。检查 OAuth token 的刷新逻辑确保心跳触发前 token 是有效的。这类问题在长时间运行的心跳场景里比较常见。5.5 心跳不触发但无报错这是最隐蔽的情况。没有报错但心跳就是不跑。按顺序检查HEARTBEAT.md 是否被判定为空用 4.1 的命令验证every配置是否被上层覆盖当前运行模式是否是 heartbeat子 agent 和 cron 不会跑心跳定时器进程是否存活。5.6 心跳烧 token 过多如果心跳频繁触发且每次都执行任务token 消耗会很快。优化方向把不紧急的任务间隔调长用空内容检测跳过无任务的心跳设置ackMaxChars限制回复长度把一次性任务从 HEARTBEAT.md 移到对话里直接说。5.7 心跳结果污染上下文如果发现对话历史里堆满了 HEARTBEAT_OK说明心跳过滤没生效。检查 heartbeat-filter.ts 相关配置确认心跳响应对在发送给 AI 前被过滤。这个机制默认是开启的如果被关掉需要重新打开。6. 把心跳接入你的日常巡检流程HEARTBEAT.md 配好之后下一步是让它真正融入你的工作流。如果你还没有可用的模型入口可以先在 TaoToken 上创建一个 API Key然后把它配置到 OpenClaw 的模型设置里。创建入口在 API Keys接入细节可以参考 接入文档。配置时三件套要写全Base URL 填https://taotoken.net/apiKey 填你创建的 API KeyModel ID 填你要用的模型。这三项缺一不可尤其是 Model ID写错了会直接报 reading choices 错误。如果你想让心跳任务跑得更稳定建议先用 模型对话 手动测一次 HEARTBEAT_PROMPT确认模型能正确读取文件并返回结果再交给定时器自动跑。长期做编码和 Agent 巡检的话Coding Plan 会更适合高频心跳场景。最后给一个实用技巧把 HEARTBEAT.md 当成一份会进化的清单。每周花五分钟回顾一下删掉已经不需要的临时任务把反复出现的检查项固化成周期任务把不再重要的任务降级或移除。心跳机制的价值不在于任务多而在于每一条任务都真的有人在看结果。