SE の雑記

SQL Server の情報をメインに Microsoft 製品の勉強内容を日々投稿

SQL Server 2025 でベクター検索による RAG の実装の一環として Azure OpenAI の Response API を呼び出す

leave a comment

Microsoft_Learn 当ブログでは、SQL Server 2025 の新機能であるベクターデータについて投稿をしたことがあります。

当ブログのポストデータは、SQL Server ベースの環境に保存していますので、Embedding の生成や Vector Search をブログのポストデータを使用して検証することができるような環境となっています。

このデータを使用して RAG によるナレッジ検索を行える、次のようなアプリを Vibe Coding で作成しました。

image

このアプリでは、AI のモデル呼び出しはすべて T-SQL 側で実装しており、モデルの活用が必要の場合は sp_invoke_external_rest_endpoint を呼び出しています。

RAG では LLM を使用して回答の整形を行いますが、この際、モデルの呼び出しに Completions ではなく、Respoinses API を使用してみたので、呼び出しについて情報を残しておきたいと思います。

エンドポイントが違うだけで呼び出し方法に大きな差があるわけではありませんが。

アプリケーションの開発環境

この部分は GitHub Copilot CLI による Vibe Coding で作成していますので、薄めに情報をまとめておきたいと思います。

開発環境

  • 使用したモデル: gpt-5.2-codex
  • MCP Server
    • Microsoft_Learn MCP Server
  • 実行環境
    • Dev Container
    • PHP + Laravel

このような環境で動作するアプリケーションとして開発しています。

 

実装のために生成したプラン

GitHub Copilot CLI: Plan before you build, steer as you go でアナウンスがありました。

GitHub Copilot CLI では、Plan mode をサポートするようになりましたので、今回の開発では Plan Mode でプランを作成してから実装を実施しています。

今回は、次のような Plan で実装を進めました。

# 実装プラン: SQL Server 2025 Vector + RAG チャット (PHP/Laravel, Dev Container)

## 課題と方針
- SQL Server 2025 の vector データ型を用いた RAG チャットを、Laravel で構築する。
- 埋め込み済みデータ(wordpress.post_vector.v1, 1536 dims)を用い、Exact k-NN(VECTOR_DISTANCE / cosine)で検索。
- LLM 呼び出しは SQL Server の sp_invoke_external_rest_endpoint 経由で Azure OpenAI を利用(API キー)。
- UI はシンプルな Blade + API、認証なし、会話履歴は保存しない。

## 事前確認(前提)
- 外部 SQL Server 2025 に接続するための環境変数を .env と devcontainer.json で用意する。
- Azure OpenAI のエンドポイント/デプロイ名/API キーが準備済み。
- SQL Server 側で external rest endpoint が有効化され、必要な権限(EXECUTE ANY EXTERNAL ENDPOINT, etc.)が付与済み。

## 作業計画
- [x] 0. Git リポジトリ初期化
- [x] 1. リポジトリ初期状態の整理(ディレクトリ構成確認、README 方針確認)
- [x] 2. Dev Container 設定を作成/更新
  - PHP/Laravel 用イメージ
  - 拡張機能(PHP, SQL tools)
  - .env 連携(SQL/AOAI 接続情報)
  - 追加: Dev Container 起動時に PHP サーバー自動起動
- [x] 3. Laravel プロジェクト初期化(composer create-project)
- [x] 4. 設定: DB 接続(sqlsrv / PDO)
  - SQL Server 2025 へ接続できるドライバ設定
  - 接続テスト用のヘルスチェック
- [x] 5. DB 側の RAG クエリ設計
  - wordpress.post_vector (ID, v1 vector(1536)) を前提
  - wordpress.wp_posts の post_content と join
  - VECTOR_DISTANCE('cosine', @qv, v1) の検索 SQL
- [ ] 6. SQL Server に LLM 呼び出し用のストアド作成
  - ストアド作成スクリプト: database/sql/rag_completion.sql (DBで実行)
  - sp_invoke_external_rest_endpoint で Azure OpenAI chat/completions を呼び出し
  - 入出力 JSON の設計(質問 + 参照情報を prompt に組み込み)
- [x] 7. Laravel 側の API 実装
  - /api/chat: routes/api.php + app/Http/Controllers/ChatController.php
  - /api/chat: 質問受け取り → DB へ渡す → 回答+引用を返す
  - SQL Server ストアド実行の repository / service 層
- [x] 8. Blade UI 実装
  - /chat: resources/views/chat.blade.php + routes/web.php
  - チャットフォーム、回答表示、引用リスト
