0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

悩ましい営業メールを判定AI Jev で撃退した。2回聞いて揃ったときだけ機械が決める

0
Posted at

プレスリリースを出したあと、窓口のアドレスに売り込みとセミナー案内が増えました。
Gmail のフィルタと正規表現で振り分けていましたが、追いつかなくなったので、文意を読む判定を AI に回しました。
この記事は、その作りと、線の引き方を実測で決めた記録です。

調べた時点の結果だけ先に書きます。
2026年10月2日、受信箱の92通が、仕分けのあと2通になりました(残ったのは請求書と、返信が必要な1通)。

正規表現で何が起きていたか

受信箱には、性質の違う4種類のメールが混ざっていました。

・人が個別に書いた返事(返信すべきもの)
・問い合わせフォームの自動返信の控え
・宣伝・セミナー案内・資料配布(逆営業)
・宛先不明の戻り

正規表現で詰まったのは、同じ言い回しが別の種類に出てくることです。

・「お問い合わせありがとうございます」は、自動返信にも人の返事にも出る
・「セミナー」で弾くと、本物の問い合わせに同じ語があったときに取りこぼす
・「見送り」をお断りとして送信禁止にしたら、「今回は見送り」という余地を残した返事まで止めてしまった

最後のものは実害が出ました。
言い回しでは「今回は」と「今後は」の違いを拾い切れません。

差出人で弾けない売り込みもあります。
提案書を Google ドキュメントに書いてこちらのアドレスへ共有し、Google の共有通知として届けるもの。
Zoom のウェビナーに勝手に招待し、Zoom の共通アドレスから招待状を届けるもの。
どちらも差出人は Google や Zoom なので、差出人で迷惑メールにすると、本当に必要な共有や招待まで止まります。
本文を読んで「誰が、何のために共有・招待してきたか」を見ないと分けられません。

判定AIに聞く

使ったのは TypeSafe AI の Jev です。
文章を生成するのではなく、渡した文章に対して「はい」である確率(noul)や、選択肢ごとの確率を返します。
1回の要求に複数の問いを入れられ、問いはそれぞれ独立に評価されます。

1通に対して聞いたのは5問です。
費用の大半は本文を送る分なので、問いは1回の要求にまとめます。

const runs = await askRepeated(`差出人: ${mail.from}\n件名: ${mail.subject}\n\n${body}`, {
  human: {
    type: 'noul',
    instructions: 'このメールは、担当者がこちらの案内を読んだうえで個別に書いた返事である。フォームの送信時に機械が自動で送る受付の控えや、宣伝・メールマガジンは当たらない。',
  },
  needReply: {
    type: 'noul',
    instructions: 'このメールに対して、こちらから返信するのが礼儀上または商談上ふさわしい。自動の受付控え、宣伝、メールマガジンは当たらない。',
  },
  counterSales: {
    type: 'noul',
    instructions: '差出人は、こちらの案内に答えるのではなく、自社の商品・サービス・セミナーなどをこちらに売り込もうとしている。',
  },
  meetingRequest: {
    type: 'noul',
    instructions: '差出人は、オンラインか対面かを問わず、打ち合わせ・面談・訪問の場を設けたいと求めている。',
  },
  stopRequest: {
    type: 'noul',
    instructions: '差出人は、今後こちらから連絡を送らないでほしいと、はっきり求めている。「今回は見送る」のように、今回について断っているだけのものは当たらない。',
  },
}, 2);

askRepeated は同じ要求を2回投げるだけの薄い関数です(社内の共通ラッパーに置いています)。
stopRequest の説明に「今回は見送る」は当たらないと書いたのが、正規表現で起きた事故への直接の対策です。

同じ入力に同じ答えは返らない

確率は聞くたびに少し揺れます。
別の用途で、同じ3問を同じ69件に3回投げて揺れ幅を測ったところ、平均で 0.013〜0.041、最大で 0.15 動きました。
3回とも同じ値だったのは、69件中2〜11件だけです。

線のすぐ近くの答えは、聞き直すと反対側へ動きます。
そこで、2回聞いて、両方が線の同じ側に落ちたときだけ機械が決め、割れたら人が見る側へ回します。

const pick = (key) => {
  const g = agreed(runs.map((r) => gateNoul(r.answers[key], LINES[key])));
  return g.act === 'auto' ? (g.value ? 'yes' : 'no') : 'unsure';
};

gateNoul は確率を線と比べて「はい」「いいえ」「決めない」の3つに分け、agreed は全回が同じ結論のときだけ auto を返します。

線は既定値ではなく実測で引く

ラッパーの既定は、0.9 以上で「はい」、0.1 以下で「いいえ」、その間は人です。
これを受信箱の26通にそのまま当てると、ほぼ全部が「決めきれない」になりました。

26通の実測値はこう分かれました。

・返信の要否:人の返事は 0.61〜0.93、宣伝・セミナー案内・自動返信は 0.05〜0.49
・今後送るな:26通の最大が 0.19

これを見て、問いごとに線を引き直しました。

const LINES = {
  human: { yes: 0.5, no: 0.3 },
  needReply: { yes: 0.6, no: 0.599 },
  counterSales: { yes: 0.6, no: 0.599 },
  meetingRequest: { yes: 0.6, no: 0.599 },
  stopRequest: { yes: 0.7, no: 0.3 },  // 送信禁止は取り返しがつかないので高めに
};

