なぜ Agent は長く動けないのか

「検索―絞り込み―レポート作成」程度のタスクなら、Agent に任せても 10 分ほどは安心して走らせられる。しかし「リポジトリ全体の循環依存を洗い出し、1 つずつ修正パッチを出し、そのパッチごとにテストを回す」というタスクを同じ Agent に任せると、30 分目にはまず異変が出はじめる。モデルはすでに読んだファイルを繰り返し、ツール呼び出しが空の結果を返してもそれを有効な入力として扱い、コンテキストが雪だるま式に膨らんで重要な制約が押し出され、最後にはそれなりに滑らかな文章を書きながら、積み上げてきた結論をすべて失った要約を返す。

このような失敗が「エラー」として現れることはほとんどない。プロセスは生きているし、終了コードは 0 のままで、ログには「タスク完了」的字すら残っているのに、生み出されたものは価値がない。根本原因は、ほとんどの Agent ランタイムのメンタルモデルが「1 セッション = 1 タスク」のままだからである。状態はメモリ上にあり、モデルは毎ステップごとに会話履歴から世界を組み立て直す。履歴が切り詰められた、あるいは圧縮されれば、世界は一度書き換えられてしまう。

長いタスクで解くべき問題は「モデルがもっと長く連続して仕事できるか」ではない。タスクのライフサイクルをプロセスのライフサイクルから切り離すことが問題なのである。プロセスは死んでもよいが、タスクは死なしてはならない。マシンは替えてよいが、状態は失ってはならない。この一文は地味に聞こえるが、以降のほぼすべてのアーキテクチャ選択を決めてしまう。会話から独立したタスクグラフが必要なのはなぜ、チェックポイントの粒度を「時間間隔」ではなく「不可逆操作の密度」で決める必要があるのはなぜ、そしてユーザーが途中で要望を変えても、単純にコンテキストをある地点で切って新一轮をやり直すわけにはいかないのはなぜ。

3 つの時間スケール:混ぜないこと

長いタスクについて語るとき、もっとも起こりやすい誤りは、まったく性質の異なる 3 つの時間スケールを同じ「タイムアウト/リトライ」仕組みに押し込むことです。するとどの層もきれいになりません。少なくとも次の 3 つが含まれます。

  • 単一ステップの時間スケール:1 回のモデル呼び出し、1 回のツール実行、1 つのサブタスクの一括実行。桁で言えば秒から分、障害モードはタイムアウト、レート制限、不正な形式の返却。
  • フェーズの時間スケール:タスクグラフ内の 1 つのノード、または 1 本のサブチェーン。たとえば「候補ファイルをすべて分類する」。桁で言えば分から数十分、障害モードはループへの陥落、局所解、反復的な自己疑い。
  • タスク全体の時間スケール:タスクを開始から成果物の提出まで丸ごと走らせること。数時間、あるいは数日に及び、その間に人の待ち時間と介入が含まれる。障害モードは予算切れ、要件のドリフト、外部世界の変化(依存関係のアップグレード、誰かがファイルを変えてしまうなど)。

この 3 層には、それぞれ別の仕組みが必要です。単一ステップ層のリトライとタイムアウトは純粋に技術的な問題なので、クライアントライブラリに任せれば十分です。フェーズ層に必要なのは進捗の推定と停滞の検出です。ただしこれはタイムアウトとは別物で、「このサブタスクはもう 12 分走っている」ということだけでは固まっていることを意味しません。重要なのは、その単位時間あたりの産出がまだ伸びているかどうかを判定することです。タスク全体層に必要なのは、予算、停止、再開、そして人間が入れる導線です。

  • 判断の目安:長いタスクを観察し続ける価値があるかどうか、もっとも簡単なシグナルはここにある。最近 N ステップ分のツール出力を重複排除したうえで、新しい情報がどれだけ増えたかを数えることです。連続して新情報量がゼロなら、どれだけ走っていてもそれは「進行している」のではなく停滞しているとみなすべきです。

以下では、永続化、冪等性、人の介入、予算、可観測性、ロールバックの 6 方向で展開します。ただしこれらは並列な 6 つのトピックではありません。永続化は土台、冪等性は土台の上に立つ耐力壁であり、残る 4 つはすべて「どこかの地点からいつでも復元されうる」という前提の上に成り立っています。

タスクグラフと状態:何を永続化し、何を永続化しないか

長いタスクの状態を永続化するとき、もっともよくある遠回りは「会話履歴をデータベースに入れておけば終わり」というものです。この道は 2 時間以内のタスクなら足ります。Agent フレームワークがすでに持つ抽象をそのまま再利用できるからです。しかしプロセス横断、マシン横断、そして日をまたぐ必要になった途端、3 つの具体的な問題にぶつかります。

  1. 会話履歴は物語であって、状態ではない。履歴とは「時系列に並べられた一文ずつ」のことで、どのステップが完了したか、どの副作用がすでに適用されたか、どの結論が後の証拠で覆されたかは教えてくれません。履歴から状態を推測し直すという行為は、失敗しうるモデルに、本来なら決定論的なコードが行うべき帳簿作業をやらせることに等しくなります。
  2. 履歴は膨らむ。数時間のツール呼び出しで数十万トークンは簡単に積み上がる。保存すること自体は難しくないが、復元ごとに全量ロードするのは遅く、かつ高いコストになる。しかも、大量の低価値コンテンツ(リトライしたときのエラー、重複したディレクトリ一覧)がモデルの視野に混入してしまいます。
  3. 履歴は並列を表現できない。3 つのサブタスクを同時に走らせ、そのうち 1 つが失敗しても、履歴は一次元の列なので「A と C は完了して B はまだ実行中」という構造化された事実を読み取ることができません。

したがって正しいやり方は、独立したタスクグラフ構造を導入することです。ノードがタスクの単位、エッジが依存関係とデータフローを表し、ノード内部には状態、入力の要約、出力の参照、そして冪等性に関する効果ログを記録します。会話履歴はそのうち 1 種類の「ノード内部の成果物」、すなわち特定ステップを実行したときのプロンプトと返却値へと格下げられ、状態を復元する唯一の根拠ではなくなります。

これだけで足りるタスクグラフ表現

以下は、意図的に切り詰めた版です。実システムではノードがもっと多くのフィールドを持ちますが、ここで挙げるものは復元に必須な最小限のセットです。

from dataclasses import dataclass, field
from enum import Enum
from typing import Any


class NodeStatus(str, Enum):
    """节点状态机。恢复逻辑只允许从这里规定的几个终态往外走。"""
    PENDING = "pending"        # 未开始
    RUNNING = "running"        # 已认领,进程可能已经死了
    BLOCKED = "blocked"        # 依赖未满足或等待人工决策
    SUCCEEDED = "succeeded"    # 完成且副作用已确认落地
    FAILED = "failed"          # 失败,是否可续跑由 retryable 决定
    ABANDONED = "abandoned"    # 被外部判定放弃,保留用于审计


@dataclass
class Node:
    """任务图中的一个节点。

    key 必须是稳定且可重算的,不能用随机 UUID——
    否则重放同一步骤会生成一个新节点,幂等就无从谈起。
    """
    key: str
    kind: str                              # 节点类型:llm_call / tool / human / subgraph
    status: NodeStatus = NodeStatus.PENDING
    deps: list[str] = field(default_factory=list)

    #产出与输入分离:产出存引用(比如对象存储路径),输入存摘要
    input_digest: str | None = None         # 输入指纹,用于检测「依赖变了但我没重跑」
    output_ref: str | None = None           # 产出落盘位置

    #幂等三件套
    effect_key: str | None = None           # 本节点所有外部副作用的唯一标识
    effect_log: list[str] = field(default_factory=list)  # 已确认落地的副作用 id
    compensations: list[str] = field(default_factory=list)# 补偿动作 id,按逆序执行

    attempt: int = 0
    retryable: bool = False                # 失败后能否原地重试
    resume_from: str | None = None          # 断点续跑时指向的上一个成功节点 key
    cost_used: float = 0.0                  # 已消耗预算(token 折算)
    wall_used: float = 0.0                  # 已消耗墙钟时间(秒)
    meta: dict[str, Any] = field(default_factory=dict)


@dataclass
class TaskGraph:
    nodes: dict[str, Node] = field(default_factory=dict)
    root: str | None = None

    def add(self, node: Node) -> Node:
        self.nodes[node.key] = node
        return node

    def ready(self) -> list[Node]:
        """返回所有依赖已满足、可立即执行的节点。

        注意:RUNNING 状态不算 ready。进程重启后残留的 RUNNING
        必须先被显式回收成 PENDING,否则既不会被执行也不会被监控。
        """
        out = []
        for n in self.nodes.values():
            if n.status != NodeStatus.PENDING:
                continue
            if all(self.nodes[d].status == NodeStatus.SUCCEEDED for d in n.deps):
                out.append(n)
        return out

    def reclaim_orphans(self, live_run_ids: set[str]) -> list[str]:
        """回收孤儿节点:状态是 RUNNING 但持有它的进程已经不在了。

        这是「进程重启后状态还在,但活儿没人干」这一类问题的唯一解法。
        返回被回收的节点 key 列表。
        """
        reclaimed = []
        for n in self.nodes.values():
            if n.status != NodeStatus.RUNNING:
                continue
            if str(n.meta.get("run_id")) in live_run_ids:
                continue
            n.status = NodeStatus.PENDING
            n.meta["reclaimed_from_run"] = n.meta.get("run_id")
            n.meta["reclaim_count"] = n.meta.get("reclaim_count", 0) + 1
            reclaimed.append(n.key)
        return reclaimed

上のコードには 2 点、個別に強調しておく価値があります。多くの実装で抜け落ちている部分です。

第一に、node key は再計算可能でなければならない。多くのチームが UUID をノード id に使うため、「同じステップの再演」はデータの階層では「新しいノードの追加」になり、冪等性が成立せず、状態機械も 2 本の平行したブランチに分裂してしまいます。正しいやり方は入力内容から key を派生させることです。たとえば hash(ノード種別 + 入力要約 + 依存出力の要約) のような形です。これなら再演は同じ key にヒットするので、復元ロジックはじめて「完了済みのものは飛ばして出力を再利用する」という判断ができます。

第二に、RUNNING は安全な状態ではない。それは単に「あるプロセスが、ある時点で自分がこの仕事を担当していると主張している」ことを意味します。プロセスが kill -9 された後、この状態は永久にそこに残り、失敗するのでも完了するのでもないゾンビになります。復元フローの最初のステップは必ずリース回収でなければなりません。各 RUNNING ノードは run_id とハートビートのタイムスタンプを持ち、新しいプロセスの起動時に「ハートビートがタイムアウトした RUNNING」をまとめて PENDING へ回収します(あるいは、そのノードが再入可能かに応じて直接 FAILED と判定します)。

チェックポイントの粒度:不可逆操作の密度で切る

