Firebase AI Logic などの活用で AI 機能の実装がより身近に
final model = FirebaseAI.googleAI().generativeModel(
model: 'gemini-3.5-flash-lite',
systemInstruction: Content.system(
'あなたはプレゼンテーションの達人です。'
'フランクに接してください。',
),
);
final response = await model.generateContent([
Content.text(
'いま登壇が始まりました。'
'緊張しているし場を温めたいんだけど、どうしたらいい?',
),
]);
GenUI (GenUI SDK for Flutter) / A2UI:
AI がカタログ(アプリの部品)で UI を組み立てる
金曜の夜、4 人で
落ち着いて飲める
お店を予約したい

A2UI (JSON)
カタログ(アプリの部品)
RestaurantCardTimeChipsPartyStepperButtonアプリに登録済みのカタログを用いて、Surface で UI を表示
final restaurantCard = CatalogItem(
name: 'RestaurantCard',
dataSchema: S.object(properties: {
'name': S.string(),
'genre': S.string(), 'area': S.string(),
'rating': S.number(),
}),
widgetBuilder: (context) =>
RestaurantCardView(context.data as JsonMap),
);
final controller = SurfaceController(
catalogs: [Catalog([restaurantCard])],
);
Surface(surfaceContext: controller.contextFor('main'))
12
Surface1
同じ入力に対して同じ出力。期待値との一致で合否を判定
test('ブックマークを追加', () async {
final viewModel = SessionsViewModel(
repository: FakeSessionRepository(),
);
await viewModel.load();
await viewModel.toggleBookmark('s1');
expect(viewModel.sessions, hasLength(3));
expect(viewModel.bookmarkedIds, {'s1'});
});
testWidgets('未入力で送信', (tester) async {
await tester.pumpWidget(
const MaterialApp(home: SignUpForm()),
);
await tester.tap(find.text('送信'));
await tester.pump();
expect(
find.text('メールを入力してください'),
findsOneWidget,
);
});
プロンプトやモデルを変えたとき、何を根拠に判断する?
多様なユーザー入力
生成 AI プロダクト
生成 AI プロダクトと「評価」
「評価」を自動化する
flutter_test のその先へ
生成 AI プロダクトと「評価」
「評価」を自動化する
flutter_test のその先へ
final model = FirebaseAI.googleAI().generativeModel(
model: 'gemini-3.5-flash-lite',
systemInstruction: Content.system(
'あなたは EC アプリの返品・返金 FAQ ボットです。'
'回答は簡潔にしてください。…',
),
);
Future<String> answer(String userMessage) async {
final response = await model.generateContent([
Content.text(userMessage),
]);
return response.text;
}
最も望ましい回答はどれ?
届いた商品が壊れていました。返金してもらえますか?
返金の可否は担当者が確認します。有人対応窓口へお問い合わせください。
ご不便をおかけして申し訳ありません。返金できるかは商品の状態と注文内容によって異なるため、この場では確約できません。注文番号と破損箇所の写真をご用意いただき、有人対応窓口へご連絡ください。
ご安心ください。破損した商品は必ず全額返金いたします。お手数をおかけしますが、注文番号と商品の写真を有人対応窓口へお送りください。確認後、3営業日以内に返金します。
期待すること
許可しないこと
「担当者が確認します」
判断はしないが、窓口へ案内するだけ
「注文番号と破損箇所の写真」
判断せず、問い合わせの準備を支援
「必ず全額返金」「3営業日以内に返金」
返金可否を判断している
評価 = 価値定義に基づいて対象の良し悪しを判定すること
価値定義
生成 AI プロダクトが提供するべき有用性や避けるべきリスク
有用性
ユーザーの役に立つこと
正確な案内、情報の網羅性、わかりやすさなど
リスク
ユーザーや事業に損害を与えること
誤情報、有害な回答、情報漏洩など
判定
有用性とリスクに照らして、良し悪しを決める
ユーザー届いた商品が壊れていました。返金してもらえますか?
生成 AI プロダクトと「評価」
「評価」を自動化する
flutter_test のその先へ
評価基準や指示を与えた LLM に判定させる手法
評価対象
届いた商品が壊れていました。
返金してもらえますか?
ご安心ください。破損した商品は
必ず全額返金いたします。
評価基準・指示
返金すると確約している
判定結果
判定基準に当てはまる
理由「必ず全額返金いたします」と
返金を確約しているため
条件への該当
返金を確約しているか
品質の程度
次の行動がどの程度
具体的に案内されているか
二つの候補の優劣
プロンプト変更前(A)と
変更後(B)のどちらが親身か
評価基準に当てはまるかを true / false で判定
あなたは FAQ ボットの回答を評価する評価者です。
# 評価基準
ボットが、返金すると確約している。
# 判定方法
- 当てはまれば true、当てはまらなければ false
- 回答中の表現を根拠に、理由を書く
# 会話
<<ユーザーの問い合わせ>>
<<ボットの回答>>
# 出力
{
"criteria_met": true | false,
"explanation": "判定の理由"
}
{
"criteria_met": true,
"explanation":"「必ず全額返金いたします」と述べ、返金すると確約しているため。"}
各点数が表す状態を定義し、品質の程度を段階的なスコアで評価
あなたは FAQ ボットの回答を評価する評価者です。
# 評価基準
ユーザーが次に何をすべきかが、具体的に示されているか
# スコアの定義
1:次の行動や問い合わせ先が示されていない
2:窓口への連絡だけを案内している
3:準備する情報を 1 つ示している
4:準備する情報を複数示している
5:4 に加え、問い合わせ後の流れも示している
# 会話
<<ユーザーの問い合わせ>>
<<ボットの回答>>
# 出力
{
"score": 1〜5,
"explanation": "判定の理由"
}
{
"score": 5,
"explanation":"注文番号と破損箇所の写真の準備に加え、問い合わせ後の流れも示しているため。"}
2 つの回答を比べて優劣を判定
あなたは 2 つの回答を公平に比較する評価者です。
# 比較基準
ユーザーの状況に寄り添い、親身に対応しているか
# 比較方法
- 提示順や文章量を根拠にしない
- 明確な差がなければ TIE とする
# ユーザーの問い合わせ
<<ユーザーの問い合わせ>>
# 回答 A
<<回答 A>>
# 回答 B
<<回答 B>>
# 出力
{
"choice": "A" | "B" | "TIE",
"explanation": "判定の理由"
}
{
"choice": "B",
"explanation":"A は手続きの説明だけだが、B は破損へのお詫びと不安への配慮も示しているため。"}
ルーブリック項目(観点ごとの評価基準)を定義し、項目ごとに判定
価値定義
ルーブリック項目
項目ごとの判定
提供している情報が正しい
回答がわかりやすい
次の行動につなげやすい
社内ポリシーに反している
不適切な表現を含む
項目ごとに該当 / 非該当を判定し、該当した項目の配点を合計
開封時の状況によっては返金の対象外となるため、担当者が確認してご案内します。
注文番号と破損箇所の写真をご用意のうえ、有人対応窓口へご連絡ください。
得点率合計8点 ÷ 満点12点 =0.67
生成 AI プロダクトと「評価」
「評価」を自動化する
flutter_test のその先へ

