LLMに渡せない大規模データから顧客層を抽出する ── コードインタープリターによる照合と集計

x facebook hatena

はじめに

こんにちは、ソリューションエンジニアの田中です。私が所属するチームでは、AIに時事情報を調査させて、自社への影響について示唆を出してもらうアプリの開発を行っています。

このアプリには、調査した時事情報から「その時事情報が自社のどの顧客層に影響を与えるのか」を推測する工程があります(アプリの全体像は以前の記事で紹介しています)。

LLMに何かを判定させるとき、判断材料になる参照データを一緒に渡すのが基本です。商品マスタ、契約条項、規程集、顧客データ。そして参照データは、網羅性を求めた時点でプロンプトに収まらなくなります。

顧客層を推測するこの工程でも、材料になるのは顧客データの集計表でした。年齢帯や利用量帯といった軸を掛け合わせ、組み合わせごとの人数を1行ずつ並べた表です。本記事では、この表を集計マスタと呼びます。この集計マスタは1万行を超えていて、プロンプトに入りませんでした。突破口になったのはコードインタープリターで、集計マスタをそのままプロンプトに入れるのをやめて、集計そのものを任せることにしました。

第一章 語感で選ばれる顧客層

最初は、参照データの大きさが問題だとは思っていませんでした。出力の質が悪いのだと考えていたからです。まず、何が起きていたかを見てください。ダミーの時事情報とダミーのデータで再現したものです。

[ダミー]競合D社、新規契約と同時加入で人気の外部サービスを1年間無料に

(概要)新規契約または他社からの切り替えと同時に指定のオプションへ加入した場合、提携する外部サービスを1年間無料で利用できる。対象となる外部サービスは、若年層を中心に利用が広がっているものだという。料金プランそのものの変更はない。

出力です。

中高年の長期利用者。小容量プランでコストを抑える節約志向の層(24,300人) 影響がありそうな理由: 外部サービスが1年間無料になる特典は月々の負担軽減に直結するため、コストを抑える節約志向のこの層では競合への乗り換え意向が高まる可能性があります。

30〜50代の中期利用者。大きめのデータ量で動画やSNSを日常的に使いこなす層(31,200人) 影響がありそうな理由: 人気の外部サービスが無料で使えるようになるため、動画やSNSを日常的に活用するこの層の関心と重なり、特典が乗り換えの動機になる可能性があります。

若年の新規・短期利用者。契約からの年数が浅く、今後の利用拡大が見込まれる層(9,800人) 影響がありそうな理由: 対象の外部サービスは若年層を中心に利用が広がっていると明記されており、この層の乗り換え検討につながる可能性があります。

3つ選ばれていますが、時事情報が語っているのは競合の獲得施策です。「1年間無料」になるのは提携する外部サービスであって、料金プランそのものの変更はないと本文に明記されています。それでも出力は「無料」という言葉に反応して、「コストを抑える節約志向の層」を選びました。名称の頭には「中高年」とあり、若年層向けとされる施策と噛み合っていません。「人気の外部サービス」という一節からは動画・SNSの層へ、「若年層」からは新規・短期の若年層へと話が広がっています。名称に書かれた年齢やデータ量の属性は素通りされ、尻尾の「節約志向」「日常的に使いこなす」といった言葉との語感の近さで選ばれている。悩まされていた過剰選定の正体はこれでした。

やっかいなのは、この出力がルール違反ではないことです。当時のプロンプトには「層の名称に明示された属性だけを使え」と書いてありました。「節約志向」も「動画やSNS」も、確かに層の名称に書かれた属性です。そして時事情報の側にも「無料」や「若年層に人気」という、それらしい言葉があります。属性が自然文である以上、「明示されている」かどうかを判断するのはLLM自身で、結局、語感が近いかどうかで決まります。規定に正確に従って、噛み合わない答えが出ているわけです。

原因は照合先にありました

