0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LINEで電気代相談AIを作った話 - シミュレーター・解説・約款を1つの会話に統合する設計

0
Last updated at Posted at 2026-05-01

TL;DR

  • 個人開発の電気プラン比較サイト「エネジェント」 (https://enegent.jp) に、 LINE で何でも相談できる AI エージェントを乗せました
  • 動機は「シミュレーター・解説記事・約款の3つの情報源を行ったり来たりするのがしんどい」 という、 自分でサイトを作って初めて気づいた UX の限界です
  • 構成はざっくり、 LINE Webhook → Cloud Run → Gemini 2.5 Flash + 3 ツール (recommend_plans / search_knowledge / query_plans_by_condition)。 同じツールセットを MCP プロトコル経由でも呼び出せる形にしてあります
  • ポイントは「Gemini が会話の流れで複数ツールを自由に組み合わせる」 ことで初めて、 3 つの情報源が 1 つの会話に統合される、 という設計です

1. 動機: 自分のサイトなのに、 自分が使うのが面倒だった

エネジェントは経産省登録の小売電気事業者 800 社超をマスタに取り込み、 主要 49 社・345 プランを約款ベースで構造化して比較する個人開発サービスです。

サイトを作っていく過程で、 ユーザーが情報にアクセスする経路が 3 つ 並列で生えていました。

  1. シミュレーター — エリア・アンペア・月額を入れて推薦結果が出る
  2. 解説記事 — 157 記事の電気代解説 (基礎知識・各社レビュー・エリア別 etc.)
  3. 約款 (ファクトカード) — 49 社の運営会社・燃調方式・解約金条件・登録番号まで構造化したナレッジ

サイトとしてはこれで「全部揃ってる」 つもりでした。 けれど、 自分が実際に「電気代見直すか…」 と思ってサイトを開いてみると、 欲しい答えにたどり着くまでに 3 つの UI を行き来している自分 がいました。

「東京で月 1 万円ぐらいで安いプラン何?」
  → シミュレーターを開く
「あ、 1 位の会社って燃調どうなってるんだろ」
  → 解説記事を開く
「市場連動嫌だから外したいな」
  → 解説記事と約款を開く
「で、 結局市場連動を外した上での 1 位は?」
  → シミュレーターに戻って、 絞り込み条件を再入力

これ、 自分が作った張本人ですら毎回つらい。 800 社のデータを構造化してファクトカードまで用意したのに、 ユーザーがその恩恵を受けるには UI を 3 つ往復する根性が要る。 ライト層が「電気代見直したい」 と思って訪れて、 ここまで往復してくれる確率は低いです。

1 つの会話の中で全部聞けたら、 そのまま答えが出る」 のが正解だな、 と素直に思いました。 そこで LINE Bot に AI エージェントを乗せました。


2. できあがったもの: LINE で 1 つの会話で済む

実際の会話の流れはこんな感じです。 (実際の応答文をベースに、 個人特定情報を伏せて再現)

ユーザー: 東京で月 1 万円ぐらい、 30A です。 安いプラン教えて

エージェント: 東京エリア・30A・月 10,000 円の条件で比較しました。
1 位は ◯◯電気の△△プラン (年間約 5,400 円お得)、 2 位は…
[プランカードを Flex Message で表示]

ユーザー: 1 位のプラン、 燃調どうなってるの? 上限ある?

エージェント: ◯◯電気の△△プランは、 燃料費調整の上限を撤廃しています。
燃料価格が上がったときに請求にそのまま反映される仕組みです。
ご参考まで、 公式約款のリンクは…

ユーザー: それ嫌だな。 燃調上限あるプランで絞り込んで

エージェント: 燃調上限ありの条件で再検索しました。
新しい 1 位は ◇◇電気の□□プランで…

3 ターンの中で、 エージェントは裏でこういう動きをしています。

ターン 呼ばれたツール 何をしているか
1 recommend_plans シミュレーターと同じ計算ロジックを呼んで上位 5 件を返す
2 search_knowledge 約款ファクトカードを意味検索して、 燃調方式の事実を引っ張る
3 recommend_plans (絞り込み付き) 「燃調上限あり」 を絞り込み条件に入れて再計算

3 つの情報源 (シミュレーター・記事・約款) が、 1 つの会話の中で自然に行き来している。 ユーザーは UI を切り替えていません。 ずっと LINE で話してるだけです。


3. 構成図 (シンプル版)

LINE ユーザー
   ↓ メッセージ送信
LINE Webhook (Cloud Run / FastAPI)
   ↓ 履歴 + 状態をサマリ化して system prompt に注入
Gemini 2.5 Flash (function calling)
   ↓ 必要なツールを呼ぶ
   ├─ recommend_plans          → Supabase Postgres (プラン DB)
   ├─ search_knowledge         → Supabase pgvector (374 ファクトカード chunks)
   └─ query_plans_by_condition → Supabase Postgres (条件絞り込み)
   ↓ 結果を Gemini に返す
   ↓ 自然な日本語に整形 (2〜4 文)
LINE Bot
   ↓ 返信 + Flex Message プランカード
LINE ユーザー

Cloud Run + Supabase + Vercel の Free 枠で月 0 円 運営です。 個人開発なので無理はしない方針。

実装は 1 コンテナに同居しています。 LINE Webhook (/webhook) と、 Web 用同期版 (/api/chat) と、 MCP サーバ (/mcp/mcp) が同じプロセスで動いていて、 全員が同じツール定義 (bot/tools.py) を共有しています。 自分の Claude Desktop からも同じツールが呼べる形にしてあります (この部分の詳細は こちらの記事 に書きました)。


4. キモ: ツール 1 セットを、 LLM に「会話の流れで自由に組み合わせさせる」

実装のキモは「3 つのツールを LLM が会話の流れで自由に組み合わせて呼ぶ」 ところに尽きます。 ルーター的なロジックでツールを振り分けるのではなく、 ツールの仕様をちゃんと書いて Gemini にまるっと渡す。

# bot/tools.py (抜粋)

TOOL_SPECS = [
    ToolSpec(
        name="recommend_plans",
        description="エリア・契約状況から最適な電力プラン上位5件を返す。 月額や使用量から逆算して比較。",
        params=[
            ToolParam("area", "string", "エリア識別子 (tokyo / kansai 等)", required=True),
            ToolParam("ampere", "string", "契約アンペア (30A 等)", required=False),
            ToolParam("monthly_kwh", "number", "月の使用量 kWh", required=False),
            ToolParam("monthly_yen", "number", "月の請求額 円", required=False),
            ToolParam("current_retailer", "string", "現契約電力会社", required=False),
        ],
        handler=_tool_recommend_plans,
    ),
    ToolSpec(
        name="search_knowledge",
        description="各電力会社のファクトカードを意味検索。 運営会社・燃調方式・解約金・約款の事実回答に必須。",
        params=[
            ToolParam("query", "string", "検索クエリ", required=True),
            ToolParam("retailer_slug", "string", "対象会社 (任意で絞り込み)", required=False),
        ],
        handler=_tool_search_knowledge,
    ),
    ToolSpec(
        name="query_plans_by_condition",
        description="DB をプラン条件で検索 (例: 燃調上限あるプラン・解約金なしプラン)",
        params=[
            ToolParam("area", "string", "エリア識別子", required=True),
            ToolParam("require_fuel_cap", "boolean", "燃調上限ありに限定", required=False),
            ToolParam("require_no_cancel_fee", "boolean", "解約金なしに限定", required=False),
        ],
        handler=_tool_query_plans_by_condition,
    ),
]

これを Gemini に渡しておくと、 ユーザーの「燃調上限ありで絞って」 という発話に対して Gemini が勝手に「query_plans_by_conditionrequire_fuel_cap=True で呼べばいいな」 と判断してくれます。 こちらが if 文で書く必要はありません。

「会話の状態」 を毎ターン system prompt に再注入

LLM は会話履歴を持っていますが、 それだけだと「3 ターン前にユーザーが言った絞り込み条件」 をしばしば忘れます。 そこで毎ターン、 こういう状態サマリを system prompt の先頭に注入しています。

# 現在の会話状態サマリ (これを踏まえて応答する)
- エリア: 東京 / 月額: 10,000円 / 世帯: 未確定 / 現契約: 未申告
- 絞り込み条件: 燃調上限あり必須
- 既提示プランID (再提示禁止): ['abc-123', 'def-456', ...]
- 直前 1 位: ◯◯電気 (switch_recommended)

これだけで、 「さっきと同じプランをまた提示してしまう」「さっき言った絞り込み条件を忘れて緩めた結果を返す」 系の事故が大幅に減りました。 ChatGPT 時代の「LLM をシンプルに使い倒す」 流儀に倣って、 状態管理は LLM の頭の中ではなくプロンプトの目に見える形に置く。


5. ハマったところ (ライト版)

LINE の Markdown は素朴

LINE bot で **強調** をそのまま送ると ** が文字列として表示されます。 こちらで **...**【...】 に置換してから送ってます。

def render_for_line(text: str) -> str:
    return re.sub(r"\*\*(.+?)\*\*", r"【\1】", text)

Free 枠のレート制限

Gemini の Free 枠は 1 分あたりのリクエスト数に限りがあります。 ピークタイムに連投されたときの保険として、 LINE 側も Web 側もユーザー単位の 1 日上限 を入れています。 個人開発で「課金破滅」 しないための最低限の防御です。

Free 枠コールドスタート

Cloud Run は min-instances=0 で動かしているので、 LINE で初回メッセージが来た瞬間にコンテナが起き上がる数秒間、 ユーザーを待たせます。 これは Free 枠運営の宿命なので諦めています。 (Web LP 側ではボタン hover で warmup を投げる UX 工夫を入れていますが、 LINE 側からは仕掛けられないので)


6. やってみての教訓

  • シミュレーター + 解説記事 + 約款を別々に置くだけでは、 ユーザーは恩恵を受けない。 統合する UI が要ります。 そして 2026 年現在、 統合の最も自然な UI は 会話 です
  • 3 ツールを LLM に渡して自由に組み合わせさせる、 というのが今のベタープラクティスでした。 ルーターを自前で書こうとすると分岐が爆発します
  • 状態は LLM の頭ではなくプロンプトに書く。 「絞り込み条件」「既提示プラン ID」 のような会話を跨ぐ情報は、 毎ターン system prompt に再注入するのが安定します
  • 同じツール定義を MCP プロトコルでも喋れる形にしておくと、 LINE 以外のクライアント (自分の Claude Desktop 等) からも同じロジックを呼べて、 個人の生活でも便利

エネジェント (https://enegent.jp) は完全に趣味の個人プロジェクトで、 月 0 円で運営しています。 「AI に電気代相談したい」 と思ったときに、 中立な比較サービスが LINE で答えてくれる、 ぐらいの体験を目指しています。 個人開発で似たようなものを作っている方の参考になれば。

過去記事もあわせてどうぞ:

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?