T-CREATOR

LangChain を Edge で走らせる:Cloudflare Workers/Deno/Bun 対応の初期配線

LangChain を Edge で走らせる:Cloudflare Workers/Deno/Bun 対応の初期配線

近年、Web 開発において Edge Computing が注目を集めています。従来のサーバーサイドレンダリングよりも高速で、よりユーザーに近い場所でコードを実行できる Edge 環境は、LangChain のような AI アプリケーションにとって革新的な可能性を秘めています。

本記事では、Cloudflare Workers、Deno、Bun という 3 つの主要な Edge Runtime プラットフォームで LangChain を動作させる方法を詳しく解説します。各プラットフォームの特徴から実装方法まで、包括的にカバーしていきます。

Edge Runtime の選択肢と特徴

Edge Runtime は従来の Node.js 環境とは異なる特徴を持ちます。まず各プラットフォームの特色を理解しましょう。

以下の図は、各 Edge Runtime プラットフォームの基本的な位置づけを示します。

mermaidflowchart TD
    edge[Edge Runtime] --> cf[Cloudflare Workers]
    edge --> deno[Deno]
    edge --> bun[Bun]

    cf --> v8_1[V8 Isolates]
    cf --> global_1[グローバル分散]
    cf --> serverless_1[完全サーバーレス]

    deno --> v8_2[V8 + Rust]
    deno --> security[セキュリティ重視]
    deno --> ts_native[TypeScript ネイティブ]

    bun --> js_core[JavaScriptCore]
    bun --> performance[高速実行]
    bun --> node_compat[Node.js 互換]

各プラットフォームは異なるエンジンと設計思想を持ち、LangChain の実行にそれぞれ独自のメリットをもたらします。

Cloudflare Workers の特徴

Cloudflare Workers は世界中に分散されたエッジネットワーク上で動作するサーバーレスプラットフォームです。V8 Isolates という独自の仕組みを採用しています。

主な特徴

  • グローバル分散: 世界 200 カ所以上のデータセンターで実行
  • Cold Start の高速化: V8 Isolates により数ミリ秒での起動
  • 制約のあるランタイム: Web 標準 API のみ利用可能
  • 無料枠の充実: 月 10 万リクエストまで無料

LangChain での利点

javascript// Cloudflare Workers での基本的なLangChain初期化例
import { ChatOpenAI } from '@langchain/openai';

export default {
  async fetch(request, env, ctx) {
    // env に環境変数が格納される(Workersの仕組み)
    const llm = new ChatOpenAI({
      apiKey: env.OPENAI_API_KEY,
      temperature: 0.7,
    });

    const response = await llm.invoke(
      'Hello from the edge!'
    );
    return new Response(
      JSON.stringify({ message: response.content })
    );
  },
};

Cloudflare Workers は特に API エンドポイントとして LangChain を利用する場合に威力を発揮します。レスポンス速度とスケーラビリティの両面で優れた性能を発揮するでしょう。

Deno の特徴

Deno は Node.js の作者が開発したモダンな JavaScript/TypeScript ランタイムです。セキュリティを重視した設計となっています。

主な特徴

  • TypeScript ネイティブサポート: 追加設定なしで TypeScript を実行可能
  • セキュリティファースト: デフォルトでファイル、ネットワークアクセスを制限
  • Web 標準 API: Fetch API、WebStreams などを標準搭載
  • モジュールシステム: URL ベースのモジュール管理

LangChain での利点

typescript// Deno での LangChain 利用例
import { ChatOpenAI } from 'https://esm.sh/@langchain/openai';
import { load } from 'https://deno.land/std@0.208.0/dotenv/mod.ts';

// 環境変数の読み込み
const env = await load();

const llm = new ChatOpenAI({
  apiKey: env['OPENAI_API_KEY'],
  temperature: 0.7,
});

// Deno の強力な型システムを活用
interface ChatRequest {
  message: string;
  context?: string;
}

export async function handleChat(
  req: ChatRequest
): Promise<string> {
  const prompt = req.context
    ? `Context: ${req.context}\nQuestion: ${req.message}`
    : req.message;

  const response = await llm.invoke(prompt);
  return response.content;
}

Deno は型安全性を重視する開発者や、セキュリティが重要なアプリケーションに適しています。

Bun の特徴

Bun は JavaScriptCore を採用した高速な JavaScript ランタイムです。Node.js との高い互換性を保ちながら、大幅な性能向上を実現しています。

主な特徴

  • 高速実行: Node.js の 3-4 倍の実行速度
  • Node.js 互換: 既存の npm パッケージをそのまま利用可能
  • 内蔵ツール: バンドラー、テストランナー、パッケージマネージャーを統合
  • 組み込み APIs: fetch、WebSocket などを標準搭載

LangChain での利点

typescript// Bun での LangChain 利用例
import { ChatOpenAI } from '@langchain/openai';
import { MemoryVectorStore } from 'langchain/vectorstores/memory';
import { OpenAIEmbeddings } from '@langchain/openai';

// Bun の高速起動を活かしたリアルタイム処理
const llm = new ChatOpenAI({
  apiKey: process.env.OPENAI_API_KEY,
  temperature: 0.7,
});

// ベクトル検索の高速化
const embeddings = new OpenAIEmbeddings();
const vectorStore = new MemoryVectorStore(embeddings);

export async function processDocuments(
  documents: string[]
) {
  console.time('Document processing');

  // Bun の高速処理でドキュメント処理を最適化
  await vectorStore.addDocuments(
    documents.map((content) => ({
      pageContent: content,
      metadata: {},
    }))
  );

  console.timeEnd('Document processing'); // 通常より高速

  return vectorStore;
}

Bun は特に計算集約的な処理や大量のドキュメント処理が必要な LangChain アプリケーションで威力を発揮します。

プラットフォーム比較表

項目Cloudflare WorkersDenoBun
実行速度高速(Cold Start 最適化)中程度非常に高速
スケーラビリティ自動スケール手動設定必要手動設定必要
Node.js 互換性限定的部分的非常に高い
TypeScript要トランスパイルネイティブネイティブ
デプロイの簡単さ非常に簡単簡単中程度
無料枠充実あり(Deno Deploy)なし(自前ホスティング)
開発体験制約あり優秀非常に優秀
セキュリティサンドボックス許可制Node.js と同等

