AI時代に、チケットというUIはまだ最適なのか

バックエンドエンジニアの伊藤です。

最近、AIコーディングエージェントを使った開発が当たり前になる中で、改めてチケットには何を書くのが最適なんだろう、そもそもチケットが生まれた経緯・役割はなんだったのだろう、AI時代にはチケットはどう変わるのだろう、ということを考えています。

ここ1、 2年で、AIによってコードを書く速度が格段に速くなってきました。簡単な修正であれば、実装そのものは数分で終わります。バグの再現条件を渡し、関連しそうなファイルを読ませ、修正案を出させ、テストまで書かせることもできます。

一方で、その作業をチケットに起こし、背景を書き、スコープを書き、受け入れ条件を書き、担当者を設定し、ステータスを動かす時間の方が重く感じることがありました。

ここで疑問が生まれます。

チケットは、AI時代のソフトウェア開発においても、最適なインターフェースといえるだろうか?

本稿では、チケットというUIそのものについて再考します。

本記事の概略

本記事の概略を先に書いておきます。

  • チケットは、実装が遅く、人間同士のハンドオフ(人から人への作業の引き渡し)が多く、文脈を一箇所に圧縮して渡す必要があった時代に適したインターフェース
  • AIによって実装、分解、レビュー補助、テスト生成が速くなると、チケットにすべてを詰め込むモデルは、少しずつ重くなっていく。
  • 短期的にはチケットの構造化が重要になるが、中長期では、チケットは開発の中心的なUIではなく、意図や証跡を見るための一つのビューになっていく
  • これから必要なのは、チケットだけを中心に置く開発ではなく、意図(Intent)を意識した開発。Ticket-firstではなく Intent-aware の開発。

チケットは何を解決していたのか

チケット、Issue、Bug report、Work item。呼び方はいろいろありますが、起源をたどると、もともとは未解決の不具合や変更要求を追跡するための仕組みでした。

たとえば Bugzilla は、自らを defect-tracking system または bug-tracking system と説明しています。Bugzilla の説明では、開発チームが未解決の bugs、problems、issues、enhancement requests、change requests を追跡するための仕組みとされています*1

しかし、チケットは単なるバグ管理表にとどまりませんでした。現代のIssue Tracking Systemは、requirements、development tasks、maintenance itemsなど、ソフトウェア開発に関わる複数の成果物を管理する基盤として使われています*2

つまりチケットは、以下のような役割を持っています。

役割 内容
入口 要望、バグ、問い合わせ、改善案を受け取る
キュー 後でやるかもしれない作業を貯める
優先順位づけ 何を先にやるべきかを決める
ハンドオフ PdM、エンジニア、QA、CS(カスタマーサクセス/カスタマーサポート)の間で仕事を渡す
責任の明確化 誰が担当するかを決める
進捗管理 Todo / Doing / Review / Done を可視化する
合意形成 仕様やスコープの認識を揃える
検証条件 何をもって完了とするかを定義する
証跡 なぜその変更をしたかを後から追えるようにする
メトリクス 件数、滞留、リードタイム、担当負荷を見る

言い換えると、チケットは「作業にまつわる不確実性を、一つのカードにまとめたもの」とも言えます。

チケットは文脈圧縮インターフェースだった

顧客の不満、PMの判断、エンジニアへの作業依頼、仕様のメモ、優先度、担当者、受け入れ条件、関連 PR、QA 結果……などなど、関係者ごとに散らばる前提や判断をチケットという一枚のカードに圧縮する。この働きは、「文脈圧縮」と呼ぶことができそうです。

チケットが多様な文脈を一枚のカードに圧縮することで、チームは非同期に仕事を進めることができました。

一方で、これまでチケットが有効だった背景には、いくつかの前提があったと考えています。

まず、実装には時間がかかりました。そのため、事前に何を作るかを整理し、優先順位をつけ、担当者を決める価値がありました。

次に、仕事は人間が順番に処理していました。そのため、チケットはキューとして機能しました。

さらに、開発には多くのハンドオフがありました。PMが背景を説明し、エンジニアが仕様を解釈し、QAが期待挙動を確認し、CSが顧客影響を追います。その間で文脈が失われないように、一枚のチケットに情報を圧縮して渡す必要がありました。

つまりチケットは、実装が遅く、ハンドオフが多く、非同期コミュニケーションにコストがかかる時代に適したインターフェースだった、と整理できます。

少し強く言うと、チケットは「実装が遅い時代の文脈圧縮フォーマット」とも言い換えられるかもしれません。しかし、AIコーディングエージェントが高速でコードを生成していく今、その前提はいま崩れつつあります。

※もちろん、規制対応、監査、大規模分散開発のように、チケットが提供してきた他の機能、たとえば証跡や責任の所在の明示は、AI時代でも変わらず必要になる場面があります。チケットの存在意義そのものを否定したいわけではありません。
これからのチケットの在り方については後半で触れたいと思います。

チケット以外の思想から学べること

チケット中心の開発を相対化する視点は私だけの意見ではなく、すでにいくつかの言説が存在しています。

ひとつはShape Upです。Basecampは、未着手アイデアを大量に抱えるバックログを「大きな重荷」だと表現しています*3。古いアイデアを定期的に整理することに時間が取られ、現在のプロジェクトを進める力が削がれる。だからこそ、少数のwell-shaped、リスクを下げた選択肢にbetするべき、という考え方です。

もうひとつはSciitという研究です。Issue trackerとSCM(ソースコード管理)が分離していると、開発者が両方の情報を手作業で整合させる必要があり、手間が大きいと指摘しています*4。Sciit はそのうえで、issueをSCMの中で(コードと同等に扱う)構成要素として置き、進捗や担当者をリポジトリの状態から推測できるようにする方向を示しています。

そしてGitHubのSpec Kitがあります。Spec Kitは、Spec-driven developmentという方法論を実装するためのツールキットです*5。これまでは「コードが正で、仕様は足場のようなもの」だったが、Spec Kitではむしろ「仕様が実装を生む中心的な成果物になる」という整理が示されています。Microsoft for Developersのブログでも、AI エージェントに正しい出力を出させるためには、まず良い文脈(context)が必要であり、決めないまま進めると codebase が de-facto specification になってしまう、と指摘されています*6

これらに共通するのは、チケットを保存形式・操作 UI・実行単位として一枚にまとめる前提を疑う動きだと考えます。バックログを軽くする、issueをSCMに埋め込む、仕様を実装の中心に置く。それぞれアプローチは違いますが、チケットが暗黙に担ってきた多くの責務を、別のレイヤーに分けていく方向性で重なっています。

チケットが担っていた役割を分解する

つまり、すべての情報をチケットが持ち続ける必要はないのかもしれません。
改めて、チケットがこれまで担ってきた役割を、AI時代に合う形で考え直してみましょう。

役割 従来の置き場 AI時代の移行先(候補)
入口
・要望
チケット ドキュメント、対話、フィードバック
背景
・意図
チケット 設計ドキュメント、ADR
仕様
・制約
チケット 実行可能な仕様(executable spec)、テスト
作業の実行 チケット AI エージェントが動的に生成する作業単位、PR
進捗管理 チケット PR、commit、test、deploy、AI エージェントのログから自動観測
検証 チケット テスト、CI、合成テスト、AI レビュー
証跡 チケット PR、commit、AI エージェントの実行履歴
振り返り チケット メトリクス、ダッシュボード
決定 チケット ADR、設計ドキュメント

具体的な移行先(情報の管理方法)については後で考えたいと思います。

ここでは一旦、役割の方に注目してください。これらの役割に共通するのは、作業そのものではなく、その背後にある「何を実現したいのか」という意図と、それを支える判断・制約・検証だった、と整理できます。チケットが守っていた中核は、作業(Task)よりも、その背後にある意図(Intent)だったとも言い換えられるのではないでしょうか。

AI時代のソフトウェア開発では、この「意図(Intent)」がより重要になってきていると感じます。

AIによって変わったこと

AI時代のソフトウェア開発では、単にコードを書くだけではなく、調査、分解、実装、テスト生成、レビュー補助、要約、トリアージ。ソフトウェア開発の周辺工程全体にAIが入り始めています。

象徴的な発信もあります。Linear は、2026 年 3 月 24 日にマーケティング上のメッセージとして "Issue tracking is dead." を打ち出しました*7。Linear によれば、従来のIssue trackingはPMが作業をスコープし、エンジニアが後から拾うハンドオフモデルのために作られたものだったとされています。今後のシステムは、ハンドオフではなく、文脈(context)とAIエージェント(agents)を中心に設計される、というのがLinearの主張です。

DORAの2025年の報告書も、AIは組織の既存の強みと弱みを増幅するものであり、AI導入による効果はツール単体ではなく組織システム全体に依存すると指摘しています*8。同報告書は、速度向上が下流の不安定さを露わにする、内部プラットフォーム品質がAI価値を決定づける、といった具体的な論点も提示しています。

ここから読み取れるのは、AI時代のボトルネックが、実装そのものから、意図の明確化、検証、責任分界へと移りつつある可能性です。
何を作るべきか、どこまで作るべきか、何を守るべきか、どうなれば正しいと言えるのか。そういった「意図(Intent)」こそが、より重要になっていく。このことはここ1, 2年で急速に共通認識になりつつあります。

なぜIntentが重要なのか

ここでいうIntentは、単なる目的文ではありません。

「なぜこれをやるのか」「何を満たせば成功か」「どこまでやれば終わりか」「どの制約を守るのか」「どんな副作用を避けたいのか」これらをまとめた、開発の起点です。

作業(Task)と意図(Intent)の違いを下記の表にまとめました。

Task Intent
粒度 作業単位 目的単位
寿命 完了したら閉じる 達成後も参照されることが多い
多い 少ない
主な担い手 AI エージェント 人間
役割 実行 判断・検証

AI時代にIntentの重要性が増すと考えられる理由は、AIが作業を高速・大量に実行できる場面が増えてきたからです。

人間の開発者であれば、曖昧なチケットでも文脈から意図を補完して、「たぶんこういうことだろう」と判断できます。AIも確認はできますが、人間より速く・大量にコードを生成できるため、曖昧な指示を誤って解釈してしまった場合に積み上がる量が桁違いに大きくなります。

そのため、AI時代には「実装できること」よりも、「意図に沿って実装されていること」の方が重要になっていくと考えています。

Taskは完了すれば閉じます。しかしIntentは、作業が完了した後も、後続の意思決定で参照されることが多い情報です。「なぜこれを作ったのか」「制約は何だったか」「どうなれば成功か」を、後から検証・参照できることが重要です。

この考え方に立つと、開発の中心にあるのが必ずしもチケットである必要はなくなります。

むしろ、これまでどおりチケットを中心に扱うと、Intentはチケット内に閉じ込められることになります。こういった方法では、複数のチケットやPRが同じIntentを参照する場面で、参照の起点がチケットの中に隠れてしまいます。また、チケットが閉じた後も生き続ける情報の追跡もしにくくなる問題もあります。

では、Intentを軸にすると、具体的にどういう情報の置き方になるのか。次節で見ていきます。

Intentを軸に、開発情報をつなげて考える

本稿では、大それた新しいシステムや大きな方法論を提案したいわけではありません。

たとえば、ある改善について次のようなIntentがあったとします。

Intent:
ユーザーが問い合わせをせずに、自分で退会手続きを完了できるようにする

この Intent の周辺には、次のような情報が紐づきます。

例えばこういった形で必要な情報を管理可能です。

  • 背景や意思決定はNotionや設計ドキュメントに置けばよい
  • 作業単位や担当はLinearやGitHub Issuesに置けばよい
  • 実装の証跡はPRに残る
  • 検証結果はCIやテストレポートに残る
  • リリース状況はDeploymentに残る
  • 運用後の変化はメトリクスや外形監視で確認できる

情報を(従来のチケットのように)一箇所に集めてはいません。適切に残されたそれぞれの情報が、どのIntentに紐づいているのか。そのつながりを追えることが重要です。(ここで新しいシステム名や方法論を提案したいわけではありませんが、 便宜的に「Intent Graph的な構造」と呼ぶことはできそうです)

もちろん、これをきれいに整備するのは簡単ではありません。Intentを軸にして、開発に関わる情報をつなげて扱いたい、という思いはありつつも、私たち自身もこの形をきれいに整備できているわけではないのです。現実には、背景はNotionにあり、作業はLinearにあり、実装はGitHubのPRにあり、結果はアプリ上にあり、それらのつながりは人間の記憶に依存していることも多いのが実情です。

実運用上は、Intent IDをどこに置くかという規約と、ツール間のwebhookやリンクの整備が必要になります。メトリクスからIntentへの逆引き(ある数値変動がどのIntentに紐づくか)を機械的に追うのも、現状ではまだ弱い部分です。このあたりの人間の規律と運用コストの部分で課題は残りますが、今後あるべき姿としてはこういった構造なのではないか、と考えています。

※「Linearや Jira の階層機能(Initiative / Project / Issue / Sub-issue)で十分では?」と思われるかもしれません。
たしかに、既存ツールの組み合わせで近い構造は組めるはずです。そもそも、ある程度成熟したチーム・組織であれば、すでに似たようなことはやっているのではないかと思います。ただ、今あるツールをうまく使うよりもより良いUIがあれば、より広く実践される可能性はあると思います。

Ticket-firstからIntent-awareへ

昨今、チケットの構造化に関する話題が増えています。背景、目的、制約、非スコープ、受け入れ条件を今まで以上にきちんと書きましょう、という流れです。これは短期的には正しい方向です。曖昧なチケットは、曖昧な実装を生みます。AIは曖昧な指示に対しても、それらしい実装を高速に返してしまうことがあるため、目的と制約を明示する価値はむしろ高まります。

しかし中長期では、状況は変わってくると考えています。

「良いチケットを書きましょう」という議論は、チケットが開発の中心にあることを前提にしています。本当に考えたいのは、チケットというUIが、AI時代の開発における最適な中心であり続けるのか、という問いです。

チケットはこれまで、意図、作業、責任、進捗、検証、証跡を一枚にまとめる便利な箱でした。その便利さは、実装が遅く、ハンドオフが多く、文脈を一箇所に圧縮する必要があった時代には大きな価値を持っていました。

しかしAIが文脈を保持し、関連するIntent、過去の判断、制約、テスト結果を踏まえて作業できるようになると、すべてをチケットに書く必要は薄れていきます。

細かい実装ToDoは、AIエージェントが動的に生成するようになっていきます。進捗ステータスは、PRやCI/CDから自動観測される方向に進みます。バグ調査は、ログや監視アラートから直接AIエージェントが始める運用も、商用ツール(SentryのAutofix、DatadogのBits AI等)で部分的に現れ始めています。

仕様は、ドキュメントだけでなく、実行可能な仕様(executable spec)としてCIに組み込まれていきます。

一方で、人間の判断は残ります。何を作るべきか。なぜ今やるのか。どんな副作用を避けたいのか。法務・セキュリティ・倫理上、何を許容しないか。これらはIntentの領域であり、AIが代替するのは難しい部分です。

AIが高速に実装できるようになるほど、「何を作るか」よりも、「なぜ作るのか」「何を守るのか」「どうなれば成功なのか」が重要になります。DORA の 2025 年の報告書が指摘するように、速度向上は下流の不安定さを露わにし、その不安定さを支えるのは、内部プラットフォームの品質や、小さく変更を進める運用のような、組織システム側の力です*9

だからこそ、これから必要なのは、チケットが担っていた役割を分解し、Intentを軸に再配置することです。Intentを意識して、ドキュメント、チケット、PR、テスト、監視、メトリクスをつなげていく方向に、少しずつ組み直していくことです。

その過程で、チケットは消えるのではなく、置かれる位置が変わると考えています。

チケットは、作業管理の唯一の中心ではなくなっていきます。作業の記録(record of work)であると同時に、Intentを軸につながった情報を、人間が見るための一つのビューになっていくのではないかと思います。

似顔絵
書いた人:伊藤

参考文献・引用元

*1:Bugzilla Project, "About - What is Bugzilla?", Bugzilla.org. URL: https://www.bugzilla.org/about/ (閲覧日: 2026-04-27)。Bugzilla が defect-tracking / bug-tracking system であり、bugs、problems、issues、enhancement、change requests を追跡するための仕組みである、という説明の出典です。

*2:Lloyd Montgomery, "Issue Tracking Ecosystems: Context and Best Practices," arXiv:2507.06704, 2025. URL: https://arxiv.org/abs/2507.06704 (閲覧日: 2026-04-27)。Issue Tracking Systems が requirements、development tasks、maintenance items などを扱う基盤であり、issue 間リンクや外部ツールとのリンクによって traceability を支える、という整理の出典です。

*3:Ryan Singer / Basecamp, "Bets, Not Backlogs," Shape Up, Chapter 7. URL: https://basecamp.com/shapeup/2.1-chapter-07 (閲覧日: 2026-04-27)。バックログを大きな重荷と捉え、古いアイデアの整理ではなく少数の well-shaped な options に bet する、という考え方の出典です。

*4:Edwards Nystrom, Dhitiwat Jongsuebchoke, Tim Storer, "Sciit: Embedding issue tracking in source control management," Science of Computer Programming, Vol. 206, Article 102628, 2021. DOI: 10.1016/j.scico.2021.102628. URL: https://doi.org/10.1016/j.scico.2021.102628 (閲覧日: 2026-04-27)。Issue tracker と SCM を分離して管理する摩擦、issue を SCM 内の first-class change control item として扱い、status や participants などを SCM の状態から推測する設計の出典です。

*5:GitHub, "Spec Kit Documentation," GitHub Pages. URL: https://github.github.com/spec-kit/ (閲覧日: 2026-04-27)。Spec-Driven Development が仕様を実行可能なものとして扱い、仕様が working implementation を直接生成するという説明、および intent-driven development の整理の出典です。

*6:Den Delimarsky, "Diving Into Spec-Driven Development With GitHub Spec Kit," Microsoft for Developers Blog, 2025-09-15. URL: https://developer.microsoft.com/blog/spec-driven-development-spec-kit (閲覧日: 2026-04-27)。AI agent に正しい出力を出させるには良い context が必要であり、何をなぜ作るのかを決めないまま進めると codebase が de-facto specification になる、という説明の出典です。

*7:Linear, "Issue tracking is dead," Linear.app/next, 2026-03-24. URL: https://linear.app/next (閲覧日: 2026-04-27)。従来の issue tracking が PM からエンジニアへの handoff model に合わせて作られたこと、今後は context と agents を中心に開発システムが変わる、という主張の出典です。

*8:DORA / Google Cloud, "State of AI-assisted Software Development 2025," DORA Research, 2025. URL: https://dora.dev/dora-report-2025/ (閲覧日: 2026-04-27)。AI は組織の既存の強みと弱みを増幅するものであり、AI 導入の効果はツール単体ではなく組織システムに依存する、という整理の出典です。

*9:DORA / Google Cloud, "State of AI-assisted Software Development 2025," DORA Research, 2025. URL: https://dora.dev/dora-report-2025/ (閲覧日: 2026-04-27)。AI は組織の既存の強みと弱みを増幅するものであり、AI 導入の効果はツール単体ではなく組織システムに依存する、という整理の出典です。

LLMアプリケーションを改善したければLangfuseを使ってみてほしい~トレース確認からプロンプトの評価までの流れ

1. はじめに

LLMアプリケーションを開発・運用していると、ぶつかりがちな壁があります。「なぜこの出力になったのか分からず改善が進まない」「プロンプトやモデルを変更したいけれど影響範囲が読めない」などです。

私たちテックドクターでは、こうした課題に対して、LLMアプリケーションのオブザーバビリティ・評価プラットフォームであるLangfuseを使うことで対処しています。

この記事では、弊社のヘルスケアアプリケーション開発の実例を交えながら、トレースによる挙動の可視化、プロンプトの一元管理、データセットの構築、そして簡易的なプロンプト評価(Prompt Experiments)の実行まで、Langfuse活用の一連の流れを解説していきます。

こんな人に読んでほしい
  • LLMアプリケーションの開発・運用に携わるエンジニア
  • Langfuseの導入を検討している方



2. 導入背景

弊社ではLLMを活用したアプリケーションを開発しています。チャットボットへの相談、バイタルデータの要約、メッセージ履歴のサマリー生成などの機能を提供しており、運用中のプロンプトは結構な数になります。
その開発を進める中で、次の2つの課題が目立つようになりました。

LLMの出力の原因を追いにくい

ユーザーから「回答がおかしい」と報告を受けても、そのときの入力や会話履歴を追跡するのが困難でした。
アプリケーションログを手がかりにLLMへの入力を再構成していたのですが、手作業となり時間がかかるうえに会話のコンテキストが欠落しやすいです。原因の特定がなかなか進みませんでした。

プロンプト変更の効果を簡単に確認する手段がない

プロンプトの改善にはエンジニアだけでなく、ドメイン知識を持つ非エンジニアメンバーも関わることがあります。そのため、非エンジニアでも簡単にGUIからプロンプトの検証ができる環境がほしいと考えました。
弊社ではこうした課題への対策として、Langfuseを導入しました。


3. Langfuseの概要

Langfuseとは

Langfuse は、LLMアプリケーション向けのオープンソースのLLMエンジニアリングプラットフォームです。

LLMへの入出力やトークン消費量をトレースとして記録・可視化できるほか、プロンプトのバージョン管理やデータセットを用いた評価まで、一つのプラットフォームで完結して行うことができます。SaaS版(US / EU / HIPAAリージョン)およびセルフホスティング環境で利用可能です。

Langfuseを導入すると、主に次のようなことができるようになります。

  • オブザーバビリティ……LLMへの入出力、処理の流れ、レイテンシ、トークン数・コストをWeb UIで確認できます。ユーザーIDやセッションIDでフィルタリングして、特定ユーザーの会話を追跡することも可能です。
  • プロンプトの管理……プロンプトをコードから分離し、Langfuse上で作成・編集できます。バージョン管理やラベル付け(production / staging)にも対応しており、コードのデプロイなしにプロンプトの切り替えやロールバックが行えます。
  • 評価……データセット(入力と期待出力のペア)を用意し、プロンプトやLLMアプリケーションの出力品質を評価できます。

参考: Langfuse Overview

なお、本記事で扱う主な機能は以下の4つです。

名称 機能
Tracing LLMの入出力やレイテンシ、トークン数・コストを階層的に記録・可視化します
Prompt Management プロンプトのバージョン管理とラベル付け(production / staging)を行います
Datasets 評価用の入力データ(と期待出力)をまとめて管理します
Prompt Experiments プロンプト × データセットの組み合わせで評価を実行し、結果を比較します

このほかにも、ユーザーフィードバックをスコアとして記録する機能、LLM-as-a-Judge(LLMを評価者として出力品質を自動判定する手法)による自動評価、Annotation Queue(評価対象をキューに溜めて人手でラベリングを進める機能)などがありますが、本記事ではとくに上記4つに焦点を当てます。

導入方法

Python SDKを使う場合

(SaaS版の場合)パッケージをインストールして環境変数を設定するだけで利用を開始できます。

pip install langfuse

# .env
LANGFUSE_SECRET_KEY="sk-lf-..."
LANGFUSE_PUBLIC_KEY="pk-lf-..."
LANGFUSE_BASE_URL="<https://cloud.langfuse.com>"  # EU region
# LANGFUSE_BASE_URL="<https://us.cloud.langfuse.com>"  # US region


Langfuseクライアントは get_client() で初期化します。環境変数を設定していれば、引数なしで認証情報が自動的に読み込まれます。

from langfuse import get_client

langfuse = get_client()


Langfuse Python SDKにはトレースの方法が複数用意されています。
以下はコンテキストマネージャを使用する例です。start_as_current_observation() を使うとブロック内の処理が自動的にトレースとして記録されます。 ほかにも、デコレータ を使用したり、手動で観測値を設定 したりすることもできます。

from langfuse import get_client

langfuse = get_client()

# コンテキストマネージャを使用してスパンを作成する
with langfuse.start_as_current_observation(
    as_type="span",
    name="process-request"
) as span:
    span.update(output="Processing complete")

    # LLM呼び出しにおいて、ネストされた生成を作成する. LLM呼び出しを記録する場合は as_type="generation" を指定
    with langfuse.start_as_current_observation(
        as_type="generation",
        name="llm-call",
        model="gpt-4o"
    ) as generation:
        # ここにLLMを呼び出すためのロジックを記述します
        generation.update(output="Generated response")

# すべてのスパンは、それぞれのコンテキストブロックを抜ける際に自動的にクローズされます。

参考: Python SDK - Getting Started / Instrumentation


フレームワークインテグレーションを使う場合

別のトレース方法として、フレームワークインテグレーションを使う方法も紹介します。

Langfuse SDKはOpenTelemetryベースで構築されているため、Pydantic AIやGoogle ADKなどのOTel対応フレームワークを使っている場合は、@observe() デコレータやコンテキストマネージャを使わなくてもトレースを自動記録できます。Langfuseはインテグレーションが充実しており、さまざまなLLMアプリケーションフレームワークと手軽に統合できます。

例えば、以下はPydantic AIを使用する例です。

from langfuse import get_client
from pydantic_ai import Agent

# Langfuseクライアントを初期化
langfuse = get_client()

# Pydantic AIのすべてのエージェントでインストルメンテーションを有効化
Agent.instrument_all()

# エージェントを作成して実行するだけでトレースが自動記録される
agent = Agent("openai:gpt-4o", instrument=True)
result = agent.run_sync("こんにちは")

参考: Pydantic AI Integration

以上がLangfuseの概要です。次のセクションからは、LLMアプリケーション改善の流れを追う形で、Langfuseの各機能について詳しくご紹介していきたいと思います。
トレースの確認、プロンプトの管理、プロンプト評価の実行の順で見ていきましょう。


4. トレースでLLMの挙動を確認する(Tracingについて)

なにか問題が起きたとき、トレース機能によってLLMへの入出力や処理の流れを追跡できます。

トレースの階層構造

Langfuseのトレースは、LLMアプリケーションの1回のリクエスト処理(ユーザー入力から応答生成まで)の実行を階層構造で記録します。 トレースの中にはobservationと呼ばれる個々の処理ステップがネストされており、それぞれのobservationには処理の種類を表すtypeが割り当てられます。 代表的なobservation typeは以下の3つです。

  • Span: 任意の処理区間(前処理、後処理など)
  • Generation: LLM呼び出し。フレームワークインテグレーション経由であれば入力プロンプト、出力テキスト、モデル名、トークン数、コストが記録される。
  • Tool: 天気予報のAPI呼び出しのような、ツール呼び出しを表す

このほかにも用途に応じた複数のobservation typeが用意されており、フレームワークインテグレーション経由では、自動的に適切なtypeが設定されます。(コンテキストマネージャ経由の場合は自分でtypeを指定することになります)

ここからは、記録されたトレースを管理画面上でどのように確認・活用するかを見ていきましょう。

一覧表示からトレースを確認する

管理画面のトレース一覧を開くと、記録されたトレースが表示されます。

画面キャプチャ

一覧画面では、トレース名・実行日時・レイテンシ、実際の入出力、使用トークン数とコスト、付与されたタグやメタデータ、ユーザーID・セッションIDなどを確認できます。

フィルター機能を使えば、ユーザーID・セッションID・タグ・日時範囲などの条件でトレースを絞り込めます。

また、入出力を含む各カラムに対してテキスト検索も可能なので、目当てのログ情報を容易に探し当てることができます。

一覧から任意のトレースを選択すると詳細画面が開き、内容がツリー形式で表示されます。 各ノードを選択すると、その処理ステップの入力・出力・実行時間・トークン数などが表示されます。

このように詳細画面で関連するトレース情報を一覧できることで、いわゆるAgentic Workflowと呼ばれるような、LLMを多段階的に呼び出すアプリケーションを構築している場合は、デバッグがとても捗ります。

セッション単位の会話追跡

弊社では活用していませんが、トレースにセッションIDを付与しておくと、管理画面から同一セッションに属する複数のトレースをまとめて閲覧できます。

会話の流れの中で「どの時点から出力がおかしくなったのか」を時系列で追えるので、こちらもデバッグに役立ちそうです。

ユーザーフィードバックの記録と活用

Langfuseでは、トレースに対して スコア(Score) を紐づけて記録できます。SDK経由でプログラムから記録したり、管理画面のUI上で人手で記録することも可能です。

弊社のアプリではユーザーがLLMの回答に対してアプリ内でGood/Badの評価を行える仕組みを実装しており、その結果をLangfuse上でトレースに紐づくスコアとして記録しています。

from langfuse import get_client

langfuse = get_client()

langfuse.create_score(
    trace_id="trace-id",
    name="user-feedback",
    value=1,       # Good: 1, Bad: 0
    data_type="NUMERIC",
    comment="ユーザーからのフィードバック",
)


記録されたスコアはトレース詳細画面に表示されます。トレース一覧画面ではスコアによるフィルタリングも可能で、たとえば「Bad評価が付いたトレースだけを抽出して、共通するパターンを分析する」といった使い方もできます。
参考: Tracing / Scores

5. プロンプトを管理する(Prompt Managementについて)

トレースで問題のあるプロンプトを特定できたら、改善を行います。そのとき役に立つのがプロンプト管理の機能です。

Prompt Managementの概要

Langfuseの Prompt Management 機能を使うと、プロンプトをコードから分離してLangfuse上でバージョン管理できます。 各バージョンには productionstaging といったラベルを付けられ、コードのデプロイなしにプロンプトの切り替えやロールバックが可能です。
プロンプトの形式にはtext(単一文字列)とchat(メッセージ配列)の2種類があります。チャットボットのようなアプリケーションでは chat 形式を使い、各メッセージの rolecontent を定義する必要があります。

バージョン管理とラベル

プロンプトを保存するたびにバージョン番号が自動でインクリメントされます。

各バージョンにはラベルを付与でき、アプリケーションからは「production ラベルの付いた最新バージョン」を取得する、という運用が可能です。

たとえば、新しいプロンプトをまず staging ラベルで作成し、評価(後述するPrompt Experiments)を経て問題がなければ production ラベルに昇格させる、というワークフローが実現できます。

variablesとmessage placeholder

プロンプト内に動的な値を埋め込むことで、プロンプトテンプレートとして使用できます。埋め込む値としてはvariables、prompt references、message placeholders がサポートされています。本記事では、主に variables と message placeholder を扱います。

  • variables: {{変数名}} の形式でプロンプト本文に記述し、実行時に文字列へ置換します。
あなたはヘルスケアアシスタントです。
ユーザーのタイムゾーンは {{TIMEZONE}} です。
現在時刻は {{CURRENT_TIME}} です。
{{LANGUAGE}} で回答してください。

  • message placeholder: chat形式のプロンプトで使う仕組みです。会話履歴のような可変長のメッセージ配列を動的に展開するためのもので、 placeholder タイプのメッセージを追加することで設定できます。

ここで定義したmessage placeholderの使用方法は、次のDatasetのセクションで説明します。

[
  { "role": "system", "content": "あなたはヘルスケアアシスタントです。" },
  { "type": "placeholder", "name": "MESSAGE_HISTORY" },
  { "role": "user", "content": "{{USER_PROMPT}}" }
]


UIでのプロンプト作成・編集

管理画面のプロンプト一覧から新規作成ボタンをクリックすると、プロンプトの作成画面が開きます。

ここでプロンプトの識別名、形式(text / chat)、プロンプト本文を入力して保存することで、プロンプトの追加が行えます。必要に応じて、モデル名や temperature といったパラメータも設定できます。

既存のプロンプトを編集する場合は、プロンプト一覧から対象を選択し、本文を修正して保存します。保存するたびに自動的に新しいバージョンとして追加され、過去のバージョンもすべて保持されるため、いつでも差分の確認やロールバックが可能です。

SDKでのプロンプト作成

プロンプトはUIだけでなく、Python SDKからもプログラムで作成・更新できます。

from langfuse import get_client

langfuse = get_client()

# text形式のプロンプトを作成
langfuse.create_prompt(
    name="health-assistant",
    type="text",
    prompt="あなたはヘルスケアアシスタントです。{{LANGUAGE}} で回答してください。",
    commit_message="create initial version",
    tags=["base"],
)

# chat形式のプロンプトを作成
langfuse.create_prompt(
    name="health-assistant-chat",
    type="chat",
    prompt=[
        {"role": "system", "content": "あなたはヘルスケアアシスタントです。"},
        {"type": "placeholder", "name": "MESSAGE_HISTORY"},
        {"role": "user", "content": "{{USER_PROMPT}}"},
    ],
    commit_message="create initial version",
    tags=["eval"],
)

弊社では、アプリケーションが実際に使用するプロンプトはGoogleのCloud Spannerで管理しており、Langfuse上には評価中のプロンプトを保存しています。両者のプロンプトの不整合を防ぐために、Spanner側のプロンプトをマスタデータとして、これをLangfuse側に反映するスクリプトを用意しています。
参考: Prompt Management Overview / Variables / Message Placeholders

6. データセットを用意する(Datasetについて)

ここまで、Langfuseのトレースおよびプロンプトの管理機能について説明してきました。これから説明するDataset、Prompt Experimentsはプロンプトの評価に関する機能です。

データセットの概要

データセット(Dataset)は、プロンプト評価に使用する入力データの集合です。
Langfuseのデータセットは、おもに以下の要素で構成されています。

  • Dataset: DatasetItemの集合。
  • DatasetItem: 個別の入出力ペア。input(入力値)、expected_output(期待する出力、任意)、metadata(カスタム属性、任意)を持つ
  • DatasetRun: データセットに対して評価を実行した結果。

主なデータセットの構築方法としては、トレースから取り込む方法、1から手動で作成する方法、CSVファイルからインポートする方法などがあります。

トレースからデータを追加する

本番やステージング環境で記録されたトレースをもとに、評価用データを作成できます。

具体的には以下の手順でDatasetを作成できます。

  1. トレース詳細画面で、データセットに追加したいobservationを選択する
  2. データセットへの追加ボタンをクリックする
  3. 次項「データセットアイテムの構造」に記載の形式に合わせて整形し、保存する

実際のユーザー入力をそのまま評価用データとして再利用できるため、現実的なテストケースを手早く蓄積できるのが利点です。

ただし、トレースに記録されたデータはプロンプトのvariablesやplaceholderの構造とは形式が異なることが多いため、データセットに追加する段階でプロンプトの変数構造に合わせた整形が必要になります。

手動でデータを作成する

プロダクトの要件を満たす典型的な入力パターンや、特定のエッジケースをあらかじめ用意しておく方法です。まだトレースが十分に蓄積されていない初期段階のデータセット構築に向いています。

なお、CSVファイルからデータセットを一括インポートすることも可能です。大量のテストケースをスプレッドシート等で管理している場合に便利です。

データセットアイテムの構造

データセットアイテムの inputexpected_output には値を格納できます。UIのPrompt Experimentsで利用する場合は、input はJSON objectである必要があり、実行時にそのJSONキーを見て、プロンプトテンプレートの同名のvariablesやmessage placeholderに値が自動で埋め込まれます。そのため、input のキー名はプロンプト側の変数名と一致させておく必要があります。

たとえば、セクション5で紹介したchat形式のプロンプトは次のような構造でした。

[
  { "role": "system", "content": "あなたはヘルスケアアシスタントです。" },
  { "type": "placeholder", "name": "MESSAGE_HISTORY" },
  { "role": "user", "content": "{{USER_PROMPT}}" }
]


このプロンプトに対してPrompt Experimentsを実行するには、データセットアイテムの inputMESSAGE_HISTORYUSER_PROMPT というキーが含まれている必要があります。弊社ではこれに加え、プロンプト内で使用している TIMEZONECURRENT_TIMELANGUAGE も共通で含めるようにしています。

フィールド 説明
USER_PROMPT ユーザーの入力テキスト
MESSAGE_HISTORY 会話履歴のメッセージ配列(会話履歴を使うプロンプトの場合)
TIMEZONE ユーザーのタイムゾーン(例: Asia/Tokyo)
CURRENT_TIME エージェント実行時の日時
LANGUAGE ユーザーの言語(例: 日本語)

実際の input は以下のようになります。

{
  "MESSAGE_HISTORY": [
    { "role": "user", "content": "寝つきが悪くて困っています" },
    { "role": "assistant", "content": "最近の生活習慣はいかがですか?" }
  ],
  "TIMEZONE": "Asia/Tokyo",
  "USER_PROMPT": "何か改善できることはありますか?",
  "CURRENT_TIME": "2026-03",
  "LANGUAGE": "日本語"
}

参考: Datasets


