在前面章节中,我们已经实现了用户核心特质的自动化提取、全生命周期记忆管理与智能体基础自进化能力,完整搭建起"记忆提取-动态更新-持久化存储-上下文注入"的闭环体系。但这套体系始终依赖用户主动发起对话触发,智能体长期处于被动响应状态,无法实现无人工干预的主动记忆运维、任务巡检与状态感知。
而心跳模式,正是打破这一局限的核心设计,它是一套内置的周期性主动唤醒机制,默认以30分钟为间隔自动唤醒智能体,在主会话上下文中读取预设任务清单执行巡检,无待办事项时返回HEARTBEAT_OK静默收尾,全程不打扰用户,这是智能体从被动工具转向主动助手的核心基础。
本节将基于心跳模式的核心设计思想,实现智能体的定时触发能力,通过固定周期的心跳轮次,完成记忆定期整合、衰减清理、任务自动化巡检等操作,补全智能体主动运维的核心能力。
8.1.1 基础的心跳模块规范Schema
对于心跳模块的设计,我们首先需要制定基础的心跳文件模块格式,核心是通过标准化的配置文件实现定时/周期任务的统一调度管理,支撑记忆更新、会话归档等自动化后台能力,保障智能体长期运行的稳定性与个性化能力的持续迭代,以下为完整的设计规范、配置示例与执行逻辑说明。
1. 核心配置文件规范
在具体的模块使用上,需要采用单JSON文件作为任务注册与调度规则的统一配置载体,文件名为heartbeat.json,通过结构化配置实现全局调度规则管控与单任务的精细化配置,兼顾通用性与灵活性。下面是作者实现的一个示例:
{
"config": {
"interval_minutes": 30,
"active_hours": {"start": "09:00", "end": "22:00"},
"timezone": "Asia/Shanghai",
"model": "glm",
"default_timeout_seconds": 300,
"max_concurrent_tasks": 2
},
"tasks": [
{
"id": "memory_update",
"name": "记忆更新",
"description": "周期性提取对话中的用户画像、偏好与核心需求,更新长期记忆文件",
"enabled": true,
"interval_minutes": 60,
"last_run": null,
"timeout_seconds": 300,
"max_retry": 2,
"retry_interval_seconds": 60,
"prompt": "检查最近对话记录,提取用户画像、偏好、需求等关键信息并更新记忆文件"
},
{
"id": "daily_summary",
"name": "每日摘要",
"description": "每日固定时间整理当日全量对话内容,生成归档级简洁摘要",
"enabled": true,
"interval_minutes": 0,
"schedule": "22:00",
"last_run": null,
"timeout_seconds": 600,
"max_retry": 1,
"retry_interval_seconds": 120,
"prompt": "整理今天的所有对话记录,生成一份简洁的每日摘要"
}
]
}
2. 字段详细说明
拆分全局调度配置与单任务配置两个维度,补充必填规则、默认值与取值约束,明确字段边界与互斥逻辑,避免调度歧义。
1)全局调度配置字段说明(config根节点)如表8-1所示
表8-1 全局调度配置字段说明
|--------------------------------|----|---------------|------------------------------------------|------------------------------------------------------------------------|
| 字段 | 选填 | 默认值 | 说明 | 取值约束 |
| config.interval_minutes | 必填 | - | 调度器默认轮询巡检间隔(分钟),调度器按此间隔遍历任务列表,判断是否触发任务执行 | 正整数,0表示禁用全局轮询(仅支持手动触发任务) |
| config.active_hours | 必填 | - | 心跳模块活跃时段,仅在该时间范围内执行轮询与任务触发,范围外暂停所有调度逻辑 | 包含start和end的对象,值为HH:MM格式的 24 小时制时间,支持跨天配置(如start:"22:00", end:"06:00") |
| config.timezone | 选填 | Asia/Shanghai | 定时任务与时段校验使用的时区,避免跨环境时区偏移导致的调度异常 | IANA标准时区格式 |
| config.model | 必填 | - | 心跳任务默认调用的大语言模型标识 | 需与系统支持的模型名称一致 |
| config.default_timeout_seconds | 选填 | 300 | 单任务默认执行超时时间(秒),防止LLM调用阻塞导致调度卡死 | 正整数 |
| config.max_concurrent_tasks | 选填 | 1 | 最大并发执行任务数,限制同时运行的任务数量,避免系统资源占用过高 | 正整数 |
2)单任务配置字段说明(tasks数组节点)如表8-2所示
表8-2 单任务配置字段说明
|----------------------------|-------|-----|------------------------------------|-------------------------------------------|
| 字段 | 必填/选填 | 默认值 | 说明 | 取值约束 |
| tasks\[\].id | 必填 | - | 任务唯一标识,用于任务区分、执行状态追踪与日志关联 | 字符串,全局唯一,仅支持字母、数字、下画线 |
| tasks\[\].name | 必填 | - | 任务可视化名称,用于日志展示与运维管理 | 字符串,简洁易懂 |
| tasks\[\].description | 选填 | - | 任务功能备注说明,用于配置维护与团队协作 | 字符串 |
| tasks\[\].enabled | 必填 | - | 任务启用开关,仅启用状态的任务会被调度器巡检与触发 | 布尔值,true/false |
| tasks\[\].interval_minutes | 必填 | - | 任务最小执行间隔(分钟),用于周期型任务;与schedule字段互斥 | 非负整数,>0 表示按此间隔周期执行;=0 表示仅按schedule固定时间触发 |
(续表)
|----------------------------------|-------|---------|-----------------------------------------------------------------|-------------------------------|
| 字段 | 必填/选填 | 默认值 | 说明 | 取值约束 |
| tasks\[\].schedule | 条件必填 | - | 固定时间触发规则(HH:MM,24 小时制),用于定时型任务;当interval_minutes=0时为必填项,与周期间隔互斥 | 符合HH:MM格式的时间字符串,单任务仅支持单个定时时间 |
| tasks\[\].last_run | 选填 | null | 任务上次执行完成时间,用于判断下次触发时机,调度器执行后自动更新 | ISO 8601标准格式时间字符串,无执行记录时为null |
| tasks\[\].timeout_seconds | 选填 | 继承全局默认值 | 单任务执行超时时间(秒),优先级高于全局默认配置 | 正整数 |
| tasks\[\].max_retry | 选填 | 0 | 任务执行失败后的最大重试次数 | 非负整数,0 表示不重试 |
| tasks\[\].retry_interval_seconds | 选填 | 60 | 任务失败重试的间隔时间(秒) | 正整数 |
| tasks\[\].prompt | 必填 | - | 任务执行提示词,将完整传递给LLM,定义任务的执行目标与规则 | 字符串,需清晰描述任务要求 |
3. 核心调度执行逻辑
本模块采用"时间轮询+规则匹配"的调度模式,核心执行流程形成完整闭环,具体说明如下。
1)配置加载与合法性校验
调度器启动时,首先加载heartbeat.json配置文件,执行全量合法性校验:校验必填字段是否完整、周期与定时配置是否互斥、时间格式是否合规、任务ID是否全局唯一等;校验失败则直接抛出异常并终止启动,避免无效配置导致的调度混乱。
2)活跃时段管控
调度器仅在config.active_hours定义的活跃时段内运行,非活跃时段暂停轮询巡检,不触发任何任务;支持跨天活跃时段配置(如夜间值守场景),自动完成跨天时间范围的匹配判断。
3)周期巡检与任务匹配
每间隔config.interval_minutes定义的时间,调度器执行一次全量任务巡检,遍历所有enabled=true的任务,按以下规则匹配触发条件:
- 周期型任务(interval_minutes>0):计算当前时间与last_run的时间差,当差值≥设定的执行间隔时触发执行;首次执行(last_run=null)直接触发。
- 定时型任务(interval_minutes=0且schedule非空):判断当前时间是否匹配schedule设定的HH:MM,且last_run非当日日期,满足条件则触发执行,确保单日仅执行一次。
4. 任务执行与状态更新
触发任务后,调度器按max_concurrent_tasks限制控制并发,调用配置的LLM模型,传入任务prompt执行指令;任务执行完成(无论成功/失败)后,自动更新last_run为当前ISO 8601 格式时间,并将执行结果、异常信息写入持久化日志。
5. 失败重试机制
任务执行超时、调用异常时,调度器按max_retry设定的次数执行重试,每次重试间隔retry_interval_seconds;超过最大重试次数后,终止本次任务执行,记录异常告警日志,不影响其他任务的正常调度。
这里作者围绕智能体系统心跳模块,给出了一套完整的标准化设计方案,核心通过heartbeat.json单配置文件实现定时、周期类自动化任务的统一调度管理,可支撑记忆更新、每日会话摘要等智能体后台常态化运行需求。方案优化了原有配置结构,拆分全局调度与单任务配置两大维度,补充了时区、超时控制、失败重试、并发限制等字段,明确了字段必填规则、取值约束与周期/定时配置的互斥逻辑,规避调度歧义。同时规范了从配置校验、活跃时段管控、轮询任务匹配、执行状态更新到异常重试的全闭环调度流程,配套配置热更新、防重复执行、全链路日志追踪等容错机制,还预留了任务依赖编排、结果回调等扩展能力,兼顾了模块运行的稳定性、配置灵活性与功能可扩展性。
8.1.2 基于心跳(定时任务)的主智能体调用
在心跳模式的具体使用方面,我们首先实现基于心跳定时任务的智能体调用方法。该方法的核心是突破智能体仅能由用户主动交互触发的限制,通过预设的周期或定时规则,自动唤醒智能体执行标准化任务。
因此,我们也将其称为基于心跳(定时任务)的智能体调用,一般适用于无须用户实时干预的后台自动化场景,常见场景包括周期性的用户记忆更新、每日会话数据的归档整理、智能体系统的日常运维巡检等。
该模式的标准化执行流程分为4步:
(1)首先是调度器会按照预设的间隔定时唤醒并执行巡检,完成前置校验工作,筛选出本轮符合触发条件的待执行任务。
(2)随后会为筛选出的目标任务创建独立的智能体执行单元,和用户正在使用的主会话智能体实例做完全的隔离,避免后台任务干扰正常的用户交互。
(3)接下来会向这个独立的执行单元注入完整的任务执行规则,包括任务指令、指定调用的模型、超时与重试规则等配置参数。
(4)之后执行单元会调用对应的大模型完成指令要求的任务,同时记录全链路的运行日志;最后会完成执行结果的落地处理,同步更新任务的执行状态,形成完整的执行闭环。
在具体实现上,我们将核心执行流程设置为定时轮询、读取heartbeat.json配置文件、判断任务是否满足执行条件、调用Agent执行任务、最终记录执行结果,代码同时提供了便捷的调用方式,开发者可通过导入HeartbeatManager类完成实例化,调用start方法即可在后台线程中启动阻塞式的心跳循环,调用stop方法即可停止心跳服务。完整代码如下:
import json
import logging
import os
import time
import asyncio
from datetime import datetime
from typing import Any, Dict, List, Optional
初始化日志记录器
logger = logging.getLogger(name)
项目根目录,基于当前文件路径向上两级计算
_PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(file)))
class HeartbeatManager:
"""心跳管理器 --- 定时巡检任务系统。"""
def init(
self,
config_path: str = "initspace/memorys/heartbeat.json",
agent=None,
):
"""
初始化心跳管理器。
参数:
config_path: 心跳配置文件 heartbeat.json 的路径
agent: 用于执行任务的 Agent 实例
"""
处理配置文件路径,优先使用传入的绝对路径,不存在则拼接项目根目录
self.config_path = os.path.abspath(config_path)
if not os.path.exists(self.config_path):
self.config_path = os.path.join(_PROJECT_ROOT, config_path)
保存 Agent 实例,用于后续执行任务
self.agent = agent
心跳停止标识,True 表示已停止/未启动
self._stopped = True
存储从配置文件加载的全局配置
self._config: Dictstr, Any = {}
存储从配置文件加载的任务列表
self._tasks: ListDict\[str, Any] = \[\]
日志文件路径,与配置文件同目录
config_dir = os.path.dirname(self.config_path)
self.log_path = os.path.join(config_dir, "heartbeat_log.json")
HEARTBEAT.md 文档路径
self.heartbeat_md_path = os.path.join(
_PROJECT_ROOT, "initspace", "brain", "HEARTBEAT.md"
)
------------------------------------------------------------------
配置读写
------------------------------------------------------------------
def _load_config(self) -> Dictstr, Any:
"""
加载心跳配置文件。
返回:
包含 config 和 tasks 的配置字典,文件不存在或为空时返回默认配置
"""
if not os.path.exists(self.config_path):
return {"config": {"interval_minutes": 30}, "tasks": \[\]}
with open(self.config_path, "r", encoding="utf-8") as f:
content = f.read().strip()
if not content:
return {"config": {"interval_minutes": 30}, "tasks": \[\]}
return json.loads(content)
def _save_config(self, data: Dictstr, Any) -> None:
"""
保存配置到文件。
参数:
data: 要保存的配置字典
"""
with open(self.config_path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
------------------------------------------------------------------
日志读写
------------------------------------------------------------------
def _load_log(self) -> Dictstr, Any:
"""
加载历史执行日志。
返回:
按日期分组的日志字典
"""
if not os.path.exists(self.log_path):
return {}
with open(self.log_path, "r", encoding="utf-8") as f:
content = f.read().strip()
return json.loads(content) if content else {}
def _append_log(self, task_id: str, task_name: str,
status: str, result: str, duration: float) -> None:
"""
追加一条任务执行日志。
参数:
task_id: 任务唯一标识
task_name: 任务名称
status: 执行状态(success/failed)
result: 执行结果摘要
duration: 执行耗时(秒)
"""
logs = self._load_log()
按日期作为日志分组的键
date_key = datetime.now().strftime("%Y-%m-%d")
构造单条日志记录
record = {
"time": datetime.now().strftime("%H:%M:%S"),
"task_id": task_id,
"task_name": task_name,
"status": status,
"result": result:200, # 结果只保留前200字符
"duration_seconds": round(duration, 2), # 耗时保留两位小数
}
将记录追加到对应日期的日志列表中
logs.setdefault(date_key, \[\]).append(record)
保存更新后的日志
with open(self.log_path, "w", encoding="utf-8") as f:
json.dump(logs, f, ensure_ascii=False, indent=2)
------------------------------------------------------------------
判断任务是否该执行
------------------------------------------------------------------
def _is_in_active_hours(self) -> bool:
"""
检查当前时间是否在活跃时段内。
返回:
True 表示在活跃时段内,可以执行任务
"""
active = self._config.get("active_hours")
未配置活跃时段则默认全天可用
if not active:
return True
将当前时间转换为当天的分钟数,便于比较
now_minutes = datetime.now().hour * 60 + datetime.now().minute
解析配置的开始和结束时间,同样转换为分钟数
start_parts = active.get("start", "00:00").split(":")
end_parts = active.get("end", "23:59").split(":")
start_min = int(start_parts0) * 60 + int(start_parts1)
end_min = int(end_parts0) * 60 + int(end_parts1)
判断当前时间是否在 start_min, end_min 区间内
return start_min <= now_minutes <= end_min
def _should_run(self, task: Dictstr, Any) -> bool:
"""
判断单个任务是否满足执行条件。
参数:
task: 任务配置字典
返回:
True 表示该任务当前应该执行
"""
任务未启用则直接返回 False
if not task.get("enabled", True):
return False
last_run = task.get("last_run")
schedule 模式(定时触发)
schedule = task.get("schedule")
if schedule:
now = datetime.now()
解析目标定时时间,转换为分钟数
parts = schedule.split(":")
target_minutes = int(parts0) * 60 + int(parts1)
now_minutes = now.hour * 60 + now.minute
容差 ±5 分钟,避免因轮询间隔错过整点
if abs(now_minutes - target_minutes) > 5:
return False
同一天只执行一次,通过比较 last_run 的日期判断
if last_run:
try:
last_dt = datetime.fromisoformat(last_run)
if last_dt.date() == now.date():
return False
except (ValueError, TypeError):
时间格式异常时,默认可以执行
pass
return True
interval 模式(间隔触发)
interval = task.get("interval_minutes", 0)
if interval <= 0:
return False
从未执行过(last_run 为空)则直接执行
if not last_run:
return True
try:
计算距离上次执行的时间间隔(分钟)
last_dt = datetime.fromisoformat(last_run)
elapsed = (datetime.now() - last_dt).total_seconds() / 60
return elapsed >= interval
except (ValueError, TypeError):
时间格式异常时,默认可以执行
return True
------------------------------------------------------------------
执行单次任务
------------------------------------------------------------------
def _get_timeout(self, task: Dictstr, Any) -> int:
"""获取任务超时时间(秒),任务未配置则用全局默认值。"""
task_timeout = task.get("timeout_seconds", 0)
if task_timeout > 0:
return task_timeout
return self._config.get("default_timeout_seconds", 300)
def _read_heartbeat_md(self) -> str:
"""读取 HEARTBEAT.md 内容,不存在时返回空字符串。"""
if not os.path.exists(self.heartbeat_md_path):
return ""
with open(self.heartbeat_md_path, "r", encoding="utf-8") as f:
return f.read().strip()
def _run_task(self, task: Dictstr, Any) -> str:
"""
执行单个心跳任务,返回结果摘要。
使用 agent.system_prompt + HEARTBEAT.md 融合后的 system prompt,
通过局部 messages 调用 agent._client.chat(),不碰 agent.messages。
支持工具调用循环、超时和重试。
"""
task_id = task.get("id", "unknown")
task_name = task.get("name", "unknown")
prompt = task.get("prompt", "")
if not prompt:
return "无任务描述,跳过"
if not self.agent:
return "未配置 Agent,跳过"
融合 system prompt:Agent 原有认知 + 心跳任务指令
heartbeat_md = self._read_heartbeat_md()
system_content = self.agent.system_prompt
if heartbeat_md:
system_content = system_content + "\n\n" + heartbeat_md
timeout_seconds = self._get_timeout(task)
max_retry = task.get("max_retry", 0)
retry_interval = task.get("retry_interval_seconds", 60)
async def _execute_once():
"""单次执行:LLM 调用 + 工具循环,使用局部 messages。"""
messages = [
{"role": "system", "content": system_content},
{"role": "user", "content": prompt},
]
for step in range(1, self.agent.max_steps + 1):
response = await self.agent._client.chat(
messages=messages,
tools=self.agent._get_tool_schemas(),
)
content_blocks = response.get("content", \[\])
stop_reason = response.get("stop_reason", "stop")
tool_calls = b for b in content_blocks if b.get("type") == "tool_use"
texts = b.get("text", "") for b in content_blocks if b.get("type") == "text"
full_text = "\n".join(texts)
if stop_reason in ("max_tokens", "length"):
return (full_text or "") + "\n输出被截断"
if full_text:
logger.info("Heartbeat 任务 %s Step %d: %s",
task_id, step, full_text:100)
if not tool_calls:
return full_text or "任务完成"
工具调用 --- 添加 assistant 消息
messages.append({
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": tc"id",
"type": "function",
"function": {
"name": tc"name",
"arguments": json.dumps(tc.get("input", {}), ensure_ascii=False),
},
}
for tc in tool_calls
],
})
执行每个工具,添加 tool 结果消息
for tc in tool_calls:
tool_name = tc"name"
tool_input = tc.get("input", {})
tool_id = tc"id"
logger.info("Heartbeat 任务 %s Step %d 工具: %s",
task_id, step, tool_name)
try:
result = await self.agent._executor.execute(tool_name, tool_input)
result_content = result or "(无输出)"
except Exception as e:
result_content = f"执行失败: {e}"
messages.append({
"role": "tool",
"tool_call_id": tool_id,
"name": tool_name,
"content": result_content,
})
return "达到最大步数限制"
带重试的执行
last_error = ""
for attempt in range(max_retry + 1):
start_time = time.time()
try:
coro = _execute_once()
if timeout_seconds > 0:
result = asyncio.run(asyncio.wait_for(coro, timeout=timeout_seconds))
else:
result = asyncio.run(coro)
duration = time.time() - start_time
self._append_log(task_id, task_name, "success", result, duration)
return result:100
except asyncio.TimeoutError:
duration = time.time() - start_time
last_error = f"超时 ({timeout_seconds}s)"
self._append_log(task_id, task_name, "timeout", last_error, duration)
logger.warning("Heartbeat 任务 %s 第 %d 次超时", task_id, attempt + 1)
except Exception as e:
duration = time.time() - start_time
last_error = f"执行失败: {e}"
self._append_log(task_id, task_name, "failed", last_error, duration)
logger.error("Heartbeat 任务 %s 第 %d 次失败: %s",
task_id, attempt + 1, e)
重试等待
if attempt < max_retry:
logger.info("Heartbeat %s 等待 %ds 后重试 (%d/%d)",
task_id, retry_interval, attempt + 1, max_retry)
time.sleep(retry_interval)
return last_error
------------------------------------------------------------------
单次巡检
------------------------------------------------------------------
def check_and_run(self) -> ListDict\[str, Any]:
"""
执行一次巡检:读取配置 → 逐个判断 → 执行符合条件的任务。
返回:
本次执行的所有任务结果列表
"""
加载最新配置
data = self._load_config()
self._config = data.get("config", {})
self._tasks = data.get("tasks", \[\])
检查是否在活跃时段内
if not self._is_in_active_hours():
logger.info("Heartbeat 当前不在活跃时段,跳过")
return \[\]
results = \[\]
遍历所有任务,逐个判断是否执行
for task in self._tasks:
if self._should_run(task):
logger.info("Heartbeat 执行任务: %s", task.get("name"))
执行任务并获取结果
result = self._run_task(task)
更新任务的上次执行时间为当前 ISO 格式时间
task"last_run" = datetime.now().isoformat()
收集执行结果
results.append({
"id": task.get("id"),
"name": task.get("name"),
"result": result,
})
else:
logger.info("Heartbeat 跳过任务: %s", task.get("name"))
只有当有任务执行时,才保存更新后的配置(含 last_run)
if results:
self._save_config(data)
return results
------------------------------------------------------------------
启动 / 停止
------------------------------------------------------------------
def start(self) -> None:
"""启动心跳循环(阻塞式,应放在后台线程)。"""
self._stopped = False
logger.info("Heartbeat 心跳已启动")
主循环,直到 _stopped 被设为 True
while not self._stopped:
每次循环都重新加载配置,支持热更新
data = self._load_config()
self._config = data.get("config", {})
interval = self._config.get("interval_minutes", 30)
全局间隔为 0 时,表示禁用心跳
if interval <= 0:
logger.info("Heartbeat 心跳已禁用 (interval=0)")
break
try:
执行一次完整巡检
self.check_and_run()
except Exception as e:
捕获巡检过程中的所有异常,避免单轮异常导致整个心跳停止
logger.error("Heartbeat 巡检异常: %s", e)
分段 sleep,便于快速响应 stop 指令
sleep_seconds = interval * 60
for _ in range(sleep_seconds):
if self._stopped:
break
time.sleep(1)
logger.info("Heartbeat 心跳已停止")
def stop(self) -> None:
"""停止心跳循环。"""
self._stopped = True
logger.info("Heartbeat 正在停止心跳...")
------------------------------------------------------------------
任务管理
------------------------------------------------------------------
def add_task(self, task: Dictstr, Any) -> bool:
"""
添加新任务。
参数:
task: 任务配置字典,需包含 id、name、prompt 等字段
返回:
True 表示添加成功,False 表示任务 ID 已存在
"""
data = self._load_config()
tasks = data.get("tasks", \[\])
检查任务 ID 是否重复
task_id = task.get("id", "")
if any(t.get("id") == task_id for t in tasks):
logger.warning("Heartbeat 任务 ID 已存在: %s", task_id)
return False
为新任务设置默认值
task.setdefault("enabled", True)
task.setdefault("last_run", None)
将新任务添加到列表
tasks.append(task)
data"tasks" = tasks
保存更新后的配置
self._save_config(data)
logger.info("Heartbeat 添加任务: %s", task_id)
return True
def remove_task(self, task_id: str) -> bool:
"""
删除任务。
参数:
task_id: 要删除的任务 ID
返回:
True 表示删除成功,False 表示任务 ID 不存在
"""
data = self._load_config()
tasks = data.get("tasks", \[\])
过滤掉要删除的任务
new_tasks = t for t in tasks if t.get("id") != task_id
长度未变说明任务不存在
if len(new_tasks) == len(tasks):
return False
data"tasks" = new_tasks
保存更新后的配置
self._save_config(data)
logger.info("Heartbeat 删除任务: %s", task_id)
return True
def enable_task(self, task_id: str, enabled: bool = True) -> bool:
"""
启用或禁用任务。
参数:
task_id: 任务 ID
enabled: True 表示启用,False 表示禁用
返回:
True 表示操作成功,False 表示任务 ID 不存在
"""
data = self._load_config()
遍历找到目标任务
for task in data.get("tasks", \[\]):
if task.get("id") == task_id:
更新任务的启用状态
task"enabled" = enabled
self._save_config(data)
action = "启用" if enabled else "禁用"
logger.info("Heartbeat %s 任务: %s", action, task_id)
return True
return False
def get_status(self) -> Dictstr, Any:
"""
返回所有任务状态。
返回:
包含全局配置、任务列表和运行状态的字典
"""
data = self._load_config()
config = data.get("config", {})
tasks = data.get("tasks", \[\])
return {
"config": config,
"tasks": [
{
"id": t.get("id"),
"name": t.get("name"),
"enabled": t.get("enabled", True),
"last_run": t.get("last_run"),
}
for t in tasks
],
"running": not self._stopped, # True 表示心跳正在运行
}
这段代码的核心是实现了一个功能完整、可直接落地使用的HeartbeatManager心跳管理器类,封装了心跳定时任务从配置管理、调度判断、任务执行、日志记录到任务全生命周期管理的全部功能,同时配套了完整的测试用例,覆盖了核心功能的验证场景。上述代码首先完成了必要的依赖模块导入,涵盖了json配置处理、日志管理、系统路径解析、时间控制、异步运行、日期时间处理与类型注解相关的基础模块,同时定义了项目根路径的全局变量,为后续各类文件的路径解析与处理提供了统一的基础。
核心的HeartbeatManager类在初始化阶段,会接收配置文件路径与Agent实例两个核心入参,自动处理并校准配置文件的绝对路径,同时完成运行状态标识、配置与任务数据的初始化,预定义了执行日志文件与HEARTBEAT.md文件的存储路径,完成整个心跳服务运行前的基础环境准备。
在类的内部,首先封装了配置与日志的持久化读写能力,其中_load_config方法负责读取指定路径的heartbeat.json配置文件,当文件不存在或内容为空时,会自动返回包含默认轮询间隔的基础配置结构,避免因配置文件异常导致服务无法启动;_save_config方法则会将更新后的配置数据以格式化的方式写入配置文件,保障配置修改的持久化生效。
同时代码配套了完整的日志管理方法,_load_log方法用于读取历史执行日志数据,_append_log方法则会以日期为核心维度,将任务的执行时间、唯一ID、任务名称、执行状态、结果摘要、执行耗时等信息结构化追加写入日志文件,实现了任务执行全链路过程的可追溯与可排查。
上面代码中封装了两个核心的任务触发判断方法,构成了整个调度逻辑的核心,其中_is_in_active_hours方法会读取配置中定义的活跃时段,将起止时间与当前时间统一转换为分钟数进行比对,精准判断当前时间是否在允许执行的活跃时段内,非活跃时段会直接跳过后续的任务执行逻辑,符合预设的调度规则。
_should_run方法则针对单个任务,区分定时触发与间隔触发两种模式做精细化的条件判断,在定时触发模式下,会校验当前时间是否在预设(定时)时间的±5分钟容差范围内,同时通过上次执行时间的日期进行比对,保证同一天内同一任务只会执行一次;在间隔触发模式下,会计算当前时间与任务上次执行时间的时间间隔,判断是否达到预设的执行周期,同时针对任务未启用、上次执行时间格式异常等各类边界情况做了兼容处理,最大程度保障任务触发的准确性与稳定性。
在此基础上,代码实现了任务执行与单次巡检的完整流程封装,_run_task方法封装了单个任务的全流程执行逻辑,会先校验任务执行指令与Agent实例的可用性,通过asyncio.run方法调用Agent的run接口执行任务prompt指令,同时记录任务的执行耗时,根据执行成功或失败的不同状态分别写入执行日志,同时捕获任务执行过程中的全部异常,避免单任务执行失败中断整个心跳调度流程。
check_and_run方法则实现了单次完整巡检的全流程闭环,会先加载最新的配置文件数据,校验当前是否处于活跃时段,随后遍历配置中的所有任务,对满足执行条件的任务发起执行调用,同步更新任务的last_run上次执行时间,最终将更新后的配置数据持久化保存,并返回本次巡检的所有任务执行结果。
上面代码同时实现了完善的服务启停控制与任务全生命周期管理能力。在启停控制方面,start方法会启动阻塞式的心跳主循环,加载配置中的全局轮询间隔,循环执行巡检任务,同时将长周期的休眠时间拆分为秒级的分段休眠,保障stop停止指令可以快速响应,不会出现长时间阻塞无法停止的问题;stop方法则会修改运行状态标识,安全触发心跳主循环的停止。在任务管理方面,代码封装了add_task、remove_task、enable_task三个核心方法,分别支持新增任务、删除指定任务、启用或禁用指定任务;其中新增任务时会自动校验任务ID的唯一性,避免重复ID导致的调度异常,所有任务状态的修改都会同步持久化到配置文件中;同时提供了get_status方法,可返回当前的全局配置、所有任务的基础状态与心跳服务的运行状态,方便开发者进行服务状态监控与日常运维管理。
下面是作者运行的代码测试结果:
if name == 'main':
配置日志级别为 INFO,便于查看测试过程
logging.basicConfig(level=logging.INFO)
print("=" * 60)
print("HeartbeatManager 测试")
print("=" * 60)
初始化心跳管理器
hb = HeartbeatManager(
config_path=os.path.join(_PROJECT_ROOT, "initspace", "memorys", "heartbeat.json"),
)
测试 1:读取状态
print("\n--- 测试 1: 获取状态 ---")
status = hb.get_status()
print(json.dumps(status, ensure_ascii=False, indent=2))
测试 2:添加任务
print("\n--- 测试 2: 添加任务 ---")
ok = hb.add_task({
"id": "test_task",
"name": "测试任务",
"enabled": True,
"interval_minutes": 5,
"schedule": None,
"last_run": None,
"prompt": "这是一个测试心跳任务,请回复 HEARTBEAT_OK",
})
print(f"添加结果: {ok}")
测试 3:获取状态(含新任务)
print("\n--- 测试 3: 获取更新后状态 ---")
status = hb.get_status()
for t in status"tasks":
print(f" {t'id'}: {t'name'} (enabled={t'enabled'}, last_run={t'last_run'})")
测试 4:禁用任务
print("\n--- 测试 4: 禁用测试任务 ---")
ok = hb.enable_task("test_task", enabled=False)
print(f"禁用结果: {ok}")
测试 5:删除任务
print("\n--- 测试 5: 删除测试任务 ---")
ok = hb.remove_task("test_task")
print(f"删除结果: {ok}")
测试 6:活跃时段判断
print("\n--- 测试 6: 活跃时段判断 ---")
hb._load_config()
data = hb._load_config()
hb._config = data.get("config", {})
print(f"当前是否在活跃时段: {hb._is_in_active_hours()}")
测试 7:单次巡检(不执行,只看判断结果)
print("\n--- 测试 7: 任务执行判断 ---")
for task in data.get("tasks", \[\]):
should = hb._should_run(task)
print(f" {task.get('id')}: should_run={should}")
print("\n" + "=" * 60)
print("测试完成")
print("=" * 60)
读者可以自行尝试。