- [x] 9. エラーハンドリング/ログ/
  - /api/chat: 失敗時の詳細エラーをログ/JSONで返却
  - SQL 例外、REST エンドポイント失敗時の可視化
- [ ] 10. セットアップ手順と環境変数のドキュメント化
  - .env / devcontainer.json / SQL 権限設定
- [ ] 11. 動作確認
  - ローカル Dev Container 起動
  - 質問 → 検索 → 回答 + 引用の end-to-end

## 参考ドキュメント
- Vector data type / VECTOR_DISTANCE: https://learn.microsoft.com/en-us/sql/sql-server/ai/vectors?view=sql-server-ver17
- Vector data type 概要: https://learn.microsoft.com/en-us/sql/t-sql/data-types/vector-data-type?view=sql-server-ver17
- sp_invoke_external_rest_endpoint: https://learn.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sp-invoke-external-rest-endpoint-transact-sql?view=sql-server-ver17
- External REST endpoint 有効化: https://learn.microsoft.com/en-us/sql/database-engine/configure-windows/server-configuration-options-sql-server?view=sql-server-ver17#configuration-options

## 注意点
- VECTOR_DISTANCE は exact 検索で、スケール次第では性能に注意。
- API キーは SQL Server の DATABASE SCOPED CREDENTIAL に保存。
- RAG のプロンプトは T-SQL で組み立て、出力 JSON を SQL でパースして返却。

 

Vibe Coding のみで実装できていなかったのは、実行する SQL の生成部分となります。

実装時にスキーマ等の情報を渡すことで実装は進められていたのですが、生成されたクエリの構文に一部誤りがあったので、この部分は手動で修正を行いながら実装をしています。

 

Plan Mode で作成するアプリケーションの全体像と作業ステップが分かりやすく見えるようになりましたので、全体的な実装についてはスムーズに進めることができたと思います。

このアプリを作成するための、モデルの使用状況等は次のようになりました。

image

image

 

T-SQL で RAG による検索を実施する

ここからが本題となります。

SQL Server ベースの環境で RAG による検索を使用する場合、Azure OpenAI は次のように利用されるのではないでしょうか。

  1. 検索対象のベースデータに対して、Embedding の付与 (AI_GENERATE_EMBEDDINGS)
  2. プロンプトに入力した内容の Embedding の作成 (AI_GENERATE_EMBEDDINGS)
  3. ベクター検索 (VECTOR_SEARCH) を使用して、プロンプトに入力した内容と Embedding を生成済みのチャンク済みポストデータでベクトル検索を実施
    • VECTOR_DISTANCE を使用することもできますが、今回は「1.」のデータに対して VECTOR INDEX を作成済みですので、VECTOR_SEARCH を利用しています。
  4. ベクトル検索したデータを LLM で整形 (sp_invoke_external_rest_endpoint)

 

既に過去の投稿の検証で「1.」は実施済みですので、今回作成したアプリケーションでは「2.~4.」を Vibe Coding で実装しています。

 

プロンプトに入力した内容でベクター検索を実行

上述の「2.」「3.」の箇所となりますが、次のようなクエリにより実現しています。

SET NOCOUNT ON;
DECLARE @question NVARCHAR(4000) = :question;
DECLARE @top INT = :top;
DECLARE @vector VECTOR(1536) = AI_GENERATE_EMBEDDINGS(@question USE MODEL [text-embedding-ada-002]);
DROP TABLE IF EXISTS #tmp;
SELECT pv.ID,
    pv.post_title,
    pv.chunk,
    vs.distance 
INTO #tmp
FROM VECTOR_SEARCH(
        TABLE = wordpress.post_vector AS pv,
        COLUMN = v1,
        SIMILAR_TO = @vector,
        METRIC = 'cosine',
        TOP_N = @top
    ) AS vs
ORDER BY distance ASC;
SELECT t.ID,
    t.post_title,
    t.chunk,
    wp.guid,
    CAST(wp.post_content AS NVARCHAR(MAX)) AS post_content,
    t.distance
FROM #tmp t
    JOIN wordpress.wp_posts AS wp ON wp.ID = t.ID;

 

プロンプトとして入力された内容で Embedding を生成し、その内容を使用して VECTOR_SEARCH を実行しています。

一時テーブルに格納して検索している理由は、VECTOR_SEARCH と直接 JOIN をした場合に次のエラーが発生していたためです。

メッセージ 15905、レベル 16、状態 10、行 6