チェックポイントの粒度が、この節で本当に決めるべきポイントです。粗すぎれば 1 回の失敗で数時間が無駄になり、細かすぎれば帳簿のオーバーヘッドと状態量が爆発し、「観測可能な進捗」がノイズの塊になってしまいます。よく使われる 3 通りの切り方がある。

  • 会話ターンで切る:N 回のモデル呼び出しごとに 1 回保存する。実装はもっとも簡単ですが、代償として 2 つのチェックポイントの間にある副作用はすべてメモリ上にあり、プロセスが落ちればすべてをやり直すことになります。
  • ステップで切る:ツール呼び出しごとに前後 1 回ずつ保存する。復元の粒度はもっとも細かくなりますが、状態の書き込み量がツール呼び出し量と等しく、しかも多くのステップ自体は非常に安価(たとえばファイルを読むだけ)なので、それ単体のためにチェックポイントを作るのは割に合いません。
  • 副作用の境界で切る:連続した読み取り専用のステップを 1 区間にまとめ、「不可逆な外部書き込み」の直前と直後だけにチェックポイントを置く。

3 番目が、実際の長いタスクにおいて私が唯一推せるデフォルトの切り方です。理由は単純で、読み取り専用ステップの再演コストはほぼゼロ(ファイルをもう一度読む、検索をもう一度呼ぶ程度。多くてもトークンを少し無駄にするだけ)である一方、外部書き込みを含むステップの再演コストは計り知れないかもしれないからです。チェックポイントを副作用の境界に合わせると、復元時に再演しなくてよいのは「副作用どうしの間」の読み取り専用作業だけで、コストは制御できます。逆にチェックポイントを 2 つの副作用の真ん中に置いてしまうと、向き合うのは「書いたかもしれないが、本当に書いたのか分からない」という、もっとも厄介な問題になります。

実際に実装するときには、各ノードにリスクのラベルを付けられます。

ノード種別外部副作用再演コスト推奨するチェックポイント
ファイルを読む / ディレクトリを列挙する / インデックスを引くなしトークンのみ統合可、区切りの終わりに 1 回保存
読み取り専用の API(問い合わせ系)を呼ぶなしトークン + レート制限のクォータ統合可、レート制限に注意
ローカルファイルへ書く(下書きなど、上書きでよいもの)ローカル、冪等に上書きできる低い書き込み直後に保存
git へのコミット / PR 作成 / メッセージ送信リモート、見える、撤回が難しい高い、しかも人を邪魔する実行前に意図を保存、実行後に確認を保存
注文 / 支払い / リソースの削除不可逆極めて高い人間による確認を必須にする

最後の 2 行に注意してください。「PR の作成」「メッセージ送信」のようなアクションは、書き込み後に 1 回だけチェックポイントを保存しても不十分です。書き込みの後、保存の前にプロセスが落ちると、復元時にこのアクションが再演され、リポジトリに重複した PR が増えてしまいます。このようなアクションは必ず「先に意図を書き、実行し、最後に確認する」という 3 段構えにしなければなりません。

async def run_node_with_intent(graph: TaskGraph, node: Node, store) -> None:
    """带意图日志的副作用执行。

    核心不变式:任何远端可见的副作用,在写入外部系统之前,
    必须先在本地持久化一条「我打算做这件事」的记录。
    """
    if node.effect_key and node.effect_key in node.effect_log:
        #已经确认落地过,直接复用,不重放
        node.output_ref = node.meta.get("output_ref_from_first_run")
        node.status = NodeStatus.SUCCEEDED
        await store.save(graph)
        return

    #第一段:写意图。必须在调用外部系统之前落盘。
    intent = {
        "effect_key": node.effect_key,
        "node_key": node.key,
        "payload_digest": node.input_digest,
        "ts": now(),
    }
    await store.append_intent(node.key, intent)

    #第二段:执行。副作用 ID 用 effect_key 派生,保证幂等键稳定。
    ext_id = await external_api.create(
        idempotency_key=node.effect_key,   # 外部系统支持的话,这是最省事的一层
        **node.meta["request"],
    )

    #第三段:写确认。哪怕这一步之前进程死了,
    #恢复时也能靠 idempotency_key 或外部查询把这次操作认领回来。
    node.effect_log.append(node.effect_key)
    node.output_ref = ext_id
    node.status = NodeStatus.SUCCEEDED
    await store.save(graph)

ここで effect_key をどう組み立てるかは、個別に説明する価値があります。満たすべき条件が 2 つあります。論理的に同じ副作用は、どんなリトライでも必ず同じ key になること、そして異なる副作用どうしは決して衝突しないことです。実践でもっとも安定するのは、「対象位置 + アクション種別 + ペイロード内容の要約」という組み合わせにすることです。たとえば pr:repo-a/branch-x:create:<payload_hash> のような形です。同じ PR が要望変更によって 2 度内容を直す必要があったとしても、それらの key は本質的に異なるので 2 回とも実行されます。これはまさに私たちが望んでいる挙動です。

チェックポイント復元が冪等であるための前提:1 ステップの再演で何が重複するのか

「操作を冪等にする」という言葉はドキュメントには腑に落ちているのに、コードにはほとんど落地しません。みんなが「1 ステップの再演とは、同じ操作をもう一度実行することだ」と前提にしているからです。ところが本当のリスクは、再演される 1 ステップがしばしば原子的な操作ではないことにあります。それは複数のサブアクションからできており、そのうち一部の再演コストはきわめて低く、別のコストはきわめて高いのです。

厄介なのは、さらに再演の意味が時間とともにずれることです。「循環依存を直す」という 1 ステップを 2 時間後に再演するのは、もはや筋が通らない可能性があります。誰かがファイルを変えた、依存のバージョンが上がった、直前の結論が古いディレクトリスナップショットのうえに立っている——、いずれも起こりうることです。 断点復元の正しい姿勢は、闇雲に再演することではなく、まず直前の成功ノードの出力がいまだに有効かどうかを判定すること、そのうえで再演するかスキップするかを決めることです。

3 種類の冪等性:「同じ関数を 2 度実行する」だけではない

1 つのノードが行うアクションを分解して見てみると、通常はまったく異なる 3 種類の副作用が含まれており、扱い方もまったく違います。

  • 読み取り系:本質的に冪等です。再演のコストはトークンとレート制限のクォータだけなので、安心して再演できます。ただし読み取った内容がすでに変わっている可能性には注意が必要です。したがって読み取りの結果には「読んだ時刻」を添えて状態に入れなければ、復元後に手元のデータが どれほど古いのかを判断できません。
  • ローカル書き込み系:「同じパスへの上書き書き込み」ならだいたい冪等です。もっとも危険なのは「追記書き込み」です。同じログファイルに復元フローが 2 度追記すると、重複行が生まれます。この種の操作は、上書き書き込みに書き換えるか、ファイル内容に effect_key を持たせて読み込み時に key で重複排除するか、どちらかにしてください。
  • リモート書き込み系:メッセージ送信、PR 作成、支払いの呼び出し、CI の起動です。ここまでが本当に外部システム側の協力が必要になる部分です。

3 つ目について、冪等性の実装には弱い順に 3 通りの強度があります。

  1. 外部システムの冪等キーに依存する。現代の API の多くは idempotency key を渡せるようになり、サーバ側が同じ key のリクエストを 1 回だけ実際に処理することを保証します。もっとも手間の少ない方法なので、まずこういうものを探してください。
  2. 事前照会で重複を除く。実行する前に外部システムを一度問い合わせ、その副作用がすでに存在しないかを確かめます(PR のタイトルとブランチの組、メッセージ本文のハッシュなど、何らかの固有の目印で判定します)。脆い方法ですが、冪等キーが使えない環境ではこれしか手段がありません。
  3. 事後照合で修復する。重複実行は許すが、定期的な照合ジョブを置き、重複が見つかったら後始末するかマークします。この方針は単独では使えません。前 2 者の保険としてしか機能しないからです。後始末そのものにも窓があるからです。
  • 実務的な判断:もしあなたのアクションが「私はこれをしようと思う」ということを,何らかの形であらわすことができず、あとから照合もしわれないなら、そのアクションは自動化されるべきではありません。人間による確認ノードに格下げすべきです。長いタスクのシステムにおいて、人間ノードは失敗ではありません。観測できない自動化へ混ぜ込むことこそ、ずっと大きな失敗です。

ここには見落としがちな冪等性の罠が 1 つあります。LLM ノード自体が本質的に冪等になり得ないということです。同じノードを 2 度再演しても、モデルが返すテキストは異なりうります(temperature を 0 にしても、モデルサービス側の実装がビット単位の一致を保証するわけではありません)。したがって LLM ノードの冪等性は「再演しても同じ結果になるから」ではなく、「再演は 1 回だけ行い、その結果を凍結する」ことで得られます。ノードの成果物が成功して保存されたあとは、復元時にディスクから読むだけで、モデルを二度と呼び出してはいけません。ここを間違えると、3 回復元したタスクが、表現が異なる上に結論が互いに矛盾しうる 3 つのレポートを生んでしまい、人はどれを信じればよいのか判断できません。

これにセットで到来的するのが「入力フィンガープリント」の仕組みです。各ノードについて input_digest を計算し、直接の依存ノードの出力要約から一緒に導出します。復元するときに、すでに SUCCEEDED 状態のノードについて、その依存する出力要約が当初記録したものと一致しないことがわかったなら、上流の内容が変わったということです。この場合は単純に成果物を再利用することはできず、「無効」とマークしたうえで、無効化の伝播ルールに従って下流もまとめて無効化しなければなりません。この連鎖こそが、「ユーザーが途中で要望を変えた」ことを正しく扱える技術的な前提になります。

人間介在の中断と再開:要望が変わったときに途中からどう続けるか

ここまでの各節で扱ったのは「機械が予期せず死んだ」場合です。この節が扱うのは「人が自分の判断で止めたあとで、違う要望を投げかけてきた」場合です。長いタスクのシステムでもっとも悪い体験を生みやすいのはここです。多くの実装がこれを「コンテキストを保存して、ユーザーが戻ってきたら会話を続ける」という単純化にしており、それがまさに誤りだからです。

まずはなぜ誤りなのかを説明します。ユーザーが 2 時間席を立ったあと戻ってきて、「あの 20 個の古いモジュールはとりあえず放っといて、先週新しく追加した分だけ扱って」と言ったとします。この一言が変えたのはタスクの目標の集合であって、タスクの言い回しではありません。もしこれを新しい user message として履歴に追記すると、モデルは自己矛盾したコンテキストに直面します。前の数十ステップの推論はすべて「すべてのモジュールを処理する」という前提の上に成り立っているのに、最新の一言がその前提を否定する。モデルは通常エラーを出しません。両方を折り合わせようとして、両方の前提が混ざった結果を出してしまいます。

正しいやり方は、「要望変更」をタスクグラフ上の無効化イベントとしてモデル化することです。あとは伝播ルールに、どの完了済み作業を無効化し、どれはまだ有効かを判断させます。そのために、まず 1 つだけ準備があります。ノードを、それが依拠している「タスクの前提」でグループ化しておくことです。

前提フィンガープリント:無効化範囲を計算できるようにする

各ノードは実行するときに、input_digest に加えて、前提タグ(precondition tags)を記録しておくとよい。そのノードがどのタスクレベルの仮定に依存しているかという情報です。たとえば「対象モジュールを絞り込む」というノード的前提タグは {"scope": "all_modules"} かもしれません。ユーザーが scope を {"scope": "new_modules"} に変えたとき、業務上の意味を一切理解していなくても、ラベル集合の差分演算を 1 回行うだけで、どのノードの成果物が無効になったかを計算できます。

