はじめに

この蚘事は Google ADK によるマルチ゚ヌゞェント瀟内ITヘルプデスク構築 をテヌマにした党4回シリヌズの第3回です。

回 テヌマ
第1回 Gemini CLI で゚ヌゞェントを動かしおみる
第2回 ADK でマルチ゚ヌゞェントを構築し Google Cloud にデプロむする
第3回本蚘事 ADK マルチ゚ヌゞェントを BigQuery ず連携させる
第4回 Gen AI evaluation service で ADK マルチ゚ヌゞェントの応答品質を枬る

第2回たでで䜜った゚ヌゞェントは、FAQ デヌタを Python のリストずしお盎接コヌドに曞いおいたした。
デモには十分ですが、実運甚を考えるずいく぀か困るずころが出おきたす。

  • FAQ を 1 件远加するたびにコヌドを曞き換えおデプロむし盎す必芁がある
  • どんな問い合わせがどう凊理されたかの履歎がどこにも残らない
  • FAQ が増えおきたずきにカテゎリ別の傟向を分析できない

本蚘事ではこれらの課題を解決するため、デヌタ゜ヌスをBigQueryに移したす。

䜕を䜜ったか

党䜓構成

第2回たでず同じ 3 ぀の゚ヌゞェント構成は維持し぀぀、デヌタの眮き堎所を、Python のリストから BigQuery ぞず切り替えたす。

ナヌザヌ問い合わせ
    ↓
ルヌタヌ゚ヌゞェント
    ↓
FAQ 怜玢゚ヌゞェント ──[SELECT]──> faq_master テヌブル
    │
    ├─ FAQ ヒット → 自分が回答 ──[INSERT resolved=true]──> inquiry_log テヌブル
    │
    └─ FAQ なし
        ↓
        ゚スカレヌション゚ヌゞェント
        ↓ チケット情報を返す
        ──[INSERT resolved=false]──> inquiry_log テヌブル

甚意した 2 ぀のテヌブル

テヌブル 圹割 䞭身
faq_master FAQ マスタヌ 8 件の FAQ。ID・カテゎリ・質問・回答・キヌワヌド・曎新日時
inquiry_log 問い合わせログ ゚ヌゞェントが応答するたびに 1 行ず぀蚘録される。誰が䜕を聞き、どの゚ヌゞェントが䜕ず答え、解決したかが残る

inquiry_log にはsatisfaction_score満足床スコアカラムも事前に甚意しおありたす。
本蚘事では NULL のたたですが、第4回評䟡で掻甚したす。

第2回からの倉曎点

項目 第2回 第3回本蚘事
FAQ デヌタ Python のリストコヌドに盎曞き BigQuery faq_master テヌブル
FAQ の远加 コヌド倉曎再デプロむ テヌブルに行を远加するだけ
問い合わせ履歎 残らない inquiry_log に1行ず぀自動蚘録
゚ヌゞェント構成 router / faq_searcher / escalation 同じツヌル関数だけ差し替え
デプロむ方法 Vertex AI Agent Engine 同じ

蚭蚈のポむント

① キヌワヌドは「固有な耇合語」にする

faq_master の keywords カラムには、各 FAQ にひもづく怜玢ワヌドをカンマ区切りで入れたす。
最初は玠盎に「アカりント」「ログむン」のような単語を入れおいたのですが、これだず無関係な問い合わせたでヒットしおしたう問題が発生したした。

ナヌザヌの質問: 「自分のアカりントで身に芚えのないログむン履歎がありたす」
→ 本来はセキュリティむンシデントずしお゚スカレヌションすべき
→ ずころが「アカりント」「ログむン」ずいうキヌワヌドに匕っかかり、
   パスワヌドリセット FAQ がヒットしおしたう

そこで、キヌワヌドを「アカりントロック」「VPN接続」「パスワヌドを忘れ」のような固有な耇合語に曞き換えたした。これだけで誀マッチが枛少したす。

単語そのものより「その FAQ でしか出おこない蚀い回し」を遞ぶず粟床が䞊がりたす。

② FAQ 怜玢は「ナヌザヌ入力にキヌワヌドが含たれるか」で刀定

ナヌザヌが「VPN に接続しようずするず認蚌゚ラヌが 」ず長文で質問しおきたずき、faq_master のどの行を返すべきか。
本蚘事では「keywords の各単語がナヌザヌ入力に含たれおいれば、その FAQ を返す」ずいうシンプルな仕様にしたした。SQL で曞くず、keywords をカンマで分割し、各キヌワヌドがナヌザヌ入力に郚分䞀臎するかを STRPOS で刀定する圢になりたす。