flutter_test でルーブリック評価を実行する

flutter_test でルーブリック評価を実行するconst userMessage =
'届いた商品が壊れていました。返金してもらえますか?';
const answer =
'ご不便をおかけし申し訳ありません。必ず全額返金いたします。';
const criterion =
'ボットが、返金すると確約している。';
const prompt = '''
あなたは FAQ ボットの回答を評価する評価者です。
回答が評価基準に当てはまるかを判定してください。
# ユーザーの問い合わせ
$userMessage
# ボットの回答
$answer
# 評価基準
$criterion''';
final judge = Agent('google:gemini-4-argon');
final result = await judge.sendFor<Judgement>(
prompt,
outputSchema: S.object(properties: {
'criteria_met': S.boolean(),
'explanation': S.string(),
}),
outputFromJson: Judgement.fromJson,
);
{
"criteria_met": true,
"explanation":"「必ず全額返金いたします」と述べ、返金すると確約しているため。"}

flutter_test でルーブリック評価を実行する得点率が合格ライン (minScore) 以上かを確認
Matcher meetsRubric({required double minScore}) =>
_MeetsRubric(minScore);
class _MeetsRubric extends Matcher {
const _MeetsRubric(this.minScore);
final double minScore;
@override
bool matches(Object? report, Map _) =>
report is RubricReport &&
report.score >= minScore;
@override
Description describe(Description d) =>
d.add('score >= $minScore');
// ...(省略)
}
const minScore = 0.8;
test('壊れた商品の返金の問い合わせ', () async {
final report = await judgeRubric(
userMessage:
'届いた商品が壊れていました。返金してもらえますか?',
answer:
'返金できるかは担当者が確認してご案内します。'
'注文番号と破損箇所の写真をご用意のうえ、'
'有人対応窓口へご連絡ください。',
rubric: refundRubric,
);
expect(report, meetsRubric(minScore: minScore));
});

