これは「関連記事表示 by Vector Search 2.0」のデモサイトです。記事コンテンツはソリューションブログから引用しています。
Craft RAGを使ってコンテンツ検索やテキスト生成をする

Craft RAGを使ってコンテンツ検索やテキスト生成をする

こんにちは、Customer Engineerのgamiです!季節の変わり目には、常に喉風邪を引いています。

さて、先日リリースした記事で、Craft Cross CMSのコンテンツをCraft RAGに同期する方法について説明しました。

Craft Cross CMSで公開したコンテンツをCraft RAGに同期する

一方で、そのCraft RAG上にインポートしたコンテンツをどのように利用できるかについては、まだ具体的には紹介できていませんでした。そこで今回はFAQボットの実装を例に、実際にCraft RAGに蓄積したナレッジを活用するための方法を紹介します。

アウトプットイメージ

最終的なアウトプットイメージは次の通りです。

KARTEの接客サービス機能を使ってWebページに配信したFAQボットに対してユーザーが質問を送信します。すると、Craft RAGから関連するコンテンツが検索され、その結果を使った自然な回答文が生成されます。

なお、この回答生成にはCraft RAGにインポートされたCraft Sites上のファイルの内容が使われます。事前にCraft RAGにFAQコンテンツをインポートしておくことで、そのコンテンツで回答できる範囲の質問に対しては精度高く答えることができます。

構成図は次の通りです。

Craft RAGには、事前にCraft Sites経由でファイルをインポートしておきます。管理画面から直接アップロードもできますし、前述した記事を参考にCraft Cross CMSで入稿したコンテンツをCraft RAGに同期することもできます。

Webページに配信した接客サービス上でユーザーが質問を入力すると、その質問がCraft Functions経由でCraft RAGに送信されます。Craft RAGでは質問に関連するコンテンツを検索でき、その結果を使って自然な回答文を生成することができます。

注意点

前提として、次の点に注意をしてください。

  • Craft Functions、Craft RAG、Craft AI Modulesが利用できるKARTEプロジェクトが必要です
  • Craft RAGへのファイルインポートについては、次の記事など参考にしてすでに完了しているものとします
  • 今回作成する機能の利用を一部ユーザーに制限する必要がある場合、そのための認証機能を追加で実装する必要があります

設定手順

設定の手順は次の通りです。

  1. Craft RAGのコーパスIDをコピーする
  2. Craft Functionsを作成する
  3. FAQボット表示用の接客サービスを作成する

順番に見てみましょう。

1. Craft RAGのコーパスIDをコピーする

まず、コンテンツ検索に利用するCraft RAGのコーパスIDを確認します。

  • [Craft > RAG] から、Craft RAGのコーパス一覧画面を開きます
  • 利用したいコーパスのIDをコピーしておきます

なお、ここでは対象コーパスに対してすでにファイルがインポートされていることを前提としてます。まだの方は、適当なファイルをCraft Sites上で作成し、Craft RAGの対象コーパスに対してインポートしておいてください。

2. Craft Functionsを作成する

次に、Craft RAGを利用してコンテンツ検索や回答生成を行うCraft Functionsを作成します。

  • [Craft > ファンクション > 新規作成 > テンプレートから作成] を選択します

  • エンドポイント経由でCraft RAG上のコンテンツを利用可能にする 」というテンプレートを検索し[取得]ボタンをクリックします

  • [反映] ボタンをクリックします

  • [設定 > ファンクションのタイプ] で「HTTPタイプ」を選択します

  • [変数] タブで次の変数の値を設定してください

変数名設定例説明
LOG_LEVELWARN出力するログのレベル(ERROR/WARN/INFO/DEBUG)
ALLOWED_ORIGINS*, https://example.comCORS許可対象のオリジン(本番環境では適切に制限してください)
RAG_CORPUS_ID1234567890123456789利用するCraft RAGコーパスのID(手順1でコピーしたもの)
AI_MODELgemini-2.5-flash-lite「テキスト生成」時に利用するLLMモデル名
SYSTEM_PROMPTあなたはKARTE Craft Functions Copilotです。Craftの質問について答えてください「テキスト生成」時に使用するシステムプロンプト
  • 適当なファンクション名をつけて [デプロイ] します
  • デプロイ完了後、[設定 > エンドポイント]からエンドポイントURLをコピーしておきます

なお、テンプレートのソースコードはGitHubで公開しています。

craft-codes/craft-functions/using-rag-via-endpoint at main · plaidev/craft-codes