図で理解できる要点:

  • Cloudflare Workers は分散実行とスケーラビリティに特化
  • Deno はセキュリティと型安全性を重視
  • Bun は実行速度と Node.js 互換性が最大の強み

LangChain の Edge 対応状況

LangChain を Edge 環境で動作させる際は、従来の Node.js 環境とは異なる制約と可能性があります。これらを理解することが成功への鍵となります。

Edge 環境での制約事項

Edge Runtime 環境では、従来のサーバーサイドアプリケーションと比べていくつかの制約があります。

mermaidflowchart TD
    nodejs[Node.js 環境] --> edge_limit[Edge 環境制約]

    edge_limit --> fs_limit[ファイルシステム制限]
    edge_limit --> module_limit[モジュール制限]
    edge_limit --> memory_limit[メモリ制限]
    edge_limit --> time_limit[実行時間制限]

    fs_limit --> no_write[書き込み不可]
    module_limit --> web_std[Web標準APIのみ]
    memory_limit --> small_mem[128MB-512MB]
    time_limit --> short_exec[30秒-10分]

制約の詳細を理解することで、適切な実装戦略を立てることができます。

ファイルシステムの制約

typescript// ❌ Edge環境では動作しない例
import fs from 'fs';
import path from 'path';

// ファイルシステムへの書き込みは不可
fs.writeFileSync('./temp.txt', 'data'); // Error!

// ❅ Edge環境対応の代替案
// メモリ内での一時データ管理
const tempStorage = new Map<string, any>();

export function storeTemporaryData(key: string, data: any) {
  tempStorage.set(key, data);
}

export function getTemporaryData(key: string) {
  return tempStorage.get(key);
}

モジュールの制約

特定の Node.js 専用モジュールは使用できません。Web 標準 API を活用した代替実装が必要です。

typescript// ❌ Node.js専用モジュールは使用不可
import crypto from 'crypto'; // Error in some edge environments

// ❅ Web標準のCrypto APIを使用
async function generateHash(data: string): Promise<string> {
  const encoder = new TextEncoder();
  const dataBuffer = encoder.encode(data);
  const hashBuffer = await crypto.subtle.digest(
    'SHA-256',
    dataBuffer
  );

  const hashArray = Array.from(new Uint8Array(hashBuffer));
  return hashArray
    .map((b) => b.toString(16).padStart(2, '0'))
    .join('');
}

メモリとタイムアウト制約

typescript// Edge環境に適したバッチ処理の実装
export async function processDocumentsBatch(
  documents: string[],
  batchSize: number = 10
) {
  const results = [];

  // メモリ制約を考慮したバッチ処理
  for (let i = 0; i < documents.length; i += batchSize) {
    const batch = documents.slice(i, i + batchSize);

    // タイムアウト対策:各バッチで時間制限チェック
    const startTime = Date.now();
    const batchResults = await Promise.all(
      batch.map((doc) => processDocument(doc))
    );

    results.push(...batchResults);

    // 実行時間が制限に近づいたら早期終了
    if (Date.now() - startTime > 25000) {
      // 25秒で制限
      console.warn('タイムアウト回避のため処理を中断');
      break;
    }
  }

  return results;
}

対応可能な LangChain 機能

一方で、多くの LangChain 機能は Edge 環境でも正常に動作します。以下は対応状況の詳細です。

完全対応機能

typescript// ✅ Chat Models - 完全対応
import { ChatOpenAI } from '@langchain/openai';
import { ChatAnthropic } from '@langchain/anthropic';

const openaiChat = new ChatOpenAI({
  apiKey: process.env.OPENAI_API_KEY,
  modelName: 'gpt-4',
});

const anthropicChat = new ChatAnthropic({
  apiKey: process.env.ANTHROPIC_API_KEY,
  modelName: 'claude-3-sonnet-20240229',
});

// ✅ Prompt Templates - 完全対応
import { PromptTemplate } from '@langchain/core/prompts';

const promptTemplate = PromptTemplate.fromTemplate(`
  あなたは{role}です。
  以下の質問に{style}で答えてください:
  
  質問: {question}
`);

// ✅ Output Parsers - 完全対応
import { StructuredOutputParser } from 'langchain/output_parsers';
import { z } from 'zod';

const parser = StructuredOutputParser.fromZodSchema(
  z.object({
    answer: z.string().describe('質問への回答'),
    confidence: z.number().describe('確信度(0-100)'),
  })
);

部分対応機能

typescript// ⚠️ Vector Stores - メモリベースのみ推奨
import { MemoryVectorStore } from 'langchain/vectorstores/memory';
import { OpenAIEmbeddings } from '@langchain/openai';

// メモリベースベクトルストア(推奨)
const memoryStore = new MemoryVectorStore(
  new OpenAIEmbeddings()
);

// ⚠️ 永続化が必要な場合は外部サービスを利用
// 例:Supabase、Pinecone、Weaviate など

制限付き対応機能

typescript// ⚠️ Document Loaders - URL/API経由のみ
// ファイルシステムアクセスは不可

// ✅ Web-based loaders
import { CheerioWebBaseLoader } from 'langchain/document_loaders/web/cheerio';
import { JSONLoader } from 'langchain/document_loaders/fs/json';

// Web からドキュメント読み込み(推奨)
const webLoader = new CheerioWebBaseLoader(
  'https://example.com'
);

// ❌ ローカルファイル読み込み(Edge環境では不可)
// const fileLoader = new JSONLoader("./local-file.json") // Error!

// ✅ API経由でのデータ取得(代替案)
async function loadExternalData(url: string) {
  const response = await fetch(url);
  const data = await response.json();
  return data.map((item: any) => ({
    pageContent: item.content,
    metadata: item.metadata || {},
  }));
}