このとき渡していた照合先(時事情報と突き合わせるデータ)は、業務担当者が人手で条件を設計して抽出した、顧客層の定義でした「小容量プランでコストを抑える節約志向の層」のように自然文で記述されていて、渡していたのは層の名称と人数のペアだけです。

顧客層の名称 人数
若年の新規・短期利用者。契約からの年数が浅く、今後の利用拡大が見込まれる層 9,800
中高年の長期利用者。小容量プランでコストを抑える節約志向の層 24,300
30〜50代の中期利用者。大きめのデータ量で動画やSNSを日常的に使いこなす層 31,200

この語感任せは、プロンプト側で禁止事項を書き足しても止まりません。推測で属性を補うなと書いても、その属性は名称の中に最初から書かれています。推測しなくても、すでにそこにあるわけです。

やるべきことは見えていました。禁止を増やすのをやめて、照合先そのものを語感で読めないものに変えればいい。具体的には、「はじめに」で触れた集計マスタです。この形にすれば、「節約志向の層」のような概念はそもそも書けません。禁止する必要がなくなります。

第二章 差し替えたい照合先が、渡せない

ところが、この集計マスタをLLMに渡せませんでした。

軸の組み合わせを機械的に展開しているので、行数が一気に増えます。コンテキストに丸ごと入りません。仮に入ったとしても、長大な表からの目視探索は取りこぼしとハルシネーションの温床です。以前の記事で紹介した Attention Dilution[1] や Lost in the Middle[2] と同じ構造の問題が、今度は表の照合で起きます。

表を小さくして渡す方向も考えられます。要約する、代表的な行だけ抜き出す。ただ、どれも抜き出す時点で誰かが基準を決めることになります。その基準を決められないからこそ、網羅的に集計した表を使いたかったわけで、本末転倒です。

RAGではなぜ解けないのか

大きな参照データを扱う定番の手段はRAGです。ただ、今回の照合には向きません。

RAGは「関連しそうな箇所を引いてきて答えさせる」仕組みです。問いに答えるための材料を集めるのが目的で、引いてこなかった部分は答えに反映されません。

いっぽう今回やりたいのは、条件に合う行を漏れなく特定して、数えることです。似た行を引いてくるだけでは足りません。引かれなかった行が何件あったのかが分からなければ、規模を答えられないからです。「近いものを見つける」タスクと「該当を数え上げる」タスクは、必要なものが違います。

同じことが、集計を伴う照合全般に言えます。該当件数、合計金額、対象範囲。答えが「どれか」ではなく「どれだけか」になる瞬間に、一部を引く方式は使えなくなります。

💡 補足知識

  • Attention Dilution
    入力情報が多くなると、「最も関連性の高い部分に集中する能力」が低下し、本来注目すべき部分への重みが軽減されてしまうこと。
  • Lost in the Middle
    重要な情報が文章の最初や最後にある場合は抽出できても、大量のテキストの真ん中に埋もれていると、AIがその情報をうまく利用できず、パフォーマンスが著しく低下すること。

第三章 渡さずに、計算させる

行き詰まりを解決したのが、コードインタープリター(LLMがコードを生成・実行する仕組み)でした。

発想を変えます。集計マスタをLLMに読ませる必要はありません。LLMには、時事情報から条件を抽出することと、計算結果を解釈することだけを担わせる。集計マスタそのものはコード実行の側に置き、条件に合う行を集計させる。渡せないなら、渡さずに計算させればいい、というだけの話です。

▼図1:判定と計算の分担

fig2_agent

ひとつのエージェントの内部で、意味を解釈するLLMの判断と、正確に数えるコード実行が同居しています。境界を引いているのはプロンプトの手順書です。

実装上のポイントは、この照合をアプリ側が制御していないことです。判定から集計までの全手順を1つのプロンプトに畳み込み、コードインタープリターを備えたエージェントに単発で渡して、内部で完結させています。アプリが受け取るのは、構造化された最終結果だけです。

