LLM

Large Language Model(大規模言語モデル)

LLM を分かりやすく

LLM(Large Language Model、大規模言語モデル)について、分かりやすく説明しますね。

想像してみてください。あなたが図書館の司書で、何千冊もの本を読んで内容を記憶しているとします。読書家のあなたは、様々なジャンルの本から得た知識を組み合わせて、訪問者の質問に答えることができます。小説の文体を真似て文章を書いたり、技術書の内容を要約したり、詩を作ったりすることも可能です。

LLM はまさにこの「超人的な司書」のような存在です。インターネット上の膨大なテキストデータ(ウェブページ、書籍、論文、コードなど)を学習して、人間のような自然な文章を生成したり、質問に答えたり、プログラムコードを書いたりできるようになりました。

LLM の仕組み

LLM は「Transformer」と呼ばれるニューラルネットワークアーキテクチャをベースにしています。簡単に言うと、「次に来る単語を予測する」という学習を何兆回も繰り返すことで、言語のパターンや構造を理解するようになります。

例えば、「今日の天気は」という文章を見せると、LLM は過去の学習から「晴れ」「曇り」「雨」などの単語が続く可能性が高いと予測します。しかし、これは単純な予測ではありません。文脈、前後の文章、話題の流れなどを総合的に判断して、最も適切な続きを生成します。

代表的な LLM

現在、いくつかの主要な LLM が存在します:

OpenAI GPT シリーズ

  • GPT-4: 最も高性能な汎用モデル。複雑な推論、コード生成、画像理解が可能
  • GPT-4 Turbo: GPT-4 の高速版。コンテキストウィンドウが広い(128K トークン)
  • GPT-3.5 Turbo: より安価で高速。一般的なタスクに十分な性能

Anthropic Claude シリーズ

  • Claude 3.5 Sonnet: コーディングと推論に優れる。200K トークンのコンテキスト
  • Claude 3 Opus: 最も高性能なモデル
  • Claude 3 Haiku: 高速で安価なモデル

Google Gemini シリーズ

  • Gemini 1.5 Pro: 最大 2M トークンの超長文対応
  • Gemini 1.5 Flash: 高速で効率的なモデル

Next.js × TypeScript で複数の LLM を活用する

実際のアプリケーション開発では、用途に応じて複数の LLM を使い分けることが重要です。Vercel AI SDK を使うと、統一的なインターフェースで複数の LLM を扱えます。

環境構築

npm install ai @ai-sdk/openai @ai-sdk/anthropic @ai-sdk/google

環境変数を設定します。

# .env.local
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
GOOGLE_GENERATIVE_AI_API_KEY=...

マルチプロバイダー対応のチャット API

// app/api/chat/route.ts
import { createOpenAI } from '@ai-sdk/openai'
import { createAnthropic } from '@ai-sdk/anthropic'
import { createGoogleGenerativeAI } from '@ai-sdk/google'
import { streamText } from 'ai'

export const runtime = 'edge'

// プロバイダーの初期化
const openai = createOpenAI({
  apiKey: process.env.OPENAI_API_KEY,
})

const anthropic = createAnthropic({
  apiKey: process.env.ANTHROPIC_API_KEY,
})

const google = createGoogleGenerativeAI({
  apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY,
})

// モデルマッピング
const models = {
  'gpt-4': openai('gpt-4'),
  'gpt-3.5-turbo': openai('gpt-3.5-turbo'),
  'claude-3-5-sonnet': anthropic('claude-3-5-sonnet-20241022'),
  'claude-3-haiku': anthropic('claude-3-haiku-20240307'),
  'gemini-1.5-pro': google('gemini-1.5-pro-latest'),
} as const

type ModelName = keyof typeof models

