目次
- 概念の整理
- 技術的背景
- コンテキストウィンドウの構成要素
- 1ターンあたりの処理フロー
- コンテキストの動作アルゴリズム
- スライディングメカニズム
- 予算管理と数式モデル
- 具体例: 10ターン連続対話
- 実装視点: 疑似コード
- シーケンス図
- 実運用のベストプラクティス
概念の整理
基本用語
用語 | 意味 | 関係性 |
---|---|---|
コンテキスト | モデルが推論に使える参照情報の集合(指示、履歴、RAG結果、ツール出力など) | 包括的な概念 |
コンテキストウィンドウ | 1ターンの推論で入力として載せられる上限(トークン数) | コンテキストの「扱える量」を決める制約 |
パッキング (packing) | ウィンドウの中に、何をどの順番・粒度で詰めるかを決める処理 | ウィンドウ管理の核心 |
退避 (eviction) | 古い・冗長な履歴を外部化または削除する処理 | ウィンドウ容量確保 |
圧縮 (compaction) | 履歴を要約や参照ハンドルに置換する処理 | ウィンドウ効率化 |
作業机の比喩
LLMは「作業用の机(ワーキングメモリ)」を持っており、その机の上(ウィンドウ)に置ける資料が限られています。必要なものを取捨選択して置く必要があり、古い資料は要約してファイリングするか、棚(外部ストレージ)に戻す必要があります。
技術的背景
なぜウィンドウ制限があるのか?
1. トークン数の上限とアテンション計算
- Transformerベースのモデルは、入力をトークン列として扱い、self-attentionを計算
- 計算コスト: 入力長の二乗オーダー(O(n²))に近くなる
- 入力が長すぎるとモデル計算が現実的でなくなる
参考文献:
- letta.com: Context window anatomy
- IBM: Transformer architecture
- hopsworks.ai: Context management
2. 訓練時制約
- モデルが訓練されたときの最大コンテキスト長(シーケンス長)の制約
- それを大きく超える範囲では性能が劣化したりロジックが破綻
- LongRoPE2等の技術: RoPE(回転位置埋め込み)をスケールすることで長文脈対応
参考文献:
- arXiv: LongRoPE2研究
- arXiv: 最大コンテキスト長と実効コンテキスト長のギャップ研究
3. 計算資源・メモリ制約
- 長い入力 → GPUメモリを大量消費
- 推論遅延も増大
- コストとのトレードオフが重要
コンテキストウィンドウの構成要素
"アナトミー"視点での分解
コンポーネント | 役割 | 特記事項 |
---|---|---|
システムプロンプト/基本ルール | モデルの振る舞いを制御する初期指示 | 常にコンテキスト先頭に配置 |
対話履歴 | ユーザ発話/モデル応答の流れを記憶 | 古いものは適切に要約・除外されるべき |
補助ドキュメント/外部知見 | RAGや検索などで取り出した参照文書 | スニペット・要約化して挿入 |
ツール出力/ログ | 呼び出した計算、APIの戻り値、ログ断片 | 生データは重いため要約化・圧縮 |
ユーザ属性・環境設定 | 利用者の嗜好、過去決定、ポリシーなど | 副次的だが応答精度に大きく作用 |
制約・禁止事項 | 守らなければいけないルール、禁止パターン | 常に参照できるように維持 |
ウィンドウ内配置の基本方針
- Pinned(固定): システムプロンプト、定義済み・変化しない部分を常設
- 動的部分: 対話履歴や長文はスニペット化、要約して代表情報だけ残す
- 外部化: 詳細データは外部ストレージに保存し、必要時に取得
1ターンあたりの処理フロー
高解像度フロー図
各ステップの詳細
ステップ | 処理内容 | 目的 |
---|---|---|
B: 質問正規化 | 曖昧語("最新"など)を絶対日付に補正、出力形式を確定 | 意図の明確化 |
C〜E: 情報取得 | RAGやAPIの結果を要点化・ハイライト化してから候補化 | 情報の質を向上 |
F: パッキング | ウィンドウ割当(Pinned/History/Evidence/Scratch)を使い優先度順に詰める | リソース最適配分 |
G〜I: 調整 | 超過時は"古い・低価値・冗長"から要約/除外、詰め直しを繰り返す | ウィンドウ制約順守 |
K〜M: 保存 | ログはそのまま増やさず、節目で/compactにより「決定・根拠・制約」を固定化 | 長期記憶の構造化 |
コンテキストの動作アルゴリズム
予算割当(バジェット)の典型例
領域 | 割合 | 説明 |
---|---|---|
Pinned(目的・制約・出力契約・既決事項) | 20–35% | 変わらない定義や制約 |
History(直近数ターンの要旨) | 10–25% | 対話の流れを保持 |
Evidence(RAG/ツールの根拠スニペット) | 30–50% | 回答の根拠となる情報 |
Scratch(モデルの思考余白) | 15–30% | 生成に必要な残余 |
重要: Scratch(出力のための余白)が不足すると品質が落ちるため、最低枠を先に確保するのが実務では必須
メッセージパッキングの優先順位
- Pinned(固定)- 常に確保
- 現ターンのユーザ質問(正規化済み)
- 現ターンの根拠(Evidence)
- 直近の要旨履歴(History;古い順に削る)
- 参考ログ/補足(余裕があれば)
退避・圧縮(Eviction/Compaction)ポリシー
優先的に退避する順序
- 重複・反復の多い長文(ストリーム断片/同義反復)
- 古い履歴(一定ターン以上前)で再参照回数が少ないもの
- 低スコアのRAGスニペット(同一根拠の下位重複)
- 生ログ(長いツール出力や大型コード塊)→ 要約へ
退避先の形式
- 段階的に圧縮: 詳細 → 1行要旨 → 段落要旨 → 構造化要約
- 本文は外部ストレージに永続化(ベクトルDB、オブジェクトストレージ、messages.jsonl)
- ウィンドウには参照ハンドル(パス/ID)のみを残す
スライディングメカニズム
「古いものが消えて新しい場所へ移動する」詳細
ターン内スライド(直前→直後)
プロセス:
- 入力前: Historyには直近Nターンの要旨が入っている
- 推論直前: 新しいEvidenceを追加し、予算内に収まるまでHistory末尾から縮約・削除
- 出力後: このターンの問答を"短い要旨"にまとめ、Historyの先頭側に挿入
- 最古処理: 最も古い要旨は下位層(外部保存+参照ハンドル)に降格
区切りでの圧縮(/compact)
実行タイミング:
- フェーズ完了時
- 長文膨張時
- モデル切替前
処理内容:
- 「決定・根拠・制約・未解決」をまとめる
- Pinnedに昇格 or 別"固定要旨"として常設
- それ以前の詳細履歴は参照ハンドル化して普段は出さない
- 必要になればRAGで引き戻す
予算管理と数式モデル
予算計算の基本式
変数定義:
-
W
= 最大ウィンドウ(例: 16,000トークン) -
S
= Scratch最小保証(例: 3,000トークン) -
P
= Pinned固定(例: 2,000トークン) -
Emax
= Evidence上限(例: 7,000トークン) -
Hmax
= History上限 =W - S - P - E
の残り
パッキング処理フロー
段階的要約の粒度
レベル | 粒度 | 用途 |
---|---|---|
粗要約 | 1–3行 | 軽度の超過時 |
中要約 | 5–10行 | 中程度の超過時 |
精要約 | 構造化JSON | 重度の超過時 |
判断基準:
- 30%超過 → 粗要約に落とす
- 50%超過 → 中要約に落とす
- 70%超過 → 精要約+外部化
具体例: 10ターン連続対話
初期設定
- W = 16,000トークン
- P = 2,500トークン(目的・制約・出力契約+既決方針)
- S = 3,000トークン(余白)
- Emax = 7,000トークン
- Hmax = 3,500トークン(残り)
ターン別の推移
ターン1
Evidence: 3,000トークン
History: 0トークン
合計: 2,500 + 3,000 + 3,000 = 8,500トークン
状態: 余裕あり
処理: 出力後、ターン1の要旨(約300トークン)をHistoryへ
ターン4
Evidence: 6,500トークン
History: 3件 × 各300 = 900トークン
合計: 2,500 + 3,000 + 6,500 + 900 = 12,900トークン
状態: まだOK
ターン6(長文RAG多め)
Evidence: 9,000トークン → 超過!
処理:
1. 低スコアEvidenceを削除 → 7,000トークンへ
2. Historyを粗要約で半分に圧縮(3件 × 150 = 450トークン)
合計: 2,500 + 3,000 + 7,000 + 450 = 12,950トークン
状態: 収まる
ターン8(会話が長引く)
Evidence: 6,000トークン
History: 7件 × 各150 = 1,050トークン
合計: 2,500 + 3,000 + 6,000 + 1,050 = 12,550トークン
状態: OK
ターン10(区切り)
処理: /compact 実行
1. 「決定・根拠・制約・未解決」を再編
2. 重要決定の一部をPinnedに昇格(P: 2,500 → 2,800)
3. これ以前のHistory詳細は外部化
4. ハンドルだけ残す(History: 2件 × 150 = 300トークンに縮小)
結果: ウィンドウが大幅にクリーンアップされる
スライディングの可視化
実装視点: 疑似コード
コンテキスト組み立て関数
type Block = {
tokens: number;
text: string;
meta?: any
};
function assembleContext(input: {
pinned: Block, // 固定
userTurn: Block, // 正規化済み質問
evidences: Block[], // スコア降順
history: Block[], // 新→旧
windowMax: number, // W
scratchMin: number, // S
evidenceMax: number // Emax
}): Block[] {
let budget = input.windowMax - input.scratchMin;
const ctx: Block[] = [];
// 1) pinned を確保
pushWithBudget(ctx, input.pinned, budget);
// 2) current user turn を確保
pushWithBudget(ctx, input.userTurn, budget);
// 3) evidences を追加(evidenceMax を尊重)
let usedE = 0;
for (const ev of input.evidences) {
if (usedE + ev.tokens > input.evidenceMax) break;
pushWithBudget(ctx, ev, budget);
usedE += ev.tokens;
}
// 4) history を追加(新→旧の順)
for (const h of input.history) {
const ok = pushWithBudget(ctx, h, budget);
if (!ok) break; // 予算切れで終了
}
// 5) 超過していたら段階要約/退避して再試行
if (budget < 0) {
const reduced = compactAndEvict(
input.history,
input.evidences
);
return assembleContext({
...input,
...reduced
});
}
return ctx;
}
// ヘルパー関数
function pushWithBudget(
ctx: Block[],
block: Block,
budget: number
): boolean {
if (budget < block.tokens) return false;
ctx.push(block);
budget -= block.tokens;
return true;
}
退避戦略の実装
function compactAndEvict(
history: Block[],
evidences: Block[]
): { history: Block[], evidences: Block[] } {
// 戦略1: Historyの末尾から粗要約化
let compactedHistory = history.map((h, idx) => {
if (idx >= history.length - 3) {
// 古い3件を粗要約
return summarizeCoarse(h);
}
return h;
});
// 戦略2: それでもダメなら削除
if (getTotalTokens(compactedHistory) > threshold) {
compactedHistory = compactedHistory.slice(0, -2);
}
// 戦略3: Evidenceは下位スコアから削除
let compactedEvidences = evidences;
if (getTotalTokens(compactedEvidences) > threshold) {
// 同根拠の重複は先に統合
compactedEvidences = deduplicateEvidences(evidences);
// それでも多ければ下位削除
compactedEvidences = compactedEvidences.slice(
0,
Math.floor(evidences.length * 0.7)
);
}
// 戦略4: 最終手段 - Pinnedの冗長削除
// (実装省略 - 定義・方針のみに絞る)
return {
history: compactedHistory,
evidences: compactedEvidences
};
}
function summarizeCoarse(block: Block): Block {
// LLMまたはルールベースで1-3行要約
return {
tokens: Math.floor(block.tokens * 0.3),
text: `[要約] ${block.text.slice(0, 100)}...`,
meta: { ...block.meta, summarized: true }
};
}
function deduplicateEvidences(evidences: Block[]): Block[] {
// 同一ソース・近接チャンクを統合
const grouped = groupBy(evidences, e => e.meta.sourceId);
return grouped.map(group => mergeChunks(group));
}
シーケンス図
ターン間の"移動"感を示すシーケンス
詳細な状態遷移
実運用のベストプラクティス
1. Scratch最小保証を必ず死守
問題: 出力余白がゼロに近いと、理屈は合っていても答えが崩れる
対策:
const SCRATCH_MIN = 3000; // 最低3000トークン確保
const SCRATCH_IDEAL = 4000; // 理想は4000トークン
if (availableTokens < SCRATCH_MIN) {
throw new Error('Insufficient scratch space');
}
2. "決定・制約・出力契約"はPinnedで常時可視
理由: 毎ターンの再学習を防ぎ、安定度が上がる
実装例:
const pinnedContent = {
objective: "...",
constraints: [...],
outputFormat: {...},
decisions: [...]
};
// 常にコンテキスト先頭に配置
context.unshift(pinnedContent);
3. RAGの冗長排除
問題: 同根拠の近接チャンクが重複してウィンドウを圧迫
対策:
function deduplicateChunks(chunks: Chunk[]): Chunk[] {
// 同一ドキュメント・近接位置のチャンクを統合
return chunks.reduce((acc, chunk) => {
const existing = acc.find(c =>
c.docId === chunk.docId &&
Math.abs(c.position - chunk.position) < 100
);
if (existing) {
existing.text += '\n' + chunk.text;
existing.score = Math.max(existing.score, chunk.score);
} else {
acc.push(chunk);
}
return acc;
}, []);
}
効果: ウィンドウ効率が跳ね上がる(30-50%の削減も可能)
4. 節目で/compact
目的: 履歴を溜めない。決定と根拠を薄く固定化し、詳細は索引に回す
実行タイミング:
- フェーズ完了時(例: 要件定義終了)
- 一定ターン数経過時(例: 20ターンごと)
- ウィンドウ使用率が80%超過時
- モデル変更前
実装例:
function shouldCompact(context: Context): boolean {
return (
context.turnCount % 20 === 0 ||
context.windowUsage > 0.8 ||
context.phaseCompleted
);
}
async function executeCompact(context: Context) {
// 1. 重要な決定と根拠を抽出
const essence = await extractEssence(context.history);
// 2. Pinnedに昇格
context.pinned = mergePinned(context.pinned, essence);
// 3. 詳細履歴を外部化
const handles = await externalizeHistory(context.history);
// 4. Historyを参照ハンドルのみに置き換え
context.history = handles;
// 5. 索引を再構築
await rebuildIndex(context);
}
5. ログは"長文そのまま"を入れない
問題: 生ログは数千〜数万トークンになることも
対策: 要約+原本ハンドルで戻せる状態に
function processToolOutput(output: string): Block {
if (output.length > 1000) {
// 長文は要約
const summary = summarize(output);
const handle = saveToStorage(output);
return {
tokens: countTokens(summary),
text: summary,
meta: {
type: 'tool_output',
fullDataHandle: handle,
retrievable: true
}
};
}
return {
tokens: countTokens(output),
text: output,
meta: { type: 'tool_output' }
};
}
6. 段階的要約の閾値設定
const SUMMARIZATION_THRESHOLDS = {
coarse: { overagePercent: 30, targetCompression: 0.3 },
medium: { overagePercent: 50, targetCompression: 0.5 },
fine: { overagePercent: 70, targetCompression: 0.7 }
};
function selectSummarizationLevel(overage: number): string {
const percent = (overage / windowMax) * 100;
if (percent < SUMMARIZATION_THRESHOLDS.coarse.overagePercent) {
return 'coarse';
} else if (percent < SUMMARIZATION_THRESHOLDS.medium.overagePercent) {
return 'medium';
} else {
return 'fine';
}
}
7. ウィンドウ使用率のモニタリング
function monitorWindowUsage(context: Context) {
const usage = {
pinned: context.pinned.tokens,
history: sumTokens(context.history),
evidence: sumTokens(context.evidence),
scratch: context.windowMax - (
context.pinned.tokens +
sumTokens(context.history) +
sumTokens(context.evidence)
)
};
const usagePercent = {
pinned: (usage.pinned / context.windowMax) * 100,
history: (usage.history / context.windowMax) * 100,
evidence: (usage.evidence / context.windowMax) * 100,
scratch: (usage.scratch / context.windowMax) * 100
};
// アラート条件
if (usagePercent.scratch < 15) {
console.warn('Scratch space critically low!');
}
if (usagePercent.history > 30) {
console.warn('History taking too much space - consider compacting');
}
return { usage, usagePercent };
}
まとめ
コンテキストウィンドウ管理の要点
- 予算管理: Pinned、History、Evidence、Scratchの明確な割当
- 優先順位: 固定 > 現質問 > 根拠 > 履歴の順で配置
- 段階的圧縮: 粗要約 → 中要約 → 精要約 → 外部化の段階的処理
- スライディング: 新しい情報を先頭に追加し、古い情報を末尾から外部化
- /compact: 定期的な構造化要約で長期記憶を最適化
実装時の注意点
- Scratch領域を必ず確保(最低15-30%)
- RAGの冗長排除で効率アップ
- 長文ログは要約+ハンドル化
- 定期的な/compactで健全性維持
- ウィンドウ使用率の継続的モニタリング
今後の拡張方向
- 動的予算調整: クエリ複雑度に応じた領域サイズの自動調整
- 優先度学習: ユーザフィードバックから重要度を学習
- 階層的要約: 多段階の要約レベルを自動選択
- コンテキスト予測: 次のターンで必要な情報を先読み
補足: 実運用ベストプラクティスの適用シーン
想定される実装シーン
「実運用のベストプラクティス」セクションは、LLMを使ったアプリケーションやエージェントを実際に開発する際の実装コード例です。
1. 対話型AIアプリケーション
- チャットボット
- カスタマーサポートエージェント
- パーソナルアシスタント
2. RAGシステム
- ドキュメント検索+生成
- 社内ナレッジベース
- 技術サポートシステム
3. エージェントシステム
- ツール呼び出しを伴うタスク実行エージェント
- 複数ステップの推論を行うエージェント
- 長期的なタスク管理エージェント
コード例の具体的な用途
各コード例は、以下のような実装課題に対応しています:
// 例1: Scratch領域の確保(品質維持)
// → LLMに十分な"考える余白"を確保しないと、
// 途中で切れたり、品質が落ちる問題への対策
const SCRATCH_MIN = 3000;
// 例2: RAG冗長排除
// → ベクトル検索で似たチャンクが大量に返ってきて
// ウィンドウを圧迫する問題への対策
chunks = deduplicateChunks(chunks);
// 例3: /compact実行タイミング
// → 対話が長くなると履歴が溜まりすぎる問題への対策
// 定期的に要約して整理する必要がある
if (shouldCompact(context)) {
await executeCompact(context);
}
// 例4: ツール出力の処理
// → APIやツールの戻り値が長文すぎて
// コンテキストを圧迫する問題への対策
const block = processToolOutput(longOutput);
実践例: カスタマーサポートボット
以下は、上記のベストプラクティスを統合した実際のエージェント実装例です:
class CustomerSupportAgent {
private context: Context;
private llm: LLMClient;
private retriever: DocumentRetriever;
constructor(config: AgentConfig) {
this.context = {
pinned: config.systemPrompt,
history: [],
windowMax: 16000,
scratchMin: 3000,
evidenceMax: 7000,
turnCount: 0
};
}
async handleUserQuery(query: string): Promise<string> {
// 1. 過去の対話履歴をチェック
const usage = monitorWindowUsage(this.context);
console.log('Window usage:', usage.usagePercent);
// 2. ウィンドウが圧迫されていたら整理
if (usage.usagePercent.scratch < 15) {
console.warn('Scratch space low - executing compact');
await executeCompact(this.context);
}
// 3. 定期的なコンパクション
if (shouldCompact(this.context)) {
console.log('Periodic compact triggered');
await executeCompact(this.context);
}
// 4. RAGで関連情報を取得
let chunks = await this.retriever.search(query, {
topK: 10,
threshold: 0.7
});
// 5. 冗長なチャンクを排除
chunks = deduplicateChunks(chunks);
console.log(`Deduplicated chunks: ${chunks.length}`);
// 6. ツール実行が必要な場合
const tools = await this.detectRequiredTools(query);
const toolOutputs = [];
for (const tool of tools) {
const rawOutput = await tool.execute(query);
// 長文出力は要約処理
const processed = processToolOutput(rawOutput);
toolOutputs.push(processed);
}
// 7. コンテキストを組み立て
const finalContext = assembleContext({
pinned: this.context.pinned,
userTurn: {
text: query,
tokens: countTokens(query)
},
evidences: [
...chunks.map(c => ({
tokens: c.tokens,
text: c.text,
meta: { source: c.source, score: c.score }
})),
...toolOutputs
],
history: this.context.history,
windowMax: this.context.windowMax,
scratchMin: this.context.scratchMin,
evidenceMax: this.context.evidenceMax
});
// 8. LLMで応答生成
const response = await this.llm.generate(finalContext);
// 9. 履歴を更新
const turnSummary = await this.summarizeTurn(query, response);
this.context.history.unshift(turnSummary);
// 10. 古い履歴を外部化
if (this.context.history.length > 10) {
const old = this.context.history.pop();
await this.externalizeToStorage(old);
}
this.context.turnCount++;
return response;
}
private async detectRequiredTools(query: string): Promise<Tool[]> {
// クエリから必要なツールを判定
// 例: "注文状況を確認して" → OrderLookupTool
return [];
}
private async summarizeTurn(
query: string,
response: string
): Promise<Block> {
// このターンを要約
const summary = await this.llm.summarize(
`User: ${query}\nAssistant: ${response}`
);
return {
tokens: countTokens(summary),
text: summary,
meta: {
turn: this.context.turnCount,
timestamp: Date.now()
}
};
}
private async externalizeToStorage(block: Block): Promise<void> {
// 外部ストレージ(DBやオブジェクトストレージ)に保存
const handle = await this.storage.save(block);
console.log(`Externalized history block: ${handle}`);
}
}
主要機能の解説
モニタリングと自動調整
// リアルタイムでウィンドウ使用状況を監視
const usage = monitorWindowUsage(this.context);
// 閾値を下回ったら自動で整理
if (usage.usagePercent.scratch < 15) {
await executeCompact(this.context);
}
RAG最適化
// 検索結果の重複を排除してウィンドウ効率を向上
chunks = deduplicateChunks(chunks);
// 同一ドキュメントの近接チャンクを統合
// → 30-50%のトークン削減が可能
ツール出力の処理
// 長文のツール出力は要約+ハンドル化
const processed = processToolOutput(rawOutput);
// 元データは外部保存、要約だけをコンテキストに
履歴管理
// 新しいターンを先頭に追加
this.context.history.unshift(turnSummary);
// 古いターンは外部化
if (this.context.history.length > 10) {
const old = this.context.history.pop();
await this.externalizeToStorage(old);
}
実装時のチェックリスト
- Scratch領域の最小保証(15-30%)を設定
- Pinned領域に固定すべき内容(目的、制約、契約)を配置
- RAGチャンクの重複排除ロジックを実装
- ツール出力の要約処理を実装
- 定期的な/compact実行の条件を設定
- ウィンドウ使用率のモニタリングとログ出力
- 履歴の外部化・復元機能の実装
- 段階的要約(粗→中→精)の閾値設定
パフォーマンス指標
指標 | 推奨値 | 警告閾値 |
---|---|---|
Scratch使用率 | 20-30% | < 15% |
History使用率 | 10-20% | > 30% |
Evidence使用率 | 30-50% | > 60% |
総ウィンドウ使用率 | 70-85% | > 90% |
/compact実行間隔 | 15-20ターン | > 30ターン |
デバッグとトラブルシューティング
問題: 応答品質が突然低下
// 原因: Scratch領域の不足
// 対策: ウィンドウ使用状況を確認
const usage = monitorWindowUsage(context);
console.log('Scratch tokens:', usage.scratch);
if (usage.scratch < SCRATCH_MIN) {
// 緊急コンパクション実行
await executeCompact(context);
}
問題: 過去の重要な決定を忘れる
// 原因: 重要情報がHistoryから流れてしまった
// 対策: Pinnedに昇格
const importantDecisions = extractKeyDecisions(context.history);
context.pinned = mergePinned(context.pinned, importantDecisions);
問題: レスポンスが遅い
// 原因: Evidence領域が大きすぎる
// 対策: チャンク数を制限、スコアで厳選
const topChunks = chunks
.sort((a, b) => b.score - a.score)
.slice(0, 5); // 上位5件のみ
まとめ: 実装のポイント
- 予防的管理: 問題が起きる前にモニタリングと自動調整
- 段階的対応: 軽度な問題は軽い対策、重度な問題は強い対策
- トレードオフの理解: 情報量 vs 応答品質のバランス
- 継続的最適化: ログを分析して閾値を調整
- ユーザー体験優先: レスポンス速度と品質のバランス
これらのベストプラクティスを適用することで、長時間の対話でも安定した品質を維持できるLLMアプリケーションを構築できます。