README.ja.md 9.4 KB

Editor Assistant API 実装解説

要求仕様

Editor Assistant API は、OpenAI AssistantAPI を使用して、マークダウンエディタの編集をサポートする機能です。主な要件は以下の通りです:

  1. ストリーミング処理

    • OpenAI からの応答をストリーミングで受け取り、Server-Sent Events (SSE) でクライアントにリアルタイムに転送
    • JSON データを適切なタイミングで解析し、クライアントに送信
  2. データ形式

    • SSE による応答は SseMessageSchema, SseDetectedDiffSchema, SseFinalizedSchema に準拠した JSON 形式
    • { message: "..." } と delta 形式の差分情報(insert, delete, retain)を含む
  3. エラーハンドリング

    • 不完全な JSON データの処理時のエラーを適切に処理
    • リソースリークの防止
  4. 効率性

    • メモリ使用量を最小限に抑える
    • 不要な通信を避け、クライアントへの適切なタイミングでのデータ送信を実現
    • メッセージの増分送信による通信量削減と、すでに処理済みの要素のスキップによる処理効率の向上

重要なインプット

実装時に参照したコード

  1. jsonrepair ライブラリ

    • 壊れた JSON や不完全な JSON を修復するライブラリ
    • 特に部分的なストリーミング JSON の処理に有効
  2. 型定義

    • message-error.ts: エラー型と定義
    • schema.ts: エディタアシスタントのメッセージと差分の Zod スキーマ定義

今後のリファクタリングに重要なインプット

  1. OpenAI API の仕様変更

    • AssistantAPI のレスポンス形式の変更に注意
  2. jsonrepair のアップデート

    • 新バージョンでの API 変更や最適化手法の変更を確認
  3. パフォーマンス監視

    • メモリ使用量と処理時間のモニタリング
    • 大規模 JSON 処理時のボトルネック特定

実装のポイント

1. ストリーミング処理と不完全JSONの修復

ストリーミング処理において、最大の課題は不完全なJSON文字列の処理です。OpenAI APIから部分的に届くJSONデータを即座に解析するために、以下の対策を実装しています:

  • jsonrepair ライブラリの採用理由
    • 通常、JSON文字列は完全な形でなければパースできません。これはストリーム処理において大きな制約となります。
    • 全ての文字列を受け取るまで待たずに、途中経過をリアルタイムにユーザーに提示するため、jsonrepairを使用して部分的なJSON文字列を修復しています。
    • これにより、メッセージと差分情報を受信次第、速やかにクライアントに届けることが可能になり、ユーザー体験が大幅に向上します。

具体例:

  // ストリームから受け取った不完全なJSONの例
  const partialJson = '{"contents": [{"message": "テキストを修正し';
  
  // 通常のJSON.parseではエラー
  // JSON.parse(partialJson); // SyntaxError: Unexpected end of JSON input
  
  // jsonrepairを使用した修復
  const repairedJson = jsonrepair(partialJson);
  // 結果: '{"contents": [{"message": "テキストを修正しています"}]}'
  
  // 修復されたJSONはパース可能
  const parsedJson = JSON.parse(repairedJson);
  // 結果: { contents: [{ message: 'テキストを修正しています' }] }
  • このように、正常なJSONとして完結していない途中のデータでも、jsonrepairは欠けている部分を補完して有効なJSONに変換します。OpenAI APIからの応答では、完全なJSONが揃うまで待つことなく、部分的に受信したデータを即座に処理できるようになります。

  • rawBufferの累積と継続的な解析

    • 受信したテキストチャンクをrawBufferに累積し、その都度jsonrepairでパース可能な形に修復しています。
    • これは特にOpenAI APIの応答がJSON形式で指定されているにもかかわらず、ストリームではその一部だけが届く特性に対応するための実装です。

2. 差分検出と適応的送信制御

