AWSはマネージドサービスがやっぱりいいですよね。
Table of Contents
Langfuse v3がついにGAしました
皆さま年の瀬ですが、いかがお過ごしでしょうか?
LLMそのものにはあまり興味が持てなかった私ですが、案件で使って依頼Langfuse、もといLLMOpsのツール群が大好きになった私。
2024年12月9日にLangfuse v3がついにGAしてから、早く使ってみたいという気持ちからあろうことか年末の忙しい時期にLangfuse v3のアーキテクチャをAWSマネージドサービスで作ってみました。
良い子の皆さんは、大掃除や年越し準備をしている時期ですが、私はLangfuse v3をAWSマネージドサービスで作っていました.....。よろしくない。
だいぶ長い記事なので先に結論
Langfuse v3のアーキテクチャをAWSマネージドサービスで作りたい場合はぜひ拙作のTerraformモジュールをご利用ください。
Langfuseとは
まずおさらいですが、LangfuseとはLLMアプリケーション向けのオープンソースの監視と分析プラットフォームです。
端的に言えば、LLMアプリケーションに対してどんな入力が行なわれ、どんな推論が走り、途中にどんなツール呼び出しが行なわれ、結果ユーザーにどんな出力があったかを可視化・分析するためのツールです。
似たようなプラットフォームとしては、代表的なものにLangSmithがありますが、LangfuseはOSS版が公開されており、ライセンス料を払わず自前の環境にセルフホステッドで構築できます。
このセルフホステッドができることがLangfuseの強みであり、長らく案件でLangfuseを使ってLLMOpsを実施してきました。
我々のLangfuse v2アーキテクチャ
前置きの話が長くなりましたが、現在の案件ではAWS上にLLMアプリケーションを開発しているため当然ながら、LangfuseもAWS上に構築していました。
Langfuse v2は、Next.jsで構築されたLangfuse ServerとTrace/Observationデータやプロンプトマネジメント用のデータを格納するデータベース(PostgreSQL)のシンプルな2コンポーネント構成でしたので、次のようなアーキテクチャで構築していました。
特徴として、Langfuse ServerはAWS App Runnerでデプロイしており、データベースはAmazon Aurora serverless v2で構築、それぞれのつなぎ込みはVPC Connectorを利用するというかなりシンプルな構成にしていました。
ありがたいことにLangfuse v2はNext.jsで構築されていることからリクエストベースでコンテナが実行されればよいため、(常駐する必要がないため)コスト最適化の観点でもAWS App Runnerでデプロイしておりました。
また、データベース(PostgreSQL)もAmazon Aurora serverless v2で構築することでApp Runnerと合わせてインフラ管理をできるだけ最小限にするAWSマネージドサービスを組み合わせて運用できてました。
開発者も限られていることや強くインフラを意識してしまうのは本質的でないと考え、このような構成にしてましたが私はこの構成がシンプルかつAWSらしい構成でとても好きでした。
Langfuse v3のアーキテクチャと移行の課題
さて、話をLangfuse v3に戻します。
Langfuse v3がGAしたということで、早く使ってみたい気持ちがはやりますが、すぐに移行できる代物ではなかったのです。
Langfuse v3はLangfuse v2とは全く異なるアーキテクチャになっていました。
アーキテクチャの比較をLangfuse公式ドキュメントから引用します。
まずv2がこちらです。
そしてv3がこちらです。
Langfuse v3は、従来のLangfuse Web Server、Langfuse OLTP(PostgreSQL) に加えAsync Worker、Langfuse Queue/Cache(Redis)、Langfuse Blob Storage(S3など)、Langfuse OLAP Database(ClickHouse) の6つのコンポーネントで構成されています。
大幅に必要なコンポーネントが増えてしまったのです!
これでは単純にAWS App Runnerのイメージをv3に更新する形で移行ができないではありませんか...。困った困った。
Langfuse v3のアーキテクチャが解決したいこと
ちょっとぶーぶーと文句が出てきそうですが、調べていくとv3のアーキテクチャはv2のアーキテクチャが解決したい課題を愚直に解決していると感じました。
Langfuseの開発に直接携わっているわけではないので課題を正しく捉えていないかもですがリアーキに関する公式ブログ記事と実際のコードを参考に自分なりの考察とともに抜粋しまとめていきます。
近年のLLMアプリケーションの需要増加とObservation特性
LLMアプリケーション自体の数が増えたり、利用者が増えたことによりLangfuse自体の利用も増えてきたことからLangfuse Cloudでのスケーラビリティが問題になってきたと公式ブログで言及されています。
Initial Pain Point: By summer 2023, spiky traffic patterns led to response times on our ingestion API spiking up to 50 seconds.
From Zero to Scale: Langfuse's Infrastructure Evolution by Steffen Schmitz and Max Deichmann
ここからは私の私見ですが上記に加えて、昨今のLLMアプリケーションは単純な入力とLLMの出力だけで構成されるアプリケーションではなく、複数のツールを呼び出し、RAGのように複数のデータストアを参照し、より複雑な出力を返すアプリケーションが増えてきています。
また、よりよい顧客体験のため時間のかかる推論を並列・非同期で処理することも増えてきたため1回の回答生成(Langfuseではこの単位をTraceと呼びます)に対していくつもの中間生成結果(Observation)が同時発生することが増えてきました。
結果として、Langfuse v2のアーキテクチャでは増えゆく需要に対して、スケーラビリティが追いつかなくなってきたということです。
プロンプトマネジメントAPIの低レイテンシー要件
Langfuseにはプロンプトマネジメント機能があります。
プロダクションコードからプロンプトを切り出すことで、LLMアプリケーションの再デプロイをせずともバージョン管理されたプロンプトを画面から任意のタイミングで差し替えできる機能のことで近年のLLMアプリケーション開発にはなくてはならない機能となってきました。
近年のLLMアプリケーションの需要増加とObservation特性で取り上げたTrace/Observationの記録(Ingestion)に比べて、プロンプトマネジメントはレイテンシーの増加がより深刻な結果をもたらします。
Ingestionはプロダクションコードのなかで非同期・ノンブロッキングの手法を用いることで、エンドユーザーにレイテンシーを与えない作りができるため影響を最小限に抑えることができます。(とはいえデータ欠損など問題はありえますが...)
このあたりの知見はLLMOpsが始まる以前の一般的なWebアプリケーションのObservationの文脈でもよく取り上げられる話なので解決策を出すことに苦労はさほどしないはずです。
しかしながら、プロンプトマネジメントはLLMアプリケーションの動作起点になる処理のためレイテンシーが高いとユーザー体験に直結してしまいます。
この点も公式ブログで言及されています。
While tracing is asynchronous and non-blocking, prompts are in the critical path of LLM applications. This made a seemingly straightforward functionality a complex performance challenge: During high ingestion periods, our p95 latency for prompt retrieval spiked to 7 seconds. The situation demanded an architectural solution that could maintain consistent low-latency performance, even under heavy system load from other operations.
From Zero to Scale: Langfuse's Infrastructure Evolution by Steffen Schmitz and Max Deichmann
高度な分析要件と立ちはだかる巨大なデータ
繰り返しになりますが、LangfuseはLLMアプリケーションのTrace/Observationを "分析" するためのツールです。
分析ということは大量のデータを処理することになります。
昨今のLLMがよりロングコンテキストな文章を扱うことができるようになったことやマルチモーダルLLMの台頭により、 Trace/Observationのデータサイズが増えてきています。
大量のデータに対して複雑なクエリを用いて実施する高度な分析を行なうためにはPostgreSQLのようなOLTP(Online Transaction Processing) より、OLAP(Online Analytical Processing) が向いています。
後ほど説明しますがLangfuse v3ではOLTPのPostgreSQLに加え、OLAPとしてClickHouseを採用しております。
With LLM analytical data often consisting of large blobs, row-oriented storage was too heavy on disk when scanning through millions of rows. The irony wasn’t lost on us - the very customers who needed our analytics capabilities the most were experiencing the worst performance. This growing pain signaled that our initial architecture, while perfect for rapid development, needed a fundamental rethink to handle enterprise-scale analytical workloads.
From Zero to Scale: Langfuse's Infrastructure Evolution by Steffen Schmitz and Max Deichmann
Langfuse v3のアーキテクチャDeep Dive
さて、そんな課題を解決するためにLangfuse v3のアーキテクチャはどのようになっているのでしょうか?
少し公式ブログと実際のコードからDeep Diveしてみます。
(ここでは記載を省きますが、Langfuse Cloud版として取り組んだプロンプトマネジメントAPIをALBのターゲットグループで分けるなどのアーキテクチャ改善のお話もとてもおもしろいのでぜひ公式ブログをくまなく読んでいただくことをおすすめします!!!)
イベントのOLTP書き込み非同期化
散々Langfuse v3のアーキテクチャを見てきたのでもううんざりかもしれませんが、v3ではLangfuseアプリケーションがWeb/Async Workerの2つのコンポーネントに分かれています。
そして、それらの間にはRedisをキューとして挟むことでTrace/ObservationイベントをOLTPであるPostgreSQLにWebコンポーネントから見て非同期で書き込むことができるようになっています。
イベント書き込みをすべてAsync Workerで行なうことで、万が一スパイクアクセスが発生したとしてもアプリケーション全体でリミットを設けながら、PostgreSQLのIOPS上限を超えることがないようにしています。
また、細かいですが、WebからAsync WorkerへのキューイングにBullMQのDelayingを使うことで、イベントの書き込み順序を保持しつつ、Workerの過度な処理を制御させることができます。
(BullMQについてはMisskeyでも採用があり、TypeScriptでのキューイングで一定の地位を築いた感じがありますね。)
このあたりの処理の詳細はこちらのコード付近をご参照ください。
OLAPとしてのClickHouse導入とレコードの更新処理
(ここは個人的にLangfuse v3のアーキテクチャのなかで一番興味深い部分です)
散々繰り返してますが、Langfuse v3ではOLAPとしてClickHouseを導入しています。ClickHouseはOLAPに特化したデータベースで、分析ワークロードにおいてPostgreSQLに比べ効率的に処理できることが特徴です。
前述したイベントのOLTP書き込みと同様に、ClickHouseに対してもイベントの書き込みはAsync Workerで非同期で行なっています。
ここでポイントになることはOLAPの1行レコード更新はOLTPに比べてかなり遅い(書き込み後の読み取り一貫性が保証されるまでの時間が長い)ということです。
LangfuseはTrace/Observationがすべて一意のIDを持っているため、同じIDに対してイベントが発生した場合にはレコードの更新処理を実施する必要があります。
しかしながら書き込み後の読み取りの一貫性が保証されるまでの時間が長いOLAPでは、読み込み時にまだ更新処理が終わっていない古いレコードを読み込んでしまう可能性があります。
ここを深ぼっていくにはClickHouseのReplacingMergeTreeの仕組みを理解する必要がありますので一度ClickHouseの話をします。
しばしお付き合いください。
ReplacingMergeTree
ClickHouseのReplacingMergeTreeは、ClickHouseにおけるMergeTreeの一種で、データの挿入時に重複がある場合に古いデータを置き換えることができるテーブルエンジンです。
例えば、以下のようなテーブルがあるとします。
CREATE TABLE events
(
id UInt64,
text String
)
ENGINE = ReplacingMergeTree()
まず、このテーブルにIDが1のレコードを挿入します。
次に、IDが2のレコードを挿入します。ここまでは特に問題のない挿入だと思います。
では、次にIDが1のレコードをText=wowに更新してみましょう。ClickHouseでReplacingMergeTreeを使っている場合、まず更新分のレコードが重複した形で挿入されます。
その後、バックグラウンドタスクで古いレコードが削除され、新しいレコードが残ることで更新処理が完了します。これがClickHouseのReplacingMergeTreeの仕組みです。
この動作をすることで、ClickHouseの各ノード・OLAPキューブで更新処理が独立して実施でき、更新のパフォーマンスが向上するのですが、バックグラウンドタスクのタイミングを制御しきれないため、レコード更新後の読み取り一貫性が保証されるまでの時間が長くなるという問題があります。
(※正確にはselect_sequential_consistencyを使うことで一貫性を保証できますが、高コストかつパフォーマンスを犠牲にする運用となってしまいます。)
上記の例ではTextカラムのみの更新でしたが、レコードの一部カラムを断続的に更新する場合、一度ClickHouseから最新のレコードを取得してから更新処理を行なう必要がでてくるため、レコードの一貫性を保つことが難しくなるというわけです。
Langfuse v3ではReplacingMergeTreeをどのように使っているのか
Langfuse v3では、この問題をAsync Worker内部でのマージ処理によって解決しています。
Async Workerはひとまとまりのイベントを処理する際に、ClickHouseに書き込みをする前に内部的にマージ処理を行ない、レコードの一貫性を保つ状態を作り出し、ClickHouseに書き込みを行なっています。少し複雑なので図にて説明します。
イベントのOLTP書き込み非同期化にて記載の通り、イベントの書き込みはRedisをキューとして挟むことで非同期化されています。
ただし、正確にはBlob Storage(S3)にイベント全データをJSONで格納し、RedisにはイベントのIDのみを格納しています。(①②)
これはLLMへのコンテキストや生成結果をRedisに保存してしまうとRedisの容量が足りなくなるためと公式ブログで語られていました。
その後Async WorkerはイベントのIDをRedisから取得し、S3からイベント全データを取得します。(③④)
そして、イベント全データをまずOLTPであるPostgreSQLに書き込みます。(⑤)
同時にClickHouseへの書き込みも実行されます。 一回のClickHouse書き込み時にAsync Worker内部でレコードの最終更新状態になるように同一のIDでのイベントをマージ処理を行ない、ClickHouseへの書き込みキューに登録します。(⑥⑦)
そして、ClickHouseに書き込みを行ないます。(⑧)
こうすることで、ClickHouseのReplacingMergeTreeの仕組みを使いつつ、短時間の断続したレコード更新の際でも、Worker内部でのマージ処理によってレコードの一貫性を保つことができるようになっています。素晴らしい...。
このあたりの処理は複雑なのでぜひWeb、Async Workerの処理機構を一度コードでじっくり読むと良いでしょう。ざっくり面白いところは下記です。
- WebでS3へのイベントアップロード処理
- WebでBullMQを駆使してRedisにイベントIDを登録
- LANGFUSE_INGESTION_QUEUE_DELAY_MSに従ってDelayingしながら登録している
- Async Workerのキュー取得エントリーポイント
- このなかでS3からデータダウンロード・Redisに連携されたイベントタイプからそれぞれの処理に進んでいる
- TraceのAsync Worker処理
- ObservationのAsync Worker処理
- mergeObservationRecordsで⑥の処理を実施している
- ClickHouseへの書き込みキュー登録処理
ここまでのまとめ
Langfuse v3のアーキテクチャはLangfuse v2のアーキテクチャが抱えていたスケーラビリティの課題を解決するために、さまざまな工夫があったことがわかりました。
しかしながら、セルフホステッドで適用する場合、Langfuse v2のアーキテクチャよりも複雑になってしまったため、残念ながら構築ハードルが高くなってしまったのもまた事実です。
次の章では、冒頭でご紹介したLangfuse v2アーキテクチャからLangfuse v3アーキテクチャに如何に移行していったかをご紹介します。
先に構成図
先に結論からですが、こちらが新しく作成したLangfuse v3のAWSマネージドサービスの構成図です。
従来通りLangfuse Server(Web)はAWS App Runnerでデプロイしています。
Langfuse OLTP(PostgreSQL)も従来通りAmazon Aurora serverless v2で構築しています。
Langfuse Cache/QueueはAmazon ElastiCacheを使っています。DBエンジンはRedisのほかにValkeyもサポートされているため少しでもインフラ代を浮かせるため、Valkeyを採用しました。
Langfuse Blob StorageはAmazon S3を使っています。
Langfuse Async WorkerはAWS ECS Fargateでデプロイしています。
Langfuse OLAP DatabaseもECS Fargateでデプロイしていますが、データの永続化のためにAmazon EFSを使っています。また、ClickHouseへWeb、Workerが内部的にアクセスできるようにCloud MapのService DiscoveryでプライベートDNSを設定しています。
上記の構成は私のGitHubにてTerraform moduleとして公開していますので、興味がある方はぜひご参照ください。
tubone24/langfuse-v3-terraform
以降の章ではLangfuse v3のアーキテクチャをAWSマネージドサービスで構築するためにはどのような工夫が必要だったかご紹介しますが、Try&Errorの構築秘話みたいなものが続くので読み飛ばしてもらって大丈夫です。
Langfuse v2の最新バージョンまでアップデート
まずv2->v3の移行についてはMigrate Langfuse v2 to v3を参考にしました。
Langfuse v3.0.0の1つ前のv2バージョンがv2.93.7でしたので、まずはLangfuse v2.93.7にアップデートしました。
Before
After
これは、万が一v3に移行した際、動かなかったときに速やかにv2に戻すため、OLTPの切り戻し用マイグレーションスクリプト(LangfuseはORMにPrismaを採用しているのでdown.sqlの実行)を期待してのことです。
しかしながら、確認して気がついたのですが、Langfuseのマイグレーションファイル一式にはdown.sqlが存在していませんでした。
(理由は推測になりますが、往々にしてdown.sqlを利用したマイグレーションの切り戻しはデータの損失が発生する可能性があるため、自動のマイグレーションスクリプトには含めないということかもしれません。私も過去のプロジェクトで似たような判断をしたことがあります。)
この場合、万が一のときは切り戻しのためのマイグレーションスクリプトを自分で作成、もしくは直接DBの書き換えを行なう必要がありますので覚悟を決めました。
一応、Langfuse v2.93.7 -> v3.0.0の差分を確認し下記2つの簡単なマイグレーションのみが存在することを確認したため、手動での切り戻しも問題ないと判断しました。
packages/shared/prisma/migrations/20241124115100_add_projects_deleted_at/migration.sql
-- AlterTable
ALTER TABLE "projects" ADD COLUMN "deleted_at" TIMESTAMP(3);
-- DropForeignKey
ALTER TABLE "job_executions" DROP CONSTRAINT "job_executions_job_output_score_id_fkey";
-- DropForeignKey
ALTER TABLE "traces" DROP CONSTRAINT "traces_session_id_project_id_fkey";
インフラ構築
Migrate Langfuse v2 to v3ではv2とv3の両方のインフラを作成しDNSかEBか何かしらの手段でトラフィックを切り替える作戦を提示されていました。
丁寧に移行するならv2のApp Runnerとv3のApp Runnerを同時に動かし、v3のApp Runnerにトラフィックを切り替えるのがよいと思いますが、私はめんどくさいので下記の方法を取りました。
- v2のAWS App Runner・Amazon Aurora serverless v2を動かし続ける
- v3で新規に必要になった各種インフラを作成する
- WorkerはRedisのキュー契機で動くため、v3のイメージを当てても問題はない。
- v3のすべてのインフラが問題なく動いたことを確認しApp Runnerのイメージをv3に切り替える
- ここでOLTP/OLAPのマイグレーションが走る
図にするとこんな感じです。
こちらに新しいコンポーネントを足し込んでいきます。
ここからは実際に検証時にぶつかった課題をご紹介します。
Web、WorkerからClickHouseへのアクセス経路をどうするか
Langfuse v3のアーキテクチャではWeb、WorkerからClickHouseへのアクセス経路をどうするかが課題でした。
ClickHouse自体は外(VPC外)から直接叩く要件はなく、かつ不要なインフラを作りたくなかったため、Internet facing / Internal限らずALBやNLBでのエンドポイント化はせず、なんとかコンテナ間で通信を行なわせたいと考えました。
やりたいことのイメージはこんな感じです。
そこで真っ先に思いついたのはAWS ECS Service Connectでした。
AWS ECS Service Connectは、Amazon ECSで動かすコンテナ間での通信を簡単に行なうための機能です。
Service Connectを使うにはすべてのサービスをECS化する必要があります。これではLangfuse v2のApp Runnerで動かしているLangfuse ServerをECSに置き換えなくてはいけません。
Web Serverはスパイクアクセスにも柔軟に対応させつつ、動いていないときのコストを最小限にしたかったので、なんとかApp Runner構成を残す道を考えました。
そこで、Amazon ECS Service Discoveryを使ってClickHouseのプライベートDNSを設定し、Langfuse Web Server、Async WorkerはプライベートDNSの名前解決を実施することで直接ClickHouseにアクセスできるようにします。
ClickHouseのデータ永続化
もう1つの課題はClickHouseのデータ永続化でした。
なるべくインフラをマネージドサービスで構築したかったので、ClickHouseのコンピュートリソースはECS Fargateで構築することにしましたが課題はデータの永続化です。
ECS Fargateのエフェメラルストレージはタスクが終了するとデータが消えてしまうため、ClickHouseのデータの永続化が課題となりました。
そこで、今回はAmazon EFSを使ってClickHouseのデータを永続化することにしました。
余談: EFSのマウント失敗
完全に余談ですが、構築で検証中にEFSのマウントがうまくできない問題にぶつかりました。
理由はEFSをマウントする際に、EFSがrootユーザーのアクセス権限のところ、ClickHouseコンテナがClickHouseユーザーでマウントしていたためアクセス権が不正で怒られていたことやEFSのセキュリティグループがClickHouseサービスのセキュリティグループに許可されていなかったことが原因でした。
それ自体は原因さえわかってしまえば修正は大したことはないのですが、いかんせんタスクデプロイのときに出る失敗ログのみで原因にたどり着くのがEFS初心者としてはとても大変でした。
(キャプチャ取り忘れましたが途中botocoreのインストールをしてね、という謎のメッセージもでたので苦労しました。EFSで永続化チャレンジするときはエラーメッセージに騙されないようにしましょう!)
check that your file system ID is correct, and ensure that the VPC has an EFS mount target for this file system ID. See https://docs.aws.amazon.com/console/efs/mount-dns-name for more detail.
Attempting to lookup mount target ip address using botocore. Failed to import necessary dependency botocore,
please install botocore first. Traceback (most recent call last): File "/usr/sbin/supervisor_mount_efs", line 52,
in <module> return_code = subprocess.check_call(["mount", "-t", "efs", "-o", opts, args.fs_id_with_path, args.dir_in_container], shell=False) File "/usr/lib64/python3.9/subprocess.py",
line 373, in check_call raise CalledProcessError(retcode, cmd) subprocess.CalledProcessError: Command '['mount', '-t', 'efs', '-o', 'noresvport', 'fs-0472ff150300da61d:/var/lib/
余談: ClickHouseのデータを飛ばしてしまった
これも検証中の余談なのですが、EFSにデータ永続化を試みているときに誤ってClickHouseのデータを飛ばしてしまいました。
これがLangfuseでは通常の運用としてはサポートされていないため、データのリカバリーは自分で行なう必要がありました。
Langfuse v3にアップデートをすると、v2時代に蓄積されたイベントをv3のClickHouseにマイグレーションする必要がありますので、別途Background Migrationsという仕組みがLangfuseにあります。
アップデート後、上記のようにBackground Migrationsのステータスを示す緑色のステータスアイコンが表示されます。
こちらをクリックすることで、Background Migrationsの進捗状況を確認できます。
お気づきかもしれませんが、**一度成功したマイグレーションについては再実行ができません!強制実行もできません!
上記のキャプチャでは実は永続化前のClickHouseにマイグレーションを実施したあと、永続化に成功したClickHouseに接続し直した直後なのですが、いくつかのマイグレーションが完了しており、もう二度と古いデータにアクセスできなくなってしまったと絶望しました。
解決策としては、Background MigrationsのステータスをOLTP(PostgreSQL)から直接Failedに変更し、Retryボタンを押せるようにすることです。
このあたりの条件は当然ドキュメントに載っていないので、コードから読み取る必要がありますし、自己責任でお願いします!
コードはここらへんです。
まず、Background Migrationsテーブルを確認すると、以下のようにstatusがSuccess / Activeのレコードが存在してます。
SELECT * FROM background_migrations;
ここで、statusがActiveのレコードをFailedに変更します。上記のNext.jsコードを参考にFailedにするにはfailed_at, failed_reasonを設定する形でOKです。
UPDATE background_migrations SET failed_at = '2024-12-28 12:00:00', failed_reason = 'Simulated failure for testing' WHERE failed_at is NULL;
すると、retryボタンが押せるようになり、再度マイグレーションを実行できます。
これで、ClickHouseデータを飛ばしてしまったときにも自己責任でデータのリカバリーができるようになりました。
(ClickHouseをセルフホステッドからマーケットプレイス経由でCloud版に契約変更する際に参考になるかもしれません。)
まとめ
大変長くなりましたが、Langfuse v3のアーキテクチャのDeepDiveとAWSマネージドサービスで構築するためにはどのような工夫が必要だったかをご紹介しました。
2025年はもっとLLMを活用したアプリケーション開発が促進されることでしょう。今のうちにLangfuse v3のアーキテクチャを理解・移行しておくことで、よりスムーズなLLMアプリ開発ができることでしょう。
今回のアップデートを身をもって体感し、Langfuseがますます好きになったので、これからもLangfuseのアップデートを楽しみにしています。