クエリはサポートされていません: 結果列のソースを特定できません。無効なメタデータです。

TOP により取得件数が制限された情報をベースにして検索を実施していますので、一時テーブルに入れて検索することによるオーバーヘッドはある程度は抑えられているかと思います。

このようなクエリにより、関連するポストの情報が取得されます。

image

 

LLM による取得した情報の整形

上記のクエリで関連するポストデータの内容が取得できましたが、この内容をそのまま返すだけでは、期待された回答にはなりません。

今回のアプリケーションであれば、次のような対応を考慮する必要があります。

  • 取得したポストデータのどの個所が質問に対しての回答になるのか?
  • 質問に対して適切出ない内容が含まれていないか?
  • RAG により取得された外部データの結果のみで回答を生成する。

 

これらの判断を行うため、取得した情報 (ポスト内容) に対して LLM による整形を行います。この個所に Azure OpenAI の Response API を使用しています。

この部分については、調整する箇所が多かったのでストアドとして実装を行っています。

CREATE OR ALTER  PROCEDURE [dbo].[usp_aoai_response]
    @prompt NVARCHAR(MAX),
    @temperature FLOAT = 0.2,
    @model NVARCHAR(100) = N'gpt-5.1',
    @url NVARCHAR(max) = N'https://<Resource Name>.openai.azure.com/openai/responses?api-version=2025-04-01-preview',
    @credential NVARCHAR(100) = N'https://<Resource Name>.openai.azure.com'
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @payload NVARCHAR(MAX) =(
        SELECT
            @model AS model,
            @temperature AS temperature,
            payload AS input
        FROM
        (
            SELECT type,role, content
            FROM
            (
                SELECT
                    'message' AS type,
                    'system' AS role,
                    'あなたはテクニカルな内容をサポートするアシスタントです。' AS content
                UNION ALL
                SELECT 
                    'message' AS type,
	                'user' AS role,
                    STRING_ESCAPE(@prompt,'json') AS content
               
            ) AS T FOR JSON PATH
         ) AS T2(payload) FOR JSON PATH, WITHOUT_ARRAY_WRAPPER
    )

    PRINT @payload;
    DECLARE @response NVARCHAR(MAX);
    DECLARE @retval INT;

    EXEC @retval = sp_invoke_external_rest_endpoint
        @url = @url,
        @method = 'POST',
        @payload = @payload,
        @credential = @credential,
        @timeout = 60,
        @headers = N'{"Content-Type":"application/json"}',
        @response = @response OUTPUT;

    SELECT
        @retval AS http_status,
        JSON_VALUE(@response, '$.result.choices[0].text') AS completion_text,
        @response AS response_json;
END

 

API キーを使用した呼び出しについては、DATABASE SCOPED CREDENTIAL を渡すことで、実施していますが、資格情報名には、資格情報名の規則 に記載されている命名規則があり、Azure OpenAI のエンドポイントの URL のパスを含める必要があります。

このクエリを実行することで、ベクター検索で取得された内容をもとに必要な情報を成型することができます。

ただし、単純な実行では上述した回答の妥当性の判断ができないため、実際に使用されるプロンプトは、ポストデータの内容のほかに、次のような内容を含んできます。

$ragPrompt = "あなたは参考情報に含まれる内容だけを使って、日本語で詳細に解説してに回答してください。\n" .
"参考情報にない内容は推測で補完しないでください。\n" .
"参考情報に質問への該当がない場合は、該当する情報が存在しない旨と理由を1文で回答してください。\n\n" .
"[参考情報]\n" . $context . "\n\n" .
"[質問]\n" . $question;

 

これで、RAG で取得した情報が、質問に対して適切な内容を含んでいるかの確認と、RAG の情報のみで生成された回答となるように調整をしています。

LLM で生成された、一般的な内容も参考になりますので、一般的な内容で生成された情報も同様のクエリで取得をしています。

$generalPrompt = "以下の質問に、一般的な知識で日本語で簡潔に回答してください。\n" .
"参考情報は使わないでください。\n\n" .
"[質問]\n" . $question;

 

これで、アプリケーションの次の内容の生成を実現することができました。

image

 

 

ブラッシュアップしないといけない箇所はまだあるのですが、SQL Server の T-SQL を使用したベクター検索として、一般的に必要な実装が行われているアプリになっているのではないでしょうか。

Share

Written by Masayuki.Ozawa

2月 4th, 2026 at 9:45 am

Posted in 未分類

Leave a Reply