RAG構築の現実解|初級エンジニアでも作れるドメイン特化AIのクラウド設計

catch-img

自社の製品仕様や社内規程を理解したAIが欲しいという方も多いでしょう。実現する技術的な選択肢は3つあり、ゼロからモデルを学習するか、既存モデルをファインチューニングするか、RAGを構築するか。

この中で、エンジニア1人が数週間で本番環境まで持っていける現実的な選択肢はRAGです。単なる消去法ではなく、社内ナレッジのように継続的に情報が更新される対象には、RAGが構造的に適しています。

課題はその先です。RAGはサンプルコードであれば30分程度で動かせる一方、業務で使える精度と構成に仕上げるまでには数ヶ月を要する技術です。

そこで本記事では、初級エンジニアがゼロからRAGを構築し、本番運用できる構成に到達するまでを次の順序で解説します。

  • なぜドメイン特化AIの現実解がRAGなのか
  • RAG構築の5工程と、つまずきやすい地点
  • 実装(Pythonコード付き・30分)
  • 精度が出ないときの改善策
  • クラウド設計(どこで何を動かすか)
  • 業務利用に向けた運用設計
SEO監修者|田村 響
SEO監修者|田村 響
SEOマーケター/コンサルタント。 BtoB領域を中心に、フリーランスとして独立後は、幅広いコンテンツ制作に携わる。また、ライターとして活動した後、メディア責任者やコンサルタントとして、40以上のメディアを担当。CV11倍の向上などの実績を持ち、読者の検索意図を踏まえた情報設計と、問い合わせ・資料請求につながる導線づくりを重視している。

NVIDIA H200を搭載したGPUが、業界最安値で使える!

最先端のNVIDIA H200を、業界最安値クラスでご提供。学習・推論のスピードはそのままに、GPUコストを大幅圧縮。生成AI・LLM学習・シミュレーションなど大規模計算に最適。オンプレミスや他社クラウドと比べても圧倒的に低コストで、最新GPUを手軽に利用できます。

ドメイン特化AIを作る3つ方法

冒頭で説明したとおり、ドメイン特化AIを作る方法は主に以下の3つです。

概要

事前学習

ファインチューニング

RAG

やること

モデル自体を一から学習

既存モデルに追加学習

既存モデルに資料を参照させる

必要なデータ量

数十億トークン規模

数百〜数千件の教師データ

手持ちの文書をそのまま利用

情報の更新

大規模GPUクラスタ

GPU数枚〜

推論のみ

根拠の提示

不可

困難

出典の明示が可能

立ち上げまでにかかる時期

数ヶ月〜

数週間

数日

どれを選べばいいかわからない時におすすめなのは、以下の図解を参考に判断してみてください。

社内規程、製品マニュアル、FAQ、過去の問い合わせ履歴などは情報が継続的に更新され、なおかつ回答の根拠を示す必要があるデータです。

ファインチューニングでは更新のたびに再学習が必要となり、さらにどの資料を根拠に回答したのかを示すことが難しいため、この2点だけでもRAGを選ぶ優位性があります。

RAGの3ステップ

RAGとは、LLMに回答させる前に外部の知識ベースから関連情報を検索し、その情報を質問と一緒に渡す仕組みです。

ステップ

処理内容

①入力

ユーザーが質問を入力する

②検索

知識ベースから質問に関連する文書を抽出する

③生成

抽出した文書と質問をセットでLLMへ渡し、回答を生成する

構造としては、「知識を問う試験」を「資料持ち込み可の試験」に変えるイメージです。モデルそのものに覚えさせるのではなく、回答するときに資料を参照させる。この違いが、情報更新への強さと根拠を提示できる仕組みに直結しています。

「構築する」の実体は2本のパイプライン

「RAGを構築する」と一言で表現されますが、実際に構築するのは性質の異なる2本のパイプラインです。ここを切り分けて捉えられるかどうかで、全体設計の見通しは大きく変わります。

初級者がつまずきやすいのは、後者だけを「RAGの実装」と考えてしまう点です。しかし回答精度を大きく左右するのは、実際には前者(インデクシング側)です。検索で取得できなかった情報は、どれだけ高性能なLLMを使っても回答に反映できません。

さらに、後述するクラウド設計でも、この2本のパイプラインでは要求される特性が正反対です。

RAG構築の5工程と初級エンジニアがつまずく地点

ここからは、RAGの以下5工程を説明しつつ、「何をするのか」と「どこでつまずきやすいのか」をセットで解説します。

  1. データソースの選定とクレンジング
  2. チャンク分割
  3. Embeddingによるベクトル化
  4. ベクトルDBへの格納
  5. 検索とLLM連携・プロンプト設計

