every Tech Blog

株式会社エブリーのTech Blogです。

ビジネスサイドにChatGPTの利用を習慣づけられるといいなーと思ってる話

はじめまして、データ&AIチームのoyabuです。データストラテジストというデータアナリストみたいな仕事をしてます

具体的には「DELISH KITCHEN」アプリのデータを抽出、分析して食品メーカーなどのクライアントさまの施策や提案にご活用頂く仕事をしています

今回はLLM(ChatGPT、OpenInterpreter)をビジネスサイドに活用してもらうためにやったことの話をします

まだ始めたばかりなので、成果自体はそんなに出てないですが、考えたことや、得られた知見を共有したいと思います

やったことは以下です

  • ChatGPTの使い方ドキュメントの作成(禁忌とユースケースの整理)
  • ChatGPTの使い方勉強会
  • OpenInterpreterを隣に座ってるカスタマーサクセスの方に試してもらう

きっかけ(課題感)

そもそもこんなことをやり始めたきっかけとしては以下な感じです

  • 大前提としてLLMが猛威をふるっており、自分としても十分その利益を享受している
    • ビジネスマンにとって、ChatGPTスキル = 今のExcelスキルぐらいになることは目に見えてる
  • ビジネスサイドでもChatGPTを使ってる人はいるが、あんまり会話に出てこない
    • 社内のSlackを漁ってもChatGPTの話をしてる人はだいたいエンジニア
    • 貼ってるアンテナ的にもそもそもの周辺情報がエンジニアには集まって来やすい
    • 気になってはいて、1回試したものの、業務に使うイメージが湧いてない方がなんだかんだ多いのかなーという妄想
  • 向き合いの部署としばらく働いて、業務もなんとなくわかってきて、細かい施策単位でChatGPTで解決出来そうなものが見えてきた
  • 社内用のChatツールが出来て、直接業務に関わる内容をフランクに聞ける環境が整った
  • 社内の運用ルール/tipsを集めたドキュメントがそもそも不在

やったこととその目的、成果/得られた知見

目的

目的はChatGPTによる、ビジネスサイドの工数削減/クオリティアップです

とりあえずは利用習慣化を目標とし、そのために以下の2点を達成するため施策を実行しました

  • ChatGPTが身近なツールだと思ってもらう
  • ChatGPTで出来ること/苦手なことを知ってもらう

習慣化の初期段階で重要なのはフリークエンシー、敵は低いモチベーションと高いハードルなので とりあえずそこを気にした施策の設計をしました

具体的にやったこと

ChatGPTの使い方ドキュメントの作成

最低限の禁忌的なルールは書きつつ、業務にそったユースケースを中心に書きました。そうした理由は以下です

  • 各人の業務に近い部分のコンテンツを拡充することで、多様な各人の興味に引っかかりやすくするため
  • 理想論でいえば、成り立ちや理論を知ることは大事ですが、興味のない人がおおそう

また完全にその業務をChatGPTで解決することは難しいかもしれませんが、今後の工数短縮やクオリティアップを見越した プロンプトのタタキが存在すること自体が重要だと思っているので、ちょっとでも解決出来そうなことがあるならとりあえず ユースケースとして入れておきました。プロンプトの夢を見ました(天啓もありました)

ドキュメント化にあたっては、なるべくバラエティに富んだユースケースを盛り込むようにしました。たとえば以下のようなカテゴリで、業務に近いユースケースを盛り込みました

  • ブレスト系
  • 従来の業務の工数を下げる系
  • 従来の業務のクオリティを上げる系
  • 直近のChatGPT Plusの機能内容
  • OpenInterpreterでより複雑なことができる話

ChatGPTの使い方勉強会

これは上記のドキュメントをそのままデモ的に紹介しました

ドキュメントが読まれないのは世の常。ある程度お時間をお借りして(今回は1時間)集中してChatGPTについて考えてもらう必要があります

共通の体験を通じて、話題にのぼりやすく、近くの席で口頭での知見の共有につながるのも勉強会の利点だと思います

やった結果はというと、「いままでChatGPTには正解のある答えを聞いて嘘つかれて使えないと思ってたけど、ブレストにめっちゃ使える!」など ちょこちょこ良い反応は得られました

作戦通りユースケースを色々紹介したことで、各々の業務での活用イメージを具体的にもってくれたようでした

とはいえ、あくまでも習慣化のための一歩目なので、ナレッジシェアやOJT的な動きは継続して行っていく必要があります

OpenInterpreterを隣に座ってるカスタマーサクセスの方に試してもらう

継続的にChatGPTに興味をもってもらうために、何か飛び道具的な実績が必要だと考えました

OpenInterpreterまじですごいという話を聞いたので、これを使ってみることにしました

ちなみに同じチームのfuruhama-sanがOpenInterpreterの実装を深掘りしてるので、ご興味のある方は見てみると面白いです

