Gaudiy Tech Blog

Gaudiyの技術、開発組織、カルチャーについてお伝えするブログです

不確実性や心理的安全性に向き合い自己組織化するチームを作る実践プラクティス

こんにちは。Gaudiyでソフトウェアエンジニア兼スクラムマスターをしている Namiki ( @ruwatana ) です。

チームが向き合う不確実性が大きいと手戻りが増えて価値提供のリードタイムが遅くなる
チーム内の心理的安全性の低さや認知負荷の高さによってエンゲージメントが低下して従業員がオンボード・定着しにくい
...

などなど、昨今のチーム開発はこうした課題で溢れかえっていることかと思います。

結局のところ、我々は具体的にどんなプラクティスを行うことで、こうした課題を解決できていくのでしょうか?

本稿では、筆者と筆者が4ヶ月ほど前に配属することになったチームがこうした問題に対して執ったアプローチおよびその効果をより具体的に示すことができればと考えています。

プロダクトチーム開発を行う皆様に何かしらの参考になれば幸いです。

1. チーム構成と特性

Gaudiyでは、複数のIP(知的財産)に関するコミュニティプラットフォーム「Fanlink」を提供しています。

service.gaudiy.com

現在、Gaudiyにはプロダクト開発チームは複数存在しますが、筆者が所属するチームは「特定のIP向けの連携機能やエンハンス開発に注力したチーム」となっています。

そのため、下記のような特性があります。

  • IPプロバイダーとの協業をより求められ、チーム開発の中でIPの影響力が大きい
  • 連携機能はリッチで新規の機能開発が求められやすい
  • マイルストーンやスコープが変化しやすい

また、メンバー構成は以下の通りです。

  • 👥 プロダクトマネージャー 1名 (+ UXデザイナー1名も兼任)
  • 👥 UI/UXデザイナー 3名
    • お試し入社者を含む ※
  • 👥 フルサイクルエンジニア 4名 ← 筆者はここに所属しています
    • お試し入社者を含む
  • (その他、BizDev・コミュニティマネージャーとも連携)

※ お試し入社の制度に関しては採用情報をご覧ください
site.gaudiy.com

ピザ2枚ルール🍕にギリギリ収まるような人数構成です(これ以上増えても減ってもバランスが崩れそうなちょうど良い状態です)。

チームの中にデザイナーも所属しており、デザイン(ディスカバリー)と開発(デリバリー)のどちらの進捗も統合的に管理するデュアルトラックアジャイルのような開発手法をとっています。

弊チームだけでなくGaudiyのプロダクト開発チームは基本的にこの構成をとっているのが特徴です。

デュアルトラックな開発チームのプロセス例

2. 特性が生み出しうるリスクや課題

さて、こうした特性が以下のような問題を生み出すと考えられます。

2-1. プレッシャーが高くストレスフルな環境に陥りやすい

IPに密着したチーム特性ということで、特定IP向けのプロダクトが生み出すアウトカムが会社というよりも特にチーム(メンバー)に直結する構造となりえます。

完全内製なプロダクトなどと比較すると、先方と握った納期・スケジュールに間に合わせなければならず、何か問題が発生した場合に社内だけでなくIPにも影響が出てしまいかねないですし、先方の要望に叶った要件を着実にデリバリーしなければならないといった外部からのプレッシャーを比較的受けやすく、一概にはいえませんがストレスフルな環境に陥りやすい構造と考えられます。

2-2. 技術的な複雑性や負債が高くなりやすい

Fanlinkは、マルチテナント(IP)向けのプロダクトですが、IPからはIP特有の要望を受けることも少なくありません(要望をいただくことは率直にありがたいことです)。

その中で、機能追加したものが特定IP専用の機能にならないように極力、汎用的に要件を落とし込むという正解があるようでない難易度の高い判断が求められることもあります。

専用の機能が増えてしまうと、固有のロジックや他チームからしたら知りもしない機能が増え、複雑性や技術的負債が増加してしまいます。もしかすると、他チームが勝手に消しにかかってしまうかもしれません。

2-3. さまざまな不確実性が高い

また、さまざまな不確実性が高いということもいえます。

Gaudiyの推奨図書にもなっている広木大地さんの「エンジニアリング組織論への招待 不確実性に向き合う思考と組織のリファクタリング」によれば、不確実性は大きく下記の3つがあるとされています。例とともに分類します。

  • 目的不確実性
    • プロダクトにおけるマーケットに何が求められているのかの正解が見えない
    • 他の競合サービスなどの取り巻く環境の変化に適応しなければいけない
  • 方法不確実性
    • スケジュールに対して要件が決まりきらずに開発を始めなければならない
    • ある大きな機能を開発中に方針・優先度が変わってペンディング・ピボットする
    • チームにプラクティスが存在しない新しい技術を用いた実現方法を求められる
  • 通信不確実性
    • PdMやデザイナーが考えた要件がデリバリーする際に適切に伝わらず手戻りを起こす
    • お試し入社者が目の前のタスクをこなす上での情報やコンテキストの不足
    • リモート主体の開発で、メンバーに聞きにくい・意見しにくい

3. それぞれの課題に対するアプローチ

ここまでは、特に目新しい情報はあまりなかったと思いますが、もしかすると「うんうん」と頷いていただけた方もいらっしゃるのかなと思います。

ここからは、より具体的なアプローチをご紹介できればと思います。

3-1. チームの成長実感や自己肯定感の醸成

もしかしたら、目的不確実性によって価値提供をしてもマーケットのアウトカムを得られないということもあるかもしれません。

そんなときもプロセスに注目し、人やチームの良かった動きやことに対してねぎらいを与えるきっかけを作ることはとても重要であると考えます。

弊チームでは、1週間1スプリントとしてスクラム開発を行っており、レトロスペクティブ(振り返り)やスプリントレビューを定期的に実施して改善を続けてきましたが、その中のHowに工夫を加えていく動きが加速しているので一部をご紹介します。

3-1-1. レトロスペクティブにSpeed boatを導入

まず、レトロスペクティブでは従来 Win-Learn-Try のようなフォーマットを用いていましたが、割と個人の内省に使われることが多く、チームや他人に対してのフィードバックが欠如してきていました。これではみんなでやる理由があまりありません。また、ずっと同じ手法を使っていると飽きも来てしまうというのもあったかもしれません。

こうした問題に気づき、チームのメンバーが立候補して、レトロスペクティブの改善のオーナーを担ってくださいました。これによって、新フォーマットの Speed boat (国内ではSail boatという方がなじみが多いかもしれません)を行うことになりました。
※ちなみに現在は、また別のフォーマットを試すなど改善がどんどん進んでいます。

Speed boatでは、そのスプリントでのチームの動きを事象別に振り返ります。

テンションが上がったアイスブレイク的な内容(太陽)、チームが進むのに助けになった要因(追い風)、逆に止めた要因(錨)、わかった潜在的なリスク(岩)を書いて内容をシェアし、最後に次に取るべきアクションをボート上に乗せて投票でネクストアクションを決めるというシンプルな内容です。

チームのSpeed boatの様子 ※抽象化してます

これによって、他者の動きを褒めたり助かったなどの声が圧倒的に増えました。また、個人よりもチームに向いた振り返りができるようになり、みんなが納得感のある振り返りの場になったと思います。

自分の成果を自分であげるよりも、他者からフィードバックをもらえる方が自己肯定感を醸成できると思いますし、何より褒められたら嬉しいですよね?

Gaudiyは評価という仕組みがない会社なため、正も負もフィードバックを他者からもらえるシチュエーションが欠如しているという特性もあり、そういった部分にもアプローチできているのかなと考えています。

3-1-2. スプリントレビューにてPdMからの今週のスポットライト選出

スプリントレビューはプロダクトマネージャーが主導していますが、ビジネス周りのメンバーも含めてディスカバリー・デリバリーのスプリント内での成果を共有する場というのが基本的な立ち位置です。

そこに、毎週チームの中で良い動きをしたメンバーにスポットライトを当て、選出の理由とともに発表いただくアジェンダを追加するという動きがありました。

こちらもレトロスペクティブの改善と同様、チームメンバーのエンゲージメントを向上する要因となっていると思います。PdMからの視点をもらえるという意味でも、たとえ選出されなかったとしても選出理由を聞けるだけで学びが大きいなと感じます。

今週のスポットライトの例 ※

※ UNIFORMな人とは弊社のクレド(行動指針)の模範となっている人のことです
note.gaudiy.com

我々は、いつもPdMの方々にスポットライトをあてていただく側なので、この場を借りて弊チームのPdMの皆様にも素晴らしいアクションをとってくださったことを改めて讃えたいと思います。ありがとうございます!

3-2. チームとしての共有知やコンテキストレベルの底上げ

特定のメンバーだけが知っていること、複数人で会話する時に相互のコンテキストの違いによって前提が合わなくてコストがかかるといったことが起きないように、短いスパンでコンテキストの共有を行っていくことが必要と考えています。特にエンジニアは4名在籍しており、ここのコンテキストレベルのズレが大きく進捗にも影響しかねないです。

下記にそれらを実現するアプローチを示します。

3-2-1. SlackのハドルミーティングやGatherに常駐する

弊チームでは地方に住むメンバーもいたりするなどリモートが主体の開発を行っています。そこで、朝から晩まで、基本的にはSlackのハドルミーティングに常駐していつでも口頭での相談や同期ができることを実現しています。

話す時以外は大体ミュート状態にしている運用で、話しかける時に「〇〇さん今いけます?」的な形で質問や相談、ペア作業などがすぐにできるような形にしています。

これには、心理的安全性を高める効果があると考えています。

最近では、試験的に Gather というサービスを用いてバーチャルオフィスを構築し、チームのミーティングなどを全てこのサービス上で行ってみるような取り組みを全社的に行なっています。

隣り合うだけでプライベートの会話空間が作り出せるのでペアプロがしやすかったり、他の人が話している場合は吹き出しマークが出て視覚的にも様子がわかるなど、会話のし始めや既に会話しているところへのジョインのハードルを下げるような狙いもありそうです。

Slackハドルミーティングとの大きな違いとして、後から会話にこっそり入室しても何もフィードバックがない(気付きにくい)ので、リアルタイム空間のような他チームのミーティングの盗み聞きみたいなこともカジュアルにできるのが斬新だなと思います(笑)

Gatherでのチームミーティングの様子
(モザイクやアバターも相まって怪しさMAXですが健全な議論をしています)

3-2-2. QAをチーム全員参加で行う

Gaudiyでは、トランクベースの開発プロセスを取っており、ほぼ毎日リリースを行なっています。そのため、前日の夕方に各チームにてデイリーQAを行って、その日にマージされた改修の品質をE2Eでチェックしています。時間としてはその日のマージした分量にもよりますが、早い場合は5分で終わる場合もあれば、30分以上かかる場合もあります。

弊チームでは基本的には、Pull Requestにスクラムのアイテム(JIRAチケットで管理)を紐づけるような運用となっていて、マージすると自動的にQAレーンに移動するようにしており、チケットに書かれた受け入れ条件に沿ってQAを行うという流れで開発を行っています。

JIRAの自動化設定

一見、エンジニアや受け入れ条件を確認するPO/PdM向けの作業に見られがちですが、ここにはデザイナーのメンバーにも参加してもらっています。

これによって、ディスカバリーとデリバリーの認知の分断を防ぐ役割や、エンジニアが気付きにくいデザイン目線でのデザイン仕様との乖離を検出しフィードバックしてもらえるなどの効果が得られています。

また、エンジニアメンバーが有志で行なったチームのコンテキストとは関係のないバグ修正や改善などに関しても、レビューからQAをチームとして担当・チケット管理するようにしており、属人化や個人への責任を緩和できるような効果も期待しています。

ちなみにコードレビューにおいても、弊チームではすべての領域の改修に関してチームのエンジニア全員のApproveを原則もらう形としており、チームメンバーが行なった改修内容のコンテキストを必ず得られるように工夫しています。(日課として、溜まったレビューを捌いたり、連携するところから業務が開始します)

3-2-3. チームで判断しきれない問題はチーム外を積極的に頼る

チームのエンジニアメンバーにおいて、どういうアーキテクチャで価値提供するかを調査・検証しレビューするというプロセスがありますが、迷った挙げ句結論がなかなか出なかったり、そもそも判断するための確度が高くないといったことが往々にして起こります。

その場合は、積極的にチーム外を頼るようにしています。
チーム外とは、社内のEnabling Teamなどの他エンジニアはもちろん、外部のコンサルタントなども含んでいます。

例えば、チーム内で認可サーバを実装する必要が出た時は、社内に詳しい知見を持つ人がいなかったため、利用するSaaSの窓口から外部の有識者を紹介していただいて仕様のレビューを受けるなどを行なってきました。

もちろん、チーム内で考えうる方針案を複数用意しつつ比較し、最適と思う案をピックアップするところまでは行い、相手のレビュー負荷を下げてから依頼することを怠ってはいけません。

このような外からの知見を獲得するプロセスを経ることによって、チームの中にも知見やコンテキストが増えていくことを期待しています。