STEP
01

データソースの選定とクレンジング

最初に決めるのは「何を読ませるか」です。ここでの選定ミスは、後工程のどんな技術的工夫でも取り返せません。

  • 対象文書の棚卸し
  • 鮮度の確認
  • 重複・矛盾の除去
  • 形式の統一

エンジニアの中で、技術的に面白くないため、この工程を軽視して先に進んでしまう人も多いです。しかし、RAG構築で最も工数を食うのはここです。実装が1に対して、データ整備と改善が5〜10というのが実感に近い比率です。

着手前にこの作業量を見積もらないと、スケジュールが必ず崩れます。

STEP
02

チャンク分割

文書を検索可能な単位に切り分けます。LLMに渡せるトークン量に上限があること、そして検索の粒度を適切にすることが目的です。意識すべきパラメータは3つです。

  • チャンクサイズ
  • オーバーラップ
  • 分割の基準

最も効果が大きいのは3つ目です。加えて、親見出しの情報を各チャンクに付与するだけで検索ヒット率が明確に変わります。

【悪い例】

幅:71.6 mm / 高さ:147.6 mm / 重量:170 g

【良い例】

iPhone 16 > 技術仕様 > サイズと重量

幅:71.6 mm / 高さ:147.6 mm / 重量:170 g

後者はチャンク単体で「何についての数値か」が分かります。

サンプルコードの多くは「1ファイル=1チャンク」または「固定文字数で機械分割」です。これで動きはしますが、実務文書では精度が出ません。最初から構造ベースの分割で組むことをおすすめします。

STEP
03

Embeddingによるベクトル化

分割したテキストを、意味を表す数値ベクトルに変換します。これにより、単語が一致しなくても意味が近ければヒットする検索が可能になります。「有給休暇」と質問して、文書内の「年次有給休暇」や「休暇の付与」にヒットするのはこの仕組みによるものです。

なお、選定時の注意点は以下のとおりです。

  • 日本語性能:英語ベンチマークの数値は日本語性能を保証しない
  • 次元数:大きいほど情報を保持できるが、計算コストとストレージが増える
  • モデル変更時は全件の再ベクトル化が必要:異なるモデルが生成したベクトルは意味空間が違うため、混在させると類似度計算が破綻する

3つ目が運用上かなり重いです。「Embeddingモデルを変えて精度を比べる」という当たり前の改善が、そのたびに全文書の再計算を伴います。この再計算をどこで回すかが、後述するクラウド設計の主要論点のひとつになります。

STEP
04

ベクトルDBへの格納

ベクトル化したデータを、高速な類似度検索に特化したデータベースに格納します。

選択肢

特徴

FAISS

ローカル完結、軽量。検証・小規模向け

pgvector

既存のPostgreSQLに載せられる。運用資産を流用できる

Chroma

導入が容易。プロトタイピング向け

Qdrant / Pinecone等

マネージド。スケールと運用負荷軽減

検証段階でマネージドサービスを選ぶ必要はほぼありません。

FAISSやpgvectorで十分です。データ量が数十万チャンクを超え、更新頻度と同時アクセスが実務レベルになった時点で移行を検討するようにしましょう。

STEP
05

検索とLLM連携・プロンプト設計

検索で得た上位チャンクを、質問と一緒にLLMへ渡します。プロンプトに必ず入れるべき要素は3つです。

  • コンテキスト限定の指示:「与えられた資料のみを根拠に回答し、記載がない場合はその旨を答えること」
  • 出典の明示:どのチャンクを参照したかを回答に含めさせる
  • 役割と出力形式の指定:想定読者に合わせた説明レベル

特に1つ目は必須です。これがないと、LLMは検索結果を無視して学習済み知識から補完し始めます。「資料に書いていないことは答えない」という制約こそが、RAGを業務で使える状態にします。

実装|最小構成のRAGを30分で動かす

ここからは、チャンク分割・ベクトル検索・出典明示まで含めた約60行のコードでRAGを動かします。

環境構築

最小構成のRAGシステムのプロジェクト雛形を作ります。

mkdir rag-minimal && cd rag-minimal
mkdir data

pip install openai faiss-cpu numpy python-dotenv

APIキーを.envに記述します。

OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxx

data/配下に、読み込ませたいテキストファイルを置きます。まずは1ファイル、数千文字程度から始めてみてください。

実装コード

app.pyとして保存します。

import os
import glob
import numpy as np
import faiss
from openai import OpenAI
from dotenv import load_dotenv

load_dotenv()
client = OpenAI()