図で理解できる要点:

  • Chat Models と Prompt Templates は全面的に対応
  • Vector Stores はメモリベースに限定される
  • Document Loaders は Web/API ベースのもののみ利用可能

プラットフォーム別セットアップ

各プラットフォームでの具体的なセットアップ手順を、実際のコード例とともに解説します。

Cloudflare Workers での初期配線

Cloudflare Workers は Wrangler CLI を使用してプロジェクトを管理します。セットアップから基本的な LangChain 実装までを順を追って説明します。

プロジェクト初期化

bash# Wrangler CLI のインストール
npm install -g wrangler

# 新規プロジェクト作成
wrangler init langchain-edge-app
cd langchain-edge-app

依存関係のインストール

bash# LangChain 関連パッケージ
npm install @langchain/openai @langchain/core
npm install --save-dev @cloudflare/workers-types typescript

wrangler.toml の設定

tomlname = "langchain-edge-app"
main = "src/index.ts"
compatibility_date = "2024-01-01"

# 環境変数の設定
[vars]
NODE_ENV = "production"

# シークレット情報(wrangler secret put で設定)
# OPENAI_API_KEY = "sk-..."

基本的な実装

typescript// src/index.ts
import { ChatOpenAI } from '@langchain/openai';
import { PromptTemplate } from '@langchain/core/prompts';

interface Env {
  OPENAI_API_KEY: string;
}

export default {
  async fetch(
    request: Request,
    env: Env,
    ctx: ExecutionContext
  ): Promise<Response> {
    // CORSヘッダーの設定
    const corsHeaders = {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type',
    };

    if (request.method === 'OPTIONS') {
      return new Response(null, { headers: corsHeaders });
    }

    try {
      // リクエストボディの解析
      const { message, context } = await request.json();

      // LangChain の初期化
      const llm = new ChatOpenAI({
        apiKey: env.OPENAI_API_KEY,
        modelName: 'gpt-3.5-turbo',
        temperature: 0.7,
      });

      // プロンプトテンプレートの作成
      const template = PromptTemplate.fromTemplate(`
        Context: {context}
        Question: {message}
        
        Please provide a helpful and accurate response.
      `);

      // プロンプトの生成と実行
      const prompt = await template.format({
        context:
          context || 'No additional context provided',
        message: message,
      });

      const response = await llm.invoke(prompt);

      return new Response(
        JSON.stringify({
          success: true,
          response: response.content,
          timestamp: new Date().toISOString(),
        }),
        {
          status: 200,
          headers: {
            'Content-Type': 'application/json',
            ...corsHeaders,
          },
        }
      );
    } catch (error) {
      return new Response(
        JSON.stringify({
          success: false,
          error:
            error instanceof Error
              ? error.message
              : 'Unknown error',
        }),
        {
          status: 500,
          headers: {
            'Content-Type': 'application/json',
            ...corsHeaders,
          },
        }
      );
    }
  },
};

デプロイメント

bash# 環境変数の設定
wrangler secret put OPENAI_API_KEY

# デプロイ実行
wrangler deploy

Cloudflare Workers の設定では、V8 Isolates の特性を活かした高速起動が可能です。特に API エンドポイントとして利用する場合の応答性能は優秀でしょう。

Deno での初期配線

Deno は TypeScript ネイティブサポートと強力なセキュリティモデルが特徴です。Import Maps を活用した効率的な開発環境を構築します。

プロジェクト構造の準備

bash# プロジェクトディレクトリ作成
mkdir langchain-deno-app
cd langchain-deno-app

# 基本ファイル構成
touch deno.json import_map.json .env main.ts

Import Maps の設定

json// import_map.json
{
  "imports": {
    "@langchain/openai": "https://esm.sh/@langchain/openai@0.0.25",
    "@langchain/core/": "https://esm.sh/@langchain/core/",
    "zod": "https://deno.land/x/zod@v3.22.4/mod.ts",
    "dotenv": "https://deno.land/std@0.208.0/dotenv/mod.ts"
  }
}

Deno 設定ファイル

json// deno.json
{
  "compilerOptions": {
    "strict": true,
    "allowJs": false,
    "checkJs": false
  },
  "importMap": "./import_map.json",
  "tasks": {
    "dev": "deno run --allow-net --allow-env --allow-read --watch main.ts",
    "start": "deno run --allow-net --allow-env --allow-read main.ts"
  },
  "permissions": {
    "net": ["api.openai.com", "anthropic.com"],
    "env": ["OPENAI_API_KEY", "ANTHROPIC_API_KEY"],
    "read": [".env"]
  }
}

メイン実装

typescript// main.ts
import { ChatOpenAI } from '@langchain/openai';
import { PromptTemplate } from '@langchain/core/prompts';
import { load } from 'dotenv';

// 環境変数の読み込み
const env = await load();

// 型安全なリクエスト定義
interface ChatRequest {
  message: string;
  context?: string;
  temperature?: number;
}

interface ChatResponse {
  success: boolean;
  response?: string;
  error?: string;
  metadata: {
    timestamp: string;
    model: string;
    tokensUsed?: number;
  };
}

class LangChainService {
  private llm: ChatOpenAI;

  constructor(apiKey: string) {
    this.llm = new ChatOpenAI({
      apiKey: apiKey,
      modelName: 'gpt-3.5-turbo',
      temperature: 0.7,
    });
  }

  async processMessage(
    request: ChatRequest
  ): Promise<ChatResponse> {
    try {
      // プロンプトテンプレートの準備
      const template = PromptTemplate.fromTemplate(`
        {context_section}
        User: {message}
        
        Please provide a thoughtful and helpful response.
      `);

      const prompt = await template.format({
        context_section: request.context
          ? `Context: ${request.context}\n`
          : '',
        message: request.message,
      });

      // 温度パラメータの動的設定
      if (request.temperature !== undefined) {
        this.llm.temperature = request.temperature;
      }

      const response = await this.llm.invoke(prompt);

      return {
        success: true,
        response: response.content as string,
        metadata: {
          timestamp: new Date().toISOString(),
          model: 'gpt-3.5-turbo',
        },
      };
    } catch (error) {
      return {
        success: false,
        error:
          error instanceof Error
            ? error.message
            : 'Unknown error',
        metadata: {
          timestamp: new Date().toISOString(),
          model: 'gpt-3.5-turbo',
        },
      };
    }
  }
}

