Slack の「技術的な質問」だけを AI で判定するためのルーブリックと CoT

Slack の「技術的な質問」だけを AI で判定するためのルーブリックと CoT

  • 社内 Slack に流れてくるメッセージの中から「技術的な質問」だけを AI で判定し判定したら指定のリアクションをつける仕組みを作りました。
  • AI 判定には Amazon Bedrock 経由で Claude(Haiku 4.5 / Sonnet 4.5)を使っています。
  • 単発メッセージの判定とスレッド返信の判定で、プロンプトを分けて設計しました。
  • スレッド判定では、ルーブリック(採点基準)と Chain of Thought(思考連鎖)を組み合わせています。
  • 思考はモデル内部だけで走らせ、出力は最小限の固定フォーマットに揃えるのが運用しやすかったです。

この記事で扱うのは、判定ロジックそのものに絞った次の3点です。

  1. 単発メッセージを「技術的な質問か」で判定するプロンプト設計
  2. スレッド返信を「親の続きか/別の新規質問か」で切り分ける判定
  3. そこで使った Chain of Thought とルーブリックの組み立て方

1. 単発メッセージの判定

1.1 まず事前フィルタで AI 呼び出しを減らす

AI に投げる前に、ルールで対象外を弾きます。コストとレイテンシ削減のためです。

項目条件
メッセージタイプtype == "message"
サブタイプsubtype なし(file_share は例外で許可)
Bot 除外bot_id が無いこと
テキスト存在text が空でないこと
ドメイン除外自社ドメインのメールアドレス以外
チャンネル除外excluded_channels に該当しない

ここを通り抜けたものだけを Claude に投げます。

1.2 プロンプトの構造

Claude に渡しているプロンプトは、上から下に次の順で並んでいます。

  1. 最重要ルール(5項目)
  2. 技術的な質問の定義
  3. 質問の特徴(疑問形・相談意図の表現パターン)
  4. 技術的な質問ではないもの
  5. 判定基準
  6. 判定手順(Chain of Thought 的な誘導)
  7. 判定例(Few-shot)
  8. 出力フォーマット

特に効いているのが「最重要ルール」と「判定手順」を分けて書くことです。

  • 最重要ルール = 守って欲しい絶対条件
  • 判定手順 = どの順で考えて欲しいか

この2つを別の役割として書き分けると、Claude の出力が安定しやすくなります。

1.3 「最重要ルール」で誤判定を先に潰す

社内 Slack でいちばん厄介な誤判定は、これです。

IT 製品の名前は出てくるが、聞いているのはライセンス価格や調達

たとえば次のメッセージは、技術的キーワードを含むのに「No」にしたいものです。

  • 「Jamf Connect Basic も合わせて20L のお見積りをいただけますでしょうか」
  • 「Notion を購入するとした場合、最低ライセンス数とエンタープライズの単価をご教示いただけますでしょうか」
  • 「更新後、ライセンスの期中追加は可能な認識でよろしかったでしょうか」

ナイーブに判定させると、製品名やキーワードに引っ張られて「Yes」と返してきがちです。

そこで、プロンプトの先頭に最重要ルールを置いています。要約するとこうです。

  • 製品名が含まれていても、ライセンス購買・価格・契約条件は「No」。
  • 「ライセンス」「見積」「購入」「調達」「手配」などが含まれ、技術的な使い方の話でなければ「No」。
  • 「20L」「10Lic」のような略記もライセンス数の表記として扱う。
  • 技術とビジネスの両側面が混在する場合、購買色が少しでも強ければ「No」。

このルールに沿った判定例を Few-shot で並べておくと、Haiku でも Sonnet でも安定して購買系を弾けるようになりました。

1.4 判定手順による軽量な Chain of Thought

最重要ルールだけを書いておくと、モデルがどの順で考えるかが曖昧になります。そこで、考える順序をプロンプトで明示しています。

【判定手順】
以下の順序で考えてください(結果だけ回答し、途中の思考は出力しないでください):

1. 「ライセンス」「見積」「購入」「調達」「手配」「単価」「価格」「料金」「契約」、
   または数字+「L/Lic/個/ライセンス」表記が含まれるか?
   → 含まれていて購買・商業の文脈なら「No」

2. 回答・報告・承諾・状況報告・お礼などの応答型か?
   → そうなら「No」

3. 商談・打ち合わせの日程調整が主旨か?
   → そうなら「No」

4. 上記に該当せず、技術的な設定・実装・使い方・トラブルシューティングを尋ねているか?
   → そうなら「Yes」

5. それ以外は「No」

これが軽量な Chain of Thought(思考連鎖)です。「全部考えた上で総合判定して」と曖昧に頼むより、ステップごとに段階的にふるい落とさせるほうがブレません。

ただし、思考過程は出力させません。出力は次の固定フォーマットに揃えます。

判定: Yes または No
信頼度: High, Medium, または Low
理由: 判定した理由を1-2文で簡潔に説明