3-3. コンテキストフォーカスによる高速デリバリーの実現

コンテキストのレベルを底上げしたり、共有したりすることも重要ですが、フォーカスすることもとても重要です。

3-3-1. フロー効率とリソース効率を意識して使い分ける

例えば、「エンジニアが4人いるなら、半分ずつに割ってそれぞれで大きなコンテキストを抱えて並行稼働すれば、二つもアウトカムを同時に得られるのでは?」というのは誰しもが考えうるかなと思います。

果たして、本当にそうでしょうか・・・?

その2ラインのレビューやQAは誰がやるのでしょうか?お互いに1人しかレビュアーがいないと質の部分に問題が生じるリスクがあり、結局相互に行わないといけないとなると設計や要件、直面した問題の背景なども十分に理解する必要が出てきてしまいます。
それを知るための時間や不確実性(ここでは先ほど挙げた通信不確実性)が暗黙的には増えてしまうわけです。

また、2つの価値提供がともに2人月ずつかかる見積もりだった場合、どちらの機能も提供まで4人2ラインでの開発では単純計算で1ヶ月かかってしまいます。

こうした半分に分けて生産性をあげようという考え方はリソース効率と呼ばれます。

もちろん適切に使えば、この考え方も大きな成果を挙げられる手法であるといえます。例えば、だれもがやり方を熟知していて誰がやっても一定のスピードでできることであれば、ペア作業で行うよりも、それぞれが分散して並行に着手する方が効率が良いに決まっています。

しかし、実装方法も定まっていない・仕様も詳しく知らないような不確実なものでは、コンテキストレベルを合わせたり、異なる視点を入れながら検討して前に進む方が手戻りも少ない可能性があります。コンテキストスイッチも不要なので、頭がパンクせずにしっかりコトに向き合えるというメリットもあるでしょう。

こうした、一つの物事を優先的に着手から完成まで行う考え方をフロー効率といいます。

例えば先ほどの価値提供の例も、全員で取り掛かったら一つの機能は半月でリリースできるかもしれません。もう半月経つと次の機能を出せて1ヶ月後から見ると価値提供完了のスピード自体は変わらないですが、一つの機能を早く価値提供でき仮説検証できるというアドバンテージが生まれます。

リソース効率とフロー効率

デュアルトラックアジャイルを敷いている我々にはこの早く検証を回せる状態はとても相性が良いと考えています。

リソース効率とフロー効率の両者をケースバイケースで使い分けていくことが、より早いリードタイム(価値提供)を生み出せるといえるのではないでしょうか。

3-3-2. ペア/モブプログラミングをカルチャーとして根付かせる

不確実なものにはフロー効率という考え方が良さそうということが先ほどの結論ではありますが、弊チームでは積極的に2人でのペアプログラミングやさらに多い人数でのモブプログラミング(モブ作業)を行なって手戻りリスクを抑えたり、同時に把握が必要なコンテキストの量を減らしレベルを揃えやすいような取り組みをしています。

プログラミングに精通しない人からすると、ペアプロする(2人で1つのことをやる)わけだから単純に速度も1/2になるのでは?と考えるかもしれませんが、下記の理由で十分に有効であると考えています。

  • ソロだとコーディング時にわからないことが出て、調査に時間がかかるかもしれない
  • 各々が分散し目前のコンテキストに集中した結果、他のメンバーのコンテキストに対してのフォローが行き届かなかったり、あまり理解しないままコードレビューまで問題に気づかず、後から手戻りするコストが発生するかもしれない
  • 従来のペアプロのドライバ・ナビゲータという役割を超えた並行的なペアプロが実現可能になったため、何もしていない時間は実は少ない
  • お試し入社者を置いてけぼりにせず、ハマりやすい罠を共有することで、早期に立ち上がってもらえる

XP(エクストリームプログラミング)などで提唱されている従来のペアプロ手法では、ドライバとナビゲータに分かれて指示を出す人とコーディングする人がそれぞれ存在しました。

同じPC上で一つのエディタを使ってお互いに操作しなければならなかったので必ず待つ人が発生してしまうという物理的な問題もあったと思われます。

しかし現在は、Visual Studio CodeのLive ShareやJetBrains製IDEのCode With Meといった主要なエディタにて、複数のPCから同時並行作業ができるソリューションが登場したことにより、双方が手を止めずに同一のブランチを並行で修正できるようになりました。もちろん、リモートにも対応していて、ホストのローカルホストポートの共有やコマンドラインの実行なども可能になっています。

これを生かし、弊チームでもペアプロ中にある程度のやるべきことだけ最初に認識を合わせたら、あとは別れてコーディングをすることも多いです。例えば、テストケースの名前だけ先に書いてその実装を分担したりするといったイメージです。一人がミーティングになっても、コネクションを繋げておけば残りの一人が作業を続行することも可能です。

頻繁にチーム内でペアプロされている様子

Gaudiyでは、どんどん新しい仲間が増えており、ほとんどのチームがお試し入社のメンバーを迎えながら開発をしています。お試し入社者の中には、特定の技術スタックは強いけど特定のスタックは経験がない・少ないというメンバーもいらっしゃいます(何を隠そう、自分が完全にそれでした)。

そうしたメンバーと作業をする場合は特にペアプロは大きな威力を発揮すると考えています。技術スタックや既存フィーチャーの仕様・コンテキストが不足していると、その時点で心理的安全性がかなり低くなってしまいます(自分も入社時はソロタスクが多く、わからないことを聞くためにも中々人が捕まりにくいなどの問題があり不安なこともありました)。自走ができる状態に持っていくための最短手段としても素晴らしいソリューションだと思います。

一方で、常にサポートが入っている状態だとアウトプットのスピードと質は保証できるかもしれませんが、個人としてのインプットや納得感・達成感は得られにくいといった問題は大いに考えられます。

時には、「一人でトライしてみたい」や、「最近あまり技術的なチャレンジできていない」といった意思やモヤをできるだけ表明しやすくし、他のメンバーはそれを尊重できるような関係性の構築も重要だといえるでしょう。

3-4. USMからバックログアイテムを転用

主にデザイナーが行うディスカバリーの手法としてはリーンXPやMVP仮説検証などを用いており、新たな価値を提供するときはユーザーストーリーマッピングという主にエンドユーザーの行動をベースとした体験を時系列などにまとめたものを制作しています。

現在はオンラインホワイトボードツールのMiroでUSM(ユーザーストーリーマップ)を作っていますが、JIRA連携の機能を利用してディスカバリーフェーズで作成されたユーザーストーリーをそのままデリバリーのバックログアイテムとして転用しています。

Miroで作ったUSMとJIRAチケットを連携し可視化するイメージ

これによって、わざわざスクラムにおけるプロダクトオーナー(PO)がバックログアイテムを作るという作業をスキップできるようになりました。

さらに、USMがユーザの行動ベースかつ比較的粒度自体も小さく設計されているため、チケットの要件記述も非常にシンプル(時にはタイトルで自明なので詳細不要)になり、要件の手戻りもしにくくなりました。

USMの例としては「ログインしていると〇〇が表示される」、「〇〇を押したら〇〇に遷移する」といった非常に小さな粒度となっています。

チーム構成のところでも紹介しましたが、メンバー数も比較的多く、デザインと開発をチーム内で抱えており、IPと密着したチーム特性もあることで、特にPdMの負荷が高くなっており、ビジネスなどのより本質的な仕事にフォーカスできるようにすべく、この手法を取り入れたという背景がありました。

チームの声としては、「チケットがわかりやすくなった」などのポジティブな反応もあったため、やって良かったと思います。

この取り組みに関しては、絶賛PDCA中になりますので、それなりに形になったタイミングでまたその内容を詳しく共有できればと考えています。

4. まとめ

いかがでしたでしょうか。最後に抽象的にはなってしまいますが、まとめです。

  1. チームの取り巻く状況や環境、特性を理解する
  2. 生まれうる/生まれた課題をしっかりと捉える
  3. 課題に対して適切なアプローチと改善のサイクルを早期に回す

この特性や課題の設定は、設定をする人のスキルセット(いわゆるメタ認知と構造化する力)に依存してしまうとは思います。そのため、その設定自体がブレていると、ただ自分がやりたいことを改善しているだけになってしまう可能性があり、本質を解決できていないことがあります。

ブレていないかは、チーム内外のメンバーに雑談ベースでも良いので壁打ちしてもらい、ブラッシュアップするのが良いかなと感じています。

筆者も、チームへのジョイン時に既存メンバーと1on1を設定させていただき、チームが置かれている状況や課題などの解像度をより鮮明にするところからアプローチしました。

この課題設定の確度を上げることが、結果的に短期での解決や改善を生む要因になったのではないかと考察しているので、非常に重要なステップだと考えています。

また、こうした取り組みがどんどん回せているのは、筆者が主導・管理したからというわけでもなく、チーム全体で一人ひとりが問題に向き合い、自律的に改善することができている結果だと考えています。

一人が主導するよりも、このように自己組織化するチームになる方が、並行してたくさんの改善が回せると思うので、チームのカルチャー創りというのも非常に重要かなと思います。

--

以上になります、お読みいただきありがとうございます!

ぜひこの記事に対しても感じたことなどありましたら、率直なフィードバックをいただけますと幸いです 🙏 

site.gaudiy.com

11/16(木)の自社イベントでも開発組織についてお話しするので、ご興味ある方はぜひ!

gaudiy.connpass.com

ウォレットを ERC-4337 の Account Abstraction で実装して感じた課題と展望

こんにちは。ファンと共に時代を進める、Web3スタートアップ Gaudiy で、ブロックチェーン周りの開発をリードしているDoi(@taro_engineer)です。

「2023年は、Web3 のマスアダプションに向けて躍進する年だ」と昨年の後半くらいから言われていましたが、実際に、技術的にも法規制的にも進展があった一年だったと思います。

GaudiyもWeb3のマスアダプションに対して、長らく課題意識を持ってきましたが、今年は事業としても大きな転換を迎える年となりました。(このあたりの Gaudiy の事業背景や変遷は、ぜひ以下の記事をご覧いただければと思います。)

techblog.gaudiy.com

中でも、大きな進展として注目すべきポイントの一つが、ERC-4337 の Audit です。Gaudiy としても Account Abstraction、特に ERC-4337 には注視していたので、大きなニュースでした。

ERC-4337 は Draft ですが、OpenZeppelin による ERC-4337 Core チームのコントラクト Audit のニュースを見たときに、今後 ERC-4337 を開発していく流れを止めることはできないと確信しました。

これを受けて、実際にGaudiy のプロダクトに適用するにはどうするか?を考え、実装してみたため、今回は ERC-4337を実装する上で感じた課題や今後の展望について書いてみたいと思います。

1. Web3のマスアダプションに向けた課題

Web3 ひいてはブロックチェーンの世界に、一般ユーザーも参加してもらうには、まず大きな課題が2つあります。

  • ウォレット(EOA)という新しい概念のアカウントを管理しないといけない
  • ガス代という手数料の存在

それぞれの課題に対して単独での解決策はあり、それらを組み合わせることで、一般的なアプリに慣れているユーザーにもブロックチェーンの世界に足を踏み入れてもらうことはできました。

一方で、完全に自由な活動をユーザーが実現するのは難しく、その解決にはスマートコントラクトのアップデートが必要など、一部のユースケースにしか対応できないという難点もありました。

すべてのオンチェーン活動に対して自由に活動できて、ユーザーが余計な学習を必要としない状態にするには、まだ一歩足らない部分があったように感じます。

2. ERC-4337 とはなにか?

上記の問題を解決するために、Account Abstraction という技術でウォレットを作ることが考えられます。これは、ブロックチェーン上のスマートコントラクトをウォレットとし、EOA と同じ振る舞いができるように、署名とガス代の抽象化を実装するシステム全体のことを指します。

そして ERC-4337 は、Ethereum 系ネットワークのアプリケーションレイヤーで Account Abstraction を実装するための標準規格にあたり、Account Abstraction の最前線といっても過言ではない提案にあたります。

Ethereum 系ネットワークのコントラクト開発で標準規格化していくであろう提案ということは、つまり、どの開発者・プロバイダーでも周辺のエコシステムを利用すれば拡張できる。例えば、 Bundler・Paymaster を切り替えて利用できたりすることが、利点として考えられます。

また、トランザクションの署名に ECDSA 以外を利用するなど、署名の抽象化も実現できるため、今後の課題になるであろう暗号の強度や、署名検証の効率化などに対しても対策ができるのが特徴です。

これ以上の詳しい内容は、他の記事などを探せば出てくるので、ここでは深く言及しないようにします。

ERC-4337 を実装するにあたり、関連記事をひとつだけ紹介すると、個人的には Alchemy が出しているブログを読めば、ERC-4337 がだいたいわかると思っています。

www.alchemy.com

Account Abstraction を実装していく中で発生する課題を解決していき、最終的にERC-4337 の複雑さに行き着くまでのプロセスを紹介してくれているので、各登場人物が何を目的として存在するのか?がよくわかります。

このブログなどを参考に、実際の動きを確かめながら実装を進めました。

