より賢い受信トレイを構築する:LangChainとGmail APIによるメール振り分けの自動化

AI tutorial - IT technology blog
AI tutorial - IT technology blog

午前2時15分のページャーの悲鳴

ページャーが鳴り響いたのは、ある火曜日の朝のことでした。私たちの主力SaaS製品のサポート用受信トレイが限界に達していました。プロモーションキャンペーンが、不名誉な形でバイラル(炎上)してしまったのです。ユーザーは混乱し、「一般的な問い合わせ」フォルダには400件以上の未読スレッドが溢れかえっていました。私のチームは、手動での振り分けという悪夢に溺れていました。私たちは、重要度の高い課金エラーと単なる「ありがとう」のメッセージを分けるためだけに、終わりのないメッセージをクリックし続けていました。

山積みになったキューを見つめながら、キーワードベースのフィルタは役に立たないどころか、むしろ逆効果であることに気づきました。急増する問い合わせを乗り切るために必要な唯一のもの、つまり「コンテキスト(文脈)」が欠けていたのです。

ボトルネック:なぜルールベースのシステムは失敗するのか

この混乱はスタッフ不足が原因ではありませんでした。自動化の脆弱さが原因だったのです。従来のメールフィルタは、正確な文字列の一致に依存しています。ユーザーが「アカウントが壊れている」と書けば、正規表現(regex)でキャッチできるかもしれません。しかし、「アップデート後にダッシュボードにアクセスできないようです。助けてください」と書かれた場合、フィルタはしばしば失敗します。キーワードを一つ見落とすだけで、優先度の高いチケットはブラックホールへと消えてしまいます。

人間のように読み取り、機械のスピードで処理できるシステムが必要でした。具体的には、次の3つの核心的な課題を解決しなければなりませんでした。

  • 意図の認識: 5,000ドルのセールスリードと、基本的な機能要望を正確に区別すること。
  • 優先順位のマッピング: 解約(チャーン)の恐れがある、不満を感じている「リスクのある」顧客を特定すること。
  • 応答のレイテンシ: 6時間の応答時間を15分未満に短縮すること。

ツールセットの評価

あの深夜のセッションで、私は3つの選択肢を検討しました。律儀に比較した結果は以下の通りです。

  1. ノーコードプラットフォーム(Zapier/Make): 単純なタスクには優れていますが、月に1万件以上のタスクを処理するようになるとコストが急騰します。また、複雑な状態管理も苦手です。
  2. 独自のPython + 正規表現: これが私たちの現状でした。顧客が私たちの想定していなかった類義語を使うたびに、システムは破綻していました。
  3. AIスタック(LangChain + Gmail API): これが明白な勝者でした。LangChainは「構造化出力」を可能にします。乱雑な要約ではなく、メールの本質(DNA)を表すクリーンなJSONオブジェクトを取得できるため、バックエンドですぐに処理できます。

戦略:LangChainによるインテリジェントなメール振り分け

本番環境において、静的なコードから「インテリジェント」なエージェントへ移行することは、ラボでしか動かないツールと、現実世界で通用するツールの差となります。私たちは、未読メールを取得し、大規模言語モデル(LLM)を使用して分類し、Gmail APIを使用して下書きを生成するパイプラインを構築しました。これにより、人間による最終確認を維持しつつ、サポート担当者が直面する「白紙の状態から返信を書く」という負担を取り除きました。

フェーズ1:Gmail APIとの連携

まず、Google Cloud ConsoleでGmail APIを有効にします。OAuth 2.0 クライアント IDとcredentials.jsonファイルが必要です。接続の管理には、標準のGoogle Authライブラリを使用します。

pip install langchain langchain-openai google-api-python-client google-auth-httplib2 google-auth-oauthlib

長時間実行されるセッション中に、スクリプトが処理の途中で停止しないよう、トークンのリフレッシュロジックを実装しました:

import os.path
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build

SCOPES = ['https://www.googleapis.com/auth/gmail.modify']

def get_gmail_service():
    creds = None
    if os.path.exists('token.json'):
        creds = Credentials.from_authorized_user_file('token.json', SCOPES)
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file('credentials.json', SCOPES)
            creds = flow.run_local_server(port=0)
        with open('token.json', 'w') as token:
            token.write(creds.to_json())
    return build('gmail', 'v1', credentials=creds)

フェーズ2:Pydanticによるインテリジェンスのモデリング

LLMからの生のテキストは解析が困難です。AIの出力を予測可能にするために、私はPydanticを使用しました。これにより、モデルはお喋りな回答を返すのではなく、構造化されたフォームに入力するように強制されます。

from pydantic import BaseModel, Field
from typing import List

class EmailAnalysis(BaseModel):
    intent: str = Field(description="Billing, Technical, Sales, または Spam に分類してください")
    priority: int = Field(description="1 (低) から 5 (緊急) でスコアリングしてください")
    summary: str = Field(description="問題の核心を1文で要約してください")
    suggested_reply: str = Field(description="プロフェッショナルな返信の下書き")

フェーズ3:LangChainオーケストレーター

このパイプラインにはGPT-4oを選びました。小規模なモデルの方が安価ですが、乱雑でカジュアルなメールを処理する場合、分類におけるGPT-4oの推論能力は大幅に信頼性が高くなります。データ形式を保証するために、with_structured_outputメソッドを使用します。

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate

llm = ChatOpenAI(model="gpt-4o", temperature=0)
structured_llm = llm.with_structured_output(EmailAnalysis)

analysis_prompt = ChatPromptTemplate.from_messages([
    ("system", "あなたはシニアサポートエンジニアです。メールを分析し、構造化された分析結果を提供してください。"),
    ("user", "件名: {subject}\n送信者: {sender}\n本文: {body}")
])

chain = analysis_prompt | structured_llm

フェーズ4:リスクのない下書き作成

AIが生成したメールを自動的に送信することは、広報上の大惨事を招きかねないリスクがあります。代わりに、私たちは下書きを作成します。これにより、サポートチームは人間による監視を維持しながら、大幅な時間短縮が可能になります。

import base64
from email.message import EmailMessage

def create_draft(service, to_email, subject, body, thread_id):
    message = EmailMessage()
    message.set_content(body)
    message['To'] = to_email
    message['Subject'] = f"Re: {subject}"
    
    # Gmail APIはbase64urlエンコーディングを想定しています
    encoded_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
    
    create_message = {
        'message': {
            'threadId': thread_id,
            'raw': encoded_message
        }
    }
    service.users().drafts().create(userId="me", body=create_message).execute()

現実世界での成果

その効果はすぐに現れました。その日の午前4時までに、システムは「緊急」フォルダを正確に埋めていました。サポートリーダーは、レビュー待ちの50件の作成済み下書きとともに目を覚ましました。初回の応答時間は平均6時間からわずか12分に激減しました。さらに重要なことに、ノイズの中に埋もれていた高価値なセールスリードを見逃すことがなくなりました。

本番環境に向けた微調整

苦労して学んだ教訓:入力をサニタイズ(浄化)することです。ユーザーはしばしば20行に及ぶ署名や、以前のスレッドの履歴全体を貼り付けます。これらはトークン予算を浪費し、LLMを混乱させます。スレッド内の最新のメッセージ以外をすべて削除するユーティリティを使用することをお勧めします。これによりコストが節約され、分析の焦点を絞ることができます。

LLMを構造化データプロセッサとして扱うことで、混沌とした受信トレイを高速なパイプラインに変えることができます。もし、まだ手作業でメールを仕分けているのであれば、その認知的負荷をLangChainに任せて、実際の問題解決に集中すべき時です。

Share: