GeoAIエージェントの中身【第2回】ES|QLツールと現在地の渡し方

BLOG

意味で碑を探し、場所と災害種別で避難場所を絞る。その検索を支えるES|QLツール、現在地の受け渡し、トレースで分かったことを紹介します。

はじめに

第1回では、防災のAIエージェントを例に、意味検索で見つけた碑の災害種別を次の避難場所検索に使う「属性の引き継ぎ」を紹介しました。今回は、その仕組みを次の順番で見ていきます。

  1. エージェントが持っている 3 つのツールは、何をしているのか
  2. 意味検索とその点数を、どこまで使うのか
  3. エージェントは、どんなルールで動いているのか
  4. 検索の中心になる現在地を、どうやって安全に渡しているのか

コードは検索の流れを追うために載せています。構文を覚えるより、Elasticsearchが確かめる条件と、LLMが判断する部分に注目してください。検証環境はElastic Cloud Serverless(Kibana 9.6.0、2026年9月確認)です。

先に用語を 5 つだけ

用語一言でいうと
Elasticsearch大量のデータを入れて、速く検索するためのデータベース。全文検索と地理検索が得意
インデックスElasticsearch に登録したデータのまとまり。今回は伝承碑2,469件と避難場所115,829件を使う
フィールドと型表の「列」と、その列に入るデータの種類。文字(keyword)、文章(text)、位置(geo_point)、意味(semantic_text)など
ES|QLElasticsearch の問い合わせ言語。FROM 表 | WHERE 条件 | SORT 並び順 のように、縦棒(パイプ)で処理をつなげる
Agent BuilderKibana の中で AI エージェントを作る機能。エージェントに「ツール」を持たせ、日本語の質問に答えさせる

全体の流れ

利用者の質問(日本語)
   ↓
エージェント(LLM: Anthropic Claude Sonnet 5、Elastic 経由)
   ├─ ① 会話に添付された現在地を読む(attachments.read)。期限を確かめる
   ├─ ② 質問に足りないもの(半径、災害種別など)があれば聞き返す
   ├─ ③ 3 つのツールから選び、空欄(引数)を埋めて呼ぶ
   ↓
ES|QL ツール(Elasticsearch が距離・種別・意味を計算)
   ↓
エージェント
   ├─ ④ 結果を選び、並べ、表にする
   └─ ⑤ 注意書きを添えて回答する

距離と登録値の照合はElasticsearchが行います。LLMは質問を解釈してツールと引数を選び、返った説明文を読んで回答を組み立てます。つまり、LLMにも判断する仕事があります。

決めること担当理由
現在地からの距離、半径の中か外かElasticsearch(ST_DISTANCE、WHERE)指定した座標と条件に基づいて計算する
災害種別の一致Elasticsearch(MV_CONTAINS)登録済みの種別と、渡された値が一致するか確認する
碑文の意味が質問に近いか(候補出し)Elasticsearch(semantic_text + MATCH)2,469 件から候補を選ぶのは検索エンジンの仕事
候補のうち質問に合いそうな碑、次に使う災害種別LLM質問と説明文、登録された種別を読んで選ぶ。判断が常に正しいとは限らない
質問の理解、聞き返し、回答文LLM自然な日本語のやり取り
指定のない半径や災害種別LLMに推測させない必要な値が無ければ聞き返す。ただし、碑に複数の登録種別がある場合は、LLMが質問と合う登録値を選ぶ

3 つのツール

第1回で紹介した3つの検索ツールは、筆者がES|QLで用意した検索文を実行します。?latitude のように ? で始まる名前は、実行時に値を入れる場所(引数)です。LLMはツールと引数を選びますが、保存済みの検索文は書き換えません。

番号は第 1 回と同じです。

番号ツール何を探す必須の空欄任意の空欄並び順返す件数
ツール 1geoai.search_disaster_lore_by_meaning伝承内容が質問に近い伝承碑緯度、経度、半径、検索文災害種別意味の近さ順(候補)最大10
ツール 2geoai.search_emergency_shelters指定した災害に対応する避難場所緯度、経度、災害種別半径近い順最大10
ツール 3geoai.search_disaster_lore伝承碑緯度、経度半径近い順最大10