tech.every.tv

OpenInterpreterのメリットの紹介はfuruhama-sanの記事を読んで頂くと分かりますが、今回魅力に感じたのは以下なことでした

  • GoogleColaboratoryという簡単に用意できる閉じた環境での実行がしやすい
  • ローカルのファイルの閲覧/編集ができる(今回はマウントしたGoogleDrive)

上記を活かして、ドメイン知識を持つビジネスサイドがある程度のデータ分析まで自立的に行えると 今後の分析業務のハードルが下がり、より濃い示唆や知見が財産として効率的に貯蓄されるメリットもあります

もちろん一筋縄でいくとは思っていませんが、その第一歩として、隣にいる人に無理を言ってOpenInterpreterを使って頂きました

マジ感謝です。隣の人なので仮にTさんと呼びましょう。Tさんはプログラミングができないカスタマーサクセスっぽい立ち位置のひとです

やったことは以下です

  • CSV形式で出力されたアンケート結果をOpenInterpreterでクロス集計する
    • おまじないをたくさん書いたGoogleColaboratoryを事前に用意
    • Tさんからやりたいことを聞いて、思想を説明しながら仮のプロンプトを目の前で書いてみる
    • OpenInterpreterがコードの実行要否を聞いてくるので、Tさんに深く考えずにどんどん実行の指示を出してもらう
      • 意図した結果ではない、処理に時間がかかるなら再度プロンプトを見つめ直すを繰り返す

結果として特定のアンケート項目をその他の項目とクロス集計したものが20個くらい得られました

エラーへの対処や、僕がColabの仕様に疎いこともあり、Tさんのお時間はそれなりに頂いてしまったのですが(4h+α)、 わりと喜んで頂けました

というのも、そもそも今回のアンケート結果のクロス集計は従来以下の問題を抱えていたようでした

  • 時間がかかる
  • ひとつひとつのクロス集計のために都度数式を組む必要があるので精神的につらい

Tさんとしては、成果物を得られたことももちろんですが、上記を自分の手でやらなくてよい。ということに好印象を持っていたようでした

また、エラーが出たときに落ちるのではなく、エラーの内容を理解した上で再度修正の処理をかけてくれるのはめっちゃ嬉しいとも仰っていました

最終的には愛着を訴えており、ひたすらこちらから命令を与えているのですが、自己再生をするだけで親近感が芽生えるのは大変興味深い現象でした

たしかにOpenInterpreterは内省にもにた処理を繰り返すのがひとつ特徴で、それが悩んでいるように見えるのが人間性を感じるポイントなのかもしれません

逆に以下の点がつらそうでした

  • インタラクティブに処理実行の有無を聞かれるので、常に判断を求められる
  • コードがガンガン出力されるが意味がわからん
  • なんだかんだめんどくさい

ここは実験する前にある程度予測はしていたのですがやはりそうか。という感じです

事前のauto_runの設定や、temperatureの調整/プロンプトの精錬である程度解決できそうではありますが、 目まぐるしく出力される情報に知らず知らずのうちに可処分精神が消費されているようでした

例えばGASを整えたスプレッドシートを提供するなどで、局所的な解決は可能な課題でしたが OpenInterpreterである程度柔軟に課題を解決出来ることはわかったので、引き続きこれをうまく使ってビジネスサイドの課題解決を目指していきたいです

※ 余談ですがアップデート後OpenInterpreterがうまく動かなくなりました(10/16時点, ver: 0.1.7)

このissueの対応をすることで解決しました

https://github.com/KillianLucas/open-interpreter/issues/637

まとめ

以上、ビジネスサイドにChatGPTの利用を習慣づけられるといいなーと思った上で 色々やってみた話を書いてみました

とりあえず第一歩目としてドキュメントの作成/勉強会など実施し、それなりの反応は頂き、多少利用は促進できた実感はあります

とはいえ、今後習慣化のためには草の根運動が重要になってきます

ビジネスサイドで解決しやすそうな課題を見つけたらその場でChatGPTに解いてもらうなど 細かい体験を繰り返していくことで、初めて習慣や文化として根付くものだと思っているので、応援よろしくお願いします

AWS Elemental MediaConvertを使ってレシピ動画のサムネイルを作成する

初めまして、DELISH KITCHEN 開発部の吉田と申します。この記事ではAWS Elemental MediaConvertを使ってレシピ動画のサムネイルを作成した方法を紹介します。

サムネイル作成の背景

DELISH KITCHEN はレシピを動画でわかりやすく基本的な料理からアレンジまで様々なレシピを公開しています。

スマホでブラウザ版のDELISH KITCHENをみていただいた場合、レシピの各工程は手順の説明とポイントを表示しています。

今回はより工程が分かりやすくなるようここに画像を追加する運びとなりました。