export async function POST(req: Request) {
  try {
    const { messages, modelName = 'gpt-4' } = await req.json()

    // 指定されたモデルを取得
    const model = models[modelName as ModelName] || models['gpt-4']

    // ストリーミングレスポンスを生成
    const result = await streamText({
      model,
      messages,
      temperature: 0.7,
      maxTokens: 2000,
    })

    return result.toDataStreamResponse()
  } catch (error) {
    console.error('LLM API Error:', error)
    return new Response('エラーが発生しました', { status: 500 })
  }
}

フロントエンドでのモデル選択

// app/chat/page.tsx
'use client'

import { useChat } from 'ai/react'
import { useState } from 'react'

type ModelOption = {
  id: string
  name: string
  description: string
  provider: string
}

const modelOptions: ModelOption[] = [
  {
    id: 'gpt-4',
    name: 'GPT-4',
    description: '最も高性能な汎用モデル',
    provider: 'OpenAI',
  },
  {
    id: 'gpt-3.5-turbo',
    name: 'GPT-3.5 Turbo',
    description: '高速で安価なモデル',
    provider: 'OpenAI',
  },
  {
    id: 'claude-3-5-sonnet',
    name: 'Claude 3.5 Sonnet',
    description: 'コーディングと推論に優れる',
    provider: 'Anthropic',
  },
  {
    id: 'gemini-1.5-pro',
    name: 'Gemini 1.5 Pro',
    description: '超長文対応(2M トークン)',
    provider: 'Google',
  },
]

