Ship 26 NYC - ワークショップ - ミニワーカー

VVercel
Computing/SoftwareSmall Business/StartupsInternet Technology

Transcript

00:00:00皆さん、こんにちは。ジョナサン・クレム、またはジェイ・クレムと申します。
00:00:11Notionで開発者プラットフォームの開発を担当しており、特に新製品に力を入れています。
00:00:17お聞きになったことがあるかもしれませんが、「Notion Workers」という製品です。本日のワークショップでは、
00:00:23Notion Workersの概要と、なぜVercel Sandboxを使って構築したのかをお話しします。
00:00:28その後、Workersのミニバージョンをお見せしながら進行します。
00:00:36Vercel Sandboxでこのような製品がどう作られるか、感覚を掴んでいただけると思います。
00:00:43Workersに馴染みがない方に説明すると、カスタムコードを書いてNotionを拡張できるSDKとランタイムです。
00:00:50サードパーティのデータをNotionに同期したり、
00:00:57エージェント用のカスタムツール呼び出しを書けます。Notion Workersで食料品を注文したり、
00:01:04スマートホームを操作するような面白い使い方や、IT・セキュリティ領域での非常に複雑なワークフローも構築されています。
00:01:11Workersの素晴らしい点は、管理すべきインフラが一切ないことです。
00:01:17自分でコードを書くかコーディングエージェントに書かせれば、Notion側が常に稼働・実行されるよう管理します。
00:01:23これはNotionユーザー、特に開発者にとって大きなメリットとなっています。
00:01:30Notionが公式インテグレーションを作ってくれるのを待つ必要はもうありません。
00:01:36「Notionでこれができたらいいのに」と思う機能は、すべて自分で書くことができるのです。
00:01:42Notion Workersの開発に着手した当初、私が一番懸念していたのは「安全性」でした。
00:01:53信用できないユーザーコードを実行するプラットフォームの構築経験が多少あったからです。
00:01:59例えば、GitHub Actionsの開発に長年携わっていました。
00:02:04そのため、こうした製品のインフラ構築の難しさ、特に安全性に一番懸念を抱いていました。
00:02:09懸念事項は山ほどあります。例えば、ユーザーが書いたコードが
00:02:14Notionのデータベースや、本来アクセス権のないサービスに触れないようにする必要があります。
00:02:20また、あるユーザーが他のユーザーに悪影響を与えないようにすることも不可欠です。
00:02:26あるユーザーのコードが、他のユーザーのコードやシークレット情報にアクセスできるようなことは絶対に防がねばなりません。
00:02:31これはセキュリティだけでなく、公平性やリソース共有の問題でもあります。
00:02:37大量のCPUやメモリを消費する処理を誰かが行っている場合、
00:02:42同時に利用している他のユーザーに不当な影響が出ないようにしたいわけです。
00:02:48ここで少し余談として、エピソードをひとつ紹介させてください。
00:02:54リソース共有というのは非常に厄介な問題です。持ち時間を削ってしまいますが、面白い話です。
00:02:58GitHub Actionsを作っていた頃(Workersでも同様の懸念がありましたが)、
00:03:02任意のコードを実行できるプラットフォームを作ると、すぐに暗号通貨のマイニングを試みる人が現れます。
00:03:07数週間前にある人と話していたら、「どうやってそれを検知するの?
00:03:10CPU使用率が100%に張り付いているかどうかで分かるでしょ?」と言われました。
00:03:14ですが、実際は違います。プラットフォームが成功すると、マイナーたちはすぐに
00:03:20スクリプトを共有し合い、CPU命令の実行方法を変更して
00:03:26一見まったく無害なアクティビティに見せかけながら、システムにフラグを立てられることなく
00:03:31限界ギリギリのリソースを消費しようとするのです。
00:03:37そのため、これを正しく制御するには長年の歳月と何重ものセキュリティ、可観測性が必要で、極めて困難です。
00:03:42もうひとつ懸念すべきなのは、コード実行プラットフォームがあると、
00:03:48それを利用したDoS攻撃やボットネットのC&Cサーバーとしての悪用です。
00:03:54Notion Workersの立ち上げ当初から、こうしたことすべてに頭を悩ませたくはありませんでした。
00:04:00私たちが大好きなこと、つまり「ユーザーに愛される製品を届けること」に集中したかったのです。
00:04:07そこで、Vercel Sandboxを採用して構築することに決めました。
00:04:14Vercel Sandboxは非常に堅牢なインフラであり、これらの問題の多くを
00:04:18最初から解決してくれることがすぐに分かりました。では、Notion Workersの超簡単なデモをお見せします。
00:04:25ここにカスタムエージェントがあり、このワークショップが呪われているかどうかを判定するのが仕事です。
00:04:33私は「水星逆行」というWorkerを書きました。ご存じの方がいるかわかりませんが、
00:04:40水星の配置が逆行の位置にあるときは、大抵の場合
00:04:47縁起が悪いとされています。このWorkerには単一のカスタムツール呼び出しがあり、
00:04:54水星が逆行中かどうかだけを教えてくれる見つけたAPIを使用しています。
00:05:01エージェントに「私のワークショップは呪われていますか?」とプロンプトを送ります。少し考えた後、
00:05:14ネット接続が正常であれば、ツール呼び出しが始まるはずです。
00:05:22水星逆行Workerを呼び出し、それが公開している唯一のツールを実行して
00:05:27水星が逆行しているかどうかを確認し、エージェントが応答してくれます。これには
00:05:37GPT-54 nanoみたいなものを繋いでいるので、普段はもっと高速です。あいにくネット環境のせいですね。
00:05:46あらかじめ水星が逆行中かどうか調べた方がいれば分かりますが、実際逆行中なんです。だからこんなことが起きているのでしょう。
00:05:53スキップしますね。後で戻って応答が届いたか確認しましょう。
00:05:59まあ、答えはもう分かっているようなものですが。さて、Notion Workersがどんなものかイメージできたところで、
00:06:05私が書いた小さなスクリプトをご紹介します。ユーザーコードの受け取り、デプロイ、
00:06:13安全な実行、そしてエージェントにそのコードを教えてツール呼び出しを可能にする基本処理を行っています。
00:06:20これ完了しましたかね? ああ、届きましたね。あー、なるほど、
00:06:28水星逆行APIがダウンしていたようです。APIからの応答がありませんでした。
00:06:35まあいいでしょう、どこかにチケットを起票しておきます。よし、基本的なスクリプトがあります。
00:06:40完全に追いついて一緒にコードを書いたりする必要はありません。
00:06:44このような製品を作る際に直面し、解決しなければならない課題のポイントをかいつまんで説明します。
00:06:49必要であれば、make notion/vercel ship 2026 workers というリポジトリに
00:06:55本日実行するすべてのコードが格納されています。
00:07:01よし、ではもう一つのスライドを開きます。
00:07:08私たちが目指しているゴールは何でしょうか? ストリーミング対応のチャットエージェントがあり、
00:07:16信頼できない、あるいは中身のわからないユーザーコードで定義されたツールを呼び出せるようにします。
00:07:22そして、これをVercel SandboxとVercelのBlobストレージサービスで構築します。
00:07:26簡単なデモを実行してみます。本当に呪われていないといいのですが。
00:07:31メッセージを1つ渡すだけです。「こんにちは」と送ると、無事に応答が返ってくるはずです。
00:07:36バックグラウンドでデプロイ処理を行っているため、少し時間がかかります。
00:07:40今回はツールが呼び出されました。「say hello」というツールが呼ばれ、挨拶を返してくれました。
00:07:47普通のメッセージとして「1+1は?」と尋ねてみましょう。
00:07:52通常通り応答がストリーミングされるのがわかります。
00:07:58Vercel AI SDKを使ったことがある方なら、これが単なるツールエージェントループだとお分かりでしょう。よし、
00:08:04ストリーミング応答が返ってきました。もっと複雑なWorkerを呼び出すこともできます。もう一つお見せしますね。
00:08:14こちらでは、今このビルの住所にいると伝えています。
00:08:17「90分間あり、史跡を見に行きたい。徒歩15分以上の移動は避けたい」という条件です。
00:08:24これが、MCPサーバーよりもWorkersが好まれることがある理由を示しています。
00:08:29MCPサーバーの場合、呼び出し可能なバラバラのツールが多数存在します。
00:08:33複雑なことをやろうとすると、その手順をエージェントに説明する必要があり、
00:08:38ステップを実行しては思考トークンを消費し、また次のステップを実行する…という流れになります。しかしこれは、
00:08:45ニューヨーク市の位置情報、交通時間、ルート計画APIを大量に使用する、巨大で複雑な探索アルゴリズムを持つWorkerを呼び出します。
00:08:52「plan outing」という名前です。
00:08:56そのWorkerが、訪問をおすすめする史跡を提案して返してくれます。
00:09:00シティ・ホール・パークの近くにあるMIPOW記念碑の「フリーダムツリーの標識」を見つけてくれたようですね。
00:09:07素晴らしい。では、これがどう仕組み化されているか見てみましょう。Workerとは何でしょうか?
00:09:14今回の場合、単一のファイルで定義されたユーザーコードです。通常はユーザーによって定義されるか、
00:09:20GitHub上のどこかに置かれ、CLIでデプロイされるものですが、ここではサンプルをリポジトリにコミットしてあります。
00:09:26各Workerは、ツール名、エージェントが適切なツールへルーティングできるようにするためのツールの説明、
00:09:34ツール呼び出し時に必要な入力内容をエージェントに知らせる入力スキーマ、
00:09:38そしてエージェントがツールを呼んだ際に実際に実行されるコードを記した実行関数を公開する必要があります。
00:09:44例を見てみましょう。先ほどお見せしたGreeter(挨拶)ツールです。
00:09:51とてもシンプルです。このWorkerはindex.tsファイル内にある1つのJavaScriptモジュールで、
00:09:59「say hello」という単一のWorkerをエクスポートしています。ビルドプロセスの仕組みは後ほど説明しますが、
00:10:04この例では、モジュールのエクスポートキーがそのままツール名にマッピングされています。
00:10:09したがって、このツール名は「say hello」となります。簡単な説明と入力スキーマがあります。ここではZodを使って
00:10:16スキーマを定義し、それをJSON Schemaに変換しています。これがエージェントの求める形式です。
00:10:22そしてシンプルな実行関数があります。もちろん、もっと複雑なものを作ることも可能です。
00:10:27「plan outing」のワークフローを開いてみると、ツールの仕組みや呼び出しタイミング、
00:10:34動作内容をエージェントに伝えるための、かなり長い説明文があるのが分かります。
00:10:39そしてスクリプト自体も遥かに長く複雑です。全部は説明しませんし、
00:10:45私も一部しか読んでいませんが、とても良く機能します。これが今の私たちの開発環境です。
00:10:54これらのWorkerはビルドされ、Vercel Blobストレージにデプロイされます。
00:10:59次に何らかの方法でWorkerの内容を読み取り、エージェントに公開します。
00:11:03そしてそれらのツールがサンドボックス内で安全に実行されるわけです。ビルド部分は段階的に説明します。
00:11:09Notion Workersにはクラウドベースのデプロイ&ビルドプロセスがあります。今回の例では、
00:11:15ディスク上にこれらのWorkerをあらかじめビルドしておきました。各Workerにはコンパイル済みのTypeScriptコードや依存関係が含まれる
00:11:22tarball(アーカイブ)があります。特別面白い部分ではないので、スキップします。
00:11:26本日最大のテーマは、「見たこともなく、信用もできないユーザーコードを、
00:11:34いかにしてエージェントが安全に公開・実行できるツールにするか」ということです。これには2つのステップがあります。
00:11:39第1ステップは、ユーザーコードがBlobストレージに置かれた後、
00:11:43そのコードの中に何があるかをどうやって把握するか、という点です。エージェントがツールを呼び出す前に、
00:11:48ツール名、入力スキーマ、ツールの説明文をエージェントに教える必要があるからです。
00:11:53そして第2の疑問は、エージェントがそのツールを呼び出すと決めた時、
00:11:58実際にどうやって安全に実行するか、ということです。
00:12:04Notion Workers SDKの設計で特に気に入っている点は、「コード自体が自分自身を説明する」というアプローチです。
00:12:11Notion Workersの設計時、避けたかったのは、ユーザーにツール定義のTypeScriptコードを書かせた上で、
00:12:16「Workerを説明する静的なマニフェストファイルも作って、同じ内容を二重で書いてください」と強いることでした。
00:12:21スクリプトを実行して解析し、ディスク上でコンパイルしてからデプロイするような、面倒なローカルビルド工程も避けたかったのです。
00:12:27そのため、開発者にとって最高の体験、つまり「ツールを書くだけで完了し、
00:12:34そこからどうやって情報を抽出するかという難しい問題はプラットフォーム側で解決する」という形を目指しました。
00:12:40そこから情報を引き出すという難題の解決は、すべてこちら側の仕事にしたかったのです。
00:12:46非常にシンプルな図ですが、コードに入る前に
00:12:52これをどのように解決していくかをお話しします。Blobストレージ上にユーザーのコンパイル済みコードがあります。
00:12:59そのコードを使ってサンドボックスを作成します。サンドボックス内でコマンドを実行すると、
00:13:04そのユーザーコードがワークスペースのルートに配置されます。ユーザーが書いた index.js ファイルをインポートすると、
00:13:11エクスポート名、説明文、入力スキーマのすべてが取得できます。
00:13:19ここからが少しトリッキーですが、非常にうまく機能します。
00:13:23そのモジュールに対して JSON.stringify を実行し、標準出力にログ出力します。これらはすべて
00:13:29サンドボックス内で処理され、デプロイスクリプトがその標準出力を受け取ってパースします。
00:13:34「よし、これでWorkerに含まれる全ツールの名前、入力値、説明文が把握できた」となるわけです。
00:13:40今回の例ではそのままエージェントに渡しますが、Notion Workersの場合は
00:13:44これがデプロイパイプラインの一部となります。取得したすべての情報をデータベースに保存しておくことで、
00:13:49カスタムエージェントを実行するたびに、データベースからツールの説明を
00:13:53取得できる仕組みです。ここまでの仕組みについて、なんとなくイメージできましたでしょうか? よし、ではデプロイスクリプトを見てみましょう。
00:14:02最初のコミットに戻る必要がありますね。このスクリプトにはデプロイ処理と
00:14:12エージェント呼び出しが組み込まれています。実にシンプルです。すべてのディレクトリとWorkerを反復処理していくだけです。
00:14:18サブディレクトリのそれぞれに、先ほどお見せしたようなWorkerコードが入っています。
00:14:23私たちのタスクは、各Workerからすべてのツール情報を引き出し、
00:14:29この tools オブジェクトに格納することです。これがツールループエージェントに渡されます。これはVercel製の
00:14:35AI SDKの一部です。そしてユーザーのメッセージをエージェントに送り、出力をストリーミングして
00:14:42スクリプトから標準出力へ書き出します。まず解決すべきは、ソースコードをどうアップロードするかです。
00:14:48この部分はとても簡単で迅速です。ライブコーディングの代わりに、コードのまとまりごとに
00:14:53スキップしながら見せていきますね。タイピングを見せられても退屈でしょうから。コードが書けることは保証しますよ、
00:14:59ただ誰もそんな所を見たくないと思うので。少し進めると、`uploadSource` という関数があります。
00:15:06中身を見ていきましょう。Vercel Blob SDKをインポートしているのがわかりますね。非常に
00:15:13シンプルです。この `uploadSource` 関数を見ると、単にこの `put`
00:15:20関数を呼び出しているだけです。ユーザーのバンドルを `[Worker名]/bundle.tar.gzip` として保存するよう指定しています。
00:15:29ディスクからファイルをストリーミングし、Blobストレージに保存します。完了すれば、
00:15:35そのBlobからサンドボックスを作成できるようになります。これでアップロード部分は解決です。
00:15:43次に必要なのは、そのバンドルされたソースコードを受け取り、そこから
00:15:49サンドボックスを生成することです。そのためにVercel Sandbox SDKをインポートしました。下の方に
00:15:56`createSandbox` というヘルパー関数もあります。一部スキップしますが、要するに
00:16:03Blobストレージ内にオブジェクトが存在する状態で、署名付きURLを発行して
00:16:10サンドボックスサービスに渡します。そうすることでサンドボックスサービスは(有効期限10分に
00:16:16設定されていると思いますが)、BlobストレージからそのBlobを取得してサンドボックスを構築できます。
00:16:23Vercel Sandboxで特に気に入っている機能の1つが、このようなtarballを直接扱える点です。
00:16:28ユーザーコードのtarballを作成しておけば、サンドボックスサービス側にtarballから生成するよう指示するだけで、
00:16:35ストレージや指定URLから取得し、ワークスペースのルートへ自動的に解凍してくれます。
00:16:40つまり全ファイルがすぐに使える状態で配置されるのです。これは `sandbox.create` を呼ぶだけで完了します。
00:16:46まだ超複雑な領域には入っていませんが、もうすぐそこに到達します。
00:16:52もう一つ補足すると、このサンプルでは簡潔にするため、実行のたびに
00:16:57新しいサンドボックスを作成しています。実際運用する際は、スナップショット機能を活用し、
00:17:02ツールが呼ばれるたびにサンドボックスを再デプロイしたり、Vercel Blobストレージ(あるいは
00:17:09使用している他のクラウドBlobストレージ)から再ストリーミングしたりしないようにします。
00:17:15プラットフォームには基本的にキャッシュ機構が組み込まれています。ここではわかりやすさを優先して割愛しました。
00:17:19これがサンドボックスの生成処理であり、ここからが非常に興味深い部分です。
00:17:23次に行うべきは、作成したサンドボックスから、
00:17:31そのWorker内で定義されているツールに関する情報を抽出することです。
00:17:36ここで一度立ち止まって、全体にちりばめられたコツをいくつか紹介します。
00:17:40本格的な商用グレードのサービスを構築する場合は特に、
00:17:44常にサンドボックスの停止処理に最善を尽くす必要があります。今回の例で言えば、
00:17:51`extractTools` の処理が終わった際、明示的にサンドボックスを削除しているのがわかるはずです。
00:17:56実運用では、非同期のクリーンアップジョブなどをキューに追加する工夫も
00:18:00必要でしょう。不要なサンドボックスを放置し続けるべきではありません。
00:18:06では、`extractTools` 関数の具体的な処理を見てみましょう。サンドボックスを用意しました。
00:18:15あきれるほど単純に見えるのに完璧に動作するので、私はこのサンプルがとても好きです。
00:18:23サンドボックス上の Node バイナリを使って Node コマンドを実行し、スクリプト内でユーザーの書いたモジュール
00:18:30(単なる index.js)をインポートします。そのオブジェクトを文字列化して、
00:18:36`console.log` を呼び出します。覚えておくべきなのは、サンドボックスはWebサーバーのように
00:18:42リクエストを送ればレスポンスが返ってくる構造ではない点です。すべての入出力はコマンド実行経由で行われます。
00:18:48ポーリング対象のサービスへレスポンスを送信させることもできますが、今回の場合は単に
00:18:52標準出力へログを出力させるだけで十分です。ここでアドバイスがあります。通常、`console.log` の出力や
00:19:00サンドボックスからの出力をそのまま鵜呑みにして信頼してはいけません。ユーザーがインストールした他のパッケージや
00:19:07実行中の別コードが、標準出力ストリームに同時にログを出力する可能性があるからです。
00:19:12そのため、出力をパース可能な特定のタグで囲む手法が効果的です。
00:19:18XML風のタグを付与するイメージです(このサンプルでは省略しています)。
00:19:23また、読み込むログのサイズ制限も忘れてはいけません。今回の例では
00:19:29すべての出力を単一のストリームに集約していますが、本番環境のアプリケーションでこれをやるのは危険です。
00:19:34ストリームが1ギガバイトに膨らみ、サーバーがメモリ不足(OOM)でクラッシュする恐れがあるからです。
00:19:39Sandbox APIにはログをストリーミングするためのコマンドが用意されているので、
00:19:45Notion Workersではそのストリームを監視し、対象となる出力開始トークンが
00:19:53現れるまで読み飛ばします。トークンを確認してから、意図的にログ出力した説明文のバッファリングを開始します。
00:19:58そして終了タグに達した瞬間に、出力ストリームの処理を停止するのです。こうしておけば、
00:20:04万が一サンドボックス内で予期せぬ異常が発生し、膨大なデータがログ出力されたとしても
00:20:08メモリを圧迫せずに済みます。不要なデータは破棄し、必要な情報だけを受け取ることができるのです。
00:20:14コマンドを実行したら完了を待ち、標準出力を取得します。
00:20:22Zodなどを使ったことがある方なら見覚えがあるでしょう。この文字列をJSONとしてパースしているだけです。
00:20:27そしてここにはZodの型があり、
00:20:31意図した構造になっているかを検証しています。
00:20:34これは信頼できないユーザーコードなので、データをそのまま信用しないことが非常に重要です。
00:20:39何が出力されるか分からないため、
00:20:43サイズと全体的な構造を必ず検証する必要があります。今回期待しているのは、
00:20:50文字列のキーが各ツールの名前となるレコードです。
00:20:57オブジェクトには説明と入力スキーマ(JSONスキーマ)が
00:21:02入ります。ここにはexecute関数がありませんね。
00:21:06返されない設計になっています。JSON.stringifyはシリアライズできない要素を
00:21:11スキップしてくれるため、単に無視されます。そのおかげで説明と入力スキーマだけを取得できます。
00:21:18上に戻ると、ここにあるworkerToolsが
00:21:23Worker内で公開されている全ツールのレコードになります。
00:21:28次に行うのは、このデータをAI SDKが求める形式に整える
00:21:36処理です。そして各ツールに実行関数を紐付ける必要があります。
00:21:42標準出力にログを出しただけでは、当然ながら実行関数は手に入りません。
00:21:46そこで問題となるのが、ツールの定義を取得したあと、
00:21:51SDKがこのWorkerやツールを呼び出したい時に実行できる関数をどう提供するかです。
00:21:58ここには executeTool というラッパーがあります。動作を
00:22:04見てみましょう。馴染みのある処理だと思います。同じ createSandbox 関数を呼び出し、
00:22:10新しいサンドボックスを作成しています。キャッシュを使う場合でも、Vercel サンドボックスには
00:22:16永続化機能があり、サンドボックスが停止するたびにディスク上の全状態を
00:22:23キャッシュします。しかし今回の機能ではそれは不要で、ユーザーコードが揃った
00:22:28初期状態のスナップショットだけが必要です。基本的にはツールが実行されるたびに
00:22:33完全にクリーンなインスタンスを用意すべきです。そうすれば、あるツールの実行で
00:22:38問題が起きても、次に実行されるツールの環境を汚さずに済みます。
00:22:43新しくサンドボックスを作成し、別の Node スクリプトを実行します。見た目は似ていますが、
00:22:49少し異なります。ユーザーが書いたモジュールをインポートし、そこから
00:22:56ツールを取得します。これは executeTool ラッパー作成時に取得したツール名です。
00:23:03その execute 関数を呼び出し、モデルから提供された
00:23:09入力を渡します。ここでは unknown 型としていますが、安全です。
00:23:19というのも、ここには…あ、飛ばしてしまいましたかね。この例では省略したかもしれませんが、
00:23:27通常行うのは…あ、やってありましたね。上に戻りましょう。ツールを
00:23:34整形してエージェントに送る際、JSON スキーマを
00:23:39Zod型に変換し直しています。そのため手動でパースする必要はありません。AI SDKのツールループコードが
00:23:46ツール呼び出しのたびに、エージェントからの入力を自動で検証してくれます。
00:23:52そのため、この値は期待通りであると信頼して問題ありません。これを execute 関数に
00:23:56ここの execute 関数に渡します。そして、その非同期関数が完了したら
00:24:03文字列化して標準出力にログを出力します。それから… このあたりは後ほど説明しますね
00:24:10説明しますが、コマンドの完了を待ちます。そして再び
00:24:14使い終わったサンドボックスを削除します。ユーザーが書いた実行関数の戻り値である
00:24:21JSONをパースし、それをツールループ
00:24:26エージェントに返します。全体の流れとしては、まずツールループエージェントが「plan outingツールを
00:24:35呼び出したい」とリクエストします。先ほど見た交通系APIを使うツールですね。すると
00:24:40エージェントが決定した入力と共にこの関数が呼ばれます。事前にアップロードされた
00:24:48コンパイル済みコードのBlobを使ってサンドボックスを作成し、ユーザーの
00:24:55execute関数を呼び出すコマンドを実行します。処理の完了を待ち、
00:25:00戻り値を標準出力にログ出力してパースし、エージェントへ返送します。エージェントは
00:25:05ツールループを継続し、別のツールを呼び出すかユーザーに回答します。コツをいくつか紹介します。
00:25:13本番システムでも同様の出力検証ルールが適用されます。ユーザーが
00:25:19パースに2GBもかかるようなオブジェクトを返せないように制限する必要があります。また、
00:25:28SDKを提供することも推奨されます。このサンプルでは省略していますが、Notion Workers SDKでは
00:25:35実行関数の戻り値がJSONシリアライズ可能でなければならない型定義になっています。
00:25:41関数の戻り値を何でも許容してしまうと、ユーザーはシリアライズできない値を返し、
00:25:47標準出力経由で送信・パースできなくなるという落とし穴にハマりがちです。
00:25:52結果としてWorkerが動かない理由が分からず混乱してしまいます。
00:25:57これはNotion Workersで実際にあった話なのですが、
00:26:05生成したNodeプロセスが必ず終了するように細心の注意を払う必要があります。
00:26:11以前Notionで、ユーザーコードの処理自体は完了しているのに
00:26:18なぜかNodeプロセスが終了しないバグがありました。サンドボックスのライフサイクルが切れる
00:26:25約5分間、プロセスがハングし続けていたのです。致命的ではないものの、
00:26:30リソースの無駄遣いでした。原因を調べると(Nodeランタイムの仕様をすっかり失念していたのですが)、
00:26:38ユーザーが setInterval や setTimeout でタイマーを設定するコードを実行していたのです。
00:26:44特に interval や未解決の Promise が残っている場合、
00:26:52スクリプトが終了してもNodeプロセスは復帰しません。
00:26:58タイマーがすべて終了するまで自動的には終了しないのです。そのため
00:27:03intervalがあると、サンドボックスが破棄されるまで永遠に動き続けます。したがって、
00:27:08実行したいコードや関数の処理が完了したタイミングで、明示的に
00:27:14プロセスを終了(exit)させる必要があります。そうすればサンドボックスが停止し、プロセスも正常終了します。
00:27:23ワークフロー全体の解説は以上です。もう一度実行してみます。
00:27:29内部の動きが分かるようにデバッグ出力を有効にします。
00:27:42まずdepartures workerをデプロイします。順を追って説明します。
00:27:48バンドルをアップロードしてWorkerをデプロイし、情報を抽出するための
00:27:54サンドボックスを作成します。抽出されたツールがこちらです。モジュールの内容を
00:28:01標準出力にログ出力すると、各ツールのキー、説明、入力スキーマが得られます。
00:28:08simple greeter workerでも同様の処理を行います。これらすべてをエージェントに渡し、
00:28:13ツール呼び出しとユーザーへの応答を行えるようにします。流れは以上です。
00:28:19信頼できないユーザーコードを受け取り、保存し、安全に情報を抽出して
00:28:26永続ストレージへの保存やエージェントへの転送を行い、安全に
00:28:31実行させるための一連の手順になります。残り10分ほどあります。
00:28:39Notion WorkersやVercelサンドボックス全般、あるいは安全なコード実行について
00:28:46質問があればぜひどうぞ。ありがとうございました。
00:28:56あ、そうだ。終了後、Notionの開発者プラットフォーム全般について話したい方は、
00:29:01私かこちらの同僚のMJにお声がけください。彼女はNotionの開発者プラットフォームのプロダクトマネージャーです。
00:29:08こんにちは。運用面での質問なのですが、ユーザーは何でも実行できるわけですよね。
00:29:15はい。複数のユーザーが似たようなツールを書くこともあると思います。
00:29:20複数のユーザーが何を行うと? 似たツールを書くことです。例えば「ニューヨーク旅行の計画」のような。
00:29:24はい。10人のユーザーが同じ機能を持つ10個のコードを書く可能性があります。
00:29:29そうですね。それを制限する仕組みはありますか? それともエージェント任せでしょうか?
00:29:33制限はしていません。複数のユーザーが同じことをしようとするなら、そのまま許可します。
00:29:37これはプロダクト設計の側面もありますね。同一組織内の複数ユーザーであれば、
00:29:42運用面だけでなくプロダクト設計としても重要になります。
00:29:46適切な共有機能を用意して、「同じ役割のWorkerがすでに存在するか」を
00:29:50検索・確認できるようにすれば、無駄な再作成を防げます。
00:29:55しかしプラットフォーム全体としては、完全に同一のコードがデプロイされる可能性は非常に低いため、
00:30:02重複排除の仕組みまでは入れていません。
00:30:19なるほど。あといくつか質問がありそうですね。マイクはどなたが持っていますか?
00:30:24聞こえませんでした。あ、すみません。
00:30:29ヘッドホンで拾えていると思っていました。
00:30:33あ、今の質問は「多くのユーザーが同じコードをデプロイした場合、
00:30:40運用側で最適化処理などを行うか」という内容でした。答えは行っていません。プロダクト設計上の課題ですね。
00:30:45ユーザーが重複作業をしないよう、優れた共有機能を開発中です。
00:30:50現在Notion Workers向けに取り組んでいます。ですがプラットフォーム運用レベルでは、
00:30:54同じコードが50回デプロイされても問題ありません。
00:30:56Workerの処理中にサンドボックスが早く終了してしまい、
00:31:04タイムアウトが発生する問題は起きていますか?
00:31:07タイムアウトについて、具体的にどのような問題でしょうか?
00:31:10例えばVercelの他プロダクトやワークフロー、
00:31:15Functionsとサンドボックス間でタイムアウトの問題はありますか?それとも順調ですか?
00:31:20特に問題は起きていません。プラットフォームは非常に安定しています。
00:31:25お世辞抜きで、本当に素晴らしい出来です。
00:31:32むしろユーザーが誤った操作をしてしまう問題の方が多く発生しています。そのため
00:31:36そうした落とし穴を無くし、開発者・非開発者を問わず
00:31:42より使いやすく改善していくことに注力しています。素晴らしいですね。質問なのですが、
00:31:48最初のユーザーコードを文字列化する部分についてです。安全性や実行可能性の
00:31:52確認のためだと思いますが、もう少し詳しくお聞きしたいです。
00:31:57コードを文字列化してWorkerタグからユーザーコードに必要な入力値と
00:32:04ツールの説明を取得するとおっしゃっていましたよね。これはNotionのエージェントが
00:32:10コードを実行するために必要な情報なのでしょうか?すでにユーザーの実行コード(executor関数)を
00:32:15走らせているように見えたので、なぜ入力値やツール自体の説明を取得する
00:32:21追加ステップを行っているのか気になりました。
00:32:24良い質問ですね。実際のプロダクトではもう少し明確ですが、ここでは簡略化しています。
00:32:29ご質問は「なぜWorker情報を得るために一度ユーザーコードを実行し、
00:32:34ツール呼び出し時に再度実行するのか」ということですね。理由は、エージェントが
00:32:39ツールを呼び出したりツールの存在を認識したりする前に、
00:32:45ツールの中身を把握してSDK経由でエージェントに伝える必要があるからです。
00:32:50例えばNotion Workersで ntn workers deploy を実行すると、ビルドパイプラインが走り、
00:32:55ビルドを実行するサンドボックスがtarballを受け取ってストレージに保存します。
00:33:02そして新しいWorkerを起動してツール名、説明、
00:33:09スキーマを抽出し、DynamoDB等に保存します。こうすることで、
00:33:14実際にツールが呼ばれるまでサンドボックスを起動せずに済みます。
00:33:21tarballの仕組みについて言及されましたが、そちらが一番気になっています。配信・配布に関しては、tarballは具体的にどのように機能するのでしょうか?
00:33:26オープンなマーケットプレイスのようなものがあるのでしょうか?それともどのような仕組みですか?
00:33:31tarballの中身や仕組みについてですね。
00:33:33技術的な仕組みとしてtarballに含まれる内容ですが、
00:33:38現在はesbuildを使用しています。実際のデプロイパイプラインでは、
00:33:47ntn workers deploy を実行するとAPIエンドポイントが呼ばれ、署名付きURLを取得して
00:33:52ソースコードをまとめ、tarballを作成します。サンドボックス上でesbuildを使ってビルドし、
00:33:58出力結果をBlobストレージに戻すことで、そこから実行できるようにしています。
00:34:04Workerの本格的なマーケットプレイスはまだありません。まずはNotionワークスペース内での
00:34:11共有機能に取り組んでいますが、将来的にはマーケットプレイスのようなものを構築する計画もあります。
00:34:18現状ではGitHub上で配布すれば問題なく動作します。多くのユーザーがそうしています。
00:34:23リポジトリを公開すれば、他の人がクローンして ntn workers deploy を実行して利用できます。
00:34:29はい。
00:34:34後ろの方で手が挙がっていますね。
00:34:37課金体系について質問させてください。
00:34:40課金についてですね?
00:34:40はい、簡単調べたところ(企業秘密まで明かす必要はありませんが)、
00:34:44カスタムエージェントはクレジット制で動いているようです。おそらく
00:34:48リソース消費量に連動していると思いますが。
00:34:51はい、その通りです。
00:34:51Vercelプラットフォームとの連携における課金の仕組みの大枠を教えていただけますか?
00:34:54なるほど、質問は「これらに対する請求はVercelプラットフォームとどう連動しているのか」ですね。
00:35:02Workerのメリットの一つは、MCPサーバーを使った肥大化した命令セットを
00:35:08エージェントに渡す運用から脱却できる点です。MCPサーバーではタスクを呼び出すたびに、
00:35:16エージェントが同じ処理を繰り返すために大量の思考トークンを消費してしまいます。
00:35:21そうした定型タスクをWorkerとしてデプロイすれば、
00:35:33Workerの実行時間に対する課金は発生するものの、AIの計算トークンよりはるかに安価に抑えられます。
00:35:40カスタムエージェントが思考し、ツールを実行し、さらに思考するケースでは、
00:35:48ツール呼び出し前に行われたエージェントのトークン消費に対して課金されます。
00:35:53そしてツール実行時は異なるレートが適用され、Notion AIクレジットの消費を大幅に抑えられます。
00:36:01最終的にエージェントが回答する際に、通常のAIクレジットが消費されます。
00:36:05つまりNotionカスタムエージェントで繰り返しのタスクがある場合、コストを大きく削減できる方法となります。
00:36:16その通りですね。またNotion AIとの連携機能としてツール呼び出しの実演をしていますが、
00:36:24WorkerはNotionへのサードパーティ同期なども行えるため、AI機能が必須というわけではありません。
00:36:31単なるAI製品ではなく、データ同期目的でも活用されています。
00:36:36私自身、LetterboxdのフィードをNotionに同期するWorkerを使っていますが、一番重宝しています。
00:36:46ほかに質問はありますか?
00:36:52もう一問いいですか?
00:37:06独自のインターフェース経由でNotionのカスタムエージェントを自社ユーザーに公開するような仕組みですね。
00:37:13現時点では提供していません。
00:37:16すみません、MJ補足ありがとうございます。質問は「自作のWorkerやカスタムエージェントを
00:37:22自社アプリを通じて自社のユーザー向けに公開する方法はあるか」でした。
00:37:28現状はそのような方法はありません。ただ、カスタムエージェントAPIのアルファ版があり、
00:37:35それを使えば実現可能です。ワークスペース内にカスタムエージェントやWorkerを定義すれば、
00:37:40API経由で呼び出してストリーミングレスポンスを受け取ることができます。技術的には可能です。
00:37:46まだ利用者は少ないですが、公開アルファ版機能として提供しています。
00:37:57ほかに質問はありますか?
00:38:00それでは、本日はお時間をいただき
00:38:04ご清聴ありがとうございました。NotionやNotion Workers、
00:38:08またはVercel Sandboxについての質問があれば、私かRJにお声がけください。ありがとうございました。