7. プロンプトを評価する(Prompt Experimentsについて)

UIから評価を実行する

プロンプトとデータセットの準備ができたら、Prompt Experimentsで評価を実行します。

  1. 管理画面のデータセット一覧から対象のデータセットを開く
  2. Experiment実行ボタンをクリックし、Prompt Experimentを選択する
  3. 名称、評価対象プロンプト、LLMのAPI接続設定、対象データセットなどを入力する。(LLM-as-a-Judgeによる自動評価を行いたい場合はEvaluatorもあわせて設定する)
  4. 実行を開始する

結果の確認と比較

実行が完了したら、Dataset Runs画面で結果を確認します。以下はテスト的に評価を実行した際の結果画面です。

画面キャプチャ

結果画面では、前述した inputexpected_output に加え、LLMからのレスポンス、そのコストとレイテンシなどを横並びで確認できます。

期待どおりの結果が得られなければ、Prompt Managementでプロンプトを修正し、再度Experimentを実行します。このサイクルを繰り返すことで、プロンプトの品質を段階的に高めていくことができます。
参考: Prompt Experiments (UI) / Datasets


8. まとめ

本記事では、Langfuseを使ったLLMアプリケーション改善の流れを、トレースの確認からプロンプト評価の実行まで一通り紹介しました。

トレースによって、問題が起きたときにLLMへの入出力や処理の流れを管理画面上で追跡できるようになります。

そのうえで、Prompt Managementでプロンプトをバージョン管理し、データセットを用意してPrompt Experimentsで修正前後の出力を並べて比較することで、プロンプトの改善サイクルを効率的に回すことができます。

今回は目視での確認・比較を中心に紹介しましたが、Langfuseにはこのほかにも、LLMを評価者として出力品質を自動スコアリングするLLM-as-a-Judgeや、Python SDKによるLLMアプリケーションの評価といった機能も用意されています。更に評価を自動化したい場合はこれらの機能の使用を検討すると良いかもしれません。




書いた人:大瀧

「自由な解析」から「確かなエビデンス」へ。ウェアラブルデータ解析におけるSAP(統計解析計画書)の重要性

はじめまして。テックドクターでデータ解析を担当している藤野です。

私はテックドクターへ参画する前は、約10年間にわたり医薬品開発における治験の統計解析に従事してきました。現在はその経験を活かし、ウェアラブルデバイスのデータを用いて、医療現場で活用されるデジタルバイオマーカー(※1)の開発に取り組んでいます。

ウェアラブルデータは、従来の臨床試験のデータに比べてデータ量が膨大で、項目の種類も多岐にわたります。そのため、解析の自由度が非常に高く、データサイエンティストとして非常に面白い領域です。しかしその反面、解析者の意図によって「都合の良い結果」を導き出してしまうバイアス(P-hacking等)が入り込みやすいという危険性もあります。

このように都合の良い解析とならないために、治験における解析の「規律」であるSAP(統計解析計画書)の考え方をウェアラブルデータ解析においても実践しています。
本記事では、ウェアラブルデータ解析においてなぜSAPが必要なのか、そして具体的にどのような点に留意して作成すべきかをご紹介します。

