Hermes Agent /goal 长任务运行时架构拆解:状态持久化、Judge 闭环与自主续航

0 阅读14分钟

拆解 Hermes /goal 如何用状态持久化、Judge 判定和自动续航,让 Agent 真正跑完长任务。 原文链接AI 小老六

导语

Agent 长任务 最让人烦的地方,往往不是它不会做,而是它太容易停下来。

修一个类型错误,它修三处就回来说“完成了”;跑一轮测试失败,它等你再敲一句“继续”;做一次依赖迁移,中间需要人反复盯着终端,像给半自动机器喂指令。到了长程任务里,这种交互方式会把开发者拖回最原始的监督岗位:人不再写代码,但要不停判断 Agent 是否该接着干。

Hermes Agent v0.13.0 里加入的 /goal,解决的不是“让模型更聪明”这个泛问题,而是一个更具体的工程问题:怎样把一次目标变成一个可持续推进、可暂停、可恢复、可判定完成的运行时流程。

这件事的关键不在命令本身,而在命令背后的四个部件:外部状态、生命周期管理、Judge 判定、继续执行队列。它们合在一起,才让 Agent 从“等用户说继续”变成“知道自己还没干完”。

inline-01-goal-control-plane.png

图:/goal 把目标状态、Judge 判定和继续调度放到同一个控制平面里。

长任务瓶颈:不在 Token,而在会话记忆

很多人第一次做 长任务 Agent 时,会本能地把问题归因给上下文窗口不够大。窗口再长一点,模型是不是就能把事情做完?实践里往往不是这样。

当一个任务在同一会话里滚动几十轮,系统会累积大量中间输出、错误日志、临时计划和已经过期的判断。上下文变厚后,模型并不会稳定地“记得更多”,反而更容易把噪声当成线索,把未完成事项压到注意力边缘。开发者社区常把这段质量下滑区域叫做 ​Dumb Zone​:不是模型真的变笨了,而是会话里堆了太多会干扰判断的东西。

Ralph Loop 给出的方向很朴素:​不要把长期记忆放在聊天记录里​。

mermaid-01.png

图:目标、外部状态与显式停止条件构成长任务执行闭环。

这个循环背后的判断很硬:​文件系统和 Git 比模型上下文更适合承载长期状态​。Agent 每一轮从干净的输入开始,通过代码、文档、测试结果和提交记录接住上一轮的进展。会话可以丢,工作目录不能丢。

原始 Ralph Loop 甚至可以简化成一个 Bash 循环:

while :; do cat PROMPT.md | claude -p --dangerously-skip-permissions; done

这当然不够产品化,但它把长期自主执行的骨架说明白了:固定目标、外部状态、循环执行、显式停止条件。

Hermes 的 /goal 是在这个骨架上补了一套可用的运行时。

/goal 本质:给 Agent 加一个目标控制平面

普通对话里,用户的每条消息都会触发一次推理。推理结束后,系统默认认为这一轮就结束了。/goal 改掉的是这个默认假设。

当用户输入:

/goal 修复 auth.ts 中所有 TypeScript 错误,确保 lint 和单测全部通过

系统不再把它当作一条普通 prompt,而是创建一份可持久化的目标状态。之后每一轮结束时,Hermes 都会追问一个问题:最近这一轮的结果,是否已经满足最初目标?如果没有,系统自动把“继续完成目标”的提示放回执行队列。

可以把 /goal 看成一个目标控制平面:

运行时部件负责的问题Hermes 中的体现
目标状态当前要完成什么,剩余多少轮,是否暂停GoalState
生命周期接口目标如何开始、暂停、恢复、清理、完成GoalManager
完成判定最近一轮是否足够满足目标judge_goal()
继续调度未完成时如何触发下一轮CLI pending input / Gateway FIFO
目标细化执行中如何追加验收标准/subgoal

这个设计的好处是,Agent 不需要在聊天里“记住自己还有一个目标”。目标被系统外化 了,后续每一轮都由运行时重新注入必要信息。

