--- title: 持久化目标 — 跨多轮锁定,让员工自己跟进 description: MateClaw 的 Goal 系统让数字员工把跨多轮的任务锁成一个目标,自己评估进度、自己续命,直到完成或耗尽预算。 head: - - meta - name: keywords content: Goal,目标管理,Agent,多轮对话,自动评估,auto-followup,持久化,MateClaw --- # 持久化目标 ## 持续执行模式(第一版) 新建目标默认 `persistentExecution=true`,省略预算时 `turnBudget=0`、`llmCallBudget=0` 表示不设累计上限。显式正预算仍生效;已有目标保留旧模式,不会在升级后自动启动。 持续目标由数据库队列和后台 supervisor 跨图片段调度;单次图执行的次数上限只结束当前片段,不结束目标。队列、冷却、重试和过期租约可在服务重启后恢复。仅当清单全部通过且有非空证据时才能完成,Stop、缺少必要输入、审批拒绝和预算耗尽会暂停,需明确 resume。 `GET /api/v1/goals/{id}/execution` 返回独立的调度状态、原因和到期时间;它是最近的调度记录,目标当前状态以 goal API 为准。流中通过 `goal_continuation` 广播调度变化。第一版支持单后端实例的原生 runtime,不保证外部工具副作用恰好一次;恢复先检查已有产物与异步句柄。预算在片段边界检查,不是逐请求的硬费用限制。 > **以前你每轮都要把上下文重复一遍。现在你定一个目标,员工自己跟。** 一次对话里你说"帮我把这个博客部署到 fly.io",员工答完一轮就停了。下一轮你要再问"DNS 配好没?证书呢?测试跑了吗?"——你在替它记目标。 Goal 把这件事翻过来。**你说一次,员工锁住目标,自己每轮自检:还差什么?要不要自己再做一步?** 它不是聊天里的一个新功能。它是员工的一种**状态**。员工头像周围多了一圈光,光填多少就是离完成多远。完成了,光消失。 --- ## 它在视觉上长什么样 不是一个 banner。不是一个 dialog。不是一个独立的标签页。 是 **assistant 头像周围的一圈环**。 | 状态 | 视觉 | 含义 | |------|------|------| | 无目标 | 头像就是头像 | 这条对话没绑目标,跟过去一样 | | 进行中 | 头像 + 橙色环 | 有目标在跟,光填到进度处 | | 评估中 | 头像 + 沙金呼吸光晕 | 后台正在判断这轮答案 | | 已完成 | 头像 + 绿色环(短暂出现) | 目标达成,环随后消失,对话继续 | | 预算耗尽 | 头像 + 红橙色环 | 用完 budget,需要你决定加预算还是放手 | **hover 头像**才显示完整 tooltip — 标题 + 还差什么。不 hover 就不打扰你。这是设计意图。 --- ## 怎么定一个目标 三种方式,按门槛从低到高: ### 方式 1 — 让员工自己定 你只要在第一次描述任务时让员工知道这是个长任务: > 我要做一个完整的项目:把 README 翻译成英文、提 PR、走 review、合并。这跨多轮,**请你用 setGoal 锁定**,每轮自我评估,turnBudget=8,autoFollowup 开启。 员工识别到"长任务"+"明确要求 setGoal"两条信号,会自动调用工具创建目标,title 从对话上下文自动归纳。你只需点开它的回答,看见头像旁边多了一圈光,就知道目标已锁。 ### 方式 2 — 直接命令工具 不想让员工判断,你直接告诉它调哪个工具、传什么参数: > 请立刻调用 setGoal 工具,title="部署博客到 fly.io",turnBudget=10,autoFollowup=true。不要问任何前置确认。 "不要问前置确认"这一句很重要 — 否则员工会先问"代码在哪?域名是什么?" 它的本能就是先澄清。 ### 方式 3 — 通过 API 程序化创建 对自动化、外部脚本,REST 端点直接可用: ``` POST /api/v1/goals { "conversationId": "conv-xxx", "title": "部署博客到 fly.io", "description": "...", "exitCriteria": "DNS+SSL+健康检查+测试通过", "turnBudget": 10, "llmCallBudget": 200, "autoFollowupEnabled": false } ``` > `agentId` / `workspaceId` 由 `conversationId` 在服务端派生,**请求体里不用传**(传了也会被忽略)。完整接口列表见 [API 参考](./api)。 --- ## 一个目标里有什么 最少四样: | 字段 | 含义 | |---|---| | **标题 (title)** | 短句,光环 hover 时显示 | | **描述 (description)** | 完整诉求 | | **退出判据 (exitCriteria)** | LLM 可读的判据,evaluator 按这个打分(比如 "DNS 配好+测试通过") | | **预算 (turnBudget + llmCallBudget)** | 防失控上限 | 可选: - **自动延续 (autoFollowupEnabled)**:开了之后,员工答完一轮如果觉得"还没完成",会自己接着做下一步,不等你催 - **冷却 (followupCooldownSeconds)**:两次自动延续之间至少隔多久 --- ## 它在后台是怎么运转的 每次员工回答完一轮,后台会跑一个评估节点。这个节点: 1. 取员工这一轮的最终回答 + 最近几条消息上下文 2. 调一个轻量 evaluator(建议指向便宜的小模型)问:完成度多少(0~1)?还差什么?该继续还是已完成? 3. 把答案写到 `mate_agent_goal_event` 时间线表里 4. 决定下一步:完成 / 预算耗尽 / 继续 / 自动延续 **关键不变量**:评估发生在 final answer 已经串给你看完之后 — **不阻塞用户看回答**。你看到回答出现 → 短暂后头像旁边的光环进度变化。 ### 自动延续是怎么发生的 持续模式会持久化下一次执行时间,由 supervisor 发起新的图片段。下面的图内延续流程只适用于 `persistentExecution=false` 的旧模式: 1. 写一条 `followup_injected` 事件到时间线 2. 给对话末尾 APPEND 一条用户消息。**1.5.0 起,如果目标有清单,这条消息会明确列出还没通过的那几条准则**——"5/8 已完成,剩余:① …… ② ……,去做剩下的";没有清单时回退到笼统的 "Continue working on the goal. Still missing: {gap}." 3. 让员工再跑一轮 reasoning,**这一轮的回答就直接接在第一轮后面** 你的体感是:员工答完一段 → 停半拍 → **继续往下做** — 就像一个人做完一步停了一下想了想然后继续。 --- ## 撞墙了怎么自己爬起来 ::: tip 新增 长任务最怕的不是难,是**卡住**——撞到迭代上限就停、某一步失败就崩、在一个工具上空转到预算耗尽。这一组机制让员工在这些地方能自己续上、绕开、爬起来,而不是停在那等你再发消息。 ::: ### 撞到迭代上限的硬延续 以前一轮 ReAct 跑满 `max_iterations`(结束原因 `MAX_ITERATIONS_REACHED`),目标子系统会**跳过**这一轮——不评估、不延续,任务就停在那。现在它走一条**硬延续**路径:把迭代计数清零、清掉"超限草稿",给员工**一段全新的满额迭代预算**接着干。 这和上面的"自动延续"不一样:自动延续是 evaluator 判"还没完成"后追加一句引导;硬延续是专门应对**撞上限**,重置的是迭代预算本身。每轮硬延续会吃掉一整段迭代配额,所以有上限——默认每轮最多 **1 次**(编译期硬顶 3 次),`0` = 关闭(回到旧行为:撞上限直接结束这轮)。 ### 停滞检测与重新规划 Plan-Execute 模式下,单个步骤可能**抛异常**,也可能陷入**停滞**——在内部工具循环里反复用同样的调用失败、或拿到同样的无新信息的结果,一路烧到工具预算上限才以空结果"完成",然后污染依赖它的后续步骤。 运行时对每轮工具响应做签名检测,两级响应: - **WARN**:同一调用重复失败几次后,注入一条系统提示让模型换个思路(同一个调用只提示一次) - **HALT**:再撑下去就标记这一步 stuck,结束内循环 一旦某步 HALT 或抛异常,运行时触发**重新规划**:清空当前计划,把"已完成步骤摘要 + 失败原因 + 绕开坏步骤"作为上下文带回规划节点,重新生成计划。每次运行最多重规划 **1 次**,UI 会收到 `plan_replan` 事件(附失败步骤序号、原因)。 ### 元工具回合不计迭代(迭代退款) 渐进式披露里的 `load_skill` / `enable_tool` 是**配置动作**,不是真正干活。如果某一轮 ReAct 里工具调用全是这类元工具,这一轮的迭代计数**不递增**(退款),免得"只顾加载技能"的模型白白耗光迭代预算。每次运行最多退款 3 次。 ### 多步计划自动派生目标 一个**多步**(≥2 步)的 Plan-Execute 计划,如果当前对话还没有活跃目标,规划节点会**自动建一个目标**,以计划的步骤作为验收准则,并广播 `goal_created` 事件刷新 UI 的目标面板。这样长计划天然就被目标系统的"跟到完成"语义托住。由 `mateclaw.goal.auto-goal-from-plan` 控制(默认开)。 --- ## 目标是一份清单(checklist,1.5.0+) 1.4.0 里 evaluator 每轮给一个完成度分数(0~1)和一句"还差什么"。问题是 **0.8 到底是什么意思**——哪几条做完了、哪几条没做,你看不清。 1.5.0 把它换成**清单**:目标 = 一组**可以逐条独立验证**的准则。 **evaluator 有两种模式:** | 模式 | 什么时候跑 | 干什么 | |---|---|---| | **bootstrap(拆解)** | 还没有准则时 | 把目标拆成清单,每条初始为"未通过" | | **verdict(裁决)** | 已有准则时 | 逐条判:这条满足了吗?给出证据 | 两种模式都用**结构化输出**——evaluator 必须返回带类型的对象(准则 `id` + `passed` + `evidence`),而不是一段自由文本让我们去猜。 **完成判定是确定性的。** 只有当**每一条准则都通过**,才判完成。20 条里过了 19 条(0.95 分)依然是"继续"——差一条就还差一条,没有模糊阈值。 **怎么给目标加清单——三种途径:** - **创建时直接带**——`setGoal` 工具传 `criteria: ["DNS 解析正确", "SSL 有效", "测试全绿"]`,或 `POST /api/v1/goals` 传 `criteria`。省去 bootstrap 那一轮。 - **让 evaluator 自己拆**——不传 criteria,第一轮评估时 bootstrap 模式自动拆解。 - **运行中追加**——`addGoalCriterion` 工具或 `POST /api/v1/goals/{id}/criteria`,往进行中的目标补一条,不用重开。 **一条准则长什么样:** ```json { "id": "C1", "text": "DNS 解析指向 fly.io", "passed": false, "evidence": "" } ``` `id` 由服务端分配(C1、C2…),`text` 是人能看懂、LLM 能判的一句话,`passed` 是 evaluator 的裁决,`evidence` 是它给的依据(输出片段、文件摘录等)。清单存在 `mate_agent_goal.criteria` 列(JSON),通过 `GoalResponse.criteria` 解析后下发,从不以裸 JSON 字符串暴露。 ### 头像旁的光,hover 出来是一张清单卡 - **没有清单**时——一句话 tooltip:标题 + evaluator 写的 gap 文本。 - **有清单**时——一张卡片:标题 + `X/Y` 进度,下面每条准则前一个 `○`(未完成)或 `✓`(绿色已完成,文字带删除线)。 评估中头像周围是沙金色呼吸光晕;完成短暂显示绿色环后消失;预算耗尽变红橙色环。 ### Evaluator SPI 评估逻辑实现了 Spring AI 的 `Evaluator` 接口:既能做目标专用的 checklist 裁决(bootstrap / verdict),也能被当成通用评估器复用(把单个目标包成一条准则跑 verdict)。失败的 evaluator 调用**照样计入 LLM 预算**,所以预算账目是准的。 > 1.4.0 的目标是"员工记住它在干什么"。1.5.0 的目标是"员工知道**具体还差哪几条**"。从一个分数,到一份能逐条勾的清单。 --- ## 内置目标工具(员工可用) 员工的工具集里默认包含以下工具(无需手动绑定,是 agent-wide 系统级工具): | 工具 | 用途 | 触发提示词示例 | |---|---|---| | **setGoal** | 创建目标 | "请用 setGoal 锁定本次目标,title=..." | | **addGoalCriterion** | 追加子准则到已有目标 | "再加一条准则:必须支持 IPv6" | | **completeGoal** | 显式标记完成 | "所有事项已做完,请 completeGoal" | | **getGoalStatus** | 查询当前 goal 状态 | "我们现在进展到哪了?" | | **waitForGoalInput** | 持续目标缺少必要输入时暂停并记录原因 | "缺少部署域名,请等待用户补充" | 完成时(`completeGoal`,或 evaluator 判定**每一条准则都通过**),员工会把这个目标的总结同步到[长期记忆](./memory),后续对话能查得回来。 --- ## 子员工不能改父员工的目标 [多员工协作](./agents)里 parent 员工可以委派 child 员工干活。Child **看不到**这些 goal 工具 — 目标是 parent 会话的状态,child 是无状态的执行体。 > 这一条是设计意图,不是 bug。child 帮 parent 做事,但目标的"所有权"留在 parent 那。 --- ## 预算耗尽时 ``` turnsUsed >= turnBudget 或 (agentLlmCallsUsed + evalLlmCallsUsed) >= llmCallBudget ``` 持续模式仅检查正预算;`0` 表示不限。达到预算后目标进入 **paused**,调度状态为 **budget_limited**,增加预算后可 resume。旧模式仍进入终态 **exhausted**,需要新建目标才能继续。当前片段的回答仍会保存。 你的选择: - **加预算 + 恢复** — 通过 `PATCH /api/v1/goals/{id}` 改 budget 后 resume(v1 暂未给 UI 提供按钮,可以走 API 或先 abandon 重新创建) - **放手** — 调 abandon,conversation 上释放槽位,可以重设新目标 --- ## 状态机 ``` create ↓ active ↓ ↑ paused active ──evaluator 全部准则通过 / completeGoal──→ completed (终态) ↓ active ──正预算用完(持续模式)───────────→ paused active ──预算用完(旧模式)───────────────→ exhausted (终态) ↓ active ──user abandon ─────────────────────→ abandoned (终态) ``` 终态 (completed / exhausted / abandoned) 不能复活。要继续就开新 goal — 这是有意保留的简单约束,避免 "重启" 带来的预算账目混乱。 **一会话一目标**:每个 conversation 同一时刻最多一个 active goal。终态 goal 留在历史里不占名额。底层用 H2 / MySQL 的生成列 + 唯一索引保证并发安全,service 层 + DB 层双重防御。 --- ## 这套系统不做什么 按设计原则保留了几个"不做": - **不做嵌套目标 / 目标树** — 一个 conversation 一个目标,不堆 OKR - **不做"目标模板"** — 每个目标是手写的,不是从库里挑的 - **不做跨 conversation 迁移目标** — 想要那效果,请用[工作流](./workflow) - **不暴露评估分数给用户** — 那个 `completionScore` 是工程内部协议,不是用户语言。UI 用一圈光说话,hover 出来:有清单时是逐条勾的清单卡,没清单时是 evaluator 写的 gap 文本(自然语言)。后端日志和 API 里仍可见数值,方便调试 --- ## 完整事件时间线(drawer 抽屉视图) 每个目标都有一份只增不删的事件时间线,按时间倒序展示: | 事件 | 触发 | |---|---| | `created` | setGoal 工具或 REST POST | | `evaluated` | 每轮答完,evaluator 跑完一次 | | `followup_injected` | autoFollowup 触发,注入了 prompt | | `completed` | evaluator 判完成或 completeGoal 工具 | | `exhausted` | budget 用尽 | | `paused` / `resumed` / `abandoned` | 用户手动操作 | | `criterion_added` | addGoalCriterion 工具 | 通过 `GET /api/v1/goals/{id}/events` 拉取(详见 [API 参考](./api))。 --- ## 配置项 `application.yml`: ```yaml mateclaw: goal: # 主开关;关闭后图节点对所有调用 pass-through enabled: true # 创建目标时 autoFollowupEnabled 的默认值(调用方未指定时) default-auto-followup: true # 运行期总开关;关掉则无论 per-goal 标志如何,都不注入自动延续 allow-auto-followup: true # 单后端实例同时运行的持久化目标 Segment 上限 max-concurrent-segments: 4 # 普通 Segment 结算后再次续跑的全局最小间隔(秒) minimum-continuation-interval-seconds: 1 # provider 或评估器发生可重试故障后,暂停认领其他目标的时间(秒;0 = 关闭) provider-failure-global-backoff-seconds: 30 # 新目标默认持续模式;省略预算表示不限(0) default-persistent-execution: true supervisor-poll-ms: 5000 # 旧模式的默认 turn 预算 default-turn-budget: 20 # 旧模式的默认 LLM 调用预算(agent + evaluator 之和) default-llm-call-budget: 200 # 自动延续之间至少隔多久(秒) auto-followup-cooldown-seconds: 0 # 单次 graph 运行内自动延续的硬上限(每条消息的安全网;总预算仍由 turnBudget 管) max-followups-per-run: 8 # 撞到迭代上限时每轮最多硬延续几次(0 = 关闭;编译期硬顶 3) max-hard-continuations-per-run: 1 # 多步 Plan-Execute 计划在无活跃目标时自动派生一个目标 auto-goal-from-plan: true # 评估器使用的模型;空字符串 = 沿用对话当前模型(便宜的小模型推荐:qwen-turbo / glm-4-flash) evaluator-model: "" # 评估 prompt 携带的历史消息条数上限 evaluator-context-messages: 8 ``` `minimum-continuation-interval-seconds` 与目标自身的 `followupCooldownSeconds` 取较大值。 provider 全局退避只保存在当前后端实例内;重启恢复仍以数据库中的 continuation `next_run_at` 和每目标失败次数为准。耐久或低配模型测试建议使用并发 `1`、最小间隔 `300` 秒、provider 全局退避 `300` 秒,再根据日志逐步放量。 --- ## 数据库 相关表使用 `mate_` 前缀: | 表 | 用途 | |---|---| | `mate_agent_goal` | 目标本体;含 status / budget / 双 LLM 计数器 / 自动延续配置 | | `mate_agent_goal_event` | 目标的事件追加日志,drawer 时间线读它 | 持续调度表 `mate_goal_continuation` 记录队列、到期时间和租约。迁移由 Flyway 跑 `V120__agent_goal.sql` 和 `V188__goal_continuation.sql`(H2 / MySQL / KingbaseES 三方言)。 --- ## 一句话总结 **Goal 不是给员工加一个功能。是改它的状态。** 以前的员工"答完就忘"。Goal 让员工跨多轮记住一件事 — 它在干什么、还差什么、什么时候算完。你只用说一次。剩下的,让头像旁边那圈光替你跟。