export default function ChatPage() {
  const [selectedModel, setSelectedModel] = useState('gpt-4')

  const { messages, input, handleInputChange, handleSubmit, isLoading, reload } = useChat({
    api: '/api/chat',
    body: {
      modelName: selectedModel,
    },
  })

  return (
    <div className="container mx-auto max-w-4xl p-4">
      <div className="flex justify-between items-center mb-6">
        <h1 className="text-3xl font-bold">マルチモデルチャット</h1>

        {/* モデル選択 */}
        <select
          value={selectedModel}
          onChange={(e) => setSelectedModel(e.target.value)}
          className="border border-gray-300 rounded-lg px-4 py-2"
          disabled={isLoading}
        >
          {modelOptions.map((option) => (
            <option key={option.id} value={option.id}>
              {option.name} - {option.provider}
            </option>
          ))}
        </select>
      </div>

      {/* 選択中のモデル情報 */}
      <div className="bg-blue-50 border border-blue-200 rounded-lg p-3 mb-4">
        <p className="text-sm text-blue-900">
          <span className="font-semibold">現在のモデル:</span>{' '}
          {modelOptions.find((m) => m.id === selectedModel)?.description}
        </p>
      </div>

      {/* メッセージ表示エリア */}
      <div className="bg-gray-50 rounded-lg p-4 h-[500px] overflow-y-auto mb-4">
        {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 text-gray-500 mb-1">
                {message.role === 'user' ? 'あなた' : selectedModel}
              </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>
  )
}

LLM を用途別に使い分ける

実践的なアプリケーションでは、タスクの性質に応じて LLM を使い分けることでコストと性能を最適化できます。

// lib/model-selector.ts
type TaskType = 'simple' | 'complex' | 'code' | 'long-context'

export function selectOptimalModel(taskType: TaskType): string {
  const modelMapping: Record<TaskType, string> = {
    // 簡単なタスク: 安価で高速なモデル
    simple: 'gpt-3.5-turbo',

    // 複雑な推論: 高性能モデル
    complex: 'gpt-4',

    // コーディング: Claude が得意
    code: 'claude-3-5-sonnet',

    // 長文処理: Gemini の超長コンテキスト
    'long-context': 'gemini-1.5-pro',
  }

  return modelMapping[taskType]
}

// 使用例
export async function POST(req: Request) {
  const { messages, taskType = 'simple' } = await req.json()

  const modelName = selectOptimalModel(taskType as TaskType)
  const model = models[modelName as ModelName]

  const result = await streamText({
    model,
    messages,
  })

  return result.toDataStreamResponse()
}

LLM のコンテキストウィンドウを理解する

LLM には「コンテキストウィンドウ」という制限があります。これは、一度に処理できるテキストの最大長です。

// lib/context-manager.ts
const contextLimits = {
  'gpt-3.5-turbo': 16385, // 約 16K トークン
  'gpt-4': 8192, // 約 8K トークン
  'gpt-4-turbo': 128000, // 約 128K トークン
  'claude-3-5-sonnet': 200000, // 約 200K トークン
  'gemini-1.5-pro': 2000000, // 約 2M トークン
}

// トークン数を概算(1 トークン ≈ 4 文字と仮定)
export function estimateTokens(text: string): number {
  return Math.ceil(text.length / 4)
}

// コンテキストウィンドウに収まるかチェック
export function checkContextLimit(
  messages: Array<{ content: string }>,
  modelName: string
): { withinLimit: boolean; estimatedTokens: number } {
  const totalText = messages.map((m) => m.content).join('')
  const estimatedTokens = estimateTokens(totalText)
  const limit = contextLimits[modelName as keyof typeof contextLimits] || 8192

  return {
    withinLimit: estimatedTokens < limit * 0.9, // 安全のため 90% を上限とする
    estimatedTokens,
  }
}

ストリーミングと非ストリーミングの使い分け

LLM の応答には、ストリーミング(逐次的に返す)と非ストリーミング(全体を一度に返す)の 2 種類があります。

// app/api/generate/route.ts
import { generateText, streamText } from 'ai'
import { createOpenAI } from '@ai-sdk/openai'

const openai = createOpenAI({
  apiKey: process.env.OPENAI_API_KEY,
})

// 非ストリーミング(構造化されたデータを返す場合に適している)
export async function POST(req: Request) {
  const { prompt, useStreaming = true } = await req.json()

  if (useStreaming) {
    // ストリーミング: ユーザーに逐次表示
    const result = await streamText({
      model: openai('gpt-4'),
      prompt,
    })

    return result.toDataStreamResponse()
  } else {
    // 非ストリーミング: JSON レスポンスとして返す
    const result = await generateText({
      model: openai('gpt-4'),
      prompt,
    })

    return Response.json({
      text: result.text,
      finishReason: result.finishReason,
      usage: result.usage,
    })
  }
}

LLM のパラメータ調整

LLM の出力は、パラメータによって大きく変わります。

// 創造的なコンテンツ生成
const creativeResult = await generateText({
  model: openai('gpt-4'),
  prompt: '短編小説を書いてください',
  temperature: 0.9, // 高い値 = より創造的
  topP: 0.95,
  maxTokens: 2000,
})

// 事実に基づいた回答
const factualResult = await generateText({
  model: openai('gpt-4'),
  prompt: '第二次世界大戦について説明してください',
  temperature: 0.3, // 低い値 = より決定的
  topP: 0.9,
  maxTokens: 1000,
})

// コード生成
const codeResult = await generateText({
  model: anthropic('claude-3-5-sonnet-20241022'),
  prompt: 'TypeScript でソート関数を実装してください',
  temperature: 0.2, // 低い値 = より正確
  maxTokens: 500,
})

まとめ

LLM は、現代の AI アプリケーション開発の中核を担う技術です。Next.js と TypeScript を組み合わせることで、複数の LLM を統一的に扱い、用途に応じて最適なモデルを選択できます。

重要なポイント:

  1. 適材適所でモデルを選択:タスクの複雑さに応じて GPT-4、Claude、Gemini を使い分ける
  2. コンテキストウィンドウを理解する:各モデルの処理可能なテキスト量を把握
  3. ストリーミングでUX向上:リアルタイムで応答を表示してユーザー体験を改善
  4. パラメータ調整でコントロール:temperature や topP で出力の特性を変更
  5. コスト管理:安価なモデルと高性能モデルを使い分けてコスト最適化

LLM を活用することで、これまで不可能だった機能を Web アプリケーションに追加できます。自然言語でのユーザーインタラクション、コンテンツ生成、データ分析など、可能性は無限大です。