3. ERC-4337 実装のポイントと展望

ではここから、実際の ERC-4337 の実装について書いていきます。この章では、大きく分けて以下3つのポイントをご紹介したいと思います。

  1. アップグレードについて
  2. ウォレットのリカバリーについて
  3. 転売の防止について

まず、ERC-4337 を Production レベルで実装する際に、必ず考慮する必要がでてくる “アップグレード”“ウォレットのリカバリー” 。そして、Gaudiyの事業特性上考える必要があった “転売の防止” について言及していきます。

前提としてGaudiyのウォレット構成は、以下のようなイメージ(概念図)となります。

出典:https://speakerdeck.com/winor30/gaudiy-web3-tech?slide=15

簡単に説明すると、コントラクトの Storage に関わるデータは AccountStorage で定義して、ロジックの変更によって collision を引き起こす可能性を減らしています。各ロジックは Manager として定義し、最低限必要な機能を備えました。

SmartAccount というコントラクトが各ロジックを束ねており、これをシングルトンとしてネットワークにデプロイします。

各ユーザーごとのアカウントとして Proxy をデプロイし、ウォレットのアップグレードが必要な場合は、ユーザーからトランザクションを発行してもらい、事前にデプロイした新しいシングルトンコントラクトに向き先を変えてもらいます。

それでは各セクションについて、記載していきたいと思います。

3-1. アップグレードについて

1つめは、アップグレードです。スマートコントラクトの実装において、最も神経を使う部分の1つが Upgradeable なコントラクトの実装だと思っています。

今までの開発であれば、OpenZeppelin のコントラクトでも一般的に提供されている ERC-1967 を利用した Proxy Pattern が、一番メジャーで使用されている選択肢かなと思います。

スマートコントラクトウォレットに関しては、この通例とされる Upgrade とは少し違うアプローチを考えなければいけません。

3-1-1. モジュラーコントラクト

まず ERC-4337 は EIP ではまだ Draft であり、今後仕様が変わる可能性が大きいです。さらに、ユーザーのアカウントという性質上、セキュリティの脆弱性に対する対応や、今後ユーザーが新機能を追加したい/逆に削除したいと感じる可能性なども、NFTなどのコントラクトと比べると高いと考えられます。

そのため、コントラクトを拡張していく考え方をもつ従来の Proxy Pattern では、スケールするウォレットを提供することは難しいと感じました。

この点から、ウォレットの実装でよく見られるのは “モジュール” という概念を用いた実装です。

ざっくりしたイメージで言うと、ERC-2535 の Diamond Proxy の考え方をベースとし、各ロジックを組み替えたり、付け替えたりできるようにモジュール化する手法です。Account Abstraction のパイオニアと呼べる各プロジェクト、例えば Safe や Argent などには、この “モジュール” という要素が組み込まれています。

ERC-4337 の文脈でいくと、ERC-6900 の提案がより標準化に向けたモジューラブルなスマートコントラクトウォレットの実装につながると考えています。これもまだ Draft なので、今も Spec が変わりつつありますが、基本的な考え方はすでに長年 Account Abstraction に取り組んでいるプロジェクトでも利用されていたりします。

また、Alchemy のエンジニアが提案者なので、今後 Alchemy という大きなプラットフォーマーが ERC-6900 の標準化に向けて大きく推進させていったり、Account Abstraction の開発市場を取りに行くような動きをする可能性があると感じています。

3-1-2. どう実装したのか?

開発当初は ERC-6900 をベースにした実装を考えたのですが、実装当時はこの提案が出てからまだ半月も経っておらず、以下の懸念がありました。

  • 研究的な実装を進めたが、大幅に仕様が変更する可能性がある
  • 既に実装されているモジューラブルな実装方法で実装しても、 ERC-6900 の考え方と大幅に違ってくる可能性もある
  • 工数と期日が逼迫している

これらの懸念を考えると、変わらないコアな部分はどこかを見極めて、ポイントだけ押さえることが大事だと判断しました。

その結果、まずは ERC-1967 を応用した Upgrade の方式(結果的には OpenZeppelin v5.0.0 に含まれた ERC-7201 に近い状態になった)で、 Storage をロジックから切り離すように実装することを決めました。今はこのコアな部分のみ実装しつつ、将来的にモジューラブルなコントラクトに移行していきたいと考えています。

この実装自体は Soul Wallet から着想を得ており、開発当時は Soul Wallet チームが GitHub で水面下に開発していることに気づけなかったのですが、現在は ERC-6900 の考え方のサンプルの1つになりえるモジュールの構成になっています。

ethereum-magicians.org

チーム全体としても Empower Day を活用して知識を付けていったり、PoC をつくったりなど、より良い改善に向けた取り組みが現在進行中です。

3-2. ウォレットのリカバリーについて

2つめが、ウォレットのリカバリーです。Account Abstraction を実装する上で、今ではリカバリー手段を実装することはスタンダードになっています。

しかしながら、どの方法を選択すれば提供するユーザーにとってリカバリーが実行しやすいのか?学習コストをかけずにリカバリー手段を設定できるのか?など、実生活に反映させていくには考えねばならない観点が複雑に絡み合っていると思います。

まずは技術的に、ウォレットをリカバリーするための主な手段として以下の2つを考えました。

  • ソーシャルリカバリーを利用したオーナーの変更
  • マルチシグを利用したオーナーの冗長化
3-2-1. ソーシャルリカバリーを利用したオーナーの変更

まず、vitalik が提唱した Social Recovery は最良のソリューションのように聞こえますが、現実的にはどのアプローチをとっても一定のデメリットがあり難しいです。

また、信頼できる家族や友人などの第三者の承認を得る、文字通りソーシャルな関係でリカバリーすることは、一般ユーザーをターゲットにするウォレットでは周りの人も Web3 に精通していない可能性が高く、実現が困難です。そのため、もう少しだけ抽象化した「本人が信頼できる Guardian を用意する・用意させる」という考え方でユーザーに届ける必要があると考えます。

各種方法はありますが、例えば OIDC を利用できる IdP を Guardian とするリカバリーが、ユーザーには一番馴染みがあって、学習コストをかけずに使用できるのではないかという仮説が考えられます。

ただ、これも良い方法に見えますが、以下のデメリットはあると考えました。

  • 公開鍵の変更があった場合、ID トークン検証用のシングルトンコントラクトのデプロイが毎回必要になる
  • IdP が悪意をもてばリカバリーを実行できてしまい、実質セミカストディアルな状態になる
  • この問題を解決しようとすると、よりアーキテクチャが複雑になる

結果的に、現在の知見や開発のためのコストや可逆性、費用対効果を考えると、他の方法で進めるのがよさそうでした。

次に、よりコスト感と見合った現実的なアプローチとして、すでに実装されているソーシャルリカバリーや関連するロジック、Argent の SecurityManager や Safe の OwnerManager などのロジックをベースに、ソーシャルリカバリーを実装することが考えられます。

この方法は現時点においては一番現実的だと思いますが、Guardian は ECDSA で検証ができる署名を作成できる必要があり、リカバリー用の秘密鍵をどこで生成し、保持してもらうか?が課題になります。

これは今までの「EOA をいかに一般ユーザーに対して安全に保管してもらうか?」と同じ課題になり、ブラウザウォレットをこれから作る我々としては悩ましい問題になりました。

3-2-2. マルチシグを利用したオーナーの冗長化

別手段として、マルチシグの仕組みを利用してオーナーを複数用意する方法も当然考えられます。

これはオーナーを増やして 1/n のマルチシグウォレットにすることで、オーナーのどれかを失っても、別のオーナーでウォレットを管理できるようになり、実質ソーシャルリカバリーに近い考え方となるということです。ロジックの複雑さはなく、実装もしやすいように思います。

ただし、複数のオーナーを設定できるようにすると、適切なオーナーを設定してもらいたいがそれを制限することは難しいと考えました。後述しますが、投機目的でウォレットの転売に利用されないか?も考慮すると、すべてのオーナーが「ユーザー自身が管理するものである」ことを検証できる必要があります。

また、ユーザーが学習コストをかけずに用意できて失いにくい方法で、かつウォレットを利用するときの体験も良い方法でオーナーを用意してもらう必要があるのは、変わらずクリアしなければいけない課題だと感じました。

3-2-3. どう実装したのか?

つまり、解決せねばならない主な課題は2点あります。

  • ユーザーに学習コストをかけずにウォレットのオーナー・リカバリーの鍵を生成してもらう
  • リカバリーをするときは適切なウォレットのオーナーにローテーションできる

この2点を現時点で達成するには、ウォレットのオーナーをオフチェーンの MPC で生成するのが適切な方法ではないかと考えました。

まず、ユーザーに今までの体験と変わりなくオーナーを生成してもらうには、ソーシャルログインでウォレットを作れるのが理想的だと考えます。これは弊社のプロダクトである Fanlink のアカウントを作れば、それがウォレットのオーナーになるようにするのがベストな状態だと考えました。

また、オンチェーン上でリカバリーをするとなると、新しいオーナーをユーザーが用意する必要があります。これもユーザーにとっては、私たちから何かしらの機能を提供しないと Metamask などの EOA を用意する必要が出てくるので、オーナーは変えずに同じ Fanlink アカウントでリカバリーができるようにする、要はオフチェーンでリカバリーできるのがベストな状態だと考えました。

これらを踏まえ、現実的に取れる選択肢を考えると、Web3Auth, Lit Protocol が当時は考えられましたが、Node の監査状況やサポート状況などを鑑みて、Web3Auth の tKey MPC を選択しました。

また、リカバリーの鍵を保持してもらう手段としては、リカバリーの鍵をプログラム上で生成して、そのデータを Cloud Storage に保存させることだと考えました。

この方法では仮に Google Drive を保存先と考えると、ユーザーは Google アカウントでアプリにアクセス権を許可すればリカバリーを取れる状態になり、体験としてはソーシャルログインのフローと変わりなくリカバリーを設定できます。

これをオンチェーンの Guardian ではなく、オフチェーンの MPC で分散した鍵をリカバリーとして設定するように考えました。このスキームでソーシャルリカバリーを実装すると、オンチェーンで Guardian を設定・保持してもらうときでも同様の流れで実現できるので、オンチェーンでのリカバリーも対応できます

参考にさせていただいた Argent や World App も同様のアプローチを取っているので、費用対効果が合っているソリューションだと思っています。

ウォレットのオーナーに関して、技術的にベストな選択をするならば、BLS や Schnorr などの集約署名を取り入れたオーナーの生成や WebAuthn を取り入れた 2Factor も導入するなど、実用性がありおもしろそうな領域はあります。

ですが、まずはユーザーに提供して本当に使いやすく Web3 の体験を楽しめるのかを確かめる必要があるため、ウォレットに関する最新技術の投入は段階的に取り組んでいく必要があると感じています。

上記に挙げた方法や理論は、数ある選択肢の一端であり、どれにも一定のデメリットがあります。ウォレットのリカバリーに関しては、これが正解だという方法はいまだなく、今後も改善が必要な分野だと思っています。

3-3. 転売の防止について

さいごに、転売の防止について触れます。Account Abstraction の利点の1つは秘密鍵を失ってもリカバリーができることですが、これは見る角度を変えると、第三者にウォレットを譲渡できるようになったということです。

3-3-1. 今までは “転売” が実質できなかった

今までの EOA では、第三者に譲渡したとしても秘密鍵を変更することはできないので、秘密鍵を知っている人、つまり譲渡した人がメモなどで秘密鍵を取っておいたら、仮に売買などを通してウォレットを譲渡したとしても確実に所有者が変わるとは言えませんでした。

ウォレットという自身の資産を管理するものを他人に受け渡すことは、実生活上では考えにくいかもしれませんが、Account Abstraction によって、このユースケースが技術的に可能になった点は、Gaudiyが考える「ファン国家」を実現するには考慮する必要がありました。

例えば、IP にまったく貢献していない人でも、ファン活動を頑張っているウォレットをお金で手に入れることができるようになるとしたら、適切なトークングラフを描けなくなってしまいます。また、投機目的のユーザーも入ってきて、ボラティリティが高くなり、本当のファンに負担がかかったり、ファンとしての熱量が下がってしまうかもしれません。

3-3-2. どう実装したのか?

これを解決するために「ウォレットのオーナーを変える処理は Gaudiy の署名も必要とする」ように実装することを考えました。具体的には、EIP-712 の署名を作成して、各メソッドで署名を検証するという、一般的なアプローチで考えました。

一方の課題として、適切なオーナーに変えようとしていることをどうやって判断するのか?は、変わらず議論しなければいけません。実装方法で他に良い方法があるとは思うのですが、今のところは IP 経済圏の公式なウォレットという状態を守るためのアプローチとなっており、その点では妥当性はあるかなと思っています。

もし、何か思いつく方・気づきがある方はぜひ議論させてほしいなと思っています。

4. まとめ

実際に取り組んでみて、今までのスマートコントラクトの開発とは違うアプローチを考えることや、現実世界で使えるレベルまで解像度をあげることは、限りある時間の中ではなかなか大変な部分もありました。この点は引き続き研鑽を積んでいきたいと思います。