SELECT faq_id, category, question, answer
FROM faq_master, UNNEST(SPLIT(keywords, ',')) AS kw
WHERE STRPOS(LOWER(@q), LOWER(TRIM(kw))) > 0
ORDER BY LENGTH(kw) DESC
LIMIT 1

ORDER BY LENGTH(kw) DESC で「より長い具䜓的なキヌワヌド」に䞀臎した FAQ を優先しおいたす。
これで「アカりントロック」が「ロック」よりも先に評䟡されたす。

BigQuery の党文怜玢 SEARCH 関数も怜蚎したしたが、デフォルトのアナラむザは日本語の圢態玠解析に察応しおおらず、自然文ク゚リで期埅どおりにヒットしたせんでした。今回は仕様がシンプルな UNNEST + STRPOS 方匏を採甚したした。

③ ログ重耇を防ぐ「責務分離」

inquiry_log ぞの曞き蟌みは、䞡方の゚ヌゞェントが䜕の制限もなく行うず1 ぀の問い合わせで 2 行のログが残っおしたいたすFAQ 怜玢゚ヌゞェントが曞いた埌、゚スカレヌション゚ヌゞェントも曞く。
そこで、゚ヌゞェントの instruction指瀺曞で曞き分けたした。

  • FAQ 怜玢゚ヌゞェント: FAQ がヒットしお自分が回答した堎合 のみ ログを曞くresolved=true
  • ゚スカレヌション゚ヌゞェント: 必ずログを曞くresolved=false

「最終応答を返した゚ヌゞェントが 1 回だけログを曞く」ずいうルヌルにするこずで、1 問い合わせ1 レコヌド を担保しおいたす。

実装のキモ

本蚘事では蚭蚈刀断が珟れるポむントを抜粋したす゚ラヌハンドリングや现かい敎圢凊理は省略。

FAQ 怜玢ツヌルagents/faq_searcher.py

search_faq() の䞭身は、䞊で説明した SQL を BigQuery に投げおヒットした 1 行を敎圢しお返すだけです。

_BQ_CLIENT = bigquery.Client()  # モゞュヌルトップで初期化再利甚される

def search_faq(query: str) -> str:
    sql = """
    SELECT faq_id, category, question, answer
    FROM `helpdesk_demo.faq_master`,
         UNNEST(SPLIT(keywords, ',')) AS kw
    WHERE STRPOS(LOWER(@q), LOWER(TRIM(kw))) > 0
    ORDER BY LENGTH(kw) DESC
    LIMIT 1
    """
    # ... ク゚リを実行しお結果を敎圢しお返す

第2回では同じ関数の䞭で for faq in MOCK_FAQ_DATA: ... ず Python ルヌプしおいた郚分が、BigQuery ぞの SELECT ク゚リに眮き換わっただけです。
゚ヌゞェントの定矩偎instruction、tools の枡し方などはほずんど倉曎しおいたせん。

ログ曞き蟌みツヌルagents/inquiry_logger.py

新芏ファむルです。BigQuery の inquiry_log テヌブルに 1 行 INSERT するだけのシンプルなツヌル。

def log_inquiry(user_question, category, routed_to, agent_response, resolved):
    row = {
        "inquiry_id": str(uuid.uuid4()),
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "user_question": user_question,
        "category": category,
        "routed_to": routed_to,
        "agent_response": agent_response,
        "resolved": resolved,
        "satisfaction_score": None,
    }
    _BQ_CLIENT.insert_rows_json(_TABLE_ID, [row])

satisfaction_score は本蚘事では None のたた入れおいたすが、第4回評䟡で倀を曎新しお䜿いたす。
このツヌルを faq_searcher_agent ず escalation_agent の䞡方の tools に登録し、それぞれの instruction で「い぀呌ぶか」を曞き分けるこずで、LLM が応答を返したあずに、文脈から刀断しおログを残す仕組みになりたす。

動䜜確認

ロヌカル実行

demo.py を実行するず、4 ケヌスの問い合わせがそれぞれ正しく振り分けられお応答が返りたす出力は冗長なので䞀郚省略。

[問い合わせ 1] VPN に接続しようずするず認蚌゚ラヌが出お接続できたせん
→ FAQ 怜玢゚ヌゞェントが NW-001 を返すカテゎリネットワヌク系

[問い合わせ 2] パスワヌドを忘れおしたいたした
→ FAQ 怜玢゚ヌゞェントが AC-001 を返すカテゎリアカりント系

[問い合わせ 3] 自分のアカりントで身に芚えのないログむン履歎がありたす
→ FAQ ヒットなし → ゚スカレヌションカテゎリセキュリティ、緊急床高

[問い合わせ 4] 瀟内の業務システムが党員䜿えなくなっおいたす
→ FAQ ヒットなし → ゚スカレヌションカテゎリシステム障害、緊急床高

inquiry_log に䜕が蚘録されたか

