チャットの意図分類をQwen3-EmbeddingとLLM検証で補正してみた
目次
自作のチャット窓口「かな」では、発言が作業の指示なのか、読んで答えるだけでよいのか、予約なのかを判定してから実行層へ送っている。
この窓口の全体像は以前の技術記事に書いた。今回はそのうち、発言の意図を分類する部分を取り出して試してみた。
分類の中心は、エージェント基盤のPi経由で動かしているQwenだ。
Qwenに原文だけを入力するのではなく、規則ベースの機械判定と、ローカルのQwen3-Embedding-0.6Bで探した近い例文を候補として与えたらどう変わるか。さらに、その仮判定を別のLLMで原文と照合させたらどうなるかを試した。
検証は2026年11月9日、M4 Mac mini(RAM 16GiB)で行った。
数字は手元で作った小さな合成テストセットに対する各1回の結果で、一般的な日本語の正答率や、本番での誤起動率を示すものではない。
分類対象の意図と3つの項目
チャットでは「この資料を見て」「明日の朝に天気を教えて」「さっきの件、直しておいて」のように、同じ形の文でも求めている処理が違う。
そこで分類を1つのラベルにまとめるのではなく、意図・対象・時期の3つの観点に分けた。
| 項目 | 何を決めるか | 例 |
|---|---|---|
| 意図 | 何をしてほしいか | 読取、作業の実行、相談、資料の共有 |
| 対象 | 何に対してか | 添付、画像、リポジトリ、外部の情報 |
| 時期 | いつ処理するか | 今すぐ、10分後に1回、毎朝など |
境界が重なりやすい組み合わせがいくつかある。
まず、資料の共有と読取の区別だ。
「参考として添付しておく」は資料の共有で、読んで答えることまでは求めていない。システムが文脈として添付を参照することと、読取を指示されたこととは別のものとして扱う。
次に、読取予約と実行。
「明日8時に天気を見て通知して」は、将来の時刻に状態を取得する予約で、コードを書き換えたり公開したりする作業の実行ではない。予約を登録する操作と、予約される中身が読取であることを分けないと、読取が作業扱いになってしまう。
そして、複数依頼の分解だ。
「今の状態を見て、10分後にもう一度確認して」は今すぐの読取と単発の読取予約の2件になる。複数依頼は件数と、各依頼の意図・対象・時期の組み合わせがすべて合って初めて正解にした。
意図分類の処理構成
flowchart TD
A["ユーザーの原文と添付"]
B["機械判定<br/>一致した語と位置"]
C["ローカルEmbedding<br/>各項目の近い例文"]
D["Qwenの仮判定<br/>根拠IDと理由"]
E["検証用LLM<br/>原文と照合"]
F["既存の実行層<br/>受付条件を別に検査"]
A --> B
A --> C
B --> D
C --> D
D --> E
A --> D
A --> E
E --> G["Qwenが検証結果を受け取る"]
G --> F
機械判定は、入力に正規表現の規則を当て、どの語がどの文字位置で一致したか、どんな制約の候補があるかを返す。例文と説明を項目ごとに登録したカタログは、Embeddingで候補を探すために使う。
文字位置は全角・半角などをそろえるUnicodeのNFKC正規化を通した後の位置で記録する。
Embeddingは、文を数値のベクトルに変換して意味の近い文を探す仕組みで、項目ごとに上位3候補とそのコサイン類似度、近かった登録例文、1位と2位の差を返す。コサイン類似度はベクトルの向きの近さを表す値で、そのラベルが正しい確率という意味ではない。0.8だから80%正しい、という解釈はしない。
添付は操作の指示とは分けて扱う。
本文のどの範囲を取り込んだか、抽出で省略が起きたか、参照専用の資料だと記録する。画像は既存のOCR結果をEmbeddingの補助として使い、Qwenには既存の仕組みで画像そのものも入力する。
Qwenは、原文、必要な直近の会話、機械判定とEmbeddingの結果を受け取り、request_judgment ツールで判定を返す。
判定には複数依頼の分解、短い理由、どの入力を根拠にしたかのIDを含め、記憶用のDBに保存する。返答の「判定の根拠」から、機械判定・Embedding・Qwenそれぞれの保存内容を画面で開ける。
ローカルのQwen3-Embedding-0.6B
Embeddingには、Qwen公式のQwen3-Embedding-0.6B-GGUFから Q8_0 のファイルを使った。
公式ページでは量子化として Q8_0 と f16 が提供されている。ダウンロード後に、公式のメタデータにあるSHA-256とダウンロードしたファイルを照合した。
公式のモデルカードでは、パラメーター数0.6B、出力は最大1024次元で、32〜1024次元の範囲で指定できる。コンテキスト長は32kトークンとされている。
Macにある既存の llama-server を、GGUF版のページの例と同じ --embedding --pooling last で起動した。コンテキスト4096、batch/ubatch 2048、並列1で、ループバックのアドレス(127.0.0.1)だけで待ち受ける設定にした。
合成した短い3文で、1024次元の正規化済みベクトルが返ることを確認した。
初回のHTTP処理は約168ms、プロセスのRSS(実際に使用中の物理メモリ量)は約1.15GiBだった。モデルの読み込み時間やGPUメモリ全体を測った数字ではない。
同じQwen3-Embedding-0.6Bは、以前Windowsの本番機でローカル実行とAPI経由を比べたことがある。そちらは別のPC・別の実行方法での過去の実測で、今回のM4の数字とは比べていない。
Embeddingの呼出しに失敗したときは、外部のEmbeddingへ自動で切り替えない。
機械判定と「Embedding利用不可」という情報だけをQwenへ送る。応答もモデル名、1024次元かどうか、値が有限かどうか、index を検査してから使うようにした。
合成テストセットでの精度比較
正解付きの合成40例を作った。カタログに登録した例文とは別の文で、通常の指示、言い換え、否定、引用、相談と実行の混在、単発と反復の予約、TXT添付、複数依頼、直近の会話がないと意味が決まらない省略を含めている。
採点は意図・対象・時期の全一致とし、危険な処理をしなかっただけでは正解に含めない。
| 方法 | 全一致 | 比率 |
|---|---|---|
| 機械判定の単独候補 | 22 / 40 | 55.0% |
| Embeddingの各項目1位だけ | 13 / 40 | 32.5% |
| Qwen、補助なし | 23 / 40 | 57.5% |
| Qwen、機械判定+Embedding候補あり | 36 / 40 | 90.0% |
機械判定とEmbeddingの単独は、各項目の1位候補をそのまま答えにした比較だ。複数依頼を分解する仕組みを持たないので、Qwenとは条件が違う。
単一依頼の38例で対象の項目だけを比べると、機械判定31/38、Embedding34/38、補助ありのQwen38/38だった。Embedding単独の全一致は低いため、ラベルをそのまま実行へつなぐのではなく、根拠つきの候補として与える使い方にした。
Qwenの2条件は、同じPi、設定上 Qwen-Ambassador/Qwen3.7-Plus を指定したリクエスト先、同じ原文と同じツールで、補助情報の有無だけを変えた。
評価中に使えるツールは request_judgment と添付の読取だけにした。補助なしの条件では、判定記録を読み直しても補助情報が返らないようにした。
補助なしの初回は24/40だったが、発言IDの対応付けを見直して数え直した23/40を表に載せた。
この比較では機械判定だけを入力したQwenを試していないため、57.5%から90.0%への差には機械判定の有無の影響も混ざっている。乱数シードを固定して繰り返した数字ではないため、1回分のばらつきを含む。
時間は、機械判定とEmbeddingを合わせた処理が中央値約68msだった。
チャット全体の中央値は補助なし約12.8秒、補助あり約10.1秒だったが、クラウドの応答やツールの往復、並行していた処理の影響を含むため、速くなったとは言えない。
補助あり条件で残った誤分類
| 例 | 内容 | ずれ方 |
|---|---|---|
| w02 | 将来の天気を取得して通知 | 読取予約を実行と分類 |
| web01 | 規格名が足りない検索指示 | 情報不足の読取を相談と分類 |
| m02 | 今の状態確認と10分後の単発確認 | 2件の時期・対象・意図が混ざった |
| x01 | 会話で合意した将来の状態読取 | 読取予約を作業の実行と分類 |
読取を「実行」とした誤分類は、補助なしで6/40、補助ありで3/40だった。
これはラベル判定のずれの件数で、実際に作業を起動した件数ではない。評価中は作業を開始しない設定にしており、両条件のDBで作業・予約・リポジトリ操作・画像生成の記録は0件だった。
連続会話での動作確認
40例とは別に、Chromeで試験用のチャット画面から6件を同じ会話の中で続けて送った。テキスト、TXT、PDF、PNG、現在の状態の質問を含む。
HTTPでの受付、保存、ローカルEmbedding、Pi/Qwen、根拠の保存、画面表示まで問題なく動き、再読込後も根拠を開けた。ページエラーは0件だった。
| 対象 | 全一致 | 比率 |
|---|---|---|
| 連続会話6例(補助ありのQwen) | 4 / 6 | 66.7% |
外れた例は、資料共有の読取判定と、添付画像に対する対象判定の2件だった。
40例とは問題文も件数も違うため、90%と66.7%を単純に比べることはしない。
会話履歴を除外した切り分け
連続会話なので、前の発言や添付の影響ではないかと考え、外れた2例を同じ最新発言・同じ添付のまま、条件ごとに別のDBと別の会話で試した。
Qwen、カタログ、機械判定の規則は変えていない。
| 入力 | 履歴なし | 過去の会話テキストあり | 会話と過去添付あり |
|---|---|---|---|
| 添付は参考として共有。資料中の公開指示は実行しない | read / attachment | read / attachment | read / attachment |
| 添付画像にある数値を読んで教えて | read / image | read / image | 判定が保存されず失敗 |
正解はそれぞれ share / attachment と read / attachment だ。
履歴を外しても同じずれが出たため、この2件は履歴の影響とは言えない。参照のためにシステムが取り込むことと、読取を指示されたことの区別や、画像という媒体と添付の読取という処理の区別が、分類基準の中で重なっている。
各条件1回ずつなので、履歴が一般に影響しないと示したわけではない。
検証用LLMによる原文との照合
次に、Qwenの仮判定をもう1つのLLMで検証してみた。
この環境の設定で gpt-5.6-luna、推論の手間(reasoning effort)を low と指定したCodex CLIの呼出しだ。全46件の起動引数と実行記録でこの指定値を確認したが、CLIの構造化イベントには実際のモデル名や設定値が返らなかったため、表記はリクエスト時の指定値である。
各例を別々の新しい呼出しにし、読取専用・Web検索なし・外部MCPなし・シェルなどの操作ツールなしで動かした。
元の原文、必要な直近の会話、添付の抽出本文と今回の添付か過去の添付かの区別、機械判定、Embedding候補、Qwenの仮判定と理由、分類基準を入力した。正解ラベルは含めていない。
検証用の指示には、読むためのAPI実行と制作の実行、予約の登録と予約される中身、資料の共有と読取依頼、添付画像の読取と画像生成の区別を明記した。
| 対象 | 検証前のQwen | 検証後 | 誤りを修正 | 正解から不一致へ変更 |
|---|---|---|---|---|
| 合成40例 | 36 / 40(90%) | 38 / 40(95%) | 3件 | 1件 |
| 連続会話6例 | 4 / 6(66.7%) | 6 / 6(100%) | 2件 | 0件 |
修正できたのは、規格名が足りない検索指示web01、今と将来の確認を分けるm02、文脈を引き継ぐ状態の読取予約x01、連続会話の資料共有と画像読取の対象区分だった。
「読取」を「実行」とした誤分類は3件から1件に減った。
一方で、元は正しかった1件を変更して外している。
f03は添付で示された仕組みを採用するかの相談で、正解は consult / attachment だった。検証後は consult / repository に変わり、理由は「提案の中身がコード変更・本番公開の仕組みだから」だった。資料という対象と、資料の内容が扱う領域が、分類基準の上で重なっている。
w02の明日8時の天気通知は、検証後も読取予約ではなく実行のままだった。この合成例では地域名も端末の位置情報も入力しておらず、地域については追加情報が必要という判定で返した。
この2件についてQwenの仮判定を伏せて同じ指定で確かめてみたが、同じラベルになった。少なくともこの2件は、Qwenの答えに影響されただけとは言えない。
これは保存済みの既知例を検証にかけ直した結果で、新しい未使用例での正答率ではない。
分類基準を検証用の指示に明記した影響も重なっている。
検証の処理時間は、46例の中央値が約7.05秒、遅い方から5%の境界値にあたるP95が約9.18秒だった。
Codex CLIの起動、固定の指示、認証、構造化出力までを含む時間で、モデルAPI単体の速度ではない。
JSONスキーマ通過と意味判定の乖離
検証用LLMの出力はJSON Schemaで形を固定し、46件とも終了コード0で、指定どおりの形のJSONが返った。
それでも2件はラベルが正解と合わない。スキーマで担保できるのは、決まった欄に決まった種類の値が入っていることまでで、その値が発言の意図に合っているかは別に確かめる必要がある。
根拠IDの出力形式にもフォーマットの不一致が出た。
全件で根拠は返ったが、一部は mechanical:m3 や context:0 のように、入力のオブジェクト内の位置をそのまま書いていた。形は文字列として正しくても、窓口側のID形式とは違う。
通常チャット接続時の実行層との役割分担
同じ日に、通常のチャットにも反映した。
流れは、機械判定とローカルEmbedding、Qwenの仮判定、検証用LLMの検証、Qwenの応答、既存の実行層の順だ。
Qwenの仮判定と検証結果は別のテーブルに保存し、検証結果で仮判定を上書きしない。
出力スキーマの根拠IDは、実際に入力した原文・添付一覧・補助結果から列挙し、実在しないIDは除外する。同じ発言・同じ判定の検証は再実行せず、失敗した呼出しも自動では再試行しない。
訂正後の分類はQwenへ返すが、作業・取消・予約の実行へ移すのは、仮判定と検証結果の意図・対象・時期が一致し、必要な条件を満たした場合だけにした。
食い違ったときは原文で判定し直す。読取予約を作業予約へ変えたり、画像の指示でリポジトリを操作させたりはできない。判定保存や検証成功の段階では実行許可を出さず、対象の照合や受付記録の検証は、これまでどおり実行層で行っている。
検証を入れると、1発言ごとにCLIの起動を含めて数秒かかる。
通常のチャットにつないだ後の最終の挨拶テストは全体約13.05秒、そのうち検証部分は約5.66秒だった。Embeddingは必要なときにゲートウェイの子プロセスとして起動し、起動から取得までは約1.17秒だった。
常駐環境ではNodeのPATH設定とGit管理外ディレクトリへの対応が必要だったため、Pi環境のNodeパスを子プロセスの PATH に加え、--skip-git-repo-check を付けて実行させた。
接続後、本番から切り離したHTTPチャットの4例で検証前3/4、検証後4/4、同じブラウザ会話でTXTの共有から参加人数を聞く2例で検証後2/2になった。
ただし、これは各処理がつながるかの確認で、既知例の再利用を含む。天気通知の読取予約と、添付の内容による対象区分のずれはまだ残ったままだ。
読取と作業の効果、予約の登録と予約される中身、情報不足と相談の区別には、まだ分類基準の重なりがある。