このほかに、エージェントは attachments.read というツールも使います。会話に添付された現在地を読むためのツールで、Agent Builder に最初から入っています。筆者が書いたツールではないので、この 3 つには数えていません。

説明は、一番簡単なツール 3 から始めます。

3 つに共通する型(ツール 3 で見る):現在地を点にして、距離を測り、半径で絞る

ツール3(伝承碑を近い順に探す)のES|QL本文です。Agent Builderで使うには、このほかに引数の型と既定値の設定が必要です。

FROM geoai-disaster-lore-v1
 | EVAL search_point = TO_GEOPOINT(CONCAT("POINT(", TO_STRING(?longitude), " ", TO_STRING(?latitude), ")"))
 | EVAL distance_m = ST_DISTANCE(location, search_point)
 | WHERE (?radius_km <= 0 OR distance_m <= ?radius_km * 1000)
 | EVAL distance_km = ROUND(distance_m / 1000.0, 3)
 | SORT distance_m ASC
 | EVAL monument_lat = ROUND(ST_Y(location), 5), monument_lon = ROUND(ST_X(location), 5)
 | KEEP name, address, disaster_types, disaster_name, description, distance_km, monument_lat, monument_lon
 | LIMIT 10
行何をしているか
FROM geoai-disaster-lore-v1伝承碑の表から始める
EVAL search_point = TO_GEOPOINT(...)EVAL は「新しい列を作る」命令。空欄の ?longitude ?latitude(現在地)を、地図上の 1 点に変える
EVAL distance_m = ST_DISTANCE(...)碑の位置(location 列、geo_point 型)と現在地の距離をメートルで出す。地球の丸みを考えた直線距離
WHERE (?radius_km <= 0 OR ...)半径の外を捨てる。半径の空欄が省略されたとき(0)は絞らない
EVAL distance_km = ROUND(...)メートルを km に直す
SORT distance_m ASC近い順に並べる
EVAL monument_lat = ...碑の緯度・経度も返します。ただし、このPoCでは検索の中心を利用者の現在地に限定しており、碑の座標を次の検索の中心には使いません。
KEEP ... / LIMIT 10返す列を選び、10 件に絞る

半径を省略できるようにしたのは、「一番近い碑は?」のような質問のためです。一番近いものを答えるのに、半径は要りません。

Agent Builderのツール作成画面

ツール 2(避難場所):災害種別で厳密に絞る

距離を計算する流れに、災害種別の一致条件を加えます。次はツール2の条件部分です。

| WHERE MV_CONTAINS(disaster_types, ?disaster_type)
    AND (?radius_km <= 0 OR distance_m <= ?radius_km * 1000)

避難場所のデータには、1 施設に「内水氾濫、土砂災害、洪水」のように複数の災害種別が付いています。Elasticsearch では 1 つの列に複数の値を入れられ、これをマルチバリューと呼びます。MV_CONTAINS は「その列に、この値とまったく同じものが入っているか」を見る関数です。

災害種別の引数は必須です。避難場所だけを求める質問で、どの災害に対応する場所か分からなければ聞き返します。第1回の連鎖検索では、先に見つけた碑に登録された災害種別から、質問に合う値をLLMが選んで渡しました。条件が一致するかの判定はElasticsearchが行います。

Agent Builderのツール作成画面

ツール 1(意味検索):伝承内容が近い碑を「候補」として出す

FROM geoai-disaster-lore-v1 METADATA _score
 | WHERE MATCH(description_semantic, ?query)
    AND (?disaster_type IS NULL OR ?disaster_type == "" OR MV_CONTAINS(disaster_types, ?disaster_type))
 | EVAL search_point = TO_GEOPOINT(CONCAT("POINT(", TO_STRING(?longitude), " ", TO_STRING(?latitude), ")"))
 | EVAL distance_m = ST_DISTANCE(location, search_point)
 | WHERE distance_m <= ?radius_km * 1000
 | EVAL distance_km = ROUND(distance_m / 1000.0, 3)
 | EVAL relevance = ROUND(_score, 3)
 | SORT _score DESC
 | EVAL monument_lat = ROUND(ST_Y(location), 5), monument_lon = ROUND(ST_X(location), 5)
 | KEEP name, address, disaster_types, disaster_name, description, distance_km, monument_lat, monument_lon, relevance
 | LIMIT 10