ちなみに、上記の変数値の中でも AI_MODELSYSTEM_PROMPT については、後述する「テキスト生成(type: “generate”)」時のみ参照され、「コンテンツ検索(type: “retrieve”)」時は無視されます。

3. FAQボット表示用の接客サービスを作成する

最後に、Craft Functionsを呼び出してFAQボット機能を提供する接客サービスを作成します。

  • 接客サービス一覧画面から接客サービスを作成し、下記のテンプレートからアクションを追加します

    • [やりたいことから探す > ユーザーに「見せる」 > 「バナー」 > Banner 01]
    • ※ フレックスエディタ対応 ではない テンプレートを選択してください
  • アクション編集画面を開き、カスタマイズタブを開きます

  • [エディタ設定 > JavaScriptバージョン] を ES14に変更します

  • HTMLを次の内容で置き換えます

<div class="chat-container">
    <div class="chat-app" krt-if="state==1">
        <div class="chat-header">
            <h1 class="chat-title">AI Assistant</h1>
            <div class="chat-header-icons">
                 <i class="chat-minimize-btn" krt-on:click="setState(1)">-</i>
                <i class="chat-close-btn karte-close">×</i>
            </div>
        </div>

        <div class="chat-body">
            <div class="welcome-message">
                <div class="ai-avatar">
                    <svg viewBox="0 0 24 24" width="24" height="24">
                        <path fill="currentColor" d="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10-4.48 10-10S17.52,2 12,2z M12,20c-4.41,0-8-3.59-8-8c0-4.41 3.59-8 8-8s8,3.59 8,8C20,16.41 16.41,20 12,20z M14.5,9h-1V7.5h-3V9h-1c-0.55,0-1,0.45-1,1v5c0,0.55 0.45,1 1,1h5c0.55,0 1-0.45 1-1v-5C15.5,9.45 15.05,9 14.5,9z"></path>
                    </svg>
                </div>
                <p>製品について自由に質問してください。FAQのデータをもとに回答します。</p>
            </div>

            <div class="input-area">
                <textarea
                    class="query-input"
                    placeholder="例: 名刺情報をCSVでエクスポートする方法はありますか?"
                    rows="3"
                    krt-model="query"></textarea>
                <div class="controls">
                    <button class="send-button" krt-on:click="sendQuery">
                        <span>送信</span>
                        <svg viewBox="0 0 24 24" width="16" height="16">
                            <path fill="currentColor" d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"></path>
                        </svg>
                    </button>
                </div>
            </div>

            <div class="loading-indicator" krt-if="isSent && isLoading">
                <div class="loading-dots">
                    <span></span>
                    <span></span>
                    <span></span>
                </div>
                <p>AIが回答を生成しています...</p>
            </div>

            <div class="result-container" krt-if="isSent && !isLoading">
                <div class="ai-response">
                    <div class="ai-avatar">
                        <svg viewBox="0 0 24 24" width="24" height="24">
                            <path fill="currentColor" d="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10-4.48 10-10S17.52,2 12,2z M12,20c-4.41,0-8-3.59-8-8c0-4.41 3.59-8 8-8s8,3.59 8,8C20,16.41 16.41,20 12,20z M14.5,9h-1V7.5h-3V9h-1c-0.55,0-1,0.45-1,1v5c0,0.55 0.45,1 1,1h5c0.55,0 1-0.45 1-1v-5C15.5,9.45 15.05,9 14.5,9z"></path>
                        </svg>
                    </div>
                    <div class="response-content">{{result}}</div>
                </div>
            </div>
        </div>

        <div class="chat-footer">
            <p>Powered by Craft RAG</p>
        </div>
    </div>
</div>
  • CSSを次の内容で置き換えます