また事業の特性上、完全にパブリックに開かれており、ユーザーに全主権があるという訳ではなく、どこかにルールを設ける必要があり、そのルールをどうコントラクトで定義するべきなのか?そもそも本当にルールが必要なのか?は研究とユーザー検証をしていく必要があると感じています。

べき論でいくと、ウォレットの UX を高めるためには、Starknet のようにチェーンがネイティブに Account Abstraction をサポートしていくことが必要だということも理解できますが、事業を進めることを考えると、可能な限り標準化に寄せながら、今の最善策を取っていくしかないと思っています。

ERC-4337 はまだまだ事例としては多くないですし、私たちも手探りで進めているので、もし何かシェアできるものがある方は、ぜひ教えてもらえたら大変嬉しいです。

site.gaudiy.com

ブロックチェーンエンジニアも募集してます。興味ある方いましたらぜひ。

herp.careers

site.gaudiy.com

また来週 11/7(火)に、Account Abstractionをデプロイする実践形式のオフラインイベントを開催するので、興味ある方いたらぜひご参加ください!

gaudiy.connpass.com

LangSmith で始める LLMOps

こんにちは。ファンと共に時代を進める、Web3スタートアップ Gaudiy の seya (@sekikazu01)と申します。

弊社では今 LLM をプロダクトに活用しているのですが、実際にユーザに提供するクオリティのものを作る・運用しようとすると様々な課題が立ちはだかってきました。

そんな数々の課題を解くために LangSmith というツールが活躍してくれた、また今後の活用・発展にもかなり期待ができるため、本記事ではそんな LangSmith について解説していきます。

LLM を使ったプロダクト開発において課題を感じている方々の参考になれば幸いです。

出てきた課題

まず LangSmith 自体の解説に入る前に、我々が直面した・ほぼ間違いなく今後するであろう課題たちをサラッとご紹介しようと思います。

大まかには次のような課題がありました。

  • プロンプトがアプリケーションコード内に書かれていたので、エンジニア以外がプロンプトチューニングをする時の受け渡しが手間
  • そもそもプロンプトチューニング自体がすごい手間
    • 様々なプロンプトテンプレートに対するインプットの組み合わせがある
    • 特に RAG で記憶を埋め込んだりするような文脈情報が多いプロンプトではデータを用意するのが大変
  • プロンプトの評価基準が言語化されていない
    • プロンプトはそれを持ってどんな体験をユーザに届けたいかの目的があるが、プロンプトの評価基準を最初からバシッと決めるのはおそらく不可能
    • なので逐次育てていくことになるが、そのログが取れていないため、後にいじったり他の環境(使用するモデルなど)で試した際にリグレッションが起きてないか確認する術がない

これらの課題に対して LangSmith の次のような機能たちがハマりました(or まだしっかり運用に乗せられてないのでハマりそうな予感がしています)。

  • プロダクトで実際に送られたプロンプトのログが取れる & それをいじれる Playground がある
    • これによりエンジニアでない人も触れるようになるし、プロンプトに渡すデータもリアルなものがあるのでそれを元にいじれる
  • Evaluation 機能でプロンプトのテストが書ける & データセット機能でそれに対するインプットも作れる
  • Hub によってプロンプトの一元管理とバージョン管理ができる

それではこれらの機能について、より具体的に解説していこうと思います。

LangSmith の基本

まずはじめにサクッと LangSmith の超基礎的なところをお話しします。

LangSmith は、LLM アプリ開発の定番フレームワークとなった LangChain の開発元と同じところが開発している LLM アプリ開発支援サービスです。

※ LangSmith は2023/10月現在は private beta な状態なので waitlist に登録する必要があります。(ちなみに "private" とは呼んでますが、記事書いても大丈夫なことはお問合せして確認しました👍)

www.langchain.com

LangChain 本体の X でもよく新機能や Tips の紹介をしているので要チェックです。

LangChain (@LangChainAI) / X

インストールは LangChain を使っている場合、非常に簡単で、下記のように環境変数をセットするだけです。

export LANGCHAIN_TRACING_V2=true
export LANGCHAIN_ENDPOINT=https://api.smith.langchain.com
export LANGCHAIN_API_KEY=<your-api-key>
export LANGCHAIN_PROJECT=<your-project>  # if not specified, defaults to "default"

LangChain を使っていない場合でも langsmith のパッケージがあるので、それを活用して自分で LLM の実行結果などのログを取るようにすることができます。

github.com

Organization 機能について

仕事で使っていく上で気になるのは「複数人で同じ計測を見られるか」ですが、こちらについては8月ごろに Organization 機能が追加されました。ただ、現状は Max 5人までなのでそこはご注意です。

Oranization機能の画面
Oranization機能の画面

それでは次は具体的な LangSmith の機能について解説していきます。

プロンプトのログと Playground

LangSmith の最も基礎となる機能はプロンプトのログです。 実行された結果はこのように一覧に表示されます。

LangSmith のログ画面
LangSmith のログ画面

詳細にいくと、こんな感じで実際に送られたプロンプトとその結果が見られます。

LangSmith のプロンプトログ詳細画面
LangSmith のプロンプトログ詳細画面

そして右上の Playground を押すと、そのままプロンプトや様々なパラメータをいじってチューニングすることができます。

LangSmith のプロンプト Playground
LangSmith のプロンプト Playground

ちなみに OpenAI API 以外にも Anthropic や Vertex などを使うこともできます。

LLM Provider の選択
LLM Provider の選択

LangSmith 導入以前はエンジニア以外がプロンプトチューニングを始めるのが手間だったのですが、今は実際に送られたプロンプトを使ってスッと Playground でいじれるようになってそのハードルが大分下がりました。

まだ「インプットを切り替えた並列実行」「繰り返し実行」などチューニングしていく上で、もう一捻り欲しいなぁと考えている機能はあるのですが、これだけでも大分嬉しいです。ありがとう LangSmith。

小ネタ: タグにプロンプト名を入れて絞り込みやすくする

プロンプトのログはそのままだと様々なログを区別するものがないため、一覧の中からチューニングしたい対象のプロンプトを目grepで見つけることになるためいささか非効率です。

これを解決するために我々は実行時にプロンプト名をタグとして入れるようにしています。実際は ChatModel を扱うところをもう少し抽象化していたりするのですが、書き方としては以下のような感じです。

llm_chain = ChatModel("model-name" ,temperature=0.7, verbose=True).chain(prompt=prompt)
result = llm_chain.run(**kwargs_template, tags=["local", "test_message"])

こうしたデータを入れておくと LangSmith の UI にも次のようにタグで絞り込むためのチェックボックスが生まれます。(プロンプト以外のタグが将来的に欲しくなるかもしれないので “prompt:” みたいな prefix つけてもいいかも)

LangSmithでタグでのフィルターUI
LangSmithでタグでのフィルターUI

モニタリング

LangSmith では Monitor 機能が搭載されており、

  • 実行数
  • 成功率
  • レイテンシー
  • トークン消費量

などなどを確認することができます。

LangSmith のモニターUI
LangSmith のモニターUI

ただちょっと痒い所に手が届かない部分もあり、例として、以前気づいたらものすごい量のトークン消費をしていたことがあったのですが、その時知りたかったのが「どのプロンプトがどれくらい実行されている & トークンを消費しているのか」でした。一覧を見ていたらなんとなくアタリはつけられますが、現状 LangSmith ではこれをソートして見る方法がありません。

なるべくログを見る場所は集約したいので、この Monitor 画面でタグでフィルターできる機能ができるといいなぁとは思っていますが、現状は LangChain のコールバックでまた別のところ(我々の場合 BigQuery)に保存してクエリを書くようにしています。

詳しくは下記記事をご参照ください。

zenn.dev

Evaluation とテスト

次に Evaluation という機能について触れていきます。

Evaluation(評価)とは呼んでいますが、目的の感覚としては単体テストを書くのに近いです。 主にはリグレッションの確認を想定しています。

例えば…

  • プロンプトのトークン利用量が激しいのでプロンプト文を変えたり圧縮テクニックを使ったりする
  • 使う LLM のモデルを変える
  • 変数の値を変える

などなどがあり、これらの変数を変える度に都度手動で組み合わせを確認するのは中々に手間です。

そこで Evaluation 機能を活用します。 めちゃくちゃコードをかいつまむと下記のように評価基準を書いて、データセットという事前に用意したインプットを元に実行するだけです。

eval_config = RunEvalConfig(
  evaluators=[
    RunEvalConfig.Criteria(
      {"適切な文章量": "50文字以上200文字以内に収まっているか"
      " Respond Y if they are, N if they're entirely unique."}
      )
  ]
)

result  = run_on_dataset(
    client=client,
    dataset_name=dataset_name,
    llm_or_chain_factory=create_chain,
    evaluation=eval_config,
    verbose=True,
)

こういった評価基準をプロンプトチューニングする過程で育てていくと後のリグレッション確認や、追加でプロンプトを調整していく時に活躍すること間違いなしでしょう。

という訳で具体的に LangSmith で Evaluation を実行するまでの道のりを紹介します。

データセットを準備する

データセットは Evaluation の文脈ではプロンプトに対するインプットとして使われます。 データセットは UI から手動でもコードからでも作ることができるのと、エライのが LangSmith メインの機能であるログからも追加することができます。

UI から追加する

New Dataset を押して Dataset を作ります。Data type は今回は key-value にしておいてください。(LLM Chain から input の JSON を入れるのが目的なため)

データセットの作成
データセットの作成

そして add Example から JSON 形式で追加します、実行したい対象のプロンプトテンプレートに応じて整えてください。ちなみにデータセット内のこの examples は全て同じフォーマットの JSON でないと Evaluation 実行時にエラーになるので気をつけてください。

データセットのデータの追加
データセットのデータの追加

ログから追加する

Lang Smithの素晴らしいところがログから追加できることです! ログの詳細ページから Add to Dataset をクリックしてください。

ログ詳細からデータセットへの追加
ログ詳細からデータセットへの追加

押すとこんな感じでインプットの JSON をデータセットに追加することができます。 これでテストデータを作る手間が大分削減できそうですやったぜ…。

抽出されたインプットとアウトプットでデータセットに追加する
抽出されたインプットとアウトプットでデータセットに追加する

コードから追加する

コードで追加することもでき、他のデータソースから引っ張ってきたり、それなりに量があるデータセットを作る場合にはこのやり方が重宝するでしょう。

from langsmith import Client

example_inputs = [
  "a rap battle between Atticus Finch and Cicero",
  "a rap battle between Barbie and Oppenheimer",
  "a Pythonic rap battle between two swallows: one European and one African",
  "a rap battle between Aubrey Plaza and Stephen Colbert",
]

client = Client()
dataset_name = "Rap Battle Dataset"

dataset = client.create_dataset(
    dataset_name=dataset_name, description="Rap battle prompts.",
)
for input_prompt in example_inputs:
    client.create_example(
        inputs={"question": input_prompt},
        outputs=None,
        dataset_id=dataset.id,
    )

以下の環境変数が os.environ['LANGCHAIN_ENDPOINT'] で読めるような状態にしておいてください。

export LANGCHAIN_ENDPOINT=https://api.smith.langchain.com
export LANGCHAIN_API_KEY=<your api key>

Evaluation を書く

それでは Evaluation を書いてみましょう!

冒頭に示した通りやり方としては Evaluation の基準を書いて

eval_config = RunEvalConfig(
  evaluators=[
    RunEvalConfig.Criteria(
      {"適切な文章量": "50文字以上200文字以内に収まっているか"
      " Respond Y if they are, N if they're entirely unique."}
      )
  ]
)

データセットに対して実行するだけです。

def create_chain():
    llm = ChatOpenAI(temperature=0)
    return LLMChain.from_string(llm, "Spit some bars about {input}.")
   
result  = run_on_dataset(
    client=client,
    dataset_name=dataset_name,
    llm_or_chain_factory=create_chain,
    evaluation=eval_config,
    verbose=True,
)

そうするとデータセット内に評価の結果が表示されます。

実行されたEvaluation結果画面
実行されたEvaluation結果画面

そして中身のスコアのところをクリックすると、なぜその評価になったのかを確認することもできます。

Evaluation詳細
Evaluation詳細

※ 余談ですがこの評価にもしっかり LLM が使われているのでお金がかかることを意識しておきましょう💰

評価プロンプトはどうなっているのか

評価にも LLM が使われているのですが、我々は基準を書いているだけです。実際に LangSmith がどんなプロンプトで評価基準を活用しているのかみてみましょう

You are assessing a submitted answer on a given task or input based on a set of criteria. Here is the data:
[BEGIN DATA]
***
[Input]: a rap battle between Aubrey Plaza and Stephen Colbert
***
[Submission]: 猫が可愛いにゃぁ
***
[Criteria]: 適切な文章量: 50文字以上200文字以内に収まっているか Respond Y if they are, N if they're entirely unique.
***
[END DATA]
Does the submission meet the Criteria? First, write out in a step by step manner your reasoning about each criterion to be sure that your conclusion is correct. Avoid simply stating the correct answers at the outset. Then print only the single character "Y" or "N" (without quotes or punctuation) on its own line corresponding to the correct answer of whether the submission meets all criteria. At the end, repeat just the letter again by itself on a new line.