※1 デジタルバイオマーカー……デジタルデバイスで測定した『日常データ』をもとにした、病気の早期発見や治療につながる客観的指標(過去記事参照

SAP(統計解析計画書)とは

SAP(Statistical Analysis Plan:統計解析計画書)とは、解析を始める前に「どのようなデータを使い、どのような解析を行うか」を細かく記載した文書のことです。
医薬品開発で必須とされる国際的なガイドラインである「ICH-E9(臨床試験のための統計的原則)」(※2)に基づいて、解析の客観性と再現性を担保することを目的としています。

※2 PMDA(独立行政法人 医薬品医療機器総合機構)「ICH-E9(臨床試験のための統計的原則)」, URL: https://www.pmda.go.jp/int-activities/int-harmony/ich/0031.html

なぜ機械学習全盛の今、SAPが必要なのか

最新のアルゴリズム開発とは対極にある「古い慣習」のように思えるかもしれません。しかし、実はアルゴリズム開発(機械学習)の前段階として、統計的な検証を行うことには大きなメリットがあります。

①客観性の担保
解析前に解析方法を決定することで、有意差が出るまで条件を変えて解析し直すことを防ぎます。

②エビデンスの作成
解析プロセスを文書化しておくことで、社内外のステークホルダーに信頼されるエビデンスを作成します。

これらはSAPの一般的なメリットですが、くわえてウェアラブルデータ解析特有の課題への対策としてもSAPは有効です。次にご説明します。

ウェアラブルデータ解析における「信頼性」の課題

ウェアラブルデータ解析においては、その自由度の高さから、特有のリスクが存在します。

多重性の問題
心拍、睡眠、歩数など、ウェアラブルデバイスからは大量の変数が得られます。
これらを総当たりで検定にかければ、偶然有意差が見つかるリスクが高まります。

欠測バイアス(※3)の問題
日常生活では、未装着や充電切れなどによるデータの欠測が発生します。
欠測の扱いで結果が大きく変わってしまうことがあります。

これらの日常のデータを信頼できるエビデンスとするために、SAPによる事前の規定がとても有効です。

※3 バイアス……解析者の思い込みやデータの偏りによって、真実とは異なる結果が出てしまうことを指します。

イメージ図

SAPの実践例と重要なポイント

具体的に、どのような点をSAPで定義すべきでしょうか。事例をもとに3点紹介します。

有効なデータと解析対象の定義

ウェアラブルデータの場合、「データが存在する=解析に使える」とは限りません。解析前に、評価の基盤となるデータの質を定義しておく必要があります。

歩数データの例:
次のような基準をあらかじめ規定しておきます。

  • 心拍データが記録されている時間を装着時間とし、「1日の装着時間が70%(16.8時間)以上」の日を有効装着日と規定する
  • さらに、評価期間7日間のうち、少なくとも4日以上の有効装着日が存在する被験者のみを解析対象とする
  • 最後に、各有効装着日の1日の歩数を算出し、評価期間中の平均値を解析に用いる

データ有効性の判定チャート

生データから加工変数への変換ロジック

高頻度な原データを解析可能な形に集約する計算式を、SAPで明文化します。

心拍変動の例:
以下のように基準を設定します。

  • 15分間隔の区間集計で算出するロジック
  • 「30bpm以下の心拍数」など生理学的にあり得ない値を異常値として除外する閾値
「探索」と「検証」の切り分け

自由にデータを深掘りして分析することで新たな知見を得るための探索的な解析項目と、事前に決められた手順で仮説を検証するための解析項目を明確に分けます。

SAP導入の効果とメリット

ここまで主にSAPがデータ解析作業にもたらす利点を紹介してきましたが、それ以外にもSAPの導入によって、プロジェクト全体に様々なメリットがあります。

解析結果の頑健性(ロバストネス)の向上
解析を事前に規定することで、「データを見てからルール(データの採用基準や統計手法)を決める」という後出しジャンケンを防ぎます。これにより、誰が解析しても結論の変わらない安定した解析結果を得ることができます。

ロバストネス向上のイメージ図

チーム内外の円滑な議論
解析方針が詳細にドキュメント化されているため、社内のレビューはもちろん、外部のステークホルダー(スポンサー・アカデミア・CRO等)や規制当局(PMDA等)との議論がスムーズになります。

将来的な治験への布石
探索的な臨床試験の段階からSAPを導入することで、将来的に治験に進む際に、スムーズに移行できる下地となります。

また、現場で実務を担うステークホルダーにも具体的なメリットがあります。

ステークホルダー 具体的なメリット
データサイエンティスト 個別のデータ採否に迷う必要がなく、解析の再現性と効率が向上します。
統計解析担当者 欠測や中間事象(服薬不遵守や併用薬の使用)に対して、治験の標準的な考え方を適用でき、ウェアラブルデータ以外の解析と整合性が取れます。
薬事・マーケティング担当者 事前に合意・規定されたプロセスに基づくエビデンスとなるため、社内外への説明に客観的な裏付けができます。

実践を通じた気づき

10年の治験解析経験を経て、今ウェアラブルデータ解析の舞台で改めて感じるのは、SAPは決して「自由な解析を制限するもの」ではないということです。むしろ、解析結果に胸を張って「これは信頼できる結果です」と言うための武器になります。

まとめ

ウェアラブルデバイスという新しい領域だからこそ、先人が臨床試験で築き上げてきた「統計の作法」が非常に強力な武器になります。

テックドクターでは、これからも最新のテクノロジーと統計学的手法を組み合わせ、デジタルバイオマーカーの社会実装に向けた信頼性の高い解析を追求していきます。


似顔絵
書いた人:藤野

Agentic Workflow導入記:巨大プロンプトで起こる諸問題を「分割」と「構造化」で解決した話

こんにちは、テックドクターの佐藤です。

今回は、アプリ開発において Agentic Workflow(自律的なエージェントワークフロー)を組み込んだ際に直面した問題と、解決方法について書きます。

その問題とは「巨大プロンプトの限界」。それを解決に導いたのは、マルチエージェントアーキテクチャ化、そしてPydanticAIを用いた型安全な連携でした。

ヘルスケアアプリ「ポポラス」にAgentic Workflowを実装

昨年11月、「ポポラス」というスマートフォンアプリをリリースしました。

populus-app.com

ポポラスは、ユーザーのHealthKitやFitbitのデータに基づき、健康に関するパーソナライズされた受け答えを提供するヘルスケアエージェントアプリです。ユーザーからの「昨日の活動量はどうだった?」といった質問に対し、実際の歩数や睡眠データを参照しながら、自然で適切なアドバイスを行うことを目的としています。

ポポラスはユーザーの質問に対する回答生成に、単一のLLMではなく、複数の専門エージェントが協調して動くAgentic Workflowを採用しています。

AgenticWorkflowとは、AIやプログラムが「自分で考えて動く」ようにする仕組み、あるいはそのための考え方のことです。AIがある目的を達成するために、都度人間の指示を受けるのではなく、自分で手順を考えたり、必要な作業を順番に進めたりできるようにします。

詳しくはこちらのエントリで解説しているので読んでみてください。
techblog.technology-doctor.com

最初のアプローチ:万能な「神エージェント」

開発当初、私たちは全てのタスク(データ取得、分析、応答生成)を一つの大きな「メインAgent」に担わせる設計を採用しました。

当初の実装構成

アーキテクチャ……ユーザー入力と履歴を、一つの巨大なプロンプトを持つエージェントに直接渡す。

ツール群……HealthKitDataFetcherFitbitDataProcessorHealthAnalyzerGetNowなど、あらゆるツールを一度に渡していました。

プロンプト……ひとつのコンテキスト内に全ての機能とガードレール(AIの挙動が意図した範囲から外れないような制御)を詰め込んでいました。

構成図
構成イメージ

最初のアプローチで問題だった点

その結果、この「ひとつの巨大なAgent + 多数のツール」という構成では、期待通りの回答を得られませんでした。具体的には以下の問題が発生しました。

  • 推論の不安定化と無限ループ
    普通の質問をしただけでも、エージェントがツール利用の判断を誤り、データ取得処理のループに入ってしまう現象("An unexpected error occurred")が頻発しました。
  • トークン消費とコストの増大
    本来不要なツールまで呼び出してしまったり、プロンプト自体が巨大であるため入力トークン数が肥大化したりして、APIコストを圧迫しました。
  • コンテキスト理解の失敗(エッジケース)
    「昨日の歩数は?」と質問した後に「じゃあ一昨日は?」と追撃した場合などに、文脈を正しく引き継げず、適切な日付のデータを取得できないケースが多発しました。
  • デバッグの困難さ
    処理がブラックボックス化していたため、意図しない挙動(ハルシネーションなど)が起きた際、原因がプロンプトにあるのかツールの選択ミスにあるのかを特定するのが極めて困難でした。

結論として、「巨大なプロンプトと多数のツールを機械に丸投げして最適な答えを出させる」というやり方ではうまくいかないことが判明しました。

改善したアプローチ:役割分担と構造化

これらの問題を解決するために、「タスクの細分化(Task Decomposition)」と「Pydanticによる入出力の構造化」を行う設計へ移行しました。

協調型Agentic Workflowへの変更

全体の構成図を見てください。単一のAgentではなく、役割を持った専門のAgent群が連携するワークフローに変更しました。

構成図
改善された構成

異なる役割を持った4つのAgentを定義しました。

Agent 役割
RouterAgent ユーザーの意図を汲み取り、適切なエージェントへ振り分ける司令塔。
FetchAgent / AnalysisAgent データ取得と分析に特化した実働部隊。
EmpathyAgent ユーザーに寄り添う会話担当。
SorryAgent 「株価を教えて」や「医療診断して」など、アプリのスコープ外または危険な質問に対して、適切に断りを入れる担当。

これらが連携してユーザーへの応答を生成します。

PydanticAIにより入出力を縛る

Agent間の連携で最も重要なのが、次のAgentが作業しやすいように情報を整形して渡すことでした。
PydanticAI(あるいはPydanticそのもの)を活用し、モデルのフィールドにdescriptionを詳しく書くことで、LLMに対して「この項目には何を入れるべきか」を強く示唆できます。

一例として、司令塔でありユーザーの質問を直接受け取る立場であるRouterAgentの出力定義はこのようにしました。

RoterAgentの出力定義

from pydantic import BaseModel, Field
from typing import Literal

class RouterDecision(BaseModel):
    """
    ユーザーの意図を分析し、次のアクションを決定するためのモデル
    """
    target_agent: Literal["FetchAgent", "EmpathyAgent", "SorryAgent"] = Field(
        description="ユーザーの要求を満たすのに最適なエージェントを選択する。"
        "健康データが必要ならFetchAgent、日常会話ならEmpathyAgent、"
        "スコープ外(株価や医療診断など)ならSorryAgentを選択。"
    )
    
    context_query: str = Field(
        description="次のエージェントに渡すための、具体的かつ自己完結した指示文。"
        "例:ユーザーが『じゃあ一昨日は?』と言った場合、ここには"
        "『2025年10月13日の歩数データを取得してください』のように"
        "日付と目的を補完して正規化したテキストを出力すること。"
    )
    
    reasoning: str = Field(
        description="なぜそのエージェントを選択したかの理由。デバッグ用。"
    )


実際のLLM出力例
※文脈:昨日の歩数の話の続き

# User: 「じゃあ一昨日は?」
# Result:
# {
#   "target_agent": "FetchAgent",
#   "context_query": "2025-10-13の歩数データを取得",
#   "reasoning": "直前の会話履歴から歩数の話題と判断し、日付を一昨日に特定したため"
# }

このように、Field(description=...) に「日付を補完して正規化せよ」といった具体的な指示を埋め込むことで、プロンプトの一部として機能させます。

これにより、後続のFetchAgentには曖昧な「一昨日」という言葉ではなく「2025-10-13」といった確定情報が渡されることになります。FetchAgentはこれを使ってAPIを叩くだけで済みます。

マルチエージェント化によるメリット

マルチエージェント化したことにより、多くのメリットが得られました。

Agentに特化したプロンプトが書けるようになった

マルチエージェント構成では、「そのエージェントに関係ない禁止事項」を書く必要がなくなります。

例えば、

  • RouterAgent には「ツールを直接呼び出してはいけない」「最終回答を生成してはいけない」という制約だけを書けばよい
  • FetchAgent には「ユーザーに話しかけてはいけない」「指定されたツールだけを呼ぶ」「推論をしない(※API経由で正確なデータを取得させるため)」という制約だけを書けばよい
  • SummarizeAgent には「取得済みデータを要約することだけに集中する」「新しいデータ取得はしない」という制約だけを書けばよい

というように、「その役割の逸脱を防ぐためのガードレール」だけを、最小限かつ強い言葉で書けるようになります。

結果として、

  • プロンプト1つあたりの制約の数が激減
  • 各制約の意味が曖昧にならない
  • モデルが「何をしてはいけないか」をほぼ確実に守る

という状態を作ることができました。

出力が安定した

もう一つの非常に大きなメリットは、出力フォーマットが安定したことです。

巨大プロンプトで全タスクをやらせていた頃は、

  • たまにJSONが壊れる
  • たまに説明文が混ざる
  • たまにキーが変わる
  • たまに謎の自然文が出る

といった「機械処理するには致命的だが、人間が見ると一見それっぽい」出力が頻発していました。

マルチエージェント化すると、

  • RouterAgent……{ intent: "...", normalized_text: "..." } しか返さない
  • FetchAgent……{ tool_results: [...] } しか返さない
  • SummarizeAgent……{ summary: "...", insight: "..." } しか返さない

というように、各エージェントに「一種類のJSON構造しか返させない」設計にできます。

これにより、

  • パースエラーがほぼゼロになる
  • 後段の処理が例外処理だらけにならない
  • ログの可読性とデバッグ効率が爆上がりする

という、「正しい状態」に一気に寄せられました。

デバッグ可能性が劇的に向上した

マルチエージェント構成にして一番「これは正解だった」と感じたのは、デバッグのしやすさです。

巨大プロンプト時代は、なにかを失敗した際に「どこで?」「なぜ?」「どの指示を誤解した?」「モデルが悪い?プロンプトが悪い?設計が悪い?」が完全にブラックボックスでした。

マルチエージェント化すると,

  • RouterAgent のログを見る → 意図分類が間違っていることがわかる
  • FetchAgent のログを見る → ツール引数の組み立てが間違っていることがわかる
  • SummarizeAgent のログを見る → 要約ロジックが微妙であることがわかる

というように、壊れている責務の場所が一瞬で特定できるようになります。

最初のアプローチとの比較

最初の「巨大プロンプト方式」との違いを一覧表にまとめました。

項目 初期設計(失敗) 改善後の設計(成功)
Agent構成 1つの巨大なAgent 複数Agentが連携
(Router, Fetch, Analysis, Empathy, Sorry)
プロンプト 全機能を網羅した巨大プロンプト タスクごとに特化した
小規模プロンプト
データ連携 自然言語(曖昧) Pydanticモデル
(型定義+Descriptionによる指示)
エラー対応 プロンプト内で禁止事項を羅列 SorryAgentへ遷移させて分離
安定性 低い(ループ、ハルシネーション多発) 高い(責任分界点が明確)
デバッグ容易性 低い(原因が分からない) 高い(原因が明確化した)
ガードレール 巨大なプロンプトに内包 最後の出力プロンプトの
Agentのみに適応

まとめ

ヘルスケアアプリのような複雑な要件をAgentic Workflowで実現するためには、「何でもできる神エージェント」を作ろうとせず、「専門家のチーム」を作ることが成功の鍵でした。

特に、PydanticモデルのDescriptionを活用して文脈(Context)を構造化データに変換するテクニックは、LLMの推論精度を劇的に向上させます。また、SorryAgentのような「断り担当」を設けることで、他のエージェントが余計なノイズに惑わされず、本来の性能を発揮できるようになりました。

今後は、この構造を活かしつつ、RAG Agent なども追加して「ポポラスって名前の由来は?」などより幅広い質問に答えられるようにしていく予定です。

また、今後のエントリではLLMのデバッグを劇的に楽にしたLangFuseの導入などにも触れたいと思います。


似顔絵
書いた人:佐藤


CSRFの生の挙動を知りたい~FastAPIとZAPを用いて検証してみた

初めまして、テックドクターでエンジニアをしている金子です。

CSRF(クロスサイト・リクエスト・フォージェリ)は非常に古典的な攻撃手法ですが、現代のWeb開発においてもその発生原理を正確に理解しておくことは重要だと思っています。

今回の記事では、CSRFについて、実際に手を動かして検証してみます。

この記事でやること

CSRFの基本的な仕組みを理解するために、わざと脆弱なサーバーを作成します。加えて「罠サイト」も用意して攻撃を再現し、何が起きているのかを脆弱性診断ツールのOWASP ZAPで観察します。

また、防御策としてCSRFトークンの有効性についても検証していきます。

※CSRF攻撃において、被害者に踏ませるためのページを本記事では「罠サイト」と呼びます。罠サイトは攻撃者が用意するもので、被害者がページを開いただけで標的サイトへリクエストが飛ぶ仕掛けが仕込まれています。

こんな人におすすめ

  • CSRFの名前は知っているが、実際どうやって攻撃が成立するのかピンときていない
  • フレームワークが守ってくれているため、生の脆弱な挙動を見たことがない
  • 攻撃の原理を自分の言葉で説明できるようになりたい

検証環境

  • ターゲットサーバー:FastAPI
  • 罠サイト(攻撃者):HTML + JavaScript(JSフレームワークは使いません)
  • ツール:OWASP ZAP、FoxyProxy

※今回は簡易的な検証のため、ターゲットサーバー、罠サイト共にローカル環境に設置します。

第1章:CSRFの仕組み

CSRFとは

CSRF(Cross-Site Request Forgery) は、日本語では「リクエスト強要」とも呼ばれます。被害者のブラウザを踏み台にして、本人の意図しないリクエストをWebアプリケーションに送信させる攻撃手法です。

攻撃の本質は「認証済みセッションの悪用」にあります。Webアプリケーションはリクエストに含まれるCookieを見て「この人は誰か」を判断しますが、そのリクエストが「本人の意思で送られたものか」までは検証していないことが多いです。CSRFではこの点が悪用されます。

攻撃者は罠サイトを用意し、被害者がそのページを開いた瞬間、被害者のブラウザから標的サイトへ不正なリクエストを自動送信させます。このとき被害者がたまたま標的サイトにログイン中であれば、ブラウザにより自動的にCookieが不正リクエストにも付与されます。そのため、サーバ側から見れば正規ユーザーからの正当なリクエストと区別がつかず、不正リクエストが処理されてしまうというわけです。

CSRFのしくみ(引用:IPA(独立行政法人 情報処理推進機構)より)

攻撃が成立する条件

CSRF攻撃が成立するには、以下の3つの条件が同時に満たされる必要があります。

  • 被害者が標的サイトにログイン済みである……ブラウザに標的サイトのセッションCookieが保存されており、有効な状態であること。ログアウト済み、またはセッション切れの場合は攻撃が成立しません。
  • 標的サイトがリクエストの正当性を検証していない……サーバ側で、CSRFトークンの検証、Refererヘッダのチェックなどを行っていない状態。つまり、「誰から送られたリクエストか」は見ているものの、「本人が意図して送ったリクエストか」までは確認しない実装になっている必要があります。
  • 被害者が攻撃者の用意した罠ページにアクセスする……メール内のリンク、SNSの投稿、不正広告など経路は様々です。被害者がそのページを開いた瞬間、隠されたフォームやimgタグによって標的サイトへリクエストが飛びます。多くの場合、被害者は攻撃が行われたことに気づきません。

※CSRFの仕組みについて、より詳細に知りたい方は次のサイトを参考にしてみてください。
安全なウェブサイトの作り方 - 1.6 CSRF(クロスサイト・リクエスト・フォージェリ) | 情報セキュリティ | IPA 独立行政法人 情報処理推進機構

具体的なインシデント例

PC遠隔操作事件(2012年)……Web掲示板のCSRF脆弱性を悪用し、無関係な一般市民のPCから犯罪予告を投稿させた事件です。被害者は誤認逮捕され、そのうち一人の大学生は自主退学に追い込まれました。サービス側のアクセスログには被害者のIPアドレスしか残らないため、当初は被害者が犯人だと疑われるなど、CSRFの「踏み台」としての恐ろしさを社会問題として浮き彫りにした事例です。

第2章:脆弱な環境を作る

では実際に検証を進めていきましょう。
まずは攻撃ターゲットとして、脆弱性を持つサーバーを構築します。

FastAPIによる簡単なサーバーの実装

このサーバーは下記のような機能を持っています。

  • ログイン機能(ログイン時、ブラウザにCookieをセット)
  • ユーザー情報としてID、パスワードのほか残高情報を持つ
  • パスワード変更APIが存在(CSRFトークンなし)

実装コードはこのようにしました。

from fastapi import FastAPI, Response, Cookie, HTTPException
from pydantic import BaseModel

app = FastAPI()

# 簡易ユーザーDB
users = {"alice": {"password": "pass123", "balance": 10000}}
sessions = {}

@app.post("/login")
def login(username: str, password: str, response: Response):
    if users.get(username, {}).get("password") == password:
        session_id = f"session_{username}"
        sessions[session_id] = username
        response.set_cookie("session_id", session_id, samesite="none", secure=False)
        return {"message": "ログイン成功"}
    raise HTTPException(401, "認証失敗")

class TransferRequest(BaseModel):
    to_user: str
    amount: int

@app.post("/transfer")  # ← CSRFトークンなし!
def transfer(req: TransferRequest, session_id: str = Cookie(None)):
    username = sessions.get(session_id)
    if not username:
        raise HTTPException(401, "未ログイン")
    users[username]["balance"] -= req.amount
    return {"message": f"{req.amount}円を送金しました"}

標的サイトの作成

次に標的となるサイトのページを作成しました。以下のような機能を持っています。

  • credentials: 'include' でCookieがやり取りする

実装コードの一部がこちらです。(重要な箇所のみ。HTML、CSSの全部、JavaScriptの一部は割愛します)

// ログイン時(ポイント:credentials: 'include')
  const res = await fetch(`${API_BASE}/token`, {
      method: 'POST',
      body: formData,
      credentials: 'include'  // ← Cookieを受け取る
  });

// 送金時(ポイント:Cookieが自動送信される)
  const res = await fetch(`${API_BASE}/transfers`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data),
      credentials: 'include'  // ← Cookieが自動で付く
  });