// HTTPサーバーの起動
async function startServer() {
  const apiKey =
    env['OPENAI_API_KEY'] || Deno.env.get('OPENAI_API_KEY');

  if (!apiKey) {
    console.error('OPENAI_API_KEY が設定されていません');
    Deno.exit(1);
  }

  const service = new LangChainService(apiKey);
  const port = 8000;

  console.log(
    `🦕 Deno + LangChain サーバーを起動中... http://localhost:${port}`
  );

  // HTTP サーバーの定義
  const handler = async (
    request: Request
  ): Promise<Response> => {
    const url = new URL(request.url);

    // CORS ヘッダー
    const corsHeaders = {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type',
    };

    if (request.method === 'OPTIONS') {
      return new Response(null, { headers: corsHeaders });
    }

    if (
      url.pathname === '/chat' &&
      request.method === 'POST'
    ) {
      try {
        const chatRequest =
          (await request.json()) as ChatRequest;
        const response = await service.processMessage(
          chatRequest
        );

        return new Response(
          JSON.stringify(response, null, 2),
          {
            status: response.success ? 200 : 500,
            headers: {
              'Content-Type': 'application/json',
              ...corsHeaders,
            },
          }
        );
      } catch (error) {
        return new Response(
          JSON.stringify({
            success: false,
            error: 'Invalid request format',
            metadata: {
              timestamp: new Date().toISOString(),
              model: 'unknown',
            },
          }),
          {
            status: 400,
            headers: {
              'Content-Type': 'application/json',
              ...corsHeaders,
            },
          }
        );
      }
    }

    // ヘルスチェックエンドポイント
    if (url.pathname === '/health') {
      return new Response(
        JSON.stringify({
          status: 'healthy',
          timestamp: new Date().toISOString(),
        }),
        {
          headers: {
            'Content-Type': 'application/json',
            ...corsHeaders,
          },
        }
      );
    }

    return new Response('Not Found', { status: 404 });
  };

  // サーバー起動
  await Deno.serve({ port }, handler);
}

// メイン実行
if (import.meta.main) {
  await startServer();
}

実行方法

bash# 開発モード(ファイル変更を監視)
deno task dev

# 本番実行
deno task start

# または直接実行
deno run --allow-net --allow-env --allow-read main.ts

Deno の強力な型システムと permission ベースのセキュリティにより、安全で保守性の高い LangChain アプリケーションを構築できます。

Bun での初期配線

Bun は高速実行と Node.js 互換性を両立したランタイムです。既存の npm エコシステムを活用しながら、パフォーマンスの向上を図れます。

プロジェクトセットアップ

bash# Bun のインストール(未インストールの場合)
curl -fsSL https://bun.sh/install | bash

# プロジェクト初期化
mkdir langchain-bun-app
cd langchain-bun-app
bun init -y

依存関係のインストール

bash# LangChain 関連パッケージ
bun add @langchain/openai @langchain/core @langchain/community
bun add langchain

# 開発依存関係
bun add -d @types/node typescript

TypeScript 設定

json// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "lib": ["ES2022", "DOM"],
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "types": ["bun-types"]
  },
  "include": ["**/*.ts"],
  "exclude": ["node_modules"]
}

高性能 LangChain サービス実装

typescript// src/langchain-service.ts
import { ChatOpenAI } from '@langchain/openai';
import { PromptTemplate } from '@langchain/core/prompts';
import { MemoryVectorStore } from 'langchain/vectorstores/memory';
import { OpenAIEmbeddings } from '@langchain/openai';
import { Document } from 'langchain/document';

interface ProcessingMetrics {
  startTime: number;
  endTime: number;
  tokensUsed: number;
  documentsProcessed: number;
}

export class HighPerformanceLangChainService {
  private llm: ChatOpenAI;
  private embeddings: OpenAIEmbeddings;
  private vectorStore: MemoryVectorStore | null = null;
  private metrics: ProcessingMetrics = {
    startTime: 0,
    endTime: 0,
    tokensUsed: 0,
    documentsProcessed: 0,
  };

  constructor(apiKey: string) {
    this.llm = new ChatOpenAI({
      apiKey: apiKey,
      modelName: 'gpt-3.5-turbo',
      temperature: 0.7,
      maxTokens: 1000,
    });

    this.embeddings = new OpenAIEmbeddings({
      apiKey: apiKey,
      modelName: 'text-embedding-ada-002',
    });
  }

  // Bunの高速処理を活かしたベクトルストア初期化
  async initializeVectorStore(
    documents: string[]
  ): Promise<void> {
    console.time('Vector store initialization');
    this.metrics.startTime = performance.now();

    // ドキュメントの並列処理(Bunの高速実行を活用)
    const docs = documents.map(
      (content, index) =>
        new Document({
          pageContent: content,
          metadata: { id: index, source: `doc_${index}` },
        })
    );

    this.vectorStore = new MemoryVectorStore(
      this.embeddings
    );
    await this.vectorStore.addDocuments(docs);

    this.metrics.endTime = performance.now();
    this.metrics.documentsProcessed = documents.length;
    console.timeEnd('Vector store initialization');
  }

  // 類似文書検索付きチャット
  async chatWithContext(
    query: string,
    maxRelevantDocs: number = 3
  ): Promise<{
    response: string;
    relevantDocs: Document[];
    processingTime: number;
  }> {
    if (!this.vectorStore) {
      throw new Error(
        'ベクトルストアが初期化されていません'
      );
    }

    const startTime = performance.now();

    // 関連文書の検索
    const relevantDocs =
      await this.vectorStore.similaritySearch(
        query,
        maxRelevantDocs
      );

    // コンテキストを含むプロンプト生成
    const contextText = relevantDocs
      .map((doc) => doc.pageContent)
      .join('\n\n---\n\n');

    const template = PromptTemplate.fromTemplate(`
      以下の文書を参考にして、質問に答えてください。

      参考文書:
      {context}

      質問: {query}

      回答:
    `);

    const prompt = await template.format({
      context: contextText,
      query: query,
    });

    const response = await this.llm.invoke(prompt);
    const endTime = performance.now();

    return {
      response: response.content as string,
      relevantDocs: relevantDocs,
      processingTime: endTime - startTime,
    };
  }