ツール1のES|QL本文です。Agent Builderの引数設定は別に必要です。ツール3との主な違いは次の4か所です。

  • METADATA _score:検索の「点数」を列として使えるようにする。_score は「どれくらい一致したか」を表す数字です。
  • WHERE MATCH(description_semantic, ?query):これが意味検索です。?query には利用者の言葉をなるべくそのまま入れます。
  • AND (?disaster_type IS NULL OR ?disaster_type == "" OR ...):災害種別を指定したときだけ、その登録値で絞ります。省略時は災害種別で絞りません。
  • SORT _score DESC:意味が近い順に、最大 10 件の候補を返します。

このツールで0行だった場合でも、「地域内に該当する碑が登録されていない」とは断定できません。意味検索を使わず、災害種別と距離だけで数えた結果とは区別します。この違いは第1回の5-6で扱いました。

また、距離順に並べるのは、意味検索で選ばれた最大10件の中だけです。半径内の全碑から「質問に合うものを距離順で10件」探す処理ではありません。候補から漏れた碑は、LLMも評価できません。

つまずきやすい所:任意の空欄には既定値が要る

Agent Builder の ES|QL ツールでは、空欄に「任意」の印を付けられます。任意の空欄に既定値(省略されたときに入る値)が要るかどうかは、ツールの作り方で違います。

  • 公式資料の説明:Kibana の画面(UI)で任意の空欄を作るときは、既定値が必須です。API で作るときは既定値がなくてもよく、省略された空欄は null(値なし)になります。ただし null はクエリの失敗につながりやすいので、既定値を付けることが強く勧められています。
  • 今回の実測:今回のツール設定では、既定値を与えずに空欄を省略すると、実行時に Unknown query parameter [disaster_type](知らない引数)というエラーになりました。

そこで、任意の文字引数には空文字 ""、任意の数値引数には 0 を既定値にしました。?disaster_type が空なら種別で絞らず、ツール2・3では ?radius_km が0なら半径で絞りません。ツール1では半径は必須です。設定の詳細はElasticのES|QLツールの資料にもあります。

意味検索(semantic)をどこで、どう使っているか

データを入れる側:説明文を 2 つの形で保存する

伝承碑の説明文は、1 つのインデックスの中に 2 つのフィールドとして入っています。

フィールド型何のため
descriptiontext(日本語の形態素解析 kuromoji)言葉で探す。「堤防」という単語が入っている碑
description_semanticsemantic_text意味で探す。「堤防が壊れた記録」に近い内容の碑

形態素解析は、日本語の文章を単語に切る処理です。日本語は単語の間に空白がないので、「堤防が壊れた」を「堤防 / が / 壊れ / た」と切る必要があります。kuromoji はそのための道具です。

semantic_text は、文章を検索用の数値表現(ベクトル)に変換して扱うフィールド型です。今回のインデックスではElastic Inference Serviceの .jina-embeddings-v5-text-small を指定しました。日本語を含む多言語に対応し、既定では1,024次元のベクトルを作ります。モデルを自分のサーバーに配置せずに使える構成です。詳しくはElasticの設定例を参照してください。

検索する側:MATCH で「意味が近い順」に候補を出す

MATCH(description_semantic, "川の堤防が切れて町が水につかった記録") と書くと、Elasticsearch は質問文も同じモデルでベクトルにして、意味が近い碑を探します。

筆者が川口市内から聞いた例です(距離は筆者の現在地からの値を、1km 単位に丸めています)。

碑説明文の一部距離
瓦曽根溜井(かわらぞねためい)防水記念碑(越谷市)明治 23 年、行田市で利根川の堤防が決壊し、越谷市域は大洪水に見舞われた約 8km
倉松落大口逆除之碑(春日部市)明治 23 年、利根川の堤防決壊と古利根川の氾濫の記録約 17km
水害伝承碑(川島町)輪中の里、川島町は度重なる大水害に見舞われた約 29km