GoalState:把目标写进数据库

Hermes 没有把 /goal 做成会话内的临时变量。它把目标落到了 ​**SessionDB.state_meta**​,key 类似 goal:<session_id>。这一步看起来不花哨,但很关键。

@dataclass
class GoalState:
    goal: str
    status: str = "active"          # active | paused | done | cleared
    turns_used: int = 0
    max_turns: int = 20
    created_at: float = 0.0
    last_turn_at: float = 0.0
    last_verdict: Optional[str] = None
    last_reason: Optional[str] = None
    paused_reason: Optional[str] = None
    consecutive_parse_failures: int = 0
    subgoals: List[str] = field(default_factory=list)

这份状态解决了几个实际问题。

第一,​任务可以跨会话​。今天设置的目标,中途关掉终端,明天重新打开后仍然能通过 /goal resume 接着跑。

第二,执行有预算。max_turns 默认 20 轮,避免目标因为判定异常或描述过宽而无限循环。

第三,系统能记住失败模式。比如 Judge 连续返回不可解析内容,consecutive_parse_failures 会增长,超过阈值后目标自动进入暂停态,而不是继续烧钱。

第四,目标可以变细。subgoals 让用户在执行过程中补充约束,不必推翻原目标重来。

这也是 Hermes 相比只做 session-based goal 的方案更完整的地方:它没有把“长期任务”寄托在一个长会话里,而是把任务本身变成了可查询、可恢复、可审计的状态对象。 inline-02-persistent-state.png 图:目标不再依赖聊天记忆,而是沉淀为可恢复、可审计的外部状态。

GoalManager:长任务需要状态机,而不是 while true

有了状态,还需要一个管生命周期的编排器。Hermes 的 GoalManager 提供了这组接口:

class GoalManager:
    def set(goal: str, *, max_turns: int = 20) -> GoalState
    def pause(reason: str = "user-paused") -> GoalState
    def resume(*, reset_budget: bool = True) -> GoalState
    def clear() -> None
    def mark_done(reason: str) -> None

    def add_subgoal(text: str) -> str
    def remove_subgoal(index_1based: int) -> str
    def clear_subgoals() -> int

    def evaluate_after_turn(last_response: str) -> Decision
    def status_line() -> str
    def next_continuation_prompt() -> str

从状态机角度看,/goal 的生命周期并不复杂,但每个状态都对应真实场景。 mermaid-02.png 图:/goal 在 active、paused、done 与 cleared 之间流转。

这里最容易被低估的是 ​**paused**​。一个长任务运行时不能只有“继续”和“结束”。网络抖动、预算耗尽、用户临时插话、Judge 配置异常,都不该把目标直接判死。暂停态给了系统一个缓冲层:任务没有完成,但也不再盲目推进。

Judge 闭环:宁可多跑一轮,也别提前庆功

/goal 能不能可靠,核心看完成判定。Hermes 把这件事交给一个辅助模型,也就是 Judge。

每一轮结束后,系统把两段信息交给 Judge:原始目标和 Agent 最近一轮回复。Judge 只需要输出一个很窄的 JSON:

{"done": false, "reason": "仍有测试失败,需要继续修复"}

判定规则故意偏保守。只有三类情况会被认为完成:

判定为 DONE 的情况例子
Agent 明确确认目标已经完成“所有 TypeScript 错误已修复,lint 和测试均通过”
最终交付物已经产生,且能看出满足目标代码、报告、文档或构建产物已经落地
目标无法继续推进,需要外部输入权限缺失、依赖不可访问、需求本身矛盾

除此之外,默认继续。

这条策略很像工程里的 false positive 控制:​误判完成的代价通常比多跑一轮更高​。多跑一轮只是多花一点时间和 token;提前结束会把半成品交给用户,后续还要人工排查它到底漏了什么。

Hermes 的 Judge prompt 也基本围绕这个原则设计:

JUDGE_SYSTEM_PROMPT = """
A goal is DONE only when:
- The response explicitly confirms the goal was completed, OR
- The response clearly shows the final deliverable was produced, OR
- The response explains the goal is unachievable / blocked / needs user input

Otherwise the goal is NOT done: CONTINUE.
"""

这不是追求“聪明判定”,而是追求“别乱放行”。在长任务里,这个取舍很对。

fail-open:Judge 挂了,任务不该跟着死

真实系统里,Judge 不是永远可靠。API 超时、网络抖动、模型返回非 JSON,都很常见。Hermes 没有把这些异常直接当成任务失败,而是设计了几层保护。

异常场景处理方式设计意图
Judge API 调用失败返回 continue外部依赖偶发失败时不中断主任务
最近一轮响应为空返回 continue没有足够证据时不判完成
Judge 返回非 JSON计数并重试容忍短期格式漂移
连续解析失败达到阈值自动暂停避免配置错误导致无意义循环
轮次预算耗尽自动暂停防止失控续跑

典型代码大概是这样的风格:

try:
    resp = client.chat.completions.create(...)
except Exception as exc:
    logger.info("goal judge: API call failed; continuing")
    return "continue", f"judge error: {type(exc).__name__}", False

这里的 fail-open 不是无脑继续。它只是在短暂异常下选择不打断执行;一旦异常变成连续模式,系统会切到 paused,让用户检查 Judge 模型或配置。

这个细节很工程化。长任务最怕两种极端:遇到一点波动就停止,或者明知判定链路坏了还继续跑。Hermes 在两者之间留了一个合理的台阶。 inline-03-judge-fail-open.png

图:Judge 失败时先保守继续,连续异常再暂停,避免长任务误停或失控。

CLI 与 Gateway:自动继续不能抢用户的话

/goal 还有一个容易被忽略的问题:当系统准备自动注入“继续执行”提示时,如果用户这时发来新消息怎么办?

Hermes 的处理原则是​用户优先​。

在 CLI 里,继续提示会进入 _pending_input 队列;在 Gateway 场景里,继续事件通过 adapter FIFO 排队。系统会确保用户显式输入优先于自动续跑提示。这样做能避免一种很糟糕的体验:用户刚想纠正方向,Agent 已经因为自动续跑又开了一轮。

mermaid-03.png

图:Judge 判定后投递继续提示,但用户输入始终优先处理。

这让 /goal 更像一个可控的后台任务,而不是一个关不掉的自动驾驶。它可以自己推进,但不会压过用户的显式指令。

Hermes 和同类方案的差异:真正拉开距离的是 持久化

/goal 这类能力在 2026 年开始快速变成 Agent 产品的标配。Codex CLI、Claude Code、Hermes 都在做,差异不在概念,而在实现边界。

能力维度Hermes AgentCodex CLIClaude Code
目标状态SessionDB 持久化偏 session-based偏 session-based
子目标支持 /subgoal未突出未突出
Judge 模型可配置 provider / model固定策略为主固定策略为主
多平台CLI 与 20+ Gateway 平台主要面向 CLI主要面向官方环境
异常处理fail-open、解析失败计数、自动暂停有继续策略有继续策略
用户中断可转暂停依实现而定依实现而定

Hermes 的优势不是“也有 /goal”,而是它把 goal 做成了一个跨会话、跨平台的状态系统。尤其是 SessionDB、/subgoal 和可配置 Judge 这三点,让它更适合自托管和多入口使用。

如果只是本地 CLI 玩一轮任务,session-based 方案够用;如果希望目标跨天、跨终端、跨聊天平台继续存在,状态持久化就不再是加分项,而是基础设施。

适合交给 /goal 的任务,必须能写出验收条件

/goal 不适合所有事。它适合边界清楚、可检查、需要多轮推进的任务。

适合使用 /goal不适合使用 /goal
大型重构、依赖迁移、补测试一两轮就能完成的小改动
安全漏洞修复、审计整改需要高频人工选择的产品讨论
性能优化,有明确指标目标暂时说不清的探索性研究
长时间构建、报告生成、资料整理需要实时确认权限或风险的操作

