国産LLM tsuzumiでRAGを作る 実測比較で見る精度と応答

はじめに

NTTビジネスソリューションズの辻本です。

この記事では、NTTの国産LLM「tsuzumi」を用いたRAG実装をガイドするとともに、ベースモデル(RAGなし)とRAG構成を同一の質問で実行し、精度・応答時間の実測値に基づいて効果を比較検証した結果を紹介します。 なお、本記事中で扱うサービス(Microsoft Azureなど)に関する記載は2025年11月時点の情報に基づきます。また、比較検証結果は筆者の実行環境・設定に依存し、 記事内で掲載しているコードは、理解しやすさを優先した簡略版(抜粋)です。実際の実装には、エラーハンドリングなどが含まれています。

対象読者

  • LLM(大規模言語モデル)を初めて触る方
  • Azureを使ったことはあるが、AI機能は初めての方
  • RAGという言葉を聞いたことはあるが、実装経験がない方

tsuzumiとRAG

tsuzumiとは

tsuzumiは、NTTが開発した日本語に特化した大規模言語モデル(LLM)で、軽量ながら高い日本語処理能力を持つとされています。

特徴:

  • Azure上で利用可能: MaaS(Model as a Service)形式で提供されており、サーバー管理不要
  • 軽量モデル: 7B(70億)パラメータで、大規模モデル(175B等)よりコンパクト

RAG(検索拡張生成)とは

なぜRAGが必要なのか:

  • ベースモデルの制約: LLMは基本的に学習データに含まれる範囲をもとに回答する
    • 例: 企業固有の情報(社内マニュアル等)は学習していない場合が多い
  • RAGの考え方: 最新情報や専門知識を検索して、LLMに与えたうえで回答させる

RAGの仕組み:

質問「NTTパビリオンのテーマは?」
↓
① ベクトル検索 ──→ ナレッジベース
↓               (8つのMarkdownファイル)
② 関連コンテキスト取得(Top 3件)
↓
③ LLM(tsuzumi)へ送信
↓「この情報を参考に回答して」
④ 回答生成「PARALLEL TRAVELです」

RAGの仕組み(ステップバイステップ):

  1. ユーザーが質問: 「NTTパビリオンのテーマは?」
  2. ベクトル検索: 質問に関連する情報をナレッジベースから検索
    • ナレッジベース=事前に用意した文書集(Markdownファイル等)
    • ベクトル検索=文章の意味的な類似度で検索
  3. コンテキスト取得: 関連度の高い情報をTop 3件取得
  4. LLMへ送信: 「この情報を参考に回答して」と質問+情報をセットで送る
  5. 回答生成: LLMが情報を基に回答を生成する

今回のユースケース

題材: 大阪・関西万博のNTTパビリオン案内ボットを構築し、tsuzumiのベースモデルとRAG構成を定量的に比較する

ナレッジベース: 8つのMarkdownファイル

knowledge/
├── 01_overview.md          # パビリオン全体概要
├── 02_zone1.md             # Zone1(コミュニケーション歴史)
├── 03_zone2.md             # Zone2(IOWN × Perfume)
├── 04_zone3.md             # Zone3(Another Me)
├── 05_finale.md            # 参加型フィナーレ
├── 06_architecture.md      # 建築コンセプト
├── 07_technology.md        # 技術(IOWN, tsuzumi)
└── 08_faq.md               # よくある質問

環境構築

ステップバイステップ実装

Step 1: プロジェクト作成

tsuzumiのデプロイや利用方法について記事の下部に参考リンクがあるためこちらもご参照ください。 

AI Foundryの概要ページ

AzureポータルでFoundryを検索し、概要ページを表示します。「リソースの作成」を選択してください。 リージョンはEast US 2で作成します。

リソースグループの作成

Step 2: tsuzumi-7bデプロイ(Marketplace経由)

AzureポータルからAzure Marketplaceを表示し、tsuzumiを検索します。

Marketplaceでtsuzumiを検索

検索結果から「購読する」を押下すると表示される「NTTDATA tsuzumi-7B Instruct」を選択すると、AI Foundryへ自動的に遷移します。「このモデルを使用する」を選択してください。

モデル選択画面

プロジェクトを作成します。下部の「高度なオプション」を開くとリソースグループを選択できます。プロジェクト名を入力し、先ほど作成したリソースグループを選択して「作成」を選択してください。

⚠️ 新規作成する場合は、リージョンを「East US 2」に設定してください。

プロジェクト作成画面

作成処理が実行されます。

自動設定の実行中

