AI自動投稿で失敗した工程から安全に再開するための設計と実装例

ブログ

AIでブログ記事の自動生成・投稿を組むと、生成自体は成功しても「画像だけ先にアップされた」「投稿が重複した」「公開ステータスだけ反映されない」といった部分的な失敗が起こりがちです。こうした状態で単純に再実行すると重複投稿やデータ不整合を招きます。本稿では制作現場で使える実例を中心に、失敗した工程から安全に再開(リカバリ)するための設計パターンと実装例を示します。

失敗パターンを分類する(まずは原因を明確にする)

再開設計は、どこで何が失敗したかを特定できることが前提です。以下の主要パターンを意識してください。

  • 完全失敗:生成→アップロード→投稿まですべて完了していない
  • 部分成功:画像アップは成功、記事投稿だけ失敗など工程間で差がある
  • 重複実行:ネットワークタイムアウト後に再送した結果、同一記事が2件生成される
  • 外部依存エラー:CMSや画像CDN側の一時的エラー

設計方針(安全に再開するための基本ルール)

  • アイドンポテンシー(idempotency)を採る:同じ仕事を複数回実行しても副作用が1回だけになる設計にする
  • 工程を小さなトランザクションに分割し、各工程の完了状態を永続化する
  • 「暫定状態(draft)」を使う:公開は最後のステップに限定し、中間は下書きで保存する
  • 明確なチェックポイント(チェック済みフラグ/タイムスタンプ)を持つ
  • ログと操作履歴を残し、手動での差し戻しや確認ができるようにする

実装パターン:ワークフローの例(生成→画像→下書き→公開)

現場で使いやすい順序と安全措置の例です。各工程で結果をDBに記録し、再開時にはDBの状態を見て次の工程を決めます。

  1. AIで本文とメタ情報を生成(生成IDを発行)
  2. 画像生成/取得し、画像IDを取得(失敗したらリトライキューへ)
  3. CMSに対して「下書き(draft)」で投稿を作成、戻り値のpost_idを保存
  4. 最終チェック(プレビューまたは自動チェック)を経て「公開(publish)」に変更

利点

  • 公開を最後に限定することで重複公開や未完成公開を防げる
  • 中間状態があるため手動確認や差分修正が容易

コード例:PythonでIDEMPOTENTな投稿処理(概念実装)

以下は概念的な実装例です。SQLiteでジョブを管理し、WordPress REST APIに下書きで投稿、成功したら公開する流れを示します。

# requirements: requests
import sqlite3
import requests
import time
import uuid

DB = 'jobs.db'
WP_API = 'https://example.com/wp-json/wp/v2'
WP_AUTH = ('user','pass')  # 実運用ではOAuthやJWTを推奨

# 初回セットアップ(テーブル)
conn = sqlite3.connect(DB)
conn.execute('''
CREATE TABLE IF NOT EXISTS jobs (
  job_id TEXT PRIMARY KEY,
  content_hash TEXT,
  status TEXT,
  attempts INTEGER DEFAULT 0,
  post_id INTEGER,
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
''')
conn.commit()
conn.close()

# ジョブ登録
def create_job(content_hash):
    job_id = str(uuid.uuid4())
    conn = sqlite3.connect(DB)
    conn.execute('INSERT INTO jobs(job_id, content_hash, status) VALUES(?,?,?)',
                 (job_id, content_hash, 'created'))
    conn.commit()
    conn.close()
    return job_id

