Loop、Interaction、Approvalの境界
—LLMの自由度を高める前に、判断と実行の責任を分けた経験—
段階ごとに動作していた複数のLLM機能を一つのフローにつなげる際、当初は次の機能を自動的に呼び出せばAgentic Workflowになると考えていました。しかし、実際に必要だったのは呼び出し順序だけではありませんでした。現在の状態で実行可能なアクションを判断し、必要なユーザーの決定を求め、ユーザーが承認した結果と実際の実行対象が同じかを検証する構造が必要でした。こうした判断と検証を一貫して行うために、Workflow LoopとDomain Adapter、構造化されたInteraction、スナップショット承認の境界を分けました。
1. 背景と課題
当初は、機能がそれぞれ独立した段階として動作していました。ユーザーが入力すると、ある段階が結果を生成し、その結果を次の段階の入力として渡した後、次の実行を選択していました。結果の生成と検証、修正、保存の機能がすべてあっても、全体のフローをつなぐ作業はユーザーに残されていました。機能は完成していましたが、目標単位での実行体験は途切れていました。
複数のAPIを決められた順序で呼び出すだけでは不十分でした。ユーザーの判断によってリクエストが変わり、検証結果に応じて前の段階を補完したり再実行したりする必要がありました。分岐や再試行、ユーザーの応答待ちを単純なループと条件文でつなぐと、フローが長くなるほど状態管理と再開が複雑になります。そこで、現在の状態と次のアクションを管理するAgentic Workflowを導入しました。必要な情報を要求し、実行結果を元に戻しにくい作業の前では承認を待った後、中断地点から実行を再開するようにしました。
Agentic Workflowを設計する際、最初に分けたのはWorkflowを実行するLLMと、ドメイン機能を実行するLLMの責任でした。Workflow LLMは現在の実行状態から次のアクション候補を選択します。ドメインLLMは各Tool内で結果を生成し、検証または修正します。Workflow LLMがドメイン結果や最終的なTool入力まで生成すると、検証済みの状態とは異なる値を参照する可能性があります。逆に、ドメインLLMが次の段階まで決定すると、機能ごとに実行ルールが重複します。そこで、Workflow Loopは実行順序と待機・再開を管理しました。Domain Adapterは現在許可されているアクションとTool入力を検証した後、ドメインToolを呼び出しました。このように分けておけば、ドメインルールが変わっても共通Workflowに手を入れず、AdapterとTool内部で対応できます。
2. 実行責任の分離
全体構造では、共通の実行手順と特定の業務目標を分離します。Workflow Loopは実行順序と待機・再開を管理し、次のアクションを判断します。Domain Adapterは業務目標に合った機能を選び、Domain Toolの呼び出しを準備します。ユーザーとLoopの間はInteraction・Approvalで、LoopとAdapterの間はDecision・Tool呼び出しインターフェースで接続しました。2つのSessionは識別子だけで接続し、実行制御情報と業務結果を分離しました。
사용자 -- Interaction / Approval --> Workflow Loop
Workflow Loop <-> Domain Adapter -> Domain Tool
Workflow Session: 실행 / 대기 / 재개 | Domain Session: 업무 결과 / 검증 / 버전
Workflow Sessionには、現在のDecision、待機中のInteraction・Approval、完了・失敗・ポリシーによるブロックの有無を保存します。Loopはこれを読み取り、Toolを呼び出すかユーザーの応答を待ち、呼び出し後には成功・失敗と次の実行位置を記録します。ここでいうWorkflowの状態とは、業務結果の内容ではなく、実行がどこまで進み、何を待っているかを意味します。Loopは業務結果の意味を解釈しません。
Domain Sessionには、特定の業務目標を遂行する中で作られた結果、結果のバージョン、検証の有無、修正対象を保存します。Adapterはこれを読み取り、実行するToolとユーザーの判断が必要かどうかを決定し、検証済みの結果から最終入力を組み立てます。承認された結果と実行しようとしている結果が同じかどうかもAdapterが確認します。ここでいうDomainの状態とは、業務がどのような結果を作り、その結果を信頼できるかどうかを意味します。
まとめると、Loopは「どのように循環するか」を、Adapterは「今何をしてよいか」を判断します。LoopはDecisionの種類に応じて共通処理を選択し、AdapterはDomain Sessionの検証結果に基づいて、修正Toolを許可するか、先にユーザーの確認を受けるかを決定します。
2.1 DecisionとPolicy
Workflow LoopとDomain Adapterは、Decisionを介して次のアクションをやり取りします。AdapterがPolicyによって許可されたアクションを計算してDecisionを返すと、Loopが実行します。Decisionは、Tool呼び出し、ユーザーへの質問、承認要求、完了、失敗、待機を表します。PolicyはAdapterが返せるアクションを制限します。検証に失敗した場合は修正Toolだけを許可し、承認されていない変更Toolはブロックします。
ここでのLLMはWorkflow LLMです。最初はLLMを使わず、Policyルールだけで許可・ブロックのフローをテストしました。その後、Workflow LLMは許可されたアクションの中から1つを選ぶか、短い意図だけを提案するようにしました。最終的なTool入力は、AdapterがDomain Sessionの検証済み結果とWorkflow Sessionの承認情報を読み取って組み立てます。LLMがこの値を直接記述することはありません。
既存の段階機能も書き直しませんでした。Tool Registryがすでに検証済みの機能を呼び出すようにラップし、WorkflowはRegistryを介してのみ実行しました。これにより、新しい実行フローを導入しながら、既存機能の動作を比較できました。共通モジュールは最初から大きく抽象化せず、実際のフローの中で契約を検証した後に判断することにしました。
책임 기준 Workflow Loop는 실행 생명주기를, Domain Adapter는 업무 결과 해석과 허용 행동을, Domain Tool은 실제 기능 수행을 담당합니다.
3. 構造化されたInteraction
実行責任を分離すると、ユーザー入力の契約に不備があることが見えてきました。当初は、Workflowがユーザーに送る質問を文字列メッセージとしてのみ返していました。簡単な質問には十分でしたが、選択肢の選択、対象の指定、変更内容の確認など、ユーザーの利便性を高めるためのコミュニケーション方法を表現するのは困難でした。バックエンドは質問が必要であるという事実だけを伝え、画面はユーザーが実際に何をすべきかを推測しなければなりませんでした。
これを解決するため、Interactionを画面上の文言ではなく、ユーザーの行動の意味として定義しました。type、target、options、requiredなどで構成されたインターフェースを使用しました。typeは一般入力、選択、確認などの種類を表し、targetは応答がどの作業結果に適用されるかを識別します。optionsは選択肢形式で選べる値を表し、requiredは応答なしで進行できるかどうかを示します。画面はこの契約を、チャット入力、選択カード、変更比較画面など、状況に応じたコンポーネントとして表現します。
{
"type": "REVIEW_DIFF",
"target": "result",
"required": true
}
4. スナップショット承認と状態
ユーザーの決定の中でも、データを変更したり外部作業を実行したりするToolには、より強固な承認契約が必要でした。承認の有無を1つのBoolean値で保存すると、ユーザーが承認ボタンを押したという事実だけが残ります。どの結果をどの状態で承認したのかは分かりません。承認後に結果が修正されたり検証状態が変わったりしても、同じ承認値を再利用できてしまいます。
そこで、承認を実行対象のスナップショットに結び付けました。承認情報には、対象Tool、結果のバージョンとハッシュ、検証結果のハッシュ、冪等性キーを含めました。実行直前に、現在の結果が承認時の結果と同じかを再度比較します。対象Toolやバージョン、ハッシュのいずれか1つでも異なれば実行しません。ユーザーは単に「実行してよい」と承認するのではなく、識別可能な特定の結果を承認します。
この検査は、承認時点と実行時点の間に発生した変更を防ぐ仕組みでもあります。ユーザーが画面で結果を確認した直後に別のリクエストが同じ結果を修正することもあり、実行キューで待機している間に検証結果が変わることもあります。承認時だけ条件を確認すると、このような変化を見逃します。実行直前に同一性を再確認してこそ、ユーザーが見た内容と実際に保存される内容が同じであるという条件を維持できます。
실행 가능 = 승인됨
&& 대상 Tool 일치
&& 결과 버전 / 해시 일치
&& 검증 결과 해시 일치
&& 멱등성 키 유효
不一致の状況は、Tool実行の失敗とも区別しました。承認がない、期限切れである、または現在の結果と異なる場合は、Toolを呼び出す前にPolicyがブロックします。一方、許可されたToolを呼び出した後に内部または外部のエラーが発生した場合は、実行失敗です。2つの状態を分けることで、再試行可能なエラーなのか、ユーザーの再承認や状態の更新が必要な状況なのかを判断しやすくなります。
4.1 監査と冪等性
承認要求、承認結果、Tool呼び出し、ポリシーによるブロックはAudit Eventとして残しました。監査記録は事後ログだけを意味するものではありません。次のDecisionが以前の実行結果を解釈し、運用中に特定の実行がなぜ進行または停止したのかを確認する根拠になります。相関関係のあるイベントを同じ実行フローにまとめると、ユーザーの応答とToolの結果を一緒に追跡できます。
冪等性キーは、同じ承認要求が2回実行されるのを防ぐために使用しました。ネットワークの再試行やユーザーの連続クリックが発生しても、すでに処理済みのキーであれば再実行しません。重要なのは、冪等性検査をTool内部だけに任せず、承認と実行の契約にも含めたことです。そうすることで、Workflowの観点から同じ意図が1回だけ処理されたことを説明できます。
4.2 永続状態
初期段階では、Workflowの状態の一部をリクエストとレスポンスに含めて渡していました。短い機能検証には便利でしたが、リロード、サーバー再起動、長時間の承認待ちにおいて安定した再開を保証するのは困難でした。承認と監査情報がクライアントの保持する状態に依存すると、サーバーが現在の実行における基準状態を自ら判断することも難しくなります。
後続の作業では、Workflow Sessionと承認、監査状態をJPAで永続化しました。
5. 適用基準とまとめ
今回の作業では、Agentic Workflowを設計するにあたり、次の基準を適用しました。第一に、実行ループとドメイン判断を先に分離します。境界を決める際には、そのロジックが業務結果の内容を知る必要があるかを確認しました。結果の意味や許可ルールを知る必要があるならAdapterに置き、待機・再開・完了のように業務に関係なく繰り返される手順ならLoopに置きます。この基準がなければ、新しいフローを追加するほどLoopの条件文と状態解釈が増えていきます。
第二に、LLMには許可された範囲で提案させ、最終的なTool入力は持たせません。Policyが可能なアクションを計算し、Adapterが検証済みの結果と承認情報から入力を組み立てます。Workflow LLMはその範囲内でアクションを選ぶだけで、結果の内容や承認値を新たに作ることはありません。このように分けると、どの値がLLMの提案で、どの値がアプリケーションによって確認された情報なのかを区別できます。自律性を高めることと、実行制御を放棄することは同じではありません。
第三に、ユーザーに送る内容を文字列だけで渡さず、行動の形式として表現します。一般入力、選択、確認には、それぞれ異なる案内と応答方法が必要です。typeで行動の種類を示し、targetで応答が適用される作業結果を指定します。選択が必要な場合はoptionsで可能な値を渡し、requiredで応答なしに進行できるかどうかも示します。構造化されたInteractionの目的は画面を制御することではなく、ユーザーとWorkflowの間のコミュニケーション方法を明確にすることです。
第四に、承認を承認時点の実行対象のスナップショットに結び付けます。承認ボタンのクリックだけを保存しても、承認後の変更や重複実行を防ぐことはできません。対象Tool、結果のバージョンとハッシュ、検証状態、冪等性キーを実行直前に再確認する必要があります。条件が一致しなければToolを呼び出さず、ポリシーによるブロックとして記録します。一方、条件を通過した後にTool内部でエラーが発生した場合は、実行失敗として残します。2つの結果を区別してこそ、再承認が必要なのか、再試行できるのかを判断できます。
中断と再開に必要なWorkflow Session、承認、監査状態は、後続の作業でJPAにより永続化しました。
今回の作業では、Agentic WorkflowをLLM呼び出しを自動的につなぐ構造ではなく、判断と実行の責任を分ける構造として整理しました。LoopとAdapterの役割を分離し、Interactionによってユーザーの応答が適用される対象を明確にし、Approvalによって承認した結果と実際の実行対象を一致させました。結局重要なのは、LLMにより多くのことをさせることではなく、誰が何を判断し、どの根拠で実行するのかを明確にすることでした。
DEVKC