コンテンツにスキップ

04. Human-in-the-loop(interrupt / resume)

ゴール

  • agent の途中で人間に判断を仰ぐ仕組みを実装する
  • interrupt()Command(resume=...) の使い方を覚える
  • これが agent harness 設計の 。AI が暴走しない安全装置

なぜ HITL が要るか

  • LLM は時々間違える。取り返しがつかない操作(メール送信、決済、本番DB書込)の前に人が見たい
  • ツール選択の根拠が薄いとき、ユーザーに確認したい
  • 規制業務では監査要件で必須

interrupt の基本

langgraph.types.interrupt() を呼ぶと、その地点でグラフが停止し、呼び出し元に制御を返す。

from langgraph.types import interrupt, Command

def review_node(state: State) -> dict:
    proposed = state["messages"][-1].content
    decision = interrupt({
        "question": "この内容で送信していいですか?",
        "proposed": proposed,
    })
    if decision["approve"]:
        return {"messages": [("system", "approved")]}
    else:
        return {"messages": [("system", f"rejected: {decision['reason']}")]}

実行:

config = {"configurable": {"thread_id": "task-1"}}

# 1回目:interrupt まで進む
result = graph.invoke({"messages": [("user", "顧客にお詫びメールを送って")]}, config)
print(result["__interrupt__"])  # interrupt の payload が見える

# ここで人間がレビュー(CLI / Slack / Web UI どれでも可)

# 2回目:Command(resume=...) で再開
graph.invoke(Command(resume={"approve": True}), config)

ポイント:

  • interrupt(...)引数が人間に見せる payload
  • Command(resume=...)引数が人間の決定
  • checkpointer が必須(state を保存しないと再開できない)

設置ポイントの選び方

パターン
Before tool call 危険なツール(メール送信、本番API)の前
After plan LLM が立てた計画を承認してから実行
Approve final answer 最終応答をユーザーに見せる前
Edit state 「LLM の draft を人が手で直す」用途

実用パターン1: 計画レビュー

def plan_node(state):
    plan = llm.invoke(state["messages"] + [("system", "計画だけ立てて、まだ実行しないで")])
    return {"plan": plan.content, "messages": [plan]}

def review_plan(state):
    decision = interrupt({"type": "plan_review", "plan": state["plan"]})
    if decision["action"] == "approve":
        return {}
    elif decision["action"] == "edit":
        return {"plan": decision["edited_plan"]}
    else:
        return {"plan": "", "messages": [("user", "計画を立て直して: " + decision["feedback"])]}

実用パターン2: 危険ツールの ガード

DANGEROUS = {"send_email", "delete_user", "execute_sql"}

def maybe_interrupt(state):
    last = state["messages"][-1]
    if any(c["name"] in DANGEROUS for c in last.tool_calls):
        return "human_review"
    return "tools"

add_conditional_edgesagentmaybe_interrupthuman_review or tools という分岐に。

Try

  1. ハンズオン2章の ReAct agent に「calculator を呼ぶ前に必ず人間確認」を入れる
  2. CLI で input("approve? y/n: ") を呼んで Command(resume) する簡易レビューUIを作る
  3. interrupt 中に thread_id を別プロセスで get_state して、本当に止まっているか確認
  4. (難)reject されたら別の道を選ぶ分岐を作る

Slack 連携の発想(先取り)

本番で HITL を使うときは、CLI ではなく Slack 等が普通。

1. interrupt 発生
2. webhook で Slack に「承認ボタン付き」メッセージ
3. ユーザーがクリック → API server が Command(resume) を投げる
4. graph が続きを実行

これを Temporal Workflow の外殻で組むと、Slack 待ちで数日寝かせても安全(→ Step 06 で扱う)。

学んだこと

  • interrupt() でグラフを止め、Command(resume=...) で再開
  • Checkpointer が必須(state がないと再開できない)
  • HITL は agent harness の安全装置。いつ・どこに置くかの設計が腕の見せどころ
  • Slack/メール経由の HITL は別レイヤー(API + webhook)で組む

設計上の問い

学習しながら考えること。答えは1つではない:

  • どのツールが危険か? 業務ドメインで違う
  • 承認者は誰? 単一ユーザー?複数承認?
  • タイムアウトは? 24h 返事がなければデフォルト reject か escalate か
  • 拒否されたら? やり直し?別ルート?タスク終了?

これらの問いを Temporal の Signal / Timer / Retry policy にどうマッピングするかが、次章 06_temporal_integration のテーマ。

次は 05_multi_agent。専門家エージェントを組み合わせる。


作成: 2026-05-16 / 最終更新: 2026-05-16