この形にしたことで、保守も軽くなりました。切り替える前はアプリ側に顧客層の定義と照合処理を持たせていたため、顧客層の定義を更新するたびにアプリ側のロジックも直す必要がありました。いまは手順書を列名に依存しない形で書き、データに依存する情報は後述の集計定義書とドメイン知識に集めています。集計マスタが差し替わってもアプリ側のコードは触りません。

第四章 委譲するとは、判断の余地を埋めること

委譲すると決めた瞬間から、別の作業が始まりました。エージェントの中で行われることを、こちらが決めておかなければなりません。丸投げすると、今度はコードの書き方に語感が入り込みます。

決める必要があったのは、大きく3つでした。

  1. 入力の言葉を集計マスタの値にどう翻訳するか
  2. 作った候補のうちどれを採用するか
  3. 数え方をどう定めるか

照合と集計をLLMに委ねる場合、この3つはどの分野でも避けて通れないと思います。

手順書としてプロンプトに定義した流れは、次のとおりです。

  1. 実データの列の確認。集計マスタの実際の列を読み取り、集計定義書の項目名と突き合わせる。列名を決め打ちにしない
  2. 影響条件の抽出。調査で得られた情報から、対象となりうる顧客層の条件を抽出する
  3. 実在する軸への対応づけ。「集計マスタに存在しない軸・区分・行を新設してはいけない」という制約が常に優先され、対応づけられない条件は使わない
  4. 条件を組み合わせて候補となる顧客層をつくる
  5. 採用基準による絞り込み
  6. 人数の集計。必ずコードインタープリターの実行によって計算し、印象や単一行の値をそのまま人数として書いてはいけない

▼図2:手順書の流れと、LLMの判断/コード実行の分担

fig2_steps_2

以下、手順の中で特に決めることが多かった3点を書きます。

時事情報の言葉を、軸に翻訳する

エージェントに渡しているのは、集計マスタ本体だけではありません。手順3の対応づけを支えるために、集計定義書(使ってよい軸と区分の定義)とドメイン知識(時事情報に出てくる言葉を、どの軸のどの区分に対応づけるかの辞書)を添えています。時事情報の言葉は業務の区分と一致しないため、この2つがないと対応づけが語感任せに戻ってしまいます。

第一章で挙げた「コストを抑える節約志向の層」のような表現もここで扱います。こうした言葉は軸として定義できないので、そのまま条件に使うことを禁止し、実在する軸への言い換えが辞書にある場合だけ使えるようにしました。たとえば「関連サービスをどれも使っていない層」なら、該当する加入軸がすべて非加入の行、と決めてあります。語感を、数えられる条件に置き換える作業です。

辞書は語彙を増やす道具ではなく、閉じる道具として使っています。ここに書かれていない対応づけは使えません。

どの候補を出力するか決める

条件の組み合わせは膨大なので、そのうちどれを出力するのかという基準が別に必要です。基準は次の3つです。

  1. 条件が軸とどれくらい合っているかを、1つずつ判定する
    1. 条件ごとに「合っている」「近い」「合っていない」「判断できない」を付けます。まとめて「全体として近そう」で済ませることは禁止しています。
  2. 根拠の弱い条件だけでは、顧客層を作らない
    1. 時事情報の条件が軸にそのまま対応するとは限りません。辞書の言い換えを挟んで補った場合や、年齢のように区分が粗い場合は、根拠が弱いものとして扱います。軸に直接対応する条件が最低1つないと、顧客層は立てられません。
  3. 点数をつけ、似た候補は代表だけ残す
    1. 各候補を内部で0〜5点に評価し、一定の点数に届いたものだけを採用します。さらに、説明できる中身が同じで、別の候補のほうが多くの条件に合っている場合は、合っている条件が少ないほうを落とします点数は内部の絞り込みにだけ使い、出力には出しません。

      この基準にはもうひとつの役目があります。基準を満たす候補が1件も作れなかった場合に、「影響なし」に戻さないと決めていることです。影響は認める、しかし根拠の強さが足りないので数えない。「影響あり(集計不可)」という分岐は、ここで発生します。

人数の数え方を決める

集計マスタの1行は軸の組み合わせごとの集計セルで、それ自体は顧客層ではありません。顧客層は条件の組み合わせで、人数は「条件に使った軸だけで行を絞り、使っていない軸は全部足す」で求めます。混同すると、たまたま条件に近い1行の人数が、そのまま顧客層の規模として出てしまいます。

▼図3:集計マスタの行と顧客層の関係fig3_master_2

複数の顧客層を出すときは、人数を足し算してはいけません。別の軸で切った顧客層は同じ行を含みうるので、単純に足すと同じ人を二重に数えます。重複を除いてから合計する、という規定を置いています。

こうした数字の生成は、すべてコード実行に閉じ込めています。LLMは巨大な表を"目視"すると、それらしい人数を作ってしまうためです。

 
# エージェント内部で生成・実行される照合コードのイメージ
# 条件に使った軸だけで絞り込み、残りの軸は合算する
cells = master[
    (master["利用量帯"] == "上位帯")
    & (master["世帯内契約数"] == "2件以上")
]
print(cells["集計人数"].sum())  # 人数は必ず実行結果から
    

第五章 変わったことと、残っている課題

第一章と同じ時事情報を、切り替えた後に入れた出力です。実際の出力は構造化された形式ですが、読みやすさのため整形して掲載しています。まず、切り替え前(第一章)の出力を再掲します。

中高年の長期利用者。小容量プランでコストを抑える節約志向の層(24,300人) 影響がありそうな理由: 外部サービスが1年間無料になる特典は月々の負担軽減に直結するため、コストを抑える節約志向のこの層では競合への乗り換え意向が高まる可能性があります。

30〜50代の中期利用者。大きめのデータ量で動画やSNSを日常的に使いこなす層(31,200人) 影響がありそうな理由: 人気の外部サービスが無料で使えるようになるため、動画やSNSを日常的に活用するこの層の関心と重なり、特典が乗り換えの動機になる可能性があります。

若年の新規・短期利用者。契約からの年数が浅く、今後の利用拡大が見込まれる層(9,800人) 影響がありそうな理由: 対象の外部サービスは若年層を中心に利用が広がっていると明記されており、この層の乗り換え検討につながる可能性があります。

続いて、切り替え後の出力です。

判定:影響あり(集計不可)

影響は認められます。本文は、競合による新規契約・他社からの切り替え時の特典という顧客獲得施策に当たり、自社からの流出圧力が直接かかる内容です。ただし、「1年間無料」は提携する外部サービスの特典であり、料金プランの変更ではないため、価格や料金に関する軸へ対応づける根拠にはなりません。本文から軸に対応づけられる条件は「若年層」→「年齢帯=19〜40歳」に限られ、これは定義上根拠の弱い条件です。指定オプションへの加入や外部サービス利用を、既存の軸へ直接対応づける根拠が与えられていないため、条件を満たす顧客層を作成できません。

同じ材料から、違う結論が出ています。まず、「無料」という言葉に反応して「節約志向の層」が選ばれることはありません。料金プランの変更ではない以上、価格に関する軸へ対応づける根拠がないと判定され、条件として採用されなかったからです。第一章で3つの層を選んでいた過剰選定は、条件を軸に対応づける段階で消えています。そのうえで、唯一対応づけられた年齢は区分が粗く根拠が足りないと判断し、数えることを拒んでいます。競合が狙っているのは外部サービスに反応する層ですが、自社の軸にその境界はありません。

一方、条件が軸に直接対応するダミーの時事情報の場合を見てみます。

[ダミー]競合E社、大容量プランを10月から値下げへ

(概要)主力の大容量プランの月額料金を改定し、値下げする。あわせて家族割引も拡充し、同一世帯で複数回線を契約している場合の割引額を引き上げる。小容量プランは改定の対象外。

出力です。

判定:影響あり

本文は、競合による大容量プランの値下げと世帯向け割引の拡充に当たり、同等の利用実態を持つ自社顧客に流出圧力がかかる内容です。本文から対応づけられる条件は「大容量プランの利用」→「利用量帯=上位帯」、「同一世帯で複数回線を契約」→「世帯内契約数=2件以上」の2つで、いずれも軸に直接対応します。