.chat-container {
    font-family: 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Noto Sans JP', 'メイリオ', Meiryo, sans-serif;
    position: fixed;
    bottom: 20px;
    right: 20px;
    z-index: 1000;
}
.chat-app {
    width: 90vw; 
    max-width: 560px;
    height: 85vh; 
    max-height: 700px;
    display: flex;
    flex-direction: column;
    background-color: #ffffff;
    border-radius: 8px;
    box-shadow: 0 8px 16px rgba(60, 64, 67, 0.3);
    overflow: hidden;
    animation: fadeIn 0.3s ease-in-out;
}
.chat-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 12px 16px;
    background-color: #2aab9f;
    color: white;
    
}
.chat-title {
    margin: 0;
    font-size: 1.25rem;
    font-weight: 500;
}
.chat-header-icons {
    display: flex;
    align-items: center;
    gap: 4px; 
}
.chat-minimize-btn, 
.chat-close-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    border-radius: 50%;
    background-color: transparent; 
    cursor: pointer;
    font-size: 18px; 
    font-weight: bold;
    font-style: normal;
    line-height: 1;
    color: white; 
    transition: background-color 0.2s;
}
.chat-minimize-btn:hover,
.chat-close-btn:hover {
    background-color: rgba(255, 255, 255, 0.2);
}
.chat-body {
    flex: 1;
    padding: 16px;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 24px;
}
.welcome-message {
    display: flex;
    align-items: center;
    gap: 16px;
    background-color: #f0f8f7; 
    border-radius: 8px;
    padding: 16px;
    margin-bottom: 16px;
}
.ai-avatar {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    border-radius: 50%;
    background-color: #2aab9f;
    color: white;
    flex-shrink: 0;
}
.welcome-message p {
    margin: 0;
    color: #202124;
    line-height: 1.5;
}
.input-area {
    display: flex;
    flex-direction: column;
    gap: 8px;
    border: 1px solid #dadce0;
    border-radius: 8px;
    padding: 12px;
    background-color: #ffffff;
    transition: border-color 0.3s;
}
.input-area:focus-within {
    border-color: #2aab9f;
}
.query-input {
    width: 100%;
    border: none;
    outline: none;
    resize: none;
    font-family: inherit;
    font-size: 1rem;
    color: #202124;
    background-color: transparent;
    padding: 0;
    line-height: 1.5;
}
.query-input::placeholder {
    color: #5f6368;
    opacity: 0.7;
}
.controls {
    display: flex;
    justify-content: flex-end;
}
.send-button {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 16px;
    background-color: #2aab9f;
    color: white;
    border: none;
    border-radius: 24px;
    font-size: 0.875rem;
    font-weight: 500;
    cursor: pointer;
    transition: background-color 0.3s;
}
.send-button:hover {
    opacity: 0.9;
}
.loading-indicator {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    padding: 16px;
    color: #5f6368;
}
.loading-dots {
    display: flex;
    gap: 8px;
}
.loading-dots span {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background-color: #2aab9f;
    animation: dotPulse 1.5s infinite ease-in-out;
}
.loading-dots span:nth-child(2) {
    animation-delay: 0.2s;
}
.loading-dots span:nth-child(3) {
    animation-delay: 0.4s;
}
@keyframes dotPulse {
    0%, 100% { transform: scale(0.8); opacity: 0.5; }
    50% { transform: scale(1.2); opacity: 1; }
}
.loading-indicator p {
    margin: 0;
    font-size: 0.875rem;
}
.result-container {
    display: flex;
    flex-direction: column;
    gap: 16px;
    animation: fadeIn 0.3s;
    position: relative;
}
.ai-response {
    display: flex;
    gap: 16px;
}
.response-content {
    flex: 1;
    background-color: #f0f8f7; 
    padding: 16px;
    border-radius: 8px;
    color: #202124;
    line-height: 1.6;
    font-size: 0.9375rem;
    max-height: 400px; 
    overflow-y: auto; 
    scrollbar-width: thin;
    scrollbar-color: #c1c1c1 transparent;
}
.response-content::-webkit-scrollbar {
    width: 8px;
}
.response-content::-webkit-scrollbar-track {
    background: transparent;
}
.response-content::-webkit-scrollbar-thumb {
    background-color: #c1c1c1;
    border-radius: 4px;
}
.response-content::-webkit-scrollbar-thumb:hover {
    background-color: #a8a8a8;
}
.chat-footer {
    padding: 8px 16px;
    text-align: center;
    color: #5f6368;
    font-size: 0.75rem;
    border-top: 1px solid #e8eaed;
}
@keyframes fadeIn {
    from { opacity: 0; transform: translateY(10px); }
    to { opacity: 1; transform: translateY(0); }
}
@media screen and (max-width: 640px) {
    .chat-container {
        bottom: 0;
        right: 0;
        width: 100%;
    }
    .chat-app {
        width: 100%;
        height: 100vh; 
        max-width: 100%;
        max-height: 100vh; 
        border-radius: 0;
        bottom: 0;
        right: 0;
    }
    .chat-body {
        padding: 12px;
    }
}
  • JavaScriptを次の内容で置き換えます
// Craft Functions のエンドポイントURL(手順2で取得したURL)
const CRAFT_FUNCTIONS_ENDPOINT = 'https://xxx.cev2.karte.io/functions/xxxxxxxxxx';

widget.show();
widget.setVal('query', '');
widget.setVal('isSent', false);
widget.setVal('isLoading', true);