目标写得越像验收单​,Judge 越容易工作。下面这类差异会直接影响 /goal 的效果。

模糊目标可判定目标
修复 bug修复 login 在空密码时返回 500 的问题,并补充回归测试
优化性能将主 API P95 降到 200ms 以下,并保留现有单测通过
写文档docs/api.md 补齐所有 REST 端点的路径、参数、返回值和示例
做重构auth.ts 改为依赖注入模式,公开接口不变,覆盖率保持 85% 以上

一个好 /goal 最好包含四类信息:任务对象、完成条件、验证方式、边界约束。

/goal 将 auth.ts 的用户校验逻辑迁移到 AuthService,保持现有 public API 不变;补齐空密码、过期 token、重复登录三个用例;运行 pnpm lint 和 pnpm test 全部通过后停止。

这类目标不需要 Judge 猜。它只需要检查 Agent 是否给出了足够清楚的完成证据。

Judge 模型不必昂贵,但必须 稳定

Hermes 允许为 goal_judge 单独配置模型,例如:

# ~/.hermes/config.yaml
goals:
  max_turns: 20

auxiliary:
  goal_judge:
    provider: openrouter
    model: google/gemini-3-flash-preview

这是一种很实用的拆分。主任务模型要负责写代码、读文档、查问题,能力需要强;Judge 只读目标和最近一轮回复,然后判断是否继续,输入短、输出短,速度和稳定性更重要。

便宜的快速模型未必能做复杂推理,但做“是否已经明确交付”这类窄判定通常够用。真正需要关注的是格式稳定性。如果模型经常输出不可解析内容,Hermes 会靠连续解析失败保护把目标暂停,但这也说明 Judge 选择不合适。

快速体验:从更新到第一个目标

要使用 /goal,先把 Hermes 更新到 v0.13.0 或更新版本:

hermes update

启动后输入 /goal,如果能看到 goal 指令,就说明功能可用。可以先用一个风险较低的任务试水:

/goal 生成一份当前项目的 DeepResearch 风格技术报告,包含架构概览、关键模块、运行方式和潜在风险;
输出为 HTML,并在本地打开预览确认无明显渲染错误。

这类任务的好处是验收边界清楚,失败也不会破坏代码库。等确认工具链、搜索能力和前端渲染能力都配置好后,再把 /goal 用到重构、补测试或安全修复上。

如果希望报告更好看,工具配置很重要。WebSearch、文件读写、前端设计相关 skill、HTML 渲染能力都会影响最终产物。/goal 只是让任务持续推进,不能凭空补齐 Agent 没有的工具。

我对 /goal 的判断:它把 Agent 的交互单位从“回复”改成了 “完成条件”

/goal 最有价值的地方,不是少敲几次“继续”。少敲字只是表面收益。

真正的变化是,用户和 Agent 之间的契约从“回答我这一轮”变成了“持续工作,直到满足这个条件”。为了支撑这个契约,系统必须把目标、状态、预算、判定和恢复机制都工程化。Hermes 的实现恰好把这些东西补齐了大半。

它也不是银弹。目标写不清,Judge 就会摇摆;工具不给力,Agent 只能原地打转;任务涉及高风险操作,用户仍然要介入确认。但对于重构、补测、报告生成、批量修复这类长链路任务,/goal 已经把 Agent 往“能放手一段时间”的方向推了一大步。

更准确地说,/goal 让 Agent 不再只是一轮对话里的聪明助手,而开始像一个有状态的执行进程:有目标,有预算,有暂停恢复,有完成判定,也有失败保护。

这才是长程 Agent 真正需要的底座。

参考资料

  • Hermes Agent v0.13.0 Release Notes[1]
  • Claude Code: Keep Claude working toward a goal[2]
  • Codex: Follow a goal[3]
  • Hermes Agent's /goal Command Makes AI Stop Quitting Halfway Through the Job[4]