Criteria 毎に改行して結果を出してというシンプルな内容ですね。

ちなみにですが、元のプロンプトは英語で与えられた文字列が日本語で大丈夫なのかと疑問に思うかもしれませんが、結論としては問題ありませんでした。おそらくですが LLM が日本語の文字列を英語として解釈してから reasoning しているように見えます。

もしかしたら日本語特有のニュアンスが失われてしまうことはあるかもしれませんが、Evaluation に影響が出るほどではないのかなと予想しています。

LangSmith のプリセット評価基準

上記では自分で評価基準を書いてましたが、LangChain もいくつかプリセットの評価基準を用意してくれているので把握しておくと楽になることがあるかもしれません。

docs.smith.langchain.com

ドキュメントには全部書いてないですが、コードから抜いてくると下記のような評価基準がありました。 自分でカスタムで作る前に同じ観点での基準があるか探してみると良さそうです。

class EvaluatorType(str, Enum):
    """The types of the evaluators."""

    QA = "qa"
    """Question answering evaluator, which grades answers to questions
    directly using an LLM."""
    COT_QA = "cot_qa"
    """Chain of thought question answering evaluator, which grades
    answers to questions using
    chain of thought 'reasoning'."""
    CONTEXT_QA = "context_qa"
    """Question answering evaluator that incorporates 'context' in the response."""
    PAIRWISE_STRING = "pairwise_string"
    """The pairwise string evaluator, which predicts the preferred prediction from
    between two models."""
    LABELED_PAIRWISE_STRING = "labeled_pairwise_string"
    """The labeled pairwise string evaluator, which predicts the preferred prediction
    from between two models based on a ground truth reference label."""
    AGENT_TRAJECTORY = "trajectory"
    """The agent trajectory evaluator, which grades the agent's intermediate steps."""
    CRITERIA = "criteria"
    """The criteria evaluator, which evaluates a model based on a
    custom set of criteria without any reference labels."""
    LABELED_CRITERIA = "labeled_criteria"
    """The labeled criteria evaluator, which evaluates a model based on a
    custom set of criteria, with a reference label."""
    STRING_DISTANCE = "string_distance"
    """Compare predictions to a reference answer using string edit distances."""
    PAIRWISE_STRING_DISTANCE = "pairwise_string_distance"
    """Compare predictions based on string edit distances."""
    EMBEDDING_DISTANCE = "embedding_distance"
    """Compare a prediction to a reference label using embedding distance."""
    PAIRWISE_EMBEDDING_DISTANCE = "pairwise_embedding_distance"
    """Compare two predictions using embedding distance."""
    JSON_VALIDITY = "json_validity"
    """Check if a prediction is valid JSON."""
    JSON_EQUALITY = "json_equality"
    """Check if a prediction is equal to a reference JSON."""

run_on_dataset の返り値

こんな感じの結果サマリっぽいものが返ってくるので、それを元に CI で判定とかもできるかもしれません。

{
   "project_name":"0f5fcfffcd824b5c9ae533e9b9b27d86-LLMChain",
   "results":{
      "83a72ad6-0929-4456-bcea-a3966ce01318":{
         "output":{
            "input":"a rap battle between Aubrey Plaza and Stephen Colbert",
            "text":"猫が可愛いにゃぁ"
         },
         "feedback":[
            "Feedback(id=UUID(""bd7ccd82-af71-4f39-a5ac-f5ef4ca53ac4"")",
            created_at=datetime.datetime(2023, 9, 6, 59, 36, 813970),
            modified_at=datetime.datetime(2023, 9, 6, 59, 36, 813970),
            "run_id=UUID(""62cd7d98-10e3-4a40-95d6-1bd14215b27f"")",
            "key=""適切な文章量",
            score=0.0,
            value=0.0,
            "comment=""The criteria is asking if the submission is between 50 and 200 characters long. The submission \"猫が可愛いにゃぁ\" is only 9 characters long. Therefore, it does not meet the criteria.\n\nN",
            "correction=None",
            "feedback_source=FeedbackSourceBase(type=""model",
            "metadata="{
               "__run":{
                  "run_id":"780943ca-496a-4ba1-8bc0-2de00bdb2d1e"
               }
            }"))"
         ]
      }
   }
}

LangSmith の Cookbook の中にも pytest を使ったサンプルコードがあるのでこちらもご参考ください。

github.com

小ネタ: RAG を使った評価

先ほど注意点としてサラッと書きましたが、この Evaluation は LLM を使って行われているため時間もかかるしお金もかかります💰

そこで最近ちょっと話題だったのが Embedding を使った評価です。この手法では Embedding で事前に用意した想定回答との類似度を見ることによって LLM の遅さと API 代の高さをカバーしています。

gpt-index.readthedocs.io

評価手法も色々策定されていってるので知見を育てていきたいですね。

Hub によるプロンプトの管理

こちらはまだ運用で試せていないのですが、結構個人的には期待している機能です。

LangSmith の中に Hub という機能があり、ざっくり言うとプロンプトの共有サイトです。色んな人が投稿しているプロンプトを見て学ぶことができます。

Hubの画面
Hubの画面

が、これはプライベートなプロンプト管理ツールとしても優秀な予感がしています…!というのも以下の機能を備えているからです。

プロンプトは private にもできる

上記は公開されたプロンプトたちですが、Organization 以外の人が見られない private な状態にすることができます。

Privateに作ったプロンプト
Privateに作ったプロンプト

hub.pull でプログラマブルにアップロードしたプロンプトを引っ張ってこれる

下記のコードだけで Hub で作成したプロンプトを読み込むことができます。

from langchain import hub
obj = hub.pull("gaudiy/some-prompt")

ちなみに LANGCHAIN_API_KEY がない場合ちゃんと読み込めないことを確認したので、プライベートにしたプロンプトもちゃんと認証されています。

これを実行すると、上記の obj の中には次のようなデータが返ってきます。一通りのプロンプトのテンプレートやインプットの定義が取得できるので、これをコードにもバッチリ反映できます。

input_variables = ['post_content', 'feedback']
output_parser = None
partial_variables = {}

messages = [
    SystemMessagePromptTemplate(
        prompt=PromptTemplate(
            input_variables=[],
            output_parser=None,
            partial_variables={},
            template='あなたはプロのプロです。',
            template_format='f-string',
            validate_template=True
        ),
        additional_kwargs={}
    ),
    
    HumanMessagePromptTemplate(
        prompt=PromptTemplate(
            input_variables=['key', 'value'],
            output_parser=None,
            partial_variables={},
            template=(
                'こちらはホゲホゲ\n'
                '{key}\n\n'
                'ホゲータ\n'
                '{value}\n\n'
                '上記のフィードバックを元に投稿の内容を修正してください。'
            ),
            template_format='f-string',
            validate_template=True
        ),
        additional_kwargs={}
    )
]

コミットログと共にバージョン管理をすることができる

コミットのタイトルや変更理由を書く場所がないのがちょっと惜しいポイントではありますが、バージョン管理をすることができます。

コミットする一連のUI
コミットする一連のUI

これらの機能と共にこの Hub をプロンプトの Single source of Truth として扱えないかなぁと画策しています。具体的には下図のように Hub で変更があったら CI で検知してコードに反映し、都度 Hub にリクエストを送らずとも実行できるようにならんかなと。

HubとCIの連携図
HubとCIの連携図

ですが、今の所以下の機能が足りておらず、ゴリ押しできなくはないのですが今後に期待かなぁという印象です。

  • 階層やタグをつけることができない
    • プロンプトが大量に増えてくるとドメインに応じて分割して管理したくなってくる
  • Oranization のもの全部とってくるみたいなことができない
  • commit した時に変更検知できない
    • Webhook とかできると嬉しいかも

データの取り扱いについて

最後に、技術的な話ではないのですが、プロンプトのデータを LangChain のサーバに送ることもあり、内容によってはそもそも送って良いのかだったり利用規約の改変の必要があるかが議題に上がったので、そこで法務の方に相談して調べていただいた内容を記します。

結論から申し上げると

LangSmithでの個人データの処理については、規約・プライバシーポリシーを踏まえると、「個人データの取り扱い」ではないと整理できるので、いわゆるクラウドでの情報の取り扱いと同じく、同意は不要と考えて問題なさそうです(弁護士確認済み)

とのことでした。

LangSmith の利用規約とセキュリティーポリシーのリンクは下記の通りです。

https://smith.langchain.com/terms-of-service.pdf https://smith.langchain.com/data-security-policy.pdf

利用規約の 4.1 に「LangChain agrees that it will not use Customer Data to develop or improve its products and services」と「個人データを取り扱わない旨」が定められており、また、4.2の「(i) ensure the security and integrity of Customer Data (ii) protect against threats or hazards to the security or integrity of Customer Data; and (iii) prevent unauthorized access to Customer Data」と定められているため「適切にアクセス制御」も行われていると考えられる、とのことでした!

もちろんプロンプトの用途によっては、法律的にはOKでも世間的・倫理的にはアウトみたいなことはあり得るので都度判断が必要ですが、ひとまず規約に明示的にこの LangSmith のための同意を得る対応は必要なさそうです。

おわりに

以上、LangSmith の機能の解説と我々のチームで発生している課題の解決への希望について触れていきました。

実は似たような、いわゆる LLMOps と呼ばれるツールは今まさに群雄割拠で色んなツールが出てきているのですが、セットアップの簡単さからもわかる通り、 LangSmith の優位性は「LangChain という LLM アプリ開発のデファクトになりつつあるツールとの連携がすごい」ことにあるのかなと考えています。 トラッキング始めるまでの準備も環境変数を仕込むだけですし、ログも LangChain の Chain の種類に応じていい感じに整えて取得してくれます。

ある意味ではロックインのリスクとも取れるかもしれないのですが、LangChain をプロダクト開発に活用されている方はトライしてみるのもいいのではないでしょうか?

最後に、Gaudiy では現在 LLM をかなり踏み込んで活用して新しいエンタメ体験を鋭意開発中です!もしご興味持っていただければ一緒に働きましょう💪

herp.careers

herp.careers

https://herp.careers/v1/gaudiy/FAqLGhW5HFr5herp.careers

まずはカジュアル面談で「いや LLM 開発実際はね〜…」みたいなところから雑談するのでも大丈夫です、お待ちしております。

https://recruit.gaudiy.com/9b513345d630418799bdbb219acc7063recruit.gaudiy.com

それでは!!!👋

なぜ今、Gaudiyは3年越しにパブリックチェーンに戻るのか?

ファンと共に時代を進める、Web3スタートアップのGaudiyで、開発責任者をしている勝又(@winor30)です。

昨日、GaudiyはArbitrum対応のプレスリリースを発表しました。

prtimes.jp

これに関連して、今回は「Gaudiyはパブリックチェーンで、大きなエンタメIPとともに、ど直球なweb3的プロダクトづくりを本気でやっていくよ!」という宣言をしたいと思います!

この記事では、なぜGaudiyは今、本気でパブリックチェーンに向き合っていくのか?という背景や、今回の宣言に至るまでの経緯についてお話ししていきます。

1. そもそもGaudiyはなにを目指している会社なのか?

Gaudiyは「ファンと共に、時代を進める。」をミッションに、エンタメIPとファンの共創的な社会を実現しようとする会社です。

gaudiy.com

この世界観を実現するためのプロダクトとして「Gaudiy Fanlink」というコミュニティサービスを開発しており、このサービス上でファンがエンタメIPに貢献できたり、ファンの熱量の最大化をできるような体験を提供しています。

2. BCGコミュニティ時代のブロックチェーン技術や取り組み

実は元々、Gaudiy FanlinkはBCGのコミュニティサービスのようなプロダクトでした。ざっくりいうと、BCGに特化したDiscordのようなイメージです。

ユーザーも共創できるようなトークンエコノミクスを取り入れた機能が実装されており、BCGのユーザー自身もコミュニティに貢献できるようなプロダクトでした。

これを開発し始めたきっかけには、イーサリアム、ALISSteemitのようなトークンエコノミーが生み出す共創コミュニティがあります。このような共創コミュニティを誰でも作れるサービスをつくり、様々な分野でトークンエコノミーによる成長や進化を起こしたい。その思いで開発を始めました。

このサービスでは、BCGという特性もあって、かなりweb3的な機能も開発していました。

1つ目は、個人の信用に応じてトークンの交換レートを変動させる「Trust Economy Bonding Curves」というbonding curve(AMMの一種)を実装しました。ユーザーによるコミュニティの貢献に対する見返りを、トークンの交換レートという形で還元する仕組みとなっています。

ここではZilliqaを使ってbonding curveのコントラクトをつくっていました。当時、Zilliaはシャーディングをかなり早く導入していたり、scillaという関数型のスマコン用の言語を開発していたりと、かなりおもしろいチェーンであったため利用した背景もあります。