アプリやPCブラウザ版のDELISH KITCHENでは工程の動画が挿入されていることから、動画のサムネイルを工程画像として利用することにしました。 しかしながら現在設定されているサムネイルは動画開始時点のキャプチャになっています。

そのため、動画開始時点のフレームになっているサムネイルをすでに動画の変換処理で利用しているAWS Elemental MediaConvertを用いてレシピごとに適切なフレームで設定できるよう実装を進めました。

AWS Elemental MediaConvertとは

AWS Elemental MediaConvert(以下、MediaConvertとします)はファイルベースの動画処理サービスで、従来のブロードキャストおよびマルチスクリーンデバイスへのインターネットストリーミングに必要な形式にメディアを変換することができます。基盤となるインフラのセットアップや管理、メンテナンスは不要で、必要なビデオ処理設定でジョブを送信するだけで使用を開始することができます。ジョブはトランスコード作業を行うもので、入力ファイルと出力ファイルを指定し、どのようなファイルを作成したいか、どのようなフォーマットで作成したいか等を設定します。 詳しくはこちら

取り組んだこと

サムネイルの作成にはMediaConvertのフレームキャプチャ機能を利用しました。 フレームキャプチャ機能は動画の静止画を作成するための機能になります。

サムネイル作成にあたっては既存で1本のレシピ動画を指定した秒数でクリッピングしているジョブがあるのでそこにフレームキャプチャを組み込むことにしました。

動画のキャプチャタイミングにはフレームレートを指定する必要があります。ドキュメントではコンソールによる設定を例にしていましたが、コンソールでは静的な値しか持つことができません。そのため、レシピごとに指定したいフレームが異なる今回の場合、コンソールによる設定ではなく動的な値を利用できる形で実装する必要がありました。

動的な値を利用するための実装方法は2つです。AWS CLIを使用してJSONによってジョブ仕様を設定する方法、あるいはSDKを使う方法です。今回は既存の実装がAWS CLIを用いていたため、フレームキャプチャのジョブ仕様もJSONで実装することにしました。

今回は弊社でMediaConvertが既に利用されていたこともあり、以下1-3の手順は特に行いませんでしたが、初めて利用する場合は以下の手順を踏む必要があります。

  1. ファイル用のストレージを作成する
    • Amazon S3 コンソールを使用してバケットを作成します。MediaConvert は、Amazon S3 または HTTP か HTTPS を使用するサーバーからの入力ファイルを受け入れます。
  2. IAM のアクセス許可をセットアップする
    • MediaConvert が S3 バケットに対する読み込みと書き込みを実行できるようにするには、それが担う Identity and Access Management (IAM) ロールを作成します。
  3. 符号化するソースファイルをアップロードする
    • Amazon S3 コンソールを使用して、Amazon S3 バケットにソースファイルをアップロードします。
  4. ジョブを作成する
    • ビデオファイルを 1 つ、または複数の出力に変換するための MediaConvert ジョブを作成します。

引用元: AWS Elemental MediaConvert 公式ページ

こちらが作成したJSONです。一部の設定は省略しています。

{
  "Settings": {
    "OutputGroups": [
      {
        ...
        "Name": "File Group",
        "Outputs": [
          {
            "ContainerSettings": {
              "Container": "RAW"
            },
            "VideoDescription": {
              "Width": <出力キャプチャの幅>,
              "Height": <出力キャプチャの高さ>
              "CodecSettings": {
                ...
                "Codec": "FRAME_CAPTURE",
                "FrameCaptureSettings": {
                  "FramerateNumerator": <fps>,
                  "FramerateDenominator": <キャプチャするフレーム番号>,
                  "MaxCaptures": 2,
                  ...
                }
              },
              ...
            },
            "Extension": "jpg",
            "NameModifier": <名前修飾子>
          }
        ],
        "OutputGroupSettings": {
          "Type": "FILE_GROUP_SETTINGS",
          "FileGroupSettings": {
            "Destination": <出力ファイルの格納先>
          }
        }
      }
    ],
    ...
    "Inputs": [
      {
        "FileInput": <入力ファイルの格納先>
      }
    ]
  }
}

出来上がったサムネイルがこちらです。

実装にあたりつまづいたところ

