自律エージェント運用でいちばん困る失敗は、派手な例外ではなく「何も残らない停止」です。
外部投稿、ログイン、ライブ確認、ファイル探索、レビュー、本文生成。ワーカーに任せる作業が増えるほど、途中に「今の権限ではできない部分」が混ざります。ここで設計を間違えると、できる作業まで巻き込んで blocked になり、下流には何も渡りません。
この記事では、実際の配信レーン運用ログをもとに、「できない部分」と「作れる成果物」を分離するために入れた構造を書きます。一般論ではなく、自分たちのワーカー運用で起きた詰まりからのメモです。
先に、実ログ
まず、配信レーンの正本にはこうあります。
「PJがintentを作る(弾)/marketingが配信を所有する(撃ち方・射場=本レーン)/大番頭がcadence保証+評価を持つ」
この時点で、1つの作業には少なくとも3つの責務が混ざります。
- 何を出すか
- どこへ出すか
- 毎日きちんと回っているか
さらに、配信レーンにはこういう原則もあります。
「投稿数は稼働保証KPIであって評価KPIでない」
つまり、投稿できたかどうかと、効いたかどうかは別です。運用側は「投稿できない理由」を雑に blocked にせず、どこまで進んだかを残す必要があります。
実際、公開ログには「できる経路」と「見かけ上のゲート」がズレた例が残っています。
「channel-login-status.jsonのqiitaフィールドはPlaywrightブラウザcookieセッション追跡用であり、本公開経路(Qiita API v2・qiita-publish.js)とは無関係な偽陰性」
このログから言えるのは、「ログイン状態が false だから公開不能」と即断すると、実際には動く API 経路まで止めてしまう、ということです。これは単なる実装バグではなく、ワーカーの状態判定が粗いと起きる構造的な失敗です。
失敗パターン: 1つの未解決を全体停止にする
よくある実装はこうです。
async function run(card) {
const source = await collectSource(card);
const post = await writePost(source);
await publish(post);
return { status: "done" };
}
この形は見た目がきれいですが、運用では弱いです。collectSource の一部が足りない、publish が権限外、外部確認が今だけ不安定、というだけで全体が止まります。
しかも、停止した時点で post が書き出されていなければ、後続の publish レーンは何も使えません。人間がログを読んで復元するしかなくなります。
公開ログにも、パーサの不一致やラベル形式の揺れで、本文・タグ・タイトルが意図せず崩れた例が複数あります。
「TITLE:/TAGS:/BODY:ラベル形式(コロン区切り・複数行値)に未対応」
「TITLE=/TAGS=/BODY=ラベル形式(=区切り・値が次行・空行区切りの複数行値)に未対応」
ここから学んだのは、自然言語の「ちゃんと処理して」では足りないということです。ワーカー間の受け渡しは、本文そのものと、実行状態と、未解決タスクを別フィールドに分ける必要があります。
入れた構造: deliverable と result を分ける
今の形では、ワーカーは最低でも2種類の出力を残します。
-
deliverable: 下流がそのまま使う成果物 -
result.json: 実行結果、使った入力、未解決の作業
重要なのは、未解決タスクを成果物本文に混ぜないことです。
marketing ワーカーのルールにも、成果物本文は「そのまま投稿される最終コピー」として書く、という制約があります。本文に「確認してください」「実投稿前に人が確認」などを混ぜると、publish 側が公開を止める原因になります。
そこで、本文には完成コピーだけを書く。運用上の残りは result.json に寄せる。これで、コンテンツと制御情報が分離されます。
status を4値に絞る
実装上は、状態を増やしすぎないことも効きました。
{
"status": "done",
"deliverable": "qiita-jp-posts.md",
"used_context_files": [
"card.json",
"published-ledger.jsonl",
"PUBLISHED.md",
"distribution-lane.md",
"posting-limits.json"
],
"pending_action": []
}
運用では、状態をだいたい次の4つに寄せます。
-
done: コア成果物が完成した -
partial: 成果物はあるが、一部の要求が未達 -
blocked: 成果物を1つも作れない -
failed: 実行中の失敗
ポイントは、blocked を軽く使わないことです。たとえば「投稿は権限外」でも、本文を作れるなら blocked ではありません。本文を成果物として残し、投稿だけを pending_action に切り出せば、次のレーンが進めます。
pending_action は「誰かよろしく」ではなく、次のカードにする
pending_action に「owner確認」だけを書くと、結局また止まります。必要なのは、次の作業カードに変換できる粒度です。
{
"pending_action": [
{
"owner": "banto",
"department": "publish",
"action": "Qiita公開レーンで、生成済みdeliverableを投稿対象として扱う",
"reason": "contentレーンは投稿しないため"
}
]
}
これなら、制作者は外向きの副作用を実行せずに済みます。一方で、publish 側は「何をすればいいか」を解釈し直さずに済みます。
この分離は、配信レーンの原則とも合っています。posting-limits には Qiita の上限が per_day: 1 と定義されています。content ワーカーが勝手に投稿まで進めると、この上限確認や同日投稿確認を別レーンで保証しづらくなります。
実装メモ: 例外を投げる前に成果物を書けるかを見る
自律ワーカーでは、処理順をこう変えるだけでも止まりにくくなります。
async function run(card) {
const result = {
status: "failed",
deliverable: card.deliverable,
used_context_files: [],
pending_action: []
};
const context = await readAllowedFiles(card);
result.used_context_files = context.files;
const article = buildArticle(context);
await writeFile(card.deliverable, article);
const missing = detectOutOfLaneWork(card);
if (missing.length > 0) {
result.status = "partial";
result.pending_action = missing;
} else {
result.status = "done";
}
await writeFile("result.json", result);
}
先に成果物を書けるなら書く。その後で、投稿・送信・公開・ログイン・課金のような外向き作業を pending_action に分離します。
これで、「できないことが1つあるから全部止まる」状態を避けられます。
何が良くなったか
この構造にしてから、ワーカーの失敗は読みやすくなります。
- 本文が未完成なのか
- 本文は完成していて投稿だけが残っているのか
- 入力が足りず、そもそも本文が作れないのか
- ツール側のパーサやゲートが壊れているのか
公開ログに残っている「偽陰性」「ラベル形式未対応」「パーサ修正後にPATCHで訂正」のような事故も、原因が result 側に残っていれば、次の修正カードにしやすくなります。
自律エージェントの運用では、賢い判断を増やすより先に、止まった時に成果物と未解決点が分離されていることが大事でした。ワーカーが全部できる必要はありません。ただし、できた分まで消してはいけません。
避けた題材
直近のQiita記事と軸が重ならないよう、次の題材は避けました。
- 投稿原稿に「確認してください」が混ざる事故を、公開前の静的検査で止める
- マルチチャンネル自動投稿で「誤爆」と「二重投稿」を防ぐ:決定論ガードの実装ノート
- バッチ処理の入力検証で throw すると、1件の不備が全体停止になる
- バッチワーカーに中立サンドボックスを渡すと、ファイル列挙タスクが構造的に失敗する
- 自動化バグで参照パスが壊れた話: ID生成とregexの境界をそろえる