「堤防が切れた」「町が水につかった」という日常の言い方から、「決壊」「氾濫」と書かれた碑を探せました。言葉が違っても関連する候補を見つけられるのが意味検索の利点です。

意味検索が「しないこと」

ここでいう意味検索はElasticsearchが質問文と碑の説明文を比べ、内容が近いものを候補として返す処理です。そのあとに、LLMが候補の説明文を読んで、質問に合うかを判断します。

例えば「川の堤防が切れて町が水につかった記録」を探したとき、半径10km内に登録されている碑は6件でした。この6件が意味検索の候補に入り、そのうち4件は災害種別が「地震」の碑でした。今回は災害種別で絞っていないため、地震の碑も候補から除かれません。候補に出たことと、質問に合うことは別です。

エージェントは、意味検索で返った最大10件をLLMに読ませて、回答に使う碑を選びます。その10件に合う碑がなくても、地域内のすべての碑に該当するものがないとは言えません。「地域内の登録が0件か」を確かめたいときは、意味検索とは別に、災害種別と距離の条件で登録件数を数えます。

候補は意味の点数順、回答は距離順

relevance は、Elasticsearchの検索スコア _score を小数点以下3桁に丸めて返す値です。質問文と碑の説明文がどれくらい似ているかを表す検索の点数です。ツール1では、Elasticsearchの _score をこの名前で返しています。使う場所は、検索と回答で違います。

段階何をするか
候補を探すSORT _score DESC で意味が近い順に並べ、最大10件をツールから返す。ここではrelevanceを使う
回答を作るLLMが候補の説明文を読んで質問に合うか判断し、合うと判断した碑を現在地から近い順に表示する。ここではrelevanceで並べない

近くの碑を知りたい質問なので、回答では距離を優先しました。ただし、距離順に並べられるのは、意味検索が返した最大10件だけです。候補に入らなかった碑が、後から距離順で追加されるわけではありません。また、relevanceは正答率や安全性を表す点数ではなく、一定以上なら必ず質問に合うという基準にも使っていません。

エージェントのルール(指示文)

エージェントの動き方のルールは「指示文」に書きます。これはエージェントを作るときに設定する文章で、LLMはツールの説明と一緒に読みます。実際の動きが常に指示どおりになるとは限らないため、検索結果も確認します。

現在地の扱い

  • 現在地は、会話の非表示の添付 geoai-current-location からだけ取る。
  • 添付に緯度、経度、取得時刻、有効期限がそろっていて、期限内のときだけ使う。
  • 2 回目以降の質問でも、検索の前に毎回添付を読み直して期限を確かめる。期限切れなら検索しない。
  • 利用者に緯度・経度の入力を求めない。緯度・経度を回答に表示しない。

ここまで書くのは、LLM が親切だからです。放っておくと「緯度と経度を教えてください」と聞いたり、前の質問で読んだ座標を期限切れでも使い続けたりします。それを止めるルールです。

聞き返しのルール

検索必須足りないとき
避難場所災害種別。半径は「範囲を求める質問」のときだけ種別が無ければ検索せず、聞き返す
伝承碑(近い順)半径は「範囲を求める質問」のときだけ範囲を求めているのに半径が無ければ聞き返す
伝承碑(意味)半径と検索文検索せず、聞き返す

基本は「半径を推測しない。既定値で補わない」です。ただし「一番近い」「最寄り」のような質問には、半径そのものが要りません。そのときは聞き返さず、半径で絞らずに近い順で検索し、「半径の指定なしで、現在地から近い順に返しました」と一言添えます。

たとえば、筆者が川口市内から「最も近い自然災害伝承碑を教えて」と聞くと、聞き返しなしで「八丁の水神社再建記念碑」(地震、約 4km)が返りました(14 秒)。「推測しない」と「不要なものを聞かない」は両立できます。

ツールの選び方

質問に「碑の内容・教訓・テーマ」の指定がある?
├─ ある → ツール 1(意味検索)
│         災害種別がはっきり言われていれば、種別の空欄にも入れる
└─ ない → 「一番近い」「周辺の一覧」→ ツール 3(近い順)