本実装においては3点つまづいたところがありました。

  1. フレームのキャプチャタイミングは秒数で指定できない

    • 当初、キャプチャのタイミングは動画のクリッピングと同様にタイムコードで指定できるのかと思っていたのですが、フレームキャプチャではタイムコードでの指定ができない仕様となっていました。 次に思いついた方法がサムネイルにしたい秒数が動画のスタート時点となるよう元の動画をクリップして0フレーム目をサムネイルに指定する方法です(以下3.参照)。しかしこちらについてもMediaConvertは動画あるいはオーディオの出力があるジョブにおいてのみフレームキャプチャを作成することができフレームキャプチャのみの出力ジョブを作成することはできません。毎回不要な動画が作成されてしまうので断念しました。したがって今回は以下2で紹介するframerateによる設定を採用しました。
  2. framerateの設定

    • キャプチャのタイミングを指定するには、framerateを設定します。具体的な設定は「framerate = framerateDenominator / framerateNumerator」となり、framerateDenominatorにはFPS(フレームレート)、framerateNumeratorはキャプチャしたいフレーム番号を指定します。 サムネイル作成時はキャプチャ対象の動画を確認しながら秒数でキャプチャのタイミングを指定していたため、これをフレーム番号に変換する必要があるのですがどうすれば適切に指定した秒数をフレーム番号に指定できるのか苦慮しました。

    コンソールで確認するとframerateは以下のように設定することになります。この場合はビデオのフレームレートが30FPSで、50フレームごとに1フレームをJPEGファイルにキャプチャします。 (参考: AWS for M&E Blog)

  3. 最大キャプチャ数は2にする必要があること

    • MediaConvertは常に動画開始時点のフレームをキャプチャするため、2枚で設定しておかないと欲しいキャプチャは取得できません。 ジョブが完了すると、出力のS3バケットに2つのポスターフレームJPG画像が見つかるので2つ目の画像ファイル(clip-poster.0000001.jpg)が指定したフレーム番号のキャプチャ画像になります。

終わりに

本記事ではAWS Elemental MediaConvertを利用して動画のサムネイルを作成する方法を紹介しました。ここまでお読みいただきありがとうございました。

Next.js を用いたマルチテナント・マルチサービス開発

はじめに

はじめまして、2023 年 4 月から新卒入社しretail HUBで Software Engineer をしているほんだ(@hon_d7174)です。Go が好きです! 現在、私たちは Next.js を使用して新規プロダクトを開発しています。このブログでは、私たちが取り組んでいるプロジェクトの中で工夫した点についてお話ししたいと思います。 また、既存のプロダクトを Next.js 13 へ移行した際の流れや工夫についてはこちらの記事をご覧ください:Next.js の Pages Router から App Router への移行に挑戦してみた

プロダクト概要

当社のプロダクトは、小売様向けにアプリを提供しています。このアプリには、クーポンやお知らせ、アプリユーザーの動向を管理するダッシュボードが含まれており、小売様はこのダッシュボードを使用してアプリのユーザーやコンテンツの管理を簡単に行うことができます。 当社のプロダクトは、以下のような特徴を持っています。

  1. マルチテナントアーキテクチャ
    各小売様は、それぞれ独自のテナントとしてシステムを利用します。これにより、異なる小売様間でのデータやリソースの分離が実現されています。
  2. アクセス制御
    各テナントやユーザーには、閲覧・操作可能なリソース、利用可能なサービスが異なります。アクセス制御により、セキュリティを強化し、情報の漏洩や不正な操作を防ぎます。

システム構成

システム構成図

私たちの作成しているプロダクト簡単なシステム構成図は上記のようになっています。この中の web client と BFF に Next.js を用いています。

Library

以下は今回説明する内容に登場する主な Library になります。

  1. SWR
  2. Auth.js(旧 NextAuth)

実装

プロダクト概要で述べたアクセス制御について一部実装について説明します。

Auth.js を用いた認証・認可の実装

はじめに認証・認可には Auth.js を用いています。Auth.js の interface を下記のように拡張しユーザー情報や Token を管理しています。Auth.js の cookie で管理することにより client side, server side で共通の状態管理が可能になります。下記では本プロダクトで実際に client side, server side で利用するサービス、テナントに関する情報を設定しています。

declare module 'next-auth' {
  interface User extends User {
    services: string[];
    chain: string;
    chains: string[];
    access_token: string;
    expires_at: number;
  }
  interface Session extends Session {
    user: {
      id: string;
      services: string[];
      chain: string;
      chains: string[];
    } & DefaultSession.user;
    access_token: string;
    expires_at: number;
  }
}

declare module 'next-auth/jwt' {
  interface JWT {
    services: string[];
    chain: string;
    chains: string[];
    access_token: string;
    expires_at: number;
    error?: 'RefreshAccessTokenError';
  }
}

次に client side において Auth.js で作成した JWE のデータにアクセスする方法についてです。
client side からアクセスする際にはuseSession()を用います。client side から JWE の更新が必要な際にはuseSession()updateと Auth.js の jwt callback を下記のように実装する必要があります。

const { data: session } = useSession();
<Menu mode="horizontal" items={chains} onClick={({ key }) => update({ chain: key })} {...props} />

jwt callback を下記のようにすることによりupdateで更新された session の内容を jwt にも反映することが可能になります。

