わずか2、3年の間に、人工知能は新たなトレンドから現代のソフトウェアに不可欠なコンポーネントへと変貌を遂げた。ChatGPT、Grok、Gemini、その他のAIアシスタントは、今や仕事でもプライベートでも、全ての人の日常生活で重要な役割を果たしています。
4D 21が4D.Vectorsと 4D AI Kitを紹介するのはそのためです:4D開発者がアプリケーションにAIを搭載した機能を追加するためのシンプルで効果的なツールを提供するためです。
私たちは、すでにAIに関する多くの例、チュートリアル、ウェビナーを共有してきました。しかし、最近私は考えました:30年前の4DアプリケーションにAIを導入するには何が必要だろうか?
そのようなアプリケーションに上位10人の顧客を尋ねれば、即座に素敵なグラフが返ってくるだろうか?
さて、どうだろう?それは、ブログの記事に値するほど簡単なことだった。
Githubのプロジェクト・ソースのリンクと簡単なデモ動画から始めよう。
4D Invoiceアプリケーション
4D Invoiceアプリケーションは、4D Depotでかなり前から利用可能だ。これは、商品、顧客、インボイスをすっきりと構造的に管理する方法の確かな例です。このアプリケーションはすでにプロジェクトモード(最新の4D AI機能を利用するための必要条件)を使っていますが、まだDataClassesを利用しておらず、フォームも最近のFormクラスより前のものです。コードベースはかなり大きく、一般的なデザインパターンを使用しており、いくつかの非自明なビジネスルールを含んでいる。
今回の実験では、AIによるアプリケーションとのインタラクションを可能にする新しいフォームを追加することが目標だった。私は、ユーザーがAIアシスタントと会話し、簡単な質問からより複雑な分析クエリまで何でも質問できるようにしたかった。実際の使用において、これはかなり “古い “アプリケーションを即座に現代的な雰囲気にし、今日のAI主導のユーザーの期待に沿わせるだろう。実にエキサイティングだ!
もうひとつの目的は、データ構造の変更を避け、既存のUIメカニズムを変更しないことだった。言い換えれば、AIのアップグレードは、非常に柔軟で、侵入を最小限に抑え、理想的には他のレガシー・アプリケーションで再利用可能でなければなりませんでした。
結果として、このアップグレードは4D.Vectorsやエンベッディングには依存していません。4DアプリにAIを導入することは、必ずしもエンベッディングやセマンティック検索を必要としません。これらは別の概念です。
準備
4Dでは、ツール・コールを使ったRAGシステムの構築に関する最近のデモを含め、AIベースの例やウェビナーをすでにいくつか共有している。今回のプロジェクトでは、People & SkillsデモからチャットUIフォームを抽出し、より汎用的で再利用可能なものにすることにした。
チャットUIは以下のコンポーネントで構成されています:
- AIChatフォームとそのフォームクラスformAIChat.4dm。このフォームは以下を提供します:
- ユーザーのプロンプトのための簡単なテキスト入力
- モデルとの会話を表示するウェブ領域
- AI_ChatWithToolsクラス:
- Resources/AIProvider.jsonで設定された推論サーバーとモデルに基づいて、4D AI Kitのチャットヘルパーを使用してAIボットをインスタンス化します。
- Resources/AITools.jsonで定義されたツールのロード
- これらのツールの実装をホストする
このクラスは完全にストリーミングモードで実行されるため、ユーザーはChatGPTのような体験を得ることができ、応答が徐々に表示されます。
- 会話のレンダリングは ChatHTMLRendererシングルトンによって処理されます。
これは AIBot に保存されたメッセージのコレクションを受け取り、Resources フォルダにあるテンプレートを使って HTML にします:
-
- chat-template.html
- chat-template.css
- ツールアイコン.svg
正直に言うと、HTMLレンダリングは、VS CodeのGitHubエージェントとClaude Sonnet 4.5の助けを借りて、基本的に “vibe-coded “でした。最も難しかったのは、ストリーミング中に適切なレンダリングを確保することで、特にテーブル、ツール呼び出し、チャートなどの要素に苦労しました。
AIツールの呼び出し
ご存知のように、AIモデルは本来、データについて何も知りません。あなたが明示的に提供するツールを通じてのみ、データにアクセスすることができます。このセクションでは、どのツールを実装し、どのように機能するかを説明する。
tool_getProducts、tool_getClients、tool_getInvoices、tool_getInvoiceLines
これらのツールはすべてデータベースへのクエリを担当します。それぞれがモデルによって要求されたフィールドに基づいてシンプルなORDAクエリを実行します。
もちろん、これらのツール機能には改善の余地があります。4D21AIのネイティブ機能のおかげで、4D.Vectorとエンベッディングを使用したセマンティック検索を想像することができる。しかし、これでは構造をより深く変更することになるので、私はよりシンプルな方法を選んだ。
他のすべての “getter “ツールに同じパターンが当てはまるので、tool_getClientsを分解してみよう:
Function tool_getClients($input : Object) : Object
この関数は、入力としてオブジェクトを受け取り、出力としてオブジェクトを返す。入力には検索パラメーターが記述され、出力には結果のデータが含まれる。
入力の検証
var $validation; $returnObject : Object
var $entities : cs.CLIENTSSelection:=ds.CLIENTS.all()
$validation:=JSON Validate($input; This._getToolArgumentsSchema(This._functionName(Call chain)))
If (Not($validation.success))
return {error: "Could not validate input parameters against JSON Schema, call the tool again with proper input parameters"}
End if
最初のステップは、入力がAITools.jsonで定義されたスキーマにマッチするかどうかを検証することです。
ほとんどの最新のモデルは、よく構造化されたツール・コールを生成するが、モデルによっては、バリデーションはまだ良い安全策である。
バリデーションが失敗した場合、ツールはエラー・プロパティを含む単純なオブジェクトを返す。モデルがこの値を読み取ることを知ることは重要なので、何が間違っていたのかを自由に明示しましょう。
デフォルト入力値の設定
$input.ID:=($input.ID) || "any"
$input.Name:=($input.Name) || "@"
$input.Contact:=($input.Contact) || "@"
$input.Total_Sales:=$input.Total_Sales || {}
$input.Total_Sales.min:=$input.Total_Sales.min || 0
$input.Total_Sales.max:=$input.Total_Sales.max || 9999999
$input.orderBy:=($input.orderBy) || {}
$input.orderBy.field:=($input.orderBy.field) || "Name"
$input.orderBy.order:=($input.orderBy.order) || "asc"
$input.top:=($input.top) || This.defaultTop
$input.countOnly:=($input.countOnly) || False
デフォルトは安全なORDAクエリを保証しますが、興味深い副次的な利点もあります。AITools.jsonに完全なJSONスキーマを書くのは面倒です。その代わりに、GitHub Copilotにコードとデフォルトの構造からスキーマを推測させた。Copilotは、上記のロジックだけに基づいて正確なスキーマ定義を作成しました。
注意すべきパラメータ
- orderByと topは出力サイズ(したがってトークンの使用量)を減らします。
- countOnlyは、データを取得せずにレコード数のみを要求するモデルを可能にします。
実際の例:
- 顧客数が必要な場合 → countOnly = true
- 上位5人の顧客が必要な場合 → top = 5 and orderBy:{field:”Total_Sales”, order:”desc”}。
出力オブジェクトの初期化
$returnObject:={}
$returnObject.form:="Clients"
$returnObject.dataClass:="CLIENTS"
$returnObject.counts:={}
$returnObject.counts.total:=$entities.length
- formは、結果を表示するためにどのUIフォームを使用できるかを示します。
- dataClassは、複数のテーブルにまたがってクエリを実行する際に重要なデータクラスを指定します。
- counts.totalは、フィルタが適用される前のレコード数です。
クエリの実行
If ($input.ID#"any")
$entities:=$entities.query("ID = :1"; $input.ID)
End if
$entities:=$entities.query("Name = :1 and Contact = :2 and Total_Sales >= :3 and Total_Sales <= :4 order by "+$input.orderBy.field+" "+$input.orderBy.order; $input.Name; $input.Contact; $input.Total_Sales.min; $input.Total_Sales.max)
この部分は簡単で、もっと汎用的なものにすることもできますが、ここではその点に重点を置いていません。
結果の確定
$returnObject.counts.totalFiltered:=$entities.length
If ($returnObject.counts.totalFiltered>$input.top)
$entities:=$entities.slice(0; $input.top)
End if
$returnObject.counts.totalSent:=$entities.length
$returnObject.entities:=($input.countOnly) ? [] : $entities.toCollection("ID, Name, Contact, City, State, Country, Discount_Rate, Total_Sales, Comments")
return $returnObject
内訳
- counts.totalFiltered: フィルタリング後のレコード数
- counts.totalSent:実際に返された数(トップ適用後)
- entities: 最終的なデータコレクション(countOnly = trueの場合は空)。
このツールは、どの属性がモデルに送信されるかを正確に制御していることにお気づきでしょう。適切なバランスをとることが重要です。
考慮すべき点
- 会話の質: より多くのデータ → より良い洞察
- パフォーマンス:データが多いほど、処理に時間がかかる
- コンテキスト・ウィンドウ:特にローカル・モデルは過負荷になると劣化する可能性がある。
- コスト:トークンが増える → クラウド推論サーバーの使用量が増える
- 機密性:フィールドを選択する際は、常にGDPRとデータの機密性を考慮すること
結論
ツールがどのようなオブジェクトを返そうとも、4D AI Kitはそれを透過的にシリアライズし、モデルに提供します。かなり便利でしょう?
テーブル間のクエリーと初期のシステム・プロンプト
このプロジェクトでのもう一つの興味深い発見は、モデルがテーブル間の関係を自動的に理解しないことでした。例えば、次のようなプロンプトがあります:
上位5人の顧客、その最高請求額、その顧客が最も多く注文した商品を教えてください。
のようなプロンプトは、必ずしも信頼できる結果をもたらさない。
問題は単純で、ClientsテーブルのIDが Invoicesテーブルへのクエリに使用されることや、請求書の行が製品に関連することなどをモデルが理解していなかったのです。
言い換えれば、モデルに構造に関する知識を与える必要があったのです。私の要求は以下の通りです:
- ハードコードされたシステム・プロンプトはいらない: 構造が変わるたびにプロンプトを手動で更新したくない。
- 完全な構造カタログがないこと: モデルはスキーマ全体を必要としない。
これを実現するために、Thomas Maulの非常に便利なクラスを再利用した:StructureInfo.4dmを再利用し、テーブル間の関係だけを抽出しました。
ここから、最初のシステム・プロンプトを生成した:
var $relations : Collection:=This._relationsInfos()
$systemPrompt:="You are a helpful assistant. I need your help to answer questions data stored in my application.\n"+\
"**CONTEXT**\n"+\
"The application stores data about invoices, products and clients"+\
"In some cases, you'll need to cross query several tables (dataClasses) in order to answer."+\
"To help you, here are the application relations between tables (dataClasses):\n"+\
JSON Stringify($relations)+"\n"+\
"**INSTRUCTIONS**\n"+\
"Analyze questions and answer step by step.\n"+\
"Use the tools at your disposal to answer everytime you think they are relevant.\n"+\
"**FORMATING**\n"+\
"Use HTML everytime.\n"+\
"Use bullet lists and Tables everytime everytime necessary\n"+\
"**IMPORTANT**\n"+\
"When calling tools, always include all required arguments in valid JSON.\n"+\
"Do not call a tool with empty arguments. If a value is missing, choose a reasonable default.\n"+\
"Always double check tools results before answering. Especially when they rely on vector search. \n"+\
"Indeed they may return results not matching with your search intention.\n"+\
"When tool calling returns data not related with the initial question, or that you cannot use to answer,\n"+\
"avoid detailing such results too much and stay short.\n"
このリレーション・メタデータを各ツールが返すdataClass情報と組み合わせることで、最終的にモデルは、テーブル間でツール・コールを連鎖させる方法を理解できるようになった。
これにより、特に以下のような複数テーブルの分析において、モデルの回答の質と精度が劇的に向上しました:
売上高上位顧客
または
最も頻繁に注文される商品
または
顧客ごとの最大の請求書
ユーザー体験:会話型UIから直接4Dフォームを開く
以前のRAGの実験では、結果を別のリストボックスに表示することが多かった。このアプローチはうまくいきましたが、いくつかの欠点がありました:
- リストボックスに表示するエンティティIDのリストを、モデルが別の隠しセクションで提供する必要がありました。これは、トークンが増え、レスポンスが遅くなり、余分なHTMLトリック(コメント付きブロックの中にIDリストを隠すなど)が必要になることを意味します。
- また、異なるエンティティタイプ(顧客、商品、請求書、…)を扱えるようにリストボックスを汎用的にするための追加作業が必要でした。
- UIが制限され、アプリケーションに統合されていないと感じました。
このプロジェクトでは、よりシンプルでエレガント、そしてユーザーにとってより自然なものを求めました。
AIがエンティティに言及するたびに、対応する4Dフォームを直接開くカスタムハイパーリンクを生成し、選択されたエンティティにフィルターをかけるのです。
これは驚くほど簡単だった。システム・プロンプトに以下の指示を追加するだけだ:
"**CUSTOM URL HANDLING**\n"+\
"Tools responses give information about the form to open when available, the dataClass and entities ID\n"+\
"When you display any element coming from a tool response, you must use a custom url so that the user can open the corresponding form\n"+\
"Such custom url must follow the following syntax examples:\n"+\
"<a href=\"myapp://openform?form=Products&dataClass=PRODUCTS&entities=989511\">A single product</a>\n"+\
"<a href=\"myapp://openform?form=Invoices&dataClass=INVOICESS&entities=654KJY,6467HGS,79864JSD\">A list of invoices</a>\n"
私のツールは常に適切なフォーム、dataClass、エンティティのリストを返すので、モデルはこれらのリンクを正しく生成するために必要なものをすべて持っています。AIモデルは、このような構造化された出力が得意です。
4DでカスタムURLを扱う
私が4D側で必要だったのは、myapp://リンクをインターセプトし、適切なエンティティ選択で適切なフォームを開くための、ウェブ領域での小さなURLフィルタリング機構だけでした:
Function webAreaEventHandler($formEventCode : Integer)
var $formToOpen : Text
var $entitySelectionToShow : 4D.EntitySelection
var $queryObject : Object
Case of
: ($formEventCode=On Load)
ARRAY TEXT($filters; 0)
ARRAY BOOLEAN($allowDeny; 0)
APPEND TO ARRAY($filters; "myapp://*") // Intercept all URLs starting with myapp://
APPEND TO ARRAY($AllowDeny; False) //Allow
WA SET URL FILTERS(*; "Web Area"; $filters; $allowDeny)
: ($formEventCode=On URL Filtering)
$url:=WA Get last filtered URL(*; "Web Area")
// Parse the URL to determine what to do
Case of
: ($url="myapp://openform?@")
$queryObject:=This.queryObjectFromUrl($url)
If ($queryObject=Null)
return
End if
If ($queryObject.entitiesCollection.length>0)
$entitySelectionToShow:=ds[$queryObject.dataClass].query("ID in :1"; $queryObject.entitiesCollection)
CALL WORKER("Generic"; "W_Generic"; $queryObject.form; True; $entitySelectionToShow)
Else
CALL WORKER("Generic"; "W_Generic"; $queryObject.form; False)
End if
End case
End case
結果
最終的な結果は、会話型UIが4Dアプリケーションの他の部分とシームレスに融合した、非常にスムーズなユーザー体験です。
ユーザーは、アプリケーションのコアロジックや構造を変更することなく、AIとチャットし、データを探索し、ネイティブの4Dフォームを即座に開くことができます。
古典的な4Dアプリケーションの上に、現代的なインタラクションモデルが、ほとんど邪魔をすることなく乗っかっているのです。
追加ツール: tool_createInvoice
GitHubプロジェクトで提供されたサンプル・データベースには、請求書がほんの少ししか含まれておらず、AIとのインタラクションを面白くするには十分ではなかった。もっとデータが必要だった。
カスタム・フォームを作ったり、請求書生成ルーチンをまた書いたりするのは面倒だし、このデモの目的とは無関係だと感じた。そして思いついた:必要なものはすでにすべて持っていたのだ。
- 会話型UI。
- 私のデータモデルを認識するAIエージェント。
- 顧客、商品、価格、人間関係の知識…。
では、AIにチャット・インターフェースから直接請求書を作成させたらどうだろう?
私がしなければならなかったのは、新しいツールを追加することだけだった:tool_createInvoice。
私のアプローチは簡単だった:
- AI_ChatWithToolsの中にシンプルな請求書作成関数を実装する。
- 処理全体をトランザクションで包む:
- エラー時:ロールバックし、明確なエラー・メッセージをモデルに送り返す。
- 成功時:請求書をコミットし、その詳細を返す。
- ツールのスキーマ、特に入力パラメータを注意深く文書化する。
- そして…GitHub CopilotにAITools.json内のJSONスキーマを生成させましょう。
結果は?
AIにまったく新しい請求書を生成するよう依頼できるようになりました。AIは関連するクライアントを見つけ、商品を選択し、合計を計算し、ツールを呼び出す。
これは、現実的なテストデータを生成するための驚くほどエレガントな方法であることが判明した。
1つ興味深い観察があります:いくつかのモデル(特にOpenAI GPT-4.1)は、実際に請求書を作成する前に、確認を求めるか、少なくとも十分な入力の詳細を求める傾向があります。
OpenAIメッセージのHTMLレンダリング
前述したように、この部分はすべてバイブでコーディングしました。再利用するもよし、適応するもよし、無視するもよし。アイディアは単純です:OpenAIメッセージのコレクションをHTMLできれいにレンダリングするのです。
しかし、もちろん、私たちのエンドユーザーは常に、基本的なテキストのダンプ以上のものを求めています。そこで、いくつかのQOL機能を追加しました:
- ツール呼び出しのための、きれいで構造化されたレイアウト。
- ワンクリックの “コピー “ボタンで、アシスタントのメッセージを簡単にコピーし、Microsoft Word、Google Docs、Eメールなどのドキュメントに貼り付けることができる。
これにより、会話体験全体が洗練されたプロフェッショナルな雰囲気になると同時に、迅速かつ軽量に実装することができます。
- 各表にはコピーボタンがあり、ユーザーは生の表データを即座に抽出し、エクセルのようなスプレッドシートに直接貼り付けることができる。
これは、ユーザーが何もエクスポートせずにアプリケーションの外でデータを操作したり比較したい場合に非常に便利です。
会話型UI内で直接チャート作成機能
請求書、顧客、製品についてAIと会話するとき、ユーザーは当然グラフを期待します。ランキング、比較、トレンド…視覚的な洞察は、今やあらゆるモダンな体験の一部です。
そこで、チャットのUIにインラインチャートレンダリングを導入することにしたのですが、GitHub Copilotを使ったちょっとしたバイブコーディングのおかげで、驚くほど簡単にできることがわかりました。
3つのステップで実装した:
1)Chart.jsを chat-template.htmlに インポートし、<chart>…</chart>ブロックを認識するようにHTMLレンダリング・エンジンを拡張する。
AIがその応答をストリーミングしている間、小さなCSSアニメーションがチャートのプレースホルダーを表示し、洗練されたUX感を与えます。
2)システム・プロンプトを拡張して、AIにチャートを要求する方法を明示的に教える。などの指示を追加した:
"**CHARTS**\n"+\
"Create charts for rankings, comparisons, trends, or distributions. Format: <chart>{...JSON...}</chart>\n"+\
"Available types: bar, line, pie, doughnut, radar, polarArea. Always include:\n"+\
"- \"type\": chart type\n"+\
"- \"data.labels\": array of x-axis labels\n"+\
"- \"data.datasets\": array with \"label\", \"data\" (numeric array), \"backgroundColor\" (color array)\n"+\
"- \"options.responsive\": true\n"+\
"- \"options.plugins.title\": {\"display\": true, \"text\": \"Chart Title\"}\n"+\
"- \"options.scales.y.beginAtZero\": true (for bar/line charts)\n"+\
"Use distinct vibrant colors (e.g., #4caf50, #2196f3, #ff9800, #e91e63, #9c27b0). Set \"legend.display\" to false for single datasets, true for multiple.\n"
3) AIにJSONでチャートの定義を生成させ、ウェブ・エリアがChart.jsを使って即座にレンダリングする。
このモデルは、会話の流れの中で直接意味のあるビジュアルを生成し、体験を劇的に豊かにする。
とはいえ、複雑な計算をこのモデルに完全に依存することはないだろう。本番での使用には、次のような専用のツールを追加するのが賢明だろう:
- 合計と累積
- グループ化と集計
- 時間ベースのサマリー
- 期間比較
- KPI抽出
4Dが難しく正確な計算を行い、AIがそれを要求し、正確で検証された数値データに基づいてグラフを描くだけです。
4Dの信頼性とAIによるUIの完璧な組み合わせだ。
結論
さて…どうする?
私たちは本質的に、やや古風なビジネス・アプリケーションを、モダンで会話的な、AIを搭載したエクスペリエンスに吹き込んだ。
しかし、これを単に「データを取得する別の方法」と見るのは、まったく的外れだろう。
確かに、「2025年の総売上高上位5社の顧客」を得るのに、会話型のUIは必要ない。
いくつかのフィルターを備えた基本的な画面でもそれは可能だ。
しかし、すぐにこれは別のものだと気づくだろう。
これはデータを探索する新しい方法なのだ。
チャット・インターフェースという一つのフォームで、ユーザーは請求書、製品、顧客…表が欲しいのか、グラフが欲しいのか、あるいはその両方が欲しいのか、何でも尋ねることができます。
これは、わずかに異なるレポートニーズのために無数の特別な画面を構築する必要性を排除します。
そして、このことが分かれば、データの照会や表示をはるかに超えたものであることも理解できるだろう。
本当の価値は、「これを見せてください」という答えにあるのではない。
本当の価値は、「これを理解する手助けをしてください」と答えることにある。
あなたのプロンプトはもはや
2024年と2025年を比較したグラフを描いてください
ではなく
総売上高、売上高上位 5 製品、売上高上位 5 取引先、1 年間の売上動向、未払い請求書など、2025 年の詳細な売上報告書を提出してください。関連する図表を示し、注目すべきトレンド、機会、リスクについて説明してください。また、製品の業績を2024年と比較し、売上とキャッシュフローを改善するための実行可能な洞察を提案する。顧客ごと、製品ごとの業績レビューを主要指標とともに記載すること。数値と説明、実行可能な洞察の両方が欲しい。レポートには2024年と2025年の比較が含まれます。このようなレポートの目標は、売上を上げる方法、どの顧客をターゲットにするか、製品のパフォーマンスを上げる方法です。
そして、このような答えが、あなたとあなたのユーザーから返ってくるのです:
あなたの4Dアプリケーションにシームレスに統合され、洞察、推論、視覚化、創造…が可能な単一の会話型インターフェース。
これは機能ではありません。
新世代のビジネスアプリへの一歩なのです。
これを自分のプロジェクトで再利用するには?
この投稿を読んでアプリに興味を持ったなら、単純に次のことができる:
- 以下のファイルをプロジェクトにコピーしてください:
- クラス
- AI_ChatWithTools.4dm
- ChatHTMLRenderer.4dm
- フォームAIChat.4dm
- 構造情報.4dm
- フォーム
- AIChat
- リソースアセット
- AIprovider.json
- AITools.json
- チャットテンプレート.html
- チャットテンプレート.css
- ツール・アイコン.svg
- 必要な適応を行う
- AI_ChatWithTools.4dmのシステムプロンプトとツールの実装
- AITools.jsonでの後続ツールの定義(これにはAIを使ってください)
- formAIChat.webAreaEventHandler()で、フォームのオープンをアプリケーションに適応させる。
- AIprovider.jsonでAIプロバイダーの設定
- クラス
以上です!
最後に、いくつか補足しておきます:
この例では、すべてのツール・ロジックはAI_ChatWithToolsクラス内に実装されています。この例では、すべてのツール・ロジックはAI_ChatWithToolsクラス内に実装されています。そうしたのは、私の目標ができるだけ邪魔にならないようにすることであり、プロジェクトがDataClassesを実装していないからです。あなたのコード・ベース・アーキテクチャにもよりますが、このようなツールはDataClassesの関数をターゲットにした方がよいでしょう。
このプロジェクトはすべて、OpenAIとgpt-4.1以上のモデルで完璧に動作します。ローカルモデルでの作業には、4Dフォーラムで議論できるような、さらなる課題が生じます!