  // バッチ処理(Bunの並列処理能力を活用)
  async processBatch(queries: string[]): Promise<
    Array<{
      query: string;
      response: string;
      processingTime: number;
    }>
  > {
    console.time('Batch processing');

    // 並列処理でクエリを処理
    const results = await Promise.all(
      queries.map(async (query) => {
        const startTime = performance.now();
        const response = await this.llm.invoke(query);
        const endTime = performance.now();

        return {
          query: query,
          response: response.content as string,
          processingTime: endTime - startTime,
        };
      })
    );

    console.timeEnd('Batch processing');
    return results;
  }

  getMetrics(): ProcessingMetrics {
    return { ...this.metrics };
  }
}

Web サーバー実装

typescript// src/server.ts
import { HighPerformanceLangChainService } from './langchain-service';

const PORT = process.env.PORT || 3000;
const API_KEY = process.env.OPENAI_API_KEY;

if (!API_KEY) {
  console.error(
    'OPENAI_API_KEY 環境変数が設定されていません'
  );
  process.exit(1);
}

const service = new HighPerformanceLangChainService(
  API_KEY
);

// サンプルドキュメントでベクトルストアを初期化
const sampleDocs = [
  'LangChainは大規模言語モデルを活用したアプリケーション開発フレームワークです。',
  'Edge Computingにより、ユーザーに近い場所でコードを実行し、レイテンシを削減できます。',
  'Bunは高速なJavaScriptランタイムで、Node.jsの3-4倍の実行速度を実現します。',
  'TypeScriptは静的型付けによりコードの品質と保守性を向上させます。',
];

// 初期化処理
await service.initializeVectorStore(sampleDocs);
console.log('✅ ベクトルストアの初期化完了');

// HTTP サーバーの起動
const server = Bun.serve({
  port: PORT,
  async fetch(req) {
    const url = new URL(req.url);

    // CORS設定
    const corsHeaders = {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type',
    };

    if (req.method === 'OPTIONS') {
      return new Response(null, { headers: corsHeaders });
    }

    // チャットエンドポイント
    if (url.pathname === '/chat' && req.method === 'POST') {
      try {
        const { message } = await req.json();
        const result = await service.chatWithContext(
          message
        );

        return new Response(
          JSON.stringify(
            {
              success: true,
              response: result.response,
              relevantDocs: result.relevantDocs.length,
              processingTime: `${result.processingTime.toFixed(
                2
              )}ms`,
              metrics: service.getMetrics(),
            },
            null,
            2
          ),
          {
            status: 200,
            headers: {
              'Content-Type': 'application/json',
              ...corsHeaders,
            },
          }
        );
      } catch (error) {
        return new Response(
          JSON.stringify({
            success: false,
            error:
              error instanceof Error
                ? error.message
                : 'Unknown error',
          }),
          {
            status: 500,
            headers: {
              'Content-Type': 'application/json',
              ...corsHeaders,
            },
          }
        );
      }
    }

    // バッチ処理エンドポイント
    if (
      url.pathname === '/batch' &&
      req.method === 'POST'
    ) {
      try {
        const { queries } = await req.json();
        if (!Array.isArray(queries)) {
          throw new Error(
            'queries は配列である必要があります'
          );
        }

        const results = await service.processBatch(queries);

        return new Response(
          JSON.stringify(
            {
              success: true,
              results: results,
              totalQueries: queries.length,
              averageTime:
                results.reduce(
                  (sum, r) => sum + r.processingTime,
                  0
                ) / results.length,
            },
            null,
            2
          ),
          {
            status: 200,
            headers: {
              'Content-Type': 'application/json',
              ...corsHeaders,
            },
          }
        );
      } catch (error) {
        return new Response(
          JSON.stringify({
            success: false,
            error:
              error instanceof Error
                ? error.message
                : 'Unknown error',
          }),
          {
            status: 500,
            headers: {
              'Content-Type': 'application/json',
              ...corsHeaders,
            },
          }
        );
      }
    }

    return new Response('Not Found', { status: 404 });
  },
});

console.log(
  `🚀 Bun + LangChain サーバー起動: http://localhost:${PORT}`
);
console.log(`利用可能なエンドポイント:`);
console.log(`  POST /chat - 単一メッセージ処理`);
console.log(`  POST /batch - バッチ処理`);

実行スクリプト

json// package.json の scripts セクション
{
  "scripts": {
    "dev": "bun run --watch src/server.ts",
    "start": "bun run src/server.ts",
    "build": "bun build src/server.ts --outdir ./dist",
    "test": "bun test"
  }
}

実行方法

bash# 環境変数設定
export OPENAI_API_KEY="your-api-key-here"

# 開発実行
bun run dev

# 本番実行
bun run start

Bun の高速実行により、同じ処理でも従来の Node.js 環境と比べて大幅な性能向上が期待できます。特にベクトル検索やバッチ処理での差は顕著に現れるでしょう。

図で理解できる要点:

  • Cloudflare Workers は Wrangler CLI によるシンプルなデプロイが可能
  • Deno は Permission モデルによるセキュリティと型安全性を実現
  • Bun は Node.js 互換性を保ちながら大幅な性能向上を実現

実装パターンと最適化

Edge 環境での LangChain 実装には、従来のサーバーサイド実装とは異なる考慮点があります。共通のパターンと最適化手法を理解することで、より効果的なアプリケーションを構築できます。

共通の実装パターン

Edge 環境では以下のようなパターンが有効です。これらのパターンを組み合わせることで、堅牢で高性能なアプリケーションを構築できます。

以下の図は、Edge 環境における一般的なアーキテクチャパターンを示しています。