callbacks: {
  async jwt({ token, user, session, trigger }) {
    if (user && trigger === 'signIn') {
      token.sub = user.id;
      token.email = user.email;
      token.name = user.name;
      token.services = user.info['services'];
      token.chain = user.info['mart'][0];
      token.chains = user.info['mart'];
    }
    // sessionが更新された際にtokenも更新する。
    trigger === 'update' && session.chain && (token.chain = session.chain);
      return token;
    },

middleware の実装

middleware では主に認証・認可状態の検証と HTTP リクエストヘッダの共通化、アクセス制御の実装を行なっています。 Next.js 13 の middleware は page 遷移時と route handler へのアクセス時の両方で同一の middleware が使われます。 middleware では hooks のような client side でしか使えない処理は行えないことに注意する必要があります。 認証・認可状態の検証は client side で用いていたuseSession()ではなく Auth.js によって生成された JWE を取得することができるgetToken()を用いて検証します。

export default async function middleware(req: NextRequest) {
    const newRequest = new NextRequest(req, req);
    const { pathname } = req.nextUrl;
    const token = await getToken({ req: req });
    const accessToken = token.access_token;
    // tokenの有無を検証する
    const isAuthenticated = !!token;

-------------------------------------省略--------------------------------------

    // 認証が必要なページへの遷移、BFFへリクエストの際に検証を行う
    if (!isAuthenticated) {
        return NextResponse.redirect(`${process.env.BASE_URL}/signIn`);
    }

-------------------------------------省略--------------------------------------

}

次に HTTP リクエストヘッダの共通化についてです。
本プロダクトでは以下の二つの header が主に共通で使われています。 一つ目は認証が必要な page, BFF へリクエストする際に必要な Authorization header です。

// AccessTokenをAuthorization headerに追加する。
newRequest.headers.set('Authorization', `${accessToken}`);

二つ目は各種リソースサーバーでテナントごとに異なる処理を行う際に必要なテナント header です。

// テナント情報が必要なエンドポイントか確認、必要な場合はheaderを追加する。
if (isMartAPIRoute(pathname)) {
    const chain = token.chain;
    newRequest.headers.set('X-Mart-Chain', chain);
}

SWR を用いた repository の実装

下記が BFF へのリクエストを行う client の実装になります。 ユーザー識別子(userID)とテナント情報(chain)を Auth.js のuseSession()を用いて取得することで認証・認可状態の検証を行います。またこれらを cache key に含めることで同一デバイスで異なるユーザーがアクセスする状態やユーザーが複数のテナントを跨いで利用するケースで不整合が起きないようにしています。加えてuserIDまたはchainが falsy な値の場合nullを key として渡すことで認証・認可状態が正しくないときにリクエストが行われないようにしています。

function useCoupon(id: string) {
    // Auth.jsから認証・認可情報、ユーザー関連情報を取得する。
    const { data: session } = useSession();
    const chain = session?.user?.chain;
    const userID = session?.user.id;
    return useSWR(chain && userID ? [`/api/coupons/${id}`, chain, userID] : null, getCouponFetcher, {
        suspense: true,
        revalidateOnMount: false,
    });
}

async function getCouponFetcher([url]: [string]): Promise<Coupon> {
  const res: Coupon = await fetch(url, {
  }).then(async (res) => {
    if (!res.ok) {
      const { message }: ErrorMessage = await res.json();
      throw { message: message, code: res.status };
    }
    return res.json();
  });
  return res;
}

終わりに

マルチテナントのアプリケーションということでユーザーに紐づく情報だけでなく現在そのユーザーがどのテナントに関連するリソースを操作しているかということを考慮する必要がある点が特異であると感じました。また詳細な権限設定はまだ実装していないのですがそれらを考慮するとさらに複雑性が増すのでより一層工夫が必要だと感じました。 マルチテナント・マルチサービスの開発を行おうと考えている人の参考になれば幸いです。 ここまで読んでいただきありがとうございました!

「挑戦」と「挑戦WEEK」

「挑戦」と「挑戦WEEK」

エブリーでCTOをしている imakei です。

エブリーでは定期的にCEOの吉田からスローガンが掲げられます。
今期のスローガンは 「挑戦」でした。

CEOのプレゼンから拝借 (※一部社内向けの内容があるので消してます)

この記事では、開発部・エンジニアにとっての「挑戦」とは何なのか、 エブリーの開発部での解釈を共有できればと思います。
また、実はこのスローガンが決まる前から開発部で取り組んでいる
「挑戦WEEK」に関しても触れられればと思います。

エブリーの開発部における挑戦の考え方

詳細は後述しますが、エブリーの開発部では、4半期ごとに「挑戦WEEK」を実施しています。
今年3月から始めすでに2回実施しているのですが、その中でアンケートなどから挙げられた課題に、

  • 事業部のロードマップを止めてまで、別のことやる意味がわからない
  • あまり「挑戦」的な内容を実施できなかった

など、挑戦とは何なのかが人によって違うことで生まれるものがありました。

そこで、自分の方で言語化したのが下記になります。

開発部における「挑戦」とは

  1. 事業における戦略の実現
  2. 技術的な観点からより上記を推進するための挑戦
  3. 中長期的な目線での強い開発組織の実現

である

(※一部社内向けの内容があるので消してます)

1. 事業における戦略の実現への挑戦

さらに単純にいうとロードマップの実現・達成になるかと思います。
弊社に限らず、ベンチャー企業では常に高い成長率を求められます。
その中で立てられるロードマップはかなりアグレッシブなものが多く、
今まで通りの開発をしていては達成できないものも多いです。
その中で戦略を理解しつつ、それを最速で実現するための試行錯誤や開発を挑戦の1つとしています。

2. 技術的な観点からより上記を推進するための挑戦

1つ目と被るところもありますが、ここでは戦略を実現するだけではなく、
それをさらに進めることに焦点を当てています。
LLMをはじめ、AIや機械学習、また弊社であればOMOに関連する技術など、
昨今のプロダクトのコアを技術が担っていたり、技術を使うことでビジネスの幅が広くなったり、
自ら手を動かさないといけないと思っていた普段の業務のほとんどを技術で効率化できる時代になりました。
これらは技術を知らないと気付けないことも多く、エンジニアからの発信も含め
常に新しい技術に好奇心を持ち、それをどう使うかを考えることが重要になってきます。

3. 持続可能な 強い開発組織 への挑戦

ここでいう 強い開発組織 とは、それぞれにメンバーが単純に高いスキルを持ち合わせるだけでなく、
そのスキルを発揮し、事業を伸ばせる組織を指しています。
強い組織 のためにはまず、今まで上げてきたような挑戦ができる必要があるのはもちろん、
それが持続可能な状態にないといけません。
持続可能であるために、各々の挑戦が単発で終わるのではなく、
継続的に挑戦できるような仕組みづくり、失敗を恐れず挑戦を讃えられる文化づくりが必要です。
その観点からだと、「挑戦WEEK」自体がそもそも挑戦なのかもしれません。
また、持続可能な組織を作っていく上で 採用 も大切になってきます。
組織は常にスケールしており、また事業もどんどん拡大するなかであっても、
挑戦をし続けるためには仲間を採用していくほかにありません。
鶏卵な部分もありますが、強い組織・働きたいと思える組織づくりをしていくことで、
強い仲間を採用でき、強い仲間を採用することでより強い組織になっていくという良い循環を生みたいです。

挑戦WEEK

さて、そんな弊社ですが、今年から「挑戦WEEK」というものを実施しています。
挑戦WEEKとは通常の各事業部のロードマップから離れ、技術的に何かに集中して挑戦する1週間を指しています。
上記の「挑戦」を踏まえると、

エブリーの 挑戦 を推進するために
エンジニアリングを一歩前に進めるための WEEK

と言えるでしょう。

挑戦WEEK

四半期ごとに実施しており、都度事業部の方にお願いして、 実施週は可能な限りミーティング等を0にしてもらい、 まとまった時間を開発に当てられるように調整してもらっています。
(寛容なみなさんにまじで感謝:pray:)

前述したようにすでに2回開催されており、11月に3回目を控えています。
第一回ではまだまだ「挑戦」が浸透せずに、日々の改善や、ただまとまって開発できる週として使ってしまうこともありましたが、
(それも事業部のためだったりするので、もちろんみんな偉いんですが、)
徐々に勘所が掴めてきているのか、11月回に向けては良い挑戦内容が集まっております。

まだまだ改善点も多いのですが、都度アップデートされていくのも楽しく、 今後、会社の挑戦を牽引する制度となるようさらに良いものにしていきたいです。

また第1回の様子はevery.thingで記事にもなっていますので、ぜひ覗いていってください!

https://everything.every.tv/20230428

さいごに

エブリーの開発部における「挑戦」をまとめてみました。
技術的な挑戦だけでなく、事業を伸ばしたり、仲間を集めるための挑戦をし続けている会社です。
そんなエブリーでは、一緒に挑戦してくれる仲間を全方位で募集しております。
このブログで少しでもおもしろうそうだなぁと思ってくれた方は一緒に働きましょう!

https://corp.every.tv/recruits#position-list

Open Interpreterの実装を深掘り

はじめまして。株式会社エブリーの開発本部のデータ&AIチームでデータサイエンティストをしている古濵です。

最近話題のOpen Interpreterについて、実装の中身を追ったので簡単な解説と所感についてまとめました。

Open Interpreter

Open Interpreterとは、LLMに指示を出し、ローカル環境でコードを実行するツールです。
公式のREADMEによると、ChatGPTの機能として使えたOpenAI Code Interpreterとは異なり、Open Interpreterの売りはローカル環境で実行できることかと思います。
自然言語を通じて、対話的にPCの一般的な機能の操作や、ファイルの作成・編集、データ分析などがローカルで実現可能です。

さっそく、ローカルファイルに対して、自然言語でどの程度タスクを指示できるのか試してみました。

簡単なタスク(画像ファイルの移動操作を例に)

Macでスクリーンショットを撮ると、デフォルトではDesktopに溜まると思います。 このスクリーンショットたちを指示通り引っ越しできるか試してみます。

指示として最初に与えた入力は以下です。

Desctop配下のスクリーンショットをDocuments配下にscreenshotというディレクトリを作って移動して

具体的なコマンドは一切教えずに試しました。 結果は以下になりました。

ここで、個人的に衝撃的だったのは、ローカルファイルに対してLLMで操作できていることではなく、自分で問題に気づいて解決できていることです。
mvコマンドが正常に動作していないことを理解した上で、lsコマンドを実行し、実行結果をもとにScreen Shotではなくスクリーンショットであること気づいて自己解決してしまいました。驚きです。

実装の解説

ここからが本題です。
自分で問題に気づいて自己解決できることをどう実現しているのか、Open Interpreterの実装の中身を追ってみました。

説明のため正確性よりもわかりやすさ重視しています。ご留意ください。

github.com

全体像

全体像としては、以下のような処理の流れです。
まず、ユーザの指示(ここではプロンプトと区別するために指示と表現します)を受け取り、LLMに渡すプロンプトを作成します。
次に、LLMがプロンプトをもとに、ユーザの指示に沿うプログラムを作成します。
最後に作成したコードを実行します。そして実行結果もしくはエラー文を再度LLMに渡すプロンプトに加えて作成し直します。
これをユーザが止めるまで繰り返します。

深掘り

ここで注目したいのは、プロンプト作成部分です。
気になったのは以下の3点でした。

  • configのsystem message
  • 参考テキストの提供(open procedures)
  • 最大tokenを超えたとき処理

上記3点を全体像に反映させると以下のようになります。
それぞれ詳細を述べていきます。

configのsystem message

OpenAIのapiを使用する際、内部では以下のmessegesのような構造を用いて、プロンプトを作成します。

response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo",
    messages=[
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": "Who won the world series in 2020?"},
        {"role": "assistant", "content": "The Los Angeles Dodgers won the World Series in 2020."},
        {"role": "user", "content": "Where was it played?"}
    ]
)