EMBED_MODEL = "text-embedding-3-small"
CHAT_MODEL = "gpt-4o-mini"
CHUNK_SIZE = 500 # 1チャンクあたりの文字数
OVERLAP = 100 # 隣接チャンクの重複文字数
TOP_K = 4 # 検索で取得するチャンク数

def load_and_chunk(data_dir="data"):
"""テキストを読み込み、オーバーラップ付きで分割する"""
chunks = []
for path in glob.glob(f"{data_dir}/**/*.txt", recursive=True):
with open(path, encoding="utf-8") as f:
text = f.read()
step = CHUNK_SIZE - OVERLAP
for i in range(0, len(text), step):
body = text[i:i + CHUNK_SIZE].strip()
if body:
chunks.append({"source": os.path.basename(path), "text": body})
return chunks

def embed(texts):
"""テキスト群をベクトル化し、コサイン類似度用に正規化する"""
res = client.embeddings.create(model=EMBED_MODEL, input=texts)
vecs = np.array([d.embedding for d in res.data], dtype="float32")
faiss.normalize_L2(vecs)
return vecs

def build_index(chunks):
"""FAISSインデックスを構築する(正規化済みの内積=コサイン類似度)"""
vecs = embed([c["text"] for c in chunks])
index = faiss.IndexFlatIP(vecs.shape[1])
index.add(vecs)
return index

def search(query, index, chunks, k=TOP_K):
"""質問に近いチャンクを取得する"""
scores, ids = index.search(embed([query]), k)
return [{**chunks[i], "score": float(s)} for s, i in zip(scores[0], ids[0])]

def answer(query, hits):
"""検索結果をコンテキストとして回答を生成する"""
context = "\n\n---\n\n".join(
f"[出典: {h['source']}]\n{h['text']}" for h in hits
)
res = client.chat.completions.create(
model=CHAT_MODEL,
messages=[
{"role": "system", "content":
"与えられたコンテキストのみを根拠に日本語で回答してください。"
"コンテキストに記載がない場合は推測せず「資料に記載がありません」と答えること。"
"回答の末尾に、参照した出典名を必ず列挙すること。"},
{"role": "user", "content":
f"# コンテキスト\n{context}\n\n# 質問\n{query}"},
],
)
return res.choices[0].message.content

if __name__ == "__main__":
chunks = load_and_chunk()
if not chunks:
raise SystemExit("data/ 配下に .txt ファイルを置いてください")
index = build_index(chunks)
print(f"{len(chunks)} チャンクをインデックス化しました")

while True:
q = input("\n質問(exitで終了): ")
if q == "exit":
break
hits = search(q, index, chunks)
print("\n--- 検索されたチャンク ---")
for h in hits:
print(f" {h['score']:.3f} | {h['source']} | {h['text'][:40]}...")
print("\n--- 回答 ---")
print(answer(q, hits))

動作確認をして検索結果を確認します。

python app.py

質問を入力すると、検索されたチャンクと類似度スコア、それに基づく回答が表示されます。

ここで意識してほしいのは、回答よりも検索結果のほうを見るという習慣です。的外れな回答が返ってきたとき、原因が「検索が拾えていない」のか「LLMが情報を活かせていない」のかは、検索結果を見ないと切り分けられません。この可視化が、次章の改善フェーズで最も重要な情報源になります。

資料に書かれていないことを質問して、「資料に記載がありません」と返れば、コンテキスト限定の指示が走っていることが確認できます。

つまづきやすいポイント

症状

原因と対処

モデル名でエラーが出る

生成AIのモデルは更新・提供終了が速い。公式のモデル一覧で現在利用できる識別子を確認する

data/のファイルが読まれない

実行ディレクトリと相対パスが一致していない。文字コードがUTF-8かも確認する

検索結果が的外れ

チャンクサイズが大きすぎるか、意味の区切りを無視した分割になっている

回答に資料外の情報が混ざる

systemプロンプトのコンテキスト限定指示が弱い。制約をより明確にする

Embeddingでエラー

一度に送信するテキスト量が上限を超えている。複数バッチに分割する

ここまでが「動く」段階です。

RAGの精度が出ない原因と改善策

自分の業務文書をRAGに読み込ませて実際に質問してみると、「一応動くけれど、業務では使えない」と感じる場面が出てきます。

RAG開発では、ここからが本番です。

原因と改善策

01

原因を切り分けてから改善する

精度改善に着手する前に、まず「どこで問題が起きているのか」を切り分けます。

  1. そもそも文書内に答えが存在するか
  2. 検索で正しいチャンクを取得できているか
  3. 正しいチャンクを取得できているのに回答が悪いか

この切り分けをせず、いきなりプロンプトだけを変更するのはRAG改善で起こりやすい遠回りです。

前章のコードで検索結果そのものを表示しているのも、「検索が悪いのか」「生成が悪いのか」を確認できるようにするためです。

原因と改善策

02

ハイブリッド検索を導入する

ベクトル検索は文章同士の「意味の近さ」を使って検索するため、言い換えや表記ゆれに強い一方、固有名詞や型番などの完全一致が重要な検索では弱くなる場合があります。

例えば、「型番XR-2400の仕様」と質問しているにもかかわらず、意味的に近い別製品の仕様が上位に表示されるケースです。

そこで有効なのが、ベクトル検索とキーワード検索を組み合わせるハイブリッド検索です。

  • ベクトル検索:表記ゆれや言い換え、意味的に近い文章を探しやすい

  • キーワード検索:固有名詞、型番、製品名、略語などの一致に強い

キーワード検索にはBM25などのアルゴリズムが利用されます。

製品情報や社内独自の用語を扱うRAGでは、ベクトル検索だけでなくキーワード検索を組み合わせることで、検索精度が大きく改善するケースがあります。比較的導入しやすいため、優先的に検討したい改善策です。

原因と改善策

03

リランキングで検索結果を並べ替える

ベクトル検索では、文章をEmbeddingモデルによってベクトルに変換し、その距離や類似度から検索結果を取得します。

ただし、文章をベクトルへ圧縮する以上、すべての情報を完全に保持できるわけではありません。そのため、必要な情報を含むチャンクが取得できていても、検索順位が低くなることがあります。

そこで利用されるのがリランキングです。例えば、以下のような2段階構成にします。

  • ベクトル検索で候補を10〜20件程度取得する

  • リランクモデルで質問と各チャンクの関連度を再評価する

  • 絞り込んだチャンクだけをLLMへ渡す

リランクモデルは、質問とチャンクの組み合わせをより詳細に評価できるため、単純なベクトル類似度よりも関連性を判断しやすくなります。

一方、すべての文書に対してリランク処理を実行すると計算量が増えます。そのため、ベクトル検索で粗く候補を絞る → リランカーで精密に並べ替えるという2段階構成にするのが基本です。

原因と改善策

04

チャンク設計を見直す

検索結果を確認して「必要な情報は検索できているが、文章の途中で切れている」という場合は、チャンク設計そのものを見直します。

代表的な改善方法は以下です。

  • チャンクサイズを調整する

  • オーバーラップ量を調整する

  • 文字数単位ではなく見出し・章・条項などの構造単位で分割する

  • チャンクに親見出しや文書タイトルなどの情報を付与する

重要なのは、すべての文書に使える万能なチャンクサイズは存在しないことです。例えば、FAQ、就業規則、技術マニュアル、議事録では文書構造が異なるため、適切な分割方法も変わります。

原因と改善策

05

クエリ変換を利用する

検索精度が上がらない原因が、文書側ではなく「質問文」にある場合もあります。利用者が入力した質問を、そのままベクトル検索に利用するとは限りません。

そこで使われるのが、Multi-queryやHyDE(Hypothetical Document Embeddings)などのクエリ変換です。

例えば、「繁忙期に社員に長く働いてもらうには?」という質問から、など、複数の検索文を生成して、それぞれで検索します。これにより、質問表現の違いによる検索漏れを減らせます。

まとめ

RAGは、社内規程や製品マニュアル、FAQなど、継続的に更新される情報を活用したドメイン特化AIを構築するうえで、現実的な選択肢のひとつです。最小構成であれば初級エンジニアでも短時間で動かせますが、業務で使える精度まで高めるには、

  • データ整備やチャンク設計
  • Embedding
  • 検索
  • リランキング
  • プロンプト設計

などを継続的に改善する必要があります。

特に重要なのは、回答結果だけを見るのではなく、「必要な情報を検索できているか」「正しいチャンクを取得できているか」を確認し、原因を切り分けながら改善することです。

RAGの精度はLLMの性能だけで決まるものではなく、検索パイプライン全体の設計によって左右されます。

こうした改善サイクルを回す際には、Embeddingモデルの比較検証やリランキングモデルの選定、さらには独自LLMのファインチューニングなど、GPUを使った試行錯誤が欠かせません。しかし自社でGPU環境を用意しようとすると、初期投資やメガクラウドの高い利用料がハードルになりがちです。

NVIDIA H200を搭載したGPUが、業界最安値で使える!

国産GPUクラウドサービス「GPUSOROBAN」は、AWS・GCP・Azureと比較して大幅に安い料金でNVIDIA GPUを提供しており、国内で初めてNVIDIAエリートパートナーに認定されたサービスです。RAGの検証・改善サイクルをコストを抑えながら継続的に回したい方は、ぜひ資料請求のうえ詳細をご確認ください。