LobeHub Agent Goals 设计解析:如何基于 Task 系统构建可持久化的 7×24 目标闭环
导读
在 LobeHub 中,"Agent 持续工作"(7×24 运行)到底意味着什么?既不是一条永远挂着的聊天会话,也不是常驻内存的守护进程,而应当是一套以数据库为最终权威、可随时从持久化状态恢复的目标驱动闭环。本文以 docs/development/agent-goals-design.md 为骨架,完整讲解 Agent Goals 的领域模型、运行时架构、决策状态机、预算与安全、租约与幂等规则以及 MVP 分阶段落地路径,并结合当前仓库中 goals 表结构、goalGraph 相关 schema 与测试及 goal-graph CLI 设计等源码证据做纵深佐证。读完你将掌握:目标(Goal)为什么不能退化成任务(Task)聚合、控制器回合(controller turn)如何做到"有限但可持续"、以及如何在任务树之上叠加验证、预算、唤醒与租约语义。
一、总体设计:Goal 是 Task 之上的"持久契约层"
1.1 核心论断
设计文档开门见山给出判断:Agent Goal 既不应是另一种聊天会话,也不应是一个永远活着的进程。它的正确形态是一个 durable control loop(可持久化控制循环),执行计划由现有的任务树(task tree)承载。文档给出的层级关系如下:
Agent
-> Goal (intent, success contract, policy, budget, lifecycle)
-> root Task (current plan)
-> subtasks + dependencies (execution graph)
-> Task Topics (individual attempts)
-> Agent Operations (runtime/cost/trace)
这一结构的分工非常清晰:
- Agent 是目标的所有者与执行者;
- Goal 是"长期有效的结果契约"(long-lived outcome contract);
- Tasks 是"可变更的执行计划"(mutable execution plan);
- Topics 是"有限的单次尝试"(finite execution attempts);
- 任何 worker 或 LLM 调用都不在两次尝试之间常驻。
设计的关键意图是"复用任务系统最强的部分,而不是让每个 Task 都变成永久性的 Agent Goal"。这与仓库当前实际落地方向一致:查看 goals 表 schema 注释可知,goal 被定位为"独立的目标实体(independent target entity)",拥有自己的定义(title/requirement)、预算与生命周期状态,并且不绑定在单个任务上——其执行载体是可选的多态链接(
subject_type/subject_id),同一个表可承载 task 驱动的 goal、会话中直接声明的 goal 或独立 goal 声明。
1.2 为什么仍然需要一等公民的 Goal
任务系统本身已经提供了非常强的执行能力,文档明确罗列了这些已有能力:
- 被指派的 Agent(assignee agents);
- 无界的任务树与依赖边(task trees and dependency edges);
- 心跳与 cron 自动化(heartbeat and cron automation);
- 每次运行的历史 topic 与交接摘要(topic-per-run history and handoff summaries);
- 操作关联、成本数据、文档、评论与简报(operation linkage, cost data, documents, comments, briefs);
- 人工检查点与暂停/恢复状态(human checkpoints and pause/resume states);
- 基于 Verify 的交付验收(verify-backed acceptance)。
既然任务已这么强,为什么任务只能当"执行基座"而不能直接当"目标聚合根"?文档给出了属于持久目标、但不应归属于某个计划节点的语义清单:
- 跨越重规划的不可变目标(invariant objective that survives replanning);
- 显式、可被机器求值的成功契约(explicit machine-evaluable success contract);
- 与任务执行状态解耦的控制器生命周期(controller lifecycle);
- 来自时间与外部事件的唤醒策略(wake-up policies);
- 全局的成本/时间/迭代/并发预算(global budgets);
- 一份"控制器为什么继续、等待、重规划或停止"的记录(controller rationale audit);
- Agent 级别的所有权,包括当前激活了哪些目标。
文档还点出了问题导向:如果把这些语义全部塞进 tasks.config,后果是——根任务被过度加载(overloaded)、按 Agent 检索目标的能力很弱(weak goal discovery)、状态迁移变得含糊(ambiguous status transitions)、且未来的重规划无法干净地替换根计划。
二、领域模型设计
2.1 agent_goals 表设计(设计文档建议)
文档建议为目标聚合新增一张表,并给出了完整的字段设计:
| Field | Purpose |
|---|---|
id |
Stable goal identity(稳定目标标识) |
agentId |
Agent that owns and pursues the goal(拥有并追求该目标的 Agent) |
userId, workspaceId, visibility |
沿用现有所有权与工作区规则 |
rootTaskId |
当前根任务/计划入口 |
title, objective |
人可读的结果描述;run 激活期间 objective 不可变 |
successCriteria |
结构化的 Verify 标准与评估器配置 |
status |
draft, active, waiting, paused, achieved, blocked, canceled, failed |
policy |
重规划节奏、重试/熔断、允许的触发器、并发度 |
budget |
最大 USD、激活运行时长、轮数、操作数、可选截止时间 |
progress |
缓存的摘要、评分、最近决策与计数器(供低成本 UI 读取) |
nextWakeAt |
持久化的基于时间的唤醒提示 |
leaseOwner, leaseExpiresAt |
单一控制器的执行租约 |
startedAt, completedAt, timestamps |
生命周期审计 |
文档同时给出两条落地约定:
- 草稿创建阶段
rootTaskId允许为 NULL; - 不要把
goalId只放进 task 的 JSONB 里,而应新增可空的、带索引的外键tasks.goalId,这样每个计划节点与操作都能被低成本地查询;根任务就是满足id === goal.rootTaskId的那个节点。
并发模型上:一个 Agent 可以拥有多个 Goal,但 MVP 阶段默认一个 Agent 只运行一个激活 Goal;后续支持多激活 Goal 属于"调度策略决策",不需要推翻 schema。
2.2 agent_goal_runs:只追加的控制器决策流
每个控制器决策都应成为一条 append-only run(与 task topic 相区分),字段设计如下:
| Field | Purpose |
|---|---|
goalId, seq |
有序的控制器历史与幂等边界 |
triggerType, triggerId |
created、timer、signal、task_completed、verify_settled、manual |
status |
queued、running、waiting、completed、failed、canceled |
decision |
execute、decompose、replan、verify、wait、achieve、pause、block |
reason |
人可读的控制器决策理由 |
inputSnapshot, output |
可复现的状态与已选动作 |
operationId |
当规划需要 LLM 时,所使用的 Agent Operation |
| timestamps | 延迟与审计 |
文档特别强调幂等:对持久触发器身份加唯一约束,例如 (goalId, triggerType, triggerId),从而保证 at-least-once 的队列投递不会产生两轮重复决策。
2.3 可选的 agent_goal_events
文档明确给出"第一个 migration 不要加这张表"的建议,除非产品需要用户可见的、且无法重建的活动流。Agent Signal 的 traces 加上 agent_goal_runs 已能覆盖最初的审计需求——这正是"表能不加就不加,用已有可推导数据重建"的务实取舍。
2.4 与仓库当前实现的对照
需要特别说明:设计文档描述的是目标架构(agent_goals 聚合 + rootTaskId),而当前仓库中 goals 表已经以"多态载体"的形态先行落地。查看 packages/database/src/schemas/goal.ts,真实表 goals 的字段包括:
- 归属字段:
userId、workspaceId、agentId、projectId(冗余存储以便列表查询与访问控制); - 目标定义:
title(非空)、requirement("什么算完成"的验收需求来源)、config(GoalConfig类型的 jsonb,运行时自动恢复与有界 Work 执行的策略); - 外层预算:
maxRounds(round 预算,null 表示不设上限)、maxTotalCost(跨所有 rounds 的总 USD 预算,null 表示不设上限); - 生命周期:
status,默认值为planning,状态枚举来自@lobechat/const/goal的goalStatuses; - 多态执行载体:
subjectType('task' | 'topic' | 'standalone' | null,枚举来自goalSubjectTypes)+subjectId,刻意不加外键,因为载体可能位于不同表; - 审计时间列、回收站软删除列(schemas/trash.ts 机制),以及 userId/workspaceId/agentId/projectId/status/subject 六组索引。
从源码注释可以明确读到仓库的设计立场:"一切执行相关的东西(rounds、cost、duration、验收检查)都留在载体及其既有表上"——这与设计文档"任务系统承担执行基座"的思想一脉相承,只是把"执行计划入口"从强约束的 rootTaskId 泛化成了可空的多态引用。仓库中同时存在的 goalGraph.ts、goalTrace.ts 与 goalGraph 测试,以及配套的 const/goal.ts,说明 goal 相关的图结构与 CLI 已经具备可运行原型。
三、运行时架构:有限控制器回合,而非常驻进程
3.1 "7×24" 的真正含义
文档做了一个重要澄清:"7x24 running"指的是目标随时可以从持久状态恢复,绝不意味着一条敞开的 HTTP 响应、一个永无止境的 LLM 循环、或一个纯内存的定时器。
每一个控制器回合(controller turn)都是有限的,其流程为:
wake event
-> acquire goal lease
-> load goal + task graph + latest handoffs + verify state + budgets
-> choose exactly one bounded decision
-> persist decision and enqueue/execute bounded actions
-> set nextWakeAt or wait for an event
-> release lease
执行载体上,文档建议:生产环境应使用持久化队列/工作流(durable queue/workflow);本地开发模式可以使用进程内调度以方便开发者,但必须明确文档化为"跨重启不持久"。
3.2 控制器是"窄输出"而非通用聊天 Agent
控制器不应是又一个通用对话 Agent,文档建议给它一个窄的输出 schema:
type GoalDecision =
| { type: 'execute'; taskIds: string[] }
| { type: 'decompose'; parentTaskId: string; tasks: ProposedTask[] }
| { type: 'replan'; mutations: TaskGraphMutation[]; reason: string }
| { type: 'verify'; subjectTaskId: string }
| { type: 'wait'; reason: string; wakeAt?: string }
| { type: 'achieve'; evidence: EvidenceRef[] }
| { type: 'pause'; reason: string }
| { type: 'block'; reason: string; requestedInput?: string };
约束规则:
- 一个回合可以并行启动一组已就绪的任务,但要受目标并发策略约束;
- 控制器绝不能递归调用自己;
- 任务完成事件负责入队下一个控制器回合。
3.3 唤醒来源:Agent Signal 作为事件路由层
文档建议用 Agent Signal 做事件解释与路由层:
task topic completed / verify settled / document changed / connector event / timer
-> source event
-> signal.goal.wake-requested
-> action.goal.enqueue-turn
-> durable goal workflow
职责划分的关键原则:
- Agent Signal 负责判断事件是否重要并去重(dedupe);
- Goal 控制器负责目标状态迁移与任务计划变更;
- 这样可以避免把每一个 connector 或 task hook 都耦合到目标编排上。
对于时间类唤醒,可以"概念上复用任务调度器",但应当暴露通用的持久 API scheduleGoalWake,而不是把每次唤醒伪装成一次心跳 task topic。延迟消息是一次性的——处理完后由控制器决定是否还需要下一次唤醒。这与 goal-graph CLI 的 tick 语义完全同构:goal tick 只推进一个确定性迁移,goal run 反复调用 tick,遇到人工门禁、预算耗尽或无进展边界即停止。
3.4 任务执行与反馈闭环
现有 TaskRunnerService 仍是启动任务工作的唯一路径。Goal 控制器只负责"选择就绪任务",自己不直接执行工具。当一个 topic 完成时,按以下顺序推进:
- Task 生命周期照旧持久化 status、handoff、brief 与 verify 关联;
- 一条 source event 记录该持久事实;
- 若该任务带
goalId,Goal 策略将其映射为一次唤醒请求; - 控制器重新加载权威 DB 状态并决定下一步。
这套设计让生命周期钩子保持"纯观察性"(observational),从而使重试变得安全。
四、成功、重规划与停止
4.1 成功是被验证出来的,不是自封的
控制器可以提议 achieve,但目标只有在其结构化成功标准通过之后才能进入 achieved。文档明确要求"复用 Verify 作为验收平面(acceptance plane)";证据必须引用持久工件、任务输出、connector 对象或检查结果,而不能只靠一段 LLM 叙述。
文档给出了经典的双层循环结构:
inner loop: one task attempt -> verify/repair that delivery
outer loop: goal progress review -> execute more tasks or replan the task graph
并且点名了既有的 feat/goal-loop-server 探索分支的价值:"一次失败的 verify 结果可以派生一个新 topic,把 handoff 与失败检查项向前携带,并在轮数/成本预算处停止"——这是非常有用的内层原语。Agent 级目标控制器应当做的是把外层循环泛化,而不是在每个任务里复制这套逻辑。
4.2 waiting、blocked、paused 三者必须区分
这是容易混淆的状态语义,文档给出精确定义:
waiting:健康状态,正在等待一个已知事件或未来时间,可自动恢复;blocked:Agent 在缺少新授权/输入的情况下无法取得有意义进展,除非阻塞条件变化,否则绝不自动恢复;paused:被用户、策略、熔断或预算显式停止,必须显式 resume 或策略变更才能恢复;failed:重试策略耗尽后仍发生的不可恢复控制器/系统故障。
文档特别提出了一条经验法则:目标不应因一次薄弱尝试就进入 blocked。应持久化"重复条件指纹"(repeated-condition fingerprint),只有当同一条件持续触发到配置的控制器回合数时才真正 block。
4.3 预算与安全:硬性服务端限额
每个激活目标都必须有服务端强制执行的硬限制:
- 总成本与每日成本(total cost and per-day cost);
- 最大控制器回合数与最大任务尝试数;
- 最大并发 task topics;
- 可选的截止时间与活跃小时窗口(active-hours window);
- 连续运行时故障熔断(consecutive runtime failure fuse);
- 允许的 tool/plugin 策略与人工审批策略。
两条执行纪律值得注意:
- 预算检查既要发生在入队前,也要发生在真正执行前——因为数据库才是权威(queued work 可能已经过期)。
- 绝不承诺字面上的不间断执行:计费失败、凭据被吊销、connector 中断、审批要求、基础设施事故,都必须迁移到"可见状态 + brief",而不是无限空转。
这一思想与 goal-graph CLI 的场景完全对应:
lh goal set-budget <goal-id> --max-rounds 20 --max-cost 10后,预算耗尽会暂停协调,但lh goal resume+lh goal run即可恢复。
五、租约与幂等规则
文档假定投递语义为 at-least-once,因此必须自己保证幂等,规则如下:
- 在创建 run 之前,先获取以
goalId为键的短时 DB/Redis 租约; - 用唯一触发器键插入 run;
- 获取租约后重新读取目标状态与预算;
- 使用稳定的动作幂等键,例如
goal:{goalId}:run:{seq}:task:{taskId}:execute; - 任务执行仍依赖其既有的 in-flight topic 冲突防护;
- 发布延迟唤醒之前就持久化
nextWakeAt;若支持取消,则保存返回的队列消息 id; - 过期唤醒是无害的:当 status、generation 或
nextWakeAt不再匹配时,处理器直接退出。
文档还建议:如果"编辑/恢复"需要廉价地作废所有排队工作,就给目标加一个 generation 整数。
这套规则与 CLI 的"可安全重入"设计互为印证:goal run 在重启后可以再次安全调用,因为生命周期状态存在 PostgreSQL 里,而不是 CLI 进程里。
六、MVP 分阶段落地路线
Phase 0:先对齐既有的任务级 goal loop
在引入聚合之前,先完成/复用既有的 feat/goal-loop-server 工作:
TaskGoalConfig(放在tasks.config.goal);- verify 失败后延续到新的 task topic;
- round 与 USD 预算;
- 失败检查与 handoff 的携带传递;
- goal 工具卡片的状态投影(tool-card state projection)。
注意它的定位:这只是一个"任务级执行原语",不是最终的 Agent-Goal 模型。
Phase 1:一等聚合 + 手动控制器
- 新增
agent_goals、agent_goal_runs以及带索引的tasks.goalId; - 新增 Goal 的 model/service 以及 CRUD/状态迁移;
- 原子地创建 Goal 及其根任务;
- 实现纯决策校验的"手动调用控制器回合";
- 支持一个 Agent 一个激活 Goal,并设置小的并发上限;
- 复用 TaskRunner 与 Verify,暂不加事件驱动唤醒。
文档评价:这一阶段证明了语义与 UI 的价值,"但不宣称 7×24 持久性"。
Phase 2:持久的时间与生命周期唤醒
- 新增持久 goal workflow 与租约/幂等;
- 发射 task-completion 与 verify-settled source events;
- 新增 Agent Signal 的 goal 唤醒策略与一次性 timer 唤醒;
- 新增重启恢复扫描:找出
nextWakeAt已过期且无有效租约的激活目标; - 补齐可观测性:队列延迟、控制器延迟、决策计数、预算使用、卡死目标。
文档评价:从这一阶段起,系统才可以在队列模式下诚实宣称无人值守的持续追求。
Phase 3:外部信号与重规划
- 允许 connector/domain 事件唤醒选定的目标;
- 新增受约束的任务图变更操作(task-graph mutation);
- 新增证据感知的进度评估与重规划提示词;
- 支持一个 Agent 多个目标,并引入显式的优先级/公平性策略。
七、建议的代码边界
文档给出了清晰的模块划分建议,核心原则是控制器绝不放进 TaskLifecycleService——生命周期完成回调必须保持有界且健壮,只负责"发射/入队然后返回"。
packages/database/src/schemas/agentGoal.ts
packages/database/src/schemas/models/agentGoal.ts
packages/database/src/models/agentGoalRun.ts
packages/types/src/agentGoal/
apps/server/src/services/agentGoal/
AgentGoalService.ts # CRUD and lifecycle invariants
GoalController.ts # bounded decide/apply turn
GoalContextBuilder.ts # goal + task graph + evidence snapshot
GoalBudgetService.ts
GoalLeaseService.ts
decisions.ts # schema and validation
apps/server/src/workflows/agentGoal/
apps/server/src/services/agentSignal/policies/goalWake/
apps/server/src/routers/lambda/agentGoal.ts
对照仓库现状,这部分"建议路径"与真实的代码组织方式略有差异——例如当前 goals 的 schema 与类型分别落在 packages/database/src/schemas/goal.ts 和 packages/types/src/goal.ts,Graph/Trace 相关 schema 也在 schemas 目录下与 goal.ts 平级。可以推断:正式落地时会把"设计文档中的聚合模型"进一步收敛到与既有 goal/Graph/Trace schema 共存的命名与边界中,且 packages/database/src/schemas/goalGraph.ts、goalTrace.ts 已被 tests/goalGraph.test.ts 覆盖,说明 goal 图结构的持久化路径已经可测试、可运行。
八、MVP 验收标准(可直接用作测试清单)
文档定义首个持久化版本"完成"的十项标准,可直接转化为验收测试:
- 创建 Goal 会原子地创建 agent 拥有的 Goal 与根任务;
- 一次控制器回合能把根任务分解成任务依赖,且只启动就绪任务;
- 重复唤醒投递不能产生重复 run 或重复 task topics;
- 队列模式下,服务重启不会丢失激活目标的未来唤醒;
- task/verify 完成能在没有前台客户端连接的情况下唤醒目标;
- 验证通过是进入
achieved的唯一自动路径; - 成本、尝试次数、并发、暂停与取消限制在服务端强制执行;
waiting、blocked、预算暂停、运行失败与achieved状态在 DB 与 UI 中可区分;- 每个控制器决策都可被审计:含触发器、理由、任务变更、operation 与成本;
- 本地模式明确提示:定时器仅用于开发、不具备持久性。
其中第 6 条与文档"成功是被验证出来的"原则首尾呼应,是最关键的一条产品语义;第 10 条则对应 3.1 中"本地模式必须文档化非持久"的要求。
九、产品形态:用户看到一张"Current Goal"卡片
最终面向 Agent 的主界面应该呈现一张 "Current goal" 卡片,包含:
- 目标与已验证进度(objective and verified progress);
- 当前计划(即现有任务树);
- 当前状态与下一个预期唤醒时刻;
- 花费/轮数/截止预算;
- 最近的控制器决策理由;
- pause、resume、编辑预算、cancel 控制按钮。
文档最后点出设计哲学:用户不需要理解控制器回合。他们只需要看到:Agent 正在追求什么、现在在做什么、在等什么,以及"什么证据才算完成"。这一产品表达在 goal-graph CLI 的迭代式操作中已有雏形——lh goal graph <goal-id> 看结构、lh goal decisions <goal-id> 看历史决策、lh goal decide 处理人工门禁,整套操作都以 PostgreSQL 中的持久生命周期为底,天然支持重启后继续。
结语
Agent Goals 设计文档回答了 LobeHub"让 Agent 7×24 干活"这一命题的架构本质:把不可变的目标契约、可变更的任务计划与有限的执行尝试三层解耦,用"持久租约 + 唯一触发器键 + 服务端预算"换取安全的重试与恢复,用"Verify 是唯一通往 achieved 的路径"守住结果可信度。仓库中已经存在的 goals 表与 goalGraph/goalTrace 体系,说明这一方向正在从设计走向实现——读者若想深入,可以继续阅读 goal 常量定义、goal 类型定义以及 goal-graph CLI 原型文档,从"状态枚举"与"CLI 生命周期"两侧补全对整套目标系统的理解。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00