避難場所の質問 → ツール 2(避難場所)

碑と避難場所の両方を求められた → ツール 1 か 3 → ツール 2(第 1 回の「属性の引き継ぎ」)

ツール 1 に渡す検索文は、利用者の言葉をなるべくそのまま使います。「川の堤防が切れて町が水につかった記録」を「洪水」に置き換えてはいけない、と書いてあります。「洪水」という一語に置き換えても意味検索ですが、「堤防が切れた」「町が水につかった」という具体的な条件が失われ、探したい内容が広がってしまうからです。

結果の見せ方

ツール 1 の結果について、LLM に 3 つの仕事をさせています。

  1. 選ぶ。 最大10件の候補の説明文を質問と照らし、「内容が一致する碑」と「参考」に分ける。一致が0件なら「今回返った候補には一致する碑がない」と言う。地域内の全碑について0件とは言わない。
  2. 並べる。 どちらも現在地から近い順。relevance では並べない。
  3. 見せる。 一致する碑は表に、参考の碑は 1 行ずつ。

説明文の要約にもルールがあります。説明文にある事実だけを短くし、数字・年・地名は原文のままにします。一致した1件目は原文を短く引用して、LLMが「それらしい教訓」を付け足していないか読者が確かめられるようにします。

安全についての言い方と、やらないこと

  • この PoC を安全判定システムとして扱わない。
  • 距離は道路距離でも徒歩距離でもなく、直線距離だと書く。
  • 避難場所の開設状況、収容の余力、経路の安全は分からないと書く。
  • 伝承碑データは全国の災害を網羅していない。0 件でも「災害がなかった」「今は安全」と言わない。
  • 住所から座標への変換、登山情報、現在の危険度や避難が必要かどうかの判定はしない。

たとえば「神戸の市役所に一番近い碑を教えて」と聞くと、エージェントは「任意の住所を中心にした検索はできません」と断ります。これは、指示文でわざとそうしている動きです(理由は第 1 回の 1 節)。

現在地をどう渡しているか

ここからは、検索の中心になる現在地の話です。作り方の細かい話は省き、設計の考え方だけを書きます。

なぜ Chrome 拡張なのか

Kibana の Agent Builder のチャット画面には、「現在地を使う」ボタンがありません。位置を渡す方法は、大きく 3 つ考えられます。

方法このPoCでの判断
利用者が緯度・経度を打ち込む現実的ではない。打ち間違えると、別の場所を検索してしまう
住所を打ち込み、座標に変えるジオコーディングは今回入れていない(第 1 回の 1 節)
ブラウザに現在地を聞く利用者が許可したときだけ取得できるため、今回採用した

小さなChrome拡張を作り、利用者がボタンを押したときだけ、ブラウザのGeolocation APIで現在地を取得しました。拡張はKibanaの画面を読み書きせず、独立した小さな窓で動きます。

拡張機能の画面

流れ

[利用者] ボタンを押す
   ↓
[Chrome 拡張] ブラウザから緯度・経度を取る
   ↓ 緯度・経度・取得時刻の 3 つだけ送る
[自分のPCで動くローカルサーバー](ElasticのAPIキーを保持)
   ├─ Agent Builder APIで新しい会話を作り、現在地を非表示で添付する
   ├─ 地図用の一時インデックスに現在地を1件書く
   ├─ 7分後に会話と地図用の現在地の削除を試みる(サーバー稼働中のみ)
   └─ 会話のURLを拡張へ返す
[Chrome 拡張] 新しいタブでその会話を開く
   ↓
[利用者] 日本語で質問する → エージェントが添付から現在地を読む

ポイントは 2 つです。

  • APIキーを拡張に持たせない。 ブラウザで動く拡張にキーを入れず、ローカルサーバーだけが保持します。
  • 現在地を添付した新しい会話を開く。 拡張は既存のKibana画面を書き換えません。新しい会話を作って位置を添付し、そのURLを開きます。

現在地は保存されるのか

現在地は保存されます。保存先ごとの扱いを分けて書きます。