エディタアシスタントの核心部分は、OpenAI APIからのレスポンスから差分情報を適切に抽出し、効率的にクライアントに送信する機能です。以下のような工夫を行っています:

  • メッセージと差分の処理の統合と最適化

    • UI/UX要件に基づく設計として、メッセージと差分の処理を単一ループで効率的に実装しています。
    • メッセージ処理:メッセージの「増分」(新しく追加された部分)のみをクライアントに送信します。これにより通信量を削減し、クライアント側の処理負荷を軽減します。
    • 差分処理:JSONノードとして確定した差分は即座に検出し通知します。ただし、確定していない(変更中の可能性がある)差分は送信を控えることでエディタの過剰な更新を防止します。
  • 処理効率の向上メカニズム

    • processedMessages Mapを使って、各メッセージ要素の前回の内容を記録し、差分のみを計算します。
    • lastProcessedContentLength を用いて、すでに処理済みの要素をスキップします。これにより大量のデータでも効率的に処理できます。

      // 処理開始位置の最適化 - 確定済み要素のスキップ
      const startProcessingIndex = Math.max(0, Math.min(this.lastProcessedContentLength, contents.length) - 1);
        
      // 単一ループでメッセージと差分を処理
      for (let i = startProcessingIndex; i < contents.length; i++) {
      // メッセージと差分の処理
      }
      
  • OpenAIストリームの特性に対応した差分確定判定

    • OpenAI APIからのJSONストリームは「前方から順に確定していく」特性があります。このAPIの特性を活用し、以下の判定ロジックを実装しています:

      // 最終要素が変化した、またはこれが最終要素ではない場合 → 差分を確定とみなす
      if (i < currentContentIndex || currentContentIndex > this.lastContentIndex) {
      // 差分を確定して送信リストに追加
      }
      
    • この条件判定は単なる技術的工夫ではなく、UXの向上を目的としています。確定していない差分を頻繁に送信すると、エディタが頻繁に更新されてユーザー体験が悪化するためです。

  • 重複防止メカニズム

    • 差分の重複送信を避けるため、一意のキーを生成するgetDiffKeyメソッドを実装しています。
    • Setデータ構造(sentDiffKeys)を使うことで、O(1)の時間複雑度で効率的に重複チェックを行います。
    • この実装は、ストリームデータの累積的な性質(同じデータが何度も現れる可能性がある)に対応するために不可欠です。
  • 増分メッセージ計算の最適化

    • メッセージ要素ごとに前回のメッセージとの差分を計算するgetAppendedContentメソッドを実装しています。
    • これにより、クライアントには新たに追加された部分のみを送信でき、通信量を大幅に削減できます。

      private getAppendedContent(previousMessage: string, currentMessage: string): string {
      // 前回のメッセージから増分部分のみを返す
      return currentMessage.slice(previousMessage.length);
      }
      

3. エラー耐性とリソース管理

ストリーミング処理においてエラー耐性とリソース管理は特に重要です。以下の対策を講じています:

  • エラーハンドリングの階層化

    • JSONパースエラーはデバッグ用にログ出力するのみとし、処理を継続します。これはストリーミングの性質上、部分的なデータでパースエラーが発生するのは正常な動作だからです。
    • 重大なエラーはクライアントに適切に通知し、リソースを解放します。
  • リソース解放の徹底

    • クライアント切断時やエラー発生時、処理完了時など、あらゆるシナリオでリソースを確実に解放するクリーンアップ処理を実装しています。
    • destroyメソッドでメモリキャッシュをクリアし、イベントリスナーを解除することで、メモリリークを防止しています。
  • 非同期ストリーム処理の安全な終了

    • ストリームの終了を適切に検出し、完全な結果を送信してから接続を終了する機構を設けています。
    • エラー時でも可能な限り正常な形でレスポンスを返し、クライアント側での復旧を容易にします。

このような設計と実装により、リアルタイム性と正確性を両立したエディタアシスタント機能を実現しています。ストリーミング処理の特性を活かしつつ、効率的なデータ処理と適応的な通知制御によって優れたユーザー体験を提供しています。