図
PRODUCT NOTE / AI エージェント・技術
社内 AI エージェントの設計
素の API 1 往復から始めて、いまは Python 約 17 万行・ツール数十本・テーブル百数十種・定時ジョブ数十本・管理画面十数枚で動いている社内エージェントの記録。
この文書は「何ができるか」ではなく、どこで形が壊れ、だから何をどう決め直したかを残した開発ノートです。同じ題材を通す側・止める側から見たノートが対になります。
TL;DR
ひとことで
エージェントの芯は 1 本のループでしかない。
難しくなるのは数が増えてからで、壊れる場所はだいたい決まっている ——ツールの説明文が毎ターンの費用になる/入口ごとに判断が分裂する/同じ事実が三重になる/走らなかったジョブは鳴らない/検査が通ったまま結論だけ逆を向く/対策を入れたつもりが本番には一度も効いていなかった。
このノートは、その 6 つに対して何を決めたかを残す。設計図は成果物ではなく、現場で更新し続ける道具として扱っている。
Problem
背景・解きたい課題
このシステムは、要件定義書から始まっていない。現場に張り付いて他人の一日を分で数え、手が止まっている場所から作るものを決めた。その進め方は速いが、代償が 1 つある —— 図が 1 枚も無いまま約 17 万行まで来た。
- 1 つで動いた形のまま数を増やした — ツール 1 個のときに正しかった置き方が、数十本になると成立しない。
- 人に見せられる図が無い — 動いているが、どのテーブルが何の事実を持っているかを説明できない。新しく入る人に渡すものが無い。
- プロンプトが書き足された結果になっている — 約 16 万字は設計した量ではなく、事故のたびに足した注意書きの総和だった。
- いちばん厄介な失敗がエラーを出さない — 検査は通り、ログは成功で、結論だけが逆を向いて外へ出ていく。
以下は、この 4 つに対して「どこまで図を描き、どこで線を引いたか」の記録です。
Core Loop
芯は 1 ターン、増えるのはその外側
最初に決めたのは「何を足すか」ではなく「何を芯に置くか」だった。芯は素の API 1 往復で、モデルを呼ぶ・結果を読む・ツールを実行する・もう一度呼ぶの 1 本のループだけ。ここに機能を書かない。
数え方: 題材システムの実測値を概数へ丸めたもの。内訳(行数・ツール・エンドポイント・テーブル・ジョブ・画面)は書籍側の付録が持つ。
増えるのはループの外側 ——ツール・入口・状態・画面・ジョブの 5 方向。この 5 つが、それぞれ別の壊れ方をする。
Solution & Scope
最初に描く図を 3 枚に絞る
背骨の 3 枚(これが無いと次の判断ができない)
- 1 ターンのフロー図 — どこでモデルを呼び、どこで止まるか
- ER 図 — どの事実をどこに置くか
- 承認境界の構成図 — 人が必ず手を入れる位置
描かないと決めたもの
- 網羅的なクラス図(増やす直前に、その範囲だけ描く)
- シーケンス図の全パターン(1 ターンのフローで足りる)
- インフラ構成の全体図(壊れ方の説明に使えなかった)
- 将来構想の図(更新されないので嘘になる)
図を網羅しようとすると止まる。「これが無いと次の判断ができない」を基準に 3 枚だけ先に描き、残りは必要になった箇所で描いて捨てる、という扱いにした。
Decision Log
主要な意思決定ログ
各判断の裏にある「なぜ」を、決定・仮説・結果の 3 点で残す。うまくいかなかったことも書く。
境界 ツールの説明文を「ドキュメント」として書くのをやめた
- DECISION
-
ツール定義の説明文を、毎ターン課金されるプロンプトの一部として扱うことにした。説明文には「いつ呼ぶか・いつ呼ばないか」だけを書き、引数の意味や背景の解説は外のカタログへ出す。
失敗時の戻り値は文章で説明せず、全ツールで同じ型に固定した。
- WHY / 仮説
-
ツールが 1 個のときは説明文の長さが問題にならない。数十本になると、説明文の総量がそのまま毎ターンの費用になる。
さらに、長い説明を足した箇所のほうが選択を誤りやすかった。条件を書き足すほど、モデルが読む分岐が増えるからだと考えた。
=説明文は「読み物」ではなく「選択のための最小の手がかり」に寄せるべき、と判断した。
- RESULT
- ツールカタログ(名前・用途・引数・失敗時の戻り値)を人間向けに別管理へ分け、モデルへ渡す定義はテンプレート 1 枚に固定した。失敗の戻り値を揃えたことで、ループ側の例外処理が 1 か所に集まった。
入口 呼ばれ方は 7 つに広げ、判断は 1 か所に寄せた
- DECISION
-
入口(チャット・Webhook・定時起動・管理画面・外部連携など)は広げてよい。ただし「何をするか」を決めるコードは 1 か所しか置かない。
入口は受け取って正規化するところまでで終わり、判断には関与させない。
- WHY / 仮説
-
入口ごとに判断を書くと、同じ修正を 7 回することになり、必ずどれかが取り残される。取り残されても動くので、気づくのは「あの入口からだと挙動が違う」と言われたときになる。
入口が増える速度のほうが、直す速度より速かった。
- RESULT
- 入口 7 つに対して判断 1 か所に落ちた。副作用として、入口ごとの差(認証の強さ・返せる情報の範囲)を「正規化の段で落とす」形に統一できた。
3 秒 同期で 3 秒という制約に、機能を削って合わせるのをやめた
- DECISION
- 一部の入口に「3 秒以内に応答を返さないと切れる」制約がある。ここに合わせて処理を削るのではなく、受付だけ返して、結果は後から別経路で届ける非同期の形に寄せた。
- WHY / 仮説
- 制約に合わせて削ると、入口ごとに機能差が出る。機能差は「判断は 1 か所」という決めと正面からぶつかる。制約は入口の性質で、判断の内容とは関係がない —— なら吸収すべきは入口側だと考えた。
- RESULT
- 遅い処理は全入口で同じ扱い(受付 → 後で通知)になった。代わりに「いつ終わるか分からない処理が増える」という別の問題が出たので、進行状況を人が見る先として管理画面を足している。
状態 同じ事実を三重に持たないと決め、現物の DDL から ER 図を起こした
- DECISION
-
テーブルが百数十種になってから、散らばった DDL を機械的に集めて ER 図を後付けで起こすことにした。
そのうえで「同じ事実(誰が・いつ・何を)を複数の場所に持たない」を規則にし、例外として許すキャッシュは「消しても再生できるもの」に限った。
- WHY / 仮説
-
同じ事実を三重に持つと、食い違ったときにどれが正かを判断する場所が無い。そして食い違いはエラーとして出ない。
出力の品質がなんとなく落ちる、という形でしか見えないので、原因として疑うまでに時間がかかる。手で ER 図を描き直すのは現実的でないので、現物から起こす側に倒した。
- RESULT
- 収集スクリプトで ER 図を起こし、4 群に割れた。三重化していた箇所はこの時点で初めて目に見えた。以後、新しいテーブルを足すときは「この事実はどこが正か」を先に決める手順にしている。
欠測 「走らなかったこと」を鳴らす仕掛けを別に作った
- DECISION
- ジョブの失敗通知とは別系統で、「所定の時刻までに走った記録が無い」を検出する側を作った。検出の対象はジョブそのものではなく、ジョブが残すはずの記録の有無。
- WHY / 仮説
-
失敗は鳴る。走らなかったことは鳴らない。
定時ジョブが数十本になると、止まっていることに気づく経路がどこにも無い。実際に、止まっていたことに気づいたのは「最近この数字が動かない」と言われてからだった。
失敗通知と同じ系統に作ると、系統ごと落ちたときに両方黙る。
- RESULT
- 欠測の検出が通知の主役になった。失敗通知のほうは件数が多くて読まれなくなっていたので、結果として見る場所が 1 つに減った。
送信口 判定を真偽の二値でなく三値にし、UNKNOWN を送信口で止めた
- DECISION
- 外へ出る前の判定を STOP GO UNKNOWN の三値にした。UNKNOWN は通さず、送信口で止めて人に回す。
- WHY / 仮説
-
二値にすると「判定できなかった」が GO 側に混ざる。混ざり方が静かで、検査は通り、ログは成功、根拠として挙げた材料も正しいのに、結論だけが逆を向いて外へ出ていく。
この壊れ方は緑のまま起きるので、監視では見つからない。判定できなかったことを、失敗とは別の状態として持つ必要がある、と考えた。
- RESULT
- UNKNOWN を人の確認へ回す経路を作った。止まる件数は増えたが、外へ出てから気づく件数が減った。止めた件のログが、プロンプトに注意書きを足す代わりの材料になっている。同じ三値の考え方は、のちに別の通知の優先度判定にも転用した。
委譲 1 体に積むか複数へ割るかを、性能ではなく承認の線で決めた
- DECISION
- 分割の基準を能力や性能に置かず、「誰宛の結果か」に置いた。自分宛(社内の依頼者自身に返るもの)は自動で通す。他人宛(第三者に届くもの)は必ず人が承認する。
- WHY / 仮説
-
モデルは変わる。性能を基準に分割すると、モデルが変わるたびに線を引き直すことになり、そのたびに安全側の設計が緩む方向へ動く。
承認の線は性能が上がっても動かない —— 誰に届くかはシステムの性能と無関係だから。
- RESULT
- 承認境界の構成図が、背骨の 3 枚目として固定された。モデルの入れ替えで見直す対象が「精度の期待値」だけになり、権限の設計を触らずに済んでいる。
移植 許可を足すとき、対になる禁止も一緒に移すことにした
- DECISION
- 新しいふるまいを許可する文を足すときは、それと対になる禁止の文を同じ場所に書く。見つかった漏れには、形式的な除去と、言い換えられた場合の検出を重ねる、二層の防御を持たせた。
- WHY / 仮説
- 許可する文だけを新しい場所に足し、対になる禁止の文を移し忘れたことがあった。許可された範囲の中で、本来は答えてはいけないことまで答えてしまった。
- RESULT
- 許可と禁止を近くに書く運用に変え、漏れを機械的に取り除く層と、自然な言い回しに変えられた場合を拾う層を重ねた。
所在 サーバーが答えをすでに持っている判断は、モデルに委ねないことにした
- DECISION
- 出す・出さないをサーバー側がすでに決められる種類の制御は、モデルの生成に委ねない。生成物からは無条件に取り除き、必要ならサーバー側が機械的に足す。
- WHY / 仮説
- モデルに判断させると、同じ条件でも出たり出なかったりする。サーバーはすでに判定材料を持っているのに、わざわざ不確実な側に判断を預ける理由が無かった。
- RESULT
- 対象の制御をサーバー側の決定的な処理に移した。先に生成を始めてから差し込む形にしたところ、取り除くはずのものがまるごと消える副作用が1つ見つかり、合わせて直した。
転用 既存の仕組みを新しい使い道に転用するとき、前提を書き出すようにした
- DECISION
- ある目的のために作った仕組みを別の目的に転用するときは、転用前にその仕組みが置いていた前提を書き出して確認する。
- WHY / 仮説
- 定期処理向けに作った仕組みを常時監視に転用したとき、前提の1つを確認しないままにしたために、ある期間まるごと機能しない状態が続いた。失敗は毎回記録されていたが、失敗を飲み込んで動き続ける設計だったので誰も気づかなかった。
- RESULT
- 転用のたびに前提を書き出す手順を加えた。あわせて、鳴らす対象の欠測検知に「動いた記録」だけでなく「狙った成果が出ているか」も加えた。
検出 検出するだけで止める力を持たない対策は、本番で動いていないのと同じだと考え直した
- DECISION
- 同種の対策を複数回足したが、どれも検出してログに残すだけで、実際の出力を変える力を持っていなかった。既定を「検出したら機械的に書き換える」側に倒し、入れた対策が本番の設定に実際に反映されているかを別途確認する工程を持つことにした。
- WHY / 仮説
- 検出だけの対策は、有効にし忘れても何も起きないので気づく手段が無い。同じ種類の対策を複数回足していたのに、どれも本番の設定には一度も反映されていなかったことが分かった。
- RESULT
- 既定で本文を書き換える方式に変え、過去に反映され忘れていた分もまとめて本番へ入れた。「直したか」ではなく「本番で効いているか」を確認する工程を、以後の作業に組み込んだ。
匿名化 書籍 Vol.3 を発売中止にし、このノートは判断だけに留めた
- DECISION
-
題材を特定できる記述を載せない、と決めた。書籍 Vol.3 は発売中止にして Vol.3.5 として作り直し中(刊行未定)。
このノートも同じ方針で、実コード・DDL・図・固有名は 1 つも置かない。規模は概数まで丸める。
- WHY / 仮説
-
社名・人名・製品名は 1 つも書いていなかった。それでも実コードとスキーマを並べれば、題材のリポジトリは特定できる。
行数・テーブル数・ジョブ数のような数字も、組み合わせれば同じ働きをする。「実名が無いから大丈夫」は判定として成立していなかった、と考え直した。
- RESULT
-
残すものを「判断と、その理由」だけに絞った。図とコードを外したぶん説明力は落ちるが、判断は題材が違っても当てはめ直せるので、残す価値があるのはこちら側だと整理した。
書籍側は体裁を教科書に組み直している。
Traps
気づきにくい壊れ方
上の判断は、どれも「静かに壊れたあと」に決めている。先に知っておきたかった順に並べる。
静かに壊れる 3 つ
- 走らなかったジョブ — 失敗は鳴る。走らなかったことは鳴らない。記録の有無を別系統で見る。
- 三重化した事実 — 食い違いはエラーにならず、出力の品質としてしか現れない。
- 緑のまま出ていく結論 — 検査は通り、根拠も正しく、結論だけが逆を向く。二値の判定では捕まらない。
もう 1 つ、壊れ方ではないが効いたものとしてプロンプトの扱いがある。約 16 万字は設計した量ではなく、事故のたびに足した注意書きの総和だった。
足すほど費用が上がり、読む分岐が増えて選択が悪くなる。いまは「注意書きを足す」前に「送信口で止める」か「ツールの戻り値で表す」で解けないかを先に見る順にしている。
実行時に何にどれだけ渡しているかは、実測の側で数えた。
Roadmap / Open Questions
これから
- 管理画面が十数枚まで増えた。閉じる操作を持たない画面は増える一方のリストを作るので、終わらせ方を持たない画面から畳む。
- 内部構造は動いているが増やせない形になっている箇所がある。レイヤを畳み直す範囲を決める(全面ではなく、次に増やす方向だけ)。
- プロンプトの約 16 万字を、どこまで「送信口の判定」と「ツールの戻り値」へ移し替えられるか。移した量と費用の関係を測る。
- 欠測の検出そのものが止まったときに、それを誰が知るか。系統をもう 1 段外に持つべきかは未決。
- 対策が本番で実際に効いているかを定期的に確認する工程は、いまのところ人手の棚卸しに頼っている。自動で確認できる範囲を広げられるかは未定。
References
関連
題材は社内で運用しているシステムです。社名・人名・製品名・リポジトリ名は記載していません。規模の数字は概数へ丸めてあり、実コード・スキーマ・図は掲載していません。記述は設計上の判断の整理であり、特定の構成を推奨するものではありません。