そもそもbonding curveとはなにか? でいうと、トークンの発行量を調整することで価格を自動的に決定するコントラクトになります。コントラクトにプールされているなんらかの通貨のトークン量に応じて、独自トークンの発行量を調整し、その発行量に応じてプールされているトークンを予めコントラクトにレートで交換できる仕組みです。イメージは、UniswapなどのAMMの一種というのがわかりやすいかなと思います。

実際につくったbonding curveの仕組みとしては、コミュニティ内でユーザーの貢献に応じた信用スコアが存在し、その信用スコアに応じてbonding curve交換レートが優遇されるという仕様でした。

また、bonding curveはシグモイド関数を利用しており、そのコミュニティの成長戦略などに応じてシグモイドの緩やかさを変更することも可能になります。

なぜbonding curveに信用スコアによる交換レートの優遇を掛け合わせたかというと、一般的なトークンで起きるボラティリティの問題を解決したかったからです。そのため、投資を目的としたユーザーよりも、コミュニティにより貢献しているファンを優遇するための信用スコアや、bonding curveのシグモイド関数の曲線を自由に設計できることを導入しました。

prtimes.jp

2つ目は、慶應義塾大学の坂井先生と共同研究・開発した、NFTに関するオークション理論「Gaudiy-Sakai方式オークション」を、Gaudiy Fanlinkのプロダクションで実装したことです。

当時はBCG中心に、NFTのプレセールなどで在庫連動型逆ダッチオークションという価格が初期高く設定され、時間経過とともに価格が下がっていく&入札があるたびに価格が上昇するオークション方式が使われていました。

ただ、この在庫連動型逆ダッチオークションには、課題がありました。それは、そもそも最初の初期設定の価格がわからず、初期が安いと思われてしまい、オークション開始と同時にいかに早く買い占めるかがベストな戦略になりがちということです。

これを「Gaudiy-Sakai方式」ではどのように解決していったかというと、一般的なオークションは「価格」を決めていくのに対して、NFTやデジタルアイテムの実質発行量が無制限という性質を考慮し、「発行量」を先に決定するフェーズを用意することで解決しました。

具体的には、1フェーズ目のオークションでNFTの「発行量」が決まり、2フェーズ目のオークションでNFTの「価格」が入札ごとに変動して決まる、競り上げ式のオークションです。参加者がより適正な評価によって入札することのできる、フェアなオークションの仕組みとなっています。

また、このときはBCGと電子マンガのIPのコラボNFTを販売するオークションを開催したのですが、対象ユーザーがBCGユーザーと一般ユーザーの2層に分かれていたため、UXをちゃんと作らないと中高生のような一般ユーザーの方が使えないものになってしまう可能性がありました。そのため、この複雑なオークション制度を、一般ユーザーが使えるようなUXを考慮してFanlinkに実装したことも工夫のひとつです。

一般的なNFT系のオークションは、やはり資本主義的な側面が強く、多くの資金を持っているユーザーが常に勝ちやすくなる側面があります。そのため、本当にほしいと思っているファンに商品が届かないリスクが存在しています。

一方のGaudiy-Sakai方式では、IPやコンテンツのファンを保護するためにそのリスクを回避し、原価やロジスティクスコストのかからないデジタルアセットで供給コントロールを適切なメカニズムデザインする設計にしました。オークションシステムとUXの両面でファンに寄り添ったものにできたのは、かなり大きい意味があったのではないかと考えています。

prtimes.jp

3つ目は、バーティカルなNFTのC2CマーケットプレイスをFanlinkに導入した事例です。

このNFTマーケットプレイスは、2019年頃に開発・実装したもので、日本では1, 2番目ぐらいに古いマケプレ開発事例になるのではないかと思います。

当時からOpenSeaはあったのですが、Gaudiyのマケプレの異なる点は、コミュニティに統合された形だったので、特定のNFTに関するレビューや会話なども行うことができ、よりユーザーフレンドリーになっていた点です。

技術的には0x protocolを利用しつつ、当時は0x protocolがNFTの支払いにはETHを利用できても売上の受け取りがERC20でしかできなかったため、ETHでも受け取れるようなForwarder contractというものを実装してUXの調整を行いました。

このように様々な新しいweb3機能の開発をしていましたが、すべてに一貫しているのは、ユーザーのことを第一に考えたプロダクトづくりです。新しい技術や概念が使われた新しい体験だからこそ、ユーザーにわかりにくく伝わる可能性が高いため、できるだけユーザーの方が直感的にも理解できるようなプロダクトを意識していました。

3. プライベートチェーンへの転換(エンタメコミュニティ)

2019年頃まではBCG中心のコミュニティを提供していましたが、ユーザーの熱量の高さや対象の広さを考え、よりマスなエンタメ市場でGaudiy Fanlinkを提供する方針にシフトしていきました。ただ、当時はweb3という言葉もほとんど浸透しておらず、「ブロックチェーン = 仮想通貨」で怪しいものという認知が一般的だったと思います。

Gaudiyのミッションは「ファンと共に、時代を進める。」という、誰もが好きなものに貢献することで生活できるような社会をつくることです。その実現のためにパブリックチェーンを使ってきましたが、ここでマスのユーザーに対する体験を考慮してプライベートチェーンを使う意思決定をしました。

ただ、ブロックチェーン的な体験やその価値を最大限分かってもらうためにも、プライベートチェーンを使いながらも、よりWeb3的な体験の開発は進めていきました。

例えば、2021年・22年の世界最大級のアイドルフェスで、Gaudiy Fanlinkでオンライン配信を行ったときのNFTサイン会はその一例です。アイドルの方がサインを書くライブ配信を行い、その配信をライブで見ていたファンのみが受け取れるような機能で、リアルとデジタルをつなぐような体験になっています。

このときのオンライン配信チケットも基本NFTで行っており、Sonyとの特許取得まで至っています。

他にも、サービス終了したアイドルに関するソーシャルゲームのデジタルアイテムをNFT化し、Gaudiy Fanlink上からそれらのデジタルアイテムにアクセスできるような機能の開発も行いました。

今までこのソーシャルゲームに毎月かなりの金額使っていたファンも少なくないタイトルであり、このデジタルアイテムをコミュニティに移してアクセス可能にしたことは、ファンの方々から非常に喜ばれるような体験になりました。

またファンの方だけでなく、このソーシャルゲームに携わっていたクリエイターやエンジニアの方にも感謝されるような体験を作れたことは、僕らが目指している「ファンも公式も同じエンタメIPに対する共創者」というビジョンに、少し近づけたことでもあるかなと考えています。

4. NFT・X to Earnの到来と疑問

同時期の外部環境としては、NBA Top Shotをはじめ、Axie InfinitySTEPNなどのNFT・X to Earn系のプロジェクトがマス層でも使われだしました。

ブロックチェーンやスタートアップに関係のないような高校の友達でもSTEPNを毎日やっていたようなレベルで、これらは非常に流行っていたので特に言及せずともおそらく多くの方が知っているかと思います。

正直な気持ちを言えば、Blockchainの技術に賭けているエンジニアの自分としては、これらのプロジェクトが流行ってマスのユーザーにも使われたのはかなり悔しい気持ちだし、正直やられた!という感じでした。

実際にNFTやX to Earnで生活が豊かになった方もいたり、海外では50代の夫婦が一軒家を買うまでに至れた事例などもあり、僕らが実現したかった世界に近いものを実現していたと思います。

ただよく考えると、これらが本当にNFTやweb3として正しい姿なのか?というと結構違うのかなと考えています。(現在ではNFTバブルがほぼ弾けていて結果論としての意見になるので少しずるい点はあるかもですが。。)

前提として、インセンティブによる動機づけや、エンタメなインターフェースと世界観を利用することでの心理的障壁の削減などによって、人々の行動を変容させ、大きな経済効果を生めた点にはものすごい可能性を感じました。

ただし、ボラティリティが高いことや、先行者優位で勝ち逃げがベストな選択肢になってしまう構造などにより、ユーザーが疲弊していったことも事実あったかなと思います。

現在では、当時流行っていたNFT関連のweb3系プロジェクトはかなり停滞しており、トークンの価格的にも冬の時代に突入していると思います。また、あれだけのユーザーに使われていた中でそのプロジェクトのことが本当に好きなユーザーはどのくらいいたのか?と思うと、体感ですがかなり少数だったと思います。

この点は、改めて考えると株式上場などの既存金融にヒントがあったなと感じました。どういうことかというと、経済圏も大きくないIPがトークン上場やNFTを発行した場合、ほとんどの人がおそらく投資目的です。するとユーティリティが目的にならないために、健全に経済が回らず、ボラティリティが上がり、ファンが悲しむし投資目的の人も不利益を被ってしまいます。

そして実は、株式上場ではこれらがきちんと対策されており、投資家保護のために一定以上の規模でないと上場(開かれたマーケット)できないのです。

そのため、大きな経済圏とファンのために、Trust Economics Bonding Curveのような貢献度に応じて取引レートが優遇されるAMMや、大きなマーチャンダイジングの強いエンタメIPでやっていくことが、よりボラティリティを安定させることにつながるのかなと考えました。

これらのプロジェクトが示してくれた可能性は理解しつつ、僕らが本当に作りたいのは、エンタメIPとファンのwin-winをつくっていけるようなエコシステムや社会です。

エンタメIPが中心として存在し、ファンも公式もこのエンタメIPを成長させるためのコントリビューターであり、この成長を持続可能かつより促進させるためにブロックチェーンのテクノロジーを使うべきだと自分は考えています。

5. Gaudiyがこれから向き合うパブリックチェーンへの挑戦

ここまで、パブリックチェーンBCGコミュニティ期〜プライベートチェーンエンタメコミュニティ期や外部環境の話をしてきました。

技術的には少し異なる部分はありますが、どの時期も一貫していることはファン目線です。そのコミュニティやIPを好きなファンが幸せになってほしい、そのためになめらかな価値分配を行うことができるブロックチェーンの力が必要で、ブロックチェーンのマスアダプションを実現しないといけないと考えています。

これらの歴史や得られた経験を糧に、これからのGaudiyは、パブリックチェーン x エンタメに着手にしていき「ファンと共に、時代を進める。」の実現に向けて本気で取り組んでいきます

これが実現できるという意思決定をした理由は、主に2つの大きなチャンスがあるためです。

1つ目は、今まで積み重ねてきたFanlinkの実績により、大規模エンタメIPのクライアント企業と、本質的に共創的なコミュニティをつくっていけるビジネス的なチャンスができたことです。

これはバブルを繰り返しながらもブロックチェーンやWeb3が社会全体に認知されてきたことも要因としてあるのですが、創業からの5年間、ブロックチェーンに関する開発をただ新しいからではなく本質的に価値のあるユーザー体験にしていこうと貫いてきたことで、クライアント企業の方々から信頼を獲得できた結果によるものです。

三菱UFJ銀行と協働でWeb3領域でのウォレットサービスを開発していく話も、こういった実績から実現に至った結果のひとつになります。

prtimes.jp

2つ目はブロックチェーン技術の進歩により、マスアダプションに向かうための下地ができつつある技術的なチャンスです。

スケーリングソリューションやウォレットのUXに関する技術がかなり進んできており、ガスレス、秘密鍵管理のUXの向上、ガスコストの低減、チェーン自体のスケーラビリティなど、一般のユーザーにも使えるレベルにはつくっていけるのではないかと考えています。

また個人的には、トークン価格的な観点で言えば「冬の時代」と今は言われますが、技術的にはむしろ一番おもしろいフェーズなのではと感じています。

これらのようなチャンスがあり、正しく掴んでいき、ミッションである「ファンと共に、時代を進める。」を実現するためにもGaudiyは下記領域などで勝負をしかけにいきます。

  • ユーザーが誰でも資産を持つことができるWallet
  • エンタメIP公認のUGCによるNFTやそれらを取引できるマーケットプレイス
  • 既存の制度にも適応できる誰でも参加が可能な新しい金融基盤

そしてこれらを進めていくためにも、ブロックチェーン領域に関するR&Dとすばやい実装、そして新しい技術をマスにも伝わるUXへ昇華させることが非常に大事だと考えています。

特にブロックチェーン領域に関するR&Dは、先日発表したGaudiy Financial Labsを中心に進めていきます。

prtimes.jp

ブロックチェーン領域に関するR&Dは、下記のような技術に対して投資していくことを考えています。

  1. Account AbstractionやUser Key ManagementなどのWalletに関する技術
  2. GaudiyにFitするappchainの探求
  3. ゼロ知識証明

Account AbstractionUser Key ManagementなどのWalletに関する技術については、今実際にGaudiy Fanlinkで使えるERC4337のスマートコントラクトウォレットを開発しています。しかし、Account Abstractionもまだ発展途上だったり、Modular Smart Contract Account(ERC-6900)のような新しい拡張性も提案されているため、引き続きR&Dしていくことも重要だと考えています。

また、Account Abstractionにおけるユーザーの秘密鍵の管理をどのように行うか?は、UXの観点では非常に大事な問題になります。

