Microsoft-Decision-1とは? Jevと同じ形の判定専用AIをAzureで試す【C#実践アプリ開発ラボ】
「C#実践アプリ開発ラボ」です。今回は、Azureで使えるようになった判定専用のモデル、Microsoft-Decision-1を試します。
Decision-1は、文章を作りません。決めておいた選択肢ごとに、確率だけを返します。判定専用のモデル「Jev」と、同じようなAPIの形です。
以下のような方に役立つ内容となっています。
この記事が役に立つ方
- 汎用のモデルに文章で答えさせて、分類やOK/NGを取り出すのが面倒だと感じている
- Decision-1という名前を見かけて、何ができるのか知りたい
- Jevのような判定専用のモデルを、会社で使っているAzureの中で使いたい
題材は、問い合わせの振り分けです。サポート窓口に届いた問い合わせを判定し、担当や人へ振り分けます。
コードと実行結果の一式はGitHubにあります。
AppLab/20261021_Decision1 at main · prota-p/AppLab
答えの形を先に決めて渡し、確率だけを返してもらう。シンプルですが、業務アプリに組み込みやすいAIです。
Microsoft-Decision-1とは?
まず、Decision-1が何をするAIで、どんな場面に向くのかを整理します。
汎用のモデルとの違い
図1は、同じ問い合わせを、2つのモデルにそれぞれの得意な仕事で渡した例です。

GPTなどの汎用のモデルは、文章を作ります。チケットの件名のように、答えの形が決まっていない仕事に向きます。
Decision-1などの判定専用のモデルは、渡した選択肢から選びます。担当を請求・技術・契約・その他から選ばせると、選択肢ごとの確率が返ります。
この問い合わせは、請求が78%、契約が22%でした。答えだけでなく、どれくらい迷ったかも数字で分かります。
なお、Decision-1は判定の理由を返しません。返るのは、問いごとの確率だけです。
向いている場面
使い分けは、次の表のとおりです。
| 返すもの | 向いている場面 | |
|---|---|---|
| 汎用のモデル(GPTなど) | 文章(指示すればJSONも) | 要約・件名・返信の下書きなど、答えの形が決まっていない仕事 |
| 判定専用のモデル(Decision-1・Jevなど) | 決めた選択肢ごとの確率 | 振り分け・OK/NGなど、選択肢が決まっていて、毎回走り、理由はいらない判定 |
汎用のモデルは、文章を作る仕事に使います。形式を指示すれば、JSONで答えさせることもできます。
判定専用のモデルは、選択肢が決まった判定に使います。届くたびに毎回走り、理由はいらない判定に向きます。
判定なら、汎用のモデルにJSONで答えさせてもできるよね?
できます。ただ、判定専用のモデルは文章を作らず、出力は数トークンだけ。そのぶん速く、トークンの消費も少なく済みます。
汎用のモデルにJSONで答えさせる方法は、WinForms編の第13回で扱っています。
APIの形 ── 状況と問いを送り、確率を受け取る
APIはシンプルです。判定する状況(state)と、問い(questions)を送ります。
{
"model": "decision-1",
"state": "勤怠管理クラウドのサポート窓口に、問い合わせフォームから次の本文が届きました。\n\n解約を考えています。今月分の料金は日割りになりますか?",
"questions": {
"team": {
"type": "choice",
"instructions": "この問い合わせを担当する窓口を選んでください。",
"criteria": {
"請求": "請求書・支払い・料金の計算",
"技術": "ログイン・操作方法・不具合・データの出力",
"契約": "プランの変更・解約・契約内容",
"その他": "どれにも当てはまらない(営業や宣伝の売り込みを含む)、または本文から担当を決められない"
}
}
}
}stateには、判定したい文章を入れます。ここでは、届いた問い合わせの本文です。
questionsには、問いを書きます。この例は、担当を4つの選択肢から選ばせる問いです。
返ってくるのは、問いと同じキー(team)の答えです。選ばれた選択肢と、選択肢ごとの確率が入っています。
{
"answers": {
"team": {
"choice": "請求",
"probabilities": { "請求": 0.73, "契約": 0.27, "技術": 0.00, "その他": 0.00 }
}
}
}ちなみに、判定専用のモデル「Jev」のAPIも、同じような形をしています。
作る ── 問い合わせ1件を振り分ける
ここからは、Decision-1で問い合わせを振り分けてみます。
勤怠管理のクラウドサービス(SaaS)を売る、架空の会社を考えます。お客さまから届いた問い合わせを読み、行き先を決めます(図2)。