def invalidate_by_precondition(graph: TaskGraph, changed: dict[str, str]) -> list[str]:
    """前提变化后,算出必须作废的节点集合。

    changed 形如 {"scope": "new_modules"},表示某个维度的
    任务前提被改成了新值。只影响依赖该维度的节点。
    """
    stale = []
    for node in graph.nodes.values():
        pre = node.meta.get("preconditions", {})
        if any(pre.get(k) != v for k, v in changed.items()):
            stale.append(node.key)
            #连带:任何依赖了 stale 节点的下游,其产出也不可信了
            for other in graph.nodes.values():
                if node.key in other.deps and other.key not in stale:
                    stale.append(other.key)
    return stale

この関数の実質的な価値はコード量にはなく、「途中から続ける」という曖昧なプロダクト上の問題を、確定的なデータの問題に変えたところにあります。1 回実行するだけで、次が分かります。14 個のノードをやり直す必要があり、9 個は再利用でき、そのうち 3 個は先にユーザーへパラメータを確認しなければならない。この能力がなければ、プロダクト側は「申し訳ありませんが、要望の変化が大きすぎます。最初からやり直してください»،という回答しかできません——そしてそれは、ユーザーがもっとも受け入れがたい答えなのです。

停止のセマンティクスは区別する

多くのシステムには「一時停止」の状態が 1 つあるだけで、異なる意図を表現できません。実際には少なくとも 4 種類あり、復元経路はまったく異なります。

  • 保留(park):タスク自体に問題はありません。単に外部の信号を待っているだけです。たとえば CI の結果を待っている、ユーザーの返信を待っている、というケースです。原地で復元できますが、待っている間は予算を消費し続けてはならず、停滞検出に「固まっている」と判定されるのも避けるべきです。
  • 縮小(narrow):範囲が小さくなる。たとえば新しく追加されたモジュールだけを対象にする場合です。すでに完了した 9 個のノードは依然として有効で、無効になるのは除外された側だけです。
  • 巻き戻し(rewind):ある完了済みノードが計算ミスだと判明し、そのノードと下流を計算し直す必要があります。復元ロジックがもっとも頻繁に起動する経路でありもっとも書くのが難しい経路です。
  • 終了(abort):タスクに意味がなくなった場合です。監査のために状態は残しますが、以降いかなるアクションも発生しません。

この 4 つを単に paused という真偽値 1 つに潰すと、短期的に列挙値を少し減らせますが、長期的には大きな代償を支払います。もっとも厄介なのは「縮小」です。完了済みの成果物を残したうえで一部だけを正確に無効化する必要があるのに対し、残り 3 種類はこのような精密な操作を必要としません。分類を明確にした結果、コード量は逆に下がります。

  • 停止設計の妥当性を測る経験則:保留状態では、タイムアウト監視と停滞検出の両方を必ず停止しなければなりません。そうでなければ、ユーザーが昼休みで 2 時間席を空けているあいだに、待機中のノードへ繰り返しアラームを飛ばし続け、最終的にアラーム疲れによって、本当の固まりに誰も気づかなくなります。

再確認の粒度

ユーザーが戻ってきたら、Agent は能動的に「何を保持し、何を無効化し、次は何を実行するか」を復唱し、そして推測できない相違点についてのみ質問すべきです。判断基準は単純です。新要望が旧要望に少しの制約を足すだけで導出できるなら、質問はしません。できないなら質問します。ただし一度に全部まとめてです。この一点が体験に与える影響は、モデル能力の差よりもずっと大きいです。ユーザーが不平を言うのは、たいてい「結果が間違っている」ことではなく、「こいつは直接私に聞けばよかったのに、自分でこじつけた方向を 20 分も走ってしまった」ということです。

コストの上限と時間予算:予算は強制であって、「お願い」ではない

長いタスクのコストには 2 つの発生源があり、性質がまったく違うので分けて管理する必要があります。トークンコスト(ステップ数に比例して増えていくもの)と、注意力コスト(ユーザーが強制的に待ち、結果を読み直すにかける時間)です。後者は前者よりも刺さることが多いのですが、いっさい計測されていないため、最適化の射程圏に入らないままです。

予算について、個別に強調しておきたい経験があります。予算をプロンプトの中に「コストに注意しましょう」と 1 行書く方法は、ほとんど効果がありません。理由は単純です。モデルが自分の支出を確認するタイミングは あと で、プロンプトの中の制約は 事前 の意図にすぎません。両者のあいだには、強制的な接続がありません。本当に効くのは スケジューリング層 での強制です。次のノードをディスパッチする前に「もう 1 つこのノードを配って、予算は足りるのか」と問うことです。

3 段の予算ゲート

実用的な方法は 3 つのゲートを設け、それぞれに異なる縮退動作を対応させることです。

ゲート発動条件縮退動作これが効く理由
ソフトしきい値予算の 70% を使った、または残りの時間が総所要時間の 70% を下回ったコンテキストを圧縮し、履歴を刈り込み、低価値なツールを無効にする節約されるのは「単価」であり、計画済みの作業量には影響しない
ハードしきい値予算を 100% 使い切ったホワイトリスト内の仕上げノードだけを実行を許す必ず 1 つでも納品できる成果物を残せる。中途半端に終わらない
緊急停止予算または時間のいずれかが 100% を超えたタスクを直ちに凍結し、現在の最良の結果と未完了リストを出力する「走っている途中で kill されて成果物が全部消える」事態を避ける

ハードしきい値にある「仕上げノードのホワイトリスト」が、長いタスクの体験を制御可能にする鍵です。 タスクが 95% のところで止められ、書きかけのドキュメントを 1 つ出したにとどまるなら、それはゼロに等しい価値です。 最初からスケジューリング層で、「レポートを生成する」「未完了リストを生成する」「ユーザーに通知する」という 3 つのノードを あらかじめ予算のあれこれにかかわらず実行を許すようにしておけば、予算が尽きたときにも 「24 件中 18 件は完了しました。残る 6 件は予算上限のため未実行です。次に行うことを提案します」と 伝える成果物を届けられます。 これは長いタスクの中で、費用対効果の観点でもっとも高い設計ディテールです。

時間予算はトークン予算よりも能動的な管理が要る

トークンの消費はステップ数にほぼ比例するので、予測できます。一方、経過時間は制御しにくい要因に引っぱられがちです。レート制限によるバックオフ、ネットワークの揺らぎ、サブタスクの想定外な伸び。両者を区別しないと、「予算は 40% 残っているのに、残り時間は 8 分」というタスクが最後の瞬間に慌ただしく締まり、品質が崖のように落ちます。

class BudgetGovernor:
    """在派发节点前做准入判断,而不是在超限后才报错。"""

    def __init__(self, token_cap: float, wall_cap: float, soft_ratio: float = 0.7):
        self.token_cap = token_cap
        self.wall_cap = wall_cap
        self.soft_ratio = soft_ratio

    def admit(self, node: Node, used_token: float, elapsed: float) -> str:
        #白名单节点永远放行:收尾产出比预算纪律更重要
        if node.meta.get("always_admit"):
            return "admit"

        token_left = self.token_cap - used_token
        time_left = self.wall_cap - elapsed

        #估算这一步的开销,留出 20% 余量给不确定性
        need = max(node.meta.get("est_token", 0) * 1.2, 1)
        if token_left < need:
            return "deny_hard"
        #剩余时间已经不够再跑一个常规节点
        if time_left < node.meta.get("est_wall", 30) * 1.5:
            return "deny_hard"

        if token_left < self.token_cap * (1 - self.soft_ratio):
            return "admit_degraded"   # 允许,但要求压缩上下文
        return "admit"

est_token と est_wall という 2 つの推定値に注意してください。これらは精密である必要はありません。実際にはノード種別の履歴平均で十分であり、同じ kind のノードの中央値だけでも動きます。重要なのはこのフィールドが存在することです。見積りを持たないスケジューラは、お金を燃やし終えてからしか問題を検出できません。粗い見積りを持つスケジューラなら、残りの予算を最重要のいくつかのノードに先に割り当てられます。「予算を自覚したタスクの並び替え」自体がそのまま品質の向上になります。リソースが乏しいときに価値ある作業を先にこの作業を優先させ、順序にたまたまぶつかってしまう事態を避けられるからです。

ついでに、admit_degraded という戻り値は説明に値します。コンテキストの圧縮は、履歴を単純に切り詰めることではありません。履歴を切り詰めることが、まさにいちばん重要な制約を失わせるからです。正しい圧縮は、保持すべきものを残すことです。タスクの中心的な制約、最初の数ステップの重要な結論、現在どの段階にいるか、そしてまだ解決していない問題。中間の検索ノイズは捨てます。ある履歴が「ノイズ」かどうかの判定基準は、それが以降の判断に影響しうるかであり、「長いかどうか」ではありません。

長いタスクの進捗の可観測性:ずっと放置しておけるようにする

4 時間走るタスクが、その 4 時間まったくブラックボックスなら、誰も本当に信用しません。これは心の問題ではなくエンジニアリングの問題です。ユーザーは在任何時点でも 1 文で「いま何をしているのか、あとどれくらい,能否,判断して待つ価値があるか」に答えられなければなりません。それができないシステムに対して、ユーザーが理性的に取れる選択肢は 5 分おきにリロードして、そして諦めることだけです。

ハートビートは情報を持ってこなければ意味がない

もっとも素朴な手法は各ノードがハートビートのタイムスタンプを更新することですが、これでは「生きているか」にしか答えられず、「何をしているか」には答えられません。有用なハートビートには少なくとも 3 つのものが含まれます。いま実行されているノードの key、そのノードの中で完了した作業単位数と総数、そしてこの実行における累積コスト。この 3 つが揃って、はじめて簡易的な残り量の推定が成り立ちます。

残り時間を推定するときの重要なディテールが 1 つあります。 平均値を使ってはならず、分位数を使うべきです。 4 時間のタスクでは、最初のノードはたいてい早いです。数ファイルを読めば終わるからです。 後のノードはだんだん遅くなります。テストを走らせ、CI を待つ必要があるからです。 平均所要時間で線形に外挿すると、残りの時間を系統的に低估します。 それは予算のスケジューリングが最終段階で慌ただしく締まる原因になります。 P80 か P90 で見積もれば保守すぎますが、ユーザーに示す数値は少し悲観的なだけで、 通常は楽観したあとに裏切られるよりずっとよいものです。

より重要なのは、不確実性を正直に表現することです。 「残り 40 分、確信度は低い」と伝えるほうが、分単位まで precise な嘘の数字より役に立ちます。 この領域には何度も現れる教訓があります。 人に偽の precise な数字を渡すと、人はその数字に合わせて待ち方を組み立てます。 その数字が当たらなかったとき、システム全体への信頼も一緒に失われます。

停滞検出:新情報量は経過時間より良いシグナルになる

タイムアウトは「固まっている」ことには気づけますが、「空回り」することには気づけません。 同じパラメータで同じ検索 API を繰り返し呼び、同じファイルを繰り返し開き、 ほとんど同じ思考を繰り返し出す Agent を考えてください。 どの呼び出しも 2 秒以内に成功して返ってくる。 タイムアウトの観点ではすべて正常ですが、実際にはすでに 20 分を無駄にしています。