プロジェクトの設定が完了すると、API情報を含む概要画面が表示されます。

tsuzumiデプロイ完了

Step 3: Pythonプロジェクト作成

仮想環境(venv)を利用します。

# プロジェクトディレクトリ作成
mkdir tsuzumi-rag-demo
cd tsuzumi-rag-demo

# 仮想環境作成
python3 -m venv venv

# 仮想環境の有効化
source venv/bin/activate

依存関係インストール:

pip install fastapi uvicorn chromadb sentence-transformers openai

Step 4: 環境変数の設定

Azure AI Foundryで表示される設定情報から.envファイルを作成します。

# Azure AI FoundryのエンドポイントとAPIキー
AZURE_MAAS_ENDPOINT=https://xxx-xxx-xxxxx.xxx.xxxxx.xxx.xxx.xxx
AZURE_MAAS_API_KEY=your-api-key-here

ナレッジベースの準備

Markdownファイルの作成

knowledge/03_zone2.mdの例:

---
title: "Zone2: IOWN × Perfume ライブ体験"
category: "exhibition"
keywords: ["IOWN", "Perfume", "3D伝送"]
---

# Zone2: IOWN × Perfume ライブ体験

## 概要
Zone2は、IOWNを使って離れた場所のライブパフォーマンスを
まるで目の前で繰り広げられているかのように体験できるゾーンです。

## 技術的特徴
- **3D点群データのリアルタイム伝送**:
  従来の映像とは異なり、人物の3D形状をリアルタイムで伝送
- **床振動装置**: 音楽に合わせた触覚フィードバック
- **超低遅延**: IOWNによりライブ感を損なわない伝送

## 体験内容
Perfumeの3人がリアルタイムでパフォーマンスを披露。
離れた場所にいるにも関わらず、まるで同じ空間にいるかのような臨場感を体験できます。

ナレッジベース読み込みスクリプト

scripts/load_knowledge.py:

from app.services.chroma_service import ChromaService

def main():
    chroma_service = ChromaService(persist_directory="./chroma_data")
    count = chroma_service.load_knowledge_base("./knowledge")
    print(f"Successfully loaded {count} documents")

if __name__ == "__main__":
    main()

実行:

python scripts/load_knowledge.py
# 出力: Successfully loaded 8 documents

ChromaDBのセットアップ

ChromaDBとは

ChromaDBは「ベクトルデータベース」です。文章を数値(ベクトル)に変換して保存し、 似た意味の文章を高速に検索できます。普通のデータベース(MySQL等)とは異なり、 「意味的な類似度」で検索できるのが特徴です。

ファイル構成

backend/
├── app/
│   ├── services/
│   │   └── chroma_service.py
│   └── main.py
└── chroma_data/

ChromaService実装

backend/app/services/chroma_service.py(抜粋):

import chromadb
from typing import List, Dict, Any
from pathlib import Path
import re

class ChromaService:
    def __init__(self, persist_directory: str = "./chroma_data"):
        self.client = chromadb.PersistentClient(path=persist_directory)
        self.collection = self.client.get_or_create_collection(
            name="pavilion_knowledge",
            metadata={"description": "NTT Pavilion knowledge base"}
        )

    def load_knowledge_base(self, knowledge_dir: str) -> int:
        knowledge_path = Path(knowledge_dir)
        documents = []

        for md_file in knowledge_path.glob("*.md"):
            content = md_file.read_text(encoding="utf-8")
            content = re.sub(r'^---\n.*?\n---\n', '', content, flags=re.DOTALL)

            documents.append({
                "id": md_file.stem,
                "content": content.strip(),
                "metadata": {"source": md_file.name}
            })

        if documents:
            self.collection.add(
                ids=[doc["id"] for doc in documents],
                documents=[doc["content"] for doc in documents],
                metadatas=[doc["metadata"] for doc in documents]
            )

        return len(documents)

    def search(self, query: str, top_k: int = 3) -> List[Dict[str, Any]]:
        results = self.collection.query(
            query_texts=[query],
            n_results=top_k
        )

        formatted_results = []
        if results["ids"] and len(results["ids"][0]) > 0:
            for i in range(len(results["ids"][0])):
                formatted_results.append({
                    "id": results["ids"][0][i],
                    "content": results["documents"][0][i],
                    "metadata": results["metadatas"][0][i],
                    "distance": results["distances"][0][i]
                })

        return formatted_results

RAGハンドラーの実装

RAGハンドラーとは

RAGハンドラーは、ベクトル検索とLLM呼び出しを組み合わせる「橋渡し役」です。 ①ベクトル検索でコンテキスト取得→②プロンプト作成→③LLM呼び出しの3ステップを実行します。

