メインコンテンツへスキップ
見出し画像

OpenAI Assistants APIを使って社内用GPTsを作った際に苦労した5つの点

    「テクノロジーで人々を適切な医療に案内する」をミッションに、ヘルスケアプラットフォームを提供しているUbie株式会社の八木(@sys1yagi)です。ソフトウェアエンジニアとして医療機関事業のプロダクト開発や新規事業の立ち上げなどを行っています。

    Ubieでは「Dev Genius」という名前の社内版ChatGPTを内製しています。

    画像
    Dev-Geniusでセッションを開始する画面

    本記事では、OpenAI Assistants APIを使って、GPTsに似た機能を社内版ChatGPTに追加した際に苦労した点についてまとめます。

    この記事はUbieアドベントカレンダー25日目にエントリーしています。

    社内用GPTsを開発する背景

    2023年の4月頃に社内版ChatGPTを開発しました。

    その後さまざまな改良が進み、現在では社内で広く活用が進んでいます。

    一方で、単純にLLMのモデルを利用できるというだけでは解決できないユースケースも増えてきました。たとえばインターネット上のコンテンツや検索結果を用いた回答の生成や、PDFなどのドキュメントファイルの内容に基づいた回答の生成などです。

    そのような機能を、LangChain.jsのAgentsを使って試作したりしましたが、使えるレベルにするにはそれなりの作り込みが必要だなと感じていました。そうした折にGPTsやAssistants APIが発表されたので、利用を検討することにしました。

    APIを直接使うかLangchainを使うか

    Assistants APIはすでにLangchain.jsでサポートされています。

    AgentExecutorを用いてAssistants APIの呼び出しを隠蔽してくれます。

    検討を開始した時点では、そもそもAssistants APIで何ができるのか、どのように使うのか、Langchain.jsがどのようにそれらをサポートしているのか分かっていませんでした。また、Assistants APIはβ版で今後どのように機能やインタフェースが変化するかも分かりません。そのため公式ライブラリのopenai-nodeを用いて、直接Assistants APIを扱うことにしました。

    できあがったもの

    Assistants APIのほぼすべての機能を使い、OpenAIのGPTsに近いものを構築できました。

    画像
    独自にGPTsを作り、様々なことができるようになった

    社内用GPTsは次のことができます。

    • 任意のGPTsを作成できる

      • GPTsに指示書を登録し、振る舞いを定義できる(およそ30000文字まで設定できます)

      • Code Interpreterによるプログラムの生成と実行

      • Retrievalによるファイルを用いた知識の埋め込み

      • Function Callingによる機能拡張

        • URLのコンテンツをフェッチする機能、検索する機能を追加しました

    • 自分が作成したGPTsや他者が公開したGPTsを使って会話ができる

    • メモリが最適化された会話ができる (Assistants APIがモデルの性能に合わせて回答に利用する会話長を自動的に調整し、できるだけコンテキストを維持して会話し続けられます)

    インターネット上のコンテンツや検索結果を用いた回答の生成や、PDFなどのドキュメントファイルの内容に基づいた回答の生成だけでなく、様々な活用や機能拡張が可能です。Retrievalを使って、Dev-Geniusのデータベース定義ファイルを埋め込み、データベースに対してどのような変更をするとよいかなどを相談できるGPTsを作りましたが、非常に便利です。

    GPTsとの違いもいくつかあります。

    • ActionsやWeb Browsing、DALL·E 3による画像生成などの機能はない

      • Function Callingを用いて自前で作る必要があります

    • GPTs Builderのような指示書の自動生成

      • 自前で作り込めば可能ですが今回はやっていません

    • アイコンの設定

      • 自前で作り込めば可能ですが今回はやっていません

    苦労した点

    社内用GPTsを作っていくに当たって色々と苦労した点がありました。以下にまとめていきます。

    1. 仕様の把握が大変

    Assistants APIの仕様や使い方はAssistants API Overviewでざっくり把握できます。Chat Completionsと比べて、Assistants APIはかなり複雑で、理解するまでに時間がかかりました。Chat CompletionsのAPIは一つだけで、利用者が使い方を柔軟に設計できます。しかし、Assistants APIは多くのAPIが存在し、さらにデータの永続化もAPI側で管理されているため、アプリケーション側の設計に落とし込むのが大変です。Assistants APIを使う上で知っておくべき主要なキーワードは以下のとおりです。

    • アシスタント

      • GPTsの本体。指示書やCode Interpreter、Retrieval、Fanction Callingの設定、ファイルの添付をします。CRUDのAPIがあります。IDをアプリケーション側で管理する必要があります。

    • スレッド

      • アシスタントとの会話のセッション。一連のメッセージやランを持っています。CRUDのAPIがあります。IDをアプリケーション側で管理する必要があります。

    • メッセージ

      • ユーザが入力したメッセージやアシスタントのレスポンス。ファイルの添付もできます。作成、更新、取得のAPIがあります。更新はメタデータの更新しかできません。

    • ラン

      • 実行結果。Assistants APIではスレッドに対してメッセージを追加したあと、ランをスタートします。ランには実行状況や実行内容を表すランステップが存在します。作成、更新、取得のAPIがあります。更新はメタデータの更新しかできません。

    • ランステップ

      • ランの中で何をしたかの表す単位。message_creation、tool_callsの二種類があり、回答を生成するまでの過程を参照できます。取得のAPIのみあります。

    各キーワードの関係性を以下に図示します。自分の理解したイメージなので、実際とは異なる点があるかもしれません。

    画像
    入れ子の関係が多く、ややこしい

    特にメッセージ、ラン、ランステップの関係はかなり混乱しました。またCode InterpreterやFunction Callingの結果はランステップにしか存在しない情報なので、スレッドに対するメッセージを取り出しただけでは抜け落ちてしまいます。処理結果をどのように取り扱うのかは予め考えておく必要があります。今回どのように扱うことにしたかについては、「永続化の仕方に迷う」の項で記述します。

    2. 実行の状態管理が大変

    Assistants APIでは、スレッドにメッセージを追加したあと、ランを作成することで回答を得られます。ランには実行の状態遷移が存在し、その状態を確認するためにランの取得を定期的に行う必要があります。

    以下は、その定期的な状態確認(ポーリング)を行うコードの例です。

    const pollingSpan = 1500;
    // ランを開始する。呼び出し後に即座にresponseオブジェクトをリターンしたいので、awaitしないスタイルで呼び出す。
    openai.beta.threads.runs.create(threadId, { assistant_id: assistantId })
      .then((result) => {
        const runId = result.id;
        const polling = async () => {
          try {
            const result = await openai.beta.threads.runs.retrieve(threadId, runId);
            if (result.status === 'RequiresAction') {
              // Function Callingの処理
              // ...
              setTimeout(polling, pollingSpan);
            } else if (result.status === 'Completed') {
              // 完了時の処理
              writer.close();
            } else if (result.status === 'Failed') {
              // 失敗時の処理
              writer.close();
            } else {
              // その他の状態の場合、再度ポーリングをスケジュールする
              setTimeout(polling, pollingSpan);
            }
          } catch (e) {
            // エラー処理
            writer.close();
          }
        };
        setTimeout(polling, pollingSpan);
      })
      .catch((e) => {
        // エラー処理
        writer.close();
      });

    setTimeoutを使用して定期的に状態を確認しています。上記コードには書いていないですが、実際にはSSEを用いて随時ユーザ側に状態を返却しています。これにより、一つのメッセージ送信を一つのリクエストで完了させる構造になっています。

    ランの実行完了までにFunction Callingで利用者側のレスポンスを要求する場合があるため、こういった仕様になるのは仕方ないのですが、自分でポーリングを管理するのは煩雑です。この問題はLimitationsに記載があり、将来解決されるのではないかと思いますが、現状では対応が必要です。

    例示した実装では、ポーリングの最中に予期せぬエラーが発生したり、実装の問題で復帰できない問題が発生した時に、「実行中のランを放置する(実行中のランがあると新しいランは作れない)」とか「進行不能のままずっとポーリングし続ける」といった状態に陥る場合があります。今回の実装では次の方針で進めることにしました。

    • 予期せぬエラーが出たらポーリングを終了する

    • 進行不能の場合は新しいスレッドを作ってもらう

    また、ランが完了するまで時間がかかるケースがあるので、画面上でステータスをアニメーションで表示するようにしています。

    画像
    ときには1分以上待つ場合も

    3. 永続化の仕方に迷う

    Assistants APIを使うに当たって永続化が必要な情報は以下の2つです。

    • アシスタントのID

    • スレッドのID

    これらを永続化しておけば、この他の情報はすべてAPIを通して取得できます。当初はこれらの情報に加えて、タイトルや説明文など、利用する上で便利になりそうな情報を付加したテーブルを持っていました。しかし、メッセージとラン、ランステップの関係の複雑さから、ユーザに表示するすべての情報を永続化することにしました。

    もしアシスタントのIDとスレッドのIDしか永続化していない場合、スレッド上の会話の結果を取り出すには次のような処理が必要です。

    • スレッドIDを用いてラン一覧を取り出す

      • カーソルページネーションの方式のため、数が多い場合に全てのランを取り出すためには再帰的な処理が必要になります

    • スレッドIDとランIDを用いてランステップ一覧を取り出す

      • ランステップは生成したメッセージIDか、Code Interpreterなどのツール実行結果を含んでいます

    • スレッドIDを用いてメッセージ一覧を取り出す

      • カーソルページネーションの方式で、数が多い場合に全てのメッセージを取り出すためには再帰的な処理が必要になります

    • ランステップとメッセージ一覧を合成し、時系列に並べる

    スレッドを開いたり、ランが完了して情報を更新する度にこれらの処理をするのはあまり現実的ではないように思えます。

    ユーザに表示する情報は全て永続化することにし、最終的に用意したテーブルは以下の通りです。

    • open_ai_assistants

      • アシスタントの情報を格納するテーブル

    • open_ai_assistant_tools

      • アシスタントで利用可能なツールの情報を格納するテーブル。ツールの種類や関数の名前や説明、パラメータなどの情報が含まれます

    • open_ai_assistant_threads

      • アシスタントのスレッドの情報を格納するテーブル。スレッドの作成日時、タイトル、可視性レベルなどの情報が含まれます

    • open_ai_assistant_uploaded_files

      • OpenAI APIにアップロードしたファイルの情報を格納するテーブル

    • open_ai_assistant_attach_files

      • アシスタントに添付したファイルの情報を格納するテーブル

    • open_ai_assistant_thread_run_step

      • スレッド内で実行したステップの情報を格納するテーブル。ステップの作成日時や実行したステップのIDなどの情報が含まれます

    • open_ai_assistant_thread_message

      • スレッド内のメッセージの情報を格納するテーブル。メッセージの作成日時や役割、実行したステップのIDなどの情報が含まれます

    • open_ai_assistant_message_attach_files

      • メッセージに添付したファイルの情報を格納するテーブル

    • open_ai_assistant_thread_message_content

      • スレッド内のメッセージの文章を格納するテーブル

    • open_ai_assistant_thread_tool_call

      • スレッドで実行したツールの呼び出し情報を格納するテーブル

    • open_ai_assistant_thread_tool_call_code_call

      • Code Interpreterの結果を格納するテーブル

    • open_ai_assistant_thread_tool_call_retrieval_call

      • Retrievalの結果を格納するテーブル

    • open_ai_assistant_thread_tool_call_function_call

      • Function Callingの結果を格納するテーブル

    当初の想定よりはかなり大変な作業になりました。実現を優先したので、最適化の余地はあるのではないかと思います。ファイル添付はしないとか、Function Callingはサポートしないなど割り切るともっとシンプルになるかなと思います。

    4. ファイルの管理方法に迷う

    アシスタントにファイルを添付して事前知識を組み込むことができます。また、会話中にメッセージにファイルを添付して回答を得ることも可能です。これは「file_id」を指定して行う形になります。ファイル操作用のAPIを使用してファイルをアップロードし、得られた「file_id」をアシスタントやメッセージに設定します。

    ファイルの管理と添付の仕方をどのようにするか迷いました。今回はアシスタントへのファイル添付は、ファイルアップロードとアシスタントへの添付の画面を別々に用意しました。

    画像
    まずはファイルを個別にアップロードする

    アシスタントの編集画面でアップロードしたファイルのIDを選択する形になっています。

    画像
    予めアップロードしたファイルを選択するスタイル

    メッセージでの添付は、ファイルアップロードとメッセージ送信を同時に行うスタイルを採用しています。

    画像
    メッセージと同時にファイルを添付する

    ファイルアップロード用の画面では、アップロードしたファイルを個別に削除できるように設定しています。逆に、アシスタントやスレッドを削除した際にファイルを削除するといった操作は行っていません。

    画像
    アップロードしたファイルを全て表示する

    ファイル用のAPIが独立していたため、アップロードしたファイルをアシスタントやメッセージに添付する、という考え方で設計してしまいました。アシスタントの操作と同時にファイルのアップロードや削除を行ったり、メッセージで添付したファイルはスレッドの削除の際以外は操作できないようにするといった形にすればよかったなと考えています。

    5. アシスタントやスレッド、ファイルがOrganization単位で管理されている

    Assistants APIを用いて作成したアシスタントやスレッドやファイルはOrganizationに対して一つの空間で管理されています。利用しているAPIキーに関係なく、全てのアシスタントやスレッドやファイルは、Organization内のAPIキーであれば操作できます。

    OpenAIの管理画面にログインすると以下のようにアシスタントとファイルのメニューが存在します。

    画像
    OpenAIの管理画面のメニュー

    Organization内で作成したアシスタントやアップロードしたファイルを全て閲覧できます。

    Data access guidanceでは、認可を実装する、APIキーの利用を制限する、アカウントを分けるといった対策が書かれています。

    今回の実装では、アシスタント、スレッド、ファイルをユーザに紐付けることで、アプリケーション上でのアクセスを制限する仕組みを導入しました。ただし、OpenAIの管理画面上では閲覧できてしまうため、事前に注意事項としてアナウンスし、許容する形としています。

    現在のAssistants APIのデータアクセスの仕組みでは、実際のプロダクトでの利用は現実的ではありません。今後のアップデートに期待しつつ、しばらくは社内の環境での実験に留める形になりそうです。

    さいごに

    本記事ではAssistants APIを用いて社内用GPTsを作った際に苦労した点について説明しました。β版ということもあって、複雑だったり、未整備な部分もあるほか、データアクセス部分にも課題がありますが、社内で利用する分には十分強力な仕組みが構築できると感じました。公開後、セールスチームがRetrievalを使用した実験を開始したり、簡単な検索体験の検証を行う仕組みを試作したり、活用が始まっています。今後どのような使い方が生まれてくるか楽しみです。

    本記事がAssistants APIを使ったアプリケーションを作る際の検討のお役に立てれば幸いです。

    【採用関連サイト】

    あなたへのおすすめ