8.1.3 定时任务主导的智能体调用
在前面的设计中,我们实现了一套完整的心跳定时任务调度机制,其核心思路是让心跳管理器与主Agent深度集成---通过复用主Agent的完整运行能力(而非仅借用底层组件),在定时规则下自动执行预设任务。这种设计既避免了重复构建Agent基础设施,又能让后台任务与主对话逻辑共享同一套LLM交互、工具调用能力,确保任务执行的一致性。
具体实现上,HeartbeatManager在初始化时接收主Agent实例,执行任务时直接调用self.agent.run(prompt),让心跳任务本质上成为"无人值守的Agent对话",只需定义好任务指令,即可利用主Agent的全部功能。
注意:本节只实现讲解代码段,具体实现在8.2.2节,读者先了解使用方法。
1. 启动心跳(与AgentV0集成)
在AgentV0 的实现中,我们封装了心跳的启动与停止逻辑,只需简单调用即可让心跳在后台独立运行:
初始化主Agent
agent = AgentV0(model_name="glm")
启动心跳后台线程(内部实例化HeartbeatManager并调用start())
agent.start_heartbeat()
之后可正常进行对话,心跳在后台不阻塞主线程
response = agent.invoke("你好")
停止心跳(可选,程序退出时后台线程会自动终止)
agent.stop_heartbeat()
2. 运行时的完整流程
心跳启动后,主线程与后台daemon线程各司其职,通过配置文件与日志协作,各自流程如图8-1所示。
具体到代码逻辑,后台线程的核心是HeartbeatManager.start()中的主循环:每次先加载最新配置(支持热更新),检查全局轮询间隔是否有效,再调用check_and_run()执行巡检;巡检时先判断当前是否在active_hours活跃时段内,再逐个检查任务是否满足触发条件(_should_run()),对满足条件的任务调用 _run_task()执行;执行时通过self.agent.run(prompt) 完成任务,记录日志并更新任务的last_run时间,最后保存配置文件。

