Background Modeは、Responses APIの処理をバックグラウンドで走らせ、長く考えるAIエージェントをタイムアウトから切り離すための機能です。公式ドキュメントでは、background: trueで非同期実行を開始し、レスポンスIDを使って状態をポーリングする流れが示されています。
この記事では、OpenAI公式のBackground mode、Remote MCP、Agents SDKのTracing/Guardrailsドキュメントを前提に、業務で使える非同期AIエージェントの設計をまとめます。架空の成功率や処理速度は置かず、実装判断に必要な「どこで待つか」「どこで失敗を戻すか」「どこまで監査ログを残すか」に絞ります。
対象は、社内検索、調査レポート作成、コードレビュー、長文要約、複数ツールをまたぐオペレーションなど、数十秒から数分かかる処理をAIエージェント化したい開発者・PMです。同期APIの延長で作ると、UI、キュー、監視、セキュリティのどこかが先に破綻します。Background Modeは万能薬ではありませんが、非同期ジョブとして設計する入口になります。
なぜ今、非同期エージェント設計が必要か
AIエージェントは、単発のチャット応答よりも待ち時間が読みにくい処理になりがちです。検索、社内DB参照、コード実行、MCPツール呼び出し、評価、再試行を組み合わせると、ユーザーが画面で待てる時間をすぐ超えます。同期HTTPリクエストのまま抱えると、クライアント切断、リバースプロキシのタイムアウト、ワーカー枯渇、二重実行の原因になります。
Background Modeの価値は、モデルが長く考えられることだけではありません。実務では、レスポンスIDをジョブIDとして保存し、UIは「受付済み」に戻し、ワーカーや定期ポーリングで完了を取りに行く設計に変えられる点が大きいです。これにより、ユーザー体験はチャットの待機画面から、タスク管理や通知に近づきます。
OpenAI公式ドキュメントでは、バックグラウンドリクエストはqueuedまたはin_progressの間ポーリングし、最終状態に到達したら結果を扱う、という考え方が説明されています。また、ポーリングのためにレスポンスデータが概ね10分保持されるため、Zero Data Retentionとは互換しない点も明記されています。つまり、セキュリティ要件によっては採用判断そのものが変わります。
- 同期UIで長時間待たせる処理を切り離す
- レスポンスID、社内ジョブID、ユーザーIDを紐づける
- 状態遷移を保存して、再実行と重複実行を防ぐ
- ZDR要件、監査要件、保持期間を事前に確認する
Background Modeの基本アーキテクチャ
最小構成はシンプルです。アプリケーションはユーザーの依頼を受け取り、社内DBにジョブを作成し、Responses APIへbackground: true付きでリクエストを送ります。返ってきたレスポンスIDを保存し、別ワーカーが一定間隔で取得APIを呼びます。状態がqueuedまたはin_progressなら待機し、完了・失敗・キャンセルなどの終端状態ならDBを更新します。
ここで重要なのは、OpenAI側のレスポンスIDだけに依存しないことです。業務アプリ側には、依頼者、入力ハッシュ、権限、通知先、作成時刻、期限、リトライ回数、最終エラーを持つ自前ジョブテーブルを置きます。レスポンスIDはその一部です。こうしておくと、API取得が一時的に失敗しても、ユーザーに同じ依頼を何度も送らせずに済みます。
from openai import OpenAI
from datetime import datetime, timezone
client = OpenAI()
def enqueue_agent_job(user_id: str, prompt: str, db):
job = db.jobs.insert({
"user_id": user_id,
"status": "submitted",
"prompt_hash": hash(prompt),
"created_at": datetime.now(timezone.utc).isoformat(),
"retry_count": 0,
})
resp = client.responses.create(
model="gpt-5.5",
input=prompt,
background=True,
)
db.jobs.update(job.id, {
"status": resp.status,
"openai_response_id": resp.id,
})
return {"job_id": job.id, "status": resp.status}
このコードは概念例です。実運用では、入力本文をそのままログに残さない、個人情報をマスクする、依頼者の権限をジョブに固定する、といった処理を追加します。特に、調査エージェントや社内FAQエージェントでは、ジョブ作成時点の権限を保存し、完了時に別ユーザーへ結果を誤配信しない設計が必要です。
UIは「処理中です」とスピナーを回し続けるより、受付完了、進行中、完了、失敗、期限切れを明示したほうが安定します。長時間実行は、チャットUIよりタスクUIの発想に寄せるのがコツです。進捗パーセンテージを捏造する必要はありません。取得できる状態だけを正直に表示し、必要なら「最終更新時刻」を出します。
ポーリング設計:2秒固定ではなく、期限と責務を決める
公式例では、ステータスがqueuedまたはin_progressの間、短い間隔で取得APIを呼ぶサンプルが示されています。学習用には分かりやすいですが、本番では全ジョブを2秒固定で回すと、ジョブ数が増えたときに自社側のワーカーもAPI呼び出しも無駄に増えます。
おすすめは、短い即時確認と、長めのバックグラウンド確認を分けることです。ユーザーが画面を開いている最初の数十秒は短めに確認し、それ以降はワーカーで指数バックオフします。完了通知が必要なら、メール、Slack、アプリ内通知など、プロダクトの通知基盤へ渡します。チャット画面を開きっぱなしにしないことが、体験と運用コストの両方に効きます。
import time
from openai import OpenAI
client = OpenAI()
TERMINAL = {"completed", "failed", "cancelled", "incomplete"}
ACTIVE = {"queued", "in_progress"}
def poll_response_until_terminal(response_id: str, db, max_seconds=540):
started = time.time()
delay = 2
while time.time() - started < max_seconds:
resp = client.responses.retrieve(response_id)
db.jobs.update_by_response_id(response_id, {
"status": resp.status,
"last_polled_at": time.time(),
})
if resp.status in TERMINAL:
return resp
if resp.status not in ACTIVE:
raise RuntimeError(f"unknown status: {resp.status}")
time.sleep(delay)
delay = min(delay * 1.5, 30)
raise TimeoutError("local polling deadline exceeded")
ローカルの期限とAPI側の状態は分けて考えます。自社ワーカーがタイムアウトしたからといって、モデル処理が失敗したとは限りません。ジョブテーブルに「次回確認時刻」を保存し、別プロセスが後で確認できるようにします。逆に、いつまでも追い続ける設計も危険です。ユーザーに価値が出る期限、社内SLA、データ保持の制約から、追跡終了の条件を決めます。
Remote MCPと組み合わせる時の境界線
Background Modeは、長時間推論だけでなく、ツール利用が絡むエージェントにも向いています。OpenAIのRemote MCPドキュメントでは、Responses APIの組み込みツールとしてtype: "mcp"を指定し、server_urlでリモートMCPサーバーを渡す形が説明されています。サーバーによってはOAuthの認可情報も必要です。
ただし、MCPを使えるからといって、すべての社内操作をモデルに直接渡すべきではありません。請求、削除、送信、権限変更など副作用の強い処理は、承認ゲートや人間確認を挟みます。Background Modeではユーザーが画面から離れている可能性があるため、同期チャットよりも「勝手に実行された」と感じられやすい点に注意します。
{
"model": "gpt-5.5",
"background": true,
"tools": [
{
"type": "mcp",
"server_label": "internal_search",
"server_description": "社内ナレッジを検索する読み取り専用MCPサーバー",
"server_url": "https://mcp.example.com/sse",
"require_approval": "never"
}
],
"input": "過去3か月の顧客問い合わせからFAQ改善案をまとめて"
}
読み取り専用の検索、ファイル列挙、ログ参照であれば、バックグラウンド実行との相性は高いです。一方で、外部送信、CRM更新、契約ステータス変更のような操作は、結果案を生成するところまでをバックグラウンドにし、確定処理は別の承認フローに分離します。MCPサーバー側でも、読み取り用と書き込み用を分け、スコープと監査ログを明確にします。
AIエージェントの長時間実行設計は、Checkpoint・Durable Executionの考え方とも接続します。Background Modeはモデル呼び出しの非同期化であり、ワークフロー全体の永続実行基盤ではありません。複数ステップをまたぐ業務では、自社のジョブ管理、キュー、ステップごとの中間成果物が必要です。
失敗パターン:同期チャットの発想を引きずる
最も多い失敗は、同期チャットのコードにbackground: trueを足しただけで本番投入することです。これでは、ジョブの所有者、再試行、期限、結果保存、通知、監査が曖昧なまま残ります。非同期化とは、待ち時間を隠す処理ではなく、責務を分割する設計です。
- 失敗1:結果をブラウザメモリだけで待つ。タブを閉じた瞬間に追跡できなくなる。
- 失敗2:ポーリング失敗をモデル失敗として扱う。一時的なネットワーク障害と終端状態を混同する。
- 失敗3:同じ依頼の二重送信を防がない。ユーザー連打やリロードで同じジョブが複数走る。
- 失敗4:ZDRやデータ保持要件を後から確認する。Background Modeの保持仕様と社内規程が衝突する。
- 失敗5:ツール実行の副作用をログだけで済ませる。承認前提の操作が裏側で進む。
特に二重送信は地味ですが危険です。入力ハッシュ、ユーザーID、対象リソースID、作成時刻を使い、一定時間内の同一依頼をまとめるか、ユーザーに既存ジョブを表示します。ジョブが失敗した場合も、新規実行とリトライを区別して記録します。これにより、あとで「何が何回実行されたのか」を追えます。
監視・トレーシング:完了したかより、なぜそうなったかを見る
非同期エージェントでは、ユーザーが待っている目の前でログを確認できません。だからこそ、トレーシングと監査ログが重要です。OpenAI Agents SDKのTracingドキュメントでは、長時間ワーカーではトレースがバックグラウンドで送られる一方、ジョブ単位で即時反映したい場合はflush_traces()を呼ぶ例が示されています。
監視したいのは、単なる成功・失敗ではありません。入力の種類、使ったツール、承認待ちになった回数、外部APIの失敗、モデルの最終状態、ユーザーへ返した要約、再試行回数です。これらを残すと、プロンプト改善、ツール権限見直し、キュー設定、SLA調整ができます。逆に、本文全文や個人情報を何でもログに残すと、あとで別のリスクになります。
from agents import Runner, trace, flush_traces
def run_background_agent(job_id: str, prompt: str, db):
try:
with trace("background_agent_job", group_id=job_id):
result = Runner.run_sync(agent, prompt)
db.jobs.update(job_id, {
"status": "completed",
"summary": result.final_output[:1000],
})
except Exception as exc:
db.jobs.update(job_id, {
"status": "failed",
"error_type": type(exc).__name__,
})
raise
finally:
flush_traces()
この例も概念コードです。実務では、トレースID、ジョブID、ユーザーID、社内リソースIDを相互に参照できるようにします。エラー本文をそのままユーザーに見せるのではなく、ユーザー向けメッセージと運用者向けログを分けます。セキュリティ上の理由で詳細を隠す場面でも、運用者が後で原因を追える情報は残します。
継続運用では、AIエージェントの継続的評価とCI/CD回帰検知のように、プロンプトやツール変更のたびに小さな評価セットを回すと安定します。Background Modeを使うジョブは、時間がかかるぶん失敗発見も遅れます。公開前の評価と公開後のトレースをセットで設計してください。
Guardrails:バックグラウンドでも入力・出力・ツールを分けて守る
Agents SDKのGuardrailsドキュメントでは、入力ガードレール、出力ガードレール、ツールガードレールがそれぞれ異なる地点で動くことが説明されています。入力ガードレールは最初のユーザー入力、出力ガードレールは最終出力、ツールガードレールは各カスタム関数ツールの呼び出し前後に関わります。Background Modeでも、この境界はそのまま重要です。
非同期処理では、危険な入力を早めに止める価値が高くなります。たとえば、個人情報の大量処理、契約外データの参照、破壊的操作の依頼は、長時間ジョブに入る前に弾くか、承認待ちにします。最終出力では、社外秘、個人情報、未確認の断定、社内リンクの露出を確認します。ツール呼び出しでは、対象ID、権限、操作種別を検査します。
- 入力:依頼者がそのデータを扱えるか、禁止テーマではないか。
- ツール:読み取り専用か、書き込みか、承認が必要か。
- 出力:根拠があるか、社外秘を含まないか、ユーザーに実行を促してよいか。
- 通知:完了メッセージを誰に、どのチャンネルへ出すか。
バックグラウンド実行は、ユーザーとの対話的な確認が薄くなります。そのため「あとで確認すればよい」ではなく、危険な分岐はジョブの状態としてneeds_approvalを持たせるのが実装しやすいです。承認が下りるまでツール実行を止める、承認期限を過ぎたら失効する、承認者と実行者をログに残す、というルールを作ります。
実装チェックリスト:本番投入前に見る15項目
Background Modeの導入可否は、モデル性能よりも周辺設計で決まります。以下のチェックリストを満たしてから、小さな社内用途で試すのがおすすめです。特に、データ保持、承認、通知、再試行は後付けすると複雑になります。
- ジョブテーブルに社内ジョブIDとOpenAIレスポンスIDを保存している
- 依頼者、権限、入力ハッシュ、対象リソースIDを保存している
- 状態遷移を
submitted、queued、in_progress、completed、failed、needs_approvalなどで管理している - ポーリング間隔にバックオフがある
- ローカル期限とAPI側の終端状態を区別している
- 二重送信を検知する仕組みがある
- ユーザー向けエラーと運用者向けエラーを分けている
- 入力本文や個人情報をログに残しすぎていない
- Background Modeのデータ保持仕様と社内ポリシーを確認している
- MCPツールを読み取り専用と書き込み用に分けている
- 副作用のある操作に承認ゲートがある
- 完了通知の宛先と権限を検証している
- トレース、ジョブID、ユーザーIDを紐づけられる
- 小さな評価セットで品質回帰を見ている
- ユーザーに「処理中」「完了」「失敗」「要承認」を明示できる
最初のユースケースは、読み取り中心で、完了が少し遅れても業務影響が小さいものを選びます。社内文書の要約、問い合わせ分類、競合情報の下調べ、コードレビュー案、FAQ改善案などです。いきなり請求処理や外部送信に入ると、失敗時の影響が大きく、承認設計も重くなります。
サンプル構成:FastAPI + キュー + ポーリングワーカー
典型的な構成は、Web API、ジョブDB、キュー、ポーリングワーカー、通知ワーカーの5つです。Web APIは依頼を受け付けるだけにし、実際のAPI呼び出しやポーリングはワーカーに任せます。小規模なら単一DBと定期実行でも始められますが、ジョブ数が増えるなら専用キューを使います。
from fastapi import FastAPI, BackgroundTasks
app = FastAPI()
@app.post("/agent-jobs")
def create_job(req: AgentJobRequest, bg: BackgroundTasks):
job = create_local_job(req.user_id, req.prompt)
bg.add_task(start_openai_background_response, job.id)
return {"job_id": job.id, "status": "accepted"}
def start_openai_background_response(job_id: str):
job = load_job(job_id)
resp = client.responses.create(
model="gpt-5.5",
input=job.prompt_for_model,
background=True,
)
save_response_id(job_id, resp.id, resp.status)
def polling_worker_tick():
for job in due_jobs(limit=50):
resp = client.responses.retrieve(job.openai_response_id)
update_job_from_response(job.id, resp)
if resp.status == "completed":
notify_owner(job.id)
elif resp.status in {"queued", "in_progress"}:
schedule_next_poll(job.id)
この形にしておくと、UIはジョブ状態を読むだけになります。画面を閉じても処理は続き、ユーザーは後で結果を確認できます。失敗した場合も、再試行可能な失敗か、入力修正が必要な失敗か、権限確認が必要な失敗かを分けられます。運用者はジョブ一覧から詰まっている処理を確認できます。
ブラウザ操作型のエージェントを扱う場合は、Computer Use本番運用ガイドも参考になります。GUI操作は待ち時間と不確実性がさらに増えるため、タイムアウト、スクリーンショット保存、手動復旧の導線を最初から設計してください。
公式情報リンクと導入時の判断基準
この記事で参照した一次情報は、OpenAIのBackground modeドキュメント、Remote MCPドキュメント、OpenAI Agents SDKのTracingドキュメント、Guardrailsドキュメント、Toolsドキュメントです。API仕様やモデル名は変わるため、本番投入前には必ず最新の公式ページを確認してください。
採用判断の目安は明確です。処理が長い、ツール呼び出しが多い、ユーザーが待機画面から離れてもよい、結果を後で通知できる、ジョブ状態をDBで管理できる。この条件を満たすならBackground Modeは有力です。逆に、即時応答が必須、ZDRが必須、状態管理を作れない、副作用操作の承認設計がない場合は、同期実行か別のワークフロー基盤を選ぶほうが安全です。
小さく始めるなら、まず読み取り専用の調査エージェントを1つ作り、ジョブ受付、Background Mode実行、ポーリング、結果保存、通知、トレース、評価の一連を通します。そこからMCPツール、承認ゲート、複数ステップ化へ広げると、技術負債を抑えながら拡張できます。
運用開始後の見直しポイント
公開後は、平均処理時間だけでなく、ユーザーが再実行した割合、承認待ちで止まった割合、ツール呼び出し失敗の種類、完了通知を開いた割合を見ます。非同期エージェントは「速く返す」より「忘れずに終わる」ことが価値になるため、チャットボットの指標だけでは判断できません。ジョブが完了したのにユーザーが読まないなら通知文面や結果画面を直し、失敗が多いなら入力制約やツール権限を見直します。
また、モデルや公式APIの仕様が変わったときに備え、API呼び出し部分を薄いアダプターに閉じ込めると保守しやすくなります。アプリ全体がResponses APIの生レスポンスに依存していると、状態名や出力構造が変わったときの修正範囲が広がります。社内ジョブの状態モデルを先に決め、外部APIの状態はそこへ変換する形にすると、将来のモデル変更やワークフロー基盤変更にも耐えやすくなります。
関連記事・次に読む
この記事を読んで導入イメージが固まってきた方へ
UravationではAIエージェント導入の研修・コンサルを行っています。Background ModeやMCPを含む業務エージェント設計を、要件定義から運用設計まで一緒に整理できます。