有効な検出方法は新情報量を数えることです。 最近のいくつかのステップのツール出力を正規化して重複排除し、 新しい内容の量がどのくらいあるかを数えます。 この指標がほぼゼロに近づいたら、Agent は原地で回っているということです。 このときにやるべきことは、ただプロセスを殺して終わりにすることではありません。 もっと有用なことをするのです。 直近の重複した内容と現在の目標をコンテキストに押し戻し、 「お前はこれらの方法をすでに試し、進展がない。別の道を選べ」と明示的に伝えます。 固まっているように見えるタスクのかなりの部分は、 モデルが「自分は何を試したか」を意識していないだけなのです。

状態そのものがユーザー UI になる

長いタスクの可観測性には素晴らしい性質が 1 つあります。 その状態は構造化され、かつ永続化されているので、 そのままユーザーインターフェースとして描画でき、追加の計測点位を仕込む必要がないからです。 これは「ブラックボックスのプロセスを走らせる」こととは本質的に違います。 任意の時点でスナップショットを生成できます。 どれが完了し、どれを実行中で、どれが破棄済みで、どれが人を待っていて、 予算をどれだけ使ったか、重要な結論は何か。

このスナップショットは、それ自体で復元メカニズムでもあります。 ユーザーが UI 上で完了済みのノードをクリックして「この結論は違う」と言えば、 システムはどこから巻き戻すか、どこから再開するかを知ることができます。 ユーザーが「47 ステップ目に戻ってやり直す」と手で書き込む必要はありません。 可観測性と可回復性は、ここでは 2 つの機能ではなく、同じ状態構造の 2 つの読み取り方向です。 これは長いタスクのランタイムを設計するうえで、もっとも記憶に留める価値のある構造的な結論です。

トレースは完全さではなく読みやすさで組織する

最後の層は追跡可能性です。長いタスクのログがただの生のイベントストリームなら、人は読み進められません。 しかし要約だけを残すと、問題の切り分けに必要な詳細が失われます。 実用的な妥協は 2 層に分けることです。 意思決定層には「なぜこのアクションを選んだのか、どの代替案を捨てたのか、判断根拠は何か」を記録します。 この層は人間向けで、自然言語で組織する必要があります。 実行層には生のリクエストとレスポンス、ツールの完全な入出力、正確なタイムスタンプを記録します。 この層は機械と事後の障害解析向けなので、どれだけ冗長でも構いません。

意思決定層は、長いタスクに特有の層です。短いタスクでは必要ありません。 短いタスクなら開発者が全過程を直接読めますが、4 時間タスクの全過程を読み切る人はいないからです。 この層の価値は、事後レビューでもっとも明白に現れます。 Agent が誤った結論を出したとき、それが「その時点での情報が不足していた」のか、 「情報は十分だったが推論を誤った」のかを区別できます。 前者なら情報獲得の刷新が必要で、後者なら推論の改善が必要です。 この 2 つの改善方向はまったく違うので、混ぜてしまうと、どの部分を直せばよいのか分からなくなります。

ロールバックか再開か:失敗に誠実な分類をつける

最後の問題であり、もっとも判断が難しい問題です。 タスクが失敗したあと、最初からやり直すのか、失敗した地点から再開するのか。 現実の多くのシステムでは答えは「場合による」であり、 コードの上では「最後のチェックポイントから一律でやり直す」というデフォルトになりがちです。 このデフォルトは、長いタスクではたいてい安全でも経済的でもありません。

「何を信じられるか」で分類する

分類の根拠は、失敗の種別(タイムアウト、エラー、アサーション失敗)ではなく、 無効化の範囲でなければなりません。

  • 純粋な計算の失敗で、外部書き込みがない:そのままリトライします。 コストはトークンだけだし、温度を上げて多様性を増やすことすら可能です。
  • 外部書き込みは起きたが、その書き込み自体が冪等:そのままリトライします。 重複実行しても結果は変わらず、わずかに時間を無駄にするだけです。
  • 外部書き込みが起きた、しかも取り消せない:リトライしてはいけません。 人が判断する必要があります。何が実際に起きたのかを確認してから、補償するか受け入れるかを決めます。 まさに前の effect_log と意図ログが存在する理由です。 これらがないと「そもそも書いたのかどうか」すら答えられません。
  • 結論性の失敗:Agent が出した中間結論が誤りだったと判明した(テストが落ちた、 後続のステップで前提が成り立たないことが分かった)。 リトライでは解けず、リトライは同じ思路でもう一度間違えるだけです。 必要なのは分岐点まで巻き戻って分岐し直すことであり、 コンテキストには「直前の経路は却下された。失敗理由は X」と明示的に書き込むことです。
  • 仕様そのものの失敗:タスクの定義自体に問題があるか、 ユーザーが途中で要望を変えた。この種類の失敗に対するリトライは純粋な資源の無駄であり、 正しい動作は人手を仰ぐことです。

5 番目の分類が、1 番目に誤診されがちです。 2 時間かけて走らせ、最後に誤った形式のドキュメントを納品したタスクに対して、 システムが出す診断はしばしば「出力の解析に失敗しました、リトライします」ですから。 その結果、リトライしても改善しないタスクが、さらに 2 時間ぶんの予算を燃やしてしまうのです。 リトライが正しいための前提は「失敗原因にランダム性があり、リトライによって結果の分布が変わる」ことです。 失敗が決定的なものなら、リトライの期待利得はゼロで、期待コストは予算全額になります。

補償トランザクション:Agent のシーンでロールバックが複雑な理由

データベースには参考にできる成熟したトランザクションモデルがありますが、 Agent のシーンの外部世界にはトランザクションがありません。 あなたの Agent が 3 つの PR を送り、2 つのメッセージを送り、設定ファイルを 1 つ書き換えました。 これらはあなたが制御できない場所に散らばっています。

実行可能な代替案はロールバックではなく補償です。 不可逆なアクションごとに補償アクション(PR を閉じる、メッセージを撤回する、ブランチを削除する)を事前に登録し、 逆順に実行します。ここには残酷な前提があります。 最初に何の補償が必要かを想像しておかなければならない、ということです。 タスクが 2 時間走ったあとに放棄することを決めても、前のどのアクションにも補償が登録されていなければ、 人手で片付けるしかありません。

帰結は直接的です。不可逆なアクションの密度が、長いタスクのリスクの上限を決めます。 4 時間走って、最後まで読み取り専用で、最後にドキュメントを 1 つ出したタスクなら、 復元もロールバックも簡単です。 しかし 4 時間走って、途中で 20 件もの変更を投入したタスクなら、 4 時間目の時点ですでに完全なロールバック能力を失っています。前へ進むしかないのです。 「PR を作成する」「メッセージを送る」アクションをタスクの末尾にまとめて実行するのは、 それ自体が一種のリスク制御であり、単なる手続き上の利便ではない、ということです。

再開の前提条件チェックリスト

あるタスクが再開をサポートするなら、次の項目は設計の段階でもう確定させておく必要があります。 あとから足しても埋めることはできません。

  1. 各ノードの key は、ランダムに生成せず入力から再計算できる。
  2. 外部副作用を持つ各ノードに、安定した照会可能な冪等識別子がある。
  3. LLM ノードの成果物は成功したらすぐ凍結し、復元時はディスクから読んで再計算しない。
  4. RUNNING 状態はリースとハートビートを持ち、プロセスが死亡したら自動的に回収される。
  5. 依存する成果物には要約が付いており、要望が変わったときに無効化範囲を計算できる。
  6. 保留・縮小・巻き戻し・終了という 4 つの意図が別々にモデル化されている。
  7. 予算がスケジューリング層で強制され、仕上げノードのホワイトリストがある。
  8. 不可逆なアクションの順序が意図的にタスク末尾に置かれ、補償アクションが登録されている。

この 8 項目のうち、モデルに関するものは 1 つもありません。 すべて状態とライフサイクルに関するものです。 それこそがまさに 1 つの事実を示しています。 Agent に長いタスクを走らせるときのボトルネックは、もともとモデルが何時間考えれるかではなく、 システムが「いま思考している」ということを、 安全に時間の外へ保存できるからです。

並行ブランチとレート制限:本当のボトルネックはスケジューラにある

タスクグラフが並列をサポートした瞬間、よく見落とされている問題が顔を出します。 タスク全体のスループットは、モデルではなく外部サービスのレート制限ウィンドウに よって決まることが多いのです。 24 ステップしかないように見えるタスクでも、そのうち 18 ステップが同じコード検索 API を叩き、 その API のクォータが毎分 10 回なら、あなたのスケジューラがどれだけ賢くても、 このタスクの下限は 2 分です。

ここから得られる判断は単純です。 タスクグラフを設計する段階で共有される外部クォータを識別し、 クォータを第一級の市民として扱う資源としてスケジューリングするべきです。 各ブランチがそれぞれ壁にぶつかって、それぞれバックオフしてリトライするのではなく。 各ブランチが自分の待ち時間を重複して消費するのが悪いところであり、 それらが同調してリトライする可能性が高いというところであり、 本来なめらかなトラフィックをスパイクに変え、レート制限をさらに悪化させるからです。

「いつ並列にして、いつ直列にするべきか」については、実務的な判断基準があります。 ブランチの間に書き込みの競合があるかどうかを見ます。 読み取り専用のブランチは、だいたい常に並列にすべきです。あいだに依存関係はなく、 唯一の制約は外部クォータだけです。 しかし書き込みが絡むなら、まず 1 つの質問に答えなければなりません。 2 つのブランチが同じ対象に書き込む可能性があるか。 可能性があるなら並列にはできません。 可能性がなければ安心して並列にしてよく、そしてその判断の結論を タスクグラフに明示的に記録しておくべきで、ランタイムに動的検出を任せるべきではありません。

この明示的な記録は重要であるだけでなく、同時に復元の前提でもあります。 もし 2 つのブランチがリソース不足のために直列化されていたなら、 復元後に並列条件が変わった場合(たとえばレート制限ウィンドウが緩和されたとき)、 元の直列という決定を保持したいのか、それとも再評価したいのかを決める必要があります。 元の決定を保持するほうが安全で、予測可能です。 再評価するほうが攻めた方法ですが、成果物の順序が変わり、結果も変わる可能性があります。 デフォルトは元の決定を保持し、状態に「この直列化はクォータ戦略によって決まった」と注記しておくべきです。 そうすればユーザーが進捗を見たとき、遅い理由を理解でき、システムがバグを疑わずに済むからです。

また、外部クォータに由来しない種類のブランチの競合が 1 つあります。 共有されたモデルコンテキストです。 多くの長いタスクは「複数の専門 Agent が同じ資料に順番に意見を述べ、最後にまとめる」という設計です。 このようなタスクは並列にできる性質に見えますが、隠れたコストがあります。 並列のブランチどうしはお互いの結論が見えないので、同時に互いに矛盾する分析を行い、 最後の統合段階が衝突の調停に大量のコストを費やします。 もしこれらの結論の間に収束性がある、つまり後の分析が前の判断を修正するはずであるなら、 直列のほうがむしろよいのです。 この種のタスクを「依存なし、並列可能」と誤診することは、長いタスクの中でもっとも目立たない無駄の 1 つです。

リトライのバックオフは決定的でなければならない