一緒に投げる問いの組を変えると、同じ問いでも値が動きます。
線は「その問いの組で測った値」なので、問いを足したら測り直します。

AIに聞かないもの

決まるものは先にコードで決めます。
AIに回すのは、文意を読まないと決められないものだけです。

・宛先不明の戻りは、差出人(mailer-daemon、postmaster)で分かる
・請求・支払い・契約の連絡は、判定にかかわらず受信箱に残す
・判定が取れなかったとき(API の失敗など)は、送信禁止にせず人が見る側へ倒す

最後の点が大事です。
「判定できなかった」と「判定の結果、問題なし」は別物で、前者を素通しにすると事故になります。

振り分けの結果

2026年10月2日に流した結果です。

・返信不要(宣伝・案内):約70通
・自動返信の控え:約15通
・宛先不明:3通
・受信箱に残したもの:2通

いまは5分おきに同じ処理が回っています。
同じメールを何度も聞かないよう、判定結果はメールの ID ごとに手元に控えています。

Jev の技術的な中身

ここからは、使ったモデルそのものの話です。
数字と仕様は、調べた時点(2026年10月2日)の公式資料から取っています。

文章を書かない「System One」モデル

Jev は TypeSafe AI の主力モデルで、同社が System One と呼ぶ種類の最初のモデルです。
LLM と同じく自然文を読みますが、文章は生成しません。
返すのは、こちらが決めた型どおりの答えと確率だけです。
名前は、カーネマンの『ファスト&スロー』の「システム1(速い直感の思考)」から来ています。

そのため、LLM に判定させるときの「出力を JSON にさせて、壊れていたら直す」という後処理が要りません。
返ってきた値に、そのまま if を書けます。

問いの型は3つ

型 聞けること 返るもの
choice 用意した選択肢から1つ選ぶ choice、選択肢ごとの確率、confidence
score 段階の説明を並べて位置を付ける(2〜10段階) score(段階の間の値もとる)、段階ごとの確率、confidence
noul はい/いいえ noul(「はい」である確率 0〜1)

今回の5問は、すべて noul です。
noul には confidence が付かないので、確率そのものに線を引いて「はい」「いいえ」「決めない」に分けています。
choice と score には、確率の分布の偏りから計算した confidence(0〜1)が付き、分布が平らなほど低くなります。

1回の呼び出しで、問いは並列・独立に評価される

1回の呼び出しに、state(判定の対象)を1つと、問いを何個でも入れられます。
問いはすべて同じ state に対して、並列に、互いに独立して評価されます。
ある問いの答えが、別の問いの答えに影響することはありません。
資料では、多くの呼び出しが約100ミリ秒で返るとされています。
問いを足しても応答時間はほとんど変わらないので、1通に5問を聞いても待ち時間は気になりません。

問いの id(今回なら counterSales など)はモデルには送られず、答えを引くための名前としてだけ使われます。
モデルが読むのは instructions(noul なら必要に応じて criteria の true/false の説明)だけなので、判断の境目は instructions に書き込みます。
今回 stopRequest の説明に「今回は見送る」は当たらないと書いたのは、このためです。

state は文字列でも JSON でもよい

state には、文字列、JSON オブジェクト、テキストの配列を渡せます。
今回は差出人・件名・本文を1本の文字列にしましたが、項目ごとに名前を付けた JSON にすることもできます。
入力はテキストだけで、画像や音声は受け付けません。

料金と上限

調べた時点の Jev 1.13 の料金は、入力100万トークンあたり 0.042 ドルで、出力は無料です。
本文を送る量で決まるので、問いを1回の呼び出しにまとめるほど安くなります。
上限は、毎秒10万トークン・毎秒40リクエストで、超えると 429 が返ります。
1回あたりの上限は 64k トークン(state と最も長い問いの合計は 32k まで)です。

日本語で使うときの注意

公式資料には、学習の主言語は英語で、日本語を含む CJK の言語は扱えるものの精度は英語に及ばない、と明記されています。
自分の文章で試してから使うこと、確率の扱いに注意することが勧められています。
今回、既定の線(0.9/0.1)のままでは日本語のメールのほとんどが「決めきれない」になり、受信箱の実測で線を引き直したのは、この性質に合わせた対応です。

モデルの版を固定する

jev-latest は、新しい版が出ると指す先が変わる別名です。
線を特定の版で測って決めたなら、jev-1.13.0 のように版を指定し、新しい版へは自分で測り直してから移るよう勧められています。
応答の model には実際に答えた版が入るので、ログに残しておくと、答えが変わったときに原因を追えます。

仕組みについての資料

まとめ

・言い回しで決めるルールは、同じ語が別の種類に出てくると破綻する
・判定AIには、問いを1回の要求にまとめて投げる
・2回聞いて揃ったときだけ機械が決める。割れたら人
・線は既定値ではなく、自分のデータで測って引く。取り返しのつかない判定ほど高くする
・コードで決まるものは AI に聞かない
・日本語で使うなら、線は必ず自分のデータで引き直し、モデルの版を固定する

使った判定AIの資料はこちらです。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?