widget.method('sendQuery', async function() {
    widget.setVal('isLoading', true);
    widget.setVal('result', '');

    try {
        const body = {
            text: widget.getVal('query'),
            type: 'generate',
        };
        const options = {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(body),
        };
    
        // APIリクエスト実行
        widget.setVal('isSent', true);
        const response = await fetch(CRAFT_FUNCTIONS_ENDPOINT, options);
    
        if (!response.ok) {
            throw new Error(`APIエラー: ${response.status} ${response.statusText}`);
        }
    
        widget.setVal('isLoading', false);
        const data = await response.json();
        const result = data.result;
        widget.setVal('result', result);
    } catch (error) {
        widget.setVal('result', `エラーが発生しました: ${error.message}`);
    }    
});
  • JavaScript冒頭の定数 CRAFT_FUNCTIONS_ENDPOINT の値に、手順2で作成したファンクションのエンドポイントURLを設定します
  • 接客サービスをテスト配信するための配信設定をして、公開します

動作検証

設定が完了したら、実際にFAQボットが動作することを確認してみましょう。

  1. 作成した接客サービスが配信されるページにアクセスします
  2. 入稿済みのナレッジで回答可能な質問をテキストボックスに入力し、送信します
  3. Craft RAGから関連するコンテンツが検索され、自然な回答が生成されることを確認します

うまく動作しない場合は、WebブラウザのコンソールやCraft Functionsのログにエラーログが出ていないかを確認してみてください。

補足

今回のテンプレートで提供される2つの呼び出しタイプについて

今回紹介したCraft Functionsテンプレートでは、エンドポイント経由で下記パラメータを受け取るようなAPIを提供します。

パラメータ名サンプル値説明
text’パスワードを変更する方法は?‘ユーザーの問い合わせテキスト
type’retrieve’ or ‘generate’Craft RAGの利用方法を指定
threshold0.8関連性スコアの閾値。これより小さい関連性のコンテンツは利用されない。デフォルトは0.8

typeについては、次の2つの呼び出しタイプがサポートされています。今回の例では “generate” を利用していますが、用途に応じて使い分けてください。

A. テキスト生成(type: “generate”):

Craft RAG上の関連するコンテンツを使ってテキスト生成まで実施します。

  • リクエストbody
{
    "text": "パスワードを変更する方法は?",
    "type": "generate",
    "threshold": 0.8
}
  • レスポンスbody
{
    "result": "パスワードの変更方法についてご案内いたします。..."
}

B. コンテンツ検索(type: “retrieve”):

Craft RAG上の関連するコンテンツをそのまま取得します。検索結果を画面表示したいケースや、結果を使って独自に回答生成したいときに利用します。

  • リクエストbody
{
    "text": "パスワードを変更する方法は?",
    "type": "retrieve",
    "threshold": 0.8
}
  • レスポンスbody
{
    "result": [
        { "text": "# title\nパスワードを忘れた場合はどうすればいいですか?...", "score": 0.96 },
        { "text": "# title\n領収書の再発行は可能ですか?...", "score": 0.53 }
    ]
}

なお、Craft Functions側では次のメソッドが使われています。

type利用メソッド
テキスト生成(type: “generate”)aiModules.gcpGeminiGenerateContent()
コンテンツ検索(type: “retrieve”)rag.retrieveContexts()

これらメソッドの詳細については、下記記事の「1-4. Craft RAGのデータを参照する方法」をご覧ください。

Craft RAG と Craft Vector Search は、どう使い分ければいいのか?

ベクトル距離閾値の調整

thresholdパラメータを指定することで、ベクトル距離の閾値を調整できます。値が小さいほど関連性の低いコンテンツも含まれ、値が大きいほど関連性の高いコンテンツのみに絞られます。

thresholdの設定例:

  • 0.7: より多くのコンテンツを検索対象にしたい場合
  • 0.8: バランスの取れた設定(デフォルト)
  • 0.9: より関連性の高いコンテンツのみに絞りたい場合

まとめ

今回は、Craft RAGに蓄積したナレッジを活用してFAQボット機能を実装する方法について説明しました。

この仕組みを使うことで、Craft RAGとCraft AI Modulesの組み合わせにより、高度なAI機能を簡単に実装できます。接客サービスを通じてユーザーに価値のある情報提供が可能になり、サポート業務の効率化にもつながります。

また今回はFAQボットを例に紹介しましたが、Craft RAGの具体的なユースケースは他にもあります。詳しくは次の記事も合わせてご覧ください。

Craft RAG と Craft Vector Search は、どう使い分ければいいのか?

というわけで、Craft RAGを使ったソリューションをぜひ試してみてください!