このおかげで、後段のパースは「判定: の次の行を見るだけ」で済みます。JSON ではなく「キー: 値」の素朴な形にしているのは、Bedrock の Claude で時々起こる JSON 崩れを避けるための割り切りです。

1.5 Haiku か Sonnet か

最初は Claude Haiku 4.5 で運用していました。安いし速いです。

ただ、ライセンス購買のような「文脈で判断する必要があるケース」では問題が出ました。Few-shot を入れても、プロンプトの明示ルールを十分に守ってくれないことがあるのです。

そこで本番環境は Sonnet 4.5 に切り替えました。

  • 本番 = Sonnet 4.5(精度重視)
  • 検証・ステージング = Haiku 4.5(コスト重視)

2. スレッド中の質問の判定

ここが今回いちばん設計を悩んだところです。

2.1 なぜスレッド判定が必要か

単発メッセージの判定をそのままスレッド返信に当てると、誤判定が頻発します。

起票したくないケース

  • 親 = AWS のトラブルシューティング
  • 子 = 「ありがとうございました。S3 からの削除手順も教えてください」

子だけ見ると技術的な質問に見えますが、これは親の会話の延長です。新規の質問としては拾いたくありません。

起票したいケース

  • 親 = Secure Boot の確認方法
  • 子 = 「別件で恐縮ですが、Okta 連携の LDAP エラーが発生しています」

これは完全な別トピックです。子も新規の質問として拾いたい。

つまりスレッド返信は、親メッセージとの関係性を踏まえて判定する必要があります。

2.2 期待される動作

挙動を4ケースに整理しました。

パターン期待動作
A技術的質問(任意)親を起票
B非技術技術的質問(親と無関係)子を起票
C技術的質問親と完全に別の新規技術的質問子も起票
D技術的質問親から連なる会話の流れの追加質問子は起票しない

C と D の境界がいちばん難しいです。ここを誤ると、

  • 1スレッドから何件も重複起票される
  • 拾うべき新規質問を取り逃す

のいずれかが起きます。

2.3 判定フロー

スレッドメッセージに対する判定の流れはこうなっています。

スレッドメッセージ受信


親メッセージのリアクションを取得
(親が技術的質問として起票済みかのヒント)


is_response_message を呼ぶ
(会話履歴 + ルーブリックで判定)

    ├─ YES(親に連なる会話) → 新規起票しない

    └─ NO(新規質問) → 単発メッセージと同じ AI 判定へ

ここでの設計の肝は1つです。

親にリアクションが付いていてもいなくても、必ず is_response_message を通す。

親が非技術な相談(たとえば購買相談)でも、子だけ見ると技術的な質問に見えるケースがあるためです。「親リアクションなし → そのまま単発判定」というショートカットは取りません。


3. ルーブリックと Chain of Thought の組み合わせ

is_response_message は、単純な Yes/No プロンプトではありません。ルーブリック方式を採っています。

3.1 ルーブリック設計(3つの観点)

3つの観点で多次元的に評価させます。

観点内容
トピック連続性high / medium / low親と最後のメッセージで、話題・対象・問題がどれだけ重なっているか
応答性high / medium / low最後のメッセージが応答・追加確認・承諾・進捗報告・お礼であるか
話題転換シグナルstrong / weak / none「別件で」「話は変わりますが」のような明示的な転換表現の有無

「Yes/No だけ即答してください」と頼むより、まず3軸で評価させてから判定ルールに当てはめさせるほうが、出力が安定します。

3.2 ルーブリックを束ねる判定ロジック

3軸の値から最終判定を導く規則も、プロンプト内に書いています。

- トピック連続性 = high または medium
  かつ 話題転換シグナル = strong でない
  → YES(連なる会話)

- 応答性 = high
  → YES(応答・報告・承諾・お礼)

- トピック連続性 = low かつ 話題転換シグナル = strong
  → NO(完全な別トピック)

- トピック連続性 = low かつ 応答性 = low
  → NO(別の新規質問)

- 例外: 最後のメッセージが「ライセンス」「見積」「購入」「調達」など
  購買・商業的な文脈の続きであれば、技術キーワードを含んでいても YES

- 判定に迷う場合は YES を優先(= 起票しない側に倒す)

特に重要なのが最後の行です。

迷ったら YES(起票しない側)に倒す。

理由はシンプルです。

  • 誤起票は「なぜ拾われた?」と聞かれて信頼を失いやすい。
  • 取り逃しは別チャンネルや手動でフォローできる。

リカバリの効きやすい方向にデフォルトを倒すべき、という運用判断です。

3.3 思考は内部、出力は最小限に

このスレッド判定でも、Chain of Thought の手順をプロンプトに書いています。