ファイル構成

backend/
├── app/
│   ├── handlers/
│   │   └── rag_handler.py
│   ├── services/
│   │   └── chroma_service.py
│   └── main.py
└── .env

RAGHandler実装

backend/app/handlers/rag_handler.py(抜粋):

from app.services.chroma_service import ChromaService
from openai import OpenAI
import os
import time

class RAGHandler:
    def __init__(self):
        self.chroma_service = ChromaService()
        self.endpoint = os.getenv("AZURE_MAAS_ENDPOINT", "")
        self.api_key = os.getenv("AZURE_MAAS_API_KEY", "")

    def process(self, question: str, temperature: float = 0.2, max_tokens: int = 800) -> dict:
        start_time = time.time()

        search_results = self.chroma_service.search(question, top_k=3)

        context = "\n\n".join([
            result['content']
            for result in search_results
        ])

        system_message = """あなたはNTTパビリオンの親切な案内スタッフです。
以下の参考情報を基にして、訪問者の質問に丁寧に答えてください。
参考情報に含まれていない内容については推測せず、「その情報は手元にございません」と正直に答えてください。"""

        user_message = f"""# コンテキスト情報
{context}

# 質問
{question}"""

        base_url = self.endpoint.rstrip("/") + "/v1/"

        client = OpenAI(
            base_url=base_url,
            api_key=self.api_key,
            timeout=30.0
        )

        response = client.chat.completions.create(
            model="tsuzumi-7b-instruct",
            messages=[
                {"role": "system", "content": system_message},
                {"role": "user", "content": user_message}
            ],
            temperature=temperature,
            max_tokens=max_tokens
        )

        elapsed_time_ms = int((time.time() - start_time) * 1000)

        return {
            "answer": response.choices[0].message.content,
            "context_retrieved": [r["metadata"]["source"] for r in search_results],
            "elapsed_time_ms": elapsed_time_ms
        }

FastAPIエンドポイント

メインアプリケーション

backend/app/main.py(抜粋):

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from app.handlers.rag_handler import RAGHandler

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

rag_handler = RAGHandler()

class QuestionRequest(BaseModel):
    question: str
    temperature: float = 0.2
    max_tokens: int = 800

@app.get("/api/health")
async def health_check():
    return {"status": "healthy"}

@app.post("/api/ask/rag")
def ask_rag(request: QuestionRequest):
    return rag_handler.process(
        question=request.question,
        temperature=request.temperature,
        max_tokens=request.max_tokens
    )

実行とテスト

FastAPI起動:

source venv/bin/activate
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000

curlでテスト:

curl -X POST http://localhost:8000/api/ask/rag \
  -H "Content-Type: application/json" \
  -d '{
    "question": "NTTパビリオンのテーマは何ですか?",
    "temperature": 0.2,
    "max_tokens": 800
  }'

ベースモデルvsRAG比較結果(実測値)

同一の質問3つを、ベースモデルとRAGパターンで実行した結果を比較します。

比較方法

  • 同一質問3つを両パターンで実行
  • 回答内容(公式情報との整合を含む)、応答時間、参照元を記録

Question 1 テーマ質問

質問: 「NTTパビリオンのテーマは何ですか?」

項目 ベースモデル RAG
回答 "NTTパビリオンのテーマは、「未来への挑戦」です。" "NTTパビリオンのテーマは「PARALLEL TRAVEL(パラレル・トラベル)」です。"
応答時間 1.422秒 1.989秒
公式情報との整合(筆者判断) ❌ 整合しない ✅ 整合しやすい
参照元 なし 08_faq.md, 05_finale.md, 06_architecture.md

スクリーンショット:

ベースモデルのJSON応答(テーマが意図した内容と異なる)

RAG版のJSON応答(テーマの回答とcontext_retrieved配列)

ポイント: RAGではナレッジベースに沿った回答になり、ベースモデルでは異なる回答になることがありました。


Question 2 IOWN技術

質問: 「IOWNとは何ですか?その特徴を教えてください。」

Before(ベースモデル):

回答: IOWNとは、NTTが提供するインターネットの仮想空間です。
ユーザーは、自分のアバターを作成し、様々な衣装やアクセサリーを身につけることができます...
  • 観察された点: 仮想空間・アバターの話が中心となり、意図した「光通信基盤」の説明としては焦点がずれていました
  • 応答時間: 5.399秒

After(RAG):

回答: IOWNは、光技術を活用した革新的な通信基盤です。
従来のネットワークでは実現困難だった、超大容量・超低遅延・低消費電力の通信を可能にします。