图8-1 主线程与后台daemon线程不同的协助流程
3. 配置定时任务(编辑heartbeat.json)
心跳的所有调度规则与任务定义都通过heartbeat.json管理,完整配置结构如下:
{
"config": {
"interval_minutes": 30, // 全局轮询间隔(分钟),0 表示禁用
"active_hours": {"start": "09:00", "end": "22:00"}, // 活跃时段,范围外不执行
"model": "glm" // (可选)任务使用的模型,默认复用主Agent
},
"tasks": [
{
"id": "memory_update", // 任务唯一标识(全局唯一)
"name": "记忆更新", // 任务可视化名称
"enabled": true, // 是否启用
"interval_minutes": 60, // 间隔触发:距上次执行超过 60 分钟触发
"schedule": null, // 定时触发:与interval二选一,格式 "HH:MM"
"last_run": null, // 上次执行时间(ISO格式,自动更新)
"prompt": "检查最近对话记录,提取用户画像、偏好、需求等关键信息并更新记忆文件"
},
{
"id": "daily_summary",
"name": "每日摘要",
"enabled": true,
"interval_minutes": 0, // 0 表示仅使用定时触发
"schedule": "22:00", // 每天 22:00 触发(±5 分钟容差)
"last_run": null,
"prompt": "整理今天的所有对话记录,生成一份简洁的每日摘要"
}
]
}
配置支持两种触发模式,代码中 _should_run() 会自动判断:
- 间隔触发(interval_minutes):当interval_minutes>0 时启用,距last_run超过设定分钟数即触发(首次运行last_run为空时直接触发)。
- 定时触发(schedule):当interval_minutes=0 且schedule非空时启用,每天在指定时间触发(±5 分钟容差),同时通过日期比对保证同一天只执行一次。
4. 运行时动态管理(通过Agent接口)
除了编辑配置文件,还可以通过Agent封装的心跳接口在运行时动态管理任务,代码如下:
1. 查看心跳状态(包含配置、任务列表、运行状态)
status = agent.heartbeat.get_status()
print("当前心跳状态:", status)
2. 动态添加任务(自动校验ID唯一性)
agent.heartbeat.add_task({
"id": "news_check",
"name": "新闻巡检",
"enabled": True,
"interval_minutes": 120, # 每 2 小时执行一次
"prompt": "搜索今日AI领域重要新闻,生成摘要并记录"
})
3. 启用/禁用任务
agent.heartbeat.enable_task("news_check", enabled=False) # 禁用
agent.heartbeat.enable_task("news_check", enabled=True) # 重新启用
4. 删除任务
agent.heartbeat.remove_task("news_check")
5. 手动停止心跳
agent.stop_heartbeat()
所有动态管理操作都会自动持久化到heartbeat.json,无须手动编辑文件。心跳任务的执行结果会自动记录到heartbeat_log.json,读者可以自行尝试并查看结果。