場所現在地は入るか
Agent Builderの会話(非表示の添付とツール呼び出しの記録)入る。7分後、サーバーが動いていれば会話の削除を試みる
地図用の一時インデックス map-session-current-location-v1現在地1件を保存する。会話と同じタイマーで削除を試みる
伝承碑・避難場所のインデックス(geoai-*)入らない
Agent Builder の動作記録(トレース)入らないとは言えない(下で説明)
自分のPCのサーバー今回の実装では位置をファイルやログへ書かない。削除タイマーはメモリ上にある

トレースについて確かめたのは、データストリーム traces-agent_builder.otel-default を、今回の緯度の値で検索して0件だったことだけです。これは「位置が記録されない」証明にはなりません。

一方で、Kibana のチャット画面では、トレースのスパン(1 つ 1 つの処理)の詳細に、その処理の入力と出力が表示されることがあります。ツールの空欄に入れた緯度・経度や、attachments.read が読んだ添付の中身が、スパンの詳細に記録されるかどうかは、確かめていません。記録の設定によって変わる可能性もあります。そのため、この記事では「トレースに位置は残らない」とは言いません。本番で使うなら、ツールの引数、attachments.read の入力と出力、トレースの収集設定の 3 つを確認する必要があります。

また、エージェントが位置を読んで引数を選ぶ過程では、LLMにも位置情報が渡ります。このPoCは「現在地がElasticのどこにも残らない」という設計ではありません。

7 分で何が消えるのか

「7 分」は、位置を取得した時刻から数えます。

何がどうなるか
添付の有効期限期限を過ぎた現在地は検索に使わず、「有効な現在地を取得できません」と答えるよう、エージェントに指示しています
会話そのものサーバー稼働中は7分後に削除APIを呼ぶ。成功すれば会話の添付と記録も削除される
地図用の現在地会話とは別の一時インデックスにある。サーバーが削除を試み、地図側でも期限切れを表示しない条件を使う

1 行目は「使わせない」ための期限で、2 行目と 3 行目が「実際に消す」処理です。この 2 つは別のものです。期限が切れても、それだけで会話が消えるわけではありません。実際に消えるのは、タイマーが動き、削除 API の呼び出しが成功したときです。

なぜ7分なのか。位置が古くなっても今の場所として検索しないよう、PoCで決めた時間です。1回の質問と短い聞き直しを想定しています。7分で自動的に完全消去されるという意味ではありません。

地図に現在地を描くには

このPoCのKibana Mapsでは、現在地・碑・避難場所を3つのレイヤーで重ねます。碑と避難場所は既存のインデックスから読み込みます。現在地の点を描くために、地図用の一時インデックス(map-session-current-location-v1)を別に用意しました。伝承碑や避難場所のデータには混ぜません。

  • 中身は現在地 1 件だけ。新しい会話を作るとき、古いものを先に消す。
  • 会話の削除と同じタイマーで、削除を試みる。サーバーを起動したときにも、期限切れを掃除する。
  • 地図のレイヤーにも「有効期限が今より後のものだけ描く」という条件を入れています。削除に失敗しても、地図を再検索・更新した際には、期限切れの位置を表示対象から除外します。

この地図はチャットの回答と自動連動しません。碑と避難場所のレイヤーは、それぞれのインデックスから点を読み込みます。災害種別で表示を絞る場合は、Kibana Mapsの検索欄で筆者が手動で指定します。表示される点は、ツールが返した最大10件とは別です。

地図上の伝承碑と避難場所の点は、国土地理院の各データを加工して筆者が表示したものです(出典は末尾に記載)。

PoC の限界

削除のタイマーはサーバーのメモリにあります。7分前にサーバーを止めたり、削除APIが失敗したりすると、会話が残ります。その場合はAgent Builderの画面かAPIで手動削除が必要です。地図用の現在地は、次にサーバーを起動したときに期限切れを掃除します。ただし、サーバーが止まっている間は削除されません。本番では削除の再試行と成功確認が必要です。

時間はどこで使われているか