Key Takeaway

Notion WorkersはVercel Sandboxを活用して信用できないユーザーコードを安全に実行し、開発者が管理インフラなしでNotionを拡張できるランタイムを提供する。

Highlights

  • Notion Workersは、カスタムコードを実行してNotionを拡張できるSDKおよびランタイムである。

  • Notion Workersの基盤にはVercel Sandboxが採用されており、管理すべきインフラが一切存在しない。

  • ユーザーコードの実行時には、未解決のPromiseやタイマーによるNodeプロセスのハングを防ぐために明示的なプロセス終了処理が必須である。

  • Workerの情報抽出では、サンドボックス内でモジュールをインポートし、標準出力経由で取得したログをZod等のスキーマで検証している。

  • Notion Workersの導入により、エージェントが定型的な処理を実行する際のAI計算トークンコストを大幅に削減できる。

Timeline

Notion Workersの概要と設計背景

  • Notion WorkersはカスタムコードでNotionを拡張し、サードパーティの同期やエージェントのカスタムツール呼び出しを可能にする。
  • 任意のコードを実行するプラットフォームでは、安全性や暗号通貨マイニング、DoS攻撃などのリソース共有課題が生じる。
  • 開発チームは、インフラ管理ではなく製品の提供に集中するため、Vercel Sandboxを採用した。