参照元:
- 08_faq.md
- 07_technology.md
- 03_zone2.md
  • 観察された点: ナレッジベースに沿った形で、特徴が整理されて提示されました、応答時間がベースの方がかかっていました。ネットワークの状態などの影響が考えられます。
  • 応答時間: 4.790秒

スクリーンショット:

ベースモデルの回答例(仮想空間・アバターの話が中心)

RAG版の回答例(複数のナレッジファイルから情報取得)

ポイント: ベースモデルは意図した観点と異なる内容が含まれる場合があり、RAGではナレッジベースに沿った説明になりました。


Question 3 Zone2の見どころ

質問: 「Zone2の見どころを教えてください。」

項目 ベースモデル RAG
回答 "Zone2は、最新技術を体験できるゾーンです..." "Zone2の見どころは、NTTが開発する次世代通信インフラ「IOWN」の技術を体験できることです。離れた場所で行われているPerfumeのライブパフォーマンスを、まるで目の前で繰り広げられているかのように体験できます。"
応答時間 2.399秒 3.998秒
具体性(筆者判断) △ 抽象的 ○ 具体例を含む
参照元 なし 08_faq.md, 03_zone2.md, 07_technology.md

ポイント: ベースモデルは一般論にとどまる回答となることがあり、RAGでは固有名詞を含む説明になりました。


総合評価

定量的比較:

指標 ベースモデル RAG 備考
平均応答時間 3.21秒 4.06秒 +0.85秒(約26%増)
公式情報との整合(筆者判断) 0/3問 3/3問
具体性(筆者判断) 一般的 詳細・具体例あり
参照元明示 なし 全て3ファイル context_retrievedで確認

定性的評価:

  • RAGでは、固有名詞や技術用語がナレッジベースに沿った説明になりました(PARALLEL TRAVEL、Perfumeなど)
  • ベースモデルは一般知識のみで回答し、固有情報については回答がぶれる場合がありました(例: IOWNを別概念として説明するケース)
    • 応答時間は約0.85秒増加しました

まとめ

RAGで確認できたこと(本検証範囲)

観察できた点:

  • 本検証の3問では、RAG構成のほうがナレッジベースに沿った回答になりました
  • 固有名詞を含む質問(PARALLEL TRAVEL、Perfumeなど)で、参照元を提示できました
  • 技術用語(IOWNなど)も、ナレッジベースに沿った形で説明されました

トレードオフ:

  • 応答時間が約0.85秒増加しました(ベクトル検索の処理が追加されるためと考えられます)
  • ナレッジベースの整備が継続的に必要になりそうです

おわりに

この記事では、国産LLM「tsuzumi」を使ったRAG実装の一例を紹介しました。 また、タスク処理能力の強化・知識の増強がうたわれた「tsuzumi 2」が記事執筆中にリリースされました。こちらも今後、機会を見て試していきたいです。

この記事のポイント:

  • RAGは知識を拡張するための有力な手法の一つ
  • ChromaDBとsentence-transformersで、RAGを実装可能
  • 本検証範囲では、ベースモデルと比べてRAG構成のほうがナレッジベースに沿った回答になりました
  • IOWNの質問で、ベースモデルが別概念の説明に寄るケースがあり、RAGの必要性を考えるきっかけになりました

この記事が、皆さんのRAG実装の第一歩になれば幸いです。


参考リンク


執筆者

辻本傑(NTTビジネスソリューションズ株式会社 バリューデザイン部 システム開発部門)
ビジネスチャットの開発・運用に携わっています。C#やクラウドが好きで、最近は生成AIの活用にも関心があります。
認定スクラムマスター(CSM)


商標

  • IOWNおよびtsuzumiはNTT株式会社の登録商標です。
  • 「大阪・関西万博」「EXPO 2025」は、2025年日本国際博覧会協会が定める名称です。
  • Microsoft AzureおよびAzure AI Foundryは、米国Microsoft Corporationの商標または登録商標です。
  • Pythonは、Python Software Foundationの登録商標です。
  • FastAPIは、tiangolo(José Sebastián Ramírez)所有の商標です。
  • Chromaは、Chroma Inc.の商標です。
  • OpenAIは、OpenAI, Inc.の商標または登録商標です。
  • 本記事中の固有名詞(アーティスト名、団体名等)は、それぞれの権利者に帰属します。
  • その他、本文中に記載されている会社名・製品名・サービス名等は、各社の商標または登録商標である場合があります。

© NTT WEST, Inc.