Kibana のトレース(処理の内訳を時間の流れで見る画面)で、第 1 回の連鎖検索 1 問(堤防決壊の碑 → 洪水の避難場所)を見ました。トレースには 6 つのスパン(処理のまとまり)が表示され、トレース全体の長さは 32.0 秒でした。

下の表は、画面に表示された順番のままです。時間も、画面に出た値をそのまま書いています。

順番画面の表示中身時間LLM への入力 / 出力トークン
1LLM(chat)LLM の呼び出し3.2 秒23,953 / 88
2TOOL(attachments.read)現在地の添付を読む1 ミリ秒未満―
3LLM(chat)LLM の呼び出し2.4 秒24,304 / 191
4TOOL(custom)ツール 1(碑の意味検索)84 ミリ秒―
5LLM(chat)LLM の呼び出し25.6 秒28,454 / 2,165
6TOOL(custom)ツール 2(避難場所)画面上で読み取れない―

なお、表の時間を足しても、32.0 秒にはなりません。32.0 秒はトレースの始まりから終わりまでの長さで、各スパンの時間の合計ではありません。スパンどうしが重なったり、間があいたりすることもあります。

この画面からは、分からないこともありました。

  • 5 番目の LLM のあとに、6 番目の避難場所ツールが並んでいます。回答の文章をどの LLM 呼び出しが書いたのかは、この画面からは分かりません。
  • 同じ質問で、チャット画面に表示された回答時間は 51 秒でした。トレースの 32.0 秒との差がどこから来るのかは、確かめていません。

分かったのは、観測できたスパンの中ではLLMのスパンが最長で、ツール1のスパンは84ミリ秒だったことです。この84ミリ秒をElasticsearch単体の実行時間とはみなしません。回答を書いた処理や、画面の51秒との差は見えていないため、待ち時間全体の内訳とは言えません。

観測した3回のLLM呼び出しでは、入力が約2.4万~2.8万トークンでした。入力の内訳を調べていないため、どの文章が時間や費用にどれだけ効いたかは分かりません。次に改善するなら、入力の内訳とツール実行後のスパンも確認します。

限界と、今後やること

項目今回確認した限界次に確かめること
回答の待ち時間連鎖検索のチャット表示は51~69秒。観測したスパンではLLMが最長だが、待ち時間全体の内訳は不明トレースに出ていない処理、画面表示時間との差、入力の内訳を確認する
候補の漏れとLLMの選別上位10件以外は読めず、抽象的な質問では一致判定もぶれる検索評価を増やし、候補数や判断基準を比較する
災害種別の言い換えLLMが指示文の対応表を使って登録済みの種別を選ぶ言い換え表をデータとして管理する方法を検討する
現在地の削除期限切れの位置を検索に使わないよう指示しているが、サーバー停止時には会話が残り得る再試行できる削除処理と成功確認を設ける

まとめ

このエージェントでは、Elasticsearchが距離と登録種別を照合し、意味が近い碑を候補として返します。LLMは質問を解釈してツールと値を選び、候補の説明文を読んで回答します。

  • ES|QLのツールは3つ。距離や登録種別の条件は、保存済みの検索文に書きました。
  • semantic_text と MATCH は、質問に関連する碑の候補を選ぶために使います。回答の距離順は、選ばれた候補の中での順番です。
  • 災害種別が必要なときは、登録済みの値を選びます。質問に必要な半径がなければ聞き返します。
  • 7分を過ぎた位置を検索に使わないよう指示し、サーバー稼働中に削除を試みます。

地理情報をAIにつなぐと、自然な質問から場所に結び付いたデータを探せます。その答えを評価するには、検索条件、候補の範囲、LLMの判断、処理の記録をそれぞれ見えるようにする必要があります。このPoCは、その作り方と残る課題を示しました。

出典・データの加工

本記事の検索結果と地図は、上記データをもとに筆者が災害種別の整理、Elasticsearchへの登録、検索、可視化を行って作成しました。国土地理院が本記事の検索結果や地図を作成したものではありません。

指定緊急避難場所は災害種別ごとに指定されます。公開データには更新前の情報や未掲載の施設がある場合があります。実際の避難には自治体などの最新情報を確認してください。

技術資料