最後に、見落とされがちなディテールを 1 つ。バックオフ戦略は決定的でなければなりません。 多くの実装はジッター付きのバックオフを使いますが、単一リクエストのシナリオでは正しい判断であり、 長いタスクでは回復可能性を壊してしまいます。 「リトライのタイミング」自体が状態の一部となり、その状態はプロセス再起動で失われるからです。 結果は、復元後のリトライタイミングがもとのリズムと合わず、 バックオフが終わった直後にまたリクエストを出して、緩和したばかりのレート制限を叩き返すのです。 正しいやり方は「次回リトライ可能になる時刻」を明示的なフィールドとして永続化し、再起動後にそれに従うことです。

これは、長いタスクが「ステートレスなリトライ」という単純なモデルを使えない理由でもあります。 プロセスをまたぐ待機を一度導入すると、 待機という意図自体を保存しなければならず、 新しいプロセスはバックオフウィンドウが有効になっていることすら知り得ないからです。

状態ストア自体がボトルネックになる

すべての状態を 1 つの状態機械の設計にまとめると、1 つのエンジニアリング上の現実を無視しやすくなります。 状態を保存するコストは、最終的に状態を実行するコストを上回ります。 数百個のノードを持つタスクでは、1 ステップ実行するたびに完全なスナップショットを書き込む必要があります。 状態が大きくなるにつれ、1 回の書き込みコストは相当なものになります。 さらにチェックポイントの粒度が細かい場合が重なると、書き込み増幅が十分に大きくなり、 ストレージ層がボトルネックになり得ます。

よく使われる 3 つの手法には、それぞれのトレードオフがあります。 全量スナップショットは実装がもっとも単純で、復元時は 1 回読むだけで���みますが、 状態が大きくなると書き込み増幅が深刻になります。 イベント記録方式(event sourcing)は状態変化の増分だけを記録するので書き込みは安く、 完全な操作履歴が自然に得られます。 しかし復元時にイベントストリーム全体を再生する必要があり、状態が大きいと復元自体が遅くなります。 定期的なスナップショット + 増分ログは前 2 つの折衷案です。 復元時は直近のスナップショットとその後の増分だけを読むので、両者の長所を取れますが、 代償として実装の複雑さも上がり、スナップショット自身のバージョン互換性の考慮も必要になります。

提案を 1 つだけするなら、 「復元に必要な最小の状態」と「監査に必要な履歴」を分けて保存することです。 前者は小さく、全体を高速に読み出せる必要があるのでスナップショットにします。 後者は大きくてもよく、追記向きなので、イベントストリームとして書きます。 これらを混ぜるのはもっともよくある設計上の誤りです。 監査情報は通常、復元情報より 1 桁から 2 桁大きいためです。 まとめると、復元の読み出しがずっと遅くなります。

スキーマの移行:状態機械を長く生かせる

長いタスクのもう 1 つの特徴は長生きすることです。 タスクが 3 日間保留されたあとに再開されたとき、そのあいだにあなたのコードはすでにいくつかのバージョンを経ているかもしれません。 状態構造にバージョンの印がなければ、復元時に認識できないフィールドの組み合わせに遭遇し、 軽い場合はエラーになり、重い場合は古い状態を誤った解釈で読み取り、黙って誤った結果を出しはじめます。

最低限の実務は 3 つです。 状態オブジェクトに schema_version を持たせる。 読み取りのときに明示的なマイグレーション関数を経由させ、旧バージョンを現行バージョンへ引き上げる。 マイグレーションに失敗したら復元を拒否してアラームを上げる、未知のフィールドを抱えたまま走らせない。 第 3 点がとくに重要です。 「正直に失敗する」ことは「曖昧さを抱えたまま実行する」ことよりはるかに良く、 後者が原因となって起きる問題は、事後ではほとんど特定できません。

これは 1 つの設計方針も説明しています。 状態の中のフィールドは意味的に安定しているものであって、実装に結合したものであってはいけません。 たとえば「チェックポイント」を「ノード N 個まで完了」と記録する方が、 「ワークスペースのスナップショット #7 を保存し、次の 12 個のファイルのハッシュを含む」と記録するよりはるかに安定しています。 後者は圧縮方式を変えるたびにすべて無効になります。

コンテキスト圧縮:長いタスクでもっとも高くつくのは推論ではなく、忘れることである

短いタスクではコンテキスト管理は問題になりません。 セッション全体が最初から最後まで収まるからです。 長いタスクではそれが第一の問題になります。 4 時間のタスクは数十万トークンのツール出力を生み出し、その大半は使えばもう必要ないものです。 読んだファイルの全文、失敗したクエリ、あとから結論で覆された中間仮説。 第 3 時間まで見えていなければならないものは、たぶん数十行しかありません。

圧縮(compaction)とはこの手法の名前ですが、多くの実装はこれを純粋に技術的なアクションとして扱っています。 上限になったら、ある規則で切り詰めるか要約するだけです。 これは 2 時間のセッションなら足りますが、日をまたぐタスクでは、 うっかり見落とすような種類の故障を作り出します。 タスクはエラーにならず、ただいつの間にか別のタスクに変わっている。

圧縮の単位は作業単位にすべきで、トークンにすべきではない

トークンしきい値で発動する圧縮には隠れた欠陥があります。ステップの途中に切ってしまうのです。 1 回のツール呼び出しが 3 万トークンの検索結果を返し、圧縮がたまたま前半を読んだところで起きる。 するとモデルが見るのは結論のない出力です。 より悪いのは、その呼び出しがまだ消費されないうちに圧縮されてしまった場合です。 再演したときに別の内容を返す可能性があり、下流の推論全体が変化した証拠の上に成り立ってしまいます。

正しい切り方は意味的な境界で切ることです。 1 つの作業単位(どのファイルを読んだ、どの結論を導出した、何を除外したか)が圧縮の最小単位です。 ある単位が完全に終わり、結論が状態に書き込まれたあとでこそ、その過程の詳細を捨ててもよいのです。 だからこそ圧縮は、コンテキストの長さではなく、タスクグラフの状態によって発動させるべきなのです。

圧縮のあとに残すべきものは 4 種類だけです。 タスクの中心的な制約、最初の数ステップの重要な結論、現在どの段階にいるか、そしてまだ解決していない問題。 中間過程は捨ててよいのですが、1 つだけ強調に値する例外があります。

  • 失敗した試行こそ、圧縮でもっとも失われやすく、もっとも失うべきでないものです。 「ファイル名で一致させて調べたが見つからなかった」という情報は、 数時間後にも価値を持ちます。これを捨てると、Agent は同じやり方をもう一度試してしまいます。

制約の静かな消失こそ、本当の代償である

圧縮でもっとも見つかりにくい被害は、事実の消失ではなく制約の消失です。 ユーザーが最初に言った「テストファイルだけを変えて、プロダクションコードには触らない」という言葉は、 1 回目の圧縮で「ユーザーが複数のモジュールの変更を要求している」と要約され、 2 回目の圧縮で「ユーザーがスコープを確定済み」になり、 3 回目のあとは完全に消えます。 そのあとも Agent は正しいことをしています。ただし、そのやっていたことはもう要件を満たしておらず、 それを指示する信号はどこにもありません。

対策は制約を会話から独立した永続オブジェクトとして昇格させることです。 タスク開始時に構造化された項目として抽出され(スコープ、禁止事項、受け入れ基準、維持すべきインターフェース)、 単独で保存し、単独で提示し、圧縮のたびにそのまま書き戻します。 要約に残すことに期待するのではなく。 この方法のコストはきわめて低く、得られるものはタスク全体の要件一貫性が「たぶん残っている」から「確実に残っている」に変わるだけです。

記憶の外部化:タスクの記憶をディスクに落とさせ、ウィンドウに詰めさせない

コンテキスト圧縮が解決するのは「収まらない」という問題であり、記憶の外部化(externalized memory)が解決するのは「収まるが見つからない」という問題です。 両者の失敗モードはまったく違います。前者はタスクを次第に忘れさせていく。後者は数十万トークンのなかで2 時間前に見つけたはずの手がかりを何度も見つけられなくなる。そしてもう一度探し、また忘れ、また探す、という悪循環に入ります。

長いタスクの記憶は「覚えておくべきテキスト」として扱うのではなく、問い合わせ可能なデータセットとして扱うべきです。 判定基準は単純です。記憶が「モデルが覚えている」ことに依存して初めて効くなら、それは 4 時間のタスクのなかでは確実に成立しません。

3 層の記憶と、それぞれの書き込みしきい値

実務では記憶を 3 層に分けます。各層の書き込みしきい値とライフサイクルは異なり、この階層設計は外部化の設計でもっとも先に考えておくべき部分です。

  • 作業記憶:いま処理しているコンテキスト。圧縮とともに捨てられる。「いまこのファイルを読んでいる」に対応します。書き込みしきい値はゼロです。既定のパスだからです。
  • 情動記憶:行った行動とその結論——何を読んだか、何を見つけたか、何を試したか、なぜ諦めたか。この層には書き込みしきい値が必要です。行動が再利用できる結論を生んだときだけ書き込むようにしないと、4 時間動くタスクが「file_00123.cs の 40 行目を読んだ」を 1 万件以上書き込むことになり、本当に重要な結論が埋もれてしまいます。
  • 意味記憶:タスクをまたいで再利用できる知識。たとえば「このリポジトリのテストは先に build を実行しなければならない」。この層はさらに高いしきい値が必要です。検証され、タスク終了時に人が確認すること。確認せずに意味記憶へ書き込むのは誤りが固定される最短経路です。

この 3 層を混ぜるのはよくある誤りです。失効したときの后果がまったく違います。作業記憶が失効すればタスクは鈍り、情動記憶が失効すればタスクは労働を繰り返し、意味記憶が失効すれば次のタスクが誤った場所から始まります。最後の層の汚染は不可逆です。タスク自体はもう終わっていて、誰もそれを検査しないからです。

書き込みしきい値:すべての情報が記憶になるわけではない

情動記憶にしきい値を設けるとき、直接使える判定基準が再利用可能性テストです。 この行動は明日もう繰り返されないと仮定します。新しいタスクがその情報を知っていたら、選ぶものは変わるでしょうか。 変わるなら書き込みます。変わらないなら(たとえば「あるファイルを読んだら空だった」)、書き込んではいけません。 この種の記録は、ほぼ常に純粋なノイズだからです。

逆に、無条件に書き込まなければならない情報が 1 つあります。失敗した試行とその理由です。 外部化の記憶のなかで価値が最も高いと同時に、もっとも捨てられやすい種類の情報です。 Agent があるディレクトリを 40 分かけて探して、ようやく「そこに何もない」と確認したなら、 この情報が残っていれば、次回は 40 分かける必要がない。

記憶の検索は、記憶の書き込みより見落とされやすい

多くのシステムは書き込みを丁寧に設計しながら、検索のほうで手を抜きます。 すべての記憶をコンテキストへ押し戻し、モデルが関係するものを自分で気づいてくれることを期待している。 これは検索問題を注意力問題へ矮小化することで、記憶量が数千件を超えた時点で確実に破綻します。

現実的なやり方は、検索をタスク状態によって駆動するものとして、モデルに起動させないことです。 スケジューラは次のノードが何であるかを知っている。だからそのノードがどの種類の記憶を必要とするかも、おおよそ分かっています。 そのノードを割り当てる前に、関連する数件を検索してノードの入力に添えておきます。 この方向の反転は重要です。「Agent が思い出して調べる」のではなく「システムが何を知るべきかを知っている」のだからです。後者は長いタスクでははるかに信頼できます。

