はじめに
社内で「ベクトル検索とRAGの勉強会をやりたい」と考えていますが、正直なところ自分自身がまだ理解しきれていません。
なので、勉強しながら書いています。この記事はその1本目です。
さて、本題に入る前に、こんな画面を見たことがないでしょうか。
AIに質問すると、途中で「検索しています」と表示されて、参考リンク付きの回答が返ってくる。もう見慣れた光景だと思います。
ここで、疑問です。
知っているなら、調べる必要はありません。そして実際、調べに行かずに答えてくることもあります。その差はどこから来るのか。
普段は便利に使っているだけなので、考えたことがありませんでした。でも、この問いの答えが、そのままこの記事のテーマです。
想定読者
- SQLは書ける(SELECT、WHERE、JOINあたりは日常的に使う)
- AIはチャットで使ったことがある
- 中身がどうなっているかは知らない
私自身がこの立ち位置なので、同じところから書きます。
この連載について
全8回を予定しています。最終的には、Oracleを使って社内文書を検索する仕組みを作るところまで行きます。
その1本目として、まずはLLMには何ができないのかを整理します。できることの話は他にたくさんあるので、ここではできないことに絞ります。
1. トークンを1つずつ選ぶ
最初に、いちばん大事なことを書きます。
LLMは、文章を書いていません。
これが自分にとって一番の躓きポイントでした。ChatGPTに質問すると文章が返ってくるので、当然「文章を作るもの」だと思っていたのですが、実際にやっていることは違いました。
LLMが1回の処理でやっているのは、これだけです。
入力されたトークン列を受け取って、次に来るトークンを1つ選ぶ
1つだけです。文章ではありません。
では、なぜ文章が返ってくるのか。繰り返しているからです。
「日本の首都は」と入れると「東京」が出てくる。それを末尾に足して「日本の首都は東京」をもう一度入れると、今度は「です」が出てくる。また足して入れる。これをひたすら繰り返した結果が、私たちが見ている文章です。
文章が少しずつ表示されていくのを見たことがあると思いますが、あれは演出ではなく、本当に1つずつ作っているからです。
トークンは単語ではない
もう一点、押さえておきたいのがトークンです。
トークンは単語ではありません。学習のときにコーパスから統計的に切り出された、文字のかたまりです。人間の言語感覚とは無関係に決まります。
- よく出てくる語は1トークンにまとまる
- あまり出てこない語は複数に割れる
- 英語の
unbelievableがun/believ/ableのように割れることもある
そして日本語は不利です。語彙が英語中心に作られているので、1文字が1トークン前後になりやすい。同じ内容でも、日本語のほうがトークンを食います。
ここで一つ、後で効いてくることを書いておきます。
モデルは文字も単語も見ていません。トークンIDの列を見ています。
つまり、私たちが数える「文字数」と、モデルにとっての「トークン数」は別物です。この違いは第5章で効いてきます。
なお、この章では「どうやって選ぶのか」は書きませんでした。そこが次の章以降の本題になります。
2. 同じ質問、違う答え
ここで一度、簡単な実験をします。
まったく同じ質問を、毎回新しいチャットで3回投げてみました。前の回答が見えていると実験にならないので、そこだけ注意しています。
まずは、答えが1つに決まる質問から。
日本の首都はどこですか。一言で答えてください
3回とも「東京」でした。ここは動きません。
ただ、よく見ると1回目だけ「東京です。」になっていて、残りの2回は「東京」で止まっています。内容は同じですが、語尾が揃っていません。
次に、答え方に幅のある質問を投げます。
データベースを正規化するメリットを3つ、箇条書きで挙げてください
今度は、はっきり違います。
- 1つ目(重複の削減)は3回とも同じ
- 2つ目(整合性・不整合の防止)も3回とも同じ
- 3つ目だけが入れ替わる
3つ目は、「更新時の異常を防止できる」だったり「保守性・拡張性が向上する」だったりします。どちらも正しいのですが、回によって違うものが出てきます。
そして、書式まで揃っていません。2回目だけ、太字も補足説明もない、あっさりした箇条書きになっています。
SQLに慣れていると、これはかなり気持ち悪いのではないでしょうか。
SELECT * FROM employees WHERE dept_id = 10;
データが変わっていなければ、このSQLは何度実行しても同じ結果を返します。当たり前です。同じ入力に同じ出力が返ることを、決定的と言います。
LLMは決定的ではありません。同じ入力でも、返ってくるものが変わります。
揺れる場所と、揺れない場所がある
ここで一つ、押さえておきたいことがあります。
「この質問は揺れる、あの質問は揺れない」という話ではありません。
Aは答えが決まっている質問でしたが、語尾は揺れました。Bは揺れる質問でしたが、1つ目と2つ目は揺れませんでした。
つまり揺れは質問の単位ではなく、もっと細かい単位で起きています。同じ回答の中に、固い場所と緩い場所が混ざっている。
なぜでしょうか。
答えは次の章にあります。というより、次の章の説明が終わると、この疑問は勝手に解けます。
3. 該当なしが存在しない
予測変換は空っぽにならない
いきなりLLMの話をする前に、身近なものから入ります。
スマホで文字を打つと、変換候補が出ます。
いま手元のスマホで、適当な文字列を打ってみてください。意味をなさない入力でも構いません。
おそらく、ならなかったと思います。何かしら並んだはずです。しかも、順番がついていたはずです。
私たちは毎日これを使っているのに、「候補が空になることはまずない」という性質を、あまり意識していません。
LLMも同じことをしている
そしてここからが本題なのですが、LLMがやっているのは、これの巨大版です。
比喩ではありません。次に来るものを予測して順位をつける、という点で本当に同じことをしています。予測変換の候補が3つや4つなのに対して、LLMは語彙の全部に順位をつけている、という違いがあるだけです。
SQLで書いてみる
ここからは、SQLの形で説明します。
LLMが1回の処理でやっていることをSQLで書くと、だいたいこうなります。
SELECT token, score
FROM vocabulary
ORDER BY score DESC
FETCH FIRST 1 ROWS ONLY;
vocabulary は語彙のテーブルだと思ってください。トークンが10万件くらい入っています。
やっていることは、全件にスコアを付けて、いちばん上を取る。それだけです。
ここで注目してほしいところがあります。
WHERE句がありません。
そして、最後まで付きません。
私たちがSQLで0件を受け取れるのは、WHERE句で絞り込んでいるからです。条件に合う行がなければ0件になる。書いていて当たり前すぎて意識しませんが、これは**「該当なし」という状態を表現できている**ということです。
LLMには、それがありません。
10万件を並べ替えれば、必ず1位が存在します。0件になる経路がない。
言い方を変えます。
LLMは「該当なし」を返さないのではなく、「該当なし」を表現するSELECT文が書けないのです。
存在しないものについて質問したとき、それでも何か返ってくるのは、この構造の当然の帰結です。
「知りません」と答えてくれる場合もありますが、あれも同じです。沈黙しているのではなく、「知りません」という文字列が1位になっただけです。無を出力する経路は、そもそもありません。
実際は1位を取っているわけではない
ここで第2章の疑問が回収できます。
上のSQLは説明のために単純化していて、実際のLLMは必ずしも1位を取っていません。上位の候補から、確率的に1件選んでいます。
SQLで無理やり書くなら、こんなイメージです。
-- 上位の候補を取ってきて、その中から確率に応じて1件選ぶ
「温度パラメータ(temperature)」という設定を聞いたことがあるかもしれません。あれは、どこまで下位を候補に入れるかの調整ノブです。
| 温度 | 挙動 |
|---|---|
| 低い | ほぼ1位が選ばれる。毎回ほぼ同じ答えになる |
| 高い | 下位も選ばれる。毎回違う答えになる |
同じ質問に違う答えが返ってきたのは、これが理由でした。
そして、第2章で見た「揺れる場所と揺れない場所」も、これで説明がつきます。
「日本の首都は」の次に来る「東京」は、1位が圧倒的に強い。だから確率的に選んでも、結果は変わりません。正規化のメリットの1つ目も同じで、「重複の削減」が抜けています。
一方で、3つ目の項目は候補が拮抗しています。「更新時の異常」も「保守性・拡張性」も、どちらも正しい。スコアが近いので、回によって選ばれるものが変わります。
揺れているのは、順位が拮抗している場所だけです。
数式で見る場合(読み飛ばして問題ありません)
モデルの最終層は、全トークンに素点(ロジット)を付けます。これを確率に変換するのが softmax です。
$$p_i = \frac{e^{z_i / T}}{\sum_j e^{z_j / T}}$$
分母が全トークンの合計になっているため、出力の合計は必ず1になります。これが「0件が存在しない」ことの数学的な理由です。
T が温度パラメータで、ロジットを割ってから softmax にかけています。T が小さいと分布が尖り(ほぼ最大値が選ばれる)、大きいと平らになります(下位も選ばれやすくなる)。
断定的な口調は確信度ではない
もう一つ、気をつけたい点があります。
LLMの回答は、たいてい断定的です。「〜です」「〜になります」と言い切ってきます。
これを見ると、つい「自信があるんだな」と思ってしまうのですが、そうではありません。
その文体は、そう書かれた文章を大量に読んだ結果、なぞっているだけです。内部の確からしさが文体に反映される仕組みにはなっていません。
だから、こういうことが起きます。
知らないことほど、自信満々に見える。
知っていることを答えるときも、知らないことを答えるときも、同じ手続きで文字を選んでいるので、出てくる文章の見た目が変わらないのです。
なお、最近のモデルは「分かりません」と答えてくることもあります。ただ、それは「分からないと判定した」というより、そう答える振る舞いを学習した結果と考えたほうが実態に近いようです。このあたりは自分もまだ調べきれていないので、断定は避けておきます。
予測変換との決定的な違い
ここまで予測変換に例えてきましたが、一点だけ、大きな違いがあります。
予測変換が見ているのは、直前の数文字です。
LLMは、入力された文章全体を見て順位を決めています。同じ「東京」を予測するにしても、判断材料の量が桁違いです。だから、長い文脈を踏まえた回答ができます。
そして、この「どこまで見ているか」には上限があります。それが第5章の話です。
4. 学習は保存ではない
さて、ここまでで「LLMは常に何か答える」ということは分かりました。
次の疑問は、答える内容はどこから来ているのかです。
集計に近い
「学習データ」という言葉から、つい「データベースに入っている」ようなイメージを持ってしまいます。私はそう思っていました。
違いました。学習データは、モデルの中に入っていません。
INSERTしたわけではないからです。
100万件の明細を集計すれば、部署ごとの合計は分かります。でも、その集計結果から元の1行1行に戻ることはできません。捨てられているからです。
LLMの学習も、大量の文章を読んでその中の傾向だけを残す作業です。元の文章そのものは残りません。
もう一段、近い例えがあります。
オプティマイザの統計情報です。
統計情報は、テーブルの中身そのものではありません。「この列にはどんな値が多いか」という分布の要約です。だから統計情報をいくら眺めても、3行目の値が何かは分かりません。
それでもオプティマイザは、統計情報だけを見て「この条件なら何件くらい返るはずだ」と予測できます。
LLMのパラメータは、これに似ています。大量の文章の傾向が畳み込まれていて、そこから「次に来そうなもの」を予測する。元の文章は入っていないのに、予測はできる。
性質が似ているというだけで、同じものではありません。ただ、イメージとしてはかなり近いと思います。
検索を切ってみる
ここからは実験です。
普段AIを使っていると、必要に応じて自動的に検索が入ります。便利なのですが、そのせいでモデル本来の姿が見えなくなっています。
なので、切ってみました。
余談ですが、これが一番苦労しました。設定でオフにしたつもりでも、前の会話に検索結果が残っていて、それを見ながら答えてくるということが何度もありました。結局、ChatGPTの無料プランではウェブ参照を切る設定に辿り着けず、ここだけGeminiで試しています。特定の製品の話をしたいわけではないので、そこはご容赦ください。
まず、こう聞きました。
Oracle Database 19cのVECTOR型の使い方を教えてください
種明かしをすると、19cにVECTOR型はありません。 Oracleにベクトル型が入ったのは23aiからです。実在する製品の、実在しない機能について聞いています。