実装した画面

上記により、このような画面ができました。

  • ログイン画面

画面キャプチャ

  • 新規登録画面

画面キャプチャ

  • ダッシュボード

画面キャプチャ

第3章:罠サイトを作る

次に罠サイトを作成します。これは実際のCSRF攻撃においては攻撃者が作成するものです。下記のような機能を持っています。

  • 隠しフォームを持つ
  • フォーム自動送信のJavaScriptが存在(今回はボタンクリック時)

実装コード(CSSは割愛)

<form id="csrfForm" action="https://localhost:8443/api/transfers" method="POST">
      <input type="hidden" name="receiver_user_id" value="attacker">
      <input type="hidden" name="amount" value="5000">
  </form>
  
  <script>
  // 被害者がクリックすると...
  document.getElementById('claimBtn').addEventListener('click', async () => {
      await fetch('https://localhost:8443/api/transfers', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
              receiver_user_id: 'attacker',
              amount: 5000
          }),
          credentials: 'include'  // ← 被害者のCookieが送られる!
      });
  });
  </script>

実装した画面

画面キャプチャ

ここまでで、検証に必要な環境の作成が完了しました。

第4章:OWASP ZAPで検証

いよいよ、ツールを使ってCSRFの挙動を検証していきます。使用するツールは2つです。

OWASP ZAP

OWASP(Open Web Application Security Project)が開発するオープンソースのセキュリティテストツールです。ローカルプロキシとして動作し、ブラウザとWebサーバー間を流れるHTTP通信をすべて傍受・記録できます。

主な機能は以下の通りです。

  • リクエスト/レスポンスの可視化……ブラウザが裏で送っているリクエストの中身(ヘッダ、Cookie、POSTパラメータ等)を確認できる
  • リクエストの改ざん・再送信……傍受したリクエストを編集して再送信することで、パラメータ操作の影響を検証できる
  • 自動スキャン……既知の脆弱性パターンを自動検出する機能もあります。(本章では使用しません)
FoxyProxy

プロキシ設定の切り替えを簡単にするブラウザ拡張機能です。ChromeFirefoxに対応しています。
通常、ZAPのようなローカルプロキシを使うにはブラウザのネットワーク設定を手動で変更する必要がありますが、FoxyProxyを使えばワンクリックで「ZAP経由」と「直接接続」を切り替えられます。検証作業中に頻繁にプロキシのON/OFFを行う場面で便利です。

攻撃の再現

下準備として、被害者のアカウントと攻撃者のアカウントをそれぞれ作成しました。単純に作成するだけなので、画像の貼付は行いません。作成時の条件とユーザIDのみ記載します。

  • 被害者のユーザーID:test_target1
  • 攻撃者のユーザーID:attacker1
  • 初期値として設定した残高:いずれのユーザーも10,000円

以下、攻撃成立までのフローです。

1. 被害者ユーザーとして正規サイトにログインします。
画面キャプチャ
画面キャプチャ
ここでZAPを確認すると、バックエンドからバックエンドからアクセストークンが発行され、Cookieに保存されたことが分かります。
画面キャプチャ


2. 別タブで罠サイトを開きます。
画面キャプチャ
画面キャプチャ


3. 「景品を受け取る」ボタンをクリックします。

その結果、Javascriptにより攻撃者に送金をするリクエストがバックエンドに送信されます。
このとき、ZAPでCookieヘッダーを見てみると、被害者がログインしたときと同じAccessTokenが使われてしまっていることがわかります。

画面キャプチャ

リクエストヘッダーとJSONペイロードも確認してみましょう。

receiver_user_id(送金を受けるアカウント)には攻撃者のアカウントが、amount(送金額)には5000円が設定されています。(このとき、sender_id(送金者)のような送金者を特定するパラメータがありませんが、バックエンドではCookieのアクセストークンによって送金者を特定できるので、仕組みとしてはsender_idを指定しなくても送金処理を実行できます。)

送金完了後に被害者のダッシュボードで画面を更新すると、残高が5000に減っていることが分かります。

画面キャプチャ

また、攻撃者のアカウントにログインすると残高が15,000に増えていることが分かります。

画面キャプチャ

以上で、CSRF攻撃が成立するまでの挙動が確認できました。

第5章:CSRFへの防御策を検証する

攻撃について理解したところで、次は防御策についても検証してみたいと思います。
CSRFへの防御策のひとつに、CSRFトークンの実装が挙げられます。

CSRFトークンの仕組み

  1. 発行: ユーザーがフォームのある画面(送金画面など)を開いたとき、サーバーは乱数で作った、「予測不可能なトークン」を生成し、HTMLの隠しフィールド(input type="hidden")に埋め込んでユーザーに渡します
  2. 送信:ユーザーが送信ボタンを押すと、入力データと一緒にこの「トークン」もサーバーへ送られます
  3. 検証: サーバーは、送られてきたトークンが、ステップ1で自分が発行したものと一致するかを確認します。
    • 一致すれば……「正規の画面からの操作だ」と判断して処理を実行
    • 不一致/トークンなしなら……「不正なリクエストだ」と判断して拒否

CSRFトークンの実装

実際に実装してみましょう。全てを掲載すると長くなるので、実装したコードのうち重要な部分のみを例として掲載します。

1. CSRFトークンの生成とCookie設定(ログイン時)

# auth.py - ログイン時にCSRFトークンをCookieにセット
import secrets

def generate_csrf_token() -> str:
    return secrets.token_hex(32)

# ログインエンドポイント内
csrf_token = generate_csrf_token()
response.set_cookie(
    key="csrf_token",
    value=csrf_token,
    httponly=False,  # JSから読み取り可能にする(重要)
    samesite="none",
    secure=True,
)


2. CSRFトークンの検証(サーバー側)

# auth.py - CSRFトークン検証関数
def verify_csrf_token(
    csrf_token_cookie: str | None = Cookie(alias="csrf_token"),
    csrf_token_header: str | None = Header(alias="X-CSRF-Token"),
) -> None:
    """CookieとヘッダーのCSRFトークンを比較"""
    if csrf_token_cookie is None or csrf_token_header is None:
        raise HTTPException(status_code=403, detail="CSRF token missing")

    if csrf_token_cookie != csrf_token_header:
        raise HTTPException(status_code=403, detail="CSRF token mismatch")

# transfer.py - 送金エンドポイントで検証を適用
@router.post("/transfers")
def transfer_money(
    request: TransferRequest,
    current_user: Annotated[UserAuth, Depends(get_current_user)],
    _csrf: Annotated[None, Depends(verify_csrf_token)],  # ← これを追加
):
    ...


3. CSRFトークンをヘッダーに含める(フロントエンド)

// dashboard.html - CookieからCSRFトークンを取得してヘッダーに含める
function getCsrfToken() {
    const cookies = document.cookie.split(';');
    for (const cookie of cookies) {
        const [name, value] = cookie.trim().split('=');
        if (name === 'csrf_token') return value;
    }
    return null;
}

// 送金リクエスト
const res = await fetch('/api/transfers', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': getCsrfToken()  // ← これを追加
    },
    body: JSON.stringify(data),
    credentials: 'include'
});

再検証

ここまででCSRFトークンの実装が完了しました。

では再びOWASP ZAPを用いて、防御策がちゃんと機能しているか検証してみましょう。

1. 被害者のアカウントにログインします。
画面キャプチャ
ZAPを確認すると、アクセストークンとCSRFトークンが発行されていることがわかります。
画面キャプチャ


2. 別タブで罠サイトを開き、「景品を受け取る」ボタンをクリックします。

ZAPを確認してみると……
画面キャプチャ
X-CSRF-Tokenがリクエストのヘッダーに存在しないことと、403エラーが出て不正なリクエストが処理されていないことが確認できます。

無事、CSRF攻撃を防御することができました!


3. 念のため、正常時のリクエストも確認します。

現在の残高は5,000円です。

画面キャプチャ

ダッシュボードより2,500円の送金操作を行いました。

画面キャプチャ

送金が実施され、残高が2,500円になりました。

ZAPでリクエストの内容を確認してみましょう。
X-CSRF-Tokenがリクエストのヘッダーに含まれていることが分かります。
画面キャプチャ

送金先である、攻撃者のアカウントにログインをして残高を確認します。
画面キャプチャ
残高が増えていました。

正常なリクエストは、正常に処理されることが確認できました。

他の防御方法

なお、CSRF対策としてCSRFトークンが唯一の手段ではありません。
他にも下記のような対策が考えられます。

  • SameSite属性の設定
  • Referer/Originヘッダの検証

本記事の趣旨から逸れるのでこれらについて詳しくは説明しませんが、複数の防御策を組み合わせ、耐攻撃性の高い実装をすることが重要です。

まとめ

今回の検証を通じて、現代のフレームワークは開発者が意識せずともCSRFから守ってくれているという事実を、肌で感じることができました。

最近はAIを使って実装をする機会が増えましたが、生成されたコードでは「何となく動くけれど、仕組みはよく分からない」という状態に陥ることも少なくありません。

こうした「何となく」の知識を確かなものにするためには、時間をかけて手を動かし、実践するプロセスもやはり重要だと改めて感じました。


似顔絵
書いた人:金子

自己申告に頼らない飲酒測定――ウェアラブルでとらえる飲酒とその影響

1.飲酒習慣を測ることの難しさ

こんにちは。この記事は、テックドクターでデータ解析を担当する坂本と藤本が共同で執筆しています。

みなさんが健康診断を受けると、飲酒の習慣を尋ねられると思います。私(坂本)はよく、「飲み過ぎ注意」と言われていました。飲酒は、健康のいろいろな面に影響を与えるとされています(※1)

飲酒の量や習慣は、主に質問紙(アンケート)などで測定されます。しかし、例えば、過去7日間を振り返って飲酒の量を思い出して回答した場合、実際より少ない量で回答してしまうなどのリスクがあると言われています(※2)
もちろん、質問紙を用いた飲酒の測定も十分に有効活用できますが、毎日の記録/長期的な記録が必要な場面では、回答の負担が大きく、継続も難しいです。

そんなとき、スマートウォッチで自動的に飲酒の記録ができたら便利ですよね。
こういった背景から、私たちは「スマートウォッチを代表とするウェアラブル端末を装着しているだけで、飲酒を自動検出し、飲酒の影響を評価する」という技術開発に挑戦することにしました。

そして先日、その成果について、第32回日本行動医学会学術総会にてポスター発表を行いました(※3)。この記事では、その発表をもとに、「ウェアラブル端末で飲酒の検出と影響評価を行う技術」、そして「ウェアラブル端末のデータからわかった、飲酒が睡眠や翌日の活動に与える影響」について紹介したいと思います。

※1
例えば、厚生労働省「健康に配慮した飲酒に関するガイドラインについて」, URL: https://www.mhlw.go.jp/stf/newpage_38541.html, Last Accsess: 2025-12-10)。

※2
例えば、
Gmel G, Daeppen JB. Recall bias for seven-day recall measurement of alcohol consumption among emergency department patients: implications for case-crossover designs. J Stud Alcohol Drugs. 2007 Mar;68(2):303-10. doi: 10.15288/jsad.2007.68.303. PMID: 17286350.

