LLMで業務を自動化するとき、最初に決めるのは「次のステップを誰が決めるか」です。コードか、モデル(LLM)か。
この記事では、AIエージェント(以下、エージェント)を「モデルが次のステップを決めるシステム」と定義します。コードが手順を決め、モデルは各ステップの中身を埋めるだけなら、それはワークフローです。この区別を明確にしておくと、「とりあえずエージェントで作る」前に、もっとテストしやすい設計で足りるかどうかを判断できます。
自律性の4つの段階
LLMを使うシステムは、次の4つの段階のどれかに位置づけられます。上の段階ほど、多くの判断をモデルに任せます。
| 段階 | 次のステップを決めるのは | テストのしかた |
|---|---|---|
| 1回の呼び出し | コード | 入力と正解の組を集めて比べる |
| 固定ワークフロー | コード | ステップごとに個別にテストする |
| ルーター(振り分け) | 振り分け先だけモデル、その先はコード | 分類器と同じく、振り分け先ごとの正解率を測る |
| エージェント | 毎ステップ、モデル | 同じケースを何度も実行し、成功率を見る |
そもそもLLMを使わず、普通のコードで書ける処理もあります。正規表現や対応表で済むなら、モデルは要りません。
エージェントだけは、実行ごとに違う手順をたどります。1回うまく動いても次も同じとは限らず、コストも毎回変わります。
手順書で回る仕事はスクリプトにする
運用の現場には、昔からこの区別があります。手順書どおりに進む作業は、スクリプトにして自動化します。手順書を書けない原因不明の障害には、経験のあるエンジニアをアサインします。そのエンジニアはログを見て、次にどこを調べるかを自分で決めます。
LLMでも同じように分けられます。手順を書ける仕事はワークフローに、次に何を調べるかを調べながら決める仕事はエージェントに任せます。ただし、このたとえには限界があります。人間のエンジニアは結果に責任を持ち、失敗から学んで次に活かします。エージェントは同じ入力でも毎回違う判断をすることがあり、仕組みを用意しない限り前回の失敗を覚えていません。だから、エージェントに任せる範囲は、人に任せる範囲より狭くしてください。
プロンプトが手順書になっていたら要注意
エージェントとして作ったものが実はワークフローだった、というケースは、プロンプトを見るとわかります。たとえば次のようなプロンプトです(架空の例です)。
あなたは問い合わせ対応アシスタントです。
1. まず問い合わせの種類を判定してください
2. 配送の問い合わせなら、必ず注文番号を確認してから照会ツールを呼んでください
3. 照会ツールは1回だけ呼んでください
4. 返金は絶対に約束しないでください
1から4はすべて、問い合わせを読む前から決まっています。それを毎回モデルに読ませて、たいていは守ってもらう作りです。たまに守られない実行が出ます。著書ではこの状態を次のように書きました。
ルールの一つひとつが、自然言語で書かれ、守られるよう祈るしかないコードです。
番号付きの手順や「必ず」「絶対に」が増えてきたら、その手順をコードに移す合図です。
段階を決める3つの問い
どのタスクも、次の3つの問いに順に答えていけば段階が決まります。
1. 手順は事前にわかっているか。 入力を受け取る前から手順を書けるなら、その手順はコードにします。段階でいえば1回の呼び出しか固定ワークフローです。決まった手順がいくつかあり、わからないのは「どれに当てはまるか」だけなら、ルーターで足ります。エージェントが必要なのは、次のステップが直前のステップの結果で変わるときだけです。
2. 間違ったステップはどれだけ高くつくか。 エージェントは、いつか間違ったステップを踏みます。間違ったレコードを読むだけなら損は小さいですが、誤った返金は取り消しが面倒です。取り消しにくい操作は、コード側の承認ステップの後ろに置いてください。プロンプトに「勝手に返金しないこと」と書いても、確率が下がるだけです。
3. 出力を、モデル以外の仕組みでチェックできるか。 テストが通るか、合計が帳簿と合うか、引用した文が原文に本当にあるか。こうした判定がモデルの外にあれば、正しい結果と、正しそうに見えるだけの結果を区別できます。判定方法が「人が毎回すべて読む」しかないなら、タスクを絞るか、下の段階にとどまってください。
例:問い合わせ窓口を段階に分ける
ECサイトの問い合わせ対応を自動化する、という架空の例に3つの問いを当てはめてみます。1つの機能に見えますが、中身は段階の違う仕事の組み合わせです。
| 仕事 | 段階 | 決め手 |
|---|---|---|
| 問い合わせ文から注文番号などを抜き出す | 1回の呼び出し | 入力1つ、出力1つで、正解と比べられる |
| 配送・返品・請求・不具合・判断不能に振り分ける | ルーター | わからないのは種類だけ |
| 配送状況を照会して定型文で返す | 固定ワークフロー | 手順を事前に書ける |
| アプリの不具合の原因を一次調査する | エージェント | 次に見るもの(ログ、直近のリリース、障害情報)が、前に見つかったことで変わる |
ルーターの出力は、JSON Schemaのenumのような構造化出力で選択肢を固定します。それでも間違った振り分け先を選ぶことはあるので、「判断不能」を正式な選択肢に入れて人に回します。誤りが高くつくなら、指示の書き方を変えた2回の呼び出しで振り分け、結果が食い違ったものを判断不能として扱う方法もあります。モデルが回答に書く「確信度90%」は、それ自体が生成されたテキストなので当てにしないでください。
エージェントを置くのは、不具合調査の1か所だけです。ツールは読み取り専用にし、顧客データの変更やクーポンの発行は人の承認を待たせます。調査結果には根拠のログ行やリリース番号を必ず添えさせ、それが実在するかをコードで確かめます。これで3つ目の問いにも答えられます。
ステップの間にゲートを置く
ワークフローを信頼できるものにするのは、ステップとステップの間に置くチェックです。ここではゲートと呼びます。ゲートは前のステップの出力を読み、「次へ進める」「理由を添えてやり直す」「人に回す」のどれかを返す、数行のコードです。
チェックする内容は3種類あります。
- 形式:JSONとして読めるか、必須の項目があるか、ラベルが決められた選択肢に入っているか
- 妥当性:件数、合計、文字数などが入力と見合っているか
- グラウンディング(根拠との照合):出力に書かれた値や引用が、元の文書に実際にあるか
LLMは、もっともらしい注文番号を作ってしまうことがあります。グラウンディングのチェックなら、それを文字列検索で捕まえられます。先ほどの例で、配送のワークフローの抽出ステップの後に置くゲートを書くと、次のようになります。
import re
import unicodedata
ORDER_ID = re.compile(r"[A-Z0-9]{10}") # 自社の注文番号の形式(例)
def normalize(s: str) -> str:
# 全角英数字を半角に、小文字を大文字にそろえ、ハイフンと空白を除く
return re.sub(r"[-\s]", "", unicodedata.normalize("NFKC", s).upper())
def gate_shipping(inquiry: str, out: dict, retries_left: int) -> str:
"""抽出結果を照会ステップへ渡してよいかを判定し、pass / retry / human を返す"""
fail = "retry" if retries_left > 0 else "human"
order_id = out.get("order_id")
# 注文番号が書かれていない問い合わせは、やり直しても出てこないので人へ
if order_id is None:
return "human"
# 形式:文字列で返っているか
if not isinstance(order_id, str):
return fail
order_id = normalize(order_id)
# 妥当性:自社の注文番号の形式に合っているか
if not ORDER_ID.fullmatch(order_id):
return fail
# グラウンディング:注文番号が問い合わせ本文に実際に書かれているか
if order_id not in normalize(inquiry):
return fail
return "pass"
すべてのステップの間に置く必要はありません。誤りが下流で気づかれずに流れてしまうステップの後と、メール送信やDB更新のように外部に作用するステップの前に置きます。
「おかしな入力に気づけるエージェントにすればいい」という案もよく出ます。しかしそれは、チェックをコードからモデルの判断へ移すことになります。コードのチェックはすべての入力で毎回動きます。モデルが気づくのは、気づいたときだけです。
エージェントに上げる合図
ワークフローで始めても、いずれ収まらなくなる部分が出てきます。次の兆候が見えたら、その部分を上の段階に移す時期です。
- 分岐の条件が「前のステップで見つかったこと」になっている(「ログにタイムアウトがあれば、次はDBの負荷を見る」など)
- 「判断不能」に回る件数が週ごとに増え、その1件ずつに何回も照会が要る
- 入力によって、必要なステップ数が2回で済んだり10回かかったりする
移すのは、手順が読めない1つのステップだけです。そのステップをエージェントにし、前後のゲートは残します。
モデルを替えたら判断し直す
この線引きは、今使っているモデルが前提です。モデルの世代が進むと1つのエージェントに任せられる範囲は広がり、前のモデル向けに作った分類器や分岐が要らなくなることもあります。正解のわかっている実際のケースを評価データセットとして残し、モデルを替えるたびに、分岐を減らした構成と今の構成を成功率とコストで比べてください。
ただし、実行を始めるトリガー(スケジュールやメールの着信)、毎回必ず必要な承認、大量の件数を流す処理はコードに残します。大量処理では、呼び出しが1回増えるだけでコストが件数倍になります。
作り始める前のチェックリスト
- 入力を見る前に手順を書き出せないか。書けるなら、1回の呼び出しか固定ワークフローにする
- わからないのが「どの種類か」だけなら、ルーターと「判断不能」の振り分け先で足りないか
- プロンプトに番号付きの手順や「必ず」「絶対に」が増えていないか
- 取り消しにくい操作は、コード側の承認の後ろにあるか
- テスト、合計の一致、引用の実在確認など、モデルの外で出力を確かめる手段があるか
- 正解付きの評価データセットがあり、モデルを替えるたびに比べ直せるか
この記事のもとになった本
この記事は、筆者の著書『AI Agents Made Simple』の第1章「そもそもエージェントは必要か」と第3章「まずはワークフローから」の考え方をもとに、新しく書き下ろしたものです。同書の日本語版(副題「実際に機能するAIエージェントを作るための意思決定ガイド」)を出しました。全28章で、各章が1つのパターンを取り上げ、どう動くのか、何が壊れるのか、どんなコストがかかるのか、どんなときに過剰なのかを扱います。
Kindle版は、発売から1週間は250円、その後は1,650円です。
※筆者は日本語を母語としないため、この記事はAIの支援を受けて日本語で書きました。不自然な表現や誤りがあれば、コメントで教えていただけると助かります。