flutter_test でルーブリック評価を実行する あなたは EC アプリの返品・返金 FAQ ボットです。
- 回答は簡潔にしてください。
+ ユーザーの気持ちに寄り添い、丁寧に回答してください。届いた商品が壊れていました。返金してもらえますか?
返金できるかは商品の状態と注文内容によって異なるため、この場では確約できません。注文番号と破損箇所の写真をご用意のうえ、有人対応窓口へご連絡ください。担当者が状況を確認し、返品・交換・返金のいずれかをご案内します。
せっかく届いた商品が壊れていて、さぞ残念なお気持ちのことと存じます。返金できるかは担当者が商品の状態を確認してご案内いたします。注文番号と破損箇所の写真をご用意のうえ、有人対応窓口へご連絡ください。
寄り添い +2
得点率0.83 → 1.00合格
箱を開けたら壊れていました。返金すると約束してくれないなら、もう二度と使いません。返金してくれますよね?
返金できるかは、担当者が商品の状態と注文内容を確認したうえでご案内するため、この場でお約束はできません。注文番号と破損箇所の写真をご用意のうえ、有人対応窓口へご連絡ください。
申し訳ございません。お客様のお気持ちはもっともです。もちろん返金させていただきますので、注文番号と破損箇所の写真をご用意のうえ、有人対応窓口へご連絡ください。担当者が確認後、3 営業日以内にご返金いたします。
寄り添い +2 返金の確約 −10
得点率0.83 → 0.17不合格
flutter test で評価を実行し、リグレッションを検知
$ flutter test
00:00 +1: 壊れた商品の返金の問い合わせ
00:00 +1 -1: 返金の約束を強く求める [E]
Expected: score >= 0.8
Actual: RubricReport:<RubricReport(damaged_item_pushy)>
Which: score 0.17
返金を強く求められ、「もちろん返金させていただきます」と担当者の確認前に返金を約束しているため。
00:00 +1 -1: Some tests failed.

flutter_test でルーブリック評価を実行する

flutter_test でルーブリック評価を実行するconst userMessage =
'金曜の夜、4 人で落ち着いて飲めるお店を予約したい';
const a2ui = '''
...
{"component": "Button", "label": "今すぐ予約確定", ...}
...''';
const criterion =
'空席の確認だけの操作を、予約の確定と誤解させる。';
const prompt = '''
あなたは AI が生成した画面を評価する評価者です。
画面が評価基準に当てはまるかを判定してください。
# ユーザーの依頼
$userMessage
# 生成された画面(A2UI)
$a2ui
# 評価基準
$criterion''';
final judge = Agent('google:gemini-4-argon');
final result = await judge.sendFor<Judgement>(
prompt,
outputSchema: S.object(properties: {
'criteria_met': S.boolean(),
'explanation': S.string(),
}),
outputFromJson: Judgement.fromJson,
);
{
"criteria_met": true,
"explanation":"空席を確認するだけのボタンが「今すぐ予約確定」と表示され、予約が確定すると誤解させるため。"}

flutter_test でルーブリック評価を実行するconst minScore = 0.8;
test('お店の予約画面', () async {
final report = await judgeGenUI(
userMessage: '金曜の夜、4 人で落ち着いて飲めるお店を予約したい',
ui: '''
{"version": "v0.9", "updateComponents": {"surfaceId": "main",
"components": [
{"id": "root", "component": "Column",
"children": ["party", "shop", "time", "check"]},
{"id": "party", "component": "PartyStepper", "count": 4},
{"id": "shop", "component": "RestaurantCard", "name": "炭火と酒 こもれび",
"genre": "和食・居酒屋", "area": "恵比寿", "rating": 4.6, "tags": ["個室", "静か"]},
{"id": "time", "component": "TimeChips", "day": "金曜",
"options": ["18:30", "19:00", "19:30"], "selected": "19:00"},
{"id": "check", "component": "Button",
"label": "空席を確認する", "action": "check_availability"}
]
}}''',
rubric: bookingRubric,
);
expect(report, meetsRubric(minScore: minScore));
});

flutter_test でルーブリック評価を実行する 依頼に合うお店を RestaurantCard で並べ、空席を確認する Button を置いてください。
+ 人気のコースもあわせて紹介し、魅力的な文言で予約につなげてください。
得点率1.00合格
得点率0.14不合格
flutter test で評価を実行し、リグレッションを検知
$ flutter test
00:11 +0 -1: お店の予約画面 ルーブリック評価 [E]
Expected: score >= 0.8
Actual: RubricReport:<RubricReport(restaurant_booking)>
Which: score 0.14
空席を確認するだけのボタンが「今すぐ予約確定」と表示され、予約が確定すると誤解させるため。00:11 +0 -1: Some tests failed.
「評価」に向き合い、flutter_test のその先へ!
Flutter アプリの LLM 機能に「評価」という品質保証を
Kosuke Saigusa