正確に答えてきました。「19cではサポートされていない」「23aiから実装された」。調べに行った形跡はありません。知っていたわけです。
次に、まったく同じ設定で、冒頭と同じことを聞きます。
今年の夏について気温の考察
こちらも、すらすらと答えてきました。
参照していないものを、参照したと書く
さて、2つ目の回答の1行目を見てください。
気象庁等の最新発表データを基に傾向をまとめました
検索は切ってあります。参照した形跡もありません。最新発表データを見ることは、できないはずです。
中身も読んでみます。太平洋高気圧とチベット高気圧、海面水温の上昇、局地的な豪雨。どれも、もっともらしい。
ただ、これはどの年の夏にも当てはまる話です。今年の話として書かれていますが、今年について何も言っていません。
私はここで背筋が寒くなりました。指摘されなければ、気づかなかったからです。
明らかに間違ったことを書いてくれるなら、まだ気づけます。一般論を最新データとして提示されると、読んでいる側には区別がつきません。
本文と出典は、同じループで作られている
なぜこうなるのか。第3章の答えがそのまま使えます。
モデルは「気象庁等の」まで出力した時点で、次のトークンを出すしかありません。何も出さないという選択肢が、仕組み上ないからです。
そして、モデルは学習の過程で無数の文章を読んでいます。だからそれらしい形は完璧に覚えています。出典を示す文章の言い回し、データに基づいた分析の書き方。
形式は完璧です。中身の裏付けがないだけです。
ここが、自分がいちばん腑に落ちたところでした。
本文と出典は、同じ1つのループで作られています。
出典を書くときだけ別のテーブルを引いている、ということはありません。実在するかを確認する処理は、どこにも存在しません。本文を書くのとまったく同じ手続きで、「最新発表データを基に」という文字が1つずつ選ばれています。
これは特定の製品の欠陥ではありません。第3章で見た通り、仕組み上どのモデルでも起きます。
持っているものと、持っていないもの
同じモデル、同じ設定、違うのは質問だけです。それでも結果は分かれました。
| 質問 | 結果 |
|---|---|
| Oracle 19cのVECTOR型 | 正確に答えた |
| 今年の夏の気温 | 一般論を、最新データとして提示した |
境目はどこにあるのでしょうか。
Oracleのバージョンとベクトル型の話は、公式ドキュメントや技術記事に何度も書かれています。そのため、残っています。
今年の夏の気温は、そもそも学習した後に起きたことです。書かれた文章が存在しません。
1回しか出てこない情報は残らない
統計情報の例えを、もう一度使います。
頻度の分布を取ると、よく出てくる値は棒が立ちますが、めったに出てこない値は埋もれます。
学習でも、同じことが起きます。
※これは実測値ではなく、イメージを表した概念図です。
ネット上に何万回も出てくる情報は強く残ります。1回しか出てこない情報は、ほとんど残りません。
そして、**社内の設計書は当然ゼロです。**一度も読まれていないので、棒が立ちようがありません。
更新できない
もう一つ、厄介な性質があります。
後から直せません。
テーブルなら、間違いを見つけたら1行UPDATEすれば済みます。モデルの中には、対象の行がありません。直すには学習をやり直すことになり、現実的ではありません。
つまり、社内の設計書については、
- そもそも読まれていない
- 仮に読ませたとしても、更新のたびに学習し直すことはできない
という二重の問題があります。1つ目だけなら「じゃあ学習させればいい」で終わるのですが、2つ目があるのでそうもいきません。
冒頭の問いに戻る
さて、冒頭でこう質問しました。
なぜ、わざわざ調べに行ったのか。そして、調べに行かないこともあるのはなぜか。
答えは、持っているかどうかでした。
Oracleのバージョンの話は持っていたので、調べませんでした。今年の夏の気温は持っていないので、調べに行きました。冒頭の画面で参考リンクが付いていたのは、モデルが覚えていたからではなく、その場で調べた結果です。
そして、調べる手段まで取り上げると、持っていないものについても何か答えます。それが「最新発表データを基に」でした。
つまり、モデル自体は何も変わっていません。外側に「探してくる仕組み」を足しているだけです。
そして実は、これがこの連載の目的地です。同じことを、社内の設計書に対してやります。
5. どこまで見ているか
第3章の最後で保留にした話です。
LLMが一度に扱えるトークン数には上限があります。これをコンテキストウィンドウと言います。
注意点が2つあります。
1つ目。入力と出力の合計です。
長い文章を読ませると、そのぶん出力に使える余裕が減ります。
2つ目。文字数ではなくトークン数です。
第1章で書いた通り、日本語はトークンを食います。「何文字まで」で見積もると外します。
会話を覚えていない
ここが個人的にいちばん意外だったところです。
LLMは、会話を覚えていません。
チャットが文脈を保っているように見えるのは、裏で過去のやりとりを毎回まるごと送り直しているからです。
1ターン目は質問1だけを送ります。2ターン目は、質問1と回答1、質問2を送ります。3ターン目は、そこにさらに回答2と質問3を足して送ります。
ターンが進むほど、送るものが増えていきます。
モデル自体は、前回何を話したかを保持していません。毎回、初対面の相手に会話の全文を渡しているようなものです。
これは実験中に、思わぬ形で確認できました。検索を切ったのに、今年の夏について答えられてしまったことがあります。理由は回答の冒頭に書いてありました。
「これまでの会話内容をもとに考察します」。同じチャットの中で切ったので、その前に検索で取ってきた内容が、まだ会話に残っていたわけです。
情報はモデルの中に入ったのではなく、会話の中に置かれている。だから毎回送り直す必要があるし、上限にも当たります。
だから、会話が長くなると上限に当たります。長いやりとりを続けていると挙動がおかしくなることがありますが、これが理由の一つです。
6. 知らないのではなく、持っていない
まとめます。
| RDB | LLM | |
|---|---|---|
| 動作 | 検索 | 生成 |
| 同じ入力 | 同じ結果 | 変わりうる |
| 該当なし | 0件を返せる | 表現できない |
| データの所在 | テーブル | パラメータ(戻れない) |
| 更新 | UPDATE | 再学習が必要 |
こうして並べると、自分がLLMに対して持っていたイメージが、だいぶRDB寄りだったことが分かります。
「検索して、なければ0件」ではなく、「常に生成して、必ず何か返す」。
そして重要なのは、LLMは社内の設計書を知らないのではなく、持っていないということです。知識が足りないのではなく、そもそも渡されていません。
なら、話は単純です。
持っていないなら、渡してしまえばいい。
次回は、実際に設計書をまるごとプロンプトに貼るとどうなるかを試します。うまくいきそうな気がするのですが、実際にやってみると壁に当たります。
この記事を書いて分かったこと
最後に、勉強しながら書いた感想を残しておきます。
一番効いたのは、SQLに翻訳してみることでした。
「該当なしを返せない」と言葉で聞いても、正直ピンと来ていませんでした。でも WHERE句がない と書いた瞬間に腑に落ちました。自分が毎日書いているものに引き寄せると、急に自分の話になります。
もう一つは、検索を切ってみたことです。普段使っている状態のままだと、どこまでがモデルの力で、どこからが外側の仕組みなのかが分かりません。切って初めて境目が見えました。
もし同じようにモヤモヤしている方がいたら、自分の得意な領域の言葉に置き換えてみるのと、便利な機能を一度切ってみるのをおすすめします。
なお、この記事は勉強しながら書いているので、間違いが含まれている可能性があります。お気づきの点があれば、コメントで教えていただけると助かります。
次回に続きます。









