Delphiアプリをエージェント対応にする

· コンポーネント
Delphiアプリをエージェント対応に:TsgcAIAgent

言語モデルに質問するだけなら簡単です。エージェントはその先へ進みます。モデルがあなたのどの関数を呼び出すかを決め、アプリケーションがそれを実行し、結果をモデルに返し、モデルが答えを出すまでループが続きます。このループを書くのは始まりにすぎません。出荷するアプリケーションには、モデルに触れさせてはならないものから遠ざけ、個人データをマシンの外に出さず、プロバイダーがモデルを廃止した日にも動き続け、ユーザーごとのコストを把握することも求められます。

sgcWebSockets 2026.11は、そのためのすべての部品を、Delphi 7から13向けのsgcAIコンポーネントに追加します。

1つのコンポーネントで完結するエージェントループ

設定済みのTsgcAIChatをエージェントに渡し、ツールを記述してRunを呼び出します。エージェントはツール定義を添えてプロンプトを送信し、モデルが要求するツールをすべて実行して結果を返送し、モデルがプレーンテキストで答えるまで繰り返します。Runはその回答を返します。

oChat := TsgcAIChat.Create(nil);
oChat.Provider := aicpAnthropic;
oChat.ChatOptions.ApiKey := 'API_KEY';
oChat.ChatOptions.Model := 'claude-opus-5-5';

oAgent := TsgcAIAgent.Create(nil);
oAgent.Chat := oChat;

oTool := oAgent.Tools.Add('get_weather', 'Current weather of a city');
oTool.AddParam('city', aatpString, 'City name');
oTool.ReadOnly := True;
oTool.OnExecute := DoGetWeather;

Memo1.Lines.Add(oAgent.Run('Do I need an umbrella in Paris today?'));

ツールは、モデルが書いたJSONオブジェクトとして引数を受け取り、結果をテキスト(通常はJSON)として返します。

procedure TForm1.DoGetWeather(Sender: TObject; const aArguments: string;
  var aResult: string);
begin
  // aArguments is a JSON object, for example {"city":"Paris"}
  aResult := '{"city":"Paris","celsius":18,"sky":"rain"}';
end;

エージェントは、すべてのプロバイダーでツール呼び出しに対応したTsgcAIChat.ChatWithToolsの上で動作します。ホスト型モデルからローカルのOllamaモデルへの切り替えはProviderとModelの変更だけで、ツールはそのままです。ループを自分で実行したい場合は、ChatWithToolsとAddToolResultも公開されています。

ループを制御する

Options.MaxStepsは1回の実行でのモデルリクエスト数を制限し、Cancelはボタン、タイマー、別のスレッドから実行を停止し、StopReasonは実行が終了した理由を示します。回答がトークン上限で打ち切られると、実行は aasrMaxTokens で停止し、その回答のツール呼び出しは、引数が不完全な可能性があるため実行されません。OnStepは各ステップを発生と同時に通知するため、ユーザーはエージェントが何をしているかを確認できます。

oAgent.Options.MaxSteps := 5;
oAgent.OnStep := oAgentStep;
vAnswer := oAgent.Run('Plan a three day trip to Rome');
case oAgent.StopReason of
  aasrCompleted:
    Memo1.Lines.Add(vAnswer);
  aasrMaxSteps:
    Memo1.Lines.Add('Stopped after 5 model requests');
  aasrMaxTokens:
    Memo1.Lines.Add('Cut by the token limit (' + oChat.LastFinishReason + ')');
  aasrCancelled:
    Memo1.Lines.Add('Cancelled');
end;
procedure TForm1.oAgentStep(Sender: TObject; const aStep: TsgcAIAgentStep);
begin
  case aStep.Kind of
    aaskToolCall:
      Memo1.Lines.Add('Calling ' + aStep.ToolName + ' ' + aStep.Arguments);
    aaskToolResult, aaskToolDenied:
      Memo1.Lines.Add(aStep.ToolName + ': ' + aStep.Content);
    aaskFinal:
      Memo1.Lines.Add('Answer: ' + aStep.Content);
  end;
end;

失敗したツールは実行を終了させません。その例外はエラーテキストとしてモデルに返され、モデルは別の方法を試せます。チャット自体のエラー、たとえばネットワーク障害は、aasrErrorで実行を終了し、例外はあなたのコードに届きます。OnUsageは、実行中のすべてのモデルリクエストのトークン数を通知します。

Delphiメソッドをそのままツールにする

ツールは、上のように名前、説明、型付きパラメータ、ハンドラーで構成されます。すでに目的の処理をするメソッドがある場合は、AddMethodでそのまま公開できます。ツール名はメソッド名になり、パラメータ、その型、結果は拡張RTTIで読み取られるため、この方法にはDelphi 2010以降が必要です。

function TForm1.Forecast(const aCity: string; aDays: Integer): string;
begin
  Result := Format('%s: sunny for %d days', [aCity, aDays]);
end;
oAgent.Tools.AddMethod(Self, 'Forecast',
  'Weather forecast of a city for the next days');

入力がより複雑なツールには、InputSchemaに独自のJSONスキーマを設定します。Enabledを使うと、ツールを削除せずに以降の実行から外せます。

MCPサーバーのツール

1つ以上のMCPクライアントをMCPClientsに追加すると、エージェントはそれらのサーバーのツールを自分のツールと並べてモデルに提示します。ツールは実行ごとに1回だけ一覧取得され、モデルが要求したときにクライアント経由で呼び出されます。サーバーがreadOnlyHintを付けたツールは読み取り専用として扱われ、ローカルツールとサーバーのツールが同名の場合はローカルが優先されます。

oMCP := TsgcWSAPIClient_MCP.Create(nil);
oMCP.MCPOptions.HttpOptions.URL := 'http://127.0.0.1:8080/mcp';
oAgent.MCPClients.Add(oMCP);

Memo1.Lines.Add(oAgent.Run('List the support tickets opened today'));

変更が起きる前の承認

すべてのツールにはReadOnlyフラグがあります。読み取り専用のツールは、モデルが要求した時点で実行されます。データを変更する、メールを送る、お金を使うといったツールは読み取り専用にせず、エージェントは実行前に、ツール名とモデルが選んだ正確な引数を添えてOnToolApprovalを呼び出します。

oTool := oAgent.Tools.Add('send_reminder', 'Sends a payment reminder');
oTool.AddParam('invoice', aatpString, 'Invoice number');
oTool.OnExecute := DoSendReminder; // ReadOnly is False: it changes data

oAgent.OnToolApproval := oAgentToolApproval;
procedure TForm1.oAgentToolApproval(Sender: TObject; const aToolName,
  aArguments: string; var aApproved: Boolean);
begin
  aApproved := MessageDlg('Run ' + aToolName + ' with ' + aArguments + '?',
    mtConfirmation, [mbYes, mbNo], 0) = mrYes;
end;

拒否されたツールはエラーではありません。モデルにはそのツールが承認されなかったことが伝えられ、モデルはそのツールなしでユーザーに回答できます。次のセクションのようにガードレールを割り当てている場合、承認が必要なのにハンドラーがないツールは拒否されるため、イベントの設定忘れで書き込みツールが通ってしまうことはありません。

ガードレール

TsgcAIGuardrailsはチャットに割り当てる独立したコンポーネントなので、同じルールがエージェントにも通常のチャットリクエストにも適用されます。役割は3つあります。

oGuard := TsgcAIGuardrails.Create(nil);
// tools: reading is allowed, deleting never
oGuard.AllowedTools.Add('get_*');
oGuard.AllowedTools.Add('list_*');
oGuard.AllowedTools.Add('send_reminder');
oGuard.DeniedTools.Add('*delete*');
oGuard.ToolApproval := aigaNotReadOnly;
// personal data never leaves the application
oGuard.PII.Enabled := True;
oGuard.PII.CustomPatterns.Add('EmployeeId=EMP-####');
// structured answers must match the schema
oGuard.Schema.Enabled := True;
oGuard.Schema.JSONSchema := '{"type":"object","properties":' +
  '{"price":{"type":"number","minimum":0}},"required":["price"]}';

oChat.Guardrails := oGuard;

マスキングを自分で呼び出して、プロバイダーが受け取る内容と、アプリケーションが受け取る内容を確認できます。

vText := oGuard.Redact('Mail bob@example.com, call +34 600 111 222, ' +
  'employee EMP-1234');
// Mail [EMAIL_1], call [PHONE_1], employee [EMPLOYEEID_1]
vText := oGuard.Restore(vText);
// Mail bob@example.com, call +34 600 111 222, employee EMP-1234

MCPサーバーのツールを固定する

MCPツールの説明と入力スキーマは、モデルが指示として読み取ります。信頼した後でサーバーがそれらを変更すると、侵害されたためでも所有者が変更したためでも、すでに使っているツールに新しい指示が紛れ込む可能性があります。これはツールポイズニングと呼ばれます。