roleには、system、user、assistantのいずれかが入ります。 userにはユーザーが出した指示、assistantはLLMの回答、systemはassistantの回答含めた動作を設定するために与えます。
system messageは必須ではありませんが、回答の品質を高めるためには重要な要素です。 原則プロンプトの最初に入れます。
詳しくはOpenAIのChat completions APIのドキュメントに書かれています。

Open Interpreterに最初に与えるsystem messageが、config内に書かれており、以下に個人的に気になったものを抜粋しました。
抜粋は翻訳をかけたものを貼ってるため、原文はリポジトリをご参照ください。

あなたはオープン・インタープリター、コードを実行することでどんな目標も達成できる世界一流のプログラマーだ。

よくあるLLMに対して、ロールを明確にするプロンプトが書かれています。

まず、計画を書いてください。各コードブロックの間に必ず計画を再確認してください。 (あなたは極度の短期記憶喪失なので、計画を保持するために各メッセージブロックの間で計画を再確認する必要がある)。

Open Interpreterは実行前に手順を書いてくれるのは、このプロンプトが効いてそうです。
また、コードの内容を忘れないように強調してます。どの変数に何の値を入れているかなどを忘れないようにするためかなと思います。

あなたがコードを実行するとき、それはユーザーのマシン上で実行される。ユーザーはあなたに、タスクを完了するために必要なコードを実行する完全かつ完全な許可を与えています。あなたは、そのユーザーを助けるために、そのユーザーのコンピュータをコントロールするための完全なアクセス権を持っています。