顧客層:大容量利用かつ世帯内複数契約の層(45,600人) 根拠とした軸:利用量帯=上位帯、世帯内契約数=2件以上(人数はコード実行による集計値)

条件が2つとも軸に直接対応しているので、顧客層が立ち、人数が出ます。この45,600人を作っているのが、第四章の最後に載せたコードです。絞り込みに使っている軸も同じなので、見比べてみてください。値下げの話であっても「節約志向」のような言葉に広がることはなく、対象プランの特徴だけが条件になり、根拠とした軸と区分が出力にそのまま書かれています。

数えられるときは軸を明示して数える。数えられないときは数えないと言う。切り替え前後の違いをまとめます。

  切り替え前 切り替え後
照合先 層の名称と人数 集計マスタ
選び方 名称との語感 時事情報の条件を軸に対応づけ
根拠 「明記されており」 対応づけた軸と区分を明示
人数 層に紐づく定義済みの値を転記 コード実行で都度集計
集計不可能 近そうな層を出す 数えないと言える

数えられるときに正確に数えられるからこそ、「いまのデータでは数えられない」という答えに意味があります。もっともらしい数字をひとつ出すより、そう言えることのほうが重要でした。

振り返ると、コードインタープリターは精度を上げる道具として入れたのではありませんでした。照合先を差し替えるための手段として入れています。やるべきことは分かっていて、渡す手段がなくて手が出せなかった。打ち手が思いつかないときと、打ち手はあるのに前提が足りないときは、別の詰まり方をします。今回は後者でした。

そして、委譲は丸投げではありませんでした。エージェントの中でどう判断するかを決めなければ、語感はコードの書き方のほうに移るだけです。第四章に書いた辞書と採用基準と数え方は、そのために用意したものです。

同じ壁に当たる3条件

振り返ると、今回の行き詰まりは3つの条件が重なったときに起きていました。

  1. 参照データが、コンテキストに収まらない
  2. 一部を引くだけでは足りず、該当を漏れなく特定する必要がある
  3. 答えが「どれか」ではなく「どれだけか」を含む

3つ揃うと、要約もRAGも効きません。問い合わせから該当する商品を絞って対象範囲を出す、事案から関係する契約条項を特定して件数を出す。題材が変わっても、この3条件が揃えば同じ壁に当たります。

そのときは、渡すことをあきらめて計算させるほうが早いかもしれません。ただし、委譲した先で何をどう判断させるかは、こちらで決める必要があります。

残っている課題も挙げておきます。

  1. 照合先の軸の見直し:最初の例のように、影響の中心に対応する軸がなければ、影響を認めても数えられません。どの軸を足し、どの区分を細かく持つかは、これからの課題です。
  2. 評価指標の整備:ここまでの効果は利用者からの定性的な手応えにとどまっています。適合率を継続的に測れる評価セットがなければ、変更が改善なのか改悪なのかを確かめられません。

どちらも、照合先が語感のままでは手のつけようがなかった部分です。軸を足しても、その軸が使われるとは限りません。適合率を測っても、何を測っているのか説明できません。条件を軸に対応づけるようになったいま、どちらも検討の足場はできています。ここからが次の課題です。

参考文献

[1] Yixiong Fang, Tianran Sun, Yuling Shi, Xiaodong Gu. AttentionRAG: attention-guided context pruning in retrieval-augmented generation. arXiv, 2025 https://doi.org/10.48550/arXiv.2503.10720

[2] Nelson F. Liu, Kevin Lin, John Hewitt, Ashwin Paranjape, Michele Bevilacqua, Fabio Petroni, Percy Liang. Lost in the middle: how language models use long contexts. arXiv, 2023 https://doi.org/10.48550/arXiv.2307.03172

ご質問・お問い合わせは
こちらよりお送りください
採用
ARISE analyticsとは

PAGE TOP