mermaidflowchart TB
    client[クライアント] --> edge[Edge Runtime]
    edge --> llm[LLM API]
    edge --> cache[キャッシュ層]
    edge --> external[外部データソース]

    subgraph "Edge Runtime"
        req_handler[リクエストハンドラー]
        prompt_gen[プロンプト生成]
        response_parse[レスポンス解析]
        error_handle[エラーハンドリング]
    end

    cache --> memory[メモリキャッシュ]
    cache --> kv[KVストレージ]

    external --> api[REST API]
    external --> vector_db[ベクトルDB]

この構成により、高速なレスポンス、効率的なキャッシュ、外部リソースへの適切なアクセスを実現できます。

1. ストリーミングレスポンスパターン

長時間の処理でタイムアウトを回避しつつ、ユーザーエクスペリエンスを向上させるパターンです。

typescript// ストリーミングレスポンスの実装例
export async function streamingChatHandler(
  request: Request
): Promise<Response> {
  const { message } = await request.json();

  // ReadableStream を作成
  const stream = new ReadableStream({
    async start(controller) {
      try {
        const llm = new ChatOpenAI({
          apiKey: process.env.OPENAI_API_KEY,
          streaming: true, // ストリーミングモードを有効化
          callbacks: [
            {
              handleLLMNewToken(token: string) {
                // トークンごとにクライアントに送信
                const data = `data: ${JSON.stringify({
                  token,
                })}\n\n`;
                controller.enqueue(
                  new TextEncoder().encode(data)
                );
              },
            },
          ],
        });

        await llm.invoke(message);

        // 完了シグナル
        const endData = `data: ${JSON.stringify({
          done: true,
        })}\n\n`;
        controller.enqueue(
          new TextEncoder().encode(endData)
        );
      } catch (error) {
        const errorData = `data: ${JSON.stringify({
          error: error.message,
        })}\n\n`;
        controller.enqueue(
          new TextEncoder().encode(errorData)
        );
      } finally {
        controller.close();
      }
    },
  });

  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      Connection: 'keep-alive',
    },
  });
}

2. コンテキスト圧縮パターン

メモリ制限のある Edge 環境で、効率的にコンテキストを管理するパターンです。

typescript// コンテキスト圧縮の実装
interface CompressedContext {
  summary: string;
  keyPoints: string[];
  recentMessages: Array<{ role: string; content: string }>;
}

export class ContextCompressor {
  private maxContextLength: number = 4000;
  private maxRecentMessages: number = 10;

  async compressContext(
    fullContext: string,
    recentMessages: Array<{ role: string; content: string }>
  ): Promise<CompressedContext> {
    // 最新メッセージは保持
    const limitedRecent = recentMessages.slice(
      -this.maxRecentMessages
    );

    // 長いコンテキストは要約
    if (fullContext.length > this.maxContextLength) {
      const summarizer = new ChatOpenAI({
        apiKey: process.env.OPENAI_API_KEY,
        modelName: 'gpt-3.5-turbo',
        temperature: 0.3,
        maxTokens: 500,
      });

      const summaryPrompt = `
        以下の長いコンテキストを200文字以内で要約してください。
        重要なポイントは別途箇条書きで3つまで抽出してください。

        コンテキスト: ${fullContext.substring(
          0,
          this.maxContextLength
        )}
        
        形式:
        要約: [要約文]
        重要ポイント:
        - [ポイント1]
        - [ポイント2] 
        - [ポイント3]
      `;

      const result = await summarizer.invoke(summaryPrompt);
      const content = result.content as string;

      // 要約とポイントを抽出
      const summaryMatch = content.match(
        /要約: (.+?)(?=\n|重要ポイント:|$)/s
      );
      const pointsMatch = content.match(
        /重要ポイント:\s*((?:- .+?\n?)+)/s
      );

      const summary =
        summaryMatch?.[1]?.trim() ||
        fullContext.substring(0, 200);
      const keyPoints =
        pointsMatch?.[1]
          ?.split('\n')
          .filter((line) => line.trim().startsWith('-'))
          .map((line) =>
            line.replace(/^-\s*/, '').trim()
          ) || [];

      return {
        summary,
        keyPoints,
        recentMessages: limitedRecent,
      };
    }

    return {
      summary: fullContext,
      keyPoints: [],
      recentMessages: limitedRecent,
    };
  }

  formatCompressedContext(
    context: CompressedContext
  ): string {
    let formatted = `背景情報: ${context.summary}\n\n`;

    if (context.keyPoints.length > 0) {
      formatted += `重要ポイント:\n${context.keyPoints
        .map((point) => `- ${point}`)
        .join('\n')}\n\n`;
    }

    formatted += `最近の会話:\n`;
    context.recentMessages.forEach((msg) => {
      formatted += `${msg.role}: ${msg.content}\n`;
    });

    return formatted;
  }
}

3. エラーレジリエンスパターン

Edge 環境での不安定な接続やタイムアウトに対応するパターンです。

typescript// リトライ機能付きLLM呼び出し
export class ResilientLLMService {
  private llm: ChatOpenAI;
  private maxRetries: number = 3;
  private retryDelay: number = 1000;

  constructor(apiKey: string) {
    this.llm = new ChatOpenAI({
      apiKey: apiKey,
      requestTimeout: 25000, // Edge環境のタイムアウトを考慮
      maxRetries: 1, // LangChain側のリトライは最小限に
    });
  }

  async invokeWithRetry(
    prompt: string,
    retryCount: number = 0
  ): Promise<{
    success: boolean;
    result?: string;
    error?: string;
  }> {
    try {
      const response = await this.llm.invoke(prompt);
      return {
        success: true,
        result: response.content as string,
      };
    } catch (error) {
      const errorMessage =
        error instanceof Error
          ? error.message
          : 'Unknown error';

      // リトライ可能なエラーかチェック
      const isRetryable =
        this.isRetryableError(errorMessage);

      if (isRetryable && retryCount < this.maxRetries) {
        console.log(
          `リトライ ${retryCount + 1}/${
            this.maxRetries
          }: ${errorMessage}`
        );

        // 指数バックオフでリトライ
        await this.sleep(
          this.retryDelay * Math.pow(2, retryCount)
        );
        return this.invokeWithRetry(prompt, retryCount + 1);
      }

      return {
        success: false,
        error: errorMessage,
      };
    }
  }