実行埌に inquiry_log を SELECT するず、4 件分のレコヌドが残っおいたした。

user_question category routed_to resolved
VPN に接続しようずするず認蚌゚ラヌが出お接続できたせん ネットワヌク系 faq_searcher_agent true
パスワヌドを忘れおしたいたした。リセットする方法を教えおください アカりント系 faq_searcher_agent true
自分のアカりントで身に芚えのないログむン履歎がありたす セキュリティ escalation_agent false
瀟内の業務システムが党員䜿えなくなっおいたす システム障害 escalation_agent false

各問い合わせが 1 行ず぀・最終応答した゚ヌゞェントの蚘録ずしお残っおいたす。
ここで重芁なのは、FAQ ヒットケヌスは resolved=true、゚スカレヌションケヌスは resolved=false で蚘録されおいる点です。
これによっお、運甚が始たったあずに「どれくらいの問い合わせを FAQ で自動解決できおいるか」を SQL ひず぀で集蚈できるようになりたした。

クラりドVertex AI Agent Engineぞの再デプロむ

第2回でデプロむした゚ヌゞェントを、BigQuery 連携版に差し替えたす。コヌド䞊の倉曎は requirements に google-cloud-bigquery を远加するだけ です。
ただし、デプロむ前にひず぀だけ準備が芁りたす。

Reasoning Engine の SA に BigQuery 暩限を付䞎

Vertex AI には、リ゜ヌス管理甚の汎甚サヌビスアカりントgcp-sa-aiplatformず、Reasoning Engine 䞊でナヌザヌコヌドを実行する専甚サヌビスアカりントgcp-sa-aiplatform-reがありたす。BigQuery にアクセスするのは埌者なので、暩限はこちらに付䞎する必芁がありたす。
具䜓的には service-<番号>@gcp-sa-aiplatform-re.iam.gserviceaccount.com-re サフィックスです。この SA に BigQuery ぞのアクセス暩限を 2 ぀付䞎しないず、゚ヌゞェントから faq_master を読んだり inquiry_log に曞き蟌んだりができたせん。

ロヌル 甹途
roles/bigquery.jobUser ク゚リゞョブを発行する暩限faq_master の SELECT に必芁
roles/bigquery.dataEditor テヌブルにデヌタを曞き蟌む暩限inquiry_log の INSERT に必芁

暩限さえ付䞎できれば、python deploy.py で玄 5 分ほどで再デプロむが完了したす。

クラりド偎でも同じ動䜜を確認

クラりドにデプロむした゚ヌゞェントに同じ 4 問い合わせを送ったずころ、ロヌカルず同じ応答が返り、inquiry_log にも同じスキヌマで 4 行のレコヌドが远加されおいたした。

user_question category routed_to resolved
VPN に接続しようずするず認蚌゚ラヌが出お接続できたせん ネットワヌク系 faq_searcher_agent true
パスワヌドを忘れおしたいたした。リセットする方法を教えおください アカりント系 faq_searcher_agent true
自分のアカりントで身に芚えのないログむン履歎がありたす セキュリティ escalation_agent false
瀟内の業務システムが党員䜿えなくなっおいたす システム障害 escalation_agent false

ロヌカルずクラりドで同じデヌタが同じスキヌマで貯たっおいく状態が完成したした。

ハマりどころ

぀たずきポむント 察凊
Reasoning Engine の SA を間違える Vertex AI 党䜓の SAgcp-sa-aiplatformではなく、gcp-sa-aiplatform-re に暩限付䞎する
extra_packages=["agents"] を忘れる 新芏远加した inquiry_logger.py が同梱されず、コンテナ起動時に ModuleNotFoundError
ログが 1 問い合わせで 2 行残る ゚ヌゞェントの instruction で「FAQ ヒット時のみ」「゚スカレヌション時のみ」ず曞き分ける
キヌワヌドの粒床が粗い 「アカりント」より「アカりントロック」のような 固有な耇合語 にする

たずめず次回に぀いお

第3回で達成したのは、芋た目の掟手さは少ないものの運甚に近づくための地味で重芁な倉曎です。

  • FAQ がコヌドから切り離され、テヌブルの行を远加するだけで増やせる
  • どんな問い合わせがどう凊理されたかが、すべお SQL で集蚈できる圢で残る
  • ロヌカルでもクラりドでも、同じスキヌマでデヌタが貯たる
    これで 第4回Vertex AI Evaluation でマルチ゚ヌゞェントの応答品質を枬る に進む土台が敎いたした。次回は inquiry_log に貯たったデヌタを評䟡のサンプルずしお䜿い、ルヌティング・回答・フォヌルバックの3軞で゚ヌゞェントの応答品質を定量的に枬りたす。

参考リンク