- HOME >
- Jamstack用語集 >
- RAG
RAG
Retrieval Augmented Generation(検索拡張生成)
RAG を分かりやすく
RAG(Retrieval Augmented Generation、検索拡張生成)は、AI の最も重要な応用技術の一つです。でも、名前だけ聞いても何のことか分からないですよね。
例え話をしましょう。あなたが試験を受けるとします。2 つの方法があります:
方法 A: 自分の記憶だけで答える
- メリット: 速い
- デメリット: 記憶違いがあるかも、古い情報かも、知らないことは答えられない
方法 B: 参考書を見ながら答える
- メリット: 正確な情報、最新の情報、幅広いトピックに対応
- デメリット: 参考書を探す時間がかかる
RAG は「方法 B」です。AI が回答を生成する前に、関連する情報をデータベースから検索して、それを参照しながら回答を作ります。これにより、AI の「幻覚(Hallucination)」を減らし、より正確で最新の情報を提供できるのです。
RAG が解決する問題
通常の LLM には、いくつかの制限があります:
- 知識の更新日が古い: GPT-4 の知識は 2023 年 4 月までなど、訓練時点で知識が固定される
- ハルシネーション: 知らないことを適当に答えてしまう
- プライベートデータにアクセスできない: あなたの会社のドキュメントは当然知らない
- ソースを示せない: どこから情報を得たのか不明
RAG はこれらすべての問題を解決します。
RAG のアーキテクチャ
RAG システムは主に 3 つのステップで動作します:
- 文書の埋め込み(Embedding): テキストを数値ベクトルに変換してデータベースに保存
- 検索(Retrieval): ユーザーの質問に関連する文書を検索
- 生成(Generation): 検索した文書を参考にして LLM が回答を生成
Next.js × TypeScript で RAG を実装する
それでは、実際に RAG システムを構築してみましょう。今回は Pinecone(ベクトルデータベース)と OpenAI を使用します。
ステップ 1: 環境構築
npm install @pinecone-database/pinecone openai ai
環境変数を設定:
# .env.local
OPENAI_API_KEY=sk-...
PINECONE_API_KEY=...
PINECONE_ENVIRONMENT=...
PINECONE_INDEX=documents
ステップ 2: ベクトルデータベースのセットアップ
// lib/pinecone.ts
import { Pinecone } from '@pinecone-database/pinecone'
let pineconeClient: Pinecone | null = null
export async function getPineconeClient() {
if (pineconeClient) {
return pineconeClient
}
pineconeClient = new Pinecone({
apiKey: process.env.PINECONE_API_KEY!,
})
return pineconeClient
}
export async function getIndex() {
const client = await getPineconeClient()
return client.index(process.env.PINECONE_INDEX!)
}
ステップ 3: 文書の埋め込みと保存
// lib/embeddings.ts
import OpenAI from 'openai'
import { getIndex } from './pinecone'
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
})
// テキストを埋め込みベクトルに変換
export async function createEmbedding(text: string): Promise<number[]> {
const response = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: text,
})
return response.data[0].embedding
}
// 文書を分割してベクトル化し、Pinecone に保存
export async function indexDocuments(
documents: Array<{ id: string; content: string; metadata?: Record<string, any> }>
) {
const index = await getIndex()
// 文書を並列で処理
const vectors = await Promise.all(
documents.map(async (doc) => {
const embedding = await createEmbedding(doc.content)
return {
id: doc.id,
values: embedding,
metadata: {
content: doc.content,
...doc.metadata,
},
}
})
)
// Pinecone に一括アップロード(100件ずつ)
const batchSize = 100
for (let i = 0; i < vectors.length; i += batchSize) {
const batch = vectors.slice(i, i + batchSize)
await index.upsert(batch)
}
return { indexed: vectors.length }
}
ステップ 4: 文書のアップロード API
// app/api/documents/upload/route.ts
import { indexDocuments } from '@/lib/embeddings'
export async function POST(req: Request) {
try {
const { documents } = await req.json()
// バリデーション
if (!Array.isArray(documents) || documents.length === 0) {
return new Response('documents 配列が必要です', { status: 400 })
}
// 文書をインデックス化
const result = await indexDocuments(documents)
return Response.json({
success: true,
indexed: result.indexed,
})
} catch (error) {
console.error('Document indexing error:', error)
return new Response('文書のインデックス化に失敗しました', { status: 500 })
}
}
ステップ 5: RAG による質問応答 API
// app/api/rag/chat/route.ts
import OpenAI from 'openai'
import { OpenAIStream, StreamingTextResponse } from 'ai'
import { createEmbedding } from '@/lib/embeddings'
import { getIndex } from '@/lib/pinecone'
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
})
export const runtime = 'edge'
export async function POST(req: Request) {
try {
const { messages } = await req.json()
// 最新のユーザーメッセージを取得
const lastMessage = messages[messages.length - 1]
const userQuery = lastMessage.content
// 1. ユーザーの質問を埋め込みベクトルに変換
const queryEmbedding = await createEmbedding(userQuery)
// 2. 類似文書を検索
const index = await getIndex()
const searchResults = await index.query({
vector: queryEmbedding,
topK: 5, // 上位 5 件を取得
includeMetadata: true,
})
// 3. 検索結果から文脈を構築
const context = searchResults.matches
.map((match) => match.metadata?.content)
.filter(Boolean)
.join('\n\n---\n\n')
// 4. 文脈を含めたプロンプトを作成
const systemPrompt = `あなたは優秀なアシスタントです。以下の文脈情報を基に、ユーザーの質問に正確に答えてください。
文脈に含まれていない情報については、「提供された情報からはわかりません」と答えてください。
# 文脈情報:
${context}
# 指示:
- 文脈情報を基に回答してください
- 文脈にない情報は推測しないでください
- 回答の根拠となる情報源を明示してください`
// 5. LLM に質問と文脈を送信
const response = await openai.chat.completions.create({
model: 'gpt-4',
stream: true,
messages: [
{
role: 'system',
content: systemPrompt,
},
...messages,
],
temperature: 0.3, // RAG では低めの temperature が推奨
})
const stream = OpenAIStream(response)
return new StreamingTextResponse(stream)
} catch (error) {
console.error('RAG Chat Error:', error)
return new Response('エラーが発生しました', { status: 500 })
}
}
ステップ 6: フロントエンドの実装
// app/rag/page.tsx
'use client'
import { useChat } from 'ai/react'
import { useState } from 'react'
export default function RAGChatPage() {
const [isUploading, setIsUploading] = useState(false)
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
api: '/api/rag/chat',
})
// 文書アップロード
const handleUploadDocuments = async (content: string) => {
setIsUploading(true)
try {
// 文書を段落ごとに分割(簡易実装)
const paragraphs = content
.split('\n\n')
.filter((p) => p.trim().length > 0)
.map((p, i) => ({
id: `doc-${Date.now()}-${i}`,
content: p.trim(),
metadata: {
uploadedAt: new Date().toISOString(),
},
}))
const response = await fetch('/api/documents/upload', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ documents: paragraphs }),
})
if (response.ok) {
const data = await response.json()
alert(`${data.indexed} 件の文書をインデックス化しました`)
} else {
alert('アップロードに失敗しました')
}
} catch (error) {
console.error('Upload error:', error)
alert('エラーが発生しました')
} finally {
setIsUploading(false)
}
}
return (
<div className="container mx-auto max-w-4xl p-4">
<h1 className="text-3xl font-bold mb-6">RAG チャットシステム</h1>
{/* 文書アップロードセクション */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<h2 className="text-lg font-semibold mb-2">文書のアップロード</h2>
<textarea
className="w-full border border-gray-300 rounded p-2 mb-2"
rows={6}
placeholder="ここに文書を貼り付けてください..."
id="document-content"
/>
<button
onClick={() => {
const textarea = document.getElementById(
'document-content'
) as HTMLTextAreaElement
handleUploadDocuments(textarea.value)
}}
disabled={isUploading}
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 disabled:bg-gray-300"
>
{isUploading ? 'アップロード中...' : 'アップロード'}
</button>
</div>
{/* チャットセクション */}
<div className="bg-gray-50 rounded-lg p-4 h-[500px] overflow-y-auto mb-4">
{messages.length === 0 ? (
<div className="text-center mt-8 text-gray-500">
<p>文書をアップロードしてから、質問を送信してください</p>
</div>
) : (
messages.map((message) => (
<div
key={message.id}
className={`mb-4 ${message.role === 'user' ? 'text-right' : 'text-left'}`}
>
<div
className={`inline-block rounded-lg px-4 py-2 max-w-[80%] ${
message.role === 'user'
? 'bg-blue-500 text-white'
: 'bg-white border border-gray-200'
}`}
>
<p className="text-xs mb-1">
{message.role === 'user' ? 'あなた' : 'AI(RAG)'}
</p>
<p className="whitespace-pre-wrap">{message.content}</p>
</div>
</div>
))
)}
</div>
{/* 入力フォーム */}
<form onSubmit={handleSubmit} className="flex gap-2">
<input
type="text"
value={input}
onChange={handleInputChange}
placeholder="質問を入力..."
className="flex-1 border border-gray-300 rounded-lg px-4 py-2"
disabled={isLoading}
/>
<button
type="submit"
disabled={isLoading || !input.trim()}
className="bg-blue-500 text-white px-6 py-2 rounded-lg hover:bg-blue-600 disabled:bg-gray-300"
>
送信
</button>
</form>
</div>
)
}
高度な RAG テクニック
1. ハイブリッド検索(ベクトル + キーワード)
// より精度の高い検索
export async function hybridSearch(query: string, topK: number = 5) {
const index = await getIndex()
// ベクトル検索
const queryEmbedding = await createEmbedding(query)
const vectorResults = await index.query({
vector: queryEmbedding,
topK,
includeMetadata: true,
})
// キーワード検索(メタデータフィルタリング)
// 実際にはElasticsearchなどと組み合わせることが多い
return vectorResults.matches
}
2. チャンキング戦略
長い文書を適切なサイズに分割することが重要です。
// lib/chunking.ts
export function chunkText(text: string, chunkSize: number = 500, overlap: number = 50) {
const chunks: string[] = []
let start = 0
while (start < text.length) {
const end = Math.min(start + chunkSize, text.length)
chunks.push(text.substring(start, end))
start = end - overlap // オーバーラップを設定
}
return chunks
}
3. メタデータフィルタリング
// 日付やカテゴリでフィルタリング
const searchResults = await index.query({
vector: queryEmbedding,
topK: 5,
filter: {
category: { $eq: 'technical' },
publishedAt: { $gte: '2024-01-01' },
},
includeMetadata: true,
})
まとめ
RAG は、AI アプリケーションにおいて最も実用的で重要な技術の一つです。Next.js と TypeScript を使うことで、型安全で保守性の高い RAG システムを構築できます。
重要なポイント:
- ハルシネーションの削減: 実際の文書を参照するため、正確な回答が可能
- 最新情報の活用: データベースを更新すれば、常に最新情報を提供できる
- プライベートデータ対応: 社内文書など、LLM が学習していないデータも活用可能
- ソースの明示: どの文書から情報を得たか追跡可能
- コスト効率: ファインチューニング不要で、特定ドメインに対応できる
RAG を活用することで、企業の知識ベース、製品マニュアル、カスタマーサポートなど、様々な用途で AI を実用化できます。