コードを実行できることがOpen Interpreterの凄さのひとつなので、実行できるように強調しているのかもしれません。

インターネットにアクセスできる。目標を達成するためにあらゆるコードを実行し、最初は成功しなくても、何度も試してください。

最近のニュースについて聞くなどした場合、クローリングして情報を得ようとするのは、このプロンプトが効いてそうです。

一般的に、できるだけ少ないステップで計画を立てる。その計画を実行するために実際にコードを実行することに関しては、1つのコードブロックですべてを行おうとしないことが重要です。
何かを試し、それに関する情報を印刷し、そこから小さな、情報に基づいたステップで続けるべきです。一回でできるようになることはないし、一回でやろうとすると、目に見えないエラーにつながることが多い。

ここで書かれているように、一度にコードは数行程度で生成・実行してくれます。
仮にエラーが出てもリカバリしやすく、Open Interpreterならではのプロンプトかなと思いました。

あなたにはどんな仕事もできる。

最後に励ましだけのプロンプトもあるので興味深いです。

参考テキストの提供(open procedures)

この処理では、ユーザが指示したタスクを解決する上で参考になるテキストを提供しています。
参考テキストも、system messageとして入力しています。

github.com

まず、あらかじめタスクとタスクに関連するテキストを紐づけた構造データ(text_db)を用意します。 このデータをベクトル化して、vector_db内にembeddingsを保存しておきます。
次にOpen Interpreterから指示がpostされると、search api内で指示もベクトル化します(query_embeddings)。
最後に、embeddingsとquery_embeddingsのコサイン類似度を計算し、似ている上位2件のテキストを返します。