【判定手順(思考手順、最終出力では省略可)】
1. 親メッセージの中心的な話題・目的を一言で要約
2. 最後のメッセージの中心的な話題・目的を一言で要約
3. 2つを比較してトピック連続性を評価
4. 最後のメッセージの応答性を評価
5. 話題転換シグナルの有無を評価
6. ルーブリックに従って総合判定

ただし出力は厳格に固定します。

判定: YES または NO
理由: 判定した理由を1-2文で簡潔に説明

「思考は内部、出力は最小限」というスタイルが、この判定では一番扱いやすい形でした。

メリットは3つです。

  1. トークンを節約できる
  • 思考全文を出させると、課金もログ量も膨らむ。
  1. 後段パースが安定する
  • 余計な前置きや Markdown 装飾が混ざらない。
  • 判定: 理由: を雑にスプリットするだけで取り出せる。
  1. 理由フィールドが運用上ちょうど良い粒度
  • 1〜2文で「なぜそう判定したか」がログに残る。
  • 振り返りや誤判定分析に十分使える。

3.4 Few-shot で C/D の境界を教え込む

ルーブリックだけだと、「同じ製品名が出てくれば連続」と過学習しがちです。そこで判定例をプロンプトに6パターン入れています。

判定
1AWS Lambda タイムアウト「確認取れました、ありがとうございます」YES(応答)
2Jamf Pro 20L 見積Jamf Connect Basic 20L 見積YES(同一領域の追加質問)
3Chrome 拡張機能の追加要求問題GWS からの削除・除外手順YES(同一スレッドの追加確認)
4Secure Boot 確認方法Okta 連携の LDAP エラーNO(完全な別トピック)
5ライセンス見積「Jamf Connect Basic は不要の認識でお間違いないでしょうか?」YES(購買相談の続き)
6AWS コスト最適化「別件で恐縮ですが、Terraform の state 破損の復旧方法を…」NO(明示的な話題転換)

意図的に並べているのは、似て非なる例の対比です。

  • 例3 と 例5 はどちらも「親の会話の延長」だが、技術相談 / 購買相談で文脈が違う。
  • 例4 と 例6 はどちらも「別トピックの新規質問」だが、明示的な転換表現の有無が違う。

「意味的にどう違うのか」をモデルに比較させたい例を Few-shot に並べておくのが、ルーブリック型プロンプトと相性が良かったです。

3.5 親リアクションをコンテキストとして渡す

最後に、入力のほうにも一工夫しています。

プロンプトの先頭に、親メッセージのリアクション状態を文章として埋め込みます。

  • 親にリアクションあり
    • 「親メッセージは既に技術的質問として起票済み(リアクションあり)です。」
  • 親にリアクションなし
    • 「親メッセージは技術的質問として起票されていません(リアクションなし)。非技術なやり取り、または技術的でない相談が続いている可能性があります。」

リアクションの有無は、システム側ですでに分かっている事実です。安価なヒントなので、活用しない手はありません。

これを判定材料の1つとして渡すことで、

  • 親が購買系で起票されていない場合 → 子も購買系の続きである可能性が高いと推論してくれる。
  • 親が起票済みの場合 → 別件かどうかをきちんと判定する方向に意識が向く。

「システムが知っていることはモデルにも教える」というのは、地味ですが効果が大きい工夫だと感じました。


4. 振り返りと学び

判定ロジックで効いた工夫を、もう一度整理しておきます。

4.1 設計面での効いた工夫

  • プロンプトを2本に分ける
    • 単発判定(is_technical_question)とスレッド判定(is_response_message)を分離。
    • 1本で全部やらせるより、はるかに安定する。
  • プロンプトの構成を固定する
    • 順序: 最重要ルール → 定義 → 判定手順 → Few-shot → 出力フォーマット
    • Haiku / Sonnet どちらでも崩れにくくなる。
  • ルーブリック方式で多次元評価
    • Yes/No の二値判定では曖昧になる「親の続き or 別件」のような連続的な判断に向く。
  • Chain of Thought は内部で走らせる
    • 出力は「判定 / 信頼度 / 理由」の最小フォーマット。
    • ログ・パース・コストのバランスが取れる。
  • 迷ったら起票しない側に倒す
    • プロンプト内に方針として明記。
    • 誤起票を減らし、運用側の信頼を得やすくなった。
  • システムの既知情報をモデルに渡す
    • 親リアクションの有無を文章として埋め込む。
    • 安価なヒントを判定文脈として活用できる。

4.2 LLM をプロダクションで使うときの所感

JSON 強制やツール呼び出しのような、構造化出力の手段はもちろん有効です。

ただ、社内ツール程度のサイズ感であれば、次の4つだけで十分実用に耐えます。

  1. プロンプトを段階的にしっかり書く
  2. ルーブリックで多次元評価させて判定ルールに当てはめる
  3. 思考は内部に閉じ込めて出力は最小に
  4. Few-shot で「境界例」を教え込む

このあと別の業務システムに LLM を組み込むときも、まずこの形をベースにして組み始めると思います。

この記事をシェア