担当がはっきりしたものは担当の受付箱へ、迷うものは人が振り分けます。不満が強いものは、上長にも知らせます。
今回作るのは、図3の小さなアプリです。問い合わせ1件の本文と問いをDecision-1に送り、判定と振り分け先を画面に出します。

フォームや受付箱は作りません。振り分け先は、画面に文字で出すだけです。
次の4つの手順で進めます。
作る手順
- Azureに用意する
- 問いを書く
- Decision-1を呼ぶ
- 答えを読む
【1】Azureに用意する
Microsoft Foundryのリソースを作り、Decision-1(モデル名Microsoft-Decision-1)をデプロイします。Bicepと手順はREADMEにあります。
勘所は、次の表の3つです。
| 項目 | 内容 |
|---|---|
| リージョン | 東日本(japaneast)・西日本(japanwest)でも使える(2026年10月17日確認) |
| 料金 | 使った分だけの課金。Microsoftの発表では、入力100万トークンあたり0.042ドル、出力は無料 |
| 上限 | 1分あたりの回数に上限がある。今回は50回/分にした |
日本のリージョンでも使えます。提供リージョンは公式の表で確かめてください。
料金は、Microsoftの発表では入力だけにかかります。今回の1件は入力が約170トークンなので、1万件でも約0.07ドルです。
上限は、1分あたりの回数で、デプロイのときに決めます。たくさん流すときは、上限を超えないように間を空けて送ります。
【2】問いを書く
Decision-1への問いは、答えの型を選んで書きます。型は、次の表の3つです。
| 型 | 返るもの | この記事での問い |
|---|---|---|
| choice | 選ばれた選択肢と、選択肢ごとの確率 | 担当(請求・技術・契約・その他) |
| score | 段階の番号を、確率で重み付けした平均 | 緊急度(低・中・高 → 0〜2) |
| noul | 「はい」の確率(0〜1) | 不満や苦情を伝えているか |
choiceは、決めた選択肢から1つを選ぶ型です。今回は、担当の振り分けに使います。
scoreは、順序のある段階で評価し、段階の番号の重み付け平均を返す型です。今回は緊急度(低=0・中=1・高=2)に使い、0.9なら「ほぼ中」です。
noulは、はい・いいえで答える問いに、「はい」の確率を返す型です。今回は、不満や苦情を伝えているかに使います。
3つの問いは、JSONで書きます。キー(team・urgency・complaint)は自分で決める名前で、答えも同じキーで返ります。
{
"team": {
"type": "choice",
"instructions": "この問い合わせを担当する窓口を選んでください。",
"criteria": {
"請求": "請求書・支払い・料金の計算",
"技術": "ログイン・操作方法・不具合・データの出力",
"契約": "プランの変更・解約・契約内容",
"その他": "どれにも当てはまらない(営業や宣伝の売り込みを含む)、または本文から担当を決められない"
}
},
"urgency": {
"type": "score",
"instructions": "この問い合わせの緊急度を評価してください。",
"criteria": ["低", "中", "高"]
},
"complaint": {
"type": "noul",
"instructions": "この問い合わせは、当社への不満や苦情を伝えていますか?"
}
}担当には「その他」を入れました。choiceは、どれにも当てはまらなくても、必ずどれかを選ぶからです。
公式ドキュメントも、無理に選ばせたくないときは「判断できない」のような選択肢を入れるよう勧めています。
【3】Decision-1を呼ぶ
Decision-1には、判定する状況をstateとして渡します。本文の前に、どこに届いた問い合わせかを1文書きます。
namespace InquiryRouter;
// 問い合わせフォームから届いた1件。判定に使うのは本文だけ
record Inquiry(int Id, string Body)
{
// Decision-1 に渡す state。どこに届いた本文かを前に書く。会社名・氏名・メールは判定に要らないので入れない
public string ToState() =>
$"勤怠管理クラウドのサポート窓口に、問い合わせフォームから次の本文が届きました。\n\n{Body}";
}今回の判定は本文だけで足りるので、会社名・氏名・メールは送りません。外部のAIに渡す情報は、判定に要るものだけにします。
契約プランなど、お客さまの情報で判定を変えたいときは、要る項目だけをstateに足します。
DecisionClientは、エンドポイントとデプロイ名を受け取ります。エンドポイントは、【1】で作ったリソースのURL(https://<リソース名>.services.ai.azure.com)です。
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using Azure.Core;
namespace InquiryRouter;
// Decision-1 を呼ぶ。state に状況、questions に問いを入れて送り、答えの JSON を返す
sealed class DecisionClient(string endpoint, string deployment, TokenCredential credential)
{
static readonly HttpClient http = new() { Timeout = TimeSpan.FromSeconds(60) };
public static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web) {
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
AccessToken token;
public async Task<JsonObject> AskAsync(string state, JsonObject questions)
{
// アクセストークン(認証の通行証)の取得は1〜2秒かかるので、期限が近づくまで使い回す(順に呼ぶ前提の簡易な作り)
if (token.ExpiresOn < DateTimeOffset.UtcNow.AddMinutes(5))
token = await credential.GetTokenAsync(
new TokenRequestContext(["https://cognitiveservices.azure.com/.default"]), default);
using var request = new HttpRequestMessage(HttpMethod.Post,
endpoint.TrimEnd('/') + "/providers/microsoft/v1/systemone");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Token);
// JsonContent はContent-Lengthを付けずに送るため、Decision-1 に 400 で断られる。文字列にしてから送る
request.Content = new StringContent(JsonSerializer.Serialize(new { model = deployment, state, questions }, Json),
System.Text.Encoding.UTF8, "application/json");
using var response = await http.SendAsync(request);
if (!response.IsSuccessStatusCode) // 400 のときは、問いの書き方の誤りが本文に書かれている
throw new HttpRequestException($"{(int)response.StatusCode}: {await response.Content.ReadAsStringAsync()}");
return (await response.Content.ReadFromJsonAsync<JsonObject>())!;
}
}呼び出しは、HttpClientでPOSTするだけです。送り先は、エンドポイントの後ろに/providers/microsoft/v1/systemoneを付けたURLです。
送る本文は、model(デプロイ名)・state・questionsの3つです。
補足:JsonContentでは送れない
JsonContentで送ると、Content-Lengthが付かず、400で断られました(2026年10月時点)。文字列にして、StringContentで送ります。
認証は、APIキーではなくEntra IDです。az loginしたアカウントで、アクセストークンをもらって送ります。
AzureCliCredentialは、裏でAzure CLIを呼んでアクセストークンを取るため、1〜2秒かかります。そこで、期限が近づくまで使い回します。
AskAsyncは、アクセストークンの取得とDecision-1への送信を順に行います。1回目は、アクセストークンの取得を含めて約2秒でした。
2回目からは、Decision-1へ送って答えを受け取るまでの0.2〜0.3秒だけです(日本から東日本リージョン、通信の往復を含む)。
1回0.2〜0.3秒なら、問い合わせが届くたびに判定しても待たされないね。
補足:本番で動かすとき
AzureCliCredentialは手元の開発用で、本番ではマネージドIDなどを使います。アクセストークンの使い回しも、順に呼ぶ前提の簡易な作りです。
【4】答えを読む
stateが次のときの出力を見てみます。問いは、【2】の3つ(担当・緊急度・不満)です。
勤怠管理クラウドのサポート窓口に、問い合わせフォームから次の本文が届きました。
解約を考えています。今月分の料金は日割りになりますか?答えは、問いと同じキーで返ります。先ほどのstateを送ったときの、実際の出力です。
{
"model": "microsoft-decision-1",
"answers": {
"complaint": {
"noul": 0.0075772716733891144,
"type": "noul"
},
"team": {
"choice": "請求",
"confidence": 0.7025085586777275,
"probabilities": {
"その他": 0.00042968088067333216,
"契約": 0.22258025977083654,
"技術": 0.0001086403401944728,
"請求": 0.7768814190082957
},
"type": "choice"
},
"urgency": {
"confidence": 0.9899083973989802,
"legend": {
"0": "低",
"1": "中",
"2": "高"
},
"probabilities": {
"0": 0.9932722649326535,
"1": 0.006692615428002947,
"2": 0.00003511963934343569
},
"score": 0.006762854706689818,
"type": "score"
}
},
"usage": {
"input_tokens": 169,
"output_tokens": 3
}
}teamは、選ばれた担当(choice)と、担当ごとの確率(probabilities)です。請求が78%、契約が22%でした。
confidenceも返りますが、公式ドキュメントに定義を見つけられなかったので、この記事では使いません。
urgencyのlegendは、段階の番号と名前の対応(0=低・1=中・2=高)です。probabilitiesは段階ごとの確率で、低が99%でした。
scoreは、番号を確率で重み付けした平均で、0.01です。ほぼ「低」と読めます。
complaintのnoulは0.008で、不満はほとんどありません。
入力は169トークン、出力は3トークンでした。文章を作らないので、出力はごくわずかです。
アプリでは、振り分けに使う4つの値をJudgmentというrecordにまとめます。担当とその確率、緊急度、不満です。
using System.Text.Json.Nodes;
namespace InquiryRouter;
// Decision-1 の答えから、振り分けに使う値を取り出したもの
sealed record Judgment(string Team, double TeamProbability, double Urgency, double Complaint)
{
public static Judgment From(JsonObject response)
{
var answers = response["answers"]!;
// choice:選ばれた選択肢と、選択肢ごとの確率
var team = answers["team"]!["choice"]!.GetValue<string>();
var teamProbability = answers["team"]!["probabilities"]![team]!.GetValue<double>();
// score:段階の番号(低=0・中=1・高=2)を確率で重み付けした平均
var urgency = answers["urgency"]!["score"]!.GetValue<double>();
// noul:問いに「はい」と答える確率
var complaint = answers["complaint"]!["noul"]!.GetValue<double>();
return new(team, teamProbability, urgency, complaint);
}
public override string ToString() =>
$"担当={Team}({TeamProbability:P0}) 緊急度={Urgency:F2} 不満={Complaint:P0}";
}Fromは、レスポンスのJSONから4つの値を取り出し、Judgmentにして返します。
試す ── 問い合わせ7件を振り分ける
次は、7件の問い合わせをそれぞれ3回判定しました(2026年10月17日、東日本リージョン)。
結果は次の表のとおりです。値は3回の範囲で、振り分け先は3回とも同じでした。
| 問い合わせ | 担当(確率) | 緊急度 | 不満 | 振り分け先 |
|---|---|---|---|---|
| #1 先月分の請求書を、PDFで再発行していただけますか。 | 請求(100%) | 0.00 | 0% | 請求の受付箱 |
| #2 今朝から社員全員がログインできず、打刻ができません。 | 技術(100%) | 2.00 | 25〜29% | 技術の受付箱 |
| #3 来月から10名増えるので、プランを上げたいです。 | 契約(100%) | 0.02〜0.04 | 0% | 契約の受付箱 |
| #4 解約を考えています。今月分の料金は日割りになりますか? | 請求(62〜78%) | 0.01 | 1% | 人が振り分ける |
| #5 CSVで出した金額が、請求書の金額と合いません。 | 請求(100%) | 0.87〜0.90 | 22〜27% | 請求の受付箱 |
| #6 何度問い合わせても返事がありません。どうなっているんですか。 | その他(100%) | 0.96〜0.97 | 100% | 人が振り分ける+上長に知らせる |
| #7 貴社サイトの集客改善について、ご提案させてください。 | その他(100%) | 0.00 | 0% | 人が振り分ける |
振り分け先は、この章の後半で説明するルールで決めたものです。担当がはっきりしていれば担当の受付箱へ、迷えば人へ、不満が強ければ上長にも、という形です。
はっきりした問い合わせは、迷わず振り分ける
#1〜#3は、担当が100%で決まりました。#2は社員全員がログインできないので、緊急度が2.00(高)です。
| 問い合わせ | 担当(確率) | 緊急度 | 不満 | 振り分け先 |
|---|---|---|---|---|
| #1 先月分の請求書を、PDFで再発行していただけますか。 | 請求(100%) | 0.00 | 0% | 請求の受付箱 |
| #2 今朝から社員全員がログインできず、打刻ができません。 | 技術(100%) | 2.00 | 25〜29% | 技術の受付箱 |
| #3 来月から10名増えるので、プランを上げたいです。 | 契約(100%) | 0.02〜0.04 | 0% | 契約の受付箱 |
#5は、データの出力(技術)か金額(請求)か迷いそうですが、請求100%でした。緊急度は「ほぼ中」です。
| 問い合わせ | 担当(確率) | 緊急度 | 不満 | 振り分け先 |
|---|---|---|---|---|
| #5 CSVで出した金額が、請求書の金額と合いません。 | 請求(100%) | 0.87〜0.90 | 22〜27% | 請求の受付箱 |
迷いは確率に表れる
#4は、解約(契約)と料金(請求)の両方にまたがります。担当は請求で、確率は62〜78%、残りはほぼ契約でした。
| 問い合わせ | 担当(確率) | 緊急度 | 不満 | 振り分け先 |
|---|---|---|---|---|
| #4 解約を考えています。今月分の料金は日割りになりますか? | 請求(62〜78%) | 0.01 | 1% | 人が振り分ける |
同じ本文でも、確率は回によって少し動きます。1回の値は、目安として読みます。
決められないものは「その他」へ
#6は、何についての問い合わせか書かれていません。担当は「その他」で、不満は100%でした。
| 問い合わせ | 担当(確率) | 緊急度 | 不満 | 振り分け先 |
|---|---|---|---|---|
| #6 何度問い合わせても返事がありません。どうなっているんですか。 | その他(100%) | 0.96〜0.97 | 100% | 人が振り分ける+上長に知らせる |
#7は、営業の売り込みです。「その他」の説明に売り込みを含めておいたので、そのとおりに落ちました。
| 問い合わせ | 担当(確率) | 緊急度 | 不満 | 振り分け先 |
|---|---|---|---|---|
| #7 貴社サイトの集客改善について、ご提案させてください。 | その他(100%) | 0.00 | 0% | 人が振り分ける |
確率で振り分ける
振り分けのルールは、図4のとおりです。