ツールピンニングを使うと、TsgcWSAPIClient_MCPは、各ツールを初めて一覧取得したときに、その名前、説明、入力スキーマ、アノテーションのSHA-256フィンガープリントを保持します。後の一覧で異なる定義が返されると、そのツールはブロックされます。一覧から除外され、エージェントは提示せず、そのツールの呼び出しは何かが送信される前に例外を発生させます。OnMCPToolChangedで変更を受け入れるか、レビューのためにブロックしたままにでき、サーバーの他のツールは引き続き動作します。

oMCP.MCPOptions.ToolPinning.Enabled := True;
oMCP.MCPOptions.ToolPinning.FileName := 'mcp_pins.json';
oMCP.OnMCPToolChanged := oMCPMCPToolChanged;
procedure TForm1.oMCPMCPToolChanged(Sender: TObject; const aToolName,
  aOldHash, aNewHash: string; var aAccept: Boolean);
begin
  // the definition of a trusted tool changed: keep it blocked
  // until somebody reviews it, then call oMCP.PinTool(aToolName)
  Memo1.Lines.Add('Tool ' + aToolName + ' changed, blocked for review');
  aAccept := False;
end;

廃止されたモデルでも出荷済みのexeは止まらない

モデル名は、インストール済みのアプリケーションよりも速く変わります。exeにコンパイルされたモデル名は、プロバイダーがそれを廃止した日に動かなくなり、すべての顧客に新しいビルドが必要になります。モデルエイリアスを使うと、コードはsmartやfastを要求し、エイリアスがモデルと、必要ならプロバイダーを決めます。一覧はファイル、JSON、URLから読み込めるため、新しいモデルへの移行は新しいリリースではなく、サーバー側の変更で済みます。

oChat.ModelAliases.Add('fast=gpt-6-luna');
oChat.ModelAliases.Add('smart=anthropic:claude-opus-5-5');
oChat.ProviderApiKeys.Add('anthropic=ANTHROPIC_KEY');
oChat.ChatOptions.Model := 'smart';

// or keep the list on your server and change it without a new build
oChat.LoadModelAliasesFromURL('https://config.example.com/ai/models.json');

フォールバックは障害に備えます。プロバイダーが接続エラー、HTTP 5xx、HTTP 429、または未知のモデルで失敗すると、同じリクエストが一覧の次のプロバイダーとモデルに送られます。会話履歴、ツール、JSON回答は引き継がれ、OnFallbackが切り替えを通知します。ローカルのOllamaモデルは、最後の手段として適しています。

oChat.Provider := aicpOpenAI;
oChat.ChatOptions.ApiKey := 'OPENAI_KEY';
oChat.ChatOptions.Model := 'gpt-6-luna';

oFallback := oChat.Fallbacks.Add;
oFallback.Provider := aicpGemini;
oFallback.Model := 'gemini-3-flash';
oFallback.ApiKey := 'GEMINI_KEY';

oFallback := oChat.Fallbacks.Add;
oFallback.Provider := aicpOllama;
oFallback.Model := 'llama3.1';

oChat.OnFallback := oChatFallback;
procedure TForm1.oChatFallback(Sender: TObject; const aFromProvider,
  aFromModel, aToProvider, aToModel, aError: string);
begin
  Memo1.Lines.Add(aFromProvider + ' ' + aFromModel + ' failed (' + aError +
    '), trying ' + aToProvider + ' ' + aToModel);
end;

すべてのリクエストのコストを把握する

TsgcAIUsageMeterは、キャッシュされた入力トークンを含むすべてのリクエストのトークン数を記録し、与えられた価格表で料金を計算して、ユーザーごとおよびセッションごとの合計を保持します。チャットに割り当て、UserIdとSessionIdを設定します。メーターはチャットに付くため、エージェントの実行中のすべてのモデルリクエストも集計されます。

oMeter := TsgcAIUsageMeter.Create(nil);
// example prices per million tokens, use the ones of your contract
oPrice := oMeter.Prices.Add;
oPrice.Provider := 'openai';
oPrice.Model := 'gpt-6*';
oPrice.InputPerMillion := 1.25;
oPrice.OutputPerMillion := 10;
oPrice.CachedPerMillion := 0.125;

oChat.UserId := 'user-1';
oChat.SessionId := 's-42';
oChat.UsageMeter := oMeter;
oChat.Chat('Hello');

Memo1.Lines.Add(Format('user-1 spent %.6f %s and %d tokens',
  [oMeter.TotalCost('user-1'), oMeter.Currency, oMeter.TotalTokens('user-1')]));