記憶には鮮度も必要です。1 時間前に設定ファイルから読んだ値は、30 分後にはすでに無効になっていることがあります。とくにタスクの実行中に他の人が同じリポジトリを編集している場合はそうです。 各記憶に生成時刻と出所(どのファイルか、どのコマンドの出力か)を付けておき、検索のときに採信すべきかどうかを判断できるようにします。 これは些細に見えますが、長いタスクの結論が「現在の世界に基づく」のか「1 時間前の世界のスナップショットに基づく」のかを決めます。

タスク分解と replanning:計画は見積であって、約束ではない

タスク分解は、短いタスクでは一度きりの決定です。 長いタスクでは繰り返し見直される見積りになります。 違いはこうです。短いタスクの分解は 30 分 silo のうちに正しいか誤りかが確定します。 長いタスクの分解は何十回も使われ、そのあいだに絶えず、環境にFriendly返されます。

約束として扱うシステムは、数十分のlocationで「計画は現実と合わなくなったのに、 でもシステムはplans的实现計画 persiste,变成了-resource困大量的-feral」に直面します。 これが典型的な失敗形態の 1 つです。Agent はすでに成立しなくなった計画を完了させるために忙しく、 すべての努力が 잘못された目標に注がれている状態です。

分解の粒度:重要性ではなく検証可能性で切る

よく使われる分解のヒューリスティクスは、重要性で切ること——つまりもっとも重要な部分から先に片付けることです。 これは短いタスクでは正しい戦略です。短いタスクならほぼ必ず最後までできるからです。 しかし長いタスクで重要性で切ると、1 つの誤りを합니다。 もっとも重要な作業はたいていもっとも不確実で、もっとも失敗しやすく、 结果是置在最前面、被最充分地踩坑,而后面那些确定性高的工作一点都没做,最终交付为零。

長いタスクでは、検証可能性で切ることが適切です。 各ノードに、何らかの手段で「できた」と判定できる明確な信号を持たせることです。 コードを読む、テストを走らせる、API を呼んで構造化された戻り値を得る——これらはすべて明確な信号があります。 一方で「建築を分析する」「妥当性を評価する」のように判定基準のないノードは、 最後に置くか、検証できる粒度まで分割するべきです。

理由はリソース配分です。長いタスクは必ず中断されます(予算切れ、ユーザーによる一時停止、環境エラー)。 したがって「中断されたとき何が残るか」が中核的な設計課題になります。 検証可能性で切れば、どの接頭辞も価値のある成果物になります。 12 個のノードが完了したなら、その 12 個のノードの結論は成立しており、残りの 12 個に依存していないからです。

replanning:トリガー条件のほうがアルゴリズムより重要である

replanning には、よくやってしまうことが 2 つあります。 1 つは、高頻度のアクションとして扱うことです。数ステップごとに replanning する。 その結果、計画が絶えず振動し、最初数ステップの結論が検証される前に覆され、 タスクが原地で揺れるようになります。

もう 1 つは、失敗後にだけ発動させることです。 しかし長いタスクでもっとも replanning が必要な時刻は、しばしば失敗が起きていないときです。 元の計画の何らかの前提が成立しなくなったと分かっただけだからです。 たとえば「20 個のモジュールの循環依存を直す」というタスクで、 15 個目まで進んだところに残りの 5 個が同じ根本原因に由来すると分かったなら、 1 つずつ直すことを続けるのは純粋な無駄です。

実用的なトリガー条件は 3 種類です。模型による即興判断ではなく、明示的に定義することを勧めます。

  1. 証拠の衝突:新しい観察が既存の結論と矛盾する。 ただしこれにはタスク状態に「既存の結論」が明示的に記録されている必要があります。 そうでなければ衝突を検出できません。
  2. パターンの認識:連続するいくつかのノードで同形の結果が出る (たとえば同じ種類のエラー、同じ根本原因の複数のかたち)。 これは通常、実行の誤りではなく分解の誤りを意味します。
  3. 限界効果の低下:直近 N 個のノードの追加価値がしきい値を下回る。 タスクが収益逓減の領域に入っていることを意味し、 「どの程度までやれば完了なのか」を定義しなおすべきです。
  • replanning でもっとも起こりやすい誤りは、「何をすべきかをあらためて考える」ことを replanning とみなすことです。 実際には replanning は少なくとも 3 つのものを保持しなければなりません。 完了済みで依然として有効な結論、否定された仮定(コンテキストの中で明示的に却下済みとしてマークすること)、 そして依然として成立する中核的な制約です。 最初の 2 つを失うとゼロからやり直すことになり、前の数時間を無駄にします。

計画のバージョニング:replanning 自体を追跡可能にする

replanning が古い計画を上書きするだけだと、 長いタスクではいつか必ず聞かれる問いに答えられなくなります。 「なぜ 2 時間目の手法と 5 時間目の手法が違うのか」と聞かれるからです。 この問いはタスク失敗のレビューで非常に価値があります。 「システムが恣意的に手法を変えた」のか、 「新しい証拠に基づいてシステムが合理的に手法を変えた」のかを区別できるからです。

やり方は軽いです。replanning のたびに新しいバージョンの計画を作り、 新しい計画そのものではなく、前のバージョンと比べて何が変わったか (ノードの追加、ノードの削除、順序の調整、目標の絞り込み)を記録します。 コストはほぼゼロです。得られるのは、タスク全体が監査可能な意思決定の連鎖になることです。 これは前に述べた「意思決定層と実行層の分離」と同じ考え方を、 計画の層に適用したものにすぎません。

最後に見落とされがちなディテールが 1 つ。 すでに送り出されたサブタスクは、計画が変わっても自動的に取り消されません。 したがって replanning は、実行中のノードを明示的に扱う必要があります。 完了を待つか,取消すか,それとも走らせて結果だけ捨てるか。 この処理をしないと、計画のバージョン更新と実際の実行とのあいだに隙間ができ、 その隙間の成果物はしばしば間違っています。

Drift と Loop:走偏离ったときの 2 つの形

長いタスクの中で Agent が逸脱する方式はあろう Canonicalが限定的で、主として 2 つに集約されます。 しかもこの 2 つは検知シグナルも修復手段もまったく違います。 両者を混同するのはよくある誤診の原因です。 多くのチームが「タスクが歪んだ」と看一眼してモデルのせいにしますが、 実際には半分がループ、半分がドリフトです。

目標ドリフト:各ステップはもっともらしいが、合わせるとユーザーのほしいものではない

ドリフト(drift)の特徴は単体ではすべて正しいという点です。 どこかのステップから誤り始めるのではなく、意思決定の道筋そのものが徐々に元の目標から離れていきます。 もっともよくある仕組みが 3 つあります。

  • 制約の逐次的な腐食:コンテキストが圧縮されるとき、制約が要約に飲まれます。 앞에서述べたとおりです。圧縮のたびに精度が少しずつ落ち、 数十回と累積した。有的タスクはすでに元の要望の中にいません。 それでも、どのステップも間違ってはいないように見えます。
  • 近接アンカリング:モデルは新しい判断を、最近見た対象に結びつけたがります。 元の目標に結びつけるのではなくてです。 ユーザーが「パフォーマンス問題を探して」と言って、最初の数ステップがあるインターフェースを見ているとき、 醒目な遅いクエリが 1 つ見つかった。すると以降の作業がすべてその 1 つのクエリを軸に進められます。 しかしユーザーが本当に関心を持っているのは、全体的なパフォーマンスの相乗効果です。
  • 部分目標の蠕動:途中で生まれた小さな目標 (たとえば「この関数を分割する」)が次第に主目標と同等の地位を得て、主目標に代わります。

ドリフトを検知する要点は、行動を検知するのでなく、目標を検知することです。 行動はいつでももっともらしく見えます。本を読んでいる、調査している、ファイルを直している。 有効な方法は定期的に見直すことです。 現在の計画、完了済みノードの一覧、そして元の要望を並べて、 モデル(または人)に「今の計画どおりに走らせたら、最初の要望を満たせますか」と問いかけます。

このチェックの間隔は意図的に粗めに取るべきです。たとえば十数個のノードごとに 1 回、という程度です。 理由は 2 つあります。1 つは、このチェック自体がモデル呼び出しを 1 回消費するため、 高密度で行うと割に合わないことです。もう 1 つは、ドリフトは累積するものであり、 高密度なチェックでは直前に起きたドリフトしか検出できないのに対し、 粗めのチェックなら「すでに誤った方向へかなり進んでいた」ような状態を検出できるからです。 それは元の要望と照合して初めてわかります。

  • 実用的な判定:タスクがたくさんのノードを連続して完了したのに、 どのノードも「これはユーザーの要望のどれかに直接対応する」と言い切れるものがないなら、 ほぼ確実にドリフトしていると思ってよいです。効率Attachments いは一致とまではovoません。

自己ループ:同じ失敗を繰り返し消費する

ループ(loop)はドリフトより見つけやすい,因为它。 繰り返しの特徴が明確だからです。同じツール、同じパラメータ、同じエラーメッセージが何度も出てきます。 偶然起こるものではなく、通常は明確な原因があります。

もっとも欺瞞性 Announcement一種があります。 変化を装って前進に見せるループです。 検索キーワードを変え、ファイルパスを変え、同じ問いを違う言い方をする。 表面上はどのステップも違うように見えますが、実質的に何も学べていません。 この種の検出は呼び出しパラメータの比較だけでは足りません。 意味を比較しなければなりません。

有効な判断基準は、前に述べた新情報量です。 ツール出力を正規化して重複排除し、追加された内容を数えます。 連続して新情報量がほぼゼロなら、パラメータがどう変わっても同じ失敗を消費し続けています。

修復手段は強調に値します。 Notify 多くの人の第一反応は「最大ステップ数を増やす」ですが、ほとんど常に誤りです。 ステップを増やしても、同じ失敗をもっと多く消費するだけだからです。 本当に有効な修復を優先順に並べると、次のようになります。

  1. すでに試した経路を伝える:重複した内容と現在の目標をコンテキストに押し戻し、 これらの方法はすでに試し、進展がないことを明示的に説明します。 「モデルが自分が何を試したかを知らない」ことに起因するループの、 相当な割合をこれで解決できます。
  2. ツールか情報源を切り替える:クエリの語を変えるのではなく、情報源の種別を変えることです。 コードを読む代わりにテストを走らせ、検索の代わりに実行時データを見る。
  3. 人間ノードへ引き上げる:同じ失敗が顕著な予算を消費したのに進展がないなら、 正しい行動は止めて人に訊くことです。消費を続けることではありません。
  4. 現在の経路を捨てて分岐する:この経路を却下済みとしてマークし、 コンテキストに却下理由を保ったうえで、別の道に進みます。

この 4 手段に共通点があります。 いずれも doing ことを変えている、crate できるステップ数だけを増やしているわけではないからです。 この区別は、ループの扱いにおいてもっとも価値のある判断です。

両者の共通前提:失敗した経路は記憶されなければならない

ドリフトとループが長���タスクの中でálogosmemo顽固な的根本原因 は、 앞에서何度も出てきた 1 点にあります。 却下された経路が残跡を残していないということです。 圧縮で捨てられるのは検索の過程であり、通常は成功した結論が残ります。 その結果、モデルは 2 時間後に「すでに通ったが道として通らなかった経路がある」ことをまったく知りません。

