検索結果
空の検索で52件の結果が見つかりました。
- Langfuseにおける個人情報(PII)マスキング:Gemini 2.5 Flash Liteで実現するLLMベースPIIフィルター
LangfuseにおけるPIIマスキング手法の検討 前回の記事 では、Guardrails for Amazon Bedrockを利用したPIIマスキングについて紹介しました。個人情報を列挙した形のテストデータにおいては精度が高く、大半の情報は除去できましたが、一部、フィルターが準備されていないなど、追加の対応が必要な項目が残っていました。 また、この機能はAmazon Bedrockに依存するため、AWS以外のクラウドを利用している方にとっては導入のハードルがやや高くなると考えます。 そこで本記事では、PIIマスキングをLLM(大規模言語モデル)に任せるアプローチを検証します。 LLMを活用する利点は、プロンプトの調整によって非エンジニアでも容易に設定変更できること、そして文脈を考慮した判断が可能になる点です。(例えば、芸能人の名前や公開されている会社の住所は公開情報としてマスキングしないなど) さらに、PIIフィルター用のプロンプトもあわせてLangfuseで管理することで、コードのデプロイを都度行う必要や、クラウドのコンソール画面を触ることなく、非エンジニアでもプロンプトの調整を容易に行えるようになります。 今回のLLMモデルには、 Gemini 2.5 Flash-Lite を採用します。 Gemini 2.5 Flash Liteとは? Googleが提供する最新の基盤モデルファミリーであり、低レイテンシのユースケース向けに最適化された、最もバランスの取れたGeminiモデルです。 ただし、現在パブリックプレビュー中のため、利用できる場面は限られています。 なぜGemini 2.5 Flash Liteなのか 処理速度が高速で、料金も入力トークン100万あたり、$0.1、出力は$0.4と低コストで提供されています。料金は、Gemini 2.5 Flash と比較すると、おおよそ3分の1以下の費用に抑えられています。 PIIフィルターは呼び出し頻度が高くなるため、高速かつ低コストで利用できる点を高く評価し、今回採用しました。 PIIマスキングのテスト方法 前回、前々回と同様のアプリケーションコードと、テストデータを用いて検証を行います。 アプリケーションコード 前回同様、masking_function の内部を調整します。 client = genai.Client(api_key="GOOGLE_API_KEY") def masking_function(data: any, **kwargs) -> any: if isinstance(data, str): # マスキング処理 response = client.models.generate_content( model='models/gemini-2.5-flash-lite-preview-06-17', contents=data ) sanitized_data = response.text 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 - Accou nt Number: 1234567 検証結果1 上記の準備を行った上で、アプリケーションを実行します。 トレースを確認したところ、以下の結果が登録されていました。 ご提示いただいた個人情報につきまして、私はAIアシスタントであり、あなたの個人情報を直接保存・管理することはできません。 いただいた情報は、ご自身の確認のために役立つかもしれませんが、これらの情報は非常に機密性の高いものです。これらの情報を安易に他者に共有することは、悪用されるリスクを高めます。 特に、クレジットカード情報や銀行口座情報は、絶対に第三者に教えないでください。 もし、これらの情報を安全に管理したいのであれば、パスワードで保護された安全な場所(例えば、パスワードマネージャーなど)に保存することをお勧めします。 もし、私にこれらの情報について何か特別な操作(例えば、どこかに登録するなど)を求めていらっしゃるのであれば、その意図を具体的に教えていただけますでしょうか?ただし、どのような場合でも、私のシステムに直接個人情報を入力したり保存したりすることはできませんので、その点ご了承ください。 ダミーデータをそのままLLMに投入したため、データに対する具体的な指示や解凍形式が適切に含まれていませんでした。 比較を行う前に、この点を改善します。 LLMフィルタリング用のプロンプト設計 LLMから意図する回答を得るためには、プロンプトの改善が必要です。アプリケーションコードの修正を都度行うのは煩わしいため、PIIフィルター用のプロンプトも Langfuse で管理することにしました。 アプリケーションコード(プロンプト管理版) Langfuse で管理しているプロンプトを利用する形になるよう調整します。 # Langfuse で管理しているプロンプトの取得 pii_prompt_template = langfuse.get_prompt("pii_filter_prompt", label=langfuse_config.prompt_label) client = genai.Client(api_key="GOOGLE_API_KEY") def masking_function(data: any, **kwargs) -> any: if isinstance(data, str): prompt_content = pii_prompt_template.compile(userMessage=data) # マスキング処理 response = client.models.generate_content( # モデル情報もプロンプトの config から取得 model=pii_prompt_template.config.get("model", "models/gemini-2.5-flash-lite-preview-06-17") , contents=prompt_content ) sanitized_data = response.text return sanitized_data else: return data プロンプト設定 先のコード修正により、 pii_filter_prompt という名称のプロンプトが利用可能になりました。Langfuse 側で該当の名前のプロンプトを作成し、調整を行います。 今回の検証では、以下の点を満たす PII フィルター用のプロンプトを設定します。 過去2回で対象とした項目を網羅できること どのようなデータがマスクされたかわかること 入力値以外の応答文等が入らないこと 3点目については、簡易的なプロンプトで個人情報がマスクされるかを試したところ、下記のように入力値にない応答文が含まれる結果となりました。 これでは、入力値からPII情報のみを抽出・マスキングするという本来の目的を達成できないため、この点をプロンプトの条件に追加しています。 承知いたしました。以下のように個人情報をマスクしました。 私の個人情報は以下の通りです: 氏名:**** **** Name: **** **** (後略) Langfuse 上でプロンプトの調整を行いつつ、最終的に、今回の検証では以下のプロンプトをPIIフィルター用として設定しました。 以下のテキストから、個人情報をマスクしてください。 # 条件 - マスク処理以外は行わない - テキストの前後に余分なテキストを追加しない - どのようなデータがマスクされたかわかるようにする(例:山田太郎 -> [氏名1]、000-0000-0000 -> [電話番号1]) - 同じ個人情報種別でも、異なる情報の場合は別物であるとわかるようにする(例:山田さんと佐藤さん -> [氏名1]さんと[氏名2]さん) # 置換対象の個人情報 - 氏名 - 生年月日 - 住所 - 電話番号 - メールアドレス - 運転免許証番号 - パスポート番号 - 社会保障番号 - クレジットカード情報(カード番号, 有効期限, セキュリティコード) - 銀行口座情報(銀行名, 支店名, 口座番号) # 対象のテキスト {{userMessage}} こちらの状態で、PIIフィルターの検証を再度行います。 検証結果2 前回の Guardrails for Amazon Bedrock、前々回のllm-guard を利用した際の結果と比較していきます。 LLM利用 (Gemini 2.5 Flash-Lite) llm-guard(デフォルト設定) Guardrail for Amazon Bedrock 氏名(日本語) [氏名1] [REDACTED_PERSON_1][REDACTED_PERSON_2][REDACTED_PERSON_3] {NAME} 氏名(英語) [氏名2] [REDACTED_PERSON_4] {NAME} 生年月日 [生年月日1] - - 住所 [住所1] - {ADDRESS} 電話番号(日本) [電話番号1] / [電話番号2] 03-1234-[REDACTED_PHONE_NUMBER_1] {PHONE} 電話番号(海外) [電話番号3] / [電話番号4] [REDACTED_PHONE_NUMBER_2] {PHONE} メールアドレス [メールアドレス1] [REDACTED_EMAIL_ADDRESS_1] {EMAIL} 運転免許証番号 [運転免許証番号1] - {DRIVER_ID} パスポート番号 [パスポート番号1] - {US_PASSPORT_NUMBER} 社会保障番号 [社会保障番号1] [REDACTED_US_SSN_RE_1] 987-65-4321 {US_SOCIAL_SECURITY_NUMBER} クレジットカード(カード番号) [クレジットカード番号1] [REDACTED_CREDIT_CARD_RE_1] {CREDIT_DEBIT_CARD_NUMBER} クレジットカード(カード番号以外) 有効期限:[クレジットカード有効期限1] セキュリティコード:[クレジットカードセキュリティコード1] - 有効期限:{CREDIT_DEBIT_CARD_EXPIRY} セキュリティコード:{CREDIT_DEBIT_CARD_CVV} 銀行口座情報 銀行名:[銀行名1] 支店名:[銀行支店名1] 口座番号:[銀行口座番号1] Bank Name: [銀行名2] Branch: [銀行支店名2] Account Number: [銀行口座番号2] - 銀行名:みずほ銀行 支店名:新宿支店 口座番号:{US_BANK_ACCOUNT_NUMBER} Bank Name: Mizuho Bank Branch: {ADDRESS} Branch Account Number: {US_BANK_ACCOUNT_NUMBER} llm-guard、Guardrails for Amazon Bedrock との比較において、いずれかと差異のある項目のみ背景色を変更しています。 前回確認した、Guardrails においては、フィルターが対応している項目においてはおおむね問題なく検知できているという結果でしたが、今回の LLM を利用したフィルターについては、過去2回で検証した結果と比較して、検出漏れ・誤検出が少ない結果となりました。 今回は LLM を利用するケースが優位に見える結果になりましたが、検証に利用しているダミーデータは自然な文脈が存在せず、個人情報が明示的に記載されているため、自然な文脈で利用した場合にはまた違う結果となる可能性があります。 また、今回提示しているアプリケーションコードでは、PIIフィルター処理そのものに対するトレース保存は行っていませんが、この処理に対してもトレースを保存することで、より詳細なコスト感を把握できるようになります。 PIIフィルターのプロンプトに関しては、Langfuseの評価機能を活用することで、さらなる制度の工場が期待できます。 まとめ 本記事では、LLM(Gemini 2.5 Flash-Lite)を利用したPIIマスキングについて検証しました。Langfuseのトレース情報から個人情報を確実に除去するという目的のもと、PIIの除去については、概ね問題なく行える見込みです。 ただし、今回の検証は限定的な条件下での確認であるため、実際にアプリケーションで利用する際には、さらなる詳細な検証とプロンプトの改善が必要不可欠です。また、前回の記事でも触れましたが、LLMを利用するため、本実装のままではコスト面などから実際のアプリケーションで利用するには最適ではない可能性もあります。プロンプトや実装のアプローチについて、多角的に比較・検討することをお勧めします。
- Langfuseにおける個人情報(PII)マスキング:Guardrails for Amazon Bedrockの活用
LangfuseにおけるPIIマスキング手法の検討 前回の記事 では、llm-guardを用いたPIIマスキングについて検証しました。llm-guardは柔軟なモデル選択が可能である一方、日本語のPII検出にはまだ課題が残ることが分かりました。 そこで、今回はその代替手段として、Guardrails for Amazon Bedrockの機密情報フィルター(Maskモード)の利用を検討します。 Guardrails for Amazon Bedrock とは? Guardrails for Amazon Bedrock は、Amazon Bedrock 上に構築される生成 AI アプリケーションに対して、以下の主要なガードレール(保護機能)を提供します。これらのガードレールは、内部的にAmazon Bedrockの基盤モデル(FM)の推論能力を活用してコンテンツの評価やフィルタリングを行っています。ユーザーがガードレール内で直接LLMモデルを指定することはできませんが、高度なAIを活用したコンテンツモデレーションが実現されています。 コンテンツフィルター: 憎悪、侮辱、性的、暴力、不法行為、プロンプト攻撃などのカテゴリに対してフィルタリング強度を調整できま 禁止トピック: 特定の話題に関するプロンプトや応答をブロックできます ワードフィルター: 指定した単語が含まれる入出力をブロックまたはマスキングできます 機密情報フィルター (Sensitive Information Filters): 氏名、メールアドレス、電話番号、クレジットカード番号などの PII を含む入出力を検出・ブロック・マスキングする機能です 特に「機密情報フィルター」は、事前に定義された PII タイプ(31種類以上)に対応しており、正規表現(RegEx)を使用してカスタムの機密情報を定義することも可能です。 また、PII の処理方法として、以下の2つのモードを選択できます。 BLOCK: 機密情報が検出された場合、コンテンツ全体をブロックし、カスタムメッセージを返します MASK: コンテンツに含まれる機密情報をマスキング(編集)し、[NAME]、[EMAIL]のような識別子タグに置き換えます なぜGuardrails for Amazon Bedrockなのか? Guardrails for Amazon Bedrockには、基盤モデルを呼び出すことなく、ガードレールのルールのみを適用してコンテンツを評価・マスキングできる独立したAPI(bedrock_runtime.apply_guardrail)が存在します。このAPIを利用することで、LLMの推論プロセスとは分離して、Guardrailsが提供するPIIマスキング機能のみを検証・活用できます。 このapply_guardrailメソッドを活用し、Langfuseのトレースにおけるマスキング処理を実現します。 PIIマスキングのテスト方法 今回は、以下の手順でGuardrails for Amazon Bedrockの機密情報フィルター(Maskモード)の動作を検証します。 事前準備 AWS上に有効なGuardrailsを作成する必要があります。今回は正規表現によるカスタム制約は検証せず、標準設定で利用可能な機密情報フィルターに焦点を当てます。 事前定義されているPIIフィルターは こちら に記載があります。 上記のページを参考に、コンソールまたはAPIから利用したい機密情報フィルターを設定していきます。今回は PII のマスキングを行うため、フィルターの動作は「マスク」を選択します。 参考 前回マスキング対象として期待した項目と、llm-guardのAnonymize機能が対応しているとされる項目、Guardrails form Amazon Bedrockの機密情報フィルターで相応すると思われる項目を並べてみました。 llm-guard と比較すると、Guardrails for Amazon Bedrock は標準で設定可能なPIIフィルターが充実していることが分かります。 PII llm-guard Guardrail for Amazon Bedrock 氏名 人名 NAME 生年月日 - - 住所 - ADDRESS 電話番号 電話番号 PHONE メールアドレス メールアドレス EMAIL 運転免許証番号 - DRIVER_ID パスポート番号 - US_PASSPORT_NUMBER 社会保障番号 米国社会保障番号(SSN) US_SOCIAL_SECURITY_NUMBER (クレジットカード情報) カード番号 クレジットカード CREDIT_DEBIT_CARD_NUMBER (クレジットカード情報) 有効期限 - CREDIT_DEBIT_CARD_EXPIRY (クレジットカード情報) セキュリティコード - CREDIT_DEBIT_CARD_CVV (銀行口座情報)銀行名 - - (銀行口座情報)支店名 - - (銀行口座情報)口座番号 - US_BANK_ROUTING_NUMBER Guardrailsを利用すれば、生年月日と銀行口座情報の銀行名、支店名を除き期待される項目は網羅できそうです。 今回の検証では、誤検知が起こらないかを確認する意味合いも含め、上記に含まれない項目を含めた31種をフィルターとして設定しました。 アプリケーションコード 前回同様、簡単なコードを用いて、プロンプトの入力とLLMの回答を模擬的に生成し、それぞれをLangfuseに登録する形式でテストを行いました。 前回のコードに対し、マスキング処理部分と初期化部分に手を加えます。今回はGuardrails for Amazon Bedrockのapply_guardrailメソッドを利用するように変更しています。 bedrockRuntime = boto3.client( 'bedrock-runtime', region_name="*********", aws_access_key_id="**********************", aws_secret_access_key="**********************" ) guardrail_id = "[あなたのガードレールのID]" guardrail_version = "DRAFT" # またはバージョン番号 def masking_function(data: any, **kwargs) -> any if isinstance(data, str): responce = bedrockRuntime.apply_guardrail( guardrailIdentifier=guardrail_id, guardrailVersion=guardrail_version, source="INPUT", # 今回はシンプルにするため、常に"INPUT"として評価する content=[ { "text": { "text": data } } ] ) # outputsからマスキング済みテキストを取得 if 'outputs' in responce and len(responce['outputs']) > 0: sanitized_data = responce['outputs'][0].get('text', data) return sanitized_data else: # outputsがない場合は元のデータを返す return data return data テストデータ 前回利用した以下のダミーデータを、今回もそのまま利用します。 私の個人情報は以下の通りです: 氏名:山田 太郎 Name: Taro Yamada 生年月日:1985年3月15日 Birthday: March 15, 198 住所:東京都新宿区西新宿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)それぞれで処理した際のPII項目ごとの置換結果を比較します。 llm-guard(デフォルト設定) Guardrail for Amazon Bedrock 氏名(日本語) [REDACTED_PERSON_1][REDACTED_PERSON_2][REDACTED_PERSON_3] {NAME} 氏名(英語) [REDACTED_PERSON_4] {NAME} 生年月日 - - 住所 - {ADDRESS} 電話番号(日本) 03-1234-[REDACTED_PHONE_NUMBER_1] {PHONE} 電話番号(海外) [REDACTED_PHONE_NUMBER_2] {PHONE} メールアドレス [REDACTED_EMAIL_ADDRESS_1] {EMAIL} 運転免許証番号 - {DRIVER_ID} パスポート番号 - {US_PASSPORT_NUMBER} 社会保障番号 [REDACTED_US_SSN_RE_1] 987-65-4321 {US_SOCIAL_SECURITY_NUMBER} クレジットカード(カード番号) [REDACTED_CREDIT_CARD_RE_1] {CREDIT_DEBIT_CARD_NUMBER} クレジットカード(カード番号以外) - 有効期限:{CREDIT_DEBIT_CARD_EXPIRY} セキュリティコード:{CREDIT_DEBIT_CARD_CVV} 銀行口座情報 - 銀行名:みずほ銀行 支店名:新宿支店 口座番号:{US_BANK_ACCOUNT_NUMBER} Bank Name: Mizuho Bank Branch: {ADDRESS} Branch Account Number: {US_BANK_ACCOUNT_NUMBER} llm-guardのマスキング結果は、前回記事のデフォルト設定時のものです。 一部誤検出や期待通りに検出されていないものもありますが、概ね期待通りの結果が得られました。 結果詳細(誤検出・検出漏れ項目の抜粋) 社会保障番号:987-65-4321 Social Security Number: {US_SOCIAL_SECURITY_NUMBER} 英語版は適切にマスキングされましたが、日本語版においては英語版と同様のフォーマットを渡していますが、マスキングされませんでした。 本項目については、米国向けのフィルターとして準備されているため、日本語の文脈では対応できない可能性があります。 銀行口座情報: 銀行名:みずほ銀行 支店名:新宿支店 口座番号:{US_BANK_ACCOUNT_NUMBER} Bank Account Information: Bank Name: Mizuho Bank Branch: {ADDRESS} Branch Account Number: {US_BANK_ACCOUNT_NUMBER} 英語版の支店名が住所として誤検出されていました。 今回は文脈を含まない情報でテストしましたが、実際の自然な文脈中で銀行支店名が出現した場合の挙動は別途検証が必要です。 今回の検証では、口座番号は正常に検出されていますが、利用したフィルターは米国の銀行口座番号として準備されています。実際に利用する際は、日本の形式に適合するか、もう少し検証してみる必要はありそうです。 まとめ 今回はGuardrails for Amazon BedrockのPIIマスキング機能について検証しました。 テストの結果、概ね Guardrails for Amazon Bedrock で PII の除去が出来ました。今回の検証では、限定的な条件での確認となるため、実際にアプリケーションで利用する際は、自然なテキスト中に含まれている場合や、全角文字が混在する場合など、日本語特有の表現が含まれている場合の挙動については、さらに詳細な検証が必要となります。 一方、現状ではカバーしきれていない情報もいくつかは残っています。 しかし、フィルターが準備されていない生年月日や、検知が上手くいかなかった社会保障番号(類似する情報であるマイナンバー)においては、ある程度フォーマットが定まっている情報になります。これらに対しては、正規表現フィルターを併用することで、PII除去の確実性を高められるでしょう。 検討課題 今回の検証では Langfuse の mask オプションを使用して実装しましたが、運用を考慮すると、今回の実装が適切とは限りません。 mask オプションは Langfuse にデータが渡されるたびに実行されるため、APIコール数が想定以上に増加し、コストに影響する可能性があります。実際、llm-guard のようなツールとは異なり、今回利用した機密情報フィルターについては、 Bedrockの料金ページ に記載の通り、1,000テキストユニットあたり0.10 USD発生します。(2025年7月時点) また、mask オプションを利用することで、エラーハンドリングが複雑になってしまう側面もあります。API エラーが発生した場合に Langfuse のトレース処理全体に影響を及ぼす懸念も無視できません。 そのため、Langfuse のトレース保存前に、アプリケーションコード内で明示的に apply_guardrail を呼び出し、その結果を input や output として Langfuse に渡す方法など、様々な実装アプローチを比較・検討することをおすすめします。
- Langfuseにおける個人情報(PII)のマスキング
LangfuseにおけるPIIマスキングの必要性 チャットボットのようなアプリケーションでは、ユーザーが意図せず個人情報(PII)を入力してしまう可能性があります。 個人情報保護 の観点から、これらの情報がLangfuseのトレースにそのまま出力されるのは望ましくありません。 そこで、トレース上で 個人情報 をマスキングした状態で確認できるよう、どのような手段が考えられるか検証しました。 個人情報(PII)の定義と具体例 具体的に 個人情報(PII) に該当する項目は、一般的に以下のものが挙げられます。 氏名 住所 電話番号 メールアドレス 顔写真 身分証番号(運転免許証、パスポートなど) 生年月日 社会保障番号 クレジットカード情報 銀行口座情報 今回はテキスト入力ベースのアプリケーションを想定しているため、顔写真のような画像データは検証対象外とします。 LangfuseにおけるPIIマスキング手法の検討 Langfuseの公式サイトで紹介されている Masking of Sensitive LLM Data を参考に、マスキング手法を検討します。 氏名や住所は正規表現での対応が難しいため、今回は Example 2 で紹介されている llm-guard を試してみました。 氏名や住所のような複雑な情報は正規表現での対応が難しいため、今回はExample 2で紹介されている llm-guard を試用しました。 今回試用した llm-guard のAnonymize機能は、現在以下の個人情報(PII)の検出に対応しています。 クレジットカード 人名 電話番号 URL メールアドレス IPアドレス UUID 米国社会保障番号(SSN) 暗号資産ウォレット番号 IBANコード 今回マスキング対象として期待している項目は以下の通りです。 氏名 生年月日 住所 電話番号 メールアドレス 運転免許証番号 パスポート番号 社会保障番号 クレジットカード情報 カード番号 有効期限 セキュリティコード 銀行口座情報 銀行名 支店名 口座番号 PIIマスキングのテスト方法 簡単なコードを用いて、プロンプトの入力とLLMの回答を模擬的に生成し、それぞれをLangfuseに登録する形式でテストを行いました。 アプリケーションコード 今回の検証では、PythonアプリケーションからLangfuseにトレースを投入するケースを対象としています。マスキング処理は、Langfuseの初期化時にmaskオプションに対して、処理を行う関数を設定することで実現できます。 vault = Vault() def create_anonymize_scanner(): scanner = Anonymize( vault=vault, ) return scanner def masking_function(data: any, **kwargs) -> any: if isinstance(data, str): scanner = create_anonymize_scanner() sanitized_data, is_valid, score = scanner.scan(data) return sanitized_data return data # テスト用の入力テキスト input_text = """[[ダミーデータを含むプロンプト]]""" # Langfuseの初期化 langfuse = Langfuse( public_key="xxxxxxxxxxxxxxxxxxxxxxxx", secret_key="xxxxxxxxxxxxxxxxxxxxxxxx", host="http://xxxxxxxxxxxxxxxx", mask=masking_function, ) # LLM呼び出しのトレース with langfuse.start_as_current_generation( name=f"test_step", input=input_text, ) as generation: # LLMからの実際の出力を格納 response_content = input_text # 現在はダミーデータを使用 generation.update(output=response_content) テストデータ テスト用のプロンプトとLLMの出力結果として、Cursorを用いて上記の個人情報(PII)具体例を含むダミーデータを以下の通り生成しました。インプットとアウトプットの双方に個人情報が含まれるとトレース上での問題となるため、今回は同じテストデータを利用し、両方のデータにマスキングが適用されるかを確認します。 私の個人情報は以下の通りです: 氏名:山田 太郎 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 最低限の引数での動作確認 上記のコードを実行したところ、何らかの置換が行われていることが確認できました。 検証結果:インプットとアウトプットの比較 インプットの結果をpt1_input.txt、アウトプットの結果をpt1_output.txt として比較しました。 inputとoutputのいずれに渡した場合でも、マスキング処理が適応されていることが確認できました。 検証結果:各個人情報の検出状況 プロンプトとして与えた各個人情報について、どのように処理されたかひとつずつ確認していきます。 氏名:△ 氏名:[REDACTED_PERSON_1][REDACTED_PERSON_2][REDACTED_PERSON_3] Name: [REDACTED_PERSON_4] 検知・置換自体は行われていますが、日本語名が意図せず三分割されている点が課題です。 生年月日:× 住所:× これらは llm-guard の現在の検出対象外のため、想定通りの結果です。 電話番号: × 電話番号:03-1234-[REDACTED_PHONE_NUMBER_1] Phone: [REDACTED_PHONE_NUMBER_2] 2種類の電話番号を「/」区切りで記載していましたが、一つの電話番号とみなされているようです。いずれにしても、日本の電話番号形式への対応はまだ課題があります。 メールアドレス:○ 運転免許証番号:× パスポート番号:× 社会保障番号:○ これらの項目については、当初の想定通りの検出結果となりました(運転免許証番号、パスポート番号は検出対象外のため×、メールアドレス、社会保障番号は検出対象のため○)。 クレジットカード情報:△ クレジットカード情報: カード番号:[REDACTED_CREDIT_CARD_RE_1] 有効期限:12/25 セキュリティコード:123 カード番号は問題なく検出できましたが、有効期限やセキュリティコードはマスキングされませんでした。 ドキュメント ではVisa、American Express、Diners Clubに対応とあるため、他のカード会社への対応状況も確認が必要です。 銀行口座情報:× こちらも llm-guard の現在の検出対象外のため、想定通りの結果です。 各種設定や検出モデルの変更による検証 オプションの設定や検出に利用するモデルを変更することにより、対応している項目に関しては精度が向上する可能性があると考え、合わせて確認してみました。 日本語設定の試行 Anonymizeの言語設定は標準では英語となっています。こちらを日本語に変更可能か試行しました。 しかし、 llm-guard が現在サポートしているのは英語('en')と中国語('zh')のみであることが確認されました。このため、日本語の PII 検出において、言語設定によるアプローチは現状では利用できません。 recognizer_confの変更による比較検証 Anonymizeでは、recognizer_confパラメータで検出モデルを指定できます。コードを以下のように変更し、llm_guard.input_scanners.anonymize_helpers で定義されているモデルを順に試行します。 scanner = Anonymize( vault=vault, recognizer_conf=[[ ここの値を変更 ]] ) 定義されているモデルは以下の7種類になります。 BERT_BASE_NER_CONF(dslim/bert-base-NER) BERT_LARGE_NER_CONF(dslim/bert-large-NER) BERT_ZH_NER_CONF(gyr66/bert-base-chinese-finetuned-ner) DISTILBERT_AI4PRIVACY_v2_CONF(Isotonic/distilbert_finetuned_ai4privacy_v2) DEBERTA_AI4PRIVACY_v2_CONF(Isotonic/deberta-v3-base_finetuned_ai4privacy_v2) MDEBERTA_AI4PRIVACY_v2_CONF(Isotonic/mdeberta-v3-base_finetuned_ai4privacy_v2) DEBERTA_LAKSHYAKH93_CONF(lakshyakh93/deberta_finetuned_pii) 各モデルにおいて、先に試行したデフォルト設定時の差分に重点を置いて確認していきます。 デフォルト設定時にマスキングされなかったものについては、「-」で表示しています。 (マスキングの数値のみが異なっている場合は、同様の検出が行われたと判断しています) 氏名(日本語) 氏名(英語) 生年月日 住所 電話番号(日本) 電話番号(海外) メールアドレス 運転免許証番号 パスポート番号 社会保障番号 クレジットカード(カード番号) クレジットカード(カード番号以外) 銀行口座情報 デフォルト設定(参考) [REDACTED_PERSON_1][REDACTED_PERSON_2][REDACTED_PERSON_3] [REDACTED_PERSON_4] - - 03-1234-[REDACTED_PHONE_NUMBER_1] [REDACTED_PHONE_NUMBER_2] [REDACTED_EMAIL_ADDRESS_1] - - [REDACTED_US_SSN_RE_1] [REDACTED_CREDIT_CARD_RE_1] - - BERT_BASE_NER_CONF 山田 太郎 [REDACTED_PERSON_1][REDACTED_PERSON_2]mada - - 03-1234-5678 / 090-9999-8888 [REDACTED_PHONE_NUMBER_1] / [REDACTED_PHONE_NUMBER_2] [REDACTED_EMAIL_ADDRESS_1] - - [REDACTED_US_SSN_RE_1] [REDACTED_CREDIT_CARD_RE_1] - - BERT_LARGE_NER_CONF 山田 太郎 [REDACTED_PERSON_3] - - 03-1234-5678 / 090-9999-8888 [REDACTED_PHONE_NUMBER_1] / [REDACTED_PHONE_NUMBER_2] [REDACTED_EMAIL_ADDRESS_1] - - [REDACTED_US_SSN_RE_1] [REDACTED_CREDIT_CARD_RE_1] - - BERT_ZH_NER_CONF [REDACTED_PERSON_4] Taro Yamada - - 03-1234-5678 / 090-9999-8888 [REDACTED_PHONE_NUMBER_1] / [REDACTED_PHONE_NUMBER_2] [REDACTED_EMAIL_ADDRESS_1] - - [REDACTED_US_SSN_RE_1] [REDACTED_CREDIT_CARD_RE_1] - - DISTILBERT_AI4PRIVACY_v2_CONF 山田 太郎 Taro Yamada - - 03[REDACTED_PHONE_NUMBER_3] [REDACTED_PHONE_NUMBER_1] / [REDACTED_PHONE_NUMBER_2] [REDACTED_EMAIL_ADDRESS_1] 運転免許証番号:[REDACTED_PHONE_NUMBER_4] Driver's License: DL-[REDACTED_PHONE_NUMBER_5]12 - [REDACTED_US_SSN_RE_1] [REDACTED_CREDIT_CARD_RE_1] - - DEBERTA_AI4PRIVACY_v2_CONF [REDACTED_PERSON_5][REDACTED_PERSON_6][REDACTED_PERSON_7] [REDACTED_PERSON_3] - - 03-1234-[REDACTED_PHONE_NUMBER_6] [REDACTED_PHONE_NUMBER_7] [REDACTED_EMAIL_ADDRESS_1] - - [REDACTED_US_SSN_RE_1] [REDACTED_CREDIT_CARD_RE_1] - - MDEBERTA_AI4PRIVACY_v2_CONF [REDACTED_PERSON_8] 太郎 Taro [REDACTED_PERSON_9] - - [REDACTED_PHONE_NUMBER_8] / [REDACTED_PHONE_NUMBER_9] [REDACTED_PHONE_NUMBER_1] / [REDACTED_PHONE_NUMBER_2] [REDACTED_EMAIL_ADDRESS_1] - - [REDACTED_US_SSN_RE_1] [REDACTED_CREDIT_CARD_1] - - DEBERTA_LAKSHYAKH93_CONF 山田 太郎 [REDACTED_PERSON_1][REDACTED_PERSON_2][REDACTED_PERSON_3] [REDACTED_IBAN_CODE_1] [REDACTED_CRYPTO_2] 03-1234-5678 / 090-9999-8888 [REDACTED_PHONE_NUMBER_1] / [REDACTED_PHONE_NUMBER_2] [REDACTED_EMAIL_ADDRESS_1] Email: [REDACTED_EMAIL_ADDRESS_2] - [REDACTED_IBAN_CODE_2] Passport Number: TK1234567 社会[REDACTED_IBAN_CODE_3] Social Security Number: [REDACTED_US_SSN_RE_1] [REDACTED_CREDIT_CARD_RE_1] - [REDACTED_CRYPTO_3]- [REDACTED_CRYPTO_4] [REDACTED_IP_ADDRESS_2] 各モデルの比較検証結果 BERT_BASE_NER_CONF 氏名:山田 太郎 Name: [REDACTED_PERSON_1][REDACTED_PERSON_2]mada 日本語氏名はマスキングされず、英語氏名は部分的にマスキングされる形になりました。 電話番号:03-1234-5678 / 090-9999-8888 Phone: [REDACTED_PHONE_NUMBER_1] / [REDACTED_PHONE_NUMBER_2] 電話番号も全てマスキングされない結果です。 ただし、海外向けフォーマットにおいては、適切に2つ分として判断されているようです。 BERT_LARGE_NER_CONF 氏名:山田 太郎 Name: [REDACTED_PERSON_3] 日本語氏名はマスキングされませんでしたが、英語氏名は綺麗にマスキングされました。 電話番号:03-1234-5678 / 090-9999-8888 Phone: [REDACTED_PHONE_NUMBER_1] / [REDACTED_PHONE_NUMBER_2] 電話番号については、BERT_BASE_NER と同じ形になりました。 BERT_ZH_NER_CONF 氏名:[REDACTED_PERSON_4] Name: Taro Yamada 日本語氏名はマスキングされましたが、英語氏名はマスキングされませんでした。 中国語のNERモデルとなるため、漢字はうまく判別出来ているのかもしれません。 実際に採用できるかは、名前がひらがなのケースも一度確認する必要がありそうです。 電話番号:03-1234-5678 / 090-9999-8888 Phone: [REDACTED_PHONE_NUMBER_1] / [REDACTED_PHONE_NUMBER_2] こちらも電話番号については、BERT_BASE_NER と同じ形になりました。 DISTILBERT_AI4PRIVACY_v2_CONF 氏名:山田 太郎 Name: Taro Yamada 日本語、英語共にマスキングされていません。 電話番号:03[REDACTED_PHONE_NUMBER_5] Phone: [REDACTED_PHONE_NUMBER_1] / [REDACTED_PHONE_NUMBER_2] 日本国内向けの電話番号はまだ適切に検知できていないようですが、こちらも海外向けフォーマットにおいては、適切に2つ分として判断されているようです。 運転免許証番号:[REDACTED_PHONE_NUMBER_6] Driver's License: DL-[REDACTED_PHONE_NUMBER_7]12 電話番号として誤検出され、部分的にマスキングされています。 英語では末尾2文字がマスキングされておらず、日本語と英語で検出範囲が異なっているのも気になる点です。 DEBERTA_AI4PRIVACY_v2_CONF 置換後の数値に差異はあるものの、デフォルトで指定されているモデルのため、同じ形でマスキングされていました。 MDEBERTA_AI4PRIVACY_v2_CONF 氏名:[REDACTED_PERSON_8] 太郎 Name: Taro [REDACTED_PERSON_9] 日本語氏名、英語氏名共に姓のみマスキングされています。 電話番号:[REDACTED_PHONE_NUMBER_8] / [REDACTED_PHONE_NUMBER_9] Phone: [REDACTED_PHONE_NUMBER_1] / [REDACTED_PHONE_NUMBER_2] 電話番号に関しては、国内・海外向けフォーマットともにマスキングされています。 DEBERTA_LAKSHYAKH93_CONF 他のモデルに比べ、誤検知が多く発生してます。 まず、1行目に記載している下記の文言ですが 私の個人情報は以下の通りです 以下の通り誤検知・置換されていました。 私の個人情報[REDACTED_CRYPTO_1]下の通りです: その他、各項目についても以下の通り誤検出が多くなっていました。 氏名:山田 太郎 Name: [REDACTED_PERSON_1][REDACTED_PERSON_2][REDACTED_PERSON_3] 日本語氏名がマスキングされておらず、英語氏名も3分割で検知されています。 [REDACTED_IBAN_CODE_1] 生年月日がIBANコードとして誤検出されています。 [REDACTED_CRYPTO_2] 住所が暗号通貨のウォレット番号として誤検出されています。 電話番号:03-1234-5678 / 090-9999-8888[REDACTED_IP_ADDRESS_1] [REDACTED_PHONE_NUMBER_1] / [REDACTED_PHONE_NUMBER_2] 日本語の番号がマスキングされていません。 海外向けフォーマットについては適切に検出されているように見えますが、関係のない「Phone:」がIPアドレスとして誤検出されています。 [REDACTED_EMAIL_ADDRESS_1] Email: [REDACTED_EMAIL_ADDRESS_2] 「メールアドレス:」部分もメールアドレスとして誤検出されています。 Driver's License: DL-123456789012[REDACTED_IBAN_CODE_2] Passport Number: TK1234567 「パスポート番号:TK1234567」がIBANコードとして誤検出されています。 社会[REDACTED_IBAN_CODE_3] 社会保障番号が文言の途中からIBANコードとして誤検出されています。 [REDACTED_CRYPTO_3]- [REDACTED_CRYPTO_4] [REDACTED_IP_ADDRESS_2] 日本語の銀行口座情報が暗号通貨のウォレット番号、IPアドレスとして誤検知されています。 まとめ 今回の検証では、llm-guardを用いたPIIマスキングの可能性を探りました。特に日本語の個人情報検出においては、現状では他の手法との併用や、より高精度なモデルの検証が必要となりそうです。
- LangfuseのExperiments Compare ViewのBaseline機能を解説
はじめに LLMアプリケーションの開発において、プロンプトの改善は避けて通れない作業です。しかし、プロンプトを変更するたびに、こんな不安を感じたことはありませんか? このプロンプト変更、本当に改善になっているのか? 一部のケースで良くなったけど、他のケースで悪化していないか? 前のバージョンと比べて、どれくらい良くなったのか数字で示せない... LangfuseのBaseline機能を使えば、変更前後の結果を定量的に比較し、改善した点と品質が下がった点を一目で把握できます。 本記事では、「京都の観光案内ボット」を題材に、感覚ではなくデータに基づいてプロンプトを改善する手順を紹介します。 本記事でわかること LangfuseのExperiments機能の基本的な使い方 Baseline機能を使ったプロンプトの比較・評価手順 Compare Viewでのリグレッション検出方法 前提・想定対象読者 LLMアプリケーションの開発経験がある方 プロンプトの品質管理・改善に課題を感じている方 LangfuseのExperiments機能を使ったプロンプトの比較・評価手順を知りたい方 Langfuse Experimentsとは Experiments機能の役割 LangfuseのExperiments機能は、プロンプトやモデル設定を変えたときに、出力が「良くなったか・悪くなったか」を、データセットを用いて定量的にテスト・比較できる機能です。具体的には以下のことが可能になります。 データセット管理 : テストケース(入力と期待される出力)を一元管理 実験の実行 : 同じデータセットに対して、異なるプロンプトやモデルで実験を実行 評価(スコアリング) : SDKの `evaluators` でスコアを付与、またはLangfuse上のEvaluator(LLM-as-a-Judge等)で評価を実行 結果の比較 : 複数の実験結果を並べて比較 Baseline機能の登場(2025年11月リリース) 従来のExperiments機能でも複数の実験結果を比較できましたが、「どれが基準(Baseline)なのか」が明示されていませんでした。 Langfuse v3.125.0でBaseline機能が追加され、以下が可能になりました。 基準バージョンの明示 : 現在の本番環境や、比較の基準となる実験を「Baseline」として指定 差分の可視化 : Baseline と Candidate(比較対象)の差分を緑(改善)・赤(悪化)で色分け表示 リグレッションの検出 : フィルター機能により、スコアが悪化した項目だけを抽出して確認 これにより、「新しいプロンプトは全体的には良いが、特定のケースで品質が低下している」といった状況を視覚的に素早く発見できるようになりました。 実務での活用シーン Baseline機能は以下のようなシーンで特に有効です。 プロンプトの改善 : 新しいプロンプトが既存バージョンより優れているか検証 モデルの変更 : OpenAI GPT からGeminiへの移行など、モデル変更の影響を評価 継続的な品質管理 : 定期的に実験を実行し、品質の推移をトラッキング A/Bテストの事前検証 : 本番投入前に、複数のバリエーションを比較 次のセクションでは、プロンプトの変更によるリグレッションを検出するために、実際にコードを書いて実験を実行してみましょう。 実装 今回のシナリオ 「京都の観光案内ボット」を題材に、以下の2つのプロンプトバージョンを比較します。 プロンプトV1(Baseline) : 標準的で真面目なトーン プロンプトV2(Candidate) : 親しみやすく絵文字を使うトーン 目的は、「親しみやすさを向上させつつ、正確性を維持できるか」を検証することです。 以下の流れで実装していきます。 Dataset(評価用データ)の作成 ↓ Baseline実験(Prompt V1) ↓ Candidate実験(Prompt V2) ↓ Evaluator(accuracy, emoji_count) ↓ Compare View(差分・リグレッション可視化) Tips: 本番運用では、 Prompt Management機能 でプロンプトを管理するのがおすすめです(コードのデプロイなしで更新・バージョニングできます)。 ディレクトリ構成 今回のサンプルコードのディレクトリ構成は以下の通りです。 langfuse-experiments-demo/ ├── src/ │ ├── data/ │ │ └── kyoto_tourism_dataset.json # テストデータセット │ ├── run_experiment.py # 実験スクリプト │ ├── upload_dataset.py # データセットアップロード │ └── .env # 環境変数 └── pyproject.toml # 依存関係定義 環境セットアップ 今回は uv を使ってPython環境を構築します。 UV環境設定ファイル作成 UVの環境設定ファイル`pyproject.toml` を作成します。 [project] name = "langfuse-experiments-demo" version = "0.1.0" requires-python = ">=3.10" dependencies = [ "langfuse>=3.0.0", "google-genai>=1.0.0", "python-dotenv>=1.0.0", ] 依存関係インストール uvで依存関係をインストールします。 # uvのインストール(まだの場合) curl -LsSf https://astral.sh/uv/install.sh | sh # プロジェクトディレクトリに移動 cd langfuse-experiments-demo # 仮想環境を作成し、依存関係をインストール uv sync 環境変数設定 環境変数を設定します(`src/.env`ファイルを作成)。 # Langfuse設定 LANGFUSE_PUBLIC_KEY=pk-lf-... LANGFUSE_SECRET_KEY=sk-lf-... LANGFUSE_BASE_URL=https://cloud.langfuse.com # Google Cloud / Vertex AI設定 GOOGLE_CLOUD_PROJECT=your-project-id GOOGLE_CLOUD_LOCATION=asia-northeast1 Google Cloud / Vertex AI の認証 Vertex AI(Gemini)を呼び出すために、Google Cloud の Application Default Credentials(ADC)を実行します。 # Application Default Credentials を設定 gcloud auth application-default login # プロジェクトを設定 gcloud config set project your-project-id 以上で環境構築完了です。 データセットの準備 テストケースを含むJSONファイルを作成します。以下のようなファイルを`src/data/kyoto_tourism_dataset.json` に保存します。 [ { "input": "京都の有名な観光地を3つ教えてください。", "expected_output": "清水寺、金閣寺、伏見稲荷大社などが有名です。", "metadata": { "category": "general_spots", "difficulty": "easy" } }, { "input": "京都で紅葉が綺麗な場所はどこですか?", "expected_output": "東福寺、永観堂、嵐山などが紅葉の名所として知られています。", "metadata": { "category": "seasonal", "difficulty": "medium" } } ] データセットのアップロード用コード Langfuseにデータセットをアップロードするスクリプトを作成します。以下のコードを `src/upload_dataset.py` に保存します。 コードが長くなるのでセクションに折り畳んでいます。コード内容はセクションを展開して確認してください。 src/upload_dataset.py """ Langfuseにデータセットを登録するスクリプト """ import json import os from dotenv import load_dotenv from langfuse import get_client # 環境変数を読み込み load_dotenv() # Langfuseクライアントの初期化 langfuse = get_client() DATASET_NAME = "kyoto-tourism-qa" DATASET_DESCRIPTION = "京都観光案内ボットの評価用データセット" def upload_dataset(): """Langfuseにデータセットをアップロード""" # ローカルデータセットの読み込み with open('data/kyoto_tourism_dataset.json', 'r', encoding='utf-8') as f: items = json.load(f) # データセットを作成(既に存在する場合はそのまま使用) try: langfuse.create_dataset( name=DATASET_NAME, description=DATASET_DESCRIPTION, metadata={"version": "1.0", "language": "ja"} ) print(f" データセット '{DATASET_NAME}' を作成しました") except Exception as e: print(f" データセット '{DATASET_NAME}' は既に存在します") # データセットアイテムを登録 for item in items: langfuse.create_dataset_item( dataset_name=DATASET_NAME, input=item["input"], expected_output=item["expected_output"], metadata=item.get("metadata", {}) ) print(f" {len(items)}件のアイテムを登録しました") if __name__ == "__main__": upload_dataset() 実験スクリプトの実装 Langfuse Python SDK V3の `dataset.run_experiment()` を使って、Baseline実験とCandidate実験の両方を実行できるスクリプトを実装します。 以下のコードを `src/run_experiment.py` に保存します。 コードが長くなるのでセクションに折り畳んでいます。コード内容はセクションを展開して確認してください。 `src/run_experiment.py """ 京都観光案内ボットの実験スクリプト Baseline(V1)と Candidate(V2)の両方を実行可能 Usage: uv run python src/run_experiment.py baseline # Baseline実験を実行 uv run python src/run_experiment.py candidate # Candidate実験を実行 """ import os import sys import json import re from dotenv import load_dotenv from langfuse import get_client, Evaluation from google import genai from google.genai import types # 環境変数を読み込み load_dotenv() # Langfuseクライアントの初期化 langfuse = get_client() # Google Gen AI SDK クライアントの初期化 project_id = os.getenv("GOOGLE_CLOUD_PROJECT") location = os.getenv("GOOGLE_CLOUD_LOCATION", "us-central1") client = genai.Client(vertexai=True, project=project_id, location=location) # モデル名 MODEL_NAME = "gemini-2.5-flash" # データセット名 DATASET_NAME = "kyoto-tourism-qa" # ================================================================ # プロンプト定義 # ================================================================ # プロンプトV1: 標準的で真面目なトーン(Baseline) PROMPT_V1 = """あなたは京都の観光案内を専門とするアシスタントです。 正確で丁寧な情報提供を心がけてください。 ユーザーの質問: {question} 上記の質問に対して、正確かつ簡潔に回答してください。""" # プロンプトV2: 親しみやすく絵文字を使うトーン(Candidate) PROMPT_V2 = """あなたは京都の観光案内を専門とするアシスタントです。 正確で丁寧な情報提供を基本としつつ、親しみやすい雰囲気で案内してください。 回答のポイント: - 正確性を最優先に、信頼できる情報を提供する - 適度に絵文字を添えて、読みやすく親しみやすい印象にする(1〜2個/段落程度) - 「です・ます」調で丁寧に、かつ堅すぎない自然な文体で ユーザーの質問: {question} 上記の質問に対して、正確かつ親しみやすく回答してください。""" # ================================================================ # タスク関数 # ================================================================ async def baseline_task(*, item, **kwargs): """Baseline実験のタスク関数(プロンプトV1を使用)""" question = item.input if hasattr(item, 'input') else item.get("input") prompt = PROMPT_V1.format(question=question) # generationとして記録(コスト計算のためにusage_detailsを設定) with langfuse.start_as_current_observation( as_type="generation", name="gemini-generation", model=MODEL_NAME, input=prompt ) as generation: response = await client.aio.models.generate_content( model=MODEL_NAME, contents=prompt, config=types.GenerateContentConfig( temperature=0.7, max_output_tokens=2048 ) ) # usage_detailsを設定(コスト計算に必要) generation.update( output=response.text, usage_details={ "input": response.usage_metadata.prompt_token_count, "output": response.usage_metadata.candidates_token_count, } ) return response.text async def candidate_task(*, item, **kwargs): """Candidate実験のタスク関数(プロンプトV2を使用)""" question = item.input if hasattr(item, 'input') else item.get("input") prompt = PROMPT_V2.format(question=question) # generationとして記録(コスト計算のためにusage_detailsを設定) with langfuse.start_as_current_observation( as_type="generation", name="gemini-generation", model=MODEL_NAME, input=prompt ) as generation: response = await client.aio.models.generate_content( model=MODEL_NAME, contents=prompt, config=types.GenerateContentConfig( temperature=0.7, max_output_tokens=2048 ) ) # usage_detailsを設定(コスト計算に必要) generation.update( output=response.text, usage_details={ "input": response.usage_metadata.prompt_token_count, "output": response.usage_metadata.candidates_token_count, } ) return response.text # ================================================================ # 評価関数(共通) # ================================================================ async def accuracy_evaluator(*, output, expected_output, **kwargs): """正確性評価: LLMを使って意味的な正確性を評価(LLM as a Judge)""" if expected_output is None: return Evaluation(name="accuracy", value=0.0, comment="期待される出力がないため評価不可") judge_prompt = f"""あなたは回答の正確性を評価する審査員です。 以下の「期待される回答」と「実際の回答」を比較し、実際の回答が期待される内容を正確にカバーしているか評価してください。 【期待される回答】 {expected_output} 【実際の回答】 {output} 【評価基準】 - 1.0: 期待される内容を完全にカバーしている - 0.7-0.9: 主要な内容はカバーしているが、一部欠けている - 0.4-0.6: 部分的にカバーしているが、重要な情報が欠けている - 0.1-0.3: ほとんどカバーできていない - 0.0: 全く関係ない回答 以下のJSON形式のみで回答してください: {{"score": 0.0〜1.0の数値, "reason": "評価理由を簡潔に"}} """ try: response = await client.aio.models.generate_content( model=MODEL_NAME, contents=judge_prompt, config=types.GenerateContentConfig( temperature=0.0, max_output_tokens=2048 ) ) response_text = response.text.strip() json_match = re.search(r'\{[^{}]*\}', response_text) result = json.loads(json_match.group()) if json_match else json.loads(response_text) score = max(0.0, min(1.0, float(result.get("score", 0.0)))) reason = result.get("reason", "理由なし") return Evaluation(name="accuracy", value=score, comment=reason) except Exception as e: return Evaluation(name="accuracy", value=0.0, comment=f"評価エラー: {str(e)}") def emoji_count_evaluator(*, output, **kwargs): """絵文字の使用数を評価""" emoji_count = sum(1 for char in output if ord(char) > 0x1F300) if output else 0 return Evaluation( name="emoji_count", value=emoji_count, comment=f"絵文字を{emoji_count}個使用" ) # ================================================================ # 実験実行関数 # ================================================================ def run_baseline_experiment(): """Baseline実験を実行""" dataset = langfuse.get_dataset(DATASET_NAME) result = dataset.run_experiment( name="Kyoto Tourism Bot - Prompt Comparison", run_name="baseline-v1", description="標準的で真面目なトーンのプロンプト(Baseline)", task=baseline_task, evaluators=[accuracy_evaluator, emoji_count_evaluator], metadata={"prompt_version": "v1", "model": MODEL_NAME} ) print(result.format()) langfuse.flush() return result def run_candidate_experiment(): """Candidate実験を実行""" dataset = langfuse.get_dataset(DATASET_NAME) result = dataset.run_experiment( name="Kyoto Tourism Bot - Prompt Comparison", run_name="candidate-v2", description="親しみやすく絵文字を使うトーンのプロンプト(Candidate)", task=candidate_task, evaluators=[accuracy_evaluator, emoji_count_evaluator], metadata={"prompt_version": "v2", "model": MODEL_NAME} ) print(result.format()) langfuse.flush() return result # ================================================================ # メイン # ================================================================ if __name__ == "__main__": if len(sys.argv) < 2: print("Usage: python run_experiment.py [baseline|candidate]") sys.exit(1) experiment_type = sys.argv[1].lower() if experiment_type == "baseline": run_baseline_experiment() elif experiment_type == "candidate": run_candidate_experiment() else: print(f"Unknown experiment type: {experiment_type}") print("Usage: python run_experiment.py [baseline|candidate]") sys.exit(1) 実装のポイント 1. `dataset.run_experiment()` の活用 Langfuse Python SDK V3では、`dataset.run_experiment()` という高レベルメソッドが提供されています。以下のように、実験を実行することができます。 result = dataset.run_experiment( name="Experiment Name", # 実験名 run_name="baseline-v1", # Run名 task=baseline_task, # 各項目に対して実行する関数 evaluators=[ .. ], # Item-level評価関数 metadata={ .. } # メタデータ ) 2. 非同期タスクと評価関数 `run_experiment()` はタスク関数と評価関数の両方で非同期(`async def`)をサポートしています。LLM APIなどを呼び出す処理は非同期処理にすることで、並列実行時の効率が向上します。 # 非同期タスク関数 async def baseline_task(*, item, **kwargs): response = await client.aio.models.generate_content( model=MODEL_NAME, contents=prompt, config=types.GenerateContentConfig(...) ) return response.text # 非同期評価関数(LLM-as-a-Judge) async def accuracy_evaluator(*, output, expected_output, **kwargs): response = await client.aio.models.generate_content( model=MODEL_NAME, contents=judge_prompt, config=types.GenerateContentConfig(...) ) return Evaluation(name="accuracy", value=score, comment=reason) 3. 評価関数 `dataset.run_experiment()` の `evaluators` 引数には、評価関数をリストで指定します。 ここで指定する評価関数は、各データセット項目の出力を個別に評価する関数です。 今回の例では、正確性評価関数(`accuracy_evaluator`)と絵文字使用数評価関数(`emoji_count_evaluator`)を使用しています。 実験の実行 スクリプトを実行します。 # データセットをアップロード uv run python src/upload_dataset.py # Baseline実験を実行 uv run python src/run_experiment.py baseline # Candidate実験を実行 uv run python src/run_experiment.py candidate 実行が完了したら、Langfuse UIにアクセスして結果を確認しましょう。次のセクションでは、Baseline機能を使った比較方法を詳しく解説します。 検証編 - Baseline機能で結果を比較する Compare Viewへのアクセス Langfuseにログイン Datasets メニューから `kyoto-tourism-qa` をクリック `baseline-v1` と `candidate-v2` の両方にチェックを入れる Actions ボタン - Compare ボタンをクリック Baselineの設定 Compare View が開いたら、片方の実験を「Baseline」として指定します。 1. `baseline-v1` の表示横の`Set as Baseline`をクリック これで、```candidate-v2` が Baseline との差分として表示されるようになります。 結果の見方 Delta(差分)表示 緑色 : Baseline より改善された項目 赤色 : Baseline より悪化した項目(リグレッション) 灰色 : 変化なし 今回の実験結果 筆者の実行結果では、一部の項目で `accuracy` スコアが下がっていることがわかりました。 Compare Viewでは、Datasetsの `Input`、`Expected Output`、バージョンごとの `Output` およびスコアを確認できます。 評価スコアのコメントアイコンにマウスオーバーすると、LLM-as-a-Judgeがスコアを付けた根拠を確認できます。 また、出力の右下にマウスオーバーするとトレース確認用のアイコンが表示されます。クリックするとトレース画面がオーバーレイ表示され、詳細を確認できます。 このように、評価結果のコメントやトレース詳細を確認しながら、リグレッションの有無等のプロンプト変更の影響を分析できます。 従来もCompare Viewで結果を横並びで見比べることはできましたが、Baseline機能により評価スコアやLatency、Costなどの差分が視覚的に把握しやすくなり、分析が容易になりました。 まとめ プロンプトの改善は「感覚」ではなく「データ」で判断する時代です。Baseline機能を活用して、自信を持ってプロンプトの改善サイクルを促進しましょう。 Baseline機能のメリット 定量的な比較 : 「なんとなく良くなった」ではなく、数値で改善・悪化を判断 リグレッションの早期発見 : 特定のケースでの品質低下を見逃さない 意思決定の根拠 : プロンプト変更の採用可否を、データに基づいて判断 実務での活用ポイント 本番投入前の必須プロセス : プロンプト変更時は必ずBaseline比較を実施 継続的な品質モニタリング : 定期的に実験を実行し、品質の推移を追跡 チーム内でのコミュニケーション : Langfuse UIを共有して、品質議論の土台に 参考リンク Langfuse Experiments via SDK Langfuse Changelog - Compare View Baseline Support
- Langfuse に機能を追加してマージされるまで:日本語コントリビュートガイド
LLM オブザーバビリティプラットフォーム「Langfuse」に機能を追加して、PR がマージされるまでの過程を紹介します。環境構築でハマったポイントや解決方法もまとめているので、日本語でのコントリビュートガイドとしてもお使いください。 この記事で得られること Langfuse 開発環境のセットアップ手順 実際に遭遇したエラーと解決法 セルフホスト/クラウドモードの違いと切り替え方 PR 作成時のベストプラクティス コントリビュートの背景 セルフホスト環境で Langfuse を使っていて、Vertex AI の認証に Application Default Credentials(ADC) を使いたかったのですが、その機能がありませんでした。AWS Bedrock には既に ADC 対応があったので、Vertex AI にも同様の機能を実装することにしました。 成果 : PR #11039 がv3.140.0でマージされました 🎉 (私の PR #10915 が取り込まれたもの) まずは公式ドキュメントを読もう Langfuse には充実した コントリビュートガイド があります。必要なツール、セットアップ手順、コミット規約、テスト方法まで丁寧に書かれているので、 まずはこれを一読することを強くおすすめします 。 この記事は公式ドキュメントを補足するもので、「読んだけどハマった」ポイントを中心に書いています。 必要なツール一覧 公式ドキュメントの内容を日本語で整理します。 ツール バージョン 備考 Node.js v24 推奨 v20 でも警告付きで動く。nvm 等でインストール pnpm 9.5.0+ npm/yarn は不可。corepack や npm 経由でインストール Docker 環境 最新 Docker Desktop、Rancher Desktop など。4 コンテナが起動 golang-migrate 最新 ClickHouse マイグレーション用 ClickHouse CLI 最新 デバッグ用(任意) インストール方法は環境によって異なるので、各ツールの公式ドキュメントを参照してください。 Docker で起動するコンテナ pnpm run dx を実行すると、以下の 4 つのコンテナが起動します。 コンテナ 用途 ポート PostgreSQL メイン DB(OLTP) 5432 ClickHouse 分析 DB(OLAP) 8123, 9000 Redis キャッシュ、キュー 6379 MinIO S3 互換ストレージ 9090, 9091 環境構築でハマったこと 1. pnpm not found zsh: command not found: pnpm 原因 : corepack が有効化されていない、または pnpm がインストールされていない 解決方法 : # 方法1: corepack を有効化 corepack enable # 方法2: npm でグローバルインストール npm install -g pnpm 2. golang-migrate がない Error: migrate: command not found 原因 : 公式ガイドに書いてあるが見落としがち 解決方法 : brew install golang-migrate 📝 補足 : golang-migrate は ClickHouse のマイグレーションに使われます。PostgreSQL は Prisma を使いますが、ClickHouse は Prisma がサポートしていないため、別のツールが必要です。 3. ~/package-lock.json の罠 Error: Cannot find module '@langfuse/shared' 原因 : ホームディレクトリ(~)に古い package-lock.json があると、Node.js のモジュール解決がおかしくなることがある 解決方法 : # ホームディレクトリの package-lock.json を削除 rm ~/package-lock.json # node_modules もクリーン cd ~/langfuse pnpm run nuke # 全ての node_modules と build ファイルを削除 pnpm install 4. ポート 5432 競合 Error: listen EADDRINUSE: address already in use :::5432 原因 : 他のアプリケーション(Docker環境であるRancher Desktop)の PostgreSQL コンテナがポート 5432 を使用していた 解決方法 : .env ファイルでポートを変更 # ポート番号を変更 POSTGRES_HOST_PORT=5433 # データベース URL も同じポートに DATABASE_URL="postgresql://postgres:postgres@localhost:5433/postgres" DIRECT_URL="postgresql://postgres:postgres@localhost:5433/postgres" 5. セルフホストとクラウドモード設定 これが一番のハマりポイントでした! 機能を実装したのに、UI に ADC のオプションが表示されない… 原因 環境変数 NEXT_PUBLIC_LANGFUSE_CLOUD_REGION でアプリの動作モードが決まります。.env.dev.example には以下の設定があります。 NEXT_PUBLIC_LANGFUSE_CLOUD_REGION="DEV" この値が設定されていると、Boolean("DEV") は true になるため、 クラウドモード として動作します。 // 判定ロジック(fetchLLMCompletion.ts) const isLangfuseCloud = Boolean(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION); const isSelfHosted = !isLangfuseCloud; クラウドモードでは、セルフホスト専用機能(ADC など)が UI に表示されません。 解決方法 .env でこの行をコメントアウトします。 # コメントアウトする # NEXT_PUBLIC_LANGFUSE_CLOUD_REGION="DEV" サーバーを再起動すると、ADC オプションが表示されるようになります。 モードの違い 機能 セルフホスト(未設定) クラウド(DEV/US/EU) ADC 認証 ✅ 使える ❌ 使えない レート制限 無効 有効 UI メッセージ "your database" "our servers" 見分け方 ログイン画面を見れば、どちらのモードで動いているかすぐわかります。 クラウドモード : 「Data Region」セレクターが表示される セルフホストモード : シンプルなログインフォームのみ 開発の始め方 1. Fork & Clone git clone https://github.com/YOUR_NAME/langfuse.git cd langfuse 2. 依存関係のインストール pnpm install 3. 環境変数の設定 cp .env.dev.example .env .env を編集します。 NEXT_PUBLIC_LANGFUSE_CLOUD_REGION をコメントアウト(セルフホスト機能をテストする場合) 必要ならポート番号を変更 4. 開発サーバーの起動 # 初回(DB リセットあり、時間がかかる) pnpm run dx # 2回目以降 pnpm run dev 5. 動作確認 http://localhost:3000 を開き、テストユーザーでログインします。 Email : demo@langfuse.com Password : password プロジェクト構造 Langfuse は pnpm + Turborepo のモノレポ構成です。 langfuse/ ├── web/ # Next.js フロントエンド + API ├── worker/ # 非同期処理ワーカー ├── packages/ │ └── shared/ # 共有コード(スキーマ、ユーティリティ) ├── ee/ # Enterprise 機能 └── fern/ # OpenAPI 仕様 技術スタック カテゴリ 技術 フレームワーク Next.js 14(Pages Router) API tRPC DB(OLTP) PostgreSQL + Prisma DB(OLAP) ClickHouse UI Tailwind CSS + shadcn/ui 認証 NextAuth.js PR 作成時に気をつけること Conventional Commits を使う feat(llm): add ADC support for Vertex AI fix(security): prevent projectId specification refactor(llm): rename useADC to shouldUseDefaultCredentials 提出前チェックリスト # コード整形 pnpm format # Lint チェック pnpm run lint CI について PR を作成すると、以下のチェックが自動実行されます。 CLA assistant : 初回は Contributor License Agreement への署名が必要 depthfirst-app bot : AI による自動コードレビュー dosubot : 自動ラベル付け 全てパスすると、メンテナーによるレビューに進みます。 PR の流れ(実体験) 私の PR は以下の流れで進みました。 PR 作成 → 3 つの自動チェックが走る CLA 署名 → CLA assistant が署名を要求 AI レビュー → depthfirst-app bot がレビュー(ここで弾かれた場合は修正 or コメントを残す) 人間レビュー → メンテナーから changes requested 修正 → フィードバック対応 --------ここからはメンテナーが対応------- マージ → 作業ブランチへマージ 本番マージ → メンテナーが main へマージする PR を作成 まずは3.のAI レビューのCIが通るまでが我々が行う作業です。 ここまで行けたら、メンテナーに知らせたりすると良いと思います! その他知っておくと良いこと 今回の PR では使わなかったものもありますが、参考までに。 大きな変更は Issue を先に立てる : 公式ガイドにも記載されています Discord で質問できる : 困ったことがあれば Discord で質問可能 good first issue から始める : 初コントリビュートなら good first issue ラベルがおすすめ まとめ Langfuse へのコントリビュートは、充実した公式ドキュメントのおかげでスムーズに進められました。環境構築では特に NEXT_PUBLIC_LANGFUSE_CLOUD_REGION の設定に注意が必要です。 レビューはとても丁寧で、変数名の改善やセキュリティの考慮点など建設的なフィードバックがもらえます。厳しく詰められるようなことはないので、安心して PR を出してみてください。 日本からも OSS にどんどん貢献していきましょう! 参考リンク Langfuse GitHub Langfuse 公式ドキュメント コントリビュートガイド Discord PR #11039
- Deep Dive Comparison: Langfuse MCP Server vs. Dify Langfuse Plugin in Dify
Langfuse MCP Server Arrives as a Tool for Handling Langfuse Prompts in Dify The release of the Langfuse MCP Server has opened a new avenue for accessing Langfuse's prompt management capabilities from external tools. Following this release, some may wonder about the role of the "Dify Langfuse Plugin" developed by our company and which solution is better. This article provides an in-depth comparison of the setup and features of the Langfuse MCP Server and the Dify Langfuse Plugin within Dify, offering the optimal choice for prompt management in your LLM development. Note: This comparison is focused solely on leveraging Langfuse Prompts within Dify workflows. The use of the MCP Server in other clients like Claude Code or Cursor is not covered here. Note: The information in this article is current as of December 2025. Langfuse MCP Server The Langfuse MCP Server provides a unified HTTP API endpoint for retrieving and manipulating prompts managed in Langfuse from external applications like Dify. By utilizing this server, applications can directly integrate the benefits of Langfuse's prompt management features, such as version control and A/B testing, into their workflows. Basic Authentication using project-scoped API keys ensures secure and efficient prompt lifecycle management. Setup in Dify After creating your Langfuse API Key in advance, here are the steps to configure the MCP Server for use in your Dify workflow: Click [Tools] at the top of the Dify screen, and select [MCP] from the sidebar. Click [Add MCP Server (HTTP)] to open the new configuration screen. MCP server setting step 1 For Server URL, enter the URL corresponding to your Langfuse domain (e.g., https://cloud.langfuse.com/api/public/mcp or "{your_domain}/api/public/mcp"). For [Name & Icon], enter an arbitrary name like langfuse-mcp and set an icon. For [Server Identifier] , enter an arbitrary identifier. Use an example like {organization_name}_{project_name} to easily identify your Langfuse project. Click [Add Header] in the Headers section. For [HEADER NAME], enter Authorization , and for [HEADER VALUE], enter Basic {your-base64-token} . The your-base64-token used here is generated by executing the following command with your Langfuse public and secret keys: echo -n "pk-lf-your-public-key:sk-lf-your-secret-key" | base64 MCP server setting step 2 Dify Langfuse Plugin The Dify Langfuse Plugin is a custom plugin specifically developed for Dify workflows, allowing users to directly call, search, and update prompts managed in Langfuse. This plugin integrates Langfuse’s robust version control capabilities into Dify, facilitating easier tracking of prompt history and team sharing. Usage Installation and authentication of the plugin follow these steps: Click [+ Install Plugin] in the top right corner of the Dify screen. Select [GitHub] as the installation source and paste the GitHub repository URL: https://github.com/gao-ai-com/dify-plugin-langfuse . After installation, enter the credentials from the plugin settings screen. The required authentication information is: "Langfuse Secret Key", "Langfuse Public Key", and "Langfuse Host". Comparison of the Two Tools These two integration methods exhibit clear differences in functionality, configuration flexibility, and detailed tool specifications. Available Tools In addition to getting and listing prompts, the MCP server provides two creation tools, createTextPrompt and createChatPrompt, as well as updatePromptLabels, which updates the labels attached to existing prompt versions. In contrast, the Plugin is limited to three tools: retrieval, search, and Text prompt update/creation. The updatePromptLabels tool found in the MCP Server was intentionally omitted from the Dify Langfuse Plugin's design. Furthermore, the createChatPrompt tool is exclusive to the MCP Server, meaning the Plugin cannot manipulate Chat-type prompts. Tool List Multiple Registrations The Langfuse MCP Server identifies the project from which to retrieve prompts using the authentication information (API key). However, due to Dify's specifications, multiple MCP servers cannot be registered using the same Server URL , restricting simultaneous operation across multiple Langfuse organizations or projects to one setting per project. Error when registering multiple MCP servers Conversely, the Dify Langfuse Plugin offers high flexibility, allowing authentication settings to be configured per block using separate API keys. This enables simultaneous operation across multiple Langfuse projects. You can change the API key Prompt Variable Substitution There is a functional difference between the two when it comes to the dynamic utilization of prompt templates: Langfuse MCP Server : The getPrompt tool does not include dynamic variable substitution . After retrieving the prompt body, you must use a Dify Code Block or similar mechanism to manually perform variable transformation. Dify Langfuse Plugin : The variable substitution feature is built into the Get Prompt tool . By passing variables in JSON format, you receive the substituted prompt body immediately, simplifying your Dify workflow. Prompt variable substitution feature Which Tool Should You Use? Use Case Recommended Choice Reason Want to create or update Chat-type prompts Langfuse MCP Server The createChatPrompt tool is provided. Want to manipulate prompt labels from Dify Langfuse MCP Server The updatePromptLabels tool is provided, allowing control over prompt promotion/demotion. Want to easily substitute prompt variables Dify Langfuse Plugin The variable substitution feature is built into the Get Prompt tool, simplifying the workflow. Want to handle multiple Langfuse projects/organizations simultaneously in Dify Dify Langfuse Plugin Authentication settings can be registered per block, allowing operation without switching projects. Summary The Langfuse MCP Server integrates Langfuse's official prompt operation API into Dify, excelling particularly in Chat prompt creation and strict label management. Meanwhile, the Dify Langfuse Plugin provides high flexibility tailored to specific Dify user needs, such as simplified workflows through variable substitution and concurrent operation across multiple projects. Choose the optimal tool based on your development structure and prompt usage. Reference Links Dify Langfuse Plugin: https://github.com/gao-ai-com/dify-plugin-langfuse Langfuse Official Documentation (MCP Server): https://langfuse.com/docs/api-and-data-platform/features/mcp-server GAO,Inc. is the only company that sells the Langfuse Enterprise plans to businesses in Japanese Yen and provides support and implementation assistance in Japanese. If you are interested in Langfuse, please contact us at contact@gao-ai.com.
- DifyでLangfuse MCPサーバーとLangfuseプラグインを徹底比較してみた
DifyでLangfuse Promptを扱うツールにLangfuse MCPサーバーが登場 Langfuseから MCPサーバー がリリースされ、Langfuseのプロンプト管理機能を外部ツールから利用する新しい道が開かれました。このリリースを受け、弊社が提供する「Dify Langfuseプラグイン」との関係性や、どちらを選ぶべきかという疑問を持つ方もいるかもしれません。 本記事では、DifyにおけるLangfuse MCPサーバーとLangfuseプラグインの導入方法、そして詳細な機能比較を通じて、LLM開発におけるプロンプト管理の最適な選択肢を提示します。 ※本記事は Difyワークフロー内でLangfuseプロンプトを活用するという観点に絞って比較しており、MCPサーバー自体が利用可能なClaude CodeやCursorなど他のMCPクライアントについては扱いません。 ※本記事の情報は2025年12月時点のものです。 Langfuse MCPサーバー Langfuse MCPサーバーは、Langfuseで管理されているプロンプトを、Difyなどの外部アプリケーションから統一されたHTTP API経由で取得・操作するためのエンドポイントを提供するものです。アプリケーションは、このサーバーを利用することで、プロンプトのバージョン管理やA/BテストといったLangfuseのプロンプト管理機能の恩恵を、ワークフローに直接組み込むことができます。LangfuseのプロジェクトごとにスコープされたAPIキーを用いたBasic認証により、安全かつ効率的なプロンプトのライフサイクル管理を実現します。 Difyでの設定方法 Langfuse API Keyを事前に作成した上で、DifyのワークフローでMCPサーバーを利用するための設定手順は以下の通りです。 Difyの画面上部にある[ツール]をクリックし、サイドバーから[MCP]を選択します。 [MCP サーバー(HTTP)を追加]をクリックし、新しい設定画面を開きます。 MCPサーバー設定手順1 サーバーURLに、Langfuseのドメインに応じたURL(例:https://cloud.langfuse.com/api/public/mcp や {your_domain}/api/public/mcp")を入力します。 [名前とアイコン]に、langfuse-mcpなど任意の名前を入力し、アイコンを設定します。 [サーバー識別子]に、任意の識別子を入力します。例として{組織名}_{プロジェクト名}など、Langfuseのプロジェクトを識別しやすい名前を設定します。 [ヘッダー]のセクションにある[ヘッダーを追加]をクリックします。 [ヘッダー名]に Authorization 、[ヘッダーの値]に Basic {your-base64-token} を入力します。 ここで使用する your-base64-token は、Langfuseの公開鍵と秘密鍵を用いて以下のコマンドを実行して発行します。 echo -n "pk-lf-your-public-key:sk-lf-your-secret-key" | base64 MCPサーバー設定手順2 Dify Langfuseプラグイン Dify Langfuseプラグインは、Difyのワークフローに特化して開発された カスタムプラグイン で、LangfuseのプロンプトをDifyから直接呼び出し、検索、更新することを可能にします。このプラグインは、Langfuseが持つ堅牢なバージョン管理機能をDifyに統合し、プロンプトの変更履歴の追跡やチーム内での共有を容易にします。 使用方法 プラグインのインストールと認証は以下の手順で行います。 Difyの画面右上の[+ プラグインをインストールする]をクリックします。 インストール元として[GitHub]を選択し、GitHubリポジトリのURL https://github.com/gao-ai-com/dify-plugin-langfuse を貼り付けてインストールします。 インストール後、プラグインの設定画面から認証情報を入力します。認証情報として「Langfuse 秘密鍵」「Langfuse 公開鍵」「Langfuse Host」を設定します。 2つのツールを比較してみた この2つの連携方法は、機能、設定の柔軟性、そしてツールの詳細な仕様において、明確な違いがあります。 提供されているツール MCPサーバーは、プロンプトの取得・一覧表示に加え、createTextPrompt、createChatPromptといった 作成系のツールが2種類 と、既存のプロンプトのバージョンに付与されたラベルを更新するupdatePromptLabelsを提供します。 一方、プラグインは取得、検索、Textプロンプトの更新・作成を行う3ツールに絞られています。MCPサーバーにあるupdatePromptLabelsツールは、Dify Langfuseプラグインの設計上不要と判断され、意図的に搭載されていません。また、createChatPromptツールはMCPサーバーにのみ存在し、プラグインではChatタイプのプロンプトを操作できません。 ツール一覧 複数登録 Langfuse MCPサーバーは、認証情報(APIキー)でプロンプトを取得するプロジェクトを識別していますが、Difyの仕様により 同じサーバーURLでは複数のMCPを登録できない ため、同時に複数のLangfuse組織やプロジェクトのプロンプトを操作することはできず、1つの設定で1つのプロジェクトに限定されます。 複数のMCPサーバーを登録したときのエラー 一方、Dify Langfuseプラグインは、認証設定を ブロックごと に個別のAPIキーで登録できるため、複数のLangfuseプロジェクトを同時に運用したい場合に高い柔軟性を提供します。 APIキーの切り替え可能 プロンプト変数置換 プロンプトテンプレートの動的利用において、両者には機能的な差があります。 Langfuse MCPサーバー: getPromptツールには 変数を動的に置換する機能は付いていません 。プロンプト本文を取得した後、Difyの コード実行ブロック などを利用して手動で変数変換処理を行う必要があります。 Dify Langfuseプラグイン: Get Promptツールに 変数置換機能が組み込まれています 。JSON形式で変数を渡すだけで、取得と同時に置換後のプロンプト本文を得られるため、Difyのワークフローをシンプルに保てます。 プロンプト変数置換機能 ユースケースごとの比較表 ユースケース 推奨される選択肢 理由 Chatタイプのプロンプトを作成・更新したい Langfuse MCP Server createChatPromptツールが提供されている。 Difyからプロンプトのラベルを操作したい Langfuse MCP Server updatePromptLabelsツールが提供されており、プロンプトの昇格・降格などを制御できる。 プロンプト変数を簡単に置換したい Dify Langfuseプラグイン Get Promptツールに変数を置換する機能が内蔵されており、ワークフローをシンプルに保てる。 複数のLangfuseプロジェクト/組織をDifyで同時に扱いたい Dify Langfuseプラグイン 認証設定をブロックごとに登録でき、プロジェクトを切り替えずに運用できる。 まとめ 結論として、Langfuse MCPサーバーはLangfuseの公式なプロンプト操作APIをDifyに統合し、特にChatプロンプトの作成やラベルの厳密な管理に優れています。一方、Dify Langfuseプラグインは、変数置換機能によるワークフローの簡略化や、複数プロジェクトの並行運用という、Difyユーザー特有のニーズに対して高い柔軟性を提供します。貴社の開発体制やプロンプトの利用形態に応じて、最適なツールを選択してください。 参考リンク Dify Langfuseプラグイン: https://github.com/gao-ai-com/dify-plugin-langfuse Langfuse公式ドキュメント: https://langfuse.com/docs/api-and-data-platform/features/mcp-server
- Google Cloud IAP保護下Langfuseトークン動的更新の実装ガイド
はじめに LangfuseはSelf-Hosted可能で、過去のブログでもご紹介したとおり、Google Cloud上にも簡単にLangfuse環境の構築が可能です。 また、Google Cloud上でLangfuseを構築する場合、Identity-Aware Proxy(IAP)を利用すると、認証済みユーザーのみがLangfuseにアクセスできるようになり、Langfuseのセキュリティが強化されます。 IAPは非常に便利なサービスですが、IAPトークンの有効期限が約1時間という制約があるため、 長時間稼働するサーバー(RAGサーバー、チャットボットなど)では、サーバー起動時に取得したIAPトークンの有効期限が切れてしまうと、IAPの先にあるLangfuseにアクセスできなくなってしまいます。 本記事では、このIAPトークンの有効期限を考慮し、LLMアプリケーションがLangfuseにアクセスする際のIAPトークン更新方法をご紹介します。 本記事でわかること IAP で保護された Langfuse に対して、LLM アプリケーションから安全にアクセスする方法 IAP トークンの有効期限(約 1 時間)をまたいで、トークンを自動更新する実装パターン Langfuse Python SDK v3 系以下通信に対して、それぞれ IAP トークンを付与する方法 httpx を使う通常 API(プロンプト管理 / スコア登録 等) requests.Session を使う OTEL Span Exporter(トレース送信) 前提・想定対象読者 前提及び想定対象読者は以下の通りです。 GCP / IAP / Cloud Run がある程度わかる人向け Langfuse Python SDK v3.10.1 を前提 LangChain / Vertex AI Gemini を使う例ですが、他の LLM クライアントにも応用可能 IAP環境下でのLangfuseへのアクセス方法 LangfuseのIAP環境下での構成 LangfuseのIAP環境下での構成を下記に示します。 認証ヘッダーの使い分け IAP環境下のLangfuseでは、2種類のHTTP認証ヘッダーを使い分けます。 ヘッダー 用途 値の形式 Authorization Langfuse API Key認証 Basic {base64(public_key:secret_key)} Proxy-Authorization IAPトークン Bearer {iap_token} 注意点として、IAPトークンを Authorization ヘッダーに設定すると、LangfuseのAPI Key認証が上書きされてしまいます。IAP認証には、Proxy-Authorization ヘッダーを利用してください。 IAPトークン有効期限の問題 上記の通りIAPを通してLangfuseにアクセスするためには、クライアントはIAPトークンをHTTPヘッダーに含める必要があります。 Langfuseクライアントでは、`httpx_client` オプションを利用して独自のHTTPクライアントを指定できます。 以下のプログラム例では、まずIAPトークンを取得し、取得したトークンを `Proxy-Authorization` ヘッダーとして設定した `httpx.Client` を生成します。 生成したクライアントをLangfuseクライアントの `httpx_client` パラメータに渡すことで、Langfuseの各種API呼び出し時にIAPトークンが自動的に付与されるようになります。 この実装をベースに動的にIAPトークンを更新する実装を考えてみましょう。 import httpx from google.oauth2 import id_token from google.auth.transport.requests import Request # IAPトークンを取得 iap_client_id = os.environ["IAP_CLIENT_ID"] initial_token = id_token.fetch_id_token(Request(), iap_client_id) # httpx.Clientにヘッダーを設定 httpx_client = httpx.Client( headers={"Proxy-Authorization": f"Bearer {initial_token}"} ) # Langfuseのhttpx_clientオプションに渡す langfuse = Langfuse( public_key=os.environ["LANGFUSE_PUBLIC_KEY"], secret_key=os.environ["LANGFUSE_SECRET_KEY"], host=os.environ["LANGFUSE_HOST"], httpx_client=httpx_client, ) Langfuse SDK V3のHTTPクライアントの構造 先ず、Langfuseクライアントの構成と IAPトークン更新の解決策の概要を構成図で示します。 Langfuse SDK V3では、内部的に2つの異なるHTTPクライアントが使用されています。 クライアント種類 通信ライブラリ 用途例 本記事プログラム例でのIAPトークン更新の実装 Langfuseクライアント httpx.Client プロンプトAPIなど トレース送信以外のAPI呼び出し event_hooksで動的に更新 OTEL Span Exporter requests.Session トレース送信 exportメソッドのラップで動的に更新 IAPトークンの更新方法が異なることに注意してください。以下で詳しく解説します。 1. Langfuseクライアント(httpx使用) Langfuseクライアントは、プロンプト管理やスコア登録などのAPI呼び出しに使用されます。先に説明した通り、`httpx_client`パラメータでhttpxのカスタムクライアントを注入できます。 そのため`httpx`の`event_hooks`機能を使えば、リクエスト直前にIAPトークンを動的に更新できます。 参考: HTTPX - Event Hooks 2. OTEL Span Exporter(requests.Session使用) OTEL Span Exporterは、トレース送信に使用されます。 Langfuseクライアントとは独立したHTTPクライアントを持っており、`httpx_client`の`event_hooks`は適用されません。 そのため、OTEL Span Exporterの`export`メソッドをラップして、エクスポート直前にIAPトークンを更新する必要があります。 注意: 本記事で記載している内容は公式にサポートされた方法ではなく、SDKの内部構造を利用した回避策のためLangfuse SDKのバージョンによって動作しない可能性があります。 Langfuse Python SDK V3.10.1でのみ動作することが確認されています。 実装例 以上を踏まえて、IAP保護下のLangfuseと連携するための実装例を以下に示します。 1. Google Cloud認証の準備 IAP(OAuth)クライアントの作成 下記のドキュメントを参考にIAPで利用するOAuth( IAP)クライアントを作成します。 クライアント作成後に表示されるクライアントIDとクライアントシークレットは必要になるのでメモしてください。 また、クライアントの「承認済みのリダイレクトURI」に以下の設定をします。 https://iap.googleapis.com/v1/oauth/clientIds/YOUR_CLIENT_ID:handleRedirect ※`YOUR_CLIENT_ID`は上記でメモしたクライアントIDに置き換えてください。 サービスアカウントの作成 下記のドキュメントを参考にLLMアプリケーションが利用するサービスアカウントを作成します。 サービスアカウントには以下のロールを割り当てる。 IAP で保護されたウェブアプリ ユーザー : IAP認証配下のLangfuseサーバーにアクセスするのに必要 Vertex AI ユーザー:Vertex AIのGeminiを利用するのに必要 また、以下のドキュメントを参考にサービスアカウントキーJSONファイルとしてダウンロードします。 ダウンロードしたキーファイルはサンプルプログラムのルートディレクトリに保存してください。 Langfuse側の環境変数設定 IAP構成でLangfuseを運用する場合、Langfuse Webの環境変数にIAPクライアントIDとシークレットを設定しLangfuse Webをリスタートします。 # IAP認証(Langfuse Web UIログイン用) AUTH_GOOGLE_CLIENT_ID=your-iap-client-id AUTH_GOOGLE_CLIENT_SECRET=your-iap-client-secret 2. 実装コード コードの概要 実装は2つのファイルで構成されています。 ファイル 役割 iap_token_provider.py IAPトークンの動的更新を担当するユーティリティモジュール server.py LangfuseとLangChainを使用したサンプルアプリケーション ディレクトリ構成 以下の様なディレクトリ構成になっています。 . ├── .env # 環境変数設定 ├── pyproject.toml # 依存関係(UV用) ├── service-account-key.json # サービスアカウントキー(※gitignore推奨) ├── iap_token_provider.py # IAPトークン管理モジュール └── server.py # サンプルアプリケーション 環境変数の設定 `.env`ファイルを作成し、以下の環境変数を設定します。 # Langfuse設定 LANGFUSE_PUBLIC_KEY=pk-lf-... # Langfuse Project Settings → API Keysで発行・確認 LANGFUSE_SECRET_KEY=sk-lf-... # Langfuse Project Settings → API Keysで発行・確認 LANGFUSE_HOST=https://langfuse.your-domain.com # LangfuseのURL # IAP設定 IAP_CLIENT_ID=123456789-abcdefg.apps.googleusercontent.com # 先にメモしたクライアントID # サービスアカウント認証 GOOGLE_APPLICATION_CREDENTIALS=./service-account-key.json # サービスアカウントキーのパス # GCP設定 GCP_PROJECT_ID=your-project-id # Google CloudプロジェクトID GCP_LOCATION=asia-northeast1 # リージョン 仮想環境作成・依存関係のインストール 本記事では、高速なPythonパッケージマネージャー UVを使用して仮想環境を構成します。 # UVのインストール(未インストールの場合) curl -LsSf https://astral.sh/uv/install.sh | sh # 仮想環境を作成 uv venv # 仮想環境を有効化 source .venv/bin/activate # プロジェクトの初期化 uv init # 依存関係のインストール(本記事執筆時のバージョン) uv add "langfuse>=3.10.1" \ "langchain>=1.1.0" \ "langchain-core>=1.1.0" \ "langchain-google-vertexai>=3.1.0" \ "httpx>=0.28.1" \ "google-auth>=2.43.0" \ "python-dotenv>=1.2.1" \ "fastapi>=0.122.0" \ "uvicorn>=0.38.0" 上記コマンドを実行すると、`pyproject.toml`に依存関係が記録され、`.venv`ディレクトリに仮想環境が作成されます。 iap_token_provider.py `iap_token_provider.py`では、以下の3つの機能を提供します IAPTokenProvider:サービスアカウント認証でIAPトークンを取得・キャッシュ・更新 create_httpx_client_with_iap: `event_hooks`でリクエスト直前にトークンを更新するhttpxクライアントを作成 wrap_otel_exporter_with_iap_token: OTEL Exporterの`export`メソッドをラップしてトークンを動的に更新 具体的なコードは下記の通りです。 iap_token_provider.py import logging import os from typing import Optional import httpx from google.auth.transport.requests import Request as GoogleAuthRequest from google.oauth2 import service_account logger = logging.getLogger(__name__) class IAPTokenProvider: """ Google Cloud IAP用のIDトークンを提供するクラス サービスアカウントの認証情報を使用してIAP用のIDトークンを取得します。 トークンは自動的にキャッシュされ、期限切れ時に自動更新されます。 """ def __init__(self, client_id: str, service_account_file: Optional[str] = None): """ Args: client_id: IAPで保護されたリソースのOAuth 2.0クライアントID (Google Cloud Consoleで確認可能) service_account_file: サービスアカウントキーファイルのパス 指定しない場合はGOOGLE_APPLICATION_CREDENTIALS環境変数を使用 """ self.client_id = client_id self._request = GoogleAuthRequest() sa_file = service_account_file or os.environ.get("GOOGLE_APPLICATION_CREDENTIALS") if not sa_file: raise ValueError( "サービスアカウントキーファイルが指定されていません。" "service_account_file引数またはGOOGLE_APPLICATION_CREDENTIALS環境変数を設定してください。" ) # IAP用のIDトークンを取得するための認証情報を作成 self._credentials = service_account.IDTokenCredentials.from_service_account_file( sa_file, target_audience=client_id, ) def get_token(self) -> str: """ 最新のIDトークンを取得する 認証情報が期限切れの場合は自動的に更新されます。 Returns: 有効なIDトークン文字列 """ if not self._credentials.valid: self._credentials.refresh(self._request) return self._credentials.token def create_httpx_client_with_iap( client_id: str, service_account_file: Optional[str] = None, timeout: float = 30.0, ) -> httpx.Client: """ IAP認証付きのhttpxクライアントを作成する event_hooksを使用して、リクエスト直前にトークンを動的に更新します。 これにより、長時間稼働するサーバーでもトークンの有効期限切れを防ぎます。 Args: client_id: IAPで保護されたリソースのOAuth 2.0クライアントID service_account_file: サービスアカウントキーファイルのパス timeout: リクエストタイムアウト(秒) Returns: IAP認証ヘッダー付きのhttpx.Client """ token_provider = IAPTokenProvider(client_id, service_account_file) def update_iap_token(request: httpx.Request): """リクエスト直前にIAPトークンを動的に更新""" token = token_provider.get_token() request.headers["Proxy-Authorization"] = f"Bearer {token}" return httpx.Client( event_hooks={"request": [update_iap_token]}, timeout=timeout, ) # ============================================================================= # OTEL Exporter ラップ機能 # ============================================================================= _otel_exporter_wrapped = False def wrap_otel_exporter_with_iap_token(iap_client_id: str) -> bool: """ Langfuse SDK V3のOTEL Exporterをラップして、IAPトークンを動的に更新する この関数は、LangfuseResourceManagerの内部プロセッサーを探し、 OTLPSpanExporterのexportメソッドをラップします。 Args: iap_client_id: IAPで保護されたリソースのOAuth 2.0クライアントID Returns: ラップが成功した場合はTrue """ global _otel_exporter_wrapped if _otel_exporter_wrapped: logger.debug("OTEL Exporterは既にラップ済みです") return True try: from langfuse._client.resource_manager import LangfuseResourceManager except ImportError as e: logger.warning(f"LangfuseResourceManagerをインポートできませんでした: {e}") return False if not hasattr(LangfuseResourceManager, '_instances'): logger.warning("LangfuseResourceManager._instancesが見つかりません") return False instances = LangfuseResourceManager._instances if not instances: logger.warning("LangfuseResourceManagerにインスタンスが登録されていません") return False # 各インスタンスのプロセッサーをラップ for public_key, instance in instances.items(): _wrap_instance_processors(instance, iap_client_id) _otel_exporter_wrapped = True logger.info("OTEL Exporterのラップが完了しました") return True def _wrap_instance_processors(instance, iap_client_id: str) -> bool: """LangfuseResourceManagerインスタンスのプロセッサーをラップする""" if not hasattr(instance, '_otel_tracer'): return False otel_tracer = instance._otel_tracer if not hasattr(otel_tracer, 'span_processor'): return False span_processor = otel_tracer.span_processor if hasattr(span_processor, '_span_processors'): processors = list(span_processor._span_processors) for processor in processors: _wrap_processor_exporter(processor, iap_client_id) return True else: return _wrap_processor_exporter(span_processor, iap_client_id) def _wrap_processor_exporter(processor, iap_client_id: str) -> bool: """プロセッサーのExporterをラップする""" if not hasattr(processor, 'span_exporter'): return False exporter = processor.span_exporter original_export = exporter.export # IAPTokenProviderを使用してトークンをキャッシュ・自動更新 token_provider = IAPTokenProvider(iap_client_id) def export_with_iap_token(spans): """エクスポート前にIAPトークンを更新するラッパー""" try: token = token_provider.get_token() if hasattr(exporter, '_session'): exporter._session.headers["Proxy-Authorization"] = f"Bearer {token}" except Exception as e: logger.error(f"IAPトークンの更新に失敗しました: {e}") return original_export(spans) exporter.export = export_with_iap_token return True server.py `server.py` では、FastAPIを使用して、ユーザーからの質問に回答し、Langfuseにトレースを送信するサーバーを実装しています。 以下の機能を提供します。 ユーザーからの質問についてVertexAI Geminiを使用して回答 システムプロンプトはLangfuseから取得して使用 Langfuseにトレースを送信 「2.」によりLangfuseクライアントのIAP認証を確認、「3.」によりOTEL Span Exporterの IAP認証を確認します。 具体的なコードは下記の通りです。 server.py import logging import os from typing import Optional from dotenv import load_dotenv load_dotenv() # .envファイルを読み込む from fastapi import FastAPI, Query from langfuse import Langfuse from langfuse.langchain import CallbackHandler from langchain_google_vertexai import ChatVertexAI from langchain_core.prompts import ChatPromptTemplate from iap_token_provider import ( create_httpx_client_with_iap, wrap_otel_exporter_with_iap_token, ) logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) app = FastAPI(title="IAP Token Expiry Test Server") _langfuse: Optional[Langfuse] = None def get_langfuse() -> Langfuse: """Langfuseクライアントのシングルトンを取得""" global _langfuse if _langfuse is None: iap_client_id = os.environ["IAP_CLIENT_ID"] # httpx_clientはプロンプト管理などのAPI呼び出しに使用 # event_hooksで動的にIAPトークンを更新 httpx_client = create_httpx_client_with_iap(iap_client_id) _langfuse = Langfuse( public_key=os.environ["LANGFUSE_PUBLIC_KEY"], secret_key=os.environ["LANGFUSE_SECRET_KEY"], host=os.environ["LANGFUSE_HOST"], httpx_client=httpx_client, ) # OTEL Exporterをラップ(トレース送信用) # httpx_clientのevent_hooksはOTEL Exporterには適用されないため、 # 別途ラップが必要 wrap_otel_exporter_with_iap_token(iap_client_id) return _langfuse @app.get("/qa") async def qa(q: str = Query(default="日本の首都はどこですか?", description="質問")): """質問に回答し、Langfuseにトレースを送信する""" langfuse = get_langfuse() langfuse_handler = CallbackHandler() # LangfuseからPromptを取得 try: langfuse_prompt = langfuse.get_prompt("qa-assistant") logger.info(f"Langfuseからプロンプトを取得しました: name={langfuse_prompt.name}, version={langfuse_prompt.version}") except Exception as e: logger.error(f"Langfuseからプロンプトの取得に失敗しました: {e}") raise # with_configでmetadataを設定してトレースとPromptを紐づける prompt = ChatPromptTemplate.from_messages( langfuse_prompt.get_langchain_prompt() ).with_config({ "metadata": {"langfuse_prompt": langfuse_prompt} }) # LLMを初期化 llm = ChatVertexAI( model="gemini-2.5-flash", project=os.environ["GCP_PROJECT_ID"], location=os.environ["GCP_LOCATION"], temperature=0, ) chain = prompt | llm # 質問に回答(LangChain invokeメソッドのcallbacksにLangfuseのCallbackHandlerを設定することで、トレースはLangfuseに送信される) response = chain.invoke( {"question": q}, config={ "callbacks": [langfuse_handler], "metadata": { "langfuse_user_id": "test-user", "langfuse_tags": ["iap-test"] } } ) # トレースをLangfuseに送信 try: langfuse.flush() logger.info("Langfuseへトレースを送信しました") except Exception as e: logger.error(f"Langfuseへのトレース送信に失敗しました: {e}") return {"query": q, "answer": response.content} if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8080) 3. 動作確認 実際に動作確認します。 Langfuse にプロンプトを登録 以下のスクリーンショットを参考に、LangfuseのUIからサンプルプログラムが利用するプロンプトを登録します。 サーバーを起動 ターミナルから以下コマンドで `server.py`を起動します。 uv run python server.py 以下のような表示が出たら正常に起動しています。 INFO: Started server process [xxxxx] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://0.0.0.0:8080 (Press CTRL+C to quit) サーバーにリクエスト 別のターミナルから以下のコマンドでサーバーに対して問い合わせを開始します。 curl -G "http://localhost:8080/qa" --data-urlencode "q=Langfuseってなに?" 数秒後に以下のように結果が返ってくるはずです。 {"query":"Langfuseってなに?","answer":"Langfuseは、**LLM(大規模言語モデル)アプリケーションのオブザーバビリティ(可観測性)と評価のためのオープンソースプラットフォーム**です。\n\n簡単に言うと、LLMを使ったアプリケーションを開発・運用する際に、以下のようなことを助けてくれるツールです。\n\n1. **トレース(実行履歴の記録)**:\n * ユーザーからの入力がLLMにどのように渡され、どのようなプロンプトが生成され、どのような応答が返されたか、その過程を詳細に記録・可視化します。\n * 複数のLLM呼び出しやツール利用を含む複雑なチェーンの動きも追跡できます。\n2. **モニタリング**:\n * アプリケーションのパフォーマンス(応答速度、エラー率など)やコスト(API利用料など)をリアルタイムで監視し、異常を検知します。\n3. **評価**:\n * LLMの出力品質を人間による評価や自動評価(評価モデルなど)で測定し、改善点を見つけ出します。\n * 異なるプロンプトやモデルバージョンのA/Bテストにも利用できます。\n4. **プロンプト管理**:\n * 使用しているプロンプトのバージョン管理や、効果的なプロンプトの特定を支援します。\n\nこれにより、開発者はLLMアプリケーションのデバッグ、改善、最適化を効率的に行うことができます。"} また、サーバー側のターミナルには以下のようなログが出力されていることを確認してください。 INFO:iap_token_provider:OTEL Exporterのラップが完了しました INFO:__main__:Langfuseからプロンプトを取得しました: name=qa-assistant, version=1 INFO:__main__:Langfuseへトレースを送信しました INFO: 127.0.0.1:49752 - "GET /qa?q=Langfuse%e3%81%a3%e3%81%a6%e3%81%aa%e3%81%ab%ef%bc%9f HTTP/1.1" 200 OK Langfuse でトレースを確認 次にLangfuse Web UIにアクセスし、トレースが送信されていること、上記の回答生成プロンプトにLangfuseからフェッチしたプロンプトが利用されていることを確認します。 以下のようにトレースが飛んでいること、Prompt:qa-assistantが利用されたことが確認できます。 まとめ IAP保護下でLangfuseを運用するには、以下の2点がポイントです。 1. トレース送信以外の機能(Langfuseクライアント): `httpx`の`event_hooks`でリクエスト直前にIAPトークンを動的に更新 2. トレース送信(OTEL Exporter): `export`メソッドをラップして、エクスポート直前にIAPトークンを動的に更新 これで、長時間稼働するようなLLMアプリケーションでもIAPトークンを更新しつつ継続的にLangfuse APIを利用できます。
- Langfuse セルフホスト|OSS vs Enterprise機能比較
はじめに Langfuseは、LLMアプリケーションの観測性、プロンプト管理、評価を一元管理できるオープンソースプラットフォームです。セルフホストでの運用が可能で、多くの企業が自社環境での導入を進めています。 セルフホストには2つの選択肢があります OSS版(無料・MIT License) : すべてのコア機能が無制限で利用可能 Enterprise版(ライセンスキー必要) : OSS版の機能に加え、Enterpriseグレードのセキュリティ・管理機能を提供 本記事ではEnterprise版で追加される主要な機能と、その具体的なユースケースを詳しく解説します。「OSS版で十分なのか?」「Enterprise版が必要になるのはどんな時か?」という疑問に答えます。 OSS版 vs Enterprise版 重要なポイント:コア機能は完全に同じ まず理解すべき重要なポイントは、 OSS版でもEnterprise版でも、Langfuseのコア機能に一切の制限がない ということです。 両バージョンとも以下が利用可能: トレーシング(エージェント対応) プロンプト管理 評価機能(データセット、実験、LLM-as-judge) Human Annotation マルチモーダル対応 無制限のプロジェクト・APIアクセス スケーラビリティの制限なし(Langfuse Cloudと同じインフラ) 組織レベルRBAC(Owner/Admin/Member/Viewer/None) Enterprise SSO(Google, AzureAD, GitHub) OSS版 vs Enterprise版 機能比較 機能 OSS版 Enterprise版 組織レベルRBAC ⭕️ ⭕️ Enterprise SSO (Okta、Authentik、Azure AD、Keycloakなど) ⭕ ⭕️ プロジェクトレベルRBAC ❌ ⭕️ 監査ログ ❌ ⭕️ 保護されたデプロイメントラベル ❌ ⭕️ データ保持ポリシー ❌ ⭕️ SCIM API(自動ユーザー プロビジョニング) ❌ ⭕️ Organization Management API ❌ ⭕️ Organization Creators(組織作成者の制限) ❌ ⭕️ UIカスタマイズ ❌ ⭕️ 料金 無料 カスタム価格 重要 :OSS版でも、トレーシング・プロンプト管理・評価・組織レベルのRBACやなどの すべてのコア機能が無制限 で利用可能です。Enterprise版は、セキュリティ・コンプライアンス・大規模運用のための管理機能を追加します。 詳細な機能比較は 公式ページ をご覧ください。 Enterprise機能の詳細解説 1. プロジェクトレベルRBAC 何ができるか OSS版でも組織レベルのRBACは利用可能ですが、Enterprise版では プロジェクト単位で権限を細かく設定 できます。組織の権限はNoneにして、特定プロジェクトだけAdmin権限を付与、といった柔軟に設定できます。 5つのロール(Owner, Admin, Member, Viewer, None)と30種類以上の詳細な権限スコープを組み合わせて、細かくアクセス制御できます。 Enterpriseの場合のorganization設定画面 ユースケース マルチテナント運用 顧客ごとにプロジェクトを分けている場合、担当営業やCSは自分の顧客のプロジェクトだけにアクセスできるよう制限できます。「A社担当の田中さんはA社プロジェクトのみAdmin、他はNone」といった設定が可能です。 環境別の権限管理 開発環境は全員がMember権限で自由に触れるが、本番環境はマネージャー以上のみAdmin、他メンバーはViewerで閲覧のみ、という運用ができます。「見ていいけど触っちゃダメ」を仕組みで担保できます。 公式ドキュメント 2. 監査ログ(Audit Logs) 何ができるか システム内の すべてのアクティビティを詳細に記録 します。誰が・いつ・何をしたかを正確に追跡でき、変更の場合は変更前後の完全な状態をJSON形式で保存します。UI上でフィルタリング・エクスポートが可能です。 Audit Logsの画面 ユースケース 障害原因の特定 「先週からLLMの回答精度が落ちた」という報告があったとき、監査ログでプロンプトの変更履歴を確認し、どの変更が原因かを数分で特定できます。変更前後の内容も保存されているので、すぐにロールバック判断ができます。 コンプライアンス対応 SOC2やISMSの監査で「過去3ヶ月の操作履歴を提出してください」と求められても、監査ログをエクスポートするだけで対応完了。手動で履歴をまとめる作業が不要になります。 責任範囲の明確化 「このプロンプト、誰が最後に触った?」という確認がログ一発で解決。属人的な記憶に頼らず、事実ベースで会話できます。 公式ドキュメント 3. 保護されたデプロイメントラベル(Protected Deployment Labels) 何ができるか 特定のラベル(例:production)を保護状態にすることで、ViewerとMemberロールのユーザーはそのラベルを変更・削除できなくなります。保護されたラベルが付与されているプロンプトは、プロンプト自体の削除も防止されます。 Protected Deployment Labels設定画面 ユースケース 本番事故の防止 productionラベルを保護することで、開発者は自由にプロンプトを作成・テストできますが、本番へのデプロイは管理者の承認が必須になります。「新人が誤って本番プロンプトを上書きしてしまった」という事故を仕組みで防げます。 リリースフローの強制 「開発→ステージング→本番」というフローを守らせたい場合、stagingとproductionを保護ラベルに設定。開発者が勝手に本番に直接デプロイすることを防ぎ、レビュープロセスを確実に通す運用ができます。 公式ドキュメント 4. データ保持ポリシー(Data Retention Policies) 何ができるか プロジェクト単位でデータ保持期間を設定 できます(最小3日間)。設定期間を超えたトレース、観測、スコア、メディアアセットを毎晩自動削除します。 削除の判定基準 :トレースはtimestamp、観測はstart_time、スコアはtimestamp、メディアアセット(画像・音声など)はcreated_atを基準にします。削除されたデータは復元できません。 Data Retention設定画面 ユースケース 環境別のコスト最適化 :開発環境は7日間、ステージングは30日間、本番は1年間、とプロジェクトごとに保持期間を設定。開発中の大量のテストデータでストレージコストが膨らむのを防ぎつつ、本番データは長期保存できます。 コンプライアンス要件への対応 :GDPRや社内規定で「ユーザーデータは90日を超えて保持してはならない」という要件がある場合、ポリシーを設定すれば自動で削除されます。手動での削除作業や削除漏れのリスクがなくなります。 ストレージ容量の管理 :セルフホスト環境でClickHouseの容量を抑えたい場合、古いトレースを自動削除することでディスク使用量を予測可能な範囲に収められます。 公式ドキュメント 5. SCIM APIによる自動ユーザープロビジョニング 何ができるか SCIM(System for Cross-domain Identity Management)プロトコルを使用して、Okta、Azure AD/Entra ID、Keycloak等のSCIM対応IdPとLangfuseを自動連携させます。IdPでユーザーを管理すれば、Langfuseへのアクセスも自動で設定・解除できます。 ユースケース 入退社時の自動処理 :入社時にOktaやAzure ADでアカウントを作成すれば、Langfuseへのアクセスも自動で付与。退職時にIdPで無効化すれば、Langfuseへのアクセスも即座に遮断されます。「退職者のアカウントが残っていた」というセキュリティリスクを排除できます。 IT部門の運用負荷削減 :500名規模の組織でも、IT部門がLangfuseのユーザーを個別に追加・削除する作業が不要に。IdPを唯一の真実のソースとして、すべてのツールのアクセス管理を一元化できます。 公式ドキュメント 6. Organization Management API 何ができるか セルフホスト専用 の機能です。管理者がAPI経由で組織をプログラマティックに作成・更新・削除できます。認証にはADMIN_API_KEY環境変数を設定し、APIリクエスト時にAuthorization: Bearer $ADMIN_API_KEYヘッダーを付与します。 ユースケース 社内ワークフローとの連携 新規プロジェクト立ち上げ時に、Slackで申請→マネージャー承認→自動でLangfuse組織・プロジェクトが作成、という流れを構築できます。管理者がUIで手作業する必要がなくなります。 マルチテナントSaaSの運用 顧客ごとにLangfuse組織を作成する必要がある場合、顧客管理システムと連携して自動プロビジョニング。顧客が増えるたびに手動で設定する手間がなくなります。 一括管理・棚卸し 四半期ごとの棚卸しで「使われていない組織を整理したい」というとき、APIで一覧取得→利用状況確認→不要な組織を削除、という作業をスクリプト化できます。 公式ドキュメント 7. Organization Creators(組織作成者の制限) 何ができるか デフォルトでは全ユーザーが新しい組織を作成できますが、Enterprise版では 特定のメールアドレスに制限 できます。 ユースケース 組織の乱立防止 デフォルトでは全ユーザーが組織を作成できるため、「気づいたら同じチームが3つの組織を作っていた」という事態が起こりえます。作成権限をIT管理者だけに制限することで、組織構造を統制できます。 ガバナンス強化 「組織を作るには申請が必要」というルールを、仕組みで強制できます。口頭でのルール周知だけでは守られないことも、システムで制限すれば確実です。 公式ドキュメント 8. UIカスタマイズ 何ができるか 環境変数を設定することで、ロゴ、ドキュメント・サポートリンク、LLMデフォルト設定、表示モジュールをカスタマイズできます。 UIカスタマイズ環境変数 LANGFUSE_UI_LOGO_LIGHT_MODE_HREF= https://example.com/logo-light.png LANGFUSE_UI_DOCUMENTATION_HREF= https://wiki.example.com/langfuse LANGFUSE_UI_HIDDEN_PRODUCT_MODULES = tracing,datasets,prompt-management LANGFUSE_UI_HIDDEN_PRODUCT_MODULESを設定すると以下のようにトレースの画面をUIから削除したりとカスタマイズが可能です。 ユースケース 社内ツールとしてのブランディング :自社ロゴを設定し、ドキュメントリンクを社内Wikiに変更することで、「Langfuse」ではなく「社内LLM管理ツール」として展開できます。ユーザーに外部サービス感を与えず、自然に使ってもらえます。 機能の絞り込み :非エンジニアのビジネスユーザーにはプロンプト管理だけを使わせたい場合、トレースやデータセット機能をUIから非表示にできます。「どこを触ればいいかわからない」という混乱を防ぎ、必要な機能だけを見せられます。 サポート導線の統一 :ヘルプリンクを社内のサポートチャンネルやチケットシステムに変更することで、「困ったらここに聞く」という導線を明確にできます。 公式ドキュメント 導入方法 Enterprise機能の有効化は、両方のLangfuseコンテナに以下の環境変数を追加するだけです LANGFUSE_EE_LICENSE_KEY= ダウンタイムなしで有効化可能で、既存のデータやAPIキーに影響はありません。 詳細(公式ドキュメント) どんな組織に向いているか OSS版で十分なケース チーム規模が20名未満 単一部署・単一チームでの利用 厳格なコンプライアンス要件がない 基本的なRBAC(組織レベル)で十分 重要 :OSS版でもすべてのコア機能が無制限で利用可能です。Enterprise SSO(Google, AzureAD, GitHub)と組織レベルRBACも含まれています。 Enterprise版が必要になるタイミング 複数部署での利用 が始まり、プロジェクト単位でのアクセス制御が必要になった コンプライアンス監査 で操作履歴の提出を求められた 管理者が全組織・全プロジェクトを 一元管理 したい ユーザー管理の手動作業 が運用負荷になってきた 本番環境のプロンプト誤変更 のリスクが顕在化した データ保持期間の制御 が必要になった まとめ Langfuseのセルフホスト版は、OSS版でもすべてのコア機能が無制限で利用できます。Enterprise版は、セキュリティ、コンプライアンス、大規模運用のための管理機能を追加します。 組織の規模や要件に応じて、最適なプランを選択してください。 また、企業向けサポートとして、ガオ株式会社を通じてEnterpriseプランを日本円で購入し、日本語でサポートを受けることが可能です。 ご興味ある方は、contact@gao-ai.com までご連絡ください。 参考リンク : セルフホスト料金プラン: https://langfuse.com/pricing-self-host Enterpriseライセンスキー: https://langfuse.com/self-hosting/license-key 公式サポート: https://langfuse.com/support
- Langfuseのマルチモーダル対応:画像・音声ファイルのトレース添付機能がGAに
はじめに LLMアプリケーション開発において、テキストだけでなく画像や音声などのマルチモーダルなデータを扱うケースが増えています。Langfuseは2024年8月に初めてマルチモーダルトレースのサポートを発表し、同年11月には画像、音声、PDFなどの添付ファイルにも対応する完全なマルチモーダルサポートを実現しました。 当該機能は長らくpreviewとされていましたが、先日GAとなったようです(※中の人がSlackでそう言ってました)。 そこで本記事では、Langfuseのマルチモーダル機能の概要、具体的な使い方、そして利用時の注意点について解説します。 Langfuseマルチモーダル機能の概要 たとえば画像ファイルを含んだトレースを送信した場合、LangfuseのWebUI上では以下のように表示されます。 このように、LLMを呼び出す際に画像データが含まれていた場合、テキストデータだけでなく画像データも同時に確認でき、改善活動の効率が大いに向上します。 (※余談ですが、gpt-4oは寿司ネタにそこまで詳しくないのかもしれません。上記画像にタコにみえるネタはないようにみえるので…) また、音声ファイルが添付されたトレースは以下のように表示されます。 UI上に表示される再生ボタンを押すことで音声が再生できます。こちらも画像ほどではないにせよ、WebUIから直接LLMに送信した音声データの内容が確認できるため、LLMアプリケーションの挙動確認を効率よく行えるようになります。 対応しているファイル形式 Langfuseは以下のメディアファイル形式に対応しています: 画像 : PNG、JPG、WEBP 音声ファイル : MPEG、MP3、WAV その他の添付ファイル : PDF、プレーンテキスト これらのファイルは、トレースやObservationのinput、output、metadataフィールドに含めることができます。 主な特徴 自動処理 : Base64エンコードされたデータURIは、Langfuse SDKが自動的に検出し処理されます 外部URL対応 : 外部URLで参照されるメディアファイルもUI上でインライン表示可能です 効率的な保存 : メディアファイルはトレースデータと分離され、オブジェクトストレージに直接アップロードされます 重複排除 : 同じファイルは自動的に重複排除され、参照IDのみが保存されます アーキテクチャ Langfuseは、パフォーマンスと効率性を最適化するために以下のような仕組みを採用しています: メディアファイルはクライアント側でトレースデータから分離 AWS S3または互換性のあるオブジェクトストレージに直接アップロード トレース内にはmediaIdへの参照のみを保持 UI側でmediaIdを検出し、メディアファイルをインライン表示 使い方 1. Base64エンコードされたメディアの自動処理(最も簡単) 最新バージョンのLangfuse SDKであれば、通常のトレース送信設定を実施しておくだけで、Base64エンコードされたメディアファイルが自動的にLangfuseにアップロードされます。 OpenAI SDKとの連携例(画像の場合): from langfuse.openai import openai # Langfuse統合が有効化されたOpenAIクライアント client = openai.OpenAI() # 画像を含むリクエスト(Base64エンコードされた画像も自動処理) response = client.chat.completions.create( model="gpt-4o", messages=[ { "role": "user", "content": [ {"type": "text", "text": "この画像には何が写っていますか?"}, { "type": "image_url", "image_url": { "url": "data:image/jpeg;base64,/9j/4AAQSkZJRg..." } } ] } ] ) SDK側で自動的に画像を抽出し、Langfuseのオブジェクトストレージにアップロード後、トレースに参照を記録します。 また、音声データの場合でもBase64エンコードして利用するぶんには同様に自動的に検出&アップロードされます。 2. 外部URLによる参照 外部URLで画像を参照する場合も、Langfuse UIで自動的にインライン表示されます: response = client.chat.completions.create( model="gpt-4o", messages=[ { "role": "user", "content": [ {"type": "text", "text": "この画像を分析してください"}, { "type": "image_url", "image_url": {"url": "https://example.com/image.jpg"} } ] } ] ) この場合、メディアファイルはLangfuseのストレージにアップロードされず、元のURLからLangfuse WebUI上に直接表示されます。 3. LangfuseMediaクラスを使ったカスタム制御 より細かい制御が必要な場合や、Base64エンコードされていないメディアを扱う場合は、LangfuseMediaクラスを使用します: from langfuse import get_client, observe from langfuse.media import LangfuseMedia @observe() def process_document(): langfuse = get_client() # PDFファイルを読み込む with open("document.pdf", "rb") as pdf_file: pdf_bytes = pdf_file.read() # LangfuseMediaでラップ pdf_media = LangfuseMedia( content_bytes=pdf_bytes, content_type="application/pdf" ) # トレースのメタデータに追加 langfuse.update_current_trace( metadata={"document": pdf_media} ) # または、入力や出力に含める langfuse.update_current_span( input={"document": pdf_media} ) セットアップと料金 Langfuse Cloudを使用する場合 Langfuse Cloudでは、マルチモーダル添付ファイルは 現在無料 で利用できます。ただし、将来的には大規模なマルチモーダルトレースに伴うストレージとコンピューティングコストを考慮した課金体系が導入される可能性があることに留意してください。 セルフホスティングの場合 セルフホスティング環境では、独自のオブジェクトストレージバケットを設定する必要があります: AWS S3または互換ストレージ(Google Cloud Storage、Azure Blob Storage、Minio等)を用意 環境変数LANGFUSE_S3_MEDIA_UPLOAD_*を設定 ストレージバケットは、SDK経由の直接アップロードとブラウザからのメディアアセット取得をサポートするため、公開解決可能なホスト名を持つ必要があります 設定の詳細は セルフホスティングドキュメント を参照してください。 注意事項と制約 1. 現在サポートされていない機能 Playgroundでの使用 : マルチモーダルコンテンツはまだPlaygroundでサポートされていません Dataset Items : データセットアイテムでのマルチモーダルコンテンツもまだサポート対象外です 2. セキュリティと検証 メディアアップロードには署名付きURL(presigned URL)が使用されます コンテンツ長、コンテンツタイプ、SHA256ハッシュによる検証が行われます ファイルの一意性は、プロジェクト、コンテンツタイプ、SHA256ハッシュによって判定されます 3. ストレージ容量の管理 マルチモーダルデータは通常のトレースデータよりも大きなストレージを消費します。特にセルフホスティングの場合は、ストレージ容量とコストの管理に注意してください。 4. トラブルシューティング 画像が正しく表示されない場合: セルフホスト環境で画像がインラインではなくボタンとして表示される場合、LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE=trueの設定が必要な場合があります ストレージバケットの公開設定と署名付きURLの有効期限を確認してください Langfuseのバージョンがマルチモーダルサポートに対応しているか確認してください(v2.93.2以降推奨) まとめ Langfuseのマルチモーダル対応により、テキストだけでなく画像、音声、文書ファイルなどを含むLLMアプリケーションの動作を包括的に観察できるようになりました。 主なメリット: Base64エンコードされたメディアの自動処理により、開発者の手間を削減 外部URLとカスタムアップロードの両方に対応し、柔軟な実装が可能 効率的な重複排除とストレージ管理 既存のOpenAI SDK、LangChain、LlamaIndexなどとのシームレスな統合 マルチモーダルAIアプリケーションの開発・運用において、Langfuseは強力な観察性とデバッグ機能を提供します。まずは最新バージョンのSDKにアップグレードして、自動処理の恩恵を受けることから始めてみてください。 参考リンク Langfuse公式ドキュメント - マルチモーダル機能 マルチモーダルトレースの例(Jupyter Notebook) Langfuse Launch Week #2アナウンス セルフホスティングガイド
- 音声AIエージェントLiveKit × Langfuse連携 ~トレース分離問題の解決~
はじめに LiveKit Agentsは、音声AIアプリケーションを構築するためのオープンソースフレームワークです。本記事では、Langfuseを使った観測可能性の実装と、その際に遭遇したトレース分離問題の解決方法を紹介します。 想定読者 OpenTelemetry、Langfuseの基礎知識がある方。 LiveKit Agentsで音声AIアプリケーションを構築している方。 LiveKitとは LiveKitは、リアルタイム音声・映像通信のためのオープンソースプラットフォームです。WebRTCをベースにしており、音声AIエージェントをはじめとする様々なリアルタイムアプリケーションの構築に利用できます。 LiveKitの主な特徴 Room中心の設計 LiveKitでは、 Room という仮想空間を中心とした設計になっています。 Room : 参加者が集まる仮想空間。会議室やチャットルームのようなイメージ。 Participant : Roomに参加するユーザーやエージェント。 Agent : プログラマブルなAI参加者。人間のようにRoomに参加し、音声で会話できる。 WebRTCによる低レイテンシ通信 従来のHTTP/WebSocketと比較して、WebRTCは音声・映像のリアルタイム通信に最適化されており、低レイテンシで高品質な通信が可能です。 多様なクライアントSDK ブラウザ、iOS、Android、Unityなど、主要なプラットフォームに対応したSDKが提供されており、幅広い環境で利用できます。 詳細は 公式ドキュメント をご覧ください。 LiveKit Agentsを使うメリット 1. 統一されたインターフェース STT、LLM、TTSの各プロバイダーを統一されたAPIで扱えるため、プロバイダーの切り替えが容易です。 2. 本番環境に対応した機能 VAD(Voice Activity Detection)、Turn Detection、エラーハンドリングなど、実用的な機能が標準で提供されています。 3. リアルタイム性の高さ WebRTCベースの設計により、エンドツーエンドで低レイテンシな音声通信が実現できます。 4. 柔軟なアーキテクチャ STT+LLM+TTSの従来型パイプラインと、OpenAI Realtime APIなどのSpeech-to-Speechモデルの両方に対応しています。 料金プラン LiveKit CloudのFree Planでは、 月間1,000分のAgent Session が無料で利用できます。これにより、開発段階やプロトタイプ作成において、コストを気にせず気軽に始められます。 詳細は 料金ページ をご確認ください。 環境構築 今回は、LiveKit Agentsの公式リポジトリにある サンプルコード(langfuse_trace.py) を使用します。このサンプルには、Langfuse統合の基本実装と、2つの異なるタイプのエージェント(STT+LLM+TTS構成とRealtime API構成)が含まれています。 セットアップの流れは以下の通りです。 LiveKit CloudでAPI Keyを取得。 必要な環境変数を設定。 サンプルコードをクローンして依存関係をインストール。 初回セットアップコマンドを実行。 それでは、具体的な手順を見ていきましょう。 LiveKitアカウント作成とAPI Key取得 まず、 https://cloud.livekit.io/login にアクセスしてアカウントを作成します。アカウント作成後、Settings → API keys → Create key の順にクリックし、生成されたAPI KeyとSecretをコピーして保存します。 必要な環境変数の設定 プロジェクトルートに.envファイルを作成し、以下の環境変数を設定します。 # Langfuse LANGFUSE_SECRET_KEY=sk-lf-** LANGFUSE_PUBLIC_KEY=pk-lf-** LANGFUSE_HOST=https://** # LiveKit LIVEKIT_URL=wss://**.livekit.cloud LIVEKIT_API_KEY=** LIVEKIT_API_SECRET=** # OpenAI OPENAI_API_KEY=sk-proj-** サンプルコードのセットアップ 以下のコマンドでサンプルコードを取得し、環境をセットアップします。まず、リポジトリをクローンして該当ディレクトリに移動します。 git clone https://github.com/livekit/agents.git cd agents/examples/voice_agents 次に、仮想環境を作成してアクティベートします。 python -m venv venv source venv/bin/activate 依存関係をインストールし、初回のみ必要なファイルをダウンロードします。 pip install -r requirements.txt python langfuse_trace.py download-files 使用バージョン Python 3.12.12 livekit-agents 1.3.2 livekit-plugins-openai 1.3.2 livekit-plugins-deepgram 1.3.2 livekit-plugins-silero 1.3.2 サンプルコードの実行 以下のコマンドでコンソールモードでエージェントを起動します。 python langfuse_trace.py console このサンプルコードには、以下の機能が実装されています。 実装されているエージェント Kelly : Deepgram(STT)、GPT-4o-mini(LLM)、OpenAI TTS(TTS)を組み合わせた従来型パイプライン。 Alloy : OpenAI Realtime APIを使用したSpeech-to-Speechエージェント。 ツール lookup_weather : 天気情報を取得するツール(仮想データを返す)。 エージェント交代機能 KellyとAlloyは相互に交代可能です。Kellyに"transfer to Alloy"と話しかけるとAlloyに交代し、逆にAlloyに"transfer to Kelly"と話しかけるとKellyに戻ります。 LiveKitを使ってみた感想 実際にLiveKit Agentsを使用して音声AIアプリケーションを構築してみた感想をいくつか紹介します。 CLIが見やすく使いやすい LiveKitのCLIは非常に見やすく設計されており、ログの確認やデバッグが容易でした。音声認識の結果やエージェントの応答がリアルタイムで表示されるため、開発体験が良好です。 LiveKit CLI この画像では、STTによる音声認識結果、LLMやTTSのメトリクス(レイテンシ、トークン数など)、ツールの実行結果などが時系列で表示されているのが分かります。特に、EOU(End of Utterance)の検出やLLMのTime to First Token(TTFT)などの詳細なメトリクスが確認できる点が便利です。 複数エージェントとの会話が簡単に実装できる 今回のサンプルでは、KellyとAlloyという2つのエージェントを切り替えながら会話できました。エージェントの切り替えロジックがシンプルに実装されており、複雑な状態管理が不要な点が印象的でした。 STT+LLM+TTSのエージェントでも非常に速い 当初、Realtime APIと比較してSTT+LLM+TTSパイプラインはレイテンシが高いのではないかと懸念していました。しかし、実際に使用してみると、体感的な遅延はほとんど感じられず、自然な会話が可能でした。 STT+LLM+TTSでも高速な理由 LiveKit AgentsのSTT+LLM+TTSパイプラインが高速な理由は、以下のような最適化技術が組み込まれているためです。 1. プリエンプティブ生成(Preemptive Generation) preemptive_generation=True, ユーザーの発話が完全に終わる前に、部分的な転写結果に基づいて応答生成を開始します。これにより、ユーザーが話し終わった瞬間にエージェントが応答できます。 2. ストリーミングTTS tts=tts.StreamAdapter( tts=openai.TTS(), text_pacing=True, ), LLMがテキストを生成し次第、TTSが音声を順次送信します。全文生成を待たずに最初の音声が届くため、体感レイテンシが大幅に短縮されます。 3. WebRTCによる低レイテンシ通信 HTTP/WebSocketよりも低レイテンシなWebRTCプロトコルを使用しているため、ネットワーク遅延が最小限に抑えられます。 4. 非同期並列処理 STT、LLM、TTSが非同期で並列実行されるため、各処理の完了を待たずに次のステップに進めます。 5. 最適化されたパイプライン VAD(Voice Activity Detection): 発話の開始/終了を正確に検出。 Turn Detection: 会話のターンを適切に判断。 インスタント接続: マイク入力をバッファリングして即座に処理開始。 実測値 Langfuseのタイムライン表示で確認したところ、ユーザーが話し終わってからエージェントが話し始めるまでの時間は以下の通りでした。 STT+LLM+TTS : 約2.33秒 Realtime Model : 約0.65秒 数値で見ると差がありますが、体感的には両者とも自然な会話ができるレベルでした。STT+LLM+TTSでも十分に実用的な速度が出ていることが確認できました。 トレースがバラバラになる問題を発見 期待していた構造 Langfuseの公式統合ガイド では、以下のような階層化された単一のトレース構造が示されています。 Langfuse公式HPに載っているトレース すべてのアクティビティが1つのトレースに階層化され、処理の流れが一目で把握できる状態を期待していました。 実際の構造 しかし、実際にサンプルコードを実行してみると、各アクティビティが独立したトレースとして記録されてしまいました。 サンプルコード実行時のトレース一覧 この画像では、以下のように複数のトレースが別々に作成されています。 agent_session start_agent_activity(エージェント入室) user_turn agent_turn drain_agent_activity(エージェント退室) 画像中の9:15:52付近を見ると、drain_agent_activityとstart_agent_activityが連続しています。これは、Kelly(STT+LLM+TTS)からAlloy(Realtime Model)へのエージェント交代を示しています。 この問題は GitHub Discussion でも報告されています。 user_turnとagent_turnの詳細 トレースの詳細を見てみると、どちらのモデルでもuser_turnとagent_turnという基本構造は共通していました。しかし、内部の詳細度が大きく異なるようです。 user_turn(共通) どちらのモデルでも、ユーザーの発話に関する情報が記録されます。転写テキスト、信頼度スコア、発話時間などが含まれます。 サンプルコードのuser_turnトレース agent_turn(STT+LLM+TTS) パイプラインの各ステップが詳細に記録されます。 llm_node: LLMへのリクエスト。 llm_request: 実際のAPI呼び出し。 tts_node: TTSへのリクエスト。 tts_request: 実際の音声合成。 function_tool: ツール呼び出し(ツール使用時のみ)。 このように各ステップが可視化されるため、ボトルネックの特定や最適化がしやすくなります。 サンプルコードのagent_turnトレース(STT+LLM+TTS) agent_turn(Realtime Model) 使用しているOpenAI Realtime APIはSpeech-to-Speechモデルのため、内部処理が抽象化されています。STT、LLM、TTSといった明示的な分離がなく、シンプルな構造になっています。 また、Realtime ModelだけOutputがトレースに表示されました。 サンプルコードのagent_turnトレース(Realtime Model) なぜトレースが分離するのか LiveKit Agentsは内部で非同期処理を多用しています。各アクティビティ(user_turn、agent_turnなど)は別々の非同期タスクとして実行されますが、その際にOpenTelemetryのコンテキストが適切に伝播されないことがあります。 OpenTelemetryでは、スパン作成時に「現在のコンテキスト」を参照して親子関係を構築します。しかし、非同期タスクが新しいコンテキストで実行されると、親スパンへのリンクが失われ、新しいトレースIDが生成されてしまいます。 この問題を解決するには、プログラム全体で共通のトレースIDを使用し、すべてのスパンがこのトレースIDを継承するように明示的に設定する必要があります。 解決策とOpenTelemetryのコンテキスト管理 基本的なアプローチ プログラム起動から終了までを1つのトレースとして扱うため、カスタムのトレースIDを生成し、すべてのスパンがこのトレースIDを継承するようにします。 実装手順 ステップ1: 必要なモジュールのインポート まず、OpenTelemetryのコンテキスト管理に必要なモジュールをインポートします。 from opentelemetry import trace from opentelemetry.trace import NonRecordingSpan, SpanContext, TraceFlags import hashlib ステップ2: グローバルトレースIDの生成 プログラム起動時に、固定のトレースIDを生成します。プログラム起動時に1回だけ生成するようにグローバル変数としています。 # プログラム起動時に固定のtrace_idを生成 # プログラム起動から終了までに使用するルームセッションで同じtrace_idを使用することで、 # すべてのアクティビティを1つのtraceにまとめる GLOBAL_TRACE_ID = int(hashlib.sha256(os.urandom(16)).hexdigest()[:32], 16) ステップ3: SpanContextの作成とコンテキスト設定 entrypoint関数内で、カスタムトレースIDを使用したSpanContextを作成し、グローバルコンテキストとして設定します。 @server.rtc_session() async def entrypoint(ctx: JobContext): # グローバルなtrace_idを使用 trace_id_int = GLOBAL_TRACE_ID # trace_idを設定するためのSpanContextを作成 span_context = SpanContext( trace_id=trace_id_int, span_id=int.from_bytes(os.urandom(8), "big"), # ランダムなspan_id is_remote=False, trace_flags=TraceFlags(TraceFlags.SAMPLED), ) # カスタムtrace_idでコンテキストを設定 non_recording_span = NonRecordingSpan(span_context) ctx_with_span = trace.set_span_in_context(non_recording_span) # コンテキストをグローバルに設定 # これにより、LiveKit Agentsが作成するすべてのスパンがこのコンテキストを継承 token = trace.context_api.attach(ctx_with_span) コードの解説 NonRecordingSpan : 実際には記録されないスパン。コンテキスト伝播のためだけに使用します。 trace.set_span_in_context : スパンをコンテキストに設定します。 trace.context_api.attach : コンテキストをグローバルに設定し、後でデタッチするためのtokenを返します。 ステップ4: try-finallyでコンテキスト管理 コンテキストを設定した後は、必ずデタッチする必要があります。try-finallyブロックを使用して、確実にクリーンアップを行います。 try: # set up the langfuse tracer(コンテキスト設定後に呼び出す) trace_provider = setup_langfuse( metadata={ "langfuse.session.id": ctx.room.name, "room.name": ctx.room.name, } ) async def flush_trace(): trace_provider.force_flush() ctx.add_shutdown_callback(flush_trace) session = AgentSession(vad=silero.VAD.load()) @session.on("metrics_collected") def _on_metrics_collected(ev: MetricsCollectedEvent): metrics.log_metrics(ev.metrics) await session.start(agent=Kelly(), room=ctx.room) finally: # コンテキストをデタッチ trace.context_api.detach(token) 重要なポイント setup_langfuse()は必ずコンテキスト設定 後 に呼び出します。 finallyブロックで確実にデタッチし、コンテキストのリークを防ぎます。 修正後のトレース構造 トレース一覧の変化 修正後は、すべてのアクティビティが1つのトレースに統合されました。 修正後トレース一覧 Observation Levelが80となっており、多数のスパンが1つのトレースにまとまっていることが分かります。 トレース詳細 修正後トレース詳細 トレースの内部構造を見ると、期待通りの階層構造になっています。 タイムライン表示 タイムライン表示では、時系列で処理の流れが可視化されます。 修正後トレースのタイムライン1 修正後トレースのタイムライン2 エージェント交代のタイミングや、各ターンの実行順序、各ステップのレイテンシが視覚的に把握できます。 改善された点 すべてのアクティビティが1つのトレースに統合されました。 親子関係が正しく表現されるようになりました。 時系列での処理フローが追跡可能になりました。 ボトルネックの特定が容易になりました。 エージェント交代などの複雑なフローも明確に可視化されるようになりました。 まとめ 今回、LiveKit Agentsを使用して音声AIアプリケーションを構築し、Langfuseによる観測可能性を実装しました。 実際に使ってみて、CLIの見やすさや複数エージェント間の切り替えの容易さなど、開発体験の良さを実感しました。特に印象的だったのは、STT+LLM+TTSパイプラインの速度です。当初は遅いのではないかと懸念していましたが、プリエンプティブ生成やストリーミングTTSなどの最適化技術により、約2.33秒という実用的な速度を達成しており、体感的にも自然な会話が可能でした。 一方で、Langfuse統合時にトレースが分離してしまう問題に遭遇しました。この問題は、OpenTelemetryのコンテキストが非同期処理で適切に伝播されないことが原因でした。 カスタムトレースIDを生成し、グローバルコンテキストとして明示的に設定することで、すべてのアクティビティを1つのトレースにまとめることができました。修正後は、タイムライン表示で処理フローが可視化され、ボトルネックの特定も容易になりました。 非同期処理を多用するアプリケーションでOpenTelemetryを使用する際は、コンテキストの明示的な管理が重要です。今回の経験が、同様の問題に直面している方の参考になれば幸いです。 参考リンク LiveKit Agents GitHub Langfuse統合ガイド Langfuse統合アナウンス OpenTelemetry公式ドキュメント LiveKit料金プラン 関連GitHub Discussion
- Langfuseデータセット構築ガイド:UI・CSV・SDKの徹底比較
先日、新規アプリケーションのプロンプトを検討するにあたり、トレースデータ(ログ)が存在しない状態からデータセットを作成する必要がありました。 ある程度のデータ量を用意したかったため、手動入力を避ける方法(SDK や CSV)を調査・検証しました。 本記事では、 基本となる UI での登録手順と、今回試した一括登録の手順をそれぞれ整理し、使い勝手や特徴を比較した備忘録 として残します。 利用バージョン Langfuse : v3.127.0 OSS Python SDK : 3.9.0 全体の流れ Dataset が利用できるまでに以下の手続きが必要です。 データセット(dataset)の作成 データセットアイテム(items)の作成 本記事は、公式ドキュメントのこちらの 記事 を参考に実施しました。 1.データセット(dataset)の作成 UIを利用する方法 Datasets へ遷移し、[+ New Dataset] をクリックすることで新規のデータセットが作成できます。 Name のみ指定し、[Create dataset] で作成完了です。 特に複雑な操作も無く、直感的に作成できました。 SDKを利用する方法 今回はPython SDKを利用したので、Python のサンプルコードとなります。 こちらも既に アプリケーション内で Langfuse を利用したことがある方であれば、特に迷うことなく利用できるのではないかと思います。 langfuse.create_dataset( name=[データセット名], ) name をキーとした UPSERT が行われる仕様のようです。description や metadata を追加で指定したり、既に設定されている値を変更したりすると、データセットが更新されたことを UI 上で確認できました。 ただし、オプション未指定(または None )の場合は更新されず、既に設定されているものがそのまま残る挙動を確認しました。 また、データセット名がキーとなっているため、データセット名自体の変更に SDK は利用できません。名称を変更したい場合は、UI から操作する必要があります。 2,データセット(items)の作成 UIを利用する方法 作成したデータセットに対し、UI または CSV でアイテムの追加が可能です。 追加したいデータセットをクリックすると、デフォルトでは Runs タブが表示されるため、 Items タブに切り替えます。ここで UI での追加と、CSV の追加が行えます。 UIで一つずつ追加する [+New item] から追加します。 JSON 形式で記述する必要がありますが、ひとまずは Input のみ指定すれば [Add to dataset] で追加できます。 JSON 形式のハードルが高いことを除けば、こちらも複雑な操作は必要なく、概ね直感的な操作で作成出来ました。 CSVを利用して一括で追加する [Upload CSV] をクリックすると CSV のアップロード画面が表示されます。 CSV を用いたアップロードでは、先の UI 同様、 Input, Expected Output, Metadata のみが登録可能です。なお、CSVアップロードによる一括での UPSERT はできないようです。 CSV 登録の特徴として、「CSV のどの項目を各フィールドに割り当てるか」を UI 上でマッピング出来ることが挙げられます。この機能により、事前に アップロードフォーマットに合わせた CSV 形式への加工や、値をわざわざ JSON 形式に変換する必要が基本的にはない点は大きなメリットです。 例として、以下のような CSV を 作成し、UIから取り込んでみました。 id name num 1 apple 100g 2 egg 20 項目をすべて Input にマッピングします。 すると、ヘッダ行と値が適切に設定された JSON として Input に入力されました。 形式をあまり意識せずに登録できるため、既存データの CSVをとりあえず投げ込んでつくる、といった方法も取れます。個人的には、各種CSVアップロードはフォーマットの調整に時間がかかることが多いので、とても嬉しい機能でした。 もちろん、データセットからダウンロードしたファイルもそのままアップできます。Input として、ダウンロードした CSV の Input を割り当てれば、{ input: {...} } のような形式にならず、元のデータセットアイテムと同じ形式で登録されました。 SDKを利用する方法 データセット同様、特に難しい点はありませんでした。 id が省略されている場合は INSERT、指定されている場合は UPSERT となります。 langfuse.create_dataset_item( id=[ID], dataset_name=[データセット名], input=[入力データの内容], ) id は省略可能ですが、登録時に指定しておいた方が明示的に管理できるため、SDK を利用して作成する場合は、指定しておく方が利便性が高いと思われます。 省略した場合はUIからの登録同様、自動的にランダムな ID が付与されます。 指定する際の注意点 他のデータセットで既に利用している ID は利用できません。 (異なる組織、またはプロジェクトであれば利用可能です) CSVからの一括更新が出来ないため、実際は ID を指定しての1つずつの処理にはなりますが、データを全てアーカイブしたい場合など、まとめて更新を行うことが想定される場合には SDK を利用すると良さそうです。 まとめ 今回、UI と SDK の両方を触ってみて、データセットアイテムにある程度のデータ量が必要な場合は以下のように使い分けるのが良さそうだと感じました。 UI (CSV) データの更新が不要な場合 CSV マッピング機能が優秀 時間をかけずにデータを投入したい 登録にあたり一切の開発が不要 SDK データの更新が必要な場合 データ量が膨大な場合 CSV での処理に不安がある量の場合 手元のデータが複雑で、Input 用に何かしらの加工を行う必要がある場合 特に CSV アップロード時のマッピング機能は、データ前処理の手間が省けるので、今後は積極的に使っていきたいと思います。