価格表はワイルドカードでプロバイダーとモデルに一致し、JSONまたはファイルから読み込めるため、価格の変更にも新しいビルドは不要です。予算を設定すると、メーターは上限になります。予算はアプリケーション全体、ユーザーごと、セッションごとに、1日、1か月、または累計で、金額またはトークン数で設定できます。予算を使い切ると、次のリクエストは送信前に拒否されるため、予算はエージェントの実行を途中で止めることもできます。

// every user can spend 2 USD a day
oBudget := oMeter.Budgets.Add;
oBudget.Scope := aubsUser;
oBudget.Period := aubpDay;
oBudget.MaxCost := 2;
// the whole application can spend 500 USD a month
oBudget := oMeter.Budgets.Add;
oBudget.Scope := aubsGlobal;
oBudget.Period := aubpMonth;
oBudget.MaxCost := 500;
try
  Memo1.Lines.Add(oAgent.Run('Remind every customer with an unpaid invoice'));
except
  on E: EsgcAIChat do
    // Usage budget "each user, day" for user-1 exceeded: ...
    Memo1.Lines.Add(E.Message);
end;

予算を警告だけにしたい場合は、BlockWhenExceededをFalseにするとOnBudgetExceededだけが発生します。レポート用に、レコードはCSVファイルや任意のTDataSetに出力できます。OpenTelemetryを有効にすると、すべてのリクエストがGenAIセマンティック規約に沿ったスパンになり、バックグラウンドでOTLP/HTTPコレクターに送信されるため、チャットが遅くなることはありません。

oMeter.OpenTelemetry.Endpoint := 'http://localhost:4318';
oMeter.OpenTelemetry.ServiceName := 'invoice-assistant';
oMeter.OpenTelemetry.Enabled := True;

oMeter.SaveToCSV('usage.csv');

デモ:請求書アシスタント

Demos\15.AI\02.Applications\11.AIAgentの新しいDelphiデモは、未払い請求書の一覧を扱うアシスタントです。エージェントには3つのツールがあります。list_unpaid_invoicesとget_invoiceは読み取り専用で、send_reminderはデータを変更するため、先に承認を求めます。エージェントはバックグラウンドスレッドで動作するので、処理中もウィンドウは応答し続けます。デモでは、この記事の残りの機能も試せます。拒否ツールとPIIマスキング、ライブのトークンおよびコストカウンター付きのセッション単位の予算とCSVエクスポート、モデルエイリアス、フォールバックプロバイダー、ツールが固定されたMCPサーバーです。

コーディングエージェントが読めるドキュメント

コーディングエージェントが良いコードを書けるのは、知っているライブラリだけです。sgcAI、sgcHTML、sgcCryptoをはじめとするsgcWebSocketsの各パックのすべてのコンポーネントに、言語モデル向けの専用Markdownページが用意され、合計224ページになりました。各ページにはコンポーネントの役割と製品ページへのリンクがあり、多くのページで主要なプロパティ、メソッド、イベントも一覧できます。TsgcAIAgentのページai-agent.mdには、完全な使用例も含まれています。llms.txtがClaude Code、Copilot、Cursorなどのエージェントをこれらのページに案内するので、エージェントは1行も書く前に実際のAPIを読み込みます。

sgcWebSocketsのエージェントスキルも新しいコンポーネントに対応しました。2026.11.0のAIスキルには、エージェントループ、ツールの承認、ガードレール、フォールバック付きのモデルエイリアス、使用量メーターのすぐ使えるパターンが含まれ、どのパターンもコンパイルできるコードから作られています。インストール方法はDelphi、C++Builder、.NET向けAIエージェントスキルで説明しています。

提供時期

TsgcAIAgent、TsgcAIGuardrails、TsgcAIUsageMeter、TsgcAIChatのモデルエイリアスとフォールバック、MCPのツールピンニングは、Delphi 7から13とC++Builder向けのsgcWebSockets 2026.11.0で登場します。これらはsgcAIコンポーネントの一部で、EnterpriseとAll-Accessエディション、および単体製品のsgcAIに含まれています。ヘルプには、エージェントループ、ツール、エイリアスとフォールバック、PIIマスキング、スキーマ検証、ツールピンニングのページがあり、すべてのプロパティ、メソッド、イベントを説明しています。

TsgcAIAgent、TsgcAIGuardrails、TsgcAIUsageMeterのコンポーネントページで詳しく読めます。

次に読む

ご質問やご意見はありますか。お問い合わせください。コードを書いた本人から返信いたします。