LLMアプリを触ってみた人の最初の感想は、たいてい「ちょっと遅い」です。精度がどれほど素晴らしくても、5秒間沈黙する画面に付き合える忍耐力はユーザーには期待できません。本記事では、ストリーミング・並列化・キャッシュの3本柱を組み合わせることで、体感レイテンシと実レイテンシの両方を劇的に改善できる、というアプローチを解説します。特にストリーミングは、実処理時間を変えずに体感を数倍改善できる最もコスパの良い最適化です。

LLMアプリのレイテンシの内訳

最適化を始める前に、レイテンシがどこで発生しているかを分解する必要があります。LLMアプリのレスポンスタイムは、次のような要素に分解できます。

【LLMアプリのレイテンシ内訳】

クライアント --> サーバー --> 前処理 --> LLM API --> 後処理 --> クライアント
   (a)          (b)        (c)       (d)         (e)        (f)

(a) クライアント送信    : 数十ms
(b) サーバー受信・認証  : 数十ms
(c) 前処理(RAG検索等) : 50〜500ms
(d) LLM API            : 500ms〜数秒(最大の割合)
    ├── TTFT(最初のトークンまで) : 200〜800ms
    └── 残りトークン生成         : 入力/出力長に依存
(e) 後処理(検証・整形): 数十〜数百ms
(f) クライアント描画    : 数十ms

※ (d)が全体の70〜90%を占めることが多く、最適化の主戦場

特に重要なのがTTFT(Time To First Token)で、これはユーザーが「応答が始まった」と感じるまでの時間です。全体生成時間より、TTFTの方がユーザー体験への影響が大きいことが知られており、最適化の優先順位は「まずTTFT、次に全体」と覚えておいてください。

ストリーミングレスポンスの実装

レイテンシ最適化で最も費用対効果が高いのがストリーミングです。生成された順にトークンをクライアントに流すことで、TTFTを実質「LLM APIのTTFT + ネットワーク」まで短縮できます。実際の処理時間は変わりませんが、ユーザーの体感は大きく変わります。

FastAPI + Server-Sent Events(SSE)での実装例を示します。

from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from openai import OpenAI

app = FastAPI()
client = OpenAI()

def stream_chat(prompt: str):
    stream = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        stream=True,
    )
    for chunk in stream:
        delta = chunk.choices[0].delta.content
        if delta:
            yield f"data: {delta}\n\n"
    yield "data: [DONE]\n\n"

@app.get("/chat")
def chat(prompt: str):
    return StreamingResponse(
        stream_chat(prompt),
        media_type="text/event-stream",
    )

並列処理とバッチ処理

複数のLLM呼び出しが必要な場合、直列実行するとレイテンシが単純に加算されます。依存関係がないLLM呼び出しは並列化することで劇的に高速化できます。例えば「長文を章ごとに要約してから統合する」というMap-Reduce型のタスクでは、章単位の要約を並列化できます。

import asyncio
from openai import AsyncOpenAI

client = AsyncOpenAI()

async def summarize_chunk(chunk: str) -> str:
    resp = await client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": f"要約: {chunk}"}],
    )
    return resp.choices[0].message.content

async def summarize_document(chunks: list[str]) -> str:
    partials = await asyncio.gather(*[summarize_chunk(c) for c in chunks])
    combined = "\n".join(partials)
    final_resp = await client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": f"統合要約: {combined}"}],
    )
    return final_resp.choices[0].message.content

並列化する際は、LLMベンダー側のレート制限にも注意してください。OpenAIではRPMとTPMの両方で制限がかかるため、セマフォで同時実行数を制御する実装が実務的です。

キャッシュ戦略

キャッシュはヒットすれば数msで応答が返るため、最も強力な最適化です。LLMのキャッシュ戦略には大きく2種類あります。

戦略ヒット率実装難易度メモリ使用量適した場面
完全一致キャッシュ低〜中(20〜30%)FAQ、定型クエリ
セマンティックキャッシュ中〜高(40〜60%)自然文の質問応答
プロンプトキャッシュ(ベンダー提供)ベンダー側長文の共通プレフィックス

セマンティックキャッシュは、質問を埋め込みに変換し、過去の質問と類似度が高ければキャッシュから応答するアプローチです。実装例を示します。

import numpy as np
from openai import OpenAI

client = OpenAI()
cache_store = []  # 実運用ではベクトルDBを使用

def embed(text: str) -> list[float]:
    r = client.embeddings.create(model="text-embedding-3-small", input=text)
    return r.data[0].embedding

def cosine(a, b):
    return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))

def semantic_cache_get(query: str, threshold: float = 0.92):
    q_emb = embed(query)
    for entry in cache_store:
        if cosine(q_emb, entry["embedding"]) >= threshold:
            return entry["response"]
    return None

def answer_with_cache(query: str) -> str:
    cached = semantic_cache_get(query)
    if cached:
        return cached
    response = llm_call(query)
    cache_store.append({"embedding": embed(query), "response": response})
    return response

プロンプト最適化と出力制御

同じタスクでも、プロンプトの書き方で出力長が大きく変わります。LLMのレイテンシは出力トークン数にほぼ比例するため、必要最小限の出力を指示するだけで全体時間が短縮されます。「50文字以内で」「箇条書きで3点だけ」といった明示的な制約が効きます。

テクニック効果実装コストトレードオフ
ストリーミング体感TTFT 50〜80%改善ストリーム対応のUI必要
並列LLM呼び出し最大で呼び出し数倍高速化レート制限に注意
セマンティックキャッシュヒット時ほぼゼロレイテンシ古い回答を返すリスク
プロンプトキャッシュTTFT大幅改善、コスト削減共通プレフィックスが必要
出力長の制約全体20〜50%改善情報量とのトレードオフ
軽量モデルへの切替全体3〜10倍高速化品質低下のリスク
エッジリージョン配置ネットワーク50〜200ms短縮コスト増

まとめ

レイテンシ最適化は「ストリーミングでTTFT短縮」「並列化で全体短縮」「キャッシュで繰り返し削減」の三層で考えます。まずはストリーミングから着手し、次に並列化、最後にキャッシュを導入する順序が失敗しにくいやり方です。関連記事として推論コスト削減トークンとはLLMコスト構造LLMアプリUX設計もあわせてご参照ください。

よくある質問(FAQ)

Q1. LLMの応答速度を最も効果的に改善する方法は?

A. ストリーミングレスポンスの実装が最も体感速度を改善します。実際の処理時間は変わりませんが、最初のトークンが即座に表示されるため、ユーザー体験が大幅に向上します。実装コストも低いため、対話型アプリでは最優先で導入すべきです。

Q2. LLMのレイテンシの目安は?

A. 対話型アプリではTTFT(最初のトークン表示まで)500ms以下、バッチ処理では全体のスループットが重要です。GPT-4oで平均200〜500ms、Claude Opusで300〜800ms程度がTTFTの目安です。ユーザーの許容範囲を超えないよう、p95でこれらの値を下回るように監視してください。

Q3. キャッシュでLLMの速度は上がりますか?

A. キャッシュヒットした場合は応答が即座(数ms)に返るため、劇的に速度が向上します。FAQ型のアプリケーションでは40〜60%のヒット率が見込め、全体のレイテンシとコストを大幅に削減できます。ただし、古い回答を返すリスクがあるため、TTL設定とキャッシュ無効化戦略を併せて検討してください。