参考テキストが実際に役立つのは、ユーザが指示したタスクが、text_db内の内容と関連するときのみです。
参考テキストを埋め込んでしまったら最後、プロンプト内にノイズとして入ってしまうのでは思いましたが、以下のようなプロンプトも同時に添えて制御しているようです(参考)。

計画の中で、もしタスクに関連するのであれば、上記の手順からステップと、もしあるのであれば、正確なコードの断片(特に非推奨の通知の場合は、--各番号のついたステップの下にそれを計画に書き込んでください)を含めてください。
繰り返しますが、もしタスクに関連するのであれば、上記の手順から 逐語的な コードの断片 を、直接あなたの計画に含めてください。

結構強引だなと思いましたが、OpenAI apiのドキュメントにも似たようなことが書かれているため、プロンプトエンジニアリングではよくある制御方法なのかもしれません。

最大token数を越えたときの制御

tokenとは、文章を意味を持つ最小単位にしたものです。
Open Interpreterで用いることができるLLMにはいくつかの選択肢があり、各モデルごとに最大token数が設定されています。
gpt-4は8192、より安価なgpt-3.5-turboは4096、ローカルで使えるCode Llamaは1048といった具合です(参考)。

このtoken数を越えないように制御する必要があり、OpenAIの対処法としては、以下の方法が挙げられています。

  1. 前の会話の要約orフィルタリング

  2. 前の会話を区分的に要約して連結し、要約の要約を作成

    しかし、Open Interpreterでは、以下の手法をとっています。

  3. token数の割合を用いて、文頭と文末からそれぞれ文字を取得し、間を...で連結

まず、(必要token数 / 使用token数) * メッセージ文字数で、プロンプトとして使用する文字数を決めます。
次に、決めた文字数を2で割った文字数(=half_length)を起点として、文頭からhalf_length数の文字と文末からhalf_length数の文字を取得します。
最後に、文頭 + ... + 文末というように連結して、token数が越えないように制御します。

github.com

Open Interpreter内のデータ構造

これまでの説明で、プロンプトが作成できました。 ここからこのプロンプトをLLMに渡し、コードを実行します この処理の流れの中で、Open Interpreterは下記フォーマットのデータ構造を管理しています。

messages: List[Dict[str, Any]]

このフォーマットで時系列順に作成されるイメージが以下になります。
ユーザ〜コード実行の間はループし続けるため、会話が続けば{role: user}{role: assistant}が交互に続いていきます。

Open Interpreterを利用する上での課題

Open Interpreterの実装を追うことで、よりその凄さが鮮明になってきました。 しかし、試す中で以下のような問題点も見えてきました。

コスト

Open Interpreterはプロンプトに様々な工夫がありますが、この工夫を実現しているがゆえに、inputとoutputのtoken数が肥大化してしまいます。 結果として、コストが増加していきます。

精度

コストを削減するために、LLMモデルとして安価なgpt-3.5-turboやローカルでCode Llamaを使うことができますが、gpt-4と比べると正直精度が微妙でした。
Open Interpreterに限りませんが、最大token数があるためLLMとの会話は無制限に続けられません。
Open Interpreterはtokenの使用量も増加しやすい上に、最大token数を越えたときの制御を要約ではなく間を...で補間するため、数回の会話で過去の会話内容を忘れ、必要な情報が抜け落ちる可能性があります。
これが少なからず精度に影響があるかもしれません。

コードの実行を期待しない場合はミスマッチ

Open Interpreterは基本どんな問題に対しても、コードを作成・実行して解決しようとします。 そのため、コードを作成したい目的だと非常に良いツールですが、それ以外の目的だと回りくどく感じます。
例えば、pdfを要約してほしいと指示したとすると、ユーザが欲しいのは要約したテキストだけです。OCRや要約用のライブラリをインストールして実行するコードを求めていません。

まとめ

Open Interpreterに関して、実装の簡単な解説と所感をまとめました。
Open Interpreterに触れる事で、その凄さを体感することができ、プロンプトエンジニアリングについても理解を深めることができました。
今後のアップデートも引き続きキャッチアップし続けようと思います。