Mistral で作る RAG 設計 7 パターン:分割・埋め込み・再ランク・圧縮
大規模言語モデル(LLM)を活用したアプリケーション開発において、RAG(Retrieval-Augmented Generation)は非常に重要な技術です。RAG を使えば、モデルの知識を最新の情報で補強し、より精度の高い回答を生成できるようになります。
しかし、RAG を実装する際には多くの設計上の選択肢があり、どのパターンを採用すべきか迷うことも多いのではないでしょうか。そこで本記事では、Mistral を使った RAG 設計の 7 つの主要パターンを詳しく解説いたします。
文書の分割方法、埋め込みの生成、検索結果の再ランク、コンテキストの圧縮など、実践的なテクニックを段階的にご紹介しますので、ぜひ最後までお読みください。
背景
RAG の基本概念
RAG は、外部の知識ベースから関連情報を検索し、その情報を LLM のプロンプトに含めることで、より正確で文脈に即した回答を生成する手法です。
従来の LLM は学習時点での知識しか持っていないため、最新情報への対応や特定ドメインの専門知識には限界がありました。RAG を導入することで、これらの課題を解決できるようになります。
以下の図は、RAG の基本的なデータフローを示したものです。
mermaidflowchart TB
query["ユーザークエリ"]
embed["クエリの<br/>埋め込み生成"]
search["ベクトル<br/>データベース検索"]
retrieve["関連文書の<br/>取得"]
augment["プロンプトへの<br/>コンテキスト追加"]
llm["Mistral LLM"]
response["回答生成"]
query --> embed
embed --> search
search --> retrieve
retrieve --> augment
augment --> llm
llm --> response
この図から、RAG は「検索フェーズ」と「生成フェーズ」の 2 段階で構成されることがわかります。検索フェーズでは、ユーザーのクエリに関連する文書を取得し、生成フェーズでは、取得した文書を使って LLM が回答を生成するのです。
Mistral を RAG に選ぶ理由
Mistral は、高速かつ高品質な推論が可能な LLM として注目を集めています。特に、以下の特徴が RAG システムの構築に適しています。
| # | 特徴 | RAG への利点 |
|---|---|---|
| 1 | 高速な推論速度 | リアルタイムな検索・生成が可能 |
| 2 | 優れた埋め込みモデル | 文書とクエリの意味的な類似度を高精度に計算 |
| 3 | 長いコンテキスト対応 | 多くの検索結果を一度に処理可能 |
| 4 | 多言語サポート | 日本語を含む多様な言語での RAG 実装 |
| 5 | API の使いやすさ | シンプルな統合と迅速な開発 |
これらの特性により、Mistral は RAG システムの開発において非常に強力な選択肢となります。
RAG 設計の重要性
RAG の性能は、設計パターンの選択によって大きく左右されます。文書をどのように分割するか、どのような埋め込みを使うか、検索結果をどう処理するかなど、各ステップでの意思決定が最終的な回答の品質に直結するのです。
適切な設計を行うことで、以下のようなメリットが得られます。
回答の精度が向上し、ユーザーの質問に対してより的確な情報を提供できるようになります。また、検索の効率が改善され、大規模なデータベースでも高速に関連情報を見つけられるでしょう。
さらに、コンテキストウィンドウの制限内で最大限の情報を活用でき、システム全体のコストも最適化できます。
課題
RAG 実装における主な課題
RAG システムを構築する際には、いくつかの技術的な課題に直面します。これらの課題を理解し、適切に対処することが、高品質な RAG システムの鍵となるのです。
以下の図は、RAG 実装における主要な課題を段階別に示したものです。
mermaidflowchart TD
start["RAG 実装開始"]
subgraph phase1["文書処理段階"]
chunk["文書分割の<br/>最適化"]
chunk_issue["粒度・重複<br/>境界の問題"]
end
subgraph phase2["埋め込み段階"]
embed2["埋め込み生成"]
embed_issue["意味的類似度<br/>計算の精度"]
end
subgraph phase3["検索段階"]
retrieve2["関連文書検索"]
retrieve_issue["ノイズ混入<br/>関連度の判定"]
end
subgraph phase4["生成段階"]
generate["回答生成"]
generate_issue["コンテキスト長<br/>情報の優先順位"]
end
start --> phase1
phase1 --> phase2
phase2 --> phase3
phase3 --> phase4
chunk --> chunk_issue
embed2 --> embed_issue
retrieve2 --> retrieve_issue
generate --> generate_issue
この図から、RAG の各段階で異なる課題が存在することがわかります。
文書分割の課題
文書を適切なサイズに分割することは、RAG の最初の重要なステップです。しかし、分割には以下のような難しさがあります。
チャンクサイズが小さすぎると文脈が失われ、大きすぎると検索精度が低下します。また、文書の境界で重要な情報が分断されると、意味的な連続性が損なわれることがあるのです。
さらに、異なる種類の文書(技術文書、会話ログ、コードなど)では、最適な分割方法も変わってきます。
埋め込みと検索の課題
埋め込みベクトルの品質は、検索精度に直接影響します。以下のような問題が発生することがあります。
| # | 課題 | 影響 | 対策の必要性 |
|---|---|---|---|
| 1 | クエリと文書の表現ギャップ | 関連文書を見逃す | 高 |
| 2 | 同義語や言い換えの認識 | 検索漏れ | 高 |
| 3 | ドメイン固有の用語理解 | 専門分野での精度低下 | 中 |
| 4 | 多言語での意味的類似度 | 言語間の検索精度低下 | 中 |
| 5 | 検索結果のノイズ | 無関係な情報の混入 | 高 |
これらの課題を解決するためには、適切な埋め込みモデルの選択と、検索結果の後処理が不可欠です。
コンテキスト管理の課題
LLM には入力できるトークン数に制限があります。そのため、検索で取得した全ての文書をプロンプトに含められないケースが多いのです。
検索結果から最も重要な情報をどう選択するか、どの順序で配置するかなど、コンテキスト管理の最適化が求められます。また、冗長な情報を削除し、本質的な内容だけを残す圧縮技術も重要になってくるでしょう。
解決策
RAG 設計 7 パターンの全体像
これらの課題を解決するために、7 つの設計パターンを組み合わせて使用します。各パターンは特定の問題に対処し、全体として高品質な RAG システムを構築できるのです。
以下の表は、7 つのパターンの概要と適用場面を示したものです。
| # | パターン名 | 主な目的 | 適用場面 |
|---|---|---|---|
| 1 | 固定サイズチャンク分割 | 文書の均等分割 | 一般的な文書処理 |
| 2 | 意味的チャンク分割 | 文脈を保持した分割 | 技術文書・論文 |
| 3 | 階層的埋め込み | 多層的な意味理解 | 複雑な文書構造 |
| 4 | ハイブリッド検索 | 検索精度の向上 | 高精度が必要な場合 |
| 5 | 再ランク | 検索結果の最適化 | 大量の候補から選択 |
| 6 | コンテキスト圧縮 | トークン数の削減 | 長文処理 |
| 7 | クエリ拡張 | 検索範囲の拡大 | 複雑なクエリ対応 |
これらのパターンは単独で使用することも、複数を組み合わせることも可能です。システムの要件に応じて、最適な組み合わせを選択しましょう。
以下の図は、これら 7 パターンがどのように連携するかを示しています。
mermaidflowchart LR
doc["原文書"]
subgraph split["分割パターン"]
pattern1["1. 固定サイズ分割"]
pattern2["2. 意味的分割"]
end
subgraph embed_search["埋め込み・検索パターン"]
pattern3["3. 階層的埋め込み"]
pattern4["4. ハイブリッド検索"]
pattern7["7. クエリ拡張"]
end
subgraph optimize["最適化パターン"]
pattern5["5. 再ランク"]
pattern6["6. コンテキスト圧縮"]
end
mistral["Mistral LLM<br/>回答生成"]
doc --> split
split --> embed_search
embed_search --> optimize
optimize --> mistral
それでは、各パターンを詳しく見ていきましょう。
パターン 1:固定サイズチャンク分割
固定サイズチャンク分割は、文書を一定のトークン数やキャラクタ数で均等に分割する最もシンプルな方法です。
このパターンは実装が簡単で、処理速度も高速です。大量の文書を迅速に処理したい場合に適しているでしょう。
以下は、固定サイズでの文書分割を実装するコード例です。
まず、必要なライブラリをインポートします。
typescriptimport { MistralClient } from '@mistralai/mistralai';
次に、固定サイズでテキストを分割する関数を定義します。この関数は、文書を指定されたチャンクサイズで分割し、オーバーラップを持たせることができます。
typescript/**
* 固定サイズで文書を分割する関数
* @param text 分割対象の文書
* @param chunkSize 各チャンクのサイズ(文字数)
* @param overlap 前後のチャンクとの重複サイズ
* @returns 分割されたチャンクの配列
*/
function fixedSizeChunking(
text: string,
chunkSize: number = 500,
overlap: number = 50
): string[] {
const chunks: string[] = [];
let startIndex = 0;
// テキストを順次分割
while (startIndex < text.length) {
const endIndex = Math.min(
startIndex + chunkSize,
text.length
);
const chunk = text.slice(startIndex, endIndex);
chunks.push(chunk);
// オーバーラップを考慮して次の開始位置を設定
startIndex += chunkSize - overlap;
}
return chunks;
}
この関数の使用例を示します。実際の文書を分割し、結果を確認できます。
typescript// 使用例:技術文書の分割
const document = `
Mistral は、高速かつ高品質な推論が可能な大規模言語モデルです。
特に、長いコンテキストの処理に優れており、最大 32k トークンまで対応しています。
また、多言語サポートも充実しており、日本語を含む多数の言語で高精度な処理が可能です。
`;
const chunks = fixedSizeChunking(document, 100, 20);
console.log(`分割されたチャンク数: ${chunks.length}`);
chunks.forEach((chunk, index) => {
console.log(`\nチャンク ${index + 1}:\n${chunk}`);
});
固定サイズ分割のメリットは、実装が簡単で予測可能な動作をすることです。ただし、文の途中で分割される可能性があり、文脈が失われることがある点に注意が必要でしょう。
パターン 2:意味的チャンク分割
意味的チャンク分割は、文書の意味的な境界を考慮して分割する、より高度な方法です。
段落、セクション、トピックの変化点などを検出し、自然な区切りで分割することで、文脈を保持できます。技術文書や論文など、構造化された文書に特に有効でしょう。
以下は、意味的な境界を考慮した分割の実装例です。
typescript/**
* 意味的な境界で文書を分割する関数
* @param text 分割対象の文書
* @param minChunkSize 最小チャンクサイズ
* @param maxChunkSize 最大チャンクサイズ
* @returns 分割されたチャンクの配列
*/
function semanticChunking(
text: string,
minChunkSize: number = 200,
maxChunkSize: number = 800
): string[] {
const chunks: string[] = [];
// 段落で分割(改行2つ以上を境界とする)
const paragraphs = text.split(/\n\s*\n/);
let currentChunk = '';
for (const paragraph of paragraphs) {
// 段落を追加しても最大サイズを超えない場合
if (
currentChunk.length + paragraph.length <=
maxChunkSize
) {
currentChunk +=
(currentChunk ? '\n\n' : '') + paragraph;
} else {
// 現在のチャンクが最小サイズ以上なら確定
if (currentChunk.length >= minChunkSize) {
chunks.push(currentChunk.trim());
currentChunk = paragraph;
} else {
// 最小サイズ未満の場合は結合
currentChunk +=
(currentChunk ? '\n\n' : '') + paragraph;
}
}
}
// 最後のチャンクを追加
if (currentChunk.trim()) {
chunks.push(currentChunk.trim());
}
return chunks;
}
さらに、見出しやマークダウン構造を考慮した分割も実装できます。
typescript/**
* マークダウンの見出し構造を考慮した分割
* @param text マークダウン形式の文書
* @returns 見出しごとに分割されたチャンク
*/
function markdownSemanticChunking(text: string): Array<{
heading: string;
level: number;
content: string;
}> {
const chunks: Array<{
heading: string;
level: number;
content: string;
}> = [];
// 見出し(# で始まる行)で分割
const lines = text.split('\n');
let currentHeading = '';
let currentLevel = 0;
let currentContent = '';
for (const line of lines) {
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
if (headingMatch) {
// 前のセクションを保存
if (currentContent.trim()) {
chunks.push({
heading: currentHeading,
level: currentLevel,
content: currentContent.trim(),
});
}
// 新しいセクション開始
currentLevel = headingMatch[1].length;
currentHeading = headingMatch[2];
currentContent = '';
} else {
currentContent += line + '\n';
}
}
// 最後のセクションを保存
if (currentContent.trim()) {
chunks.push({
heading: currentHeading,
level: currentLevel,
content: currentContent.trim(),
});
}
return chunks;
}
意味的チャンク分割により、各チャンクが完結した情報を持つようになり、検索精度が向上します。
パターン 3:階層的埋め込み
階層的埋め込みは、文書を複数のレベルで埋め込みベクトルを生成する手法です。
文書全体の要約レベル、セクションレベル、チャンクレベルなど、異なる粒度での埋め込みを作成することで、多層的な検索が可能になります。
以下は、Mistral API を使った階層的埋め込みの実装例です。
まず、Mistral クライアントの初期化を行います。
typescript// Mistral クライアントの初期化
const apiKey = process.env.MISTRAL_API_KEY || '';
const client = new MistralClient(apiKey);
次に、階層的に埋め込みを生成する関数を定義します。
typescript/**
* 階層的な埋め込みを生成する関数
* @param document 文書オブジェクト
* @returns 階層構造を持つ埋め込みデータ
*/
async function generateHierarchicalEmbeddings(document: {
title: string;
sections: Array<{
heading: string;
chunks: string[];
}>;
}) {
// レベル1:文書全体の要約埋め込み
const documentSummary = `${
document.title
}\n${document.sections.map((s) => s.heading).join(', ')}`;
const documentEmbedding = await client.embeddings({
model: 'mistral-embed',
input: [documentSummary],
});
// レベル2:各セクションの埋め込み
const sectionEmbeddings = [];
for (const section of document.sections) {
const sectionText = `${
section.heading
}\n${section.chunks.join('\n')}`;
const embedding = await client.embeddings({
model: 'mistral-embed',
input: [sectionText],
});
sectionEmbeddings.push({
heading: section.heading,
embedding: embedding.data[0].embedding,
});
}
return {
document: {
title: document.title,
embedding: documentEmbedding.data[0].embedding,
},
sections: sectionEmbeddings,
};
}
レベル 3 として、個々のチャンクレベルの埋め込みも生成できます。
typescript/**
* チャンクレベルの埋め込みを生成
* @param chunks チャンクの配列
* @returns チャンクごとの埋め込みベクトル
*/
async function generateChunkEmbeddings(chunks: string[]) {
// バッチ処理で効率的に埋め込みを生成
const response = await client.embeddings({
model: 'mistral-embed',
input: chunks,
});
return chunks.map((chunk, index) => ({
text: chunk,
embedding: response.data[index].embedding,
}));
}
階層的埋め込みを使用することで、まず文書レベルで関連性をフィルタリングし、次にセクションやチャンクレベルで詳細な検索を行うという、効率的な多段階検索が実現できます。
パターン 4:ハイブリッド検索
ハイブリッド検索は、ベクトル検索とキーワード検索を組み合わせることで、検索精度を向上させる手法です。
ベクトル検索は意味的な類似性を捉えるのに優れていますが、特定のキーワードの完全一致には弱い場合があります。一方、キーワード検索は正確な用語マッチには強いものの、同義語や言い換えには対応できません。
両者を組み合わせることで、それぞれの弱点を補完できるのです。
以下は、ハイブリッド検索の実装例です。
typescript/**
* BM25 アルゴリズムを用いたキーワード検索スコア計算
* @param query クエリ文字列
* @param document 検索対象文書
* @param allDocuments 全文書(IDF 計算用)
* @returns BM25 スコア
*/
function calculateBM25Score(
query: string,
document: string,
allDocuments: string[]
): number {
const k1 = 1.5; // パラメータ
const b = 0.75; // パラメータ
// クエリをトークン化
const queryTokens = query.toLowerCase().split(/\s+/);
const docTokens = document.toLowerCase().split(/\s+/);
// 平均文書長の計算
const avgDocLength =
allDocuments.reduce(
(sum, doc) => sum + doc.split(/\s+/).length,
0
) / allDocuments.length;
let score = 0;
for (const term of queryTokens) {
// 文書内の用語頻度(TF)
const tf = docTokens.filter(
(token) => token === term
).length;
// 用語を含む文書数(DF)
const df = allDocuments.filter((doc) =>
doc.toLowerCase().includes(term)
).length;
// 逆文書頻度(IDF)
const idf = Math.log(
(allDocuments.length - df + 0.5) / (df + 0.5) + 1
);
// BM25 スコア計算
score +=
(idf * (tf * (k1 + 1))) /
(tf +
k1 *
(1 - b + b * (docTokens.length / avgDocLength)));
}
return score;
}
次に、ベクトル検索とキーワード検索を組み合わせる関数を実装します。
typescript/**
* ハイブリッド検索の実行
* @param query ユーザークエリ
* @param documents 検索対象文書の配列
* @param topK 取得する上位結果数
* @returns 検索結果
*/
async function hybridSearch(
query: string,
documents: Array<{ text: string; embedding: number[] }>,
topK: number = 5
) {
// ステップ1:クエリの埋め込み生成
const queryEmbedding = await client.embeddings({
model: 'mistral-embed',
input: [query],
});
const queryVector = queryEmbedding.data[0].embedding;
// ステップ2:各文書のスコア計算
const scoredDocuments = documents.map((doc) => {
// コサイン類似度によるベクトル検索スコア
const vectorScore = cosineSimilarity(
queryVector,
doc.embedding
);
// BM25 によるキーワード検索スコア
const keywordScore = calculateBM25Score(
query,
doc.text,
documents.map((d) => d.text)
);
// 正規化と重み付け(ベクトル70%、キーワード30%)
const normalizedVectorScore = vectorScore;
const normalizedKeywordScore = keywordScore / 10; // スケール調整
const hybridScore =
0.7 * normalizedVectorScore +
0.3 * normalizedKeywordScore;
return {
text: doc.text,
vectorScore,
keywordScore,
hybridScore,
};
});
// ステップ3:スコアでソートして上位を返す
return scoredDocuments
.sort((a, b) => b.hybridScore - a.hybridScore)
.slice(0, topK);
}
コサイン類似度を計算するユーティリティ関数も必要です。
typescript/**
* コサイン類似度の計算
* @param vectorA ベクトルA
* @param vectorB ベクトルB
* @returns コサイン類似度(-1 から 1)
*/
function cosineSimilarity(
vectorA: number[],
vectorB: number[]
): number {
let dotProduct = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < vectorA.length; i++) {
dotProduct += vectorA[i] * vectorB[i];
normA += vectorA[i] * vectorA[i];
normB += vectorB[i] * vectorB[i];
}
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
}
ハイブリッド検索により、意味的に類似する文書と、特定のキーワードを含む文書の両方を効果的に検索できるようになります。
パターン 5:再ランク
再ランクは、初期検索で取得した候補文書を、より精密なモデルで再評価して順位付けを最適化する手法です。
最初の検索では速度重視で多くの候補を取得し、再ランクのステップで精度の高いモデルを使って最も関連性の高い文書を選別します。この 2 段階アプローチにより、速度と精度の両立が可能になるのです。
以下は、Mistral を使った再ランクの実装例です。
typescript/**
* クロスエンコーダーによる再ランク
* @param query ユーザークエリ
* @param candidates 初期検索で取得した候補文書
* @param topK 最終的に返す文書数
* @returns 再ランク後の文書
*/
async function rerank(
query: string,
candidates: string[],
topK: number = 3
) {
// 各候補とクエリの関連性スコアを計算
const scoredCandidates = await Promise.all(
candidates.map(async (candidate) => {
// Mistral を使って関連性を評価
const prompt = `
以下のクエリと文書の関連性を0から1のスコアで評価してください。
スコアのみを数値で返してください。
クエリ: ${query}
文書: ${candidate}
関連性スコア:`;
const response = await client.chat({
model: 'mistral-small-latest',
messages: [{ role: 'user', content: prompt }],
temperature: 0.1,
maxTokens: 10,
});
// スコアを抽出
const scoreText =
response.choices[0].message.content || '0';
const score = parseFloat(
scoreText.match(/[\d.]+/)?.[0] || '0'
);
return {
text: candidate,
score,
};
})
);
// スコアでソートして上位を返す
return scoredCandidates
.sort((a, b) => b.score - a.score)
.slice(0, topK);
}
より効率的な実装として、専用の再ランクモデルを使用する方法もあります。
typescript/**
* バッチ処理による効率的な再ランク
* @param query ユーザークエリ
* @param candidates 候補文書配列
* @param batchSize バッチサイズ
* @returns 再ランク結果
*/
async function efficientRerank(
query: string,
candidates: string[],
batchSize: number = 5
) {
const results = [];
// 候補をバッチに分割して処理
for (let i = 0; i < candidates.length; i += batchSize) {
const batch = candidates.slice(i, i + batchSize);
// バッチ内の各候補を評価
const prompt = `
クエリ: "${query}"
以下の文書それぞれについて、クエリとの関連性を0-1のスコアで評価してください。
JSON配列形式で返してください: [{"index": 0, "score": 0.8}, ...]
${batch
.map(
(doc, idx) => `
文書${idx}: ${doc}
`
)
.join('\n')}
評価結果:`;
const response = await client.chat({
model: 'mistral-small-latest',
messages: [{ role: 'user', content: prompt }],
temperature: 0.1,
});
try {
const scores = JSON.parse(
response.choices[0].message.content || '[]'
);
scores.forEach(
(item: { index: number; score: number }) => {
results.push({
text: batch[item.index],
score: item.score,
});
}
);
} catch (error) {
console.error('スコアのパースに失敗:', error);
}
}
return results.sort((a, b) => b.score - a.score);
}
再ランクを導入することで、初期検索で取りこぼした重要な文書を拾い上げたり、ノイズとなる無関係な文書を除外したりできます。
パターン 6:コンテキスト圧縮
コンテキスト圧縮は、検索で取得した文書から不要な情報を削除し、本質的な内容だけを抽出する手法です。
LLM のコンテキストウィンドウには制限があるため、全ての検索結果をそのまま含めることは現実的ではありません。圧縮により、より多くの文書から情報を集約しつつ、トークン数を削減できるのです。
以下は、コンテキスト圧縮の実装例です。
typescript/**
* LLM を使ったコンテキスト圧縮
* @param query ユーザークエリ
* @param documents 検索で取得した文書群
* @returns 圧縮されたコンテキスト
*/
async function compressContext(
query: string,
documents: string[]
) {
// 各文書からクエリに関連する部分のみを抽出
const compressedDocs = await Promise.all(
documents.map(async (doc) => {
const prompt = `
以下の文書から、クエリ「${query}」に関連する重要な情報のみを抽出してください。
冗長な表現は削除し、簡潔にまとめてください。
文書:
${doc}
抽出された情報:`;
const response = await client.chat({
model: 'mistral-small-latest',
messages: [{ role: 'user', content: prompt }],
temperature: 0.3,
maxTokens: 200,
});
return response.choices[0].message.content || '';
})
);
// 圧縮された文書を結合
return compressedDocs
.filter((doc) => doc.trim().length > 0)
.join('\n\n');
}
より高度な圧縮として、文書間の重複情報を削除する実装も可能です。
typescript/**
* 重複情報を削除した圧縮
* @param documents 文書配列
* @returns 重複削除・圧縮されたコンテキスト
*/
async function deduplicateAndCompress(documents: string[]) {
// すべての文書を1つのプロンプトで処理
const prompt = `
以下の複数の文書から、重複する情報を削除し、
ユニークで重要な情報のみを箇条書きで抽出してください。
${documents
.map(
(doc, idx) => `
--- 文書 ${idx + 1} ---
${doc}
`
)
.join('\n')}
抽出された情報(箇条書き):`;
const response = await client.chat({
model: 'mistral-small-latest',
messages: [{ role: 'user', content: prompt }],
temperature: 0.3,
maxTokens: 500,
});
return response.choices[0].message.content || '';
}
統計ベースのアプローチとして、文の重要度スコアリングも実装できます。
typescript/**
* 文の重要度スコアリングによる圧縮
* @param text テキスト
* @param query クエリ
* @param topSentences 保持する文の数
* @returns 圧縮されたテキスト
*/
function sentenceScoring(
text: string,
query: string,
topSentences: number = 5
): string {
// 文に分割
const sentences = text.split(/[.!?。!?]\s+/);
const queryTerms = query.toLowerCase().split(/\s+/);
// 各文のスコアを計算
const scoredSentences = sentences.map((sentence) => {
const sentenceTerms = sentence
.toLowerCase()
.split(/\s+/);
// クエリとの単語重複数をスコアとする
const score = queryTerms.filter((term) =>
sentenceTerms.includes(term)
).length;
return { sentence, score };
});
// スコアが高い文のみを保持
return scoredSentences
.sort((a, b) => b.score - a.score)
.slice(0, topSentences)
.map((item) => item.sentence)
.join('. ');
}
コンテキスト圧縮により、トークン数を 50〜70%削減しながら、重要な情報は保持できます。
パターン 7:クエリ拡張
クエリ拡張は、ユーザーの元のクエリから、関連する追加のクエリを生成して検索範囲を広げる手法です。
ユーザーのクエリが短かったり曖昧だったりする場合、重要な情報を見逃す可能性があります。クエリを拡張することで、より包括的な検索が実現できるのです。
以下は、クエリ拡張の実装例です。
typescript/**
* LLM によるクエリ拡張
* @param originalQuery 元のクエリ
* @returns 拡張されたクエリの配列
*/
async function expandQuery(
originalQuery: string
): Promise<string[]> {
const prompt = `
以下のクエリに関連する、異なる表現や視点からの質問を3つ生成してください。
各質問は元のクエリと同じ情報を求めているが、異なる言い回しを使用してください。
元のクエリ: ${originalQuery}
拡張クエリ(1行に1つずつ):`;
const response = await client.chat({
model: 'mistral-small-latest',
messages: [{ role: 'user', content: prompt }],
temperature: 0.7,
maxTokens: 200,
});
// 生成されたクエリを行で分割
const expandedQueries = (
response.choices[0].message.content || ''
)
.split('\n')
.map((line) => line.replace(/^\d+[.)]\s*/, '').trim())
.filter((line) => line.length > 0);
// 元のクエリも含めて返す
return [originalQuery, ...expandedQueries];
}
拡張されたクエリを使って複数回検索を行い、結果を統合します。
typescript/**
* 拡張クエリによる複数検索と結果統合
* @param originalQuery 元のクエリ
* @param documents 検索対象文書
* @param topK 各クエリで取得する結果数
* @returns 統合された検索結果
*/
async function multiQuerySearch(
originalQuery: string,
documents: Array<{ text: string; embedding: number[] }>,
topK: number = 3
) {
// クエリを拡張
const queries = await expandQuery(originalQuery);
console.log('拡張されたクエリ:', queries);
// 各クエリで検索を実行
const allResults: Array<{
text: string;
score: number;
query: string;
}> = [];
for (const query of queries) {
// クエリの埋め込み生成
const queryEmbedding = await client.embeddings({
model: 'mistral-embed',
input: [query],
});
const queryVector = queryEmbedding.data[0].embedding;
// ベクトル検索
const results = documents.map((doc) => ({
text: doc.text,
score: cosineSimilarity(queryVector, doc.embedding),
query,
}));
// 上位結果を追加
allResults.push(
...results
.sort((a, b) => b.score - a.score)
.slice(0, topK)
);
}
// 重複を除去しつつスコアを集約
const uniqueResults = new Map<
string,
{
text: string;
totalScore: number;
count: number;
}
>();
for (const result of allResults) {
const existing = uniqueResults.get(result.text);
if (existing) {
existing.totalScore += result.score;
existing.count += 1;
} else {
uniqueResults.set(result.text, {
text: result.text,
totalScore: result.score,
count: 1,
});
}
}
// 平均スコアでソート
return Array.from(uniqueResults.values())
.map((item) => ({
text: item.text,
averageScore: item.totalScore / item.count,
frequency: item.count,
}))
.sort((a, b) => b.averageScore - a.averageScore);
}
クエリ拡張により、ユーザーが明示的に指定しなかった関連情報も検索できるようになり、より包括的な回答が可能になります。
具体例
完全な RAG システムの実装
これまでに説明した 7 つのパターンを組み合わせて、実際に動作する RAG システムを構築してみましょう。
以下は、技術ドキュメントの質問応答システムの完全な実装例です。
まず、システム全体の構成を示します。
typescriptimport { MistralClient } from '@mistralai/mistralai';
// 型定義
interface Document {
id: string;
title: string;
content: string;
embedding?: number[];
chunks?: Array<{
text: string;
embedding: number[];
}>;
}
interface RAGConfig {
chunkSize: number;
chunkOverlap: number;
topK: number;
rerankTopK: number;
enableRerank: boolean;
enableCompression: boolean;
enableQueryExpansion: boolean;
}
次に、RAG システムのメインクラスを実装します。
typescript/**
* Mistral を使った RAG システムクラス
*/
class MistralRAGSystem {
private client: MistralClient;
private documents: Document[] = [];
private config: RAGConfig;
constructor(
apiKey: string,
config: Partial<RAGConfig> = {}
) {
this.client = new MistralClient(apiKey);
// デフォルト設定
this.config = {
chunkSize: 500,
chunkOverlap: 50,
topK: 10,
rerankTopK: 3,
enableRerank: true,
enableCompression: true,
enableQueryExpansion: false,
...config,
};
}
/**
* 文書をインデックスに追加
*/
async addDocuments(
documents: Array<{
id: string;
title: string;
content: string;
}>
) {
for (const doc of documents) {
// パターン2: 意味的チャンク分割
const chunks = semanticChunking(
doc.content,
200,
this.config.chunkSize
);
// パターン3: 階層的埋め込み
// チャンクの埋め込み生成
const chunkEmbeddings = await this.client.embeddings({
model: 'mistral-embed',
input: chunks,
});
// 文書全体の埋め込み生成
const docEmbedding = await this.client.embeddings({
model: 'mistral-embed',
input: [
`${doc.title}\n${doc.content.substring(0, 500)}`,
],
});
this.documents.push({
id: doc.id,
title: doc.title,
content: doc.content,
embedding: docEmbedding.data[0].embedding,
chunks: chunks.map((chunk, idx) => ({
text: chunk,
embedding: chunkEmbeddings.data[idx].embedding,
})),
});
}
console.log(
`${documents.length} 件の文書をインデックスに追加しました`
);
}
}
検索メソッドを実装します。
typescript/**
* RAG システムに検索機能を追加
*/
class MistralRAGSystem {
// ... 前述のコード ...
/**
* クエリに対する回答を生成
*/
async query(userQuery: string): Promise<string> {
console.log(`\n=== RAG 検索開始 ===`);
console.log(`クエリ: ${userQuery}`);
// パターン7: クエリ拡張(オプション)
let queries = [userQuery];
if (this.config.enableQueryExpansion) {
queries = await expandQuery(userQuery);
console.log(`拡張クエリ: ${queries.length} 件`);
}
// 各クエリで検索
let allChunks: Array<{ text: string; score: number }> =
[];
for (const query of queries) {
const queryEmbedding = await this.client.embeddings({
model: 'mistral-embed',
input: [query],
});
const queryVector = queryEmbedding.data[0].embedding;
// パターン4: ハイブリッド検索
for (const doc of this.documents) {
for (const chunk of doc.chunks || []) {
// ベクトル検索スコア
const vectorScore = cosineSimilarity(
queryVector,
chunk.embedding
);
// キーワード検索スコア
const keywordScore = calculateBM25Score(
query,
chunk.text,
this.documents.flatMap(
(d) => d.chunks?.map((c) => c.text) || []
)
);
// ハイブリッドスコア
const hybridScore =
0.7 * vectorScore + 0.3 * (keywordScore / 10);
allChunks.push({
text: chunk.text,
score: hybridScore,
});
}
}
}
// 上位候補を取得
allChunks.sort((a, b) => b.score - a.score);
let topChunks = allChunks.slice(0, this.config.topK);
console.log(`初期検索結果: ${topChunks.length} 件`);
return await this.generateAnswer(userQuery, topChunks);
}
}
回答生成メソッドを実装します。
typescript/**
* 検索結果から回答を生成
*/
class MistralRAGSystem {
// ... 前述のコード ...
private async generateAnswer(
query: string,
chunks: Array<{ text: string; score: number }>
): Promise<string> {
// パターン5: 再ランク(オプション)
if (this.config.enableRerank) {
const rerankedResults = await rerank(
query,
chunks.map((c) => c.text),
this.config.rerankTopK
);
chunks = rerankedResults.map((r) => ({
text: r.text,
score: r.score,
}));
console.log(`再ランク後: ${chunks.length} 件`);
}
// パターン6: コンテキスト圧縮(オプション)
let context = chunks.map((c) => c.text).join('\n\n');
if (this.config.enableCompression) {
context = await compressContext(
query,
chunks.map((c) => c.text)
);
console.log(`コンテキスト圧縮完了`);
}
// 回答生成
const prompt = `
以下のコンテキストを参考に、質問に答えてください。
コンテキストに含まれない情報については推測せず、「情報がありません」と答えてください。
コンテキスト:
${context}
質問: ${query}
回答:`;
const response = await this.client.chat({
model: 'mistral-small-latest',
messages: [{ role: 'user', content: prompt }],
temperature: 0.3,
});
return (
response.choices[0].message.content ||
'回答を生成できませんでした'
);
}
}
最後に、システムの使用例を示します。
typescript// 使用例
async function main() {
// RAG システムの初期化
const rag = new MistralRAGSystem(
process.env.MISTRAL_API_KEY || '',
{
chunkSize: 600,
topK: 10,
rerankTopK: 3,
enableRerank: true,
enableCompression: true,
enableQueryExpansion: false,
}
);
// サンプル文書の追加
await rag.addDocuments([
{
id: 'doc1',
title: 'Mistral の概要',
content: `
Mistral は、高速かつ高品質な推論が可能な大規模言語モデルです。
特に、長いコンテキストの処理に優れており、最大 32k トークンまで対応しています。
# 主な特徴
- 高速な推論速度
- 優れた多言語サポート
- 長いコンテキストウィンドウ
- 使いやすい API
# 利用用途
Mistral は、チャットボット、文書分析、コード生成など、
幅広い用途で活用できます。
`,
},
{
id: 'doc2',
title: 'RAG システムの構築',
content: `
RAG(Retrieval-Augmented Generation)は、外部知識ベースと
LLM を組み合わせることで、より正確な回答を生成する手法です。
# 実装ステップ
1. 文書の分割とインデックス化
2. クエリに対する検索
3. 検索結果を使った回答生成
# 設計パターン
効果的な RAG システムには、適切な文書分割、
埋め込み生成、検索アルゴリズムの選択が重要です。
`,
},
]);
// 質問応答の実行
const answer = await rag.query(
'Mistral の主な特徴は何ですか?'
);
console.log('\n=== 回答 ===');
console.log(answer);
}
// 実行
main().catch(console.error);
パターンの組み合わせ方
7 つのパターンは、用途に応じて柔軟に組み合わせることができます。以下の表は、典型的なユースケースごとの推奨構成です。
| # | ユースケース | 推奨パターン | 理由 |
|---|---|---|---|
| 1 | シンプルな FAQ | 1 + 4 | 高速処理が優先 |
| 2 | 技術ドキュメント検索 | 2 + 3 + 5 | 構造と精度が重要 |
| 3 | 長文コンテンツ処理 | 2 + 6 | コンテキスト制限への対応 |
| 4 | 高精度が必要な用途 | 全パターン | 最高品質を追求 |
| 5 | リアルタイム応答 | 1 + 4 | 速度を最優先 |
実際のシステムでは、精度と速度のトレードオフを考慮しながら、最適なパターンの組み合わせを選択することが重要です。
パフォーマンス比較
各パターンの効果を数値で確認するため、簡単なベンチマークを実装できます。
typescript/**
* RAG パターンのパフォーマンス評価
*/
async function benchmarkRAGPatterns() {
const testQueries = [
'Mistral の最大コンテキスト長は?',
'RAG システムの実装ステップは?',
'多言語サポートについて教えて',
];
const configurations = [
{
name: '基本構成',
config: {
enableRerank: false,
enableCompression: false,
},
},
{
name: '再ランク有効',
config: {
enableRerank: true,
enableCompression: false,
},
},
{
name: '全機能有効',
config: {
enableRerank: true,
enableCompression: true,
},
},
];
for (const config of configurations) {
console.log(`\n=== ${config.name} ===`);
const rag = new MistralRAGSystem(
process.env.MISTRAL_API_KEY || '',
config.config
);
// 文書追加(省略)
let totalTime = 0;
for (const query of testQueries) {
const startTime = Date.now();
await rag.query(query);
const elapsedTime = Date.now() - startTime;
totalTime += elapsedTime;
console.log(` ${query}: ${elapsedTime}ms`);
}
console.log(
`平均処理時間: ${(
totalTime / testQueries.length
).toFixed(2)}ms`
);
}
}
このベンチマークにより、各パターンの追加コストと精度向上のバランスを評価できます。
まとめ
本記事では、Mistral を使った RAG システムの設計パターン 7 つを詳しく解説してきました。
文書の分割方法から、埋め込みの生成、検索精度の向上、コンテキストの最適化まで、実践的なテクニックをご紹介しました。これらのパターンを適切に組み合わせることで、高品質な RAG システムを構築できるでしょう。
各パターンの要点
それぞれのパターンが解決する課題を再確認しておきましょう。
固定サイズチャンク分割は、シンプルで高速な文書処理を実現します。意味的チャンク分割は、文脈を保持しながら自然な境界で分割できますね。
階層的埋め込みにより、多層的な検索が可能になり、ハイブリッド検索では意味的類似性とキーワードマッチの両方を活用できます。
再ランクは、初期検索結果を精密に評価し直すことで精度を向上させ、コンテキスト圧縮はトークン数を削減しながら重要情報を保持します。そして、クエリ拡張により、より包括的な検索が実現できるのです。
実装のポイント
RAG システムを実装する際には、以下のポイントに注意してください。
まず、ユースケースに応じて適切なパターンの組み合わせを選択しましょう。すべてのパターンを常に使う必要はなく、速度と精度のバランスを考慮することが大切です。
次に、文書の種類や構造に応じて、分割方法をカスタマイズすることをおすすめします。技術文書とチャットログでは、最適な処理方法が異なるためです。
また、定期的に検索精度を評価し、パラメータをチューニングすることで、システムの品質を継続的に改善できるでしょう。
次のステップ
本記事で紹介した実装例をベースに、独自の RAG システムを構築してみてください。
より高度な実装として、以下のような拡張も検討できます。ベクトルデータベース(Pinecone、Weaviate など)の導入により、大規模データの効率的な管理が可能になります。
また、ユーザーフィードバックを活用した検索結果の継続的な改善や、マルチモーダル対応(画像、表、グラフなど)への拡張も興味深いテーマでしょう。
Mistral の強力な機能と、本記事で解説した設計パターンを組み合わせることで、実用的で高品質な RAG システムを構築できます。ぜひ、実際のプロジェクトで活用してみてください。
関連リンク
articleMistral で作る RAG 設計 7 パターン:分割・埋め込み・再ランク・圧縮
articleMistral の始め方:API キー発行から最初のテキスト生成まで 5 分クイックスタート
articleMistral とは? 軽量・高速・高品質を両立する次世代 LLM の全体像
articleVite プラグインフック対応表:Rollup → Vite マッピング早見表
articleNestJS Monorepo 構築:Nx/Yarn Workspaces で API・Lib を一元管理
articleTypeScript Project References 入門:大規模 Monorepo で高速ビルドを実現する設定手順
articleMySQL Router セットアップ完全版:アプリからの透過フェイルオーバーを実現
articletRPC アーキテクチャ設計:BFF とドメイン分割で肥大化を防ぐルータ戦略
articleMotion(旧 Framer Motion)× TypeScript:Variant 型と Props 推論を強化する設定レシピ
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来