top of page

検索結果

空の検索で52件の結果が見つかりました。

  • Langfuseによるプロンプト管理 (後半) - プロンプト開発&実験編

    [ 前回の記事 ] では、「Langfuse」を活用したプロンプト管理の方法を具体的に解説しました。Prompt をハードコードすることなく、Diff 、 Commit コメント 、Duplicate (複製) 、タグ などの機能を、直感的にエンジニアから非エンジニアまで幅広く使うことができますという内容です。まだご覧になっていない方は、ぜひチェックしてみてください。 さて今回は、Langfuseでのプロンプト開発と評価について触れていきたいと思います。 Langfuseによるプロンプトの新規作成 Langfuse にログインをしたら、左側のメニューバーから [Prompts] を選択し、右上の [+ New prompt] から新規作成を始めます。 画面右側の [+ New prompt] を選択 そして以下の新規作成ページに移りますので、必要項目を埋めるだけです。 新規作成ページ 各項目の説明は以下の通りです。 Name は Prompt に対する任意の名前で、プログラム側から指定するもの Prompt に具体的に入れたいプロンプト内容を入力します。Text か Chat を選ぶことができ、Chat を指定すると、それぞれのrole (System, Assistant, Developer, User) を指定してメッセージを入れることが可能です。Prompt の中には {{variable}} の形式で変数を埋め込め、ユーザーの入力値などを受け取って処理するために使えます。 Config は前回のブログでご紹介した通り、JSONで任意の値を入れることができるパラメーターになっております。例えば以下のようにモデル名の指定などで使うことができます。 { "model": "models/gemini-2.0-flash-001" } LabelsはデフォルトでProduction ラベルをつけるかどうかの選択です。Productionになっていると、プログラムコードでラベルを指定しない場合に選択されて使用されます。後で変更可能です。 Commit message は必須ではありませんが、Version1 以降はつけておくとチームメンバーなどに意図を伝えるために有益だと思います。 Playgroundの利用 まず新規での作り方を記載しましたが、場合によっては0から考えるのが難しいことがあると思います。そんな時に使える Playground 機能をご紹介します Playground 画面 左側のメニューから [Playground] を選択すると、Playground画面に遷移します。 基本的な使い方としては、試してみたいPrompt を [Messages] に入れ、画面右側で定義した [Model] で、モデルの種類や、各種パラメータを設定するだけ* です。また変数を入れて、右下の [Variables] に値を入れます。 例えば今回のPromptには 以下の文章が LLM の動作原理を説明する文章として正しいかどうかを判定しなさい。 {{text_input}} という記載が含まれており、この変数 text_input の値として テキストボックスに LLM は、主に「Transformer」と呼ばれる深層学習 (ディープラーニング) のアーキテクチャを基盤としています。このモデルは、大量のテキストデータ (書籍、ウェブサイト、記事など) を読み込み、単語や文の間の関係性 (文脈、意味など) を学習します。また学習した知識を基に、与えられた入力 (プロンプト) に対して、最も適切と思われる出力 (文章、回答など) を生成します。 を入れます。そしてSubmit すると、 [Output] にLLM からの回答が入っています。 この内容が気に入れば、右上の [Save as prompt] を押すと先ほどの新規作成画面に遷移し、そのまま新規Prompt にすることができます。 気に入らなければ好きなだけ修正するなり、初期化したければ [Reset playground] をするだけです。 *Model自体の登録はこの画面ではなく、メニューの [Settings] から あらかじめ設定しておく必要がありますのでご注意ください。Langfuse側からLLMモデルを利用しますので、課金されることにご注意ください。 Prompt Experiment の利用 新規作成後、LLMアプリケーションの運用においてPromptの改善が必要な場面に出くわすと思います。Prompt Experiment 利用すると変更したプロンプトに対して、リリース前に過去のユーザーのTrace をDataset にするなどして挙動が改善するかを確かめることができます。 Prompt Experiment 文字通りPrompt の実験として、対象Prompt とバージョンやモデルなどを指定し、 利用するDataset を設定します。 Datasetの指定 このDataset は0から作っておくもよいですし、Trace から直接Dataset に入れることで過去正常に動いたものがデグレしないか、不具合あったInputが改善されているかを実際に確かめていただくことができるようになります。また、LLM as a judge などと連携することで、自動で定量的な評価なども付加することが可能です。LLM as a judge については、 こちらのブログ で詳しく紹介しておりますので、是非あわせてご覧ください。(当該ブログの " テスト実行と結果の可視化" に相当する部分が本項に相当します。) なお今回ご紹介したPlayground とExperiment はSelf-hosted のOSS 版には含まれません (SaaS であれば無償のHobbyプランで使えます) のでご注意ください。 まとめ:Langfuseでプロンプト新規開発とテストを簡単に。 いかがでしたでしょうか。Langfuseを活用し、Promptの新規作成 - 管理までを簡単に実施できることがご確認いただけたと思います。本記事が効率的なLLMアプリケーションの開発とオペレーションのお役に立てれば幸いです。 本機能の具体的なユースケースや使い方のデモなどにご興味がある方は、ぜひ こちらから お気軽にお問い合わせください。

  • Langfuseによるプロンプト管理 (前半) - 基本 & 管理編

    [ 前回の記事 ] では、プロンプト管理の重要性にくわえて、コード埋め込みやGit または データベースによる管理の課題について解説しました。今回は、それらの問題を解決すべく「Langfuse」を活用したプロンプト管理の方法を具体的に解説します。 Langfuseとは?:LLMの開発と運用に特化したオープンプラットフォーム Langfuseは、生成AIアプリケーションの開発や運用に特化したオープンソースのプラットフォームです。可視化やテストなど生成AIアプリケーションのライフサイクル全体を管理するものですが、今回は特にプロンプト管理に焦点を当ててご紹介をしていきます。 Langfuseによるプロンプト管理の主な特徴は以下のとおりです。 使いやすいUI: 直感的なインターフェースで、プロンプトの作成、編集、比較、テストが容易に行えます。 バージョン管理: 変更履歴を自動的に記録し、過去のバージョンとの比較やコピー、修正などが簡単に行えます。 詳細な分析: プロンプトのパフォーマンス(応答時間、コスト、品質スコアなど)を詳細に分析し、改善点を見つけやすくします。 柔軟な評価: LLM as a judge などによる自動評価指標に加えて、人間による評価 (Human Annotation) も組み合わせて、多角的にプロンプトを評価できます。 チームでの共有: プロンプトをチーム内で共有し、共同で作業できます。 実践!Langfuseによるプロンプト管理の具体的なステップ 前提: Prompt はLangfuse から取ってくる Langfuseを使うと、プロンプト管理はどのように変わるのでしょうか? まず前提としてアプリケーションコードに埋め込む形とは異なり、Prompt 本体は Langfuse に格納され、それをプログラム側から fetch する形となります。 アプリケーションはLangfuse に格納されているPrompt を取りに行き、適切なものを入手します。なお本番運用した際に都度取得をする必要は必ずしも無いので、TTL を設定してキャッシュする運用が現実的だと思います (デフォルト60 sec、0で即時反映)。 以下はPython のコード例です。極めてシンプルな実装が可能です。 この場合、wweという名前で管理されているPrompt をfetch してきます。 from langfuse import Langfuse # Initialize Langfuse client langfuse = Langfuse() prompt = langfuse.get_prompt("wwe") TTLを設定する場合や、特定のラベルのPrompt を取得したい場合は以下のようになります。ラベルについては後述します。 # TTL を 300に指定 prompt = langfuse.get_prompt("wwe", cache_ttl_seconds=300) # the-rock-is-cooking というラベルの Promptを取得 prompt = langfuse.get_prompt("wwe", label="the-rock-is-cooking") その他のオプションや記述例はLangfuse の公式ドキュメントに豊富な例が用意されていますので、参考にしてみてください。 URL: https://langfuse.com/docs/prompts/get-started 余談ですが、同サイトの右上にある Search 欄から、Ask AI (Cmd + k) で日本語で質問することも可能です。日本語でも精度が高いので、ぜひ使ってみてください。 さてこれから、実際のプロンプト管理を見ていきましょう。 Prompt の作成とバージョン管理 LangfuseのUI上では、直感的にプロンプトを直接作成・編集できます。 変更を加えるたびに、自動的に新しいバージョンが作成され、変更履歴が記録されます。 以下は プロンプト管理画面のサンプルです。 これは toyidea という名前のプロンプトで、右側で各バージョンを確認することができます。現在、Production に適用されているのは Version 2 のプロンプトです。 また、latest ラベルがついている Version 3 にはコメントがついており、"独自アイディアの追加プロンプト試行" とコメントがついています。併せて作業者もそれぞれ情報として付加されており、いつ誰が何のためにバージョンを変えたのかも非常に分かりやすくなっています。 ラベルについては最新のものには自動で latest がつきますが、この画像のように自分で任意の名前をつけ、前述のサンプルのように指定したものを fetch することもできます。 Version 間での詳細を比較したい場合には、GUI 上でDiff をすることもできます。相違部分がハイライトされおり、一目で差異が分かります。 またPrompt にはConfig として、以下のように任意の情報を持たせておくことができます。モデル名, Temperature などをPrompt と一緒に取得することで、管理を一元化し、結果の整合性を維持することが期待されます。 例えばPrompt はLangfuse から持ってきても、コードの中で別のモデルを指定していると、テストしておいた結果と実際の結果は異なってしまいますが、管理をまとめることでそのような問題を防ぐことができ、コード自体もシンプルになります。 作成したプロンプト活用して、別のアプリケーションなどのために新たなプロンプトを作りたいこともあるでしょう。その際には、Duplicate 機能で任意のVersion だけ あるいはすべてのバージョンを含んでコピーを作ることができます。新たに Prompt を作る際に、どこからか Copy and Paste したり、新規で書く必要はありません。過去に作られている資産を使って作業を効率化することが可能なのです。 まとめ:Langfuseで簡単だけど効果的な プロンプト管理 他にも便利な機能がありますが、それらの紹介は別の機会に譲るとして、今回は一旦ここまでとしたいと思います。見ていただいた通り、Git でのオペレーションもSQL も不要かつ、非常に効果的な管理ができます。 本記事では、プロンプト管理の前提としてLangfuse で管理をする構成やどのように管理をされるのかという点について、いくつかの主要機能について説明をしていきました。 ぜひ参考にしていただき、プロンプトをハードコードする構成ではなく、LLM Ops を実現される一助になれば幸いです。 次回は Langfuse における実際のプロンプトの開発と評価について紹介します。 管理に加えて非常に有意義な機能を簡単に使うことができますので、ぜひご覧ください。

  • [LLMOps] プロンプト管理の課題

    はじめに:生成AIが抱える困難とプロンプト 生成AIアプリケーションの開発は、従来のソフトウェア開発とは異なる難しさがあります。 その一つが、生成AIの出力の不安定さです。そしてこの不安定さに大きく関わっているのが、プロンプトです。生成AIは、人間が与える指示、つまりプロンプトに基づいて動作しますが、プロンプトが適切でなければ、生成AIはその能力を十分に発揮できません。 しかし、プロンプトの重要性は認識されつつも、その管理は後回しにされがちです。多くの開発現場では、プロンプトがコードの中に直接埋め込まれ、場当たり的に修正されているのが現状ではないでしょうか。(少なくとも、筆者は多くそのような現場を見聞きしています) LLMOps のプロンプト管理とは?:なぜ必要で、何が問題なのか プロンプト管理とは、生成AIへの指示(プロンプト)を体系的に作成、テスト、改善、保存、共有するプロセス全体を指します。 プロンプト管理の目的は、主に以下の4つです。 品質向上: 生成AIの出力の品質を向上させ、安定させる。 一貫性確保: 同じプロンプトからは常に同じ品質の出力が得られるようにする。 効率化: プロンプトの作成、テスト、改善のサイクルを効率化する。 再利用性向上: 良いプロンプトをチーム内で共有し、再利用できるようにする。 これらの目的を達成するために、プロンプトを適切に管理する必要があります。 しかし、現状では以下のような問題点があります。 問題点1:コード埋め込みプロンプトの罠 開発時には、作業を優先させることに意識がいってしまい、プロンプトはプログラムコードの中に直接埋め込まれてしまいがちで、結果として以下のような問題を引き起こします。 保守性の低下: プロンプト変更のたびにアプリケーションと一緒に テスト, デプロイのパイプラインなどを回すため、とにかく手間や時間ががかかる。 再利用性の低下: 他のアプリケーションやチームメンバーがプロンプトを再利用しにくい。 可読性の低下: コードとプロンプトが混在し、コード全体の読みやすさが下がる。 バージョン管理の困難: プロンプトの変更履歴が追跡しにくく、問題発生時の原因特定が難しい。 上記のうち特に保守性の低下は致命的であり、開発,修正,テストというサイクルに無駄な工数や待ち時間などが発生してしまいます。 Prompt をコードに埋め込むとコードと同じ工程でリリースをすることになるが、その効果は特になく時間と手間だけが発生する 問題点2:Gitやデータベースでの管理の限界 プロンプトをコードから分離するためには、Gitやデータベースで管理する方法も考えられます。それらの特徴を大まかにまとめるたものが以下の表です。 プロンプト管理:データベース vs. Git 項目 データベース Git 対象ユーザー システム開発者 システム開発者 (あるいはGit作業についてトレーニングを受けた方) 利用スキル要件 DB設計・運用、SQL、加えて何らかの管理用アプリケーションが必要になるケースがある Git操作(コミット、ブランチ、マージ等) データ形式 構造化データ(プロンプト、メタデータ) 非構造化データ(主にテキスト) バージョン管理 履歴記録可能(実装依存)、差分比較は困難 履歴記録・追跡が容易、過去バージョンへ復元可、差分比較も容易 共有・コラボ 共有可能(同時アクセス、排他制御はDBによる)、専門知識必要、リアルタイム共同編集は困難 共有容易(リモートリポジトリ)、プルリクエストでレビュー可、権限管理可、ただし非技術者には難解 テスト・評価 テスト結果保存は要開発、自動評価連携は困難、プロンプトと出力結果の紐付けについても開発が必要 テスト自動化はCI/CD連携でアプリケーションと一体で実施 (プロンプト単体の評価は不可能)、自動での評価機能なし、出力結果との紐付け困難 検索性・再利用性 属性検索可、自然言語特性の完全な表現は困難 Git機能だけでは不十分(別途プロンプトライブラリ等必要) この表からわかるように、Gitとデータベースはそれぞれプロンプト管理に活用できる側面があるものの、多くの課題が残ります。 また プロンプトは本来はシステム開発者ではなく、生成AIのアウトプットの良し悪しが分かる 業務エキスパート (ドメインエキスパート) によって修正、テスト、評価をするべき にもかかわらず、両者ともシステム開発者の手により管理されることになります。 プロンプトがコードと一体化することにより、システム開発者はプロンプトの責任を持たざるを得ないが、正解が分からない悲劇 コードにプロンプトを取り込むことにより、生成AIのシステム開発者の管理スコープはプロンプトに及びます。なぜならば、変更できるのがシステム開発者だけだからです。 しかし生成AIシステムの品質を追求するためには、組織のあるべき姿として、コード管理はシステム開発者が実施し、プロンプトは生成AIに出して欲しい期待値を知っている業務エキスパートの手に委ねるべきなのです。 まとめ:プロンプト管理の課題を克服するために 生成AIアプリケーションの品質は、プロンプトの品質に大きく左右されます。しかし、プロンプトはコードに埋め込まれたり、Gitやデータベースで十分に管理されていなかったりすることが多く、様々な問題を引き起こしています。 これらの問題を解決し、生成AIのポテンシャルを最大限に引き出すためには、プロンプト管理の仕組みをアプリケーション開発者以外でも対応できるよう整備し、プロンプト管理とテスト - 改善のプロセスを一元的に確立させることが不可欠です。 [ 次回の記事 ] では、これらの課題を解決するための実例を「Langfuse」と、Langfuseを活用したプロンプト管理を通して詳しく解説します。

  • Langfuseのサーバーサイドマスキングを試してみた

    過去、Langfuseでのマスキングについて触れてきましたが、これまではクライアントサイドで対応するしかありませんでした。しかし、ついに先日のリリース( v3.152.0 )で、サーバーサイドでのマスキングが設定可能になりました。 ※ 注意 :現時点ではEE(Enterprise Edition)ライセンス専用機能となっています。 今回は公式ドキュメントを参考に、実際に設定してみました。 基本設定 事前準備 マスク処理を行う、Langfuseからのコールバック先が必要になります。 今回は設定の挙動を確認したいため、公式の 実装例 をそのまま利用し、Cloud Runにデプロイしました。 設定 設定自体は、Langfuse Workerのコンテナに環境変数 LANGFUSE_INGESTION_MASKING_CALLBACK_URL  を設定すれば利用できるようになります。 ※ 注意 :Langfuse Worker コンテナにEEライセンスが適用されていない場合、コールバックURLを設定しても、そのURLへリクエストが送られることはありません。 実際にEEライセンスが適用されていない状態でトレースを送信すると、手元の環境では以下のエラーログが出力されました。 warn: Ingestion masking callback URL is configured but enterprise license is not available. Masking will be disabled. 実験 サーバーサイドマスキングは、OpenTelemetryエンドポイント(/api/public/otel)経由のイベントに適用されます。Python SDK v3.x系はこれに対応しているため、SDKを利用した簡易的なトレース送信コードを作成し、テストしました。 テストコード # ※環境に合わせてクライアントを初期化してください langfuse = get_client() with langfuse.start_as_current_observation( as_type="span", name="test-trace", input="山田太郎様からの問い合わせ", ) as root_span: trace_id = langfuse.get_current_trace_id() with langfuse.start_as_current_observation( as_type="span", name="test-span", input="山田太郎です。登録しているメールアドレスは何ですか?", ) as span: span.update( output="山田太郎様のメールアドレスはtest-user@example.comです。", metadata={ "user_email": "test-user@example.com", "support_phone": "123-456-7890" }, ) root_span.update_trace( output="問い合わせが完了しました。", ) langfuse.flush() 検証1:公式の実装例 まずは、サンプル実装に対してリクエストを送信した場合にどうなるか確認してみます。 1階層目のトレース(test-trace)には、今回置換対象となる項目が含まれていないため、2階層目、3階層目のトレース情報を確認してみます。 結果 2階層目 3階層目 置換対象となる、メールアドレス・電話番号が含まれている箇所について、Output、Metadata ともにマスキング(置換)されていることが確認出来ました。 検証2:組織・プロジェクト単位でのマスク mask_trace 関数内で x_langfuse_org_id や x_langfuse_project_id を利用することで、組織単位・プロジェクト単位で異なるマスク処理を適用することができます。 ここで、コールバック先に設定しているアプリケーションの mask_trace を以下の通り変更します。特定の組織のみ、マスク処理の異なるmask_pii2が適用されるようにしました。 def mask_pii2(data): print(f"mask_pii2: {data}") if isinstance(data, str): data = f"[REDACTED] {data}" return data elif isinstance(data, dict): return {k: mask_pii2(v) for k, v in data.items()} elif isinstance(data, list): return [mask_pii2(item) for item in data] return data async def mask_trace( request: Request, x_langfuse_org_id: Optional[str] = Header(None), x_langfuse_project_id: Optional[str] = Header(None) ): body = await request.json() if x_langfuse_org_id == "xxxxxxxxxxxxxxxxxxxxxxx": masked_body = mask_pii2(body) else: masked_body = mask_pii(body) return masked_body コード上の引数 x_langfuse_org_id、x_langfuse_project_id は、Langfuseから送られてくるHTTPヘッダー(x-langfuse-org-id、x-langfuse-project-id)から取得されています。そのため、ヘッダーが付与されていない際には値が取得できない可能性があります。なお、今回利用したPython SDKによる送信方法では、問題なく付与されていました。 ※ 上記コードのように条件分岐を行う場合、指定するのは name ではなく、 id であることに注意して下さい 結果 指定した組織には mask_pii2 が、それ以外の組織には mask_pii が適応されることを確認出来ました。 しかし、mask_pii2は以下の通り、想定される形にはなりませんでした。 サーバサイドで行う場合は、リクエストボディ全体が処理対象となります。そのため、今回のように再帰的に処理をかけると、トレースの構造を定義する resource.attributes などのキーまで書き換わってしまい、Langfuse側で正しくInput/Outputとして認識されなくなってしまいました。 クライアントサイドで利用していた処理を移植したい場合は、メタデータを破壊しないよう、対象となるデータ部分だけを加工するなどの工夫が必要となりそうです。 参考:同じmask_pii2をクライアントサイドの maskオプションに指定した場合 補足:Dataset Run への影響について UIから Dataset Runを実行した場合にはマスク処理が 適応されないこと を確認しました。 マスキングされることによるEvaluator への影響や、結果の目視がしにくいと言った問題は起こらなさそうです。 まとめ 実際に試してみた結果、細かい制御の手軽さという点では、クライアントサイドでマスク処理を行う方に分がある印象です。 しかし、「いざという時のセーフティーネット」として活用できたり、既にOpenTelemetryエンドポイント経由で動作しているアプリケーションに対して、「アプリ側のコード改修無しでマスク処理を適用できる」など、サーバサイドならではのメリットも多く存在します。 また、今回は検証していませんが、環境変数 LANGFUSE_INGESTION_MASKING_PROPAGATED_HEADERS を設定することで、オリジナルのリクエストから任意のカスタムヘッダーをコールバック先へ伝播させることも可能なようです。これを活用すれば、要件に合わせてさらに柔軟なマスキング制御を実現していくこともできそうです。

  • ガバナンスを高めるKong AI Gateway + Langfuseで“アプリ計装なし”のLLMオブザーバビリティ

    LMアプリケーションの可観測性(オブザーバビリティ)を確保しようとする際、Langfuse SDK や OpenTelemetry SDK をアプリケーション側に組み込んで計装するのが一般的なアプローチですが、これは多少なりとも手間がかかることと、社内のエージェントを勝手に動かす人などが意図的に観測されないように対応しないこともありえるでしょう。 しかしながら、LLM への呼び出しを LLM Gateway に集約することで、アプリケーション側での計装が不要になり、ガバナンスを高めることにも寄与します。 そこでこの記事では、Pythonアプリ(Google ADKエージェント / google-genai SDK)からのLLM呼び出しを API gateway である Kong経由に切り替え、Kongの組み込みOpenTelemetr プラグインから Langfuse の OTLP/HTTP エンドポイントへ直接トレースを送信するまでの手順をまとめます。 本記事の作成にあたりまして、Langfuse Night #3 の川村さんの登壇内容を参考にさせていただきました。ありがとうございます! https://www.docswell.com/s/shukawam/Z2QJD9-langfuse-and-kong-gateway 検証環境 コンポーネント バージョン 備考 Kong Gateway 3.10 OSS版を利用 Kong plugins bundled ai-proxy, opentelemetry を使用 google-adk 1.x google-genai SDK 1.x Langfuse Cloud OTLP/HTTP endpoint を使用 この記事での検証はネットワーク的にはローカルでの docker compose を前提としています。Kongとアプリは同一Dockerネットワーク内で通信し、Kong自体は外部公開しません。 全体構成 アーキテクチャの全体像は以下の通りです。 User (curl) ↓ MCP Server / ADK Agent (Python) ↓ genai SDK (base_url → Kong) Kong AI Gateway(Docker network 内部) ├── ai-proxy plugin → LLM リクエストを Vertex AI に送信 └── opentelemetry plugin → Langfuse OTLP/HTTP endpoint どのプラグインで Langfuse に送るか Langfuse の Kong 連携ガイド では、kong-plugin-ai-tracing を導入してトレースする手順が「Recommended」として案内されています。 一方で今回は、追加プラグインの導入・管理を増やしたくなかったので、Kong 組み込みの opentelemetry プラグインを使いました。Kong 側は OTLP/HTTP(Protobuf)で送信でき、バックエンド直送も Collector 経由も選べます。 実装手順 細かい実装の手順やConfigはざっくりこちらにまとめました。ご参考までにどうぞ。 Kong 設定 kong.yaml _format_version: "3.0" services: - name: gemini-service # Service 定義上 url は必須だがダミー値。 # ai-proxy が upstream を置き換え、Vertex AI へ直接接続するため転送されない。 url: http://httpbin.konghq.com routes: - name: gemini-route paths: - /gemini plugins: - name: ai-proxy config: route_type: llm/v1/chat llm_format: gemini logging: log_statistics: true auth: gcp_use_service_account: true gcp_service_account_json: '${GCP_SERVICE_ACCOUNT_JSON}' # 本番環境では、サービスアカウント鍵(JSON)の配布・保管・ローテーションが # セキュリティ上の課題・運用負荷になりやすいため、サービスアカウントキーを # 使わない認証を推奨します。今回はあくまで検証用ということでご理解ください。 model: provider: gemini name: gemini-2.5-flash options: gemini: api_endpoint: us-central1-aiplatform.googleapis.com project_id: ${GCP_PROJECT_ID} location_id: ${GCP_LOCATION_ID} - name: opentelemetry config: traces_endpoint: ${LANGFUSE_HOST}/api/public/otel/v1/traces headers: Authorization: "Basic ${LANGFUSE_AUTH_BASE64}" コード側でKongに向ける コード記載例 KONG_BASE_URL = os.getenv("KONG_BASE_URL", "http://kong:8000/gemini")model = Gemini(model="gemini-2.5-flash", base_url=KONG_BASE_URL) client = genai.Client(http_options={"base_url": KONG_BASE_URL}) docker compose(最小構成) docker-compose.yaml services: kong: build: context: . dockerfile: Dockerfile.kong environment: KONG_DATABASE: "off" KONG_DECLARATIVE_CONFIG: /opt/kong/kong.yml KONG_PLUGINS: bundled KONG_PROXY_LISTEN: "0.0.0.0:8000" # 詰まりどころ(後述) KONG_NGINX_PROXY_CLIENT_BODY_BUFFER_SIZE: "8m" LANGFUSE_PUBLIC_KEY: ${LANGFUSE_PUBLIC_KEY} LANGFUSE_SECRET_KEY: ${LANGFUSE_SECRET_KEY} LANGFUSE_HOST: ${LANGFUSE_HOST:-https://cloud.langfuse.com} GCP_PROJECT_ID: ${GCP_PROJECT_ID} GCP_LOCATION_ID: ${GCP_LOCATION_ID} volumes: - ./credentials/service-account-key.json:/app/credentials/service-account-key.json:ro app: environment: - KONG_BASE_URL= http://kong:8000/gemini - GOOGLE_API_KEY=dummy depends_on: - kong 動作確認 1. Kong の起動確認 docker compose exec kong kong health 2. Kong 経由で Gemini が応答するか docker compose exec kong curl -s -X POST http://localhost:8000/gemini/v1/chat/completions \  -H "Content-Type: application/json" \  -d '{"messages":[{"role":"user","content":"hello"}],"model":"gemini-2.5-flash"}' \  | jq .choices[0].message.content 3. Langfuse にトレースが届くか Langfuse の Traces を開いて確認します。 実行結果 (Traceの比較) 実装できたら、Langfuseの画面で結果を確認してみます。 すると、エージェント側で計装したもの(パターンA) と Kong経由で自動でLangfuseにTraceを飛ばすもの (パターンB) では微妙に確認できるものが異なることに気がつくと思います。 両者とも基本的な要素は全てカバーされており、構造・トークン・モデル名・レイテンシーなどを確認することができますが、パターンB においては、Traceレベルの Input/Outputに情報が入らず、Promptとの紐付けなどもできません。一方でパターンAはエージェント側で計装しますので、もちろんどのようにでも表示可能です。 パターンA. Kong を通さないもの (エージェント側で計装) トップレベルのTraceにもちゃんと Input/Output が入ってる パターンB. Kong経由 (エージェント側で実装なし) 大体の情報は入ってるが、トップレベルのTraceには Input/Output は入らない おまけ: 実装時の注意メモ 1. リクエストボディが大きいと ai-proxy が 400 を返す エージェントがツールを使うと、2回目以降の LLM 呼び出しで会話履歴+ツール結果が乗り、ボディが急に大きくなります。Nginx 側のデフォルトが小さいと、ボディがテンポラリに逃げたりして ai-proxy の処理が崩れ、次のエラーに当たりました。 400 "request body doesn't contain valid prompts" ツール不使用だと通るが、ツール使用だと落ちる(この分岐が地味に厄介) 対策:先ほどの compose ファイルにもあった通り、Nginx のバッファサイズを上げます。 KONG_NGINX_PROXY_CLIENT_BODY_BUFFER_SIZE: "8m" 2. preserve 前提で組むと 3.11 以降で詰まる preserve は 3.11.0.0 で deprecated です。将来削除される前提なので、早めに “新しい route_type”(今回なら llm/v1/chat)に寄せておくのが安全かと思われます。 まとめ: 今回は、Kong AI GatewayとLangfuseを用いて、アプリケーション側に計装SDKを組み込まずにLLMのオブザーバビリティを確保する手順をご紹介しました。 ゲートウェイ層にLLMの呼び出しとトレース送信の責務を集約することで、アプリケーション側のコードを汚さずに素早く観測を始められるのは、この構成の大きな強みです。

  • LLMアプリの評価データをバージョン管理する - Langfuseのデータセットバージョニングで実験の再現性を確保する

    LLMアプリケーションの開発で、こんな経験はないでしょうか。 「先週と同じ条件で実験したいのに、データセットを更新したから再現できない…」 「評価データを改善したいけど、過去の結果と比較できなくなるのが怖い…」 「チームメンバーがデータセットを変更して、自分の実験の前提が変わってしまった…」 「実験後にデータを1件修正したら、あの実験で何を入力していたか確認できなくなった…」 評価データセットを更新すると、過去の実験結果との比較が難しくなります。同じプロンプトで精度が変わったとき、それがモデル改善によるものか、データセットの変更によるものかを切り分けられなくなるのです。特にチーム開発では、誰かがデータセットを更新することで、実行中の実験の前提が変わってしまう可能性もあります。 Langfuseのデータセットバージョニング機能は、この課題を解決します。この機能により、 評価データの変更とモデルの変更を分離して管理 し、実験の再現性を確保できるようになりました。 本記事でわかること データセットバージョニングが必要になる具体的な場面 Langfuseのタイムスタンプベースバージョニングの仕組み 実験の再現性確保・チーム開発での活用方法 なぜデータセットバージョニングが必要か 「先週のデータセットで再度実験したいのに、もう戻せない」 「精度が下がったけど、データが変わったせいなのか、モデルのせいなのか分からない」 「他のメンバーがデータセットを更新して、自分の実験が意図しない影響を受けた」 LLMアプリの評価では、評価データセットは「固定されたもの」ではなく、継続的に改善されるべきものです。エッジケースを発見したら追加し、不適切なテストケースを修正し、より実践的なシナリオを反映するよう更新していく。しかし、従来のデータセット管理では、更新のたびに過去のバージョンが上書きされてしまいます。これにより、以下のような問題が発生します: 再現性の欠如 : 実験結果のRun Itemを開いても「今の最新データ」が表示されるため、実験を実行した当時の入力が何だったか確認できない。実験後にデータを1件でも修正すると、「当時の入力で動いていたのか、修正後の入力で動いていたのか」が永久に不明になる 比較の困難 : データセット更新前後で精度が変わったとき、データの変化によるものか、モデルの変化によるものか判断できない チーム開発での衝突 : 複数人で開発していると、データセット更新が他のメンバーの実験に意図せず影響を与える LLMOpsの観点では、 データのバージョン管理はモデルのバージョン管理と同じくらい重要 です。MLOpsでは学習データのバージョン管理は一般的ですが、LLMアプリの評価データも同様に管理すべきなのです。 Langfuseのデータセットバージョニング機能 Langfuse v3.151.0 で、実験向けのデータセットバージョニング機能が強化されました。データセットアイテムの各更新が履歴として保持され、ISO 8601形式のタイムスタンプ(例: `2026-01-21T14:35:42Z`)で特定の時点のデータセットを参照できます。 この仕組みにより、実験実行時に「どのバージョンのデータセットを使うか」を明示的に指定でき、実験結果には使用したバージョンが自動的に記録されます。後から実験結果を見たときに、「この実験は当時のどのデータで実行されたか」が正確に追跡できるのです。 実現できること データセットバージョニングにより、以下のような運用が可能になります。 1. 実験の完全な再現性 「先週のプロンプト改善で精度が5%向上したはずなのに、今日試したら再現できない…」 こんな経験はないでしょうか。バージョニング機能があれば、実験実行時のデータセットバージョンが自動記録されるため、数週間後でも当時と全く同じ条件で実験を再現できます。実験結果の画面には使用したバージョンが記録されており、クリックするだけで当時のデータ一覧に遷移できます。「あの実験はどのデータで動かしたっけ?」で悩む必要はありません。 2. 安全なデータセット改善 データセットを改善・修正しても、過去のバージョンは保持されます。「新しいケースを追加したら精度が下がった。元のデータセットで再確認したい」といった場合に、更新前のバージョンで再実験できるため、安心してデータセットを継続的に改善できます。 3. データ変更とモデル変更の分離 同じプロンプトで2回実験を実行して結果が異なった場合、バージョンを確認することで原因を切り分けられます: - 同じバージョン → モデル側の問題(APIの非決定性など) - 異なるバージョン → データが変わったため この切り分けができることで、デバッグや改善の方向性を正しく判断できます。CI/CDパイプラインに組み込む際も、バージョン指定によって特定の評価データセットに対する回帰テストが可能になります。 4. チーム開発での安心感 実験を作成するとき、バージョンを指定して「先週時点のデータだけで実験する」という条件を固定できます。チームメンバーがその後ケースを追加・修正しても、自分の実験には影響しません。各メンバーが独立して作業しながら、必要に応じて最新版を取り込むという柔軟な運用ができます。 使い方 データセットバージョニングは、LangfuseのUIとSDK/APIの両方から利用できます。 UIでの操作 バージョン履歴の確認 データセット詳細画面から、過去のバージョンを一覧で確認できます。各バージョンには「Copy version timestamp (UTC)」ボタンがあり、SDKやAPIで使用するタイムスタンプをそのままコピーできます。 バージョン一覧から過去時点のデータセット状態を確認できる 実験実行時のバージョン指定 実験を作成する際、バージョン選択ドロップダウンからデータセットのバージョンを指定できます。デフォルトは最新版ですが、過去のバージョンを選択することも可能です。 実験作成時に使用するデータセットのバージョンを指定できる 選択したバージョンで実験が実行され、使用したバージョンは実験結果のメタデータに自動記録されます。 実験結果でのバージョン確認 実験Run詳細画面には、使用したデータセットのバージョンが表示されます。このバージョン日時はリンクになっており、クリックすると当時のデータセットアイテム一覧に遷移できます。 Run詳細からワンクリックで当時のデータ一覧に遷移できる SDKでの使用 SDKを使って実験を自動化している場合も、バージョニングを活用できます。` `get_dataset()`の `version` パラメータに日時オブジェクトを渡すことで、特定時点のデータセットを取得できます。 from datetime import datetime, timezone from langfuse import get_client langfuse = get_client() # 特定バージョン(2026年1月21日時点)のデータセットを取得 version = datetime(2026, 1, 21, 14, 35, 42, tzinfo=timezone.utc) dataset = langfuse.get_dataset(name="my-dataset", version=version) # 取得したバージョンのアイテムで実験を実行 def my_task(*, item, **kwargs):    return my_llm_function(item.input) result = dataset.run_experiment(    name="experiment-v1",    task=my_task, ) まとめ LLMアプリの開発において、評価データのバージョン管理は実験の再現性と正確な分析のために不可欠です。Langfuseのデータセットバージョニング機能を使えば、データの変化とモデルの変化を分離して管理でき、チームでの並行開発も安心して進められます。 データセットを更新するたびに「過去の結果と比較できなくなる」不安から解放され、自信を持ってLLMアプリの品質を改善できるようになります。ぜひ実際の開発フローに取り入れてみてください。 参考リンク - Langfuse Changelog - Dataset Versioning for Experiments

  • Langfuse の Observation レベル評価:「どのステップが悪いのか」をスコアで特定できるようになった

    こんにちは。ガオ株式会社の黒澤です。 Langfuse v3.153.0 で [PR #11861]( https://github.com/langfuse/langfuse/pull/11861 ) がマージされ、LLM-as-a-Judge を Observation 単位で実行できるようになりました。本記事ではその背景と使い方をまとめます。 課題:Trace 全体への評価では「どこが悪いか」がわからない LLM アプリの評価で、こんな状況に陥ったことはありませんか。 RAG アプリの LLM as a Judge スコアが下がった。でも、ドキュメント検索が悪いのか、回答生成が悪いのか、判断できない。 これは、従来の Langfuse の評価機能が Trace(エンドツーエンドのリクエスト全体) を評価単位としていたためです。 Before:Evaluator は Trace 全体にしか設定できなかった Langfuse で LLM as a Judge Evaluator を作成すると、評価対象は Trace 全体の最終出力のみでした。 ▲ Run on に Live Tracesを選択すると、Trace全体の評価となる たとえば以下のような RAG パイプラインでは、`retrieve`(検索)と `llm`(生成)が別々の Observation として存在しますが、評価できるのは最終出力だけでした。 Trace: ユーザーの質問 ├─ Span: retrieve ← ここは評価できなかった └─ Generation: llm ← ここの最終出力だけが評価対象 スコアが低くても「検索が悪いのか、生成が悪いのか」の切り分けは、ログを手で読むしかありませんでした。 After:個々の Observation を評価ターゲットに指定できるようになった 今回のアップデートで、Evaluator の設定画面に Observation をターゲットにするオプション が追加されました。 ▲ 例:retrieve 用。Target で Live Observations を選択し、Where で Type=SPAN, Name=retrieve を指定 ▲ 例:llm 用。Type=GENERATION, Name=llm でフィルタ。retrieve と llm で別々の Evaluator を設定できる ※ 以上は RAG パイプラインの一例。実際の Observation 名や Type はアプリの実装に合わせて設定する。 検索と生成を Langfuse へ送信する簡単なサンプルソースです。ここでは例示として、実際の検索先や LLM は呼ばず、固定文言を Langfuse へ送信します。 ソースコード """ Trace 構造: Trace: rag-pipeline ├─ Span: retrieve ← Context Relevance の評価対象 └─ Generation: llm ← Answer Relevance の評価対象 """ import os try: from dotenv import load_dotenv load_dotenv() except ImportError: pass from langfuse import get_client, propagate_attributes def retrieve_documents(query: str) -> list[str]: """ドキュメント検索をシミュレート(モック)""" mock_docs = [ "Python は 1991年に Guido van Rossum によって開発されました。", "Langfuse は LLM アプリケーションの観測・評価プラットフォームです。", ] return mock_docs[:2] def generate_answer(query: str, context: list[str]) -> str: """LLM による回答生成をシミュレート(モック)""" context_text = "\n".join(context) return f"検索された文脈:{context_text[:50]}... に基づき、{query} への回答を生成します。" def run_rag_pipeline(query: str) -> str: langfuse = get_client() with langfuse.start_as_current_observation( as_type="span", name="rag-pipeline", input={"query": query} ) as root_span: with propagate_attributes(tags=["observation-eval-sample"]): with langfuse.start_as_current_observation( as_type="span", name="retrieve", input={"query": query} ) as retrieve_span: documents = retrieve_documents(query) retrieve_span.update(output={"documents": documents, "count": len(documents)}) with langfuse.start_as_current_observation( as_type="generation", name="llm", model="gpt-4o-mini", input={"query": query, "context": documents}, ) as llm_gen: answer = generate_answer(query, documents) llm_gen.update(output={"answer": answer}, usage_details={"input_tokens": 50, "output_tokens": 30}) root_span.update(output={"answer": answer}) langfuse.flush() return answer if __name__ == "__main__": answer = run_rag_pipeline("Python はいつ開発されましたか?") print(answer) これにより、先ほどの RAG パイプラインに対して次のような評価設計が可能になります。 評価対象 Observation 評価軸 検出したいもの retrieve Span Context Relevance 検索結果がクエリに関連しているか llm Generation Answer Relevance 回答がユーザーの質問に答えているか Trace 詳細画面でも変化がわかる 設定後、Trace の詳細画面を開くと、 各 Observation の行にもスコアが表示される ようになります。 ▲ 各 Observation 行にスコアが付いている。一目で「どのステップが悪いか」が把握できる 一目で「どのステップのスコアが低いか」が把握できます。たとえば: Context Relevance: 0.50(検索結果の精度が低い) Answer Relevance: 1.00(生成自体は質問に答えている) この場合、問題は検索ロジック側にあると判断できます。 まとめ:評価の粒度がアーキテクチャの複雑さに追いついた Before(Trace レベル) After(Observation レベル) 評価対象 最終出力のみ 各中間ステップも評価可能 問題の原因特定 最終出力のスコアは取れるが、複数ステップがある場合は「どのステップが悪いか」の切り分けが困難 各ステップのスコアで特定可能 向いている構成 シンプルな LLM 呼び出し RAG・エージェント・多段階チェーン アプリが複雑になるほど、「何かがおかしい」の検知だけでは不十分です。Observation レベルの評価は、その「どこがおかしいか」をデータとして取り出す手段です。

  • Google ADKで作ったエージェントに Langfuseのトレースにプロンプトを紐付ける方法

    Google ADK(Agent Development Kit)のトレースに Langfuse のプロンプト情報を紐付ける方法を解説します。これにより、プロンプトごとのコスト・レイテンシ分析や A/B テストが可能になります。 なぜ紐付けが必要なのか 紐付けができないと何が困るか ・プロンプトごとのコスト・レイテンシを分析できない ・A/B テストでプロンプトバージョンを比較できない ・どのプロンプトが本番で使われているか追跡できない GoogleADKInstrumentor だけでは不十分 GoogleADKInstrumentor を使えば、Google ADK のトレースを Langfuse に送信できます。 from openinference.instrumentation.google_adk import GoogleADKInstrumentorGoogleADKInstrumentor().instrument() しかし、これだけではプロンプト紐付けがされません。 Langfuse ダッシュボード └── call_llm (GENERATION) └── promptName: null ← 紐づいていない 他のフレームワークとの違い Langchain や OpenAI SDK では、Langfuse が公式にラッパーやコールバックを提供しており、簡単にプロンプト紐付けができます。 フレームワーク プロンプト紐付け方法 LangChain Langfuse公式Callbackがある OpenAI SDK Langfuse公式ラッパーがある(`langfuse_prompt`引数) Google ADK OTel/OpenInference経由 → prompt属性の概念がない しかし、Google ADK は OpenTelemetry + OpenInference 経由でトレースを送信するため、Langfuse の標準的な方法では紐付けができません。この問題は GitHub Issue #7937 で議論されており、本記事ではその回避策を解説します。 仕組み ■ 解決のポイント Langfuse がプロンプトを認識するには、LLM 呼び出しのスパンに以下の属性を設定する必要があります。 ・langfuse.prompt.name - プロンプト名 ・langfuse.prompt.version - プロンプトバージョン 本記事では SpanProcessor と ContextVar を組み合わせて、call_llm スパンにこれらの属性を自動付与します。 ■ 全体の流れ 1. instruction 関数内でプロンプトを取得 └─ ContextVar にプロンプト情報を保存 2. call_llm スパンが開始される └─ SpanProcessor.on_start() が呼ばれる └─ ContextVar からプロンプト情報を取得 └─ スパンに属性を設定 3. Langfuse がプロンプトリンクを認識 └─ ダッシュボードで分析可能に ■ なぜ SpanProcessor を使うのか Google ADK は LLM 呼び出しを別スレッド(並行処理)で実行します。通常の OTel Context 伝播では、プロンプト情報を LLM 呼び出しに渡せません。 SpanProcessor を使うと、スパン作成時に直接属性を設定できるため、この問題を回避できます。ContextVar はスレッドをまたいで値を保持できるので、組み合わせて使います。 手順 ■ 必要なパッケージ pip install langfuse google-adk openinference-instrumentation-google-adk opentelemetry-sdk python-dotenv ■ 完全なコード例 以下のコードをコピペで動作します。 import asyncio from contextvars import ContextVar from dotenv import load_dotenv from google.adk.agents import Agent from google.adk.runners import Runner from google.adk.sessions import InMemorySessionService from google.genai import types from langfuse import get_client from openinference.instrumentation.google_adk import GoogleADKInstrumentor from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider, SpanProcessor # ============================================================ # Langfuse プロンプト紐付け用コード # ============================================================ prompt_info_var = ContextVar("prompt_info", default=None) class LangfusePromptProcessor(SpanProcessor): """call_llm スパンにプロンプト情報を付与する""" def on_start(self, span, parent_context=None): if hasattr(span, "name") and span.name == "call_llm": prompt_info = prompt_info_var.get() if prompt_info: span.set_attribute("langfuse.prompt.name", prompt_info["name"]) span.set_attribute("langfuse.prompt.version", prompt_info["version"]) def on_end(self, span): pass def shutdown(self): pass def force_flush(self, timeout_millis=30000): return True # ============================================================ # アプリケーションコード # ============================================================ async def main(): load_dotenv() langfuse = get_client() # 1. TracerProvider を作成し、SpanProcessor を登録 provider = TracerProvider() provider.add_span_processor(LangfusePromptProcessor()) trace.set_tracer_provider(provider) # 2. Google ADK のトレースを有効化 GoogleADKInstrumentor().instrument() # 3. instruction 関数を定義(ここでプロンプトを紐付け) def get_instruction(ctx): prompt = langfuse.get_prompt("my_agent_instruction") prompt_info_var.set({"name": prompt.name, "version": prompt.version}) return prompt.compile() # 4. Agent を作成 agent = Agent( name="my_agent", model="gemini-2.5-flash", instruction=get_instruction, # 関数を渡す tools=[], ) # 5. セッションを作成して実行 session_service = InMemorySessionService() await session_service.create_session( app_name="my_app", user_id="user-1", session_id="session-1" ) runner = Runner(agent=agent, app_name="my_app", session_service=session_service) user_msg = types.Content(role="user", parts=[types.Part(text="Hello")]) for event in runner.run(user_id="user-1", session_id="session-1", new_message=user_msg): if event.is_final_response(): print(event.content.parts[0].text) # 6. トレースデータを送信 langfuse.flush() if __name__ == "__main__": asyncio.run(main()) ``` ■ 環境変数(.env) LANGFUSE_PUBLIC_KEY=pk-lf-xxx LANGFUSE_SECRET_KEY=sk-lf-xxx LANGFUSE_BASE_URL=https://xxx GOOGLE_API_KEY=xxx ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 注意点・よくある落とし穴 1. TracerProvider の登録順序 SpanProcessor は GoogleADKInstrumentor().instrument() の「前に」登録する必要があります。 【正しい順序】 provider = TracerProvider() provider.add_span_processor(LangfusePromptProcessor()) # 先に登録 trace.set_tracer_provider(provider) GoogleADKInstrumentor().instrument() # 後から計装 【動かない順序】 GoogleADKInstrumentor().instrument() provider.add_span_processor(...) # 既に計装済みで反映されない 2. instruction には関数を渡す プロンプトを動的に取得するには、instruction に「関数」を渡す必要があります。 【正しい書き方】 def get_instruction(ctx): prompt = langfuse.get_prompt("my_prompt") prompt_info_var.set({"name": prompt.name, "version": prompt.version}) return prompt.compile() agent = Agent(instruction=get_instruction, ...) 【動かない書き方】 agent = Agent(instruction="You are a helpful assistant", ...) 3. プロンプトリンクは Generation 単位 Langfuse の仕様により、プロンプトリンクは Generation(LLM 呼び出し)スパンにのみ関連付けられます。トレース全体やエージェント実行単位への紐付けはできません。 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 結果 設定が成功すると、Langfuse ダッシュボードで以下が確認できます。 ・Generation スパンにプロンプトリンクが表示される ・Prompt Metrics でコスト・レイテンシ分析が可能 ・プロンプトバージョンごとの比較ができる Langfuse ダッシュボード(設定後) └── call_llm (GENERATION) └── promptName: "my_agent_instruction" ← 紐づいた! └── promptVersion: 1 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 参考リンク Langfuse OpenTelemetry Docs Langfuse Prompt Management Google ADK OpenInference Google ADK この問題の議論Issues 公式のプロンプト紐付けドキュメント

  • Langfuse Trace詳細画面における特殊レンダリングパターンガイド

    本記事では、LangfuseのTrace詳細画面で利用できる主要な特殊レンダリングパターンを解説します。これらのパターンを活用することで、トレース情報をより視覚的かつ構造的に表示できます。 対象読者 LangfuseでLLMアプリケーションのトレースを取得している開発者 Langfuseのトレースのアナリスト・手動評価者 はじめに 複雑になるトレース構造 近年のLLMは多機能化・高性能化が進み、1つのトレースに様々な情報を詰め込むことが一般的になってきました。画像認識、音声処理、ツール呼び出し、推論過程など、単純なテキスト入出力だけでは済まないケースが増えています。 しかし、情報量が増えれば増えるほど、本来可視化を目的としているはずのトレース画面が逆に見にくくなってしまう、という問題が発生します。大量のJSON文字列が羅列されるだけでは、重要な情報を見逃したり、デバッグに時間がかかったりします。 そこで役立つのが、Langfuseが提供する特殊レンダリング機能です。適切なJSON構造を使うことで、同じデータをより整理された形で表示でき、開発効率が大きく向上します。 特殊レンダリングが用意されている理由 Langfuseのトレースは、OpenAI、Anthropic、Geminiといった主要なLLMプロバイダーのレスポンススキーマに対応しています。各プロバイダーが採用している独自のJSON構造を自動的に認識し、適切な形式で画面表示するため、開発者はプロバイダー固有のフォーマットをそのまま記録できます。 以降では、実際にどのようなJSON構造が特殊なレンダリング対象となるのか、主要4パターンを見ていきます。 パターン1: ChatML形式の会話履歴 ChatML(Chat Markup Language)は、LLMとの会話履歴を構造化して記録する標準的な形式です。Langfuseは複数のChatML記述方法に対応しています。 ChatML形式のトレース表示 基本構造 配列の各要素がroleとcontentキーを持つオブジェクトであれば、自動的にChatML形式として認識されます。 [ {"role": "user", "content": "こんにちは"}, {"role": "assistant", "content": "こんにちは!何かお手伝いできることはありますか?"} ] 対応するroleと表示 画面上では、roleに応じて異なるラベルと背景色が適用されます。 user: "User"と表示 assistant: "Assistant"と表示(アクセント背景色で区別) system: "System"と表示(アクセント背景色で区別) tool: "Tool"と表示 function: "Function"と表示 developer: "Developer"と表示 カスタム表示名 nameフィールドを追加すると、roleではなくそちらが優先表示されます。 { "role": "assistant", "name": "AI Agent Alpha", "content": "処理を開始します" } この場合、画面には"AI Agent Alpha"と表示されます。 ラッパー形式 以下のような構造にも対応しています。 { "messages": [ {"role": "user", "content": "質問です"} ] } ネストされた配列形式も認識されます。 [[{"role": "user", "content": "質問です"}]] パターン2: ツール定義と実行結果 Function CallingやTool Useと呼ばれる機能を使う際、Langfuseはツールのスキーマ定義と実行ログを視覚的に表示します。 ツール定義と実行結果のトレース表示 ツールスキーマの定義 メッセージにtoolsフィールドを追加すると、画面上部に「Tools」セクションが表示されます。 { "role": "user", "content": "東京の天気を調べて", "tools": [ { "name": "get_weather", "description": "指定した都市の天気情報を取得します", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "都市名"} }, "required": ["city"] } } ] } ツールの呼び出し記録 アシスタントがツールを呼び出した際の記録は、tool_callsフィールドで表現します。 { "role": "assistant", "content": null, "tool_calls": [ { "id": "call_abc123", "name": "get_weather", "arguments": "{\"city\": \"東京\"}" } ] } 画面上では、メッセージ内にTool Invocationカードが表示されます。 ツールからの返答 ツール実行結果は、role: "tool"のメッセージとして記録します。 { "role": "tool", "tool_call_id": "call_abc123", "content": "{\"temperature\": 22, \"condition\": \"晴れ\"}" } OpenAI形式のネスト構造 OpenAIのChat Completions APIで使われるネスト構造にも対応しています。 { "tool_calls": [ { "id": "call_abc123", "type": "function", "function": { "name": "get_weather", "arguments": "{\"city\": \"東京\"}" } } ] } パターン3: マルチモーダルコンテンツ マルチモーダルファイルのアップロード方法や全般的な使い方については、 こちら の記事で詳しく解説されていますので、そちらをご参照ください。 ここでは、特殊レンダリングに必要なJSON構造について解説します。 基本構造 マルチモーダルコンテンツとして認識されるには、contentが配列で、各要素にtypeキーが必要です。 { "role": "user", "content": [ {"type": "text", "text": "この画像について教えて"}, {"type": "image_url", "image_url": {"url": "https://example.com/image.png"}} ] } 画像の表示(type: "image_url") 画像を表示するには、以下のような構造を使います。 { "role": "user", "content": [ {"type": "text", "text": "この画像を説明して"}, { "type": "image_url", "image_url": { "url": "https://example.com/image.png", } } ] } 対応しているURL形式 形式 例 HTTP/HTTPS URL https://example.com/image.png Base64 Data URI data:image/png;base64,iVBORw0KGgo... Langfuseメディアトークン @@@langfuseMedia:type=image/jpeg|id=|source=base64@@@ 対応している画像形式 : PNG, JPEG, JPG, GIF, WebP 画面上では、画像が実際に表示され、クリックでリサイズ可能です。 画像のトレース表示 音声入力(type: "input_audio") 音声データを入力として記録する場合は、以下の構造を使います。 { "role": "user", "content": [ { "type": "input_audio", "input_audio": { "data": "@@@langfuseMedia:type=audio/mp3|id=|source=base64@@@" } } ] } 音声データは、Langfuseメディア参照形式で記録します。画面上ではオーディオプレーヤーが表示されます。 音声出力(audioフィールド) アシスタントが音声で返答した場合は、メッセージにaudioフィールドを追加します。 { "role": "assistant", "content": "音声でお答えします", "audio": { "data": "@@@langfuseMedia:type=audio/mp3|id=|source=base64@@@", "transcript": "これは音声の文字起こしです。" } } 画面には、文字起こしテキストとオーディオプレーヤーが両方表示されます。 音声のトレース表示 その他のファイル(PDF等) Langfuseメディアトークンを使うことで、PDFなど任意のファイルタイプも添付できます。 @@@langfuseMedia:type=application/pdf|id=|source=base64@@@ 画面上では、ファイルアイコンと表示リンクが表示されます。 その他のファイルのトレース表示 パターン4: 推論過程(Thinking Block) 一部のLLMは、最終的な回答を生成する前の推論過程を出力します。Langfuseはこれを折りたたみ可能な「Thinking」ブロックとして表示します。この特殊レンダリングはv3.148.0から搭載されています。 標準形式(Anthropic形式) { "role": "assistant", "content": "答えは4です。", "thinking": [ { "type": "thinking", "content": "これは基本的な算数問題です。2+2=4と計算しました。", "summary": "計算結果は4" } ] } summaryは省略可能です。 推論過程のトレース表示 リダクテッド形式(暗号化された推論内容) Anthropic APIでは、推論内容が暗号化されて返される場合があります。 { "role": "assistant", "content": "回答内容", "redacted_thinking": [ { "type": "redacted_thinking", "data": "" } ] } OpenAI Responses API形式(自動変換) OpenAIの新しいResponses APIでは、推論が別メッセージとしてoutput配列内に記録されます。Langfuseはこれを自動的にthinkingフィールドへ変換します。 { "output": [ { "type": "reasoning", "content": [{"type": "text", "text": "計算を行っています..."}], "summary": [{"type": "text", "text": "結果は4"}] }, { "role": "assistant", "content": "4です。" } ] } Gemini形式(自動変換) Gemini APIでは、thought: trueというフラグで推論部分を示します。 { "parts": [ {"text": "計算中...", "thought": true}, {"text": "4です。"} ] } この形式も内部的にthinkingフィールドへ変換され、画面上で折りたたみ可能なブロックとして表示されます。 よくある間違いとトラブルシューティング 間違い1: roleの指定ミス ChatML形式では、roleとcontentの両方が必要です。どちらか一方だけでは認識されません。 {"content": "こんにちは"} // 認識されない {"role": "user", "content": "こんにちは"} // 正しい 間違い2: MediaURIをどこかに入れれば自動レンダリングされると思い込む Langfuseの公式ドキュメント を読むと、「MediaURIをtraceのどこかに入れれば、いい感じにレンダリングされる」という印象を受けるかもしれません。 しかし、実際には 指定されたJSON形式に従わないとレンダリングされません 。例えば、以下のような単純な文字列として埋め込んでも、画像として表示されません。 { "role": "user", "content": "@@@langfuseMedia:type=image/jpeg|id=xxx|source=base64@@@" } 正しくないJSON形式のトレース 正しくは、マルチモーダルコンテンツの構造に従う必要があります。 { "role": "user", "content": [ {"type": "text", "text": "画像の説明"}, { "type": "image_url", "image_url": { "url": "@@@langfuseMedia:type=image/jpeg|id=xxx|source=base64@@@" } } ] } 今後への期待 これらの特殊レンダリング機能は非常に便利ですが、改善の余地もあります。 特に マルチモーダルファイルの添付については、もっと柔軟な仕様にしてほしい というのが個人的な願いです。現状では、前述の通り厳密なJSON構造に従わないとレンダリングされませんが、MediaURIをinput、output、metadataのどこかに含めるだけで自動的に認識されるような仕組みになれば、開発者の負担が大きく軽減されるでしょう。 公式ドキュメントの記述からは「どこに入れても大丈夫」という印象を受けますが、実際には構造化が必要です。この点が改善されることを期待しています。 まとめ LLMアプリケーションの開発において、トレース情報の可視化は非常に重要です。情報量が増えれば増えるほど、適切な構造化と表示方法が求められます。 本記事で紹介したLangfuseの特殊レンダリングパターンを活用することで、以下のメリットが得られます。 複雑なトレース情報が整理され、一目で理解できるようになる デバッグ時に重要な情報へ素早くアクセスできる チーム内でのトレース共有がスムーズになる 主要LLMプロバイダーのレスポンス形式をそのまま記録できる ただし、本記事で紹介したように、指定のJSON構造でないと特殊レンダリングされません。この点には注意してください。 今後、Langfuseがさらに柔軟なレンダリング機能を提供してくれることを期待しつつ、今あるレンダリングの仕様を最大限活用して、より効率的なLLM開発を進めていきましょう。 参考リンク Langfuse公式ドキュメント - Improved ChatML rendering Changelog Langfuse公式ドキュメント - Multi Modality 弊社記事 - Langfuseのマルチモーダル対応:画像・音声ファイルのトレース添付機能がGAに

  • 【入門編】Langfuseで画像OCRの精度検証をシンプルに始める方法

    Geminiの性能向上によりOCRは実用的になりましたが、高精度を目指すならプロンプト調整は必須です。しかし、調整のたびに画像と結果を目視で見比べるのは、手間がかかりミスも誘発します。 そこで本記事では、Langfuseを使ってこの作業を自動化します。「評価」と聞くと難しそうですが、今回は複雑な指標や設定を使わず、かつ、チーム運用は一旦忘れ、まずは 「自分のPC上で、正解データと一致するか」 だけをチェックする気軽な構成を目指します。 なお、プロンプト改善のサイクルを回すことが目的なので、今回は繰り返しテストに適した Dataset Run 機能を利用します。 Datasetの準備 Dataset Runを実行するには、データセットの作成が必要です。 ※ 基本的な作成方法については、以前の記事( Langfuseデータセット構築ガイド:UI・CSV・SDKの徹底比較 )にて紹介しています。 しかし、ここで一つ問題が発生します。 期待する結果の値(Expected Output)はテキストなので問題なく登録できますが、 こちらの記事 でも言及している通り、 2026年1月現在、LangfuseのDataset Itemsはマルチモーダルコンテンツ(画像やPDFの直接保持)をサポートしていません。 また、Web UIから実行する場合のDataset Run機能も、同様にマルチモーダルには未対応です。 そこで今回は、SDK を使ってDataset Runを実行するアプローチをとります。 「スクリプトを書く必要がある」と聞くと手間に感じるかもしれませんが、Web上で完結させないことによるメリットもあります。 スクリプトが動く環境に画像ファイルを置いておけば、Langfuseに画像実体をアップロードする必要や、Amazon S3 や Google Cloud Storageなどのストレージサービスを準備する必要も無く、LLMに投げる直前にローカルファイルを読みこむだけで済むため、構成がシンプルになります。 データセットの構造 ファイルの実体はローカルに置きますが、「どのデータセット項目が、どの画像ファイルに対応するか」を紐付ける必要があります。 今回は手軽さを優先し、ひとまず Datasetの `input` にはファイル名(パス)だけを記述することにします。 例: この形式なら、実ファイルを含める必要が無いため、CSVを使った一括インポートなど、好きな方法でDataset Itemsを登録できます。 評価データとスクリプトの準備 ディレクトリ構成イメージ 今回は以下の画像ファイルを評価にかけます。 ディレクトリ構成は以下の通りです。 ├── ocr.py # 今回作成するスクリプト └── data/ # 画像置き場 ├── image_001.png ├── image_002.png └── ... 評価データ 今回はテスト用に以下を準備しました。 テスト用画像ファイル:Nano Banana Proを用いて、適当なレシート画像を作成 Dataset:テストデータ(csv)をLangfuseのWeb UIから「Import CSV」で一括登録 テスト用画像ファイル テストデータ(csv) Expected Output に shop_name、date、price をマッピングしています。 input shop_name date price image001.png DAILY STORE 2024/01/25 780 image002.png スーパーABC 2024/01/26 1098 image003.png COFFEE STAND 2024/01/27 1600 実際のデータセット(一部) スクリプトの準備 今回は手軽にやるため、処理内容・評価関数共に最低限の作りにします。 LLMは Vertex AI(Gemini 2.5 Flash)を利用し、評価関数としては「店舗名の取得が正常に行えているか?」のチェックのみを実施します。 また、プロンプトの改善を容易にするため、プロンプトはコードへの記述ではなく、Langfuseで管理する形にしました。 import os from dotenv import load_dotenv from langfuse import Langfuse, Evaluation from google import genai import json # 設定(APIキー等は環境変数から読み込み) load_dotenv() langfuse = Langfuse() client = genai.Client( project=os.getenv("GOOGLE_CLOUD_PROJECT"), location=os.getenv("GOOGLE_CLOUD_REGION", "asia-northeast1"), vertexai=True, http_options=genai.types.HttpOptions() ) target_file_path = "data/" prompt = langfuse.get_prompt("ocr") dataset = langfuse.get_dataset(name="ocr_test") # OCRタスクの定義 def ocr_test(item): # Datasetのinput(ファイル名)から実際のパスを生成 input_filename = item.input compiled_prompt = prompt.compile(input = item.input) # ローカルファイルをバイナリで読み込む file_path = os.path.join(target_file_path, input_filename) with open(file_path, "rb") as f: file_data = f.read() # Geminiへ送信(画像データ + プロンプト) response = client.models.generate_content( model="gemini-2.5-flash", contents=[ genai.types.Part.from_bytes(data=file_data, mime_type='image/png'), compiled_prompt, ], config=genai.types.GenerateContentConfig( response_mime_type="application/json", temperature=0.0, response_schema={ "type": 'ARRAY', "items": { "type": "OBJECT", "properties": { "shop_name": { "type": 'STRING', "description": '店舗名', }, "date": { "type": 'STRING', "description": '日付', }, "price": { "type": 'NUMBER', "description": '金額', } }, "required": [ "shop_name", "date", "price" ], }, } ), ) return response.text # 評価関数(単純一致) def simple_evaluator(*, input, output, expected_output=None, **kwargs): # 実務では output のチェックや、改行コードの削除や正規化(strip等)を推奨 response_data = json.loads(output) check = response_data[0].get("shop_name") == expected_output.get("shop_name") if check: comment = "Success" else: comment = "False" return Evaluation( name='output_check', value=check, metadata={ "expected": expected_output, "actual": output }, comment=comment, data_type="BOOLEAN" ) # status: ACTIVEのみを取得 active_items = [item for item in dataset.items if item.status == "ACTIVE"] langfuse.run_experiment( name=f"{prompt.name}_{prompt.version}", description=f"Dataset Run from SDK: {prompt.name}_{prompt.version}", task=ocr_test, data=active_items, evaluators=[simple_evaluator], ) ※ dataset.run_experiment でも Dataset Run は実行できますが、データのフィルタリング(data 引数の指定)が出来ません。そのため、ARCHIVE済みアイテムも含む全件が対象となってしまいます。 評価について LLMによる採点(LLM-as-a-judge)を入れた方がそれらしく見えますが、LLMを使う以上コストが発生します。OCRのように正解が明確なタスク(完全一致または簡単なルールベースで比較ができる場合)であれば、LLMより単純な比較の方が精度が見込める場合も多いです。 今回はスクリプトで評価まで完結させていますが、「あいまいな表現に対しても評価したい」など、別軸でLLM-as-a-judgeも適応させたい場合は、Langfuseの設定で後から追加することも可能です。 実行結果 スクリプトを実行すると、Langfuseの画面に結果が反映されます。 今回は結果がBOOLEANとなる評価を設定しましたが、このようにEvaluatorで設定した項目に対して、True・Falseの件数が表示されます。 詳細画面を確認すると、このように実際の値の比較や、どのデータが OK/NG だったかの確認も一目で出来ます。 今回は項目が少なく、評価軸もシンプルなので、Output と Expected Output を見比べれば何が問題かを容易に読み取れます。 実際の運用では、Metadataなどを活用するとよさそうです。 上記画面では、結果に対してマウスオーバーで Evaluation にて設定したコメントやMetadataが表示されます。 吹き出しにマウスオーバーした場合(comment="Success"が表示) {...}部分にマウスオーバーした場合(metadataの中身が表示) さらに、過去の実行結果の比較も出来るため、「さっきのプロンプトの方が精度が良かったな…」といった振り返りも容易です。 差分の比較方法については、 LangfuseのExperiments Compare ViewのBaseline機能を解説 もあわせて確認頂くと、より詳細な比較が行えるかと思います。 まとめ 今回はとにかく「マルチモーダルな評価」を「とりあえず簡単にやってみる」ことに着目して実験しました。 この構成なら、ひとまずLLMが使える状態さえ準備出来れば、スクリプトを実行するだけで、OCRの精度評価をLangfuseに任せてしまえます。 チーム開発への展望 今回は個人のPCで完結させましたが、もしチームで実行したい場合は Amazon S3 や Google Cloud Storage などのストレージサービスを利用するのがおすすめです。 スクリプト上のローカルファイルからファイルを取得する処理を、各々のストレージサービスに対応した形に修正すれば、同じように実行できます。 一例として、 Vertex AI(Gemini) + Google Cloud Storage (GCS) の構成であれば、スクリプト上でファイルを読み込む必要もありません。権限の設定は必要ですが、GCS上のファイル(`gs://...`)は直接Geminiに読み込ませることが可能となっています。 (スクリプトの「ローカルファイルをバイナリで読み込む」ブロックの処理も不要となります。)

  • Langfuseのプロンプト変更制限 解説(Protected Prompt Labels)

    本ブログでは、Langfuseを複数人で運用する環境下において、「プロンプトを誰でも変更されるのが不安」「うっかり本番用のラベルを動かしてしまった」というヒヤリハットや、「誰でも本番環境を変更できてしまう」というガバナンス上の課題を感じていたチームにとって、必須の機能をご説明します! カジュアルに本番プロンプトを変えられてしまう運用はリスクでしかない なぜこの機能が必要なのか?(利用シーン) 複数人でLLMアプリを開発・運用していると、以下のようなリスクに直面します。この機能は、それらをシステム的に解決します。 1. 「うっかり事故」の根絶 開発中、未検証のプロンプトに誤って production ラベルを付け替えてしまい、本番稼働中のアプリの挙動が突然変わってしまう。 2. プロンプト版「デプロイ承認フロー」の確立 「プロンプトの作成・改善はメンバー全員で行いたいが、本番環境への反映(デプロイ)はリーダーや管理者の承認・操作に限定したい」。 ソフトウェア開発では当たり前のこの権限分離を、プロンプト管理においても実現できます。 3. QA・検証環境の固定 テスト中に、対象のプロンプトバージョンが誰かの手によって意図せず変更されるのを防ぎます。検証環境の再現性と安定性を担保するために役立ちます。 機能の概要と仕組み Protected Prompt Labels は、特定のラベル(例:production や staging)を文字通り「保護状態」にし、操作できるユーザー権限を制限する機能です。 導入後の権限コントロール 管理者が特定のラベルを「Protected(保護)」に設定すると、以下の挙動になります。 一般メンバー(Member / Viewer) 保護されたラベルを 操作(移動・削除・付与)できません 。 これにより、現場レベルでの誤操作による本番影響リスクが物理的にゼロになります。 管理者(Admin / Owner) これまで通り、ラベルの操作が可能です。 「メンバーからの変更提案をレビューし、問題なければ管理者がラベルを付け替えて本番反映する」という運用が可能になります。 Langfuse のProtected Prompt Labels設定方法 Settings -> Protected Prompt Labels で対象の Label を設定するだけです。既に作られているLabelを対象にすることができます。他に操作は必要ありません。 選択したら Add をするだけで完了です。 本機能を使った運用フロー例 開発・検証 : メンバーは新しいプロンプトを作成し、dev や staging ラベルを用いて検証・改善を行う。 レビュー依頼 : 検証が完了したら、管理者に本番反映を依頼する。 本番反映(デプロイ) : 管理者は内容をレビューし、問題がなければ production ラベル を新しいバージョンのプロンプトに付与する。 「プロンプトの変更(開発)」と「本番への適用(運用)」を明確に分離することは、複数人で開発をする際における安全なサービス運用の鍵となります。 いかがでしたでしょうか? 本機能は TeamプランやEnterpriseプランなどでご利用いただけるものになっていますが、複数人でLangfuseを利用する際には必須のものだと思います。本番環境のサービスに是非利用してみてください。

  • Langfuseにおける個人情報(PII)マスキング:Sensitive Data Protection の活用

    これまで、LangfuseでのPIIマスキング手法として、 llm-guard 、 Guardrails for Amazon Bedrock 、そして LLM(Gemini 2.5 Flash Lite) によるマスキング手法を検討してきました。 今回は、Google Cloudの機密データ保護機能である Sensitive Data Protection(旧:Cloud Data Loss Prevention, Cloud DLP)  の利用について検討します。 Sensitive Data Protection とは? Sensitive Data Protection (旧称:Cloud Data Loss Prevention、Cloud DLP)は、Google Cloudが提供する 機密データ保護サービス です。データの検出、分類、匿名化といった強力な機能を提供し、個人情報(PII)をはじめとする機密情報の漏洩リスクを軽減します。 このサービスは、単なるパターンマッチングにとどまらず、機械学習や文脈分析を組み合わせることで、より高精度な機密データ検出を実現します。特に、 クレジットカード番号、社会保障番号、メールアドレス、電話番号 など、世界中の150種類以上の組み込みの InfoType(情報タイプ)  検出器をサポートしており、非常に広範な種類のPIIに対応できます。また、独自のビジネスニーズに合わせて カスタムInfoType を定義することも可能です。 Guardrails for Amazon Bedrockと同様に、APIを通じてPIIマスキングを実行できます。今回の検証では、 DLP API を利用してマスキングを行います。 Guardrails for Amazon Bedrock との違いは? これまでの検証で取り上げた Guardrails for Amazon Bedrock は、Sensitive Data Protection と目的が近いサービスです。どちらもLLMの入出力におけるPIIマスキングに利用できますが、いくつか重要な違いがあります。 特徴 \ ソリューション Sensitive Data Protection Guardrails for Amazon Bedrock 最適なユーザー Google Cloud環境でLLMアプリケーションを運用するユーザー AWS BedrockをLLMプラットフォームとして利用するユーザー 検出範囲と精度 汎用PII検出、150種類以上の高精度なInfoType(PIIに特化) Bedrockの入出力に特化、PIIを含む多様なフィルタタイプ 適用レイヤー アプリケーション層での柔軟な適用(API経由) Bedrock API呼び出し時(リアルタイム) リアルタイム性 API経由でリアルタイム処理が可能 リアルタイムで適用可能 Langfuse連携 API呼び出し結果をLangfuseへ記録可能 API呼び出し結果をLangfuseへ記録可能 その他特徴 高度な匿名化オプション、広範なPII検出器、 コンテキストを考慮した検出 Bedrockでのコンテンツモデレーションの一元化、 リアルタイムポリシー適用 Guardrails for Amazon BedrockはAmazon Bedrockと密接に統合されており、Bedrockを利用する際に特化したリアルタイム制御を提供します。一方、Sensitive Data ProtectionはGoogle Cloudの汎用的な機密データ保護サービスであり、より広範なPIIタイプに対応し、アプリケーション層で柔軟に組み込める点が特徴です。 今回の検証では、PII検出の対象をサンプルコード内で設定しますが、Guardrailsと同様に コンソール上での設定も可能 です。サービスとして運用する際は、コンソールで検出対象を追加・調整し、アプリケーションの修正やリリースなしでポリシーを更新できる テンプレートの利用を検討 することをおすすめします。 テスト方法 今回は以下の手順で、Sensitive Data ProtectionのPIIマスキング機能を検証していきます。 事前準備 まず、Sensitive Data Protection が提供する InfoType検出器(検出対象)  を確認します。検出器のリファレンスは こちら にあります。過去の検証と同様に、今回はカスタム検出器は利用せず、 組み込みのInfoType検出器のみ を使用します。 当初、すべてのInfoTypeを設定しようとしましたが、検出器の設定可能上限数(150個)を超過したため(総数216個)、 カテゴリがPIIに該当するものに限定 して設定することにしました。 多数のInfoTypeを手動でリスト化するのは大変なので、以下のPythonコードでAPIからPII関連のInfoTypeを自動で取得します。 # DLPクライアントを初期化(クォータプロジェクトを指定) dlp_client = dlp_v2.DlpServiceClient client_options={"quota_project_id": project_id} ) def get_info_types(): info_types_response = dlp_client.list_info_types( request={ 'parent': f"projects/{project_id}", 'filter': 'supported_by=INSPECT' # 検査可能なInfoTypeに絞る } ) included_info_types = [] pii_list = [] for info_type in info_types_response.info_types: if info_type.name in included_info_types: continue # categoriesを反復してPIIを探す is_pii = False for category in info_type.categories: if category.type_category.name == "PII": is_pii = True break; if is_pii: included_info_types.extend(info_type.specific_info_types) pii_list.append({ 'name': info_type.name, }) return pii_list 参考 マスキング対象として期待する項目と、llm-guardのAnonymize機能、Guardrails for Amazon Bedrockの機密情報フィルター、そしてSensitive Data ProtectionのInfoType検出器で対応すると思われる項目を比較表にまとめました。 PII llm-guard Guardrail for Amazon Bedrock Sensitive Data Protection 氏名 人名 NAME PERSON_NAME 生年月日 - - DATE_OF_BIRTH 住所 - ADDRESS GEOGRAPHIC_DATA 電話番号 電話番号 PHONE PHONE_NUMBER メールアドレス メールアドレス EMAIL EMAIL_ADDRESS 運転免許証番号 - DRIVER_ID DRIVERS_LICENSE_NUMBER パスポート番号 - US_PASSPORT_NUMBER PASSPORT 社会保障番号 米国社会保障番号(SSN) US_SOCIAL_SECURITY_NUMBER US_SOCIAL_SECURITY_NUMBER JAPAN_INDIVIDUAL_NUMBER(マイナンバー) (クレジットカード情報) カード番号 クレジットカード CREDIT_DEBIT_CARD_NUMBER CREDIT_CARD_DATA (クレジットカード情報) 有効期限 - CREDIT_DEBIT_CARD_EXPIRY CREDIT_CARD_DATA (クレジットカード情報) セキュリティコード - CREDIT_DEBIT_CARD_CVV CREDIT_CARD_DATA (銀行口座情報)銀行名 - - - (銀行口座情報)支店名 - - - (銀行口座情報) 口座番号 - US_BANK_ROUTING_NUMBER FINANCIAL_ID llm-guardと比べると検出項目は充実していますが、Guardrails for Amazon Bedrockでカバーしきれていなかった「 生年月日 」もSensitive Data Protectionでは検出できるようです。銀行口座情報の銀行名や支店名を除けば、期待されるほとんどのPII項目を網羅できる見込みです。 今回の検証では、誤検知が発生しないかも確認するため、PIIカテゴリに属する 85種類の検出器をすべて設定 しました。 アプリケーションコード 過去の記事と同様に、簡単なPythonコードを用いてプロンプトの入力とLLMの回答を模擬的に生成し、それぞれをLangfuseに登録する形式でテストを行います。 get_info_types関数とDLPクライアントの初期化部分を追記、masking_functionの中身をDLP APIを利用するように修正します。マスキング方法は、今回は 検出されたInfoTypeの名前で置換 する設定にします。 今回は行いませんが、「*」のような特定の文字で置き換えたり、固定文言の指定、設定した文言のいずれかと置換、といった設定も可能です。 def masking_function(data: any, **kwargs) -> any: if isinstance(data, str) and len(data) > 0: # 利用可能なPIIのInfoTypeを取得 info_types_list = get_info_types() request = { 'parent': f"projects/{project_id}", 'inspect_config': { 'info_types': info_types_list }, 'deidentify_config': { 'info_type_transformations': { 'transformations': [ { 'primitive_transformation': { 'replace_with_info_type_config': {} } } ] } }, 'item': {'value': data} } # DLPを使用したPIIマスキング response = dlp_client.deidentify_content( request=request ) sanitized_data = response.item.value return sanitized_data else: return data テストデータ 過去の記事と同様に、以下のダミーデータを流用してテストを行いました。 ダミーデータ 私の個人情報は以下の通りです: 氏名:山田 太郎 Name: Taro Yamada 生年月日:1985年3月15日 Birthday: March 15, 1985 住所:東京都新宿区西新宿2-8-1 Address: 2-8-1 Nishi-Shinjuku, Shinjuku-ku, Tokyo, Japan 電話番号:03-1234-5678 / 090-9999-8888 Phone: +81-3-1234-5678 / +81-90-9999-8888 メールアドレス:taro.yamada@example.com Email: taro.yamada@example.com 運転免許証番号:123456789012 Driver's License: DL-123456789012 パスポート番号:TK1234567 Passport Number: TK1234567 社会保障番号:987-65-4321 Social Security Number: 987-65-4321 クレジットカード情報: - カード番号:4111-2222-3333-4444 - 有効期限:12/25 - セキュリティコード:123 Credit Card Information: - Card Number: 4111-2222-3333-4444 - Expiry Date: 12/25 - Security Code: 123 銀行口座情報: - 銀行名:みずほ銀行 - 支店名:新宿支店 - 口座番号:1234567 Bank Account Information: - Bank Name: Mizuho Bank - Branch: Shinjuku Branch - Account Number: 1234567 検証結果 以下に、llm-guard(デフォルト設定)、Guardrails for Amazon Bedrock(apply_guardrail)、そして今回検証したSensitive Data Protectionそれぞれで処理した際のPII項目ごとの置換結果を比較します。 llm-guard (デフォルト設定) Guardrail for Amazon Bedrock Sensitive Data Protection 氏名(日本語) [REDACTED_PERSON_1][REDACTED_PERSON_2][REDACTED_PERSON_3] {NAME} [PERSON_NAME] 氏名(英語) [REDACTED_PERSON_4] {NAME} [PERSON_NAME] 生年月日 - - [DATE_OF_BIRTH] 住所 - {ADDRESS} [PERSON_NAME][GEOGRAPHIC_DATA] 電話番号(日本) 03-1234-[REDACTED_PHONE_NUMBER_1] {PHONE} [PHONE_NUMBER] / [PHONE_NUMBER] 電話番号(海外) [REDACTED_PHONE_NUMBER_2] {PHONE} [PHONE_NUMBER] / [PHONE_NUMBER] メールアドレス [REDACTED_EMAIL_ADDRESS_1] {EMAIL} [EMAIL_ADDRESS] 運転免許証番号 - {DRIVER_ID} [DRIVERS_LICENSE_NUMBER] DL-[DRIVERS_LICENSE_NUMBER] パスポート番号 - {US_PASSPORT_NUMBER} パスポート番号:[DRIVERS_LICENSE_NUMBER] Passport Number: [PASSPORT] 社会保障番号 [REDACTED_US_SSN_RE_1] 987-65-4321  {US_SOCIAL_SECURITY_NUMBER} 987-65-4321 クレジットカード(カード番号) [REDACTED_CREDIT_CARD_RE_1] {CREDIT_DEBIT_CARD_NUMBER} 4111-2222-3333-4444 クレジットカード(カード番号以外) - 有効期限:{CREDIT_DEBIT_CARD_EXPIRY} セキュリティコード:{CREDIT_DEBIT_CARD_CVV} 有効期限:12/25 セキュリティコード:123 銀行口座情報 - 銀行名:みずほ銀行 支店名:新宿支店 口座番号:{US_BANK_ACCOUNT_NUMBER} Bank Name: Mizuho Bank Branch: {ADDRESS} Branch Account Number: {US_BANK_ACCOUNT_NUMBER} 銀行名:[PERSON_NAME]銀行 支店名:[GEOGRAPHIC_DATA] 口座番号:[FINANCIAL_ID] Bank Name: Mizuho Bank Branch: Shinjuku Branch Account Number: [FINANCIAL_ID] llm-guard(デフォルト設定)とGuardrails for Amazon Bedrock(apply_guardrail)の結果は、以前の記事で検証した際のデータを記載しています。Sensitive Data Protection の検証において、上記2回と明示的に差異がある項目の背景色を変更しています。 今回の検証で目立つ点としては、期待していたクレジットカード情報や社会保障番号が検出されませんでした。また、いくつかの誤検出も見られます。DLPの検出器には、 コンテキストの手がかり がある場合にのみ識別できるものもあるため、もう少し自然な文脈を与えたテキストであれば、結果は変わってくるかもしれません。 結果詳細(誤検出・検出漏れ項目の抜粋) 氏名 氏名:[PERSON_NAME]Name: [PERSON_NAME] 検出自体は問題なさそうですが、元のテキストの改行が消えてしまっています。 住所 住所:[PERSON_NAME][GEOGRAPHIC_DATA] 何かが人名として誤検出されてしまっています。また、どこまでが人名として検出されているかは不明なため推測にはなりますが、おそらく日本語住所+改行+Address:+英語住所がひとまとめに住所としてみなされているようです。 運転免許証番号 運転免許証番号:[DRIVERS_LICENSE_NUMBER] Driver's License: DL-[DRIVERS_LICENSE_NUMBER] 誤検出というほどではありませんが、「DL-」というプレフィックスが残ってしまっています。 パスポート番号 パスポート番号:[DRIVERS_LICENSE_NUMBER] Passport Number: [PASSPORT] 日本語版が運転免許証番号として誤検出されています。 社会保障番号 社会保障番号:987-65-4321 Social Security Number: 987-65-4321 期待とはことなり、PIIとして検出されませんでした。 クレジットカード情報 クレジットカード情報: - カード番号:4111-2222-3333-4444 - 有効期限:12/25 - セキュリティコード:123 Credit Card Information: - Card Number: 4111-2222-3333-4444 - Expiry Date: [CREDIT_CARD_EXPIRATION_DATE] - Security Code: 123 英語版の有効期限のみが検出されました。 銀行口座情報 銀行口座情報: - 銀行名:[PERSON_NAME]銀行 - 支店名:[GEOGRAPHIC_DATA] - 口座番号:[FINANCIAL_ID] Bank Account Information: - Bank Name: Mizuho Bank - Branch: Shinjuku Branch - Account Number: [JAPAN_BANK_ACCOUNT] 日本語の銀行名が個人名、支店名が住所として誤検出されています。 FINANCIAL_ID に JAPAN_BANK_ACCOUNT は含有されていますが、異なる検出器で置き換わっているのは気になります。 検出の精度は十分とは言えませんが、Guardrailsやllm-guardでは検出できなかった一部の項目(例:生年月日)を捉えることができました。 まとめ 今回はGoogle Cloudの Sensitive Data Protection を利用したPIIマスキング機能について検証しました。 テストの結果、一部の項目で優れた検出能力を示しましたが、完全に網羅できているわけではなく、誤検出も見られました。今回の検証で検出できなかったPII項目の一部は、llm-guardのような別のフィルタリングツールでカバーできる可能性があります。そのため、 Sensitive Data Protectionとllm-guardを併用 することで、コストを抑えつつカバー範囲を広げられるかもしれません。 また、今回の検証は限定的な条件での確認となります。実際にアプリケーションで利用する際は、自然なテキスト中にPIIが含まれている場合や、全角文字が混在する場合など、 日本語特有の表現が含まれている場合の挙動 については、さらに詳細な検証が必要不可欠です。 検討課題 Sensitive Data Protectionの料金体系 も考慮すべき点です。料金は、検出・変換されたデータ量(バイト数)に基づいて発生します。月間1GBまでの利用は無料ですが、それを超えると1GBあたり2~3 USDの費用がかかるため、他のソリューションと比較して高額になる可能性があります。

bottom of page