担当が「その他」か、担当の確率が90%未満なら、人が振り分けます。それ以外は、担当の受付箱へ送ります。
これとは別に、不満が50%以上なら、上長にも知らせます。
このルールは、RouterのRouteメソッドに書いています。
namespace InquiryRouter;
// 判定から振り分け先を決める
static class Router
{
// しきい値は7件の実測を見て置いた例の値。自社の問い合わせで決める
const double TeamThreshold = 0.9;
const double ComplaintThreshold = 0.5;
public static string Route(Judgment judgment)
{
// 担当が「その他」か、担当の確率が低ければ人が振り分ける
var destination = judgment.Team == "その他" || judgment.TeamProbability < TeamThreshold
? "人が振り分ける"
: $"{judgment.Team}の受付箱";
// 不満が強ければ、上長にも知らせる
return judgment.Complaint >= ComplaintThreshold
? $"{destination}+上長に知らせる"
: destination;
}
}しきい値は、7件の結果を見て置いた例の値です。実際には、自社の問い合わせで確かめて決めます。
公式ドキュメントも、最上位の確率が低いときは、人や別のモデルへ回す使い方を挙げています。
緊急度は、今回は画面に出すだけで、振り分けには使っていません。
迷ったものだけ人が見ればいいから、全部を人が読むより、ずっと楽になるね。
気をつけること ── 問いどうしは独立
3つの問いは1回で送りましたが、答えは問いごとに別々に出ます。ある問いの答えを、別の問いが見ることはありません。
そのため、「担当が請求なら、返金の依頼かも判定する」のように、答えを受けて次の問いを変えることはできません。
そうしたいときは、多段にします。1回目で担当を判定し、請求だったら2回目で「返金の依頼か」を送ります。
答えを組み合わせるだけなら、アプリの側で行います。今回のRouteも、担当と不満をアプリで組み合わせています。
もう1つ、公式ドキュメントは、言い回しや選択肢の順番で値が変わりうること、人に関わる重い判断の唯一の根拠にしないことを挙げています。
まとめ
今回は、Decision-1で問い合わせを振り分けました。重要なポイントは以下です。
今回のポイント
- Decision-1は、文章を作らず、決めた選択肢ごとの確率だけを返す判定専用のモデル。出力がわずかで、応答は今回1回0.2〜0.3秒
- 問いは3つの型(choice・score・noul)で書く。choiceには「その他」の逃げ道を用意する
- 確率を見て、迷ったもの(確率が低い・「その他」)だけ人が見る。どこで線を引くかは、自社の問い合わせで試して決める
- 問いどうしは独立。答えを組み合わせるのはアプリの側で、答えで次の問いを変えるなら多段にする
Decision-1は振り分けのほかに、「文章にAIへの指示が紛れ込んでいないか」の判定にも使えます。
この使い方は、シリーズ「プロンプトインジェクションを門番で防ぐ」で扱う予定です。
判定だけなら、文章を作らせなくていい。答えの形を先に決めて、確率で受け取る。それがDecision-1の使いどころです。
引き続き、C#でいろいろな技術を試していきましょう!