# 投稿処理(retry/ idempotency)
def process_job(job_id, title, content):
    conn = sqlite3.connect(DB)
    cur = conn.cursor()
    cur.execute('SELECT status, attempts, post_id FROM jobs WHERE job_id=?', (job_id,))
    row = cur.fetchone()
    if not row:
        raise Exception('job not found')
    status, attempts, post_id = row

    # 既に公開済みなら何もしない(idempotent)
    if status == 'published' and post_id:
        print('already published:', post_id)
        conn.close()
        return

    # 画像アップ等は別工程として扱う(ここでは省略)

    # 下書きが未作成なら作成する
    if status in ('created','draft_failed'):
        payload = {'title': title, 'content': content, 'status': 'draft'}
        r = requests.post(f'{WP_API}/posts', json=payload, auth=WP_AUTH, timeout=10)
        if r.status_code == 201:
            post = r.json()
            cur.execute('UPDATE jobs SET status=?, post_id=?, attempts=attempts+1, updated_at=CURRENT_TIMESTAMP WHERE job_id=?',
                        ('draft_created', post['id'], job_id))
            conn.commit()
            post_id = post['id']
        else:
            cur.execute('UPDATE jobs SET status=?, attempts=attempts+1 WHERE job_id=?',
                        ('draft_failed', job_id))
            conn.commit()
            conn.close()
            raise Exception('failed to create draft: ' + r.text)

    # 最終公開
    if status in ('draft_created','publish_failed') or (status == 'created' and post_id):
        # ここで自動チェック(プレビュー検証など)を入れても良い
        r2 = requests.post(f'{WP_API}/posts/{post_id}', json={'status': 'publish'}, auth=WP_AUTH, timeout=10)
        if r2.status_code == 200:
            cur.execute('UPDATE jobs SET status=?, attempts=attempts+1, updated_at=CURRENT_TIMESTAMP WHERE job_id=?',
                        ('published', job_id))
            conn.commit()
        else:
            cur.execute('UPDATE jobs SET status=?, attempts=attempts+1 WHERE job_id=?',
                        ('publish_failed', job_id))
            conn.commit()
            conn.close()
            raise Exception('failed to publish: ' + r2.text)
    conn.close()

# 再開側のロジックはDBのstatusを見て決められる

上記のポイント:

  • ジョブに固有ID(job_id)を付与し、状態(status)をDBに保持する
  • 「draft_created」「published」等の状態で分岐することで、処理を安全に再開できる
  • 複数回実行されても既にpublishedなら何もしない(idempotency)

部分成功(画像だけ先にアップされた)を安全に扱う方法

よくある例は画像がCDNにアップされ、投稿作成が失敗した場合です。次の点を設計に入れてください。

  • 画像の外部保存時に内部で参照可能なimage_idを得てジョブに紐づける
  • 投稿作成時はimage_idを参照してメディアを添付する(IDベースで管理すると重複アップロードを避けられる)
  • 万一画像が作成済みで投稿が未作成なら、再実行側はimageの再アップを行わず既存のimage_idを使う

リトライ戦略とバックオフ(現場での実践例)

  • 短時間の一時エラーには指数バックオフで再試行(例:1s, 2s, 4s, 8s)
  • 一定回数を超えたら自動停止して人による確認を促す(例:5回)
  • 恒久的エラー(認証失敗など)は即時停止してアラートを投げる

自動化は便利ですが、人の介入ポイント(手動承認・差し戻しUI)を必ず残してください。

運用チェックリスト(導入後に最低限見るべき項目)

  • ジョブテーブルに未完了(draft_failed / publish_failed)レコードが溜まっていないか
  • 重複した投稿スラッグや同一content_hashの複数投稿がないか
  • 外部API(CMS/CDN)のレスポンスコードとエラーメッセージを監視しているか
  • 公開前に自動QA(言及チェック、著作権キーワードなど)を挟んでいるか

テスト方法(安全性を確認する手順)

  • ローカル/ステージング環境で故意に部分失敗を作り、再開ロジックが期待通り動くか確認する
  • ネットワーク障害を模してリトライ・バックオフを検証する(toxiproxy等)
  • 同一ジョブを並列に2回起動してidempotencyが守られるか検証する

よくあるミスと回避策

  • ミス: 投稿作成→公開を一度に行う。回避: 下書き保存を経由する
  • ミス: 画像をURLベースで無制限に再アップする。回避: 画像にユニークIDを持たせる
  • ミス: DBに状態を持たずログだけで管理する。回避: 明示的なジョブテーブルを用いる

まとめ

AIを使った自動投稿で失敗から安全に再開するための鍵は「状態の永続化」と「同一ジョブの冪等性」です。工程を小さく分けてチェックポイントを設け、下書き→公開と公開を最後に限定するワークフローにすることで、多くの失敗ケースを安全に処理できます。実装面ではジョブテーブル、idempotencyキー、画像ID管理、指数バックオフ、手動差し戻しUIの組み合わせが実用的です。まずはステージングで部分失敗を意図的に作るテストを行い、本番運用に入る前に再開フローを確実に確認してください。

コメント

タイトルとURLをコピーしました