Account Abstractionによって検証方法が抽象化され、多様性が生まれたことが良いところではありつつ、ウォレットをノンカストディアルに扱うにはユーザー自身しか管理できない秘密な情報が必要です。この秘密をどのように管理していくか?に関する研究は、Wallet全体のUXに直結するため今後も行っていく必要があると考えています。(既存プロダクトだとweb3authやlit protocolはこの分野に該当すると考えています)

秘密の管理の話は↓下記記事が参考になります。

zenn.dev

またGaudiyにFitするappchainの探求については、今後Gaudiy独自のユースケースを満たし、チェーンの効率性を上げる必要が非常に高いため、行っていくべきだと考えています。

L1でNFTの取引をすることに$100ぐらいガス代を消費するときに比べて、Rollup技術の登場で比べものにならないくらい安くなったことは、現在のスケーリングソリューションの進化の結果だと考えています。

一方、L2と呼ばれる今あるロールアップはあくまでも汎用的なものであり、より限定的なユースケースを満たすためにはappchainが必要になってきます。例えば、プライバシーが必要になるケースや、よりコストやスケーラブルにする代わりにtrustなポイントを増やすValidiumなどがあると思います。

Gaudiyの「ファン国家」の基盤となるようなchainはどのようなchainにすべきか?を事業面や体験面など様々な観点で考え、ベストなappchainをつくっていくためにも、これらに関する技術投資をしていきたいです。

ゼロ知識証明については、今おそらくブロックチェーン業界でも最もhotなトピックだと思います。

ゼロ知識証明はざっくりいうと、「証明したい主張を明らかにすることなく、主張の妥当性が検証可能になる技術」であり、この情報の非対称性のような特性がプライバシーやスケーリングをもたらします。

また、個人的に思うゼロ知識証明がブロックチェーンにもたらす一番の価値は、様々なオフチェーンのイベントをオンチェーンで検証可能になることだと考えています。

例えば、zk rollupもオフチェーン(L1の外)で発生した複数のトランザクションによって生じる変更の正しさを証明できるproofを生成し、オンチェーン(L1)にコミットすることで、誰でもオフチェーンでのトランザクションの妥当性を検証することができます。

最近の例では、RISC Zeroが提供しているzkVMは、任意のコード(Rust、C、C++)を正しく実行できたことをプログラムの出力とproofで検証可能になるコンピューティング基盤を提供しています。これによりオンチェーンでは複雑で難しい計算も柔軟に実行でき、かつ情報の秘匿化も可能になります。

「既存の制度にも適応できる誰でも参加が可能な新しい金融基盤」をつくっていく上で、この特性はパブリックチェーンの"パーミッションレス"と既存制度にも適応できる"パーミッションド"の絶妙なバランスに不可欠だと自分は考えており、ゼロ知識証明に関する研究も進めていきます。

また、パブリックチェーンで挑戦していく第一歩として、Gaudiyの利用していくパブリックチェーンのひとつとしてArbitrumの利用を意思決定しました。

prtimes.jp

Arbitrumを利用する理由は、Rollup技術がセキュリティとスケーラビリティの両立できるソリューションでマーケットとしても非常に大きくなっているためです。そして、Rollupソリューションの中でも最も使われているArbitrumを選択しました。

今だと他のOptimistic rollupやZK rollupも選択肢としてはありますが、ガス代の安さ、安定して利用できるぐらいの実績があるか?、dAppsやコアデベロッパーの数などのエコシステムの充実度を考慮した結果、Arbitrumがベストな選択肢であろうという結論に至りました。

また、Arbitrum OrbitによるL3を構築することも可能なので、ユースケース次第ではArbitrumの上にappchainを構築できる可能性も、技術的余白としてあることが選定の一因になっています。

6. まとめ

今回の記事では、Gaudiyが今までどのようなブロックチェーンに関する開発をしてきたのか?という話と、それらの成果と外部環境から生まれた2つのチャンスに対して、どのくらい本気で向き合っていくかに関して説明させていただきました。

個人的な話ですが、自分は2018年ごろからブロックチェーンに可能性を見出し、GaudiyにJoinしたエンジニアです。

そんな自分は今、最もブロックチェーン関する開発や本質的な価値提供ができるチャンスだと感じており、それができる最高の場所のひとつがGaudiyだと本気で思っています。

エンタメ企業がIPの利用をひらいていき、IPを使ってコンテンツをつくっていくようなクリエイターやコントリビューターが増えていき、これらの貢献によってIPの経済圏が拡張されていくような未来が、本格的に実現できそうなフェーズに達しています。

このチャンスを最大限生かしていきたいので、ぜひ興味ある方はカジュアル面談とかお願いします!

https://recruit.gaudiy.com/b4c7694e91f84aeaa48eab904c5497a9recruit.gaudiy.com

また、11月にAccount Abstractionの実践ワークショップ型イベントを開催するので、ブロックチェーン技術に少しでも興味のある方はぜひご参加ください!

gaudiy.connpass.com

参考文献

zenn.dev

vitalik.ca

www.risczero.com

UnityのWebGLアプリ開発における"使えないライブラリ問題"の回避策

ファンと共に時代を進める、Web3スタートアップのGaudiyでUnityエンジニアをしているくりやま(@xamel7)です。

Gaudiyでは"Gaudiy Fanlink"というブロックチェーンや生成AIなどの技術を活用したファンプラットフォームで、漫画、アニメ、アイドルといったIP(知的財産コンテンツ)独自のコミュニティの開発・運営をしています。

service.gaudiy.com

このFanlinkの一機能として、現在、新たに開発を進めているのがIPのカジュアルゲームです。

「GANMA!コミュニティ」で先日公開されたカジュアルゲーム

▼登録不要で遊べます

ganma-community.com

WebサービスであるFanlinkとの連携が必要なこともあり、GaudiyのUnityチームではWebGLビルドによるアプリケーション開発を行っています。

WebGLビルドは、スタンドアロンやモバイルなどのネイティブアプリ開発とは異なる開発方法を取るケースが多々出てきますが、情報が少ないと思うので、今回はその一部を紹介したいと思います。

1. WebGLとは?

本題に入る前に、UnityにおけるWebGLの特徴について説明したいと思います。 WebGLはブラウザ上で開発したアプリを動作させることができるため、様々なメリットが得られます。

1-1. メリット

  • 一度アプリを作ってしまえば同一のビルドで複数のOS上のブラウザから動作させることができる。
  • ゲームやアプリをプレイするためにダウンロードが不要なので、ユーザーが気軽に遊ぶことができる。開発においてもGoogleやAppleなどの認証を受ける必要が無いので計画を立てやすい。
  • C#だけでなくJavaScriptの豊富なOSSも活用することができる。

一方で、以下のようなデメリットも存在します。

1-2. デメリット

  • Unityの一部標準機能やUnity向けのライブラリで使えないものがソコソコある。 
    • JavaScriptやHTMLと連携することで回避できることが多い。
  • フロントエンド開発時においても、JavaScriptなどのWeb技術の知識が必要になる場合がある。 
    • 2023年移行はChatGPT,Copilotを使えば普段使っていない言語でも案外なんとかなる。
  • モバイルプラットフォームは、2022.3LTS時点においてサポート対象外です。
    • 躓くことは多いが、ここ最近はインターネット上に情報が増えているので大体なんとかなる。
  • プラットフォームによっては利用できるリソース(特にRAM)が少ない
    • Addressablesの利用や最適化が必要になることが多い、Sceneについても無駄のない構成に設計する必要がある。

その他にもプラットフォーム特有の制限が結構あるので、気になる方は以下の公式ドキュメントをご参照ください。

docs.unity3d.com

2. WebGLを使うとエラーが発生するケース

上記デメリット内で挙げた通り、WebGLビルド時には一部のUnity向けライブラリが使えないことがあります。GaudiyのバックエンドはGCPで構成されていますが、GCPのUnity向けライブラリについてもその対象となるため、現在は利用できません。

今回紹介する例では、Firestore(NoSQL ドキュメント データベース)へのアクセス方法について紹介したいと思います。

2-1. 共通の構成

データベース構造

1ドキュメントのみのシンプルな構造にしています。

Scene構成

読み書きイベントリガー用のButtonとInputFieldを配置しています。

2-2. Unity用FirebaseSDKを利用した方法

まずは最もシンプルなUnity用のSDKを利用する方法について説明します。WebGL以外であれば、この方法が一般的です。

ソースコード

データベース内の"FieldString"フィールドの文字列を読み書きする単純な例として記載しています。 この例ではSDKで用意されているメソッドに対して、"FieldString"フィールド格納場所を指定するだけで取得することができます。

    /// <summary>
    /// SDKを利用してFirestoreからドキュメントを取得する
    /// </summary>
    private async UniTaskVoid ReadFirestore()
    {
        var db = FirebaseFirestore.DefaultInstance;
        DocumentReference docRef = db.Collection("FirestoreCollection").Document("FirestoreDocument");

        _readText.SetText("DBの読み込み開始");

        // スナップショットの取得
        var snapshot = await docRef.GetSnapshotAsync().AsUniTask();

        if (snapshot.Exists)
        {
            // スナップショットをDictionaryに変換する
            Dictionary<string, object> snapshotDict = snapshot.ToDictionary();
            
            // 取得したドキュメントのFieldString要素を取得する
            var fieldString = snapshotDict["FieldString"].ToString();
            
            if(!string.IsNullOrEmpty(fieldString))
            {
                // 取得したドキュメントのFieldString要素が存在する場合はテキストに反映する
                _readText.SetText(fieldString);
            }
            else
            {
                _readText.SetText("FieldStringは空です");
            }
        }
        else
        {
            Debug.Log($"指定したドキュメントが存在しません");
        }
    }

    /// <summary>
    /// Firestoreのドキュメントを書き込む
    /// </summary>
    private async UniTask WriteFirestore()
    {
        Dictionary<string, object> data = new Dictionary<string, object>()
        {
            { "FieldString", _writeInputField.text },
        };
        await FirebaseFirestore.DefaultInstance.Collection("FirestoreCollection").Document("FirestoreDocument")
            .SetAsync(data).AsUniTask();
        Debug.Log($"DBに\"{_writeInputField.text}\"を書き込みました。");
    }

このスクリプトをUnityEditor上で実行し、テキストを入力後に書き込み→読み込みボタンの順で押すと、読み書きに成功していることが確認できます。

ただし、このスクリプトでWebGLビルドしてみると、以下のようにエラーが大量発生してしまいます。。。

原因は以下の通りで、DLLの設定を確認するとUnity向けのFirebaseSDKはWebGLビルドには対応していないことがわかります。

よって、WebGLビルドを利用する時はUnity用のSDKを使用せず、別の方法で読み書きする必要があります。

そこで本記事では、REST API を使った方法JavaScript用のSDKを利用する方法の2パターンを紹介したいと思います。

3. 使えないライブラリ問題の回避策(GCP編)

3-1. REST API を使った方法

REST APIは多くのプラットフォームで利用できます。WebGLビルドもその対象で、他プラットフォームと同様に利用することができるので、FireStore用にAPIが公開されていれば、UnityWebRequest(HTTPリクエスト)の機能を使用することで読み書きがすることができます。

また、今回のFirebaseSDKに限らず、他サービス向けのUnity用SDKでもWebGLに対応していないことは多々あるので、REST APIに対応している場合は同様の方法で対処することができます。

ソースコード

Firestoreに対してREST APIで読み書きする場合はJson形式でやり取りすることになるので、Json変換用のクラスを定義しておきます。

/*
 応答データの例
 
{
    "name": "projects/【プロジェクト名】/databases/(default)/documents/FirestoreCollection/FirestoreDocument",
    "fields": {
        "FieldString": {
            "stringValue": "テスト"
        }
    }
}
*/

using System;
using Newtonsoft.Json;

// 応答データ構造に基づいたクラスを定義
[Serializable]
public class FirestoreResponse
{
    [JsonProperty("name")] public string Name { get; set; }
    [JsonProperty("fields")] public Fields Fields { get; set; }
}

[Serializable]
public class Fields
{
    public FieldString FieldString { get; set; }
}

[Serializable]
public class FieldString
{
    [JsonProperty("stringValue")] public string StringValue { get; set; }
}

API情報を読み書き用のスクリプトから取得できる場所に設定しておきます。

    // Firestore APIのエンドポイント。プロジェクトID、コレクション名、ドキュメント名を適切に設定する必要があります。
    public const string URL =
        "https://firestore.googleapis.com/v1/projects/【プロジェクト名】/databases/(default)/documents/【コレクション名】/【ドキュメント名】";

    public const string APIKey = 【APIキー】;

※ 分かりづらいですが (default) の部分はそのまま (default) と記載する必要があります。 ※【】部分の情報は、FirebaseSDK利用時にStreamingAssetsに格納していた google-services.json に記載されている情報を転記します。

次に読み込み用のスクリプトを準備します。取得後のデータは先程作成したJson変換用クラスを使い、デシリアライズして目的のフィールド要素を受け取ります。