※3
◎坂本・◯藤本・深見(2025). ウェアラブル端末を用いた20歳以上の一般成人を対象とする飲酒の多変量予測モデルの開発とアルコール飲料摂取が日常生活に与える影響の評価. 第32回日本行動医学会学術総会, 神奈川県相模原市 相模原市立産業会館, 2025年12月6日. (◎は責任著者・発表者、◯は発表者)

イメージ画像

2.飲酒日を検出する指標の開発

ウェアラブルデバイスデータを用いた「飲酒日」の検出

ウェアラブル端末では、主に心拍数、歩数、睡眠などの情報が取得できます。
しかし、例えば「心拍数が高い」というだけでは、運動や緊張、発熱などと区別がつけられず、飲酒を特定することができません。飲酒を見つけ出すためには相応の工夫が必要です。

そこで、「お酒を飲むと心拍が早くなる」という経験に着想を得て、「歩いていない時間に持続的に心拍数が上昇する」という特徴を定量化する計算手法を開発しました。
この指標を、本記事では「飲酒指標(Alcohol Index)」と呼ぶことにしましょう。この指標が、飲酒と高い関連性を示しました(※4)

※4
2026年1月現時点: 特許出願中

グラフ画像
図. 飲酒があった日の飲酒指標の例

飲酒指標を代表的な情報として、睡眠中の心拍数などを加えた特徴量を用いて機械学習モデルによる解析(XGBoost)を行ったところ、飲酒日の検出精度は90%程度となりました。

グラフ画像
図. 飲酒日検知モデルの性能

予測モデルの妥当性評価と研究報告

飲酒の検出精度が90%と聞くと、「すぐにでも実用化できる」ように感じられるかもしれません。しかし、安全な技術を確立するためには、多面的な検証を積み重ねる必要があります。
例えば、「どのような人でも同じ性能が得られるのか」や「誤判定が生じやすい条件は何か」といったような、公平性やリスク、安全性等に関する検証や対策が必要となります。

研究報告において大切なのは、何が未検証/未対策であるかを誠実に報告し、次の検証に繋げることです。「TRIPOD+AI声明」という研究報告で求められる事項が整理された国際的な指針(※5)があるので、それに沿って多面的な評価と今後の課題をまとめました。

ウェアラブル端末を装着しているだけで飲酒の自動記録が可能になり、健康管理など様々な場面で活用できるようになるという未来に向けて、検証を積み重ねたいと考えています。

※5
Collins G S, Moons K G M, Dhiman P, Riley R D, Beam A L, Van Calster B et al. TRIPOD+AI statement: updated guidance for reporting clinical prediction models that use regression or machine learning methods BMJ 2024; 385 :e078378 doi:10.1136/bmj-2023-078378


3.ウェアラブルデバイスを用いた飲酒の翌日の影響評価

次に、実際に飲酒が睡眠や翌日の活動にどのような影響を及ぼすかについても解析しました。

社内での取り組みを通して可視化できたデータをご紹介していきます。

心拍変動指標について

今回解析に利用した、心拍変動に関する指標を紹介しておきます。

  • RMSSD……副交感神経活動を反映する指標。これが低下した場合、十分にリラックスできていないと考えられる。
  • SDNN……交感・副交感神経の両方の変動を反映する指標であり、値が高いほど活発な自律神経活動が発生していたと考えられる。

データと参加者

下記の対象者/対象データを解析しました。

  • 21名、4932日分の飲酒報告と対応する日のウェアラブル端末装着情報を利用(男性57%)
  • 前日の飲酒報告(非飲酒・少量・中量・大量)
  • アンケート回答日と翌日のウェアラブル装着がある者
  • 装着時間が70%以上の日のみ採用
  • 平均値±3SD以内の睡眠時間の日

※今回は飲酒の有無はアンケートを利用して判定しました。

解析方法

解析では、一元配置分散分析、線形混合モデルと呼ばれる手法を使用しました。

  • 一元配置分散分析(ANOVA)……飲酒量4段階の群間差を比較
  • 線形混合モデル(LMM)……個人差を調整し、飲酒有無の影響を精緻に検討

解析結果

まず、一元配置分散分析により見えてきた結果をご紹介していきます。

睡眠 大量の飲酒をした場合、少量・中量飲酒に比べてさらに睡眠中の最低心拍が高くなっていました。(図1)
翌日の心拍 大量の飲酒をすると、翌日の安静時心拍数が高くなっていました。(図2)
翌日の活動 少量・中量の飲酒をすると、飲酒しなかった場合に比べて翌日の歩数が多くなっていました。(図3)
大量の飲酒をすると、少量・中量の飲酒に比べて翌日の歩行ペースが遅くなっていました。(図4)
少量の飲酒をすると、飲酒しなかった場合に比べて翌日の高強度運動時間割合が上がっていました。(図5)

次に、線形混合モデルにより見えてきた結果をご紹介します。

睡眠 飲酒をすると睡眠中の最低心拍が高くなっていました。(図6)
飲酒をすると睡眠中のSDNNが高くなっていました。(図7)
飲酒をすると睡眠中のRMSSDが低くなっていました。(図8)
翌日の心拍 飲酒をすると翌日の安静時心拍数が高くなっていました。(図9)
グラフ画像
図1: 飲酒レベルごとの睡眠中の最低心拍数比較
グラフ画像
図2: 飲酒レベルごとの安静時心拍数比較
グラフ画像
図3: 飲酒レベルごとの翌日の合計歩数比較
グラフ画像
図4: 飲酒レベルごとの翌日の歩行ペース比較
グラフ画像
図5: 飲酒レベルごとの翌日の高強度運動時間比較
グラフ画像
図6: 飲酒日と非飲酒日の睡眠中の最低心拍数比較
グラフ画像
図7: 飲酒日と非飲酒日の睡眠中のSDNN比較
グラフ画像
図8: 飲酒日と非飲酒日の睡眠中のRMSSD比較
グラフ画像
図9: 飲酒日と非飲酒日の安静時心拍数比較

考察

飲酒をすると、しなかった日に比べて睡眠中の最低心拍およびSDNNが高くなり、RMSSDが低くなっています。このことから、飲酒した日は自律神経が交感神経優位な状態になりやすく、睡眠の質が低下している可能性があります。

また、大量の飲酒をすると中量以下の飲酒時に比べて睡眠中の最低心拍が高くなり、翌日の歩行ペースが遅くなります。ここからは、大量の飲酒は特に睡眠および翌日の活動に大きな影響を与える可能性が読み取れます。
一方で、少量の飲酒では飲酒していない場合との差が少ないことや、むしろ活動量が増える人もおり、個人差が大きい可能性が考えられます。

4.まとめ

ウェアラブルデバイスのデータを使用することで、飲酒日を検出する指標の開発と、飲酒の影響の評価を行うことができました。

さらなる研究によって、より安全で確かな技術を確立してゆくことが必要とされています。技術開発が進めば、アンケートの併用がなくともより手軽に飲酒の影響についての解析ができるようになるかもしれません。
ウェアラブルデバイスを活用することで日々の行動を振り返る負担を減らしつつ、健康状態への理解を深めることができます。
こうした技術が、一人ひとりの生活を少し便利に、そして健やかにする助けになることを期待しています。

5.今後の展望

飲酒を検知する技術については、まだ様々な方々を対象とした多面的な検証と安全性の評価を必要としています。

本テーマに関する共同開発にご関心のある方がいらっしゃいましたら、お気軽に、お問い合わせフォームよりテックドクターまでご連絡ください。

似顔絵似顔絵
書いた人:坂本、藤本

FlutterのFlavor設定完全ガイド:iOS/Android対応のマルチ環境を構築する

モバイルアプリ開発では、開発・ステージング・本番など複数の環境を切り替えて運用することがあります。
Flutterでのアプリ開発において、その運用に役立つのがFlavorです。

Flavorは、アプリを複数の環境ごとに設定を切り替えるための仕組みです。
例えば「開発環境ではテスト用API、本番環境では本番APIを使いたい」といった場合に、Flavorを使うことでビルド時に環境を選択し、自動的に異なる設定を適用することができます。

この記事では、そんな便利なFlavorの設定方法をご紹介します。iOS/Androidの両方に対応しているので、マルチプラットフォーム向けアプリの開発に役立ててください。

この記事はこんな人向けです
  • Flutterで複数環境のアプリビルドを構築したい方
  • iOS/Android両方のFlavor設定を網羅的に知りたい方
  • Firebase等の外部サービスを環境ごとに切り替えたい方

 

1. 概要:Flavorとは

Flutterでは「Flavor」という仕組みを使って、同一コードベースから異なる設定のアプリをビルドできます。

Flavorを使うことで、例えばこういった設定を環境ごとに切り替えられます:

  • アプリ名・アイコン:開発版と本番版を異なる見た目にすることで視覚的に区別できます
  • Bundle ID / Application ID:同一端末に複数環境のアプリを共存させることができます
  • APIエンドポイント:環境ごとに接続先の自動切り替えが可能です
  • Firebase設定:環境ごとに異なるFirebaseプロジェクトを使用できます
プラットフォームごとの実現方法

AndroidとiOSそれぞれにおいて、Flavorを定義する方法は下記のとおりです。

  • Android:Gradleの productFlavors 機能を使用
  • iOS:XcodeのSchemeとBuild Configurationを使用

それぞれ、詳しいやり方は記事中で説明していきます。

本記事で構築する環境(Flavor)

本記事では、Flutter公式ドキュメントを参考に、4環境(local、development、sandbox、production)のFlavor設定手順を解説します。

Flavor 用途 アプリ名 Bundle ID /
Application ID
local ローカル開発 [LOC] アプリ名 *.local
development 開発サーバー接続 [DEV] アプリ名 *.development
sandbox ステージング環境 [SAN] アプリ名 *.sandbox
production 本番リリース アプリ名 (サフィックスなし)

では、具体的な手順を紹介していきましょう。

2. Android側のFlavor設定

まずはAndroidでの構成方法です。
Gradleの productFlavors 機能を使用し、シンプルに定義できます。

build.gradleの設定

android/app/build.gradleflavorDimensionsproductFlavors を追加します。

android {
   // ... 既存の設定

   flavorDimensions += "default"

   productFlavors {
       create("local") {
           dimension = "default"
           manifestPlaceholders = [
               appName: "[LOC] アプリ名",
           ]
           applicationIdSuffix = ".local"
       }
       create("development") {
           dimension = "default"
           manifestPlaceholders = [
               appName: "[DEV] アプリ名",
           ]
           applicationIdSuffix = ".development"
       }
       create("sandbox") {
           dimension = "default"
           manifestPlaceholders = [
               appName: "[SAN] アプリ名",
           ]
           applicationIdSuffix = ".sandbox"
       }
       create("production") {
           dimension = "default"
           manifestPlaceholders = [
               appName: "アプリ名",
           ]
           applicationIdSuffix = ""
       }
   }
}


ポイント解説

  • flavorDimensions:Flavorのグループを定義します。複数の軸でFlavorを管理する場合に使用します(例:有料版/無料版 × 環境)
  • applicationIdSuffix:ベースのApplicationIDに追加されるサフィックスです。これにより同一端末に複数環境のアプリをインストール可能にできます。
  • manifestPlaceholders:次に説明するAndroidManifest.xmlで参照するための変数を定義します。
AndroidManifest.xmlでの変数参照

くわえて、android/app/src/main/AndroidManifest.xml でplaceholderを参照します。

<application
   android:label="${appName}"
   android:name="${applicationName}"
   android:icon="@mipmap/ic_launcher">
   <!-- ... -->
</application>

Android側の設定手順は以上です!

3. iOS側のFlavor設定

iOSのFlavor設定はAndroidより少し複雑です。
XcodeのSchemeとBuild Configurationを使用します。まずBuild ConfigurationとSchemeを作成し、それぞれを紐づけたあと、具体的な設定を定義していく流れです。

Build Configurationの作成

Xcodeで`Runner.xcodeproj`を開き、以下の手順でBuild Configurationを作成します。

  1. プロジェクトナビゲータで Runner プロジェクトを選択
  2. Info タブ → Configurations セクションを開く
  3. 「+」ボタンで以下のConfigurationを追加:
Configuration名 複製元
Debug-local Debug
Debug-development Debug
Debug-sandbox Debug
Debug-production Debug
Release-local Release
Release-development Release
Release-sandbox Release
Release-production Release
Profile-local Release
Profile-development Release
Profile-sandbox Release
Profile-production Release

