AIエージェントをゼロから構築する (2): 脳に身体を

その1 では、MAIS(マルチエージェント相互運用システム)の技術的基盤を構築いたしました。「脳」の配線に成功し、LiteLLMを用いた堅牢なアダプターを構築し、IRIS資格情報でAPIキーをロックダウンし、そしてついにPython相互運用性のパズルを組み立てたのです。 しかしながら、現時点では我々のシステムはLLMへの単なる未加工のパイプに過ぎません。テキストを扱うことはできますが、アイデンティティを欠いているのです。
さて、この第2部では、エージェントの構造についてご説明いたします。単純なAPI呼び出しから、構造化されたペルソナへと進みます。LLMをビジネスロジックの層でラップし、その名称やロールを定義し、そして最も重要な点として、隣接する要素を認識する能力を付与する方法について学んでまいります。
私たちのマシンの「魂」を構築しましょう。
エージェントの構造: ただのプロンプトではなく
「脳」(LLM)との接続が確立したところで、次にその「脳」にパーソナリティを付与する必要があります。よくある誤解として、エージェントとは単に「役立つアシスタントです。」といったシステムプロンプトに過ぎないという見方がありますが、それは単なるチャットボットに過ぎません。
真正なエージェント型AIは、監視を必要としない点で際立っています。それは自律性と、任務を完遂しようとする強い意欲を兼ね備えています。例えば、販売予約の前に在庫を確認するなど先を見据え、障害に遭遇した場合でも、単に諦めるのではなく回避策を模索します。
この複雑性をIRIS内に封じ込めるため、dc.mais.adapter.Agentクラスを開発いたしました。これは「ペルソナ定義」として機能し、生LLMを厳密な運転境界内で効果的にラップします。
あらゆるエージェントは特定の設定セットに基づいて構築されます。まず最初に、一意な識別子と専門領域(例:「フランス料理の専門家」)を確立するため、名前とロールを設定します。幻覚やスコープクリープを防ぐため、明確な目標と詳細なタスクをチェックリストとして設定します。また、OutputInstructions(出力指示)を通じてコミュニケーション基準を徹底し、エージェントに簡潔な表現を求めたり特定の文字の使用を避けさせたりするとともに、実行が許可されているツール(JSON定義)を提供します。
「ターゲット」が重要な理由について
いずれの観点から見ても、この仕組みにおいて最も重要なコンポーネントはターゲットです。このプロパティにより、いわゆる分散型ハンドオフが可能となります。
すべての処理を中央のスーパーバイザー経由でルーティングする代わり、Targetプロパティはチェーン内の有効な次のステップをカンマ区切りでリスト化します。例えば、MenuExpertエージェントは自身の役割が料理選びの支援であることを認識しています。しかし、Targetプロパティのおかげで、ユーザーが「お会計をお願いします」と言った時点で、必ず CashierAgentに処理を引き継がなければならないことも理解しています。
これは「推論エンジン」を構築し、LLMが自身の限界を理解できるようにします:「私は食品の専門家ですが、支払いの処理は許可されていません。レジ係に連絡する必要があります。」
以下は、そのクラスの現在までの様子です:
Class dc.mais.adapter.Agent Extends Ens.OutboundAdapter
{
/// 本番の設定において、どのプロパティを表示するかを制御する
Parameter SETTINGS = "Name:Basic,Role:Basic,Goal:Basic,Tasks:Basic:textarea?rows=5&cols=50,OutputInstructions:Basic:textarea?rows=5&cols=50,Tools:Basic:textarea?rows=5&cols=50,Target:Basic,Model:Basic,MaxIterations,Verbose";
/// エージェントの一意な識別子
Property Name As %String;
/// このエージェントで達成すべき主目的
Property Goal As %String(MAXLEN = 100);
/// エージェントの機能と専門性についての説明。例:「このアシスタントは知識豊富で役立つ存在であり、フォローアップの質問を提案する。」
Property Role As %String(MAXLEN = 350);
/// エージェントが回答をどのようにフォーマットし、提示すべきかについてのガイドライン
Property OutputInstructions As %String(MAXLEN = 1000);
/// エージェントが遂行すべき責任と行動の順序付きリスト
Property Tasks As %String(MAXLEN = 1000);
/// エージェントに利用可能な呼び出し関数のリスト
Property Tools As %String(MAXLEN = 10000);
/// 有効な次のエージェント名(カンマ区切り、例:「OrderTaker,OrderSubmitter」)
Property Target As %String(MAXLEN = 1000);
// コンマ区切りまたはJSON形式による拡張性
/// LLM model name (allows different agents to use different models)
Property Model As %String;
/// 停止するまでのツール呼び出しの最大反復回数
Property MaxIterations As %Integer [ InitialExpression = 3 ];
}
この階層化こそが秘訣です。これにより、単なる「AIが理解してくれることを願う」という一般的なアプローチから、「計画、割り当て、監視」という体系的なシステムへと進化させます。本質的には、LLMの予測不可能性をビジネスロジックという安全策で包み込んでいるのです。
しかしながら、データベースクラスにこれらのプロパティを保持するだけではありません。これらの厳密な設定を、LLMが理解できる自然言語の指示に変換する必要もあります。
ここでダイナミックプロンプトエンジニアリングが活躍する場面となります。
エージェントのパーソナリティを構築するためのファクトリーとして機能するGetAgentInstructionsというメソッドを実装いたしました。これは単に文字列を連結するのではなく、AIのメンタルモデル全体を層ごとに構築するものです。
「ご近所様の知識」(ハンドオフ)
If (..Target '= 「」) ブロック内のロジックに注意すべきです。これはネットワークを結びつける接着剤のような役割を果たしています。実際、この処理によってエージェントに、その隣接するエージェントが誰であるかを正確に伝えているのです。
これは「許可リスト」として機能します。これにより、MenuExpertが顧客を存在しないParkingAttendantに転送しようとすることを防止します。プロンプトレベルで業務プロセスフローを強制します。実際の転送メカニズムはオーケストレーター(後ほど説明いたします)に属しますが、転送の「認識」はここから始まります。
防御的プロンプティング
また、ツール使用ガイドラインのセクションにも留意すべきです。モデルに対して明示的に指示しています:「データを推測したり、創作したりしないでください」
これは英語に用いられる防衛的プログラミングです。モデルがメニューを誤って生成したり、注文確認を偽装したりするのを事前に防止し、当社が提供するネイティブツールを利用するよう強制しています。
以下の実装方法をぜひ確認してみましょう:
Method GetAgentInstructions(Output oPrompt As %String) As %String
{
Set tSC = $$$OK
Set oPrompt = "" // Ensures it's not null
Try {
Set prompt = "You are "_..Name_", a specialized agent."_$C(10)
Set:(..Role '= "") prompt = prompt_"## Your Role: "_..Role_$C(10)
Set:(..Goal '= "") prompt = prompt_"## Your Goal: "_..Goal_$C(10)
Set:(..Tasks '= "") prompt = prompt_"## Your Tasks: "_..Tasks_$C(10)
Set:(..OutputInstructions'= "") prompt = prompt_"## Output Instructions: "_..OutputInstructions_$C(10)
// --- ハンドオフロジック:ご近所様のご紹介 ---
If (..Target '= "") {
Set prompt = prompt_"## Handoff Capabilities:"_$C(10)
Set prompt = prompt_"- You can transfer the conversation ONLY to the following agents: "_..Target_$C(10)
Set prompt = prompt_"- Use the 'handoff_to_agent' tool with one of these exact names."_$C(10)
}
// --- ツールの防御的プロンプティング ---
If (..Tools '= "") {
Set prompt = prompt_"## Tool Usage Guidelines:"_$C(10)
Set prompt = prompt_"- You have access to functions (tools) to get real data."_$C(10)
Set prompt = prompt_"- You MUST call the function natively when needed."_$C(10)
Set prompt = prompt_"- Do NOT guess or invent data. Use the function."_$C(10)
Set prompt = prompt_"- NEVER write the function call JSON in the response text. Just trigger the function."_$C(10)
}
Set prompt = prompt_"# Remember: You are part of a multi-agent system."
Set oPrompt = prompt
} Catch ex {
Set tSC=ex.AsStatus()
$$$LOGERROR("Error generating instructions: "_ex.DisplayString())
}
Return tSC
}
エージェントがそれぞれの指示を受け取り、隣接するエージェントを認識したところで、実際にその指示を確実に実行させるための指揮官が必要です。そこで、オーケストレーターを参入させましょう。
神経系:ビストロ・クルーのオーケストレーション
いよいよ神経系への移行の時が参りました:オーケストレーターです。
オーケストレーターを把握するには、抽象的な理論を議論するよりも、具体的なプロジェクトを作成する方がはるかに容易だと存じます。それでは、dc.samplesパッケージに移動し、「ビスト・ロクルー」を構築して、当フレームワークの検証をしてみましょう。
コンセプトは明確です。小さなビストロのスタッフチームを構築します。お客様をお迎えするグリーターと、メニューの詳細を担当するメニューの専門家が必要となります。
1. スタッフの採用(エージェントの設定)
dc.mais.operation.Agentクラスは再利用性を考慮して設計しておりますため、これらのエージェント用に新しいコードを記述する必要はなく、単に本番へ追加し設定を構成するだけで結構です。

[キャプション:クルーの設定:本番に再利用可能なエージェントの操作を組み込みます。新たなコードは不要で、設定のみで行えます。]
まず最初のAgent.Greeterを追加しましょう。基本的なパラメータにおいて、その本質を定義する必要があります:```text
名称:受付係
ロール:お客様をお迎えし、メニューに関する初期情報を提供する
目標:お客様に歓迎されていると感じていただき、適切な専門スタッフへご案内する
タスク:
- お客様を親切かつプロフェッショナルに歓迎する
- 当店の特選メニューについて簡単にご案内する
- お客様のご要望(メニュー情報、ご注文、または一般的なご質問)を把握する
- お客様が詳細なメニュー情報をご希望の場合、メニューの専門担当者に引き継ぐ 出力指示:
- お客様を親切かつプロフェッショナルに歓迎する
- 返答は簡潔で読みやすいものにする(2~3文以内)
- お客様の関心を引くため、必ず質問で終了する

*MenuExpert*については、後ほど戻ります。まず、脳が備わっていることを確認しましょう。エージェントと同様に、以前に構築したアダプターを使用して汎用的なビジネスオペレーション`dc.mais.operation.LLM`を作成しました。これを本番に加えるだけで、準備は完了です。
素晴らしい\!\!
### **2\. フローの構築(BPL)**
それでは、`Orchestrator`というビジネスプロセスを作成します。
このプロセスには、以下の内容を含むリクエストメッセージが必要となります:
* **送信元:** メッセージを送信するエージェントの名称(該当する場合)。
* **アサイジニー:** 当社が対象とする特定のエージェントです。
* **コンテンツ:** ユーザーの相互作用テキスト。
レスポンスに際しては、エージェントの返答を保持するため、単純な `Content` プロパティで十分です。
状態をクリーンに保つため、私は**コンテキスト変数**の使用を好みます。したがって、BPLで最初に行うことは、入力されたリクエストのプロパティをコンテキストに割り当てることです。
**「コールドスタート」ロジック:**これが最初の実行である場合、`Assignee`は空になります。誰が会話を開始するかを決定する必要があります。そのため、簡単な`If`条件を追加しました:`Assignee`が空の場合、ターゲットを`'greeter'`に設定します。
**ルーティング:**この時点では、ターゲットに基づいて**スイッチ**を使用して経路を追跡します。
* `'greeter'`の場合:`Agent.Greeter`を呼び出します。
* `'menu_expert'`の場合:`Agent.MenuExpert`を呼び出します。
**ご注意:** ここでは同期呼び出しを行っております(`Async`フラグを無効化します)。その理由は、現時点でエージェントにユーザーへの*応答*を求めておらず、エージェント操作に対してその**システムプロンプト**(パーソナリティ)の返却を要求しているためです。
この結果を`context.CurrentSystemPrompt`に保存することをお忘れなく。
**シナプス:**最後に、ルートが確定し、正しいプロンプトが得られた後、**LLM**を呼び出すことができます。
* **Request.Content:** `context.CurrentSystemPrompt` (ルール)
* **Request.UserContent:** ユーザーからの実際のテキスト。
<p><img horiginal="1794" src="/sites/default/files/inline/images/images/003.png" woriginal="1875"></p>
このシンプルなフロー(ルーター \-\> ペルソナを取得 \-\> LLMを呼び出す)により、最初のテストを実行できます。
そのプロセスに「Hello」を送信しました、そして…
*ほら\!* グリーターは設定どおり、温かくプロフェッショナルな歓迎の言葉を返しました。まさに生きているようです!\!
## **エージェントに手を与える:ツールとReActループ**
それでは、クルーについて話しましょう。前回は*グリーター*についてお話ししましたが、とても魅力的ではありますものの、実際の食事に関しては、率直に申し上げて少々役に立たない存在です。
**メニューの専門家**を起動します。
このエージェントとグリーターの主な違いは、メニューの専門家が単なるトレーニングデータ(価格を錯覚させる)に依存しない点です。リアルタイムデータへのアクセスが必要です。そのためには**ツール**が必要となります。
この例を簡素化するため、標準的なビジネス操作として`Tool.GetMenu`を作成しました。
この段階では、**MenuExpert**の設定画面の「ツール」項目において、標準的なOpenAI JSONスキーマに準拠した以下の関数定義を貼り付ける必要があります:
```json
[{
"type": "function",
"function": {
"name": "get_menu",
"description": "Get the full bistro menu",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
}]
これは脳のAPIドキュメントとして機能します。つまり、LLMに対して次のように指示しているのです:「メニューが必要な場合は、引数を必要としないget_menuという関数をご利用」
一息つく:ReActパラダイム
配線の実装に先立ち、その背後にある理論を理解しておくことが重要です。このシステム全体は、ReAct(Reason plus Actの略称)と呼ばれるフレームワークに依存しています。2022年の論文で提唱されたこの概念は、AIエージェントの構築方法を根本的に変革しました。
ReAct以前は、LLMは純粋にテキスト補完エンジンでした。ReActでは、言語的推論(思考)と行動(ツール)を交互に実行するようモデルに強制します。一言で言えば、次のような内部のモノローグに似ています:
思考:お客様はコック・オ・ヴァンの価格をお知りになりたいようです。私は存じ上げません。 アクション:get_menu() オブザーベーション:{」Coq au Vin」: 28.00} 思考:価格がわかったので、今ならお答えできます。 最終答え:コック・オ・ヴァンは28ドルです。
メモリを*「私が覚えていること」(コンテキスト履歴)と捉え、ReActを「私が問題を解決する方法」*(推論と行動のループ)とお考えましょう。ReActがなければ、エージェントは単なる受動的なオブザーバーに過ぎません。ReActがあれば、エージェントは能動的な問題解決者へと変貌するのです。
欠けている部分:神経系
これまで多くの内容を扱ってまいりました。LLM(第1部)への安全な接続を確保し、厳格なロール、目標、ツールを備えたエージェントを定義するとともに、それらの推論を駆動するReAct理論についても探求してまいりました(第2部)。
しかしながら、コードを確認した後で、問題点を発見しました:すべてがスタティックです。
MenuExpertとget_menuツールの定義はあります。しかし、これらを結びつけるものは何もありません。ツール呼び出しを捕捉し、SQLを実行し、結果を脳にフィードバックするループが存在しません。また、エージェントが「助けが必要です」と言った際のハンドオフを処理する仕組みもありません。
才能(俳優)と指示(台本)は揃っておりますが、演じる場所がありません。舞台もまだ整っておりません。
最終章となる第3部では、神経系を構築します。以下のような作業を行います:
- InterSystems BPLを使用してオーケストレーターを実装します。
- 自律的なライフサイクルを管理するためのダブルループアーキテクチャを構築します。
- ツールを実行し、「ハンドオフ」信号を動的に処理します。
- ビジュアルトレーシングを活用し、エージェントの思考をリアルタイムで観察します。
コーヒーを準備しておいてください。次の第3部で全てを命を吹き込みます!