私はAIの専門家ではありません。
3日前まで、エージェントという言葉の意味もよく分かっていませんでした。それでも、複数のAIに仕事を分担させて24時間動かす構成を作ることはできました。
そして、3日間で8回、自分の間違いを訂正しました。
この記事は、うまくいった話ではありません。初心者が何につまずくかの記録です。動くコードも出します。
1日目 ― 「動かす」の前に「止まる」を考えていなかった
最初にやったのは、データを集める処理を動かすことでした。動きました。数時間かかる処理です。
問題は、それが途中で止まったときに誰も気づかないことでした。
私は画面を見ていました。見ていない間に止まったら? そのときは、次に画面を開いたときに気づきます。それが何時間後かは、運次第です。
ここで最初の間違いをしました。生死を2値で考えたことです。
最初の実装 動いている / 止まっている
実際には3つ目があります。「分からない」です。
# 生死は3値で持つ。**不明を死に丸めない**
def state(job, now):
if pid_alive(job.pid):
return "RUNNING"
if job.progress_age is None: # 進捗の出所が読めない
return "UNKNOWN" # ← **ここを DEAD にしない**
if job.progress_age > job.stall_sec:
return "STALLED"
return "NOT_RUNNING"
「分からない」を「死んでいる」に丸めると、生きている処理を殺しにいきます。
逆に「生きている」に丸めると、死んだまま何時間も気づきません。
3つ目を持つだけで、両方が防げます。
進捗の出所は、ジョブ自身に宣言させる
見張りを作って、すぐ次の間違いをしました。
ログファイルの更新時刻で「進んでいるか」を判定したのです。
ところが、ログではなくデータベースに書く処理がありました。ログは静かなままです。
3時間無音に見えて、実際は正常に動いていました。
# ジョブごとに「進んでいる証拠がどこに出るか」を宣言させる
jobs:
- id: collect
progress: {kind: file_mtime, path: logs/collect.log}
stall_sec: 1800
- id: probe
progress: {kind: sqlite, path: data/live.db, column: started_at}
stall_sec: 900
宣言できないジョブは登録させないことにしました。
「どこを見れば進捗が分かるか」を書けないものは、そもそも見張れません。
2日目 ― 複数に分けたら、誰が何をしているか分からなくなった
作業を分担させました。データ収集、分析、検証。速くなりました。
同時に、「いま何が動いていて、何が終わったか」が分からなくなりました。
チャットの履歴をさかのぼって確認する。それ自体が作業になっていきます。
このあたりで、AIを増やすほど自分の仕事が増えるという奇妙な状態に気づきます。
答えは単純でした。状態を会話ではなくファイルに置く。
会話に置く その会話を開いた人しか知らない。**閉じたら消える**
ファイルに置く 20分ごとに動く小さなプログラムが読める。**文脈が要らない**
3日目 ― 費用を測ってみたら、桁が違った
ふと、どれだけ使っているのかを調べました。
セッションの長さ 17時間
ターン数 469
読み直したトークン 108,575,962
→ 1ターンあたり 231,505
1回のやりとりごとに、23万トークン。
会話が長くなるほど、AIは毎回それまでの全部を読み返します。知らないうちに、1歩の値段が上がり続けていました。
もっと意外だったのはこちらです。
別のAIに作業を投げた場合 1回あたり 3,372 トークン
自分の会話で進めた場合 1ターン 231,505 トークン
約69分の1でした。
私は逆に思っていました。「AIを増やすと高くなる」と。だから自分で抱え込んでいた。
その日の作業の85%は、投げるべきものでした。
トークンを減らしたいなら、最初に見るのはプロンプトの長さではありません。セッションの寿命です。
監視を止めたら、その1時間後に対象が死んだ
長時間動く処理に、見張りをつけていました。
その処理を止めようとする過程で、私は先に見張りのほうを止めました。
1時間後、処理は別の理由で異常終了しました。誰も気づきませんでした。
13:25 異常終了
14:21 「進んでる?」と聞かれて、はじめて発覚
→ **57分間、何も動いていなかった**
止めたい対象こそ、止まった瞬間を知る必要がある。
当たり前のことですが、その場では分かりませんでした。
いま見張りは機械が持っています。人もAIも持ちません。
人が見張る その人が止めた瞬間、見る目が消える
AIが見張る 費用がかかり、しかも会話が長くなるほど高くなる
機械が見張る 20分ごとに動く小さなプログラム。**費用はゼロ**
AIは、古い記録を疑わない
3日間で3回、同じことが起きました。
自分が前に書いたメモが古くなっていて、AIがそれを現在の状況として読んで、正しく推論して、間違った結論に着く。
AIは書かれていることを信じます。疑いません。
だから記録が古いだけで、そこから先の判断が全部ずれます。しかも一見もっともらしいので、気づきにくい。
対処は、記録と実物を機械が突き合わせることでした。
# 記録が「公開済み」と言っている記事が、本当に公開されているか
for rec in published_records():
live = fetch_title(rec["url"]) # 実物を取りに行く
if live != rec["title"]:
print("記録と実物が食い違う:", rec["file"], "|", rec["title"], "≠", live)
生成物ではなく、記録のほうが先に古くなります。
後日談 ― 自分の「できました」を数えてみた
ここまでの間違いには共通点があります。どれも「対処済み」と自分で記録していました。
そこで、「できました」と言った回数そのものを数える台帳を作りました。
要点はひとつです。申告した時点で「どうすれば嘘だと分かるか」を書かせる。
$ python claims.py add --by 自分 --kind done \
--claim "記事を公開した" \
--check "APIの一覧に当該記事が含まれる" # ← これが無いと登録できない
$ python claims.py add --by 自分 --kind done --claim "テスト" --check ""
却下: --check(どうすれば嘘だと分かるか)が空。
**反証条件を書けない申告は、申告として数えない。**
初日の結果です。
申告 6件(真3 / 偽1 / 未検証2)
的中率 75% 確かめたもののうち本当だった割合
検証率 67% そもそも誰かが確かめた割合
偽が1件出ました。「記事を公開した」と記録した直後、反証条件どおりに機械が確認したら、
実際には公開されていませんでした(投稿数の制限に当たって拒否されていた)。
私はそれを知らずに「公開しました」と報告する寸前でした。
**検証率のほうが大事だと思っています。**的中率だけを見ると、
「確かめやすいことしか確かめていない」状態を見逃します。
**この数字は上界です。**台帳に載るのは捕まった誤りだけなので、本当の誤り率はこれ以上あります。
同時に1つしか使えないものが、24時間運用の上限を決めた
複数を並列に動かそうとして、すぐ詰まりました。同時に1つしか使えない資源があったからです。
外部への接続、台帳への書き込み、公開。これらは並列にできません。
逆に言うと、それ以外は何本でも同時に動かせます。
排他 接続 / 台帳への書き込み / 公開 → 常に1本
それ以外 調べる・書く・検証する → 何本でも同時
役割で分けるのをやめて、資源で分けることにしました。
「調査担当」「実装担当」と分けても、両方が同じ接続を使うなら並列度は1のままです。
実際、同じ資源を使う3つの部門を1つに統合しました。分けても速くならなかったからです。
ロックはファイル1つで足ります。
# 誰が握っているかと、そのプロセスが生きているかを見る
lock = read_lock() # {"pid": 1234, "owner": "collect"}
if lock and pid_alive(lock["pid"]):
skip("%s が使用中" % lock["owner"]) # **絶対に起動しない**
**「ロックファイルがある」だけでは足りません。**握っていたプロセスが死んでいると、
ロックだけが残ります。pidの生死まで見ないと、永久に起動できなくなります。
使い回そうとして、初めて分かったこと
3日目に作った見張りを、別の作業でも使おうとしました。動きませんでした。
中を見たら、最初のプロジェクトの実パスが直接書かれていました。
ロックファイルの場所、データベースの場所、判定に使う条件。
書いてあったもの C:\...\session.lock / C:/.../live.db / 特定の条件名
起きたこと 別の作業では、**存在しないパスを見に行った**
厄介なのは、それでも動いてしまうことです。
ファイルが無ければ「進捗が読めない」となり、「分からない」が「止まっている」に化けます。
最初に3値にしておいたのに、設定が間違っていれば意味がありません。
道具の側に、特定の作業の事情を1つも持たせない。
パスも資源名も条件も、外の設定ファイルから渡すようにしました。
道具 仕組みだけを持つ。**固有の値はゼロ**
設定 プロジェクトごとに1ファイル。**無ければその機能は黙って無効**
「どの設定で動いているか」が1行で出るようにもしました。
それが分からない状態が、いちばん危ないと思ったからです。
3日で分かったこと
1 会話が長いほど高い。**作業の区切りで切る**
2 任せたほうが安い。自分で抱えると桁が変わる
3 見張りは機械が持つ。人が持つと、人が止めた瞬間に消える
4 AIは記録を疑わない。だから記録を機械が点検する
5 並列度を決めるのは役割ではなく資源。**同じ資源なら分けても速くならない**
6 道具に固有のパスを埋めない。**使い回そうとした日に必ず気づく**
7 「できました」は測れる。**測るには反証条件を先に書かせる**
7つとも、始める前には想像していませんでした。
AIは速く作ります。問題は、作ったものが正しいかを確かめる側が追いつかないことでした。
だから私は、確かめるほうを機械にしました。
作るのはAI、確かめるのは機械、決めるのは人。
初心者が3日でたどり着いたのは、この分け方ひとつです。
次は、この申告の台帳を続けた結果を書きます。件数が増えたときに的中率がどう動くか、
そして測っていること自体が申告を変えてしまうかを測ります。
この連載は、実際に測った数字だけで書いています。
同じことを試す人の役に立ちそうなら、ストックしておいてください。
この連載
AIエージェントを実際に回して、かかった費用と壊れた箇所を測って書いています。推測は書きません。実測値だけです。
- Claude Code のトークン使用量を実測したら1ターン23万 ― 節約に効いたのは「セッションを切る」だった
https://qiita.com/manabu49-ai/items/2598a30d5140e4445ab6 - 57分間、死んだジョブを誰も見ていなかった ― AIエージェントの見張りを機械に渡す
https://qiita.com/manabu49-ai/items/5c7506c94b5be58c040e - AIに100通り試させたら、効果ゼロなのに5個が「有意」だった
https://qiita.com/manabu49-ai/items/397a018948e1ab7ef29e - 仕様書は2日前から手元にあった ― 読まずに実装した対処が、データを永久に失う設計だった
https://qiita.com/manabu49-ai/items/9b947604d3dc4958fa36 - Qiitaで何が読まれるかを3,478本から探した ― 106通り掃いて、残ったのは6つだけだった
https://qiita.com/manabu49-ai/items/d78a5aeaecf8d7f3f20b - 本記事 ― AIを触り始めて3日で24時間動くものを作った ― そして8回間違えた
番外 ― Claude のモデル提供終了、猶予は告知から約60日 ― 過去9回を数えて、手元を検査するスクリプトを書いた
https://qiita.com/manabu49-ai/items/bc2a7be2a6ee4ceb6185
**無料記事は結論まで全部書きます。**出し惜しみはしません。