Jsonの変換にはNewtonSoft.Jsonを利用しています。

    /// <summary>
    /// Firestoreからドキュメントを取得する
    /// </summary>
    private async UniTaskVoid ReadFirestore()
    {
        UnityWebRequest request = UnityWebRequest.Get(FirebaseManager.URL + "?key=" + FirebaseManager.APIKey);

        await request.SendWebRequest();

        if (request.result == UnityWebRequest.Result.ConnectionError || request.result == UnityWebRequest.Result.ProtocolError)
        {
            Debug.Log("Error: " + request.error);
        }
        else
        {
            // JSONを取得してFieldString要素を取り出す
            var jsonData = request.downloadHandler.text;
            FirestoreResponse response = JsonConvert.DeserializeObject<FirestoreResponse>(jsonData);
            string fieldString = response.Fields.FieldString.StringValue;
            
            if(!string.IsNullOrEmpty(fieldString))
            {
                // 取得したドキュメントのFieldString要素が存在する場合はテキストに反映する
                _readText.SetText(fieldString);
            }
            else
            {
                _readText.SetText("FieldStringは空です");
            }
        }
    }

書き込み時は、Bodyに変換済みのJsonを格納する形でリクエストを送ります。

    /// <summary>
    /// REST APIを利用してFirestoreのドキュメントを書き込む
    /// </summary>
    private async UniTask WriteFirestore()
    {
        // 送信するJSONデータ。この例では、"FieldString"というフィールドに"MyValue"という値を設定しています。
        var writeText = _writeInputField.text;
        string json = GenerateJson(writeText);

        // リクエストの設定
        UnityWebRequest request = new UnityWebRequest(FirebaseManager.URL + "?key=" + FirebaseManager.APIKey, "PATCH");
        byte[] bodyRaw = Encoding.UTF8.GetBytes(json);
        request.uploadHandler = new UploadHandlerRaw(bodyRaw);
        request.downloadHandler = new DownloadHandlerBuffer();
        request.SetRequestHeader("Content-Type", "application/json");

        // リクエストの送信
        await request.SendWebRequest();

        // レスポンスの処理
        if (request.result == UnityWebRequest.Result.ConnectionError ||
            request.result == UnityWebRequest.Result.ProtocolError)
        {
            Debug.Log(request.error);
        }
        else
        {
            Debug.Log($"DBに\"{writeText}\"を書き込みました。");
        }
    }

    /// <summary>
    /// 送信用Jsonオブジェクトを生成する
    /// </summary>
    private static string GenerateJson(string inputText)
    {
        FirestoreResponse data = new FirestoreResponse
        {
            Fields = new Fields
            {
                FieldString = new FieldString { StringValue = inputText }
            }
        };
        return JsonConvert.SerializeObject(data);
    }

これでFirestoreに対して読み書きをすることが可能になります。

課題として、REST APIでは単純な読み書きは可能ですが、バッチ処理(トランザクション)や複雑なクエリに対応することができません。 それらの処理を行いたい場合は、次に紹介するJavaScript向けのSDKを利用する必要があります。

3-2. JavaScript向けのSDKを使った方法

この方法ではWebフロントエンドの機能を利用するため、jslibというJavaScriptに近い形式のファイルを利用してJavaScript用のSDKを扱います。

WebGL固有の方法でFirestoreとやり取りすることになるため、ソースコードを他のプラットフォームと共有する場合は #if UNITY_WEBGL && !UNITY_EDITOR などを追加し、他プラットフォームビルド時に参照されないようにする必要があります。

本手順は、これまでの方法と比べ少し複雑な手順となるため、最初におおまかな流れを説明します。

  • JavaScript向けSDKの読み込み
  • C#からjslibへ初期化メソッドの呼び出し
  • jslib内でSDKインスタンスの初期化
  • C#からjslibへ読み込みメソッドの呼び出し
  • jslib内で読み込み or 書き込みの実行 (以降は読み込みの場合のみ)
  • jslibからC#へ読み込み結果の通知
  • C#でUnityのテキストUIに結果を表示

事前準備

テンプレートの修正

ビルド時に生成されるindex.html等でSDKの読み込みを行う必要があるのですが、デフォルトの状態だとビルド時に出力されるHTMLファイルを編集することができません。

HTMLファイルを編集するためには Assets/WebGLTemplates のディレクトリにテンプレートファイルを格納する必要があります。 Unityで用意されているデフォルトテンプレートは以下に格納されているため、 Assets/WebGLTemplates にディレクトリごとコピーして格納します。

※ Macの場合は以下のディレクトリに格納されています。 /Applications/Unity/Hub/Editor/【Unityバージョン】/PlaybackEngines/WebGLSupport/BuildTools/WebGLTemplates/Default/

格納後は PlayerSettings > Player > Resolusion and Presentation > WebGLTemplate に格納したディレクトリの名前でテンプレートが登録されています。

今回はCustomという名前でディレクトリを作成しています。 ※反映されない場合はUnityを再起動してみてください。

JavaScript用SDKの導入

次にHTMLファイル(今回はindex.html)に以下のタグを追加してください。 これを追記することで後述するjslibファイル内からFirebaseインスタンスを参照することが可能になります。

  <script src="https://www.gstatic.com/firebasejs/9.22.0/firebase-app-compat.js"></script>
  <script src="https://www.gstatic.com/firebasejs/9.22.0/firebase-firestore-compat.js"></script>
  <script src="https://www.gstatic.com/firebasejs/9.22.0/firebase-auth-compat.js"></script>
  <script>
    window.firebase = firebase; // グローバルスコープにFirebaseインスタンスを公開
  </script>

ソースコード(jslib)

jslibファイル名自体は拡張子 .jslibであればファイル名は自由ですが、 Pluginsという名前のディレクトリに格納する必要があるので注意してください。 普段Unityを扱っていると見慣れない記述だらけのため、コメントを多めに記載しています。

var FirebasePlugin = {
    // IndexDBのパラメータを保持するオブジェクト、ダッシュボードで取得した値を転記してください。
    $FirebaseConfig: {
        apiKey: "xxxxxxx",
        authDomain: "xxxxxxx",
        projectId: "xxxxxxx",
        storageBucket: "xxxxxxx",
        messagingSenderId: "xxxxxxx",
        appId: "xxxxxxx",
        measurementId: "xxxxxxx"
    },
    $DB: null
    ,
    FirebaseInit: function () {
        window.firebase.initializeApp(FirebaseConfig); // windowを付与することでグローバルな値を取得することができます。
        DB = window.firebase.firestore();
    },
    ReadFirestoreJS: function (instanceID, callback) {
        // instanceIDは本メソッドを呼び出したインスタンスのID
        // callbackはC#側で定義したコールバック関数
        
        // ドキュメントの取得
        var docRef = DB.collection("FirestoreCollection").doc("FirestoreDocument");

        docRef.get().then(function (doc) {
            if (doc.exists) {
                
                var json = JSON.stringify(doc.data()); // jsonを文字列に変換してポインタに渡す
                var bufferSize = lengthBytesUTF8(json) + 1; // バッファするサイズを取得
                var buffer = _malloc(bufferSize); // バッファ用メモリを確保
                stringToUTF8(json, buffer, bufferSize); // 文字列をUTF8に変換してポインタに渡す
                Module.dynCall_vii(callback, instanceID, buffer); // C#で定義したコールバック関数を呼び出す
            } else {
                console.log("ドキュメントが見つかりません");
            }
        }).catch(function (error) {
            console.log("Error:", error);
        });
    },
    WriteFirestoreJS: function (keyPtr, valuePtr) {
        // 文字列はポインタとして渡されるので文字列に変換する
        var key = UTF8ToString(keyPtr);
        var value = UTF8ToString(valuePtr);

        // Firestoreドキュメントの取得
        var docRef = DB.collection("FirestoreCollection").doc("FirestoreDocument");
        
        docRef.update({
            [key]: value
        })
        .then(function () {
            console.log("ドキュメントの更新に成功しました");
        })
        .catch(function (error) {
            console.error("Error: ", error);
        });
    },
};
autoAddDeps(FirebasePlugin, '$DB'); // $DBのコードストリップ防止
autoAddDeps(FirebasePlugin, '$FirebaseConfig'); // $FirebaseConfigのコードストリップ防止
mergeInto(LibraryManager.library, FirebasePlugin); // LibraryManagerにFirebasePluginを統合

ソースコード(C#)

C#側では [DllImport("__Internal")] を付与することでjslibで定義(mergeInto())したメソッドの呼び出しが可能になります。

また、 [MonoPInvokeCallback(typeof(Action<int,string>))] を付与することで、jslib側にC#メソッドのポインターを渡すことができます。jslibにポインターを渡し、読み込み完了時の通知を受け取ることを実現しています。

まずは読み込み側のソースコードです。

public sealed class FirestoreReaderWebGL : MonoBehaviour
{
    [DllImport("__Internal")] // jslib内の関数を呼び出すためのattribute
    private static extern string ReadFirestoreJS(int instanceId, Action<int, string> receiveCallback); // jslib内の"ReadFirestoreJS"を呼び出す

    // 後述のコールバックで実行元のインスタンスIDとインスタンスをマッピングするためのDictionary
    private static readonly Dictionary<int, FirestoreReaderWebGL> Instances = new(); 

    [SerializeField,Header("読み込み実行ボタン")] private Button _readButton;
    [SerializeField,Header("読み込み結果反映用テキスト")] private TextMeshProUGUI _resultText;
    
    // 読み込みボタン押下イベントを購読する
    private IObservable<Unit> ReadButtonObservable => _readButton.OnClickAsObservable().ThrottleFirst(TimeSpan.FromSeconds(1));

    private void Start()
    {
        // インスタンスIDの紐づけを行う
        var instanceId = GetInstanceID(); 
        Instances.Add(instanceId, this);
        
        // 読み込みボタン押下イベントを購読する
        ReadButtonObservable
            .TakeUntilDestroy(this)
            .Subscribe(_ => ReadFirestore());
    }

    /// <summary>
    /// jslib経由でFirestoreからドキュメントを取得する
    /// </summary>
    private void ReadFirestore()
    {
        ReadFirestoreJS(GetInstanceID(),OnReadFirestore);
    }
    
    /// <summary>
    /// ReadFirestoreJSの実行結果のコールバック
    /// static関数のみ利用可能
    /// </summary>
    [MonoPInvokeCallback(typeof(Action<int,string>))] // jslibからのコールバックとして利用する際は本attributeを付与する
    private static void OnReadFirestore(int instanceId,string jsonString)
    {
        // Newtonsoft.Jsonを使用してJSONをDictionaryに変換する
        Dictionary<string, string> snapshotDict = JsonConvert.DeserializeObject<Dictionary<string, string>>(jsonString);
        
        // 取得したドキュメントのFieldString要素を取得して結果を表示する
        var fieldString = snapshotDict["FieldString"];
        if(!string.IsNullOrEmpty(fieldString))
        {
            Debug.Log($"取得したドキュメント : {fieldString}");
            Instances[instanceId].SetResultText(fieldString);
        }
        else
        {
            Instances[instanceId].SetResultText("fieldStringは空です");
        }
    }
    
    /// <summary>
    /// 結果テキストの反映
    /// </summary>
    private void SetResultText(string text)
    {
        _resultText.SetText(text);
    }
}

最後に書き込み側のソースコードです。こちらは単純に書き込む値をjslibに送っているだけのコードになります。 jslib側では文字列はpointerとして渡されるため、JavaScriptで文字列として扱えるように UTF8ToString()を通す必要があるので注意が必要です。

public sealed class FirestoreWriterWebGL : MonoBehaviour
{
    [DllImport("__Internal")] // jslib内の関数を呼び出すためのattribute
    private static extern void WriteFirestoreJS(string key, string value); // jslib内の"WriteFirestoreJS"を呼び出す

    [SerializeField, Header("書き込み実行ボタン")] private Button _writeButton;

    [SerializeField, Header("書き込む文字列登録用InputField")]
    private TMP_InputField _writeInputField;

    private IObservable<Unit> WriteButtonObservable => _writeButton.OnClickAsObservable();

    private void Start()
    {
        // 書き込みボタン押下イベントを購読
        WriteButtonObservable
            .TakeUntilDestroy(this)
            .ThrottleFirst(TimeSpan.FromSeconds(1)) // 連打防止
            .Subscribe(
                _ => WriteFirestoreJS("FieldString", _writeInputField.text) // jslib経由でFirestoreに書き込む
                );
    }
}

これでSDKを利用した読み書きが可能になります。

4. まとめ

UnityにおけるWebGLアプリ開発はブラウザ上で動作するため、他プラットフォームと異なる振る舞いをすることが多いのですが、今回説明したような工夫をすることでトラブルを回避する方法があったりします。

私はこれまでWebGLでのアプリ開発をいくつか経験してきましたが、今のところUnity非サポートのモバイルでも、最終的には(意味深)なんとかなることが多いので、WebGL未経験のエンジニアの方にもぜひ挑戦してもらいたいです!

そしてまだまだUnityWebGLの情報が少ないのでシェアしてほしいですw

長文となりましたが最後まで読んでいただき有難うございました!!興味ある方は、ぜひカジュアルにお話ししましょう!

recruit.gaudiy.com

くりやま (@xamel7) / X

Unityエンジニア以外も、全方位で採用強化中です!

recruit.gaudiy.com