  private isRetryableError(errorMessage: string): boolean {
    const retryableErrors = [
      'timeout',
      'network',
      'rate limit',
      'temporary',
      '429', // Too Many Requests
      '502', // Bad Gateway
      '503', // Service Unavailable
      '504', // Gateway Timeout
    ];

    return retryableErrors.some((pattern) =>
      errorMessage.toLowerCase().includes(pattern)
    );
  }

  private sleep(ms: number): Promise<void> {
    return new Promise((resolve) =>
      setTimeout(resolve, ms)
    );
  }
}

パフォーマンス最適化

Edge 環境でのパフォーマンス最適化は、レスポンス速度とリソース使用量の両面から考える必要があります。

1. プロンプトキャッシング

typescript// LRU キャッシュでプロンプトレスポンスを管理
class LRUCache<T> {
  private cache = new Map<string, T>();
  private maxSize: number;

  constructor(maxSize: number = 100) {
    this.maxSize = maxSize;
  }

  get(key: string): T | undefined {
    const value = this.cache.get(key);
    if (value) {
      // アクセス順を更新(最新に移動)
      this.cache.delete(key);
      this.cache.set(key, value);
    }
    return value;
  }

  set(key: string, value: T): void {
    if (this.cache.has(key)) {
      this.cache.delete(key);
    } else if (this.cache.size >= this.maxSize) {
      // 最も古いアイテムを削除
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);
    }
    this.cache.set(key, value);
  }

  has(key: string): boolean {
    return this.cache.has(key);
  }
}

export class CachedLLMService {
  private llm: ChatOpenAI;
  private cache = new LRUCache<string>(50);
  private cacheTimeout = 1000 * 60 * 10; // 10分

  constructor(apiKey: string) {
    this.llm = new ChatOpenAI({ apiKey });
  }

  private generateCacheKey(prompt: string): string {
    // プロンプトのハッシュを生成(簡易版)
    let hash = 0;
    for (let i = 0; i < prompt.length; i++) {
      const char = prompt.charCodeAt(i);
      hash = (hash << 5) - hash + char;
      hash = hash & hash; // 32bit整数に変換
    }
    return `prompt_${Math.abs(hash)}`;
  }

  async invoke(prompt: string): Promise<string> {
    const cacheKey = this.generateCacheKey(prompt);

    // キャッシュから確認
    if (this.cache.has(cacheKey)) {
      console.log('キャッシュヒット:', cacheKey);
      return this.cache.get(cacheKey)!;
    }

    // LLM API 呼び出し
    console.log('LLM API 呼び出し:', cacheKey);
    const response = await this.llm.invoke(prompt);
    const result = response.content as string;

    // キャッシュに保存
    this.cache.set(cacheKey, result);

    // 一定時間後にキャッシュから削除
    setTimeout(() => {
      if (this.cache.has(cacheKey)) {
        this.cache.delete(cacheKey);
      }
    }, this.cacheTimeout);

    return result;
  }
}

2. バッチ処理最適化

typescript// 効率的なバッチ処理実装
export class OptimizedBatchProcessor {
  private llm: ChatOpenAI;
  private batchSize: number = 5;
  private concurrentBatches: number = 2;

  constructor(apiKey: string) {
    this.llm = new ChatOpenAI({
      apiKey,
      maxConcurrency: this.concurrentBatches,
    });
  }

  async processBatch(
    prompts: string[]
  ): Promise<
    Array<{
      prompt: string;
      response: string;
      error?: string;
    }>
  > {
    const results: Array<{
      prompt: string;
      response: string;
      error?: string;
    }> = [];

    // プロンプトをバッチに分割
    const batches = this.chunkArray(
      prompts,
      this.batchSize
    );

    console.log(
      `${prompts.length}件のプロンプトを${batches.length}バッチで処理開始`
    );

    // バッチを順次処理(メモリ使用量を制御)
    for (let i = 0; i < batches.length; i++) {
      const batch = batches[i];
      console.log(
        `バッチ${i + 1}/${batches.length} 処理中...`
      );

      // バッチ内のプロンプトを並列処理
      const batchPromises = batch.map(async (prompt) => {
        try {
          const startTime = performance.now();
          const response = await this.llm.invoke(prompt);
          const endTime = performance.now();

          console.log(
            `プロンプト処理完了: ${(
              endTime - startTime
            ).toFixed(2)}ms`
          );

          return {
            prompt: prompt,
            response: response.content as string,
          };
        } catch (error) {
          return {
            prompt: prompt,
            response: '',
            error:
              error instanceof Error
                ? error.message
                : 'Unknown error',
          };
        }
      });

      const batchResults = await Promise.all(batchPromises);
      results.push(...batchResults);

      // バッチ間でのメモリ解放(ガベージコレクションを促す)
      if (i < batches.length - 1) {
        await this.sleep(100); // 短い休憩でシステムリソースを開放
      }
    }

    const successCount = results.filter(
      (r) => !r.error
    ).length;
    const errorCount = results.filter(
      (r) => r.error
    ).length;

    console.log(
      `バッチ処理完了: 成功 ${successCount}件, エラー ${errorCount}件`
    );

    return results;
  }

  private chunkArray<T>(array: T[], size: number): T[][] {
    const chunks: T[][] = [];
    for (let i = 0; i < array.length; i += size) {
      chunks.push(array.slice(i, i + size));
    }
    return chunks;
  }

  private sleep(ms: number): Promise<void> {
    return new Promise((resolve) =>
      setTimeout(resolve, ms)
    );
  }
}

3. メモリ使用量最適化

typescript// メモリ効率的なドキュメント処理
export class MemoryEfficientDocProcessor {
  private llm: ChatOpenAI;
  private maxDocumentSize: number = 2000; // 文字数制限

  constructor(apiKey: string) {
    this.llm = new ChatOpenAI({ apiKey });
  }