Notion Workersは開発者がNotionの機能を独自に拡張するためのSDKおよびランタイム環境である。任意のコードを実行可能なプラットフォームの構築において、不正なリソース消費やセキュリティリスクを防ぐことは極めて困難である。そのため、堅牢なインフラを提供するVercel Sandboxを基盤として採用している。

ツールの情報抽出とサンドボックスの仕組み

  • Workerは単一ファイルのユーザーコードとして定義され、ツール名、説明、入力スキーマ、実行関数を公開する。
  • コードの安全性と信頼性を担保するため、サンドボックス内でモジュールをインポートし、標準出力経由でメタデータを抽出して検証する。
  • タイマーや未解決のPromiseによるNodeプロセスのハングを防ぐため、処理完了時には明示的なプロセスの終了が必要となる。

各Workerはツールの詳細な説明や入力スキーマを含み、エージェントが適切なルーティングを行えるように設計されている。Blobストレージに保存されたバンドルからサンドボックスが作成され、Nodeバイナリを介してコードの解析や実行が行われる。また、ログサイズの制限やプロセスのクリーンアップを適切に行うことで、リソースの無駄遣いやメモリ圧迫を防いでいる。

運用上の課題とQ&Aセッション

  • 同一内容のWorker重複デプロイに対しては、プラットフォームレベルでの制限ではなくプロダクト設計上の共有機能で対応している。
  • Vercel SandboxとAI SDKの連携により、エージェントの思考トークン消費を抑えつつ効率的なタスク実行を実現している。
  • 将来的なマーケットプレイスの計画に加え、現状はGitHub経由でのコード共有やカスタムエージェントAPIのアルファ版提供が行われている。

運用の現場では、ユーザーが重複するツールを作成する場合があるが、プラットフォーム側で自動的な最適化や重複排除は行われていない。代わりに、ユーザー同士が共有しやすい仕組みの構築が進められている。また、Workerを活用することでAIの計算コストが軽減され、データ同期などAI機能以外の用途でも広く活用されている。

Community Posts

View all posts