重要:Configuration名は必ず {BuildType}-{flavor名} の形式にしてください。Flutterがこの命名規則でFlavorを識別します。

Schemeの作成

次に、各FlavorごとにSchemeを作成します。

  1. ProductSchemeNew Scheme… を選択
  2. Scheme名を入力(例:localdevelopmentsandboxproduction

重要:Scheme名は小文字で、Flavor名と完全に一致させるようにしてください。

SchemeとConfigurationの紐付け

作成したSchemeを、Configurationに割り当てていきます。

  1. ProductSchemeManage Schemes… を開く
  2. 各Schemeを選択し「Edit...」をクリック
  3. 以下のように各アクションにConfigurationを割り当て:


localスキームの例:

アクション Build Configuration
Run Debug-local
Test Debug-local
Profile Profile-local
Analyze Debug-local
Archive Release-local


重要:全てのSchemeで「Shared」にチェックを入れてください。これにより.xcscheme ファイルがリポジトリにコミットされ、チームで共有できます。


作成されたSchemeファイル( ios/Runner.xcodeproj/xcshareddata/xcschemes/ 配下)の例:

<?xml version="1.0" encoding="UTF-8"?>
<Scheme version = "1.7">
  <BuildAction parallelizeBuildables = "YES" buildImplicitDependencies = "YES">
     <!-- 省略 -->
  </BuildAction>
  <TestAction buildConfiguration = "Debug-local">
     <!-- 省略 -->
  </TestAction>
  <LaunchAction buildConfiguration = "Debug-local">
     <!-- 省略 -->
  </LaunchAction>
  <ProfileAction buildConfiguration = "Profile-local">
     <!-- 省略 -->
  </ProfileAction>
  <AnalyzeAction buildConfiguration = "Debug-local">
  </AnalyzeAction>
  <ArchiveAction buildConfiguration = "Release-local" revealArchiveInOrganizer = "YES">
  </ArchiveAction>
</Scheme>

 

Build Settingsの設定

各ConfigurationでのBundle IDやアプリ表示名を設定します。

  1. Runner ターゲット → Build Settingsタブを開く
  2. 「+」→「Add User-Defined Setting」で以下のカスタム設定を追加:

DISPLAY_PRODUCT_NAME_PREFIX(アプリ名のプレフィックス):

Configuration
Debug-local [LOC]
Debug-development [DEV]
Debug-sandbox [SAN]
Debug-production (空)
Release-* 同上
Profile-* 同上


PRODUCT_BUNDLE_IDENTIFIER:

Configuration
*-local com.example.app.local
*-development com.example.app.development
*-sandbox com.example.app.sandbox
*-production com.example.app

 

Info.plistの設定

ios/Runner/Info.plist でBuild Settings変数を参照します。
これにより、アプリのビルド時にはConfigurationごとに設定した値が使用されるようになります。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
   <!-- アプリ表示名:プレフィックス + アプリ名 -->
   <key>CFBundleDisplayName</key>
   <string>$(DISPLAY_PRODUCT_NAME_PREFIX)アプリ名</string>

   <!-- Bundle IDはBuild Settingsから自動取得 -->
   <key>CFBundleIdentifier</key>
   <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>

   <!-- その他の設定... -->
</dict>
</plist>

iOS側の設定は以上で完了です!

4. Firebase設定(FlutterFire CLI)

複数の設定ファイルを生成することで、Flavorごとに異なるFirebaseプロジェクトを使用することができます。ここではその設定方法を紹介します。

事前に準備しておくこと

  • 各Flavorに対応したFirebaseプロジェクトが作成済み
  • Firebase CLIとFlutterFire CLIがインストール済み
# Firebase CLIのインストール
npm install -g firebase-tools
firebase login

# FlutterFire CLIのインストール
dart pub global activate flutterfire_cli

 

Flavor別の設定ファイルを生成

各Flavorに対して flutterfire config コマンドを実行し、設定ファイルを生成します。
こうすることで、FlutterFire CLIはDart設定ファイルの生成に加えて、iOS/Android両方の設定ファイルを適切な場所に配置し、必要なBuild Phase設定も自動で行います。

# development環境の設定
flutterfire config \
 --project=your-project-development \
 --out=lib/config/firebase/firebase_options_development.dart \
 --ios-bundle-id=com.example.app.development \
 --ios-out=ios/flavors/development/GoogleService-Info.plist \
 --android-package-name=com.example.app.development \
 --android-out=android/app/src/development/google-services.json

# sandbox環境の設定
flutterfire config \
 --project=your-project-sandbox \
 --out=lib/config/firebase/firebase_options_sandbox.dart \
 --ios-bundle-id=com.example.app.sandbox \
 --ios-out=ios/flavors/sandbox/GoogleService-Info.plist \
 --android-package-name=com.example.app.sandbox \
 --android-out=android/app/src/sandbox/google-services.json

# production環境の設定
flutterfire config \
 --project=your-project-production \
 --out=lib/config/firebase/firebase_options_production.dart \
 --ios-bundle-id=com.example.app \
 --ios-out=ios/flavors/production/GoogleService-Info.plist \
 --android-package-name=com.example.app \
 --android-out=android/app/src/production/google-services.json

 
ここで使用する主要なオプション

オプション 説明
--project FirebaseプロジェクトID
--out Dart設定ファイルの出力先
--ios-bundle-id iOSのBundle ID
--ios-out GoogleService-Info.plistの出力先
--android-package-name AndroidのパッケージID
--android-out google-services.jsonの出力先

 

生成されるファイル構成

コマンド実行後、以下のファイルが生成されます:

lib/config/firebase/
├── firebase_options_development.dart
├── firebase_options_sandbox.dart
└── firebase_options_production.dart

ios/flavors/
├── development/
│   └── GoogleService-Info.plist
├── sandbox/
│   └── GoogleService-Info.plist
└── production/
   └── GoogleService-Info.plist

android/app/src/
├── development/
│   └── google-services.json
├── sandbox/
│   └── google-services.json
└── production/
   └── google-services.json

つづいて、各FirebaseプロジェクトをDartのコード内から扱うための設定をします。

Flavor enumの定義

lib/config/env/flavor.dart でFlavorを定義します。Dart 3以降ではenumに直接メソッドやgetterを定義できます。

import 'package:firebase_core/firebase_core.dart';
import 'package:your_app/config/firebase/firebase_options_development.dart'
   as development;
import 'package:your_app/config/firebase/firebase_options_production.dart'
   as production;
import 'package:your_app/config/firebase/firebase_options_sandbox.dart'
   as sandbox;

enum Flavor {
 local,
 development,
 sandbox,
 production;

 /// Dart define から Flavor を取得
 static const _flavorStr = String.fromEnvironment('FLAVOR');
 static Flavor get fromDartDefine => Flavor.values.byName(_flavorStr);

 /// Firebase設定
 FirebaseOptions get firebaseOptions {
   switch (this) {
     case Flavor.local:
     case Flavor.development:
       return development.DefaultFirebaseOptions.currentPlatform;
     case Flavor.sandbox:
       return sandbox.DefaultFirebaseOptions.currentPlatform;
     case Flavor.production:
       return production.DefaultFirebaseOptions.currentPlatform;
   }
 }
}

設定自体はこれで完了です。

main.dartでの使用例

実際にDartのコード内でFirebaseを扱うにはこのようにします。

void main() async {
 WidgetsFlutterBinding.ensureInitialized();

 final flavor = Flavor.fromDartDefine;

 // Flavorに応じたFirebase初期化
 await Firebase.initializeApp(
   options: flavor.firebaseOptions,
 );

 runApp(MyApp(flavor: flavor));
}

以上で、Firebaseに関する設定は完了です。

5. アプリアイコンの切り替え

flutter_launcher_icons パッケージを使用して、Flavor別のアイコンを生成することもできます。
各環境用のアプリを視覚的に区別できるようになり、便利です。

アイコン画像の配置

各Flavor用のアイコン画像を作成し、assets/app_icons/ ディレクトリに配置します。

assets/
└── app_icons/
   ├── app_icon_local.png
   ├── app_icon_development.png
   ├── app_icon_sandbox.png
   └── app_icon_production.png

アイコン画像は1024x1024px以上の正方形PNGファイルを推奨します。

設定ファイルの作成

各Flavor用の設定ファイルを、プロジェクトルートに作成します。

flutter_launcher_icons-local.yaml

flutter_launcher_icons:
 image_path: "assets/app_icons/app_icon_local.png"
 android: true
 ios: true
 remove_alpha_ios: true

 
flutter_launcher_icons-development.yaml

flutter_launcher_icons:
 image_path: "assets/app_icons/app_icon_development.png"
 android: true
 ios: true
 remove_alpha_ios: true

 
flutter_launcher_icons-sandbox.yaml

flutter_launcher_icons:
 image_path: "assets/app_icons/app_icon_sandbox.png"
 android: true
 ios: true
 remove_alpha_ios: true

 
flutter_launcher_icons-production.yaml

flutter_launcher_icons:
 image_path: "assets/app_icons/app_icon_production.png"
 android: true
 ios: true
 remove_alpha_ios: true

 

アイコン生成

最後に、下記のコマンドを実行するとアイコンの生成が行われます。

# 全Flavorのアイコンを生成
dart run flutter_launcher_icons

# 特定Flavorのみ生成
dart run flutter_launcher_icons --flavor development

以上で、全ての設定が完了しました。

7. 実行方法

最後に、ここまでで設定したFlavorを使い、実際にアプリを実行/ビルドする方法を紹介します。

コマンドラインから実行
# local環境で実行
flutter run --flavor local --dart-define=FLAVOR=local

# development環境で実行
flutter run --flavor development --dart-define=FLAVOR=development

# sandbox環境でリリースビルド
flutter run --flavor sandbox --dart-define=FLAVOR=sandbox --release

# production環境でアーカイブ
flutter build ios --flavor production --dart-define=FLAVOR=production
flutter build appbundle --flavor production --dart-define=FLAVOR=production

 

Makefileでの管理

毎回コマンドを打つのは面倒なので、Makefileにまとめると便利です。

run: ## Run app. Pass F={local,development,sandbox,production}, M={profile,release} (optional)
ifeq (${M},)
 flutter run --flavor ${F} --dart-define=FLAVOR=${F}
else
 flutter run --flavor ${F} --dart-define=FLAVOR=${F} --${M}
endif

 
使用例:

make run F=development        # デバッグモード
make run F=production M=release  # リリースモード

 

IDE(VS Code / Android Studio)での実行

各IDEで実行する場合は、それぞれ設定が必要です。

VS Code(設定)
.vscode/launch.json に設定を追加:

{
 "version": "0.2.0",
 "configurations": [
   {
     "name": "Local",
     "request": "launch",
     "type": "dart",
     "args": ["--flavor", "local", "--dart-define=FLAVOR=local"]
   },
   {
     "name": "Development",
     "request": "launch",
     "type": "dart",
     "args": ["--flavor", "development", "--dart-define=FLAVOR=development"]
   },
   {
     "name": "Sandbox",
     "request": "launch",
     "type": "dart",
     "args": ["--flavor", "sandbox", "--dart-define=FLAVOR=sandbox"]
   },
   {
     "name": "Production",
     "request": "launch",
     "type": "dart",
     "args": ["--flavor", "production", "--dart-define=FLAVOR=production"]
   }
 ]
}


上記の設定により、Run and Debugから構成名を選択して実行できるようになります。

Android Studio(設定)

  1. 上部メニューから RunEdit Configurations... を選択
  2. 左上の「+」ボタンをクリックし、「Flutter」を選択
  3. 各Flavor用の設定を作成:
項目 設定値(developmentの例)
Name Development
Dart entrypoint lib/main.dart
Additional run args --dart-define=FLAVOR=development
Build flavor development

4. 同様に local 、sandbox、production 用の設定も作成

これにより、ツールバーのドロップダウンから実行したいFlavorを選択して実行できるようになります。

8. まとめ


本記事では、FlutterのFlavor設定をiOS/Android両対応で解説しました。

ポイントのおさらい
  1. Android:build.gradleproductFlavors で設定する
  2. iOS:Xcode Scheme + Build Configurationで設定する。命名規則が重要
  3. Firebase:FlutterFire CLIで各Flavorの設定ファイルを生成する
  4. Dart:--dart-define でFlavorを渡し、enumで管理する

Flavorを適切に設定することで、開発効率とリリース品質の両方を向上させることができます。