  async processLargeDocument(
    document: string,
    query: string
  ): Promise<{
    response: string;
    chunksProcessed: number;
    memoryUsage: string;
  }> {
    const startMemory = this.getMemoryUsage();

    // ドキュメントをチャンクに分割
    const chunks = this.splitDocument(
      document,
      this.maxDocumentSize
    );
    console.log(
      `ドキュメントを${chunks.length}チャンクに分割`
    );

    const relevantChunks: string[] = [];

    // チャンクを順次処理(メモリ使用量を制御)
    for (let i = 0; i < chunks.length; i++) {
      const chunk = chunks[i];

      // 関連性チェック
      const isRelevant = await this.checkRelevance(
        chunk,
        query
      );

      if (isRelevant) {
        relevantChunks.push(chunk);
      }

      // 定期的なガベージコレクション促進
      if (i % 5 === 0) {
        await this.sleep(10); // システムにGCの機会を与える
      }
    }

    console.log(
      `${chunks.length}チャンク中${relevantChunks.length}チャンクが関連`
    );

    // 関連チャンクのみで回答生成
    const context = relevantChunks.slice(0, 5).join('\n\n'); // 上位5チャンクのみ使用
    const response = await this.generateResponse(
      context,
      query
    );

    const endMemory = this.getMemoryUsage();

    return {
      response: response,
      chunksProcessed: relevantChunks.length,
      memoryUsage: `開始: ${startMemory}, 終了: ${endMemory}`,
    };
  }

  private splitDocument(
    document: string,
    maxSize: number
  ): string[] {
    const chunks: string[] = [];
    const sentences = document.split(/[.!?]+/);

    let currentChunk = '';
    for (const sentence of sentences) {
      if (currentChunk.length + sentence.length < maxSize) {
        currentChunk += sentence + '. ';
      } else {
        if (currentChunk) {
          chunks.push(currentChunk.trim());
        }
        currentChunk = sentence + '. ';
      }
    }

    if (currentChunk) {
      chunks.push(currentChunk.trim());
    }

    return chunks;
  }

  private async checkRelevance(
    chunk: string,
    query: string
  ): Promise<boolean> {
    const prompt = `
      以下のテキストチャンクが質問に関連しているかを「はい」または「いいえ」で答えてください。

      チャンク: ${chunk.substring(0, 500)}
      質問: ${query}
      
      関連性:
    `;

    try {
      const response = await this.llm.invoke(prompt);
      return response.content
        .toString()
        .toLowerCase()
        .includes('はい');
    } catch (error) {
      console.warn('関連性チェックでエラー:', error);
      return true; // エラー時は保守的に関連ありとする
    }
  }

  private async generateResponse(
    context: string,
    query: string
  ): Promise<string> {
    const prompt = `
      以下のコンテキストを参考にして、質問に答えてください。

      コンテキスト: ${context}
      質問: ${query}
      
      回答:
    `;

    const response = await this.llm.invoke(prompt);
    return response.content as string;
  }

  private getMemoryUsage(): string {
    if (
      typeof performance !== 'undefined' &&
      performance.memory
    ) {
      const memory = (performance as any).memory;
      return `${Math.round(
        memory.usedJSHeapSize / 1024 / 1024
      )}MB`;
    }
    return 'N/A';
  }

  private sleep(ms: number): Promise<void> {
    return new Promise((resolve) =>
      setTimeout(resolve, ms)
    );
  }
}

図で理解できる要点:

  • ストリーミングレスポンスでタイムアウト回避と UX 向上を両立
  • コンテキスト圧縮でメモリ制約に対応
  • キャッシングとバッチ処理でパフォーマンスを最適化

まとめ

LangChain を Edge 環境で動作させることは、従来のサーバーサイドアプリケーションとは異なる挑戦ですが、適切な理解と実装により大きなメリットを得られます。

各プラットフォームの使い分け

Cloudflare Workers は、グローバルな分散実行と自動スケーリングが最大の魅力です。API エンドポイントとして LangChain を提供する場合や、世界中のユーザーに低レイテンシでサービスを提供したい場合に最適でしょう。制約はありますが、Web 標準 API の範囲内であれば非常に高いパフォーマンスを発揮します。

Deno は、セキュリティと TypeScript 開発体験を重視する場合の最良の選択です。Permission-based のセキュリティモデルにより、本番環境でも安心してデプロイできます。開発チームが型安全性を重視し、セキュアなアプリケーションを構築したい場合に推奨されます。

Bun は、既存の Node.js エコシステムを活用しながら高速実行を実現したい場合に適しています。計算集約的な処理やバッチ処理でその真価を発揮し、従来比 3-4 倍の性能向上が期待できます。

実装時の重要ポイント

Edge 環境での成功には、以下の点が重要です:

  • 制約の理解: ファイルシステム、メモリ、実行時間の制約を理解し、それに応じた実装を行う
  • 最適化戦略: キャッシング、ストリーミング、バッチ処理などの手法を適切に組み合わせる
  • エラーハンドリング: 不安定な環境に対応するための堅牢なリトライ機能とエラー処理
  • 監視とログ: パフォーマンスを継続的に監視し、改善点を特定する仕組みの構築

今後の発展

Edge Computing 環境はまだ発展途上の技術領域です。各プラットフォームも継続的にアップデートされ、制約が緩和されたり新機能が追加されたりしています。LangChain 自体も Edge 対応が進んでおり、今後はより多くの機能が Edge 環境で利用可能になるでしょう。

特に注目すべきは、WebAssembly を活用したより軽量な LLM の実行や、Edge-specific な新しい LangChain モジュールの登場です。これらの技術革新により、Edge 環境での AI アプリケーション開発はさらに身近なものとなっていくと予想されます。

Edge 環境での LangChain 活用は、従来のサーバーサイドアプリケーションでは実現できなかった、新しいユーザーエクスペリエンスとパフォーマンスを提供する可能性を秘めています。適切な知識と実装により、次世代の AI アプリケーションの基盤を構築していきましょう。

関連リンク