したがってループ検知とドリフト確認をどれだけ充実させても、 「却下された経路とその理由」が一等市民の状態として永続化されていなければ、 それらは最終的にすべて破綻します。 失敗を遅らせるだけで、失敗を避けるわけではないからです。 この結論は数時間走った Agent なら何でも当てはまり、追加の議論は必要ありません。

チェックポイント再開の真の意味:あなたは復元していると思っていて、実際には再演しているかもしれない

ここまでの各節では、チェックポイントの粒度、冪等キー、無効化の伝播を説明しました。 この節が扱うのは、より根本的な問題です。 復元という操作そのものが、実際には何をしているのか。 多くのシステムは「チェックポイントから復元する」を「残りのステップを再度実行する」として実装しており、 この 2 つの文は、長いタスクにおいてはまったく違う意味を持ちます。

残りのステップを再度実行することは、暗黙の前提を 1 つ含みます。 完了済みのステップは安全にスキップできる、という前提です。 この前提が成り立つのは「世界が変わっていない場合」だけです。 そして数時間スケールでは、世界が変わることがむしろ常態なのです。

復元のときは先に照合する。単純に続行しない

正しい復元フロー���、最初のステップはノードを 1 つも実行することではなく、 完了済みノードの成果物が依然として成立しているかを検証することです。 この照合はコスト递增の 3 層@[Desugar] なっており、必要な层次までordo して構いません。

  • 存在性の照合:成果物がまだ存在するか(ファイルが削除されていないか、 リモートのオブジェクトが残っているか)。もっとも安価ですが、 もっとも粗い問題しか除外できません。
  • フィンガープリントの照合:成果物の内容の要約が当初の記録と一致するか。 それが依存する入力はどうでしょうか。この層で「基于 certainファイルとして導いた結論だが、そのファイルが変化した」という ケースを検出できます。
  • 意味的照合:外部世界的前提が依然として成立しているか。 たとえば「CI が green」という結論は、CI の状態をもう一度調べる必要があります。 数時間前の绿灯が、今は red になっているかもしれないからです。

照合の結果は、単に 1 度リトライ Roadmap させるだけではなく、無効化の伝播を動かす必要があります。 これが、前の input_digest しくみが復元シーンで果たす本当の役割です。 「やるかどうか」を判断の問題から、計算可能な問題へ変えるからです。

復元の 3 つのモードと、それぞれの代償

エンジニアリングの上では 3 つの復元モードを区別する必要があり、 実装の複雑さと正しさの要求がまったく違います。

復元モード何をするか適用条件主なリスク
信頼復元完了済みのノードをそのままスキップするタスクが純粋な読み取りで、しかも短い世界が変わっていれば成果物はすべて古くなり、あとから追跡もできない
フィンガープリント復元要約を照合し、失効した下流を再実行するデフォルトの選択。たいていの長いタスクに適する要約は構文レベルのもので、意味レベルの失効は読み取れない
意味復元外部前提を再検証し、必要なら分岐し直すCI、デプロイ状態、協働者の成果物に依存する場合コストが高く、さらに「誰が検証するのか」という答えも必要になる

多くのチームは 1 番目の段階から 3 番目へ飛ばすか、あるいは逆に—— ずっと 1 番目の段階に留まって、問題が起きてから一時的に照合を足す—— Licuid を見ます。 後者のほうが危険です。照合ロジックが正常系でテストされていないからです。

「Exactly-Once」は幻想である。At-Least-Once に冪等性Buddy を足すのが現実である

長いタスクの実行セマンティクスは、本質的に at-least-once execution しか実現できません。 あるノードは複数回実行されうる,而且、你并不知道它,到底被执行了几次。 分布式システムの教訓はすべてここで適用されます。 ちょうど 1 回の実行をAttend しようとするのではなく、重複実行を無害にするべきです。

実装に落とすと、これは 3 つを意味し、どれも欠けません。

  1. 各ノードには安定して再計算できる key があり、再演しても新しいノードは生じない。
  2. 外部副作用を持つ各ノードには冪等な識別子があり、この識別子は副作用が起きていない就已经に書かれている。
  3. 復元のときは、まず既存の副作用記録を引き継いでから、実行するかどうかを決める。

この 3 つのうちもっとも抜けやすいのは、2 番目のタイミングです。 「実行が成功してから冪等記録を書く」のは自然な順序に見えますが、 それは 1 つの窓を残します。実行は成功し、記録は書かれず、プロセスが落ちた。 復元のときに手がかりがなく、于是重複実行されます。 「この窓はログで解決できるか」と聞かれたときの答えはいつでも「できない」です。 欠けているのがまさにそのログだからです。

復元ストーム:1 つの再起動が複数の再演を引き起こす

最後に、見落とされがちなリスクを 1 つ。再現动作そのものが増幅されることがあるのです。 あるプロセスの起動時にすべての RUNNING ノードを PENDING へ回収し、 同時にスケジューラが複数の実行者设立了なら、複数の実行者が同じノードの群を同時に引き Taking します。 リースの仕組みがないシステムでは、これによって同じノードが複数回実行され、 そのたびに外部副作用が生じる可能性があります。

対策は標準的なリース(lease)です。 ノードが引き Taking されるときに期限つきのリースを付与し、 実行者は定期的にリースを延長し、実行前にリースが有効かを確認します。 復元のときは、リースが失効したノードだけを回収し、すべての RUNNING ノードを回収してはいけません。

このディテールはインフラ層の瑣末な事のように見えますが、 「チェックポイント再開」という機能が複数実行者の環境で安全かどうかを左右します。 そして複数実行者は、単一マシンから本番環境へ進むときMadison 必经marineJNI_buf 土堤である mods。 それは冗長性、長時間タスクの必要性、コスト制御と直結しています。

コスト制御:請求書をスケジューリングのシグナルに変える

前節では 3 段の予算ゲートを説明しました。あれは守備です。 この節は攻めます。1 円あたりの価値をどうやって引き上げるか。 守備型の予算制御の本質は「超えそうになったら止まる」ことにあり、 長いタスクが本当に必要としているのは「予算の中でいちばん重要なことを終わらせる」ことです。

各ノードのコスト密度を計算する

コスト密度の定義は単純です。そのノードが消費した予算を、それが前進させたタスクの進捗で割る。 進捗は、完了済みノード数、カバーできた作業単位数、あるいはタスクグラフに残っているノードの見積り、 いずれかで近似できます。

この指標があれば、短いタスクでは必要のないことをスケジュールが사는ようになります。 予算が残っているうちに能動的に取舍りをすることです。 具体的には、あるブランチのコスト密度が高止まりしていることに気づいたら、 それを早めに止めて予算を他のブランチに配ったほうが、たいてい走らせて終わらせるより有利です。

def should_continue(node: Node, used_token: float,
                    progress_delta: float, density_limit: float) -> str:
    """成本密度闸门:在节点执行中途中决定是否继续。

    progress_delta 是这个节点在一段时间内推进的进度估计。
    密度持续偏高意味着这个节点正在吞噬预算却没在交付。
    """
    if progress_delta <= 0:
        #零推进:不一定是失败(可能正在跑一个长耗时工具),
        #但绝不能在这种状态下继续派发新节点
        return "hold"

    density = node.cost_used / progress_delta
    if density > density_limit * 3:
        #严重偏高:立即中止,保留已获得的产出
        return "abort"
    if density > density_limit:
        #偏高:允许完成当前节点,但不再派发它的后继
        return "finish_no_successor"
    return "continue"

このロジックの要点は、「中断する」ことと「後続,派 Greenwood」を 2 つのアクションに分けていることです。 多くの実装はコスト超過時に分岐ごと殺してしまうため、すでに走った部分の成果物まで失敗として扱ってしまいます。 その成果物には価値があります。成果物を残して以降を放棄するほうが、コストの考え方に合った処理です。

コストはトークンだけではない

長いタスクにはucaution まったく無視されがちでないコストがあります。待機コストです。 4 時間走るタスクでは、その 4 時間ユーザーはほかのことをできません。 タスク自体の実計算が 10 分しかなくてもです。 このコストは請求書には載りませんが、長いタスク最主要的ユーザーコストです。

この観察には直接的なエンジニアリング上の含意があります。 不確実性を待機時間から追い出すことは、トークンを圧縮するより効果的なコスト最適化だということです。 具体的に言えば、最初から並列にできるものはすべて前倒しで並列にする(読み取り専用のブランチ)、 タスク開始時に一度で集められる情報は途中で何度も聞かない、 そして人間の確認が必要なノードは、できるだけ後ろではなく前に置く (早く失敗して早く表面化させる。3 時間走ってから尋ねるためではない)。

最後の 1 つは、しばしばkę ironically 逆に実装されています。 多くのシステムは「走れるだけ走らせて、最後にまとめてユーザーに確認する」という流れを採ります。 理由は「何度も聞くとユーザーに煩わacific される」というものです。 しかし長いタスクではこれは危険です。 ユーザーへの確認が必要なパラメータが 3 時間目に尋ねられ、 ユーザーが別の答えを返したら、前の 2 時間の作業の大部分が無駄になります。 確認地点を前倒しすれば、ユーザーに打扰する回数は増えますが、作業の無駄を避けられます。 この取引は、長いタスクではたいてい見合うものです。

キャッシュと再利用:長いタスクにおけるコストの大头

長いタスクには、短いタスクにはないコスト構造の特徴があります。大量の繰り返すコンテキストです。 タスクの制約、コードベースの骨格、すでに検証済みの中間結論が、 数十回の呼び出しで何度も送信し直されています。 この繰り返しは、トークン課金のときは实实在在のコストになります。

緩和手段の层次ははっきりしています。 プロンプトキャッシュが解決するのは「同じ接頭辞が繰り返し課金される」問題で、 安定した部分を前へ、不安定な部分を後ろへ置き、途中で変化内容を挿入しないことが前提です。 重複内容の削減が解決するのは「そもそも渡しすぎている」問題で、 長い引用を参照識別子に置き換えて、必要になったときに取得するようにします。 タスクレベルの再利用が解決するのは「同じノードが複数回の実行で同じことをしている」問題で、 決定的な部分をモデル呼び出しから取り出してコードにする手法です。

3 番目がもっとも大きく、もっとも見落とされています。 長いタスクのかなりの部分の作業は、実は決定的です。 ルールによるマッチング、形式の変換、指標の計算。 この部分をモデルから奪えば、節約になるだけでなく、この種のステップの失敗モード自体がなくなります。 判断基準は単純です。もしそれを関数として書けるなら、それはモデル呼び出しを使うべきではありません。

人間介在のチェックポイント設計:どこで止めるか、どれだけ止めるか、何を訊くか

前節では停止の意図を保留・縮小・巻き戻し・終了の 4 つに分けました。あれは意味の層の話です。 この節が扱うのはより実際的な問題です。 人間のチェックポイントをどこに置くべきか、そして各チェックポイントで人に何を求めるべきか。

よくある設計の誤りは、人間チェックポイントを「エラー処理の保険」だと考えることです。 タスクが失敗したときだけ人間にエスカレーションする。 この設計は短時間のタスクなら辛うじて成立しますが、長いタスクでは逆です。 長いタスクの失敗の代償は非常に高く、しかも失敗 provincially に初めて気づくという往往是数時間が無駄になったことを意味します。 人間の介入は能動的に設計された一等公民の仕組みでなければならず、 例外パスkaryotype ではありません。

4 種類のチェックポイントと、それぞれの価値

「介入のコスト 대비 得られる価値」で並べると、人間チェックポイントは 4 種類あり、 実装の難易度が増加します。

  • 確認型:不可逆なアクションを実行する前に 1 つ訊く。 もっとも単純で、価値は明確で、代償は中断です。 金額が大きく、影響範囲が広いアクションに向きます。
  • 方向型:大きなコストを投じる前に、全体の方向を確認する。 たとえば「私はこの 5 つのモジュールから取りかかりますが、どれを先に直すのがよいでしょうか」という確認です。 この種類の費用対効果がもっとも高く、1 回のunny中断で数時間の不確実性を purchases できるからです。
  • 例外型:システムが自分で決められない状況に直面したとき。 難しいのは「自分で決められない」の判定基準です。判定が緩すぎると人間が事実上パンクし、 厳すぎるとシステムが曖昧な点で勝手に推測します。
  • 受け入れ型:すべてが終わったあと、納品の前に確認する。 もっとも必要そうに見えますが、実際には価値がもっとも低くなります。 この段階で方向が間違っていても、投じた数時間已经是取り返せないからです。

この並びが持つ実践上の含意は単純です。 人間チェックポイントはできるだけ前へ動かすべきだということです。 4 時間走るタスクが最後の受け入れだけで人間が入quet다면、 この人間介入で挽回できる作業量はゼロです。 本当に見合うのは 10 分目に 1 回訊くことです。

5 回に分けて訊くより、1 回で訊き切ったほうが速い

人間介在のコストは中断そのものだけではありません。 ユーザーの認知再構築コストもかかるのです。 タスクの背景を思い出さなければならず、既存の成果物を読み直さなければならず、 判断を組み立て直さなければなりません。 ユーザーが 2 時間席を空けて戻ってきたとき、最初にすることは 「いま实际情况到底是什么状態か」を理解することです。 この過程自体に 10 分以上かかります。

したがって良いチェックポイント設計には、明確な要件があります。 1 回のインタラクションの中で、訊くべきことをすべて訊き切るということです。 チェックポイントを起動するとき、システム Permalink すべての保留中質問を先に集め、 まとめて提示しなければなりません。 1 つ訊いて、答えを待って、次の 1 つを訊く、ruits ではありません。

提示の内容にも讲究があります。 十分にattendしたチェックポイントには 4 つのものが含まれ、 1 つでも欠ければユーザーは迅速に判断できません。 いま何が完了したか(簡潔なリストで、流水帳ではなく)、 いまどこで詰まっているか(具体的な意思決定の 1 点まで特定して)、 有哪些選択肢があり、それぞれ何种代价が伴Regarding か、そしてあなたが推し薦める案とその根拠です。 4 番目がもっとも省kalinescalar lentes されますが、影響はもっとも大きくなります。 推し_BITS_IFCPP がなければ、ユーザーは実質的にシステムのために企画工作在QWERTY していることになります。

保留待機の正しい実装

前に述べたとおり、保留状態ではタイムアウト監視と停滞検出を止める必要があります。 ここ|implementation 点も 1 つ補足します。保留には期限戦略が必要です。

保留されたタスクを永久に待たせ続ければ、環境は変化し、人は忘[I have enough] れ、依存関係はアップグレードされます。 合理的なやり方は、保留ノードに明確な時間条件をではなく、 再評価の条件を 1 つ与えることです。 たとえば「X リポジトリに新しいコミット arrived したら再起動する」、 「CI が終わったら再起動する」、 「48 時間誰も XXXなければ再確認が必要とマークする」といった条件です。

  • 人間チェックポイントでもっとも忘れられがちな指標は「中断された回数」です。 これはシステム品質と負の相関を持ちます。 ユーザーを 5 回邪魔しなければ完了できないタスクは、 不確実性を十分に絞れていないことを意味するのがふつうです。

チェックポイントの設計が:admin 好不好を判定する簡単な 검사が 1 つあります。 この人間介入を取り除いたら、タスクは明らかに悪くなるか。 答えが「いいえ、わずかに手戻りが増えるだけ」なら、このチェックポイントは不要です。 自制心を得させるだけです。 答えが「はい、しかも代償が大きい」なら必要なものです。 これを新しいチェックポイントを追加する際の准入基準にすれば、 どのヒューリスティックよりも信頼できます。

2026 年の現実:何が収束し、何がまだ賭けか

この節は予測ではありません。 ここまでの各節のメカニズムを「現在の実装の可靠性の高さ」で階層化することです。 階層化の意義は、上位のものをそのまま使える、下位のものは設計時に逃げ道を残しておく、 という使い分けができることにあります。 具体的な実装詳細は会社ごとで大きく異なりますが、 状態の安定性はコードの構造そのものから判断できます。

ほぼ収束した部分

タスクの状態と実行プロセスの分離は、もはや論証を要する設計選択ではなくなっています。 状態を外部ストレージに置き、実行プロセスを差し替え可能な短命コンポーネントにするというやり方は、 長いタスクのシステムにおける事実上の標準になりました。 関連する配套设施——チェックポイント、リース、冪等キー——もほぼ定型的になり、 差は主に封じ方にあって、セマンティクスにはありません。

コンテキスト圧縮を必須の仕組みとして扱うことも同様に普及しました。 実際に広く採用されているのは「意味的な境界で圧縮し、失敗した試行は別に保持する」formeで、 単純なトークン単位の切り詰めではありません。 前に述べた制約の外部化の手法はまだ標準装備ではありませんが、 コストが極めて低く効果が明確な改善であり、 通常は既存アーキテクチャに独立した制約ストアを 1 つ足すだけですみます。

予算をスケジューリング層で強制するのも共识になっています。 予算をプロンプトに書いてスケジュール層で強制しないやり方は、 真面目な長いタスクではもうほとんど見られません。 この転換の背後にはコスト統制の成熟があります。 1 回のタスクコストが数十ドルから数百ドルに達しうる情境で、 「事後に超過発覚する」ことは受け入れられなくなったからです。

収束途上にあり、まだ分かれている部分

記憶の階層構造の方向性は明確になりました(作業、情動、意味の 3 層)。 しかし各層の具体的な境界と書き込み閾値は、まだ合意がありません。 もっとも議論が分かれているのは意味記憶の准入基準です。 ある判断はタスクをまたぐ知識として定着させるべきなのか、 それとも今回タスクの結論としてだけ残すべきなのか。 これは誤りが固定されるリスクに直結し、各社の戦略差が大きくなっています。

タスク分解の粒度と replanning のタイミングにも、統一的な答えはありません。 早期に分解し頻繁に replanning する方式は環境変化への反応が速い反面、 計画が振動する代償が大きい。粗く分解し replanning を控える方式は安定する反面、 逸脱したときの損失が大きい。 実システムではタスク種別ごとに選ぶことが多く、設定だけで決まっているわけではありません。

人間チェックポイントの位置は、プロダクトの面で技術面より大きな差があります。 技術的な実装(一時停止、復元、無効化の伝播)は成熟していますが、 「ユーザーにどれくらい打扰するか」という問題に技術的な答えはなく、 ユーザー種別についての想定に依存するしかありません。 技術選定的时候に確認しておきたい点です。 それは反过来には、アーキテクチャにどの程度の柔軟性が求められるかを決定するからです。

依然として賭けの部分

長いタスクの品質評価は、現在もっとも弱い环节です。 あるタスクが「うまくできたかどうか」を判断するとき、短いタスクなら成果物を直接見ればよいのですが、 長いタスクでは成果物は分散した結論数十点とレポート 1 本になり、 1 つの指標では測り、切れていません。 その結果、この種のシステムの改善には信頼できるフィードバックがなく、 人手によるレビューに頼るしかありません。

タスクをまたぐ知識の信頼性管理にも、よい答えはありません。 あるタスクの中で検証済みの結論は、そのまま次のタスクに使ってよいのか。 どの程度の検証があれば足りるのか。 前に「意味記憶への書き込みには人の確認が必要だ」と言いましたが、 タスク数が多いと 1 件ずつ確認することはできない 于是 標本抽出になり、 抽出Commutative 基準をどうするかは、いまほとんど各社の判断に頼っています。

コストと品質のトレードオフ曲線が、長いタスク上でどう形になるかもまだ分かりません。 短いタスクでは明瞭なパレート・フロンティアがあります。 予算をつければ品質が上がり、飽和するまで。 長いタスクではこの曲線はもっと複雑です。 予算をつければカバレッジが上がる-RJ,也可能 dutch. 同じ作業の重複 substantially xerxesのかArena.

ある方式が信頼できるかどう判断するか

自分に評価基準を与えるなら 4 つの問いを Asking ことができます。 これらはすべてドキュメントを読むのではなく、コードの構造を見ることしかずに答えられません。

  1. プロセスが落ちたとき、タスクの状態は完全かつ読み取り可能か。 もし復元のためにログから状態を再構成しなければならないなら、 それはまだ真の永続化タスクではありません。
  2. 同じノードが 2 回実行されたとき、外部システムに余分なものが増えるか。 増えるなら、副作用経路に冪等性がありません。
  3. ユーザーが要望を変えたとき、どの作業がすでに無効になったかを算出できるか。 できないなら、要望変更はやり直しでしか対応できません。
  4. タスクが逸脱したとき、どれだけの速さで気づくか。 答えが「終わってから」なら、途中の成果物の価値はほぼゼロです。

この 4 つの問いのうち、どれか 1 つでも答えられないなら、 そのシステムは数時間スケールでは信頼できません。 モデル能力がどれだけ強く、デモがいかに良く見えても、関係ないからです。 これ Carroll 冒頭の文の意味であり、 長いタスクのボトルネックはモデルではなく、状態とライフサイクルの管理にあるということです。

最後に

最初の問いに戻ります。数時間走る Agent は、プロセス再起動をどう生き延びるか。 答えは 1 文に凝縮できます—— 変化しうるもの、 잃われうるものをすべてプロセスのメモリから永続化されたタスクグラフへ移し、 そしてプロセスを、いつでも死に、死んだあとに差し替えられる実行器に変える。

この転換は単なるエンジニアリング上の細部に聞こえますが、問題の性質を変えます。 プロセス内の Agent は「いま思考している実体」であり、 考え終わるのを待つか、殺すかしかできません。 永続化された Agent は「進捗の半分が外部に置かれた状態機械」であり、 いつでも一時停止でき、実行者を差し替えられ、目標を書き換えられ、 そして中断した場所から続けさせられます。 後者がこそ、長いタスクを処理する能力を本当に具备しています—— より賢いからではなく、連続して生きていestens要らなくなったからです。

最後に、逸れやすい点を 1 つ。注意这一天: このアーキテクチャでもっとも重要な部分——冪等キー、入力フィンガープリント、無効化の伝播、予算ゲート——は、 デモをもっとかっこよく見せられる機能ではありません。 エラーが起きたときに、システムの/NT 동작が体裁よく見えるようにするだけです。 しかし長いタスクという領域では、体裁detachWell すべてです。 4 時間走ったタスクを、ユーザーは完成したからと褒めることはありません。 途中で崩溃して、半Immediate 品物を出し、 あるいは 2 時間を無駄に繰り返していたことに気づいたとき、 このツールを完全に放棄するからです。