LLM の動き〜Structured Output確認〜
この記事では、C# を使用して、LLM(大規模言語モデル)の Structured Output 機能を実装し、HTTP通信をリアルタイムで観察しながら、自然言語の問い合わせを固定スキーマの JSON に変換する仕組みを理解します。
想定読者
- C# 開発者で LLM の Structured Output 実装に興味がある方
- LLMの応答を JSON 形式で安定して受け取りたい方
本記事で実装すること
以下の Structured Output シナリオを観察できるプログラムを実装します:
-
Structured Output - リクエストに
json_schemaを指定して出力形式を制約 - C# 型への変換 - 返ってきた JSON 文字列をクラスへデシリアライズ
- アプリ側の検証 - ローカルモデルの揺らぎに備えて最低限の妥当性チェックを実施
処理フロー図
環境構成
システム要件
| 項目 | 仕様 |
|---|---|
| OS | Windows 11(記事執筆環境) |
| .NET | net10.0 |
| 主要パッケージ | System.Net.Http(標準) |
| LLM 実行環境 | LM Studio(ローカルホスト) |
Structured Output とは
Structured Output は、LLM に「自然文ではなく、このスキーマに一致する JSON を返してほしい」と機械可読な形式で指示する仕組みです。
単にプロンプトで「JSONで返してください」と書く方法でも動くことはありますが、それはあくまでモデルの解釈に依存します。Structured Output では、リクエストボディに response_format を設定して JSON の形を API レベルで明示します。対応しているモデルであれば、より安定して形式化された JSON を受け取れるようになります。
基本的な動作フロー
- クライアント側で JSON Schema を定義 - 必須キー、型、列挙値を決める
-
response_formatを付与して送信 -type: "json_schema"とスキーマ本体を送る -
LLM が JSON を生成 - 返答は
message.contentに JSON 文字列として入る -
アプリ側でデシリアライズ -
System.Text.Jsonで C# クラスへ変換 - 必要なら追加検証 - 形式は合っていても意味が怪しい場合があるため、業務ルールを別で確認する
システムプロンプトでの指定との違い
- システムプロンプトでの指定: JSON らしい出力を要求するだけでキーや型の厳密さは弱い
- Structured Output: スキーマでキー、型、列挙値、必須項目を明示できる
今回の実装では、JSON Schema 本体はチャット履歴には含めず、response_format として別枠で送っています。 ただし、出力品質を安定させるため、summary や affectedUsers のような一部の項目制約は システムプロンプト に自然言語でも補助的に書いています。
つまり役割分担として、構造の本体は response_format、補助的なニュアンス指定は システムプロンプト となっています。
重要なポイント
- 形の保証と意味の保証は別: スキーマ通りの JSON でも、中身が正しいとは限らない
- 再試行が有効: 形式が揺れる場合、アプリ側で検証して補正リクエストを出すと改善しやすい
実装コード解説
ソースコード全体
今回のサンプルは StructuredOutputApp というコンソールアプリとして作成しました。
1. 共有コンポーネント: HttpClientLoggingHandler.cs
HTTP通信の全トラフィック(リクエスト/レスポンス)を可視化するカスタムハンドラーです。今回は response_format がそのまま見えるのがポイントです。
HttpClientLoggingHandler.cs
using System;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
public class HttpClientLoggingHandler : HttpClientHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine(">>> HTTP リクエスト <<<");
Console.ResetColor();
Console.WriteLine($"メソッド: {request.Method}");
Console.WriteLine($"URL: {request.RequestUri}");
if (request.Headers.Any())
{
Console.WriteLine("ヘッダー:");
foreach (var header in request.Headers)
{
Console.WriteLine($" {header.Key}: {string.Join(", ", header.Value)}");
}
}
if (request.Content is not null)
{
Console.WriteLine("リクエストボディ:");
var contentText = await request.Content.ReadAsStringAsync(cancellationToken);
Console.WriteLine(FormatJson(contentText));
request.Content = new StringContent(contentText, Encoding.UTF8, "application/json");
}
Console.WriteLine();
var response = await base.SendAsync(request, cancellationToken);
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("<<< HTTP レスポンス >>>");
Console.ResetColor();
Console.WriteLine($"ステータス: {response.StatusCode}");
if (response.Headers.Any())
{
Console.WriteLine("ヘッダー:");
foreach (var header in response.Headers)
{
Console.WriteLine($" {header.Key}: {string.Join(", ", header.Value)}");
}
}
if (response.Content is not null)
{
Console.WriteLine("レスポンスボディ:");
var contentText = await response.Content.ReadAsStringAsync(cancellationToken);
Console.WriteLine(FormatJson(contentText));
response.Content = new StringContent(contentText, Encoding.UTF8, "application/json");
}
Console.WriteLine();
return response;
}
private static string FormatJson(string json)
{
if (string.IsNullOrWhiteSpace(json))
{
return "(empty)";
}
try
{
using var document = JsonDocument.Parse(json);
return JsonSerializer.Serialize(document.RootElement, new JsonSerializerOptions
{
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
});
}
catch
{
return json;
}
}
}
コードのポイント:
-
SendAsyncをオーバーライドして HTTP 通信を丸ごと観察 - JSON を整形して表示するため、
response_formatの中身まで見やすい - ボディを読んだ後に
StringContentで詰め直しているため、その後の処理でも再利用できる
2. Structured Output デモ: Program.cs
問い合わせ文を JSON Schema に沿って構造化し、C# クラスへ変換する本体です。
Program.cs
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
sealed class InquiryTriageResult
{
[JsonPropertyName("summary")]
public string Summary { get; set; } = string.Empty;
[JsonPropertyName("category")]
public string Category { get; set; } = string.Empty;
[JsonPropertyName("priority")]
public string Priority { get; set; } = string.Empty;
[JsonPropertyName("affectedUsers")]
public int AffectedUsers { get; set; }
[JsonPropertyName("needsSameDayResponse")]
public bool NeedsSameDayResponse { get; set; }
[JsonPropertyName("escalationTarget")]
public string EscalationTarget { get; set; } = string.Empty;
}
sealed class StructuredOutputResponse
{
public InquiryTriageResult Result { get; init; } = new();
public string RawJson { get; init; } = string.Empty;
public string FinishReason { get; init; } = string.Empty;
}
class Program
{
private static readonly string[] AllowedCategories = ["account", "billing", "bug", "feature_request", "other"];
private static readonly string[] AllowedPriorities = ["low", "medium", "high", "critical"];
private static readonly JsonSerializerOptions DeserializeOptions = new()
{
PropertyNameCaseInsensitive = true
};
static async Task Main(string[] args)
{
Console.WriteLine("=== Structured Output デモ ===\n");
var loggingHandler = new HttpClientLoggingHandler();
var endpoint = "http://localhost:1234";
var modelName = "openai/gpt-oss-20b";
Console.WriteLine($"エンドポイント: {endpoint}");
Console.WriteLine($"モデル: {modelName}\n");
var userMessage = "社内ポータルでパスワード再設定メールが届きません。午前中から3人が同じ症状で、今日中に経費精算を締めたいです。";
Console.WriteLine("--- ユーザー入力 ---\n");
Console.WriteLine($"{userMessage}\n");
try
{
using var httpClient = new HttpClient(loggingHandler)
{
BaseAddress = new Uri(endpoint)
};
Console.WriteLine("--- Structured Output リクエスト送信 ---\n");
var response = await SendStructuredOutputRequest(httpClient, modelName, userMessage);
Console.WriteLine("\n--- Structured Output 解析結果 ---\n");
Console.WriteLine($"finish_reason: {response.FinishReason}");
Console.WriteLine($"summary: {response.Result.Summary}");
Console.WriteLine($"category: {response.Result.Category}");
Console.WriteLine($"priority: {response.Result.Priority}");
Console.WriteLine($"affectedUsers: {response.Result.AffectedUsers}");
Console.WriteLine($"needsSameDayResponse: {response.Result.NeedsSameDayResponse}");
Console.WriteLine($"escalationTarget: {response.Result.EscalationTarget}");
Console.WriteLine("\n--- 生JSON ---\n");
Console.WriteLine(response.RawJson);
}
catch (Exception ex)
{
Console.WriteLine($"エラーが発生しました: {ex.Message}");
if (ex.InnerException is not null)
{
Console.WriteLine($"詳細: {ex.InnerException.Message}");
}
}
Console.WriteLine("\n=== デモ完了 ===");
}
private static async Task<StructuredOutputResponse> SendStructuredOutputRequest(HttpClient httpClient, string modelName, string userMessage)
{
string? retryReason = null;
for (var attempt = 1; attempt <= 2; attempt++)
{
var response = await SendStructuredOutputAttempt(httpClient, modelName, userMessage, retryReason);
var validationError = ValidateResult(response.Result);
if (validationError is null)
{
return response;
}
Console.WriteLine($"Structured Output の検証に失敗しました: {validationError}");
retryReason = $"前回の出力は無効でした。{validationError}。メタ説明やスキーマ説明を含めず、問い合わせ本文だけを根拠に JSON を返してください。escalationTarget は helpdesk / identity_team / portal_ops / finance_system / other のいずれかにしてください。";
}
throw new InvalidOperationException("Structured Output を2回試しましたが、安定したJSONを取得できませんでした。");
}
private static async Task<StructuredOutputResponse> SendStructuredOutputAttempt(HttpClient httpClient, string modelName, string userMessage, string? retryReason)
{
var messages = retryReason is null
? new object[]
{
new
{
role = "system",
content = "あなたは問い合わせ本文から値を抽出するデータ抽出器です。説明文、Markdown、コードフェンス、メタ発言は禁止です。summary は問い合わせ内容だけを日本語で20文字以上40文字以下で要約してください。affectedUsers は本文から読める人数を整数で返し、読み取れない場合は 1 にしてください。"
},
new
{
role = "user",
content = $"以下の問い合わせを分類してください。\n---\n{userMessage}\n---"
}
}
: new object[]
{
new
{
role = "system",
content = "あなたは問い合わせ本文から値を抽出するデータ抽出器です。説明文、Markdown、コードフェンス、メタ発言は禁止です。summary は問い合わせ内容だけを日本語で20文字以上40文字以下で要約してください。affectedUsers は本文から読める人数を整数で返し、読み取れない場合は 1 にしてください。"
},
new
{
role = "system",
content = retryReason
},
new
{
role = "user",
content = $"以下の問い合わせを分類してください。\n---\n{userMessage}\n---"
}
};
var requestBody = new
{
model = modelName,
messages,
response_format = BuildResponseFormat(),
temperature = 0.1,
max_tokens = 14000
};
var requestJson = JsonSerializer.Serialize(requestBody, new JsonSerializerOptions
{
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
});
using var content = new StringContent(requestJson, Encoding.UTF8, "application/json");
using var response = await httpClient.PostAsync("/v1/chat/completions", content);
var responseText = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
throw new InvalidOperationException($"HTTP {(int)response.StatusCode}: {responseText}");
}
using var document = JsonDocument.Parse(responseText);
var choice = document.RootElement.GetProperty("choices")[0];
var finishReason = choice.GetProperty("finish_reason").GetString() ?? "(unknown)";
var message = choice.GetProperty("message");
if (message.TryGetProperty("refusal", out var refusalElement) && refusalElement.ValueKind == JsonValueKind.String)
{
throw new InvalidOperationException($"モデルが Structured Output を拒否しました: {refusalElement.GetString()}");
}
var contentText = message.GetProperty("content").GetString();
if (string.IsNullOrWhiteSpace(contentText))
{
throw new InvalidOperationException("message.content が空でした。モデルが JSON を返していません。");
}
var result = JsonSerializer.Deserialize<InquiryTriageResult>(contentText, DeserializeOptions);
if (result is null)
{
throw new InvalidOperationException("Structured Output のデシリアライズに失敗しました。");
}
return new StructuredOutputResponse
{
Result = result,
RawJson = contentText,
FinishReason = finishReason
};
}
private static string? ValidateResult(InquiryTriageResult result)
{
if (string.IsNullOrWhiteSpace(result.Summary))
{
return "summary が空です";
}
var loweredSummary = result.Summary.ToLowerInvariant();
if (loweredSummary.Contains("json") || loweredSummary.Contains("schema") || loweredSummary.Contains("respond"))
{
return "summary にメタ説明が含まれています";
}
if (Array.IndexOf(AllowedCategories, result.Category) < 0)
{
return "category が許可された列挙値に含まれていません";
}
if (Array.IndexOf(AllowedPriorities, result.Priority) < 0)
{
return "priority が許可された列挙値に含まれていません";
}
if (result.AffectedUsers < 1)
{
return "affectedUsers が 1 未満です";
}
if (Array.IndexOf(new[] { "helpdesk", "identity_team", "portal_ops", "finance_system", "other" }, result.EscalationTarget) < 0)
{
return "escalationTarget が許可された列挙値に含まれていません";
}
if (result.Summary.Length < 10)
{
return "summary が短すぎます";
}
return null;
}
private static object BuildResponseFormat()
{
return new
{
type = "json_schema",
json_schema = new
{
name = "inquiry_triage",
strict = true,
schema = new
{
type = "object",
properties = new
{
summary = new
{
type = "string",
description = "問い合わせ内容を一文で要約した文章"
},
category = new
{
type = "string",
@enum = new[] { "account", "billing", "bug", "feature_request", "other" },
description = "問い合わせカテゴリ"
},
priority = new
{
type = "string",
@enum = new[] { "low", "medium", "high", "critical" },
description = "対応優先度"
},
affectedUsers = new
{
type = "integer",
minimum = 1,
maximum = 1000,
description = "影響を受けている人数"
},
needsSameDayResponse = new
{
type = "boolean",
description = "当日中の優先対応が必要か"
},
escalationTarget = new
{
type = "string",
@enum = new[] { "helpdesk", "identity_team", "portal_ops", "finance_system", "other" },
description = "主担当にすべきチーム"
}
},
required = new[]
{
"summary",
"category",
"priority",
"affectedUsers",
"needsSameDayResponse",
"escalationTarget"
},
additionalProperties = false
}
}
};
}
}
コードのポイント:
-
BuildResponseFormat()で OpenAI 互換のresponse_formatを組み立てる -
json_schema.strict = trueにより、余計なキーを抑制しやすくする -
ValidateResult()を入れて、ローカルモデル特有のメタ発言や列挙値ズレを検出する -
max_tokens = 14000にしているので、将来的に配列やネストを増やしたスキーマでも余裕を持たせやすい
実行結果
実際にこのコードを実行すると、以下のような構造化結果が返ってきました。
finish_reason: stop
summary: 社内ポータルのパスワード再設定メール不着
category: account
priority: high
affectedUsers: 1
needsSameDayResponse: True
escalationTarget: identity_team
生の JSON は以下の通りです。
{
"summary": "社内ポータルのパスワード再設定メール不着",
"category": "account",
"priority": "high",
"affectedUsers": 1,
"needsSameDayResponse": true,
"escalationTarget": "identity_team"
}
この結果から分かること
- 形式はかなり安定する: 文字列、整数、真偽値、列挙値に分解できた
- 後段処理が楽になる: そのまま C# の型へマッピングできる
-
意味の補正はまだ必要: 今回の問い合わせ本文には「3人が同じ症状」とあるのに、
affectedUsersは1になった
この最後の点が重要です。Structured Output は JSON の形を整える技術 であり、値の正しさまで自動保証する技術ではありません。そのため、今回のようにアプリ側の検証や再試行を入れておくと扱いやすくなります。また、何が値として入って欲しいかのコンテキストも重要です。
LM Studioログから読み取れること
今回はクライアント側の HTTP ログだけでなく、LM Studio 側のサーバーログも確認できました。これにより、response_format がサーバーまでそのまま届き、モデル推論でも Structured Output として扱われていることが分かります。
ログの要点
Received request: POST to /v1/chat/completions
...
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "inquiry_triage",
"strict": true,
...
}
}
[openai/gpt-oss-20b] Running chat completion on conversation with 2 messages.
slot update_slots: id 3 | task 237 | new prompt, n_ctx_slot = 14080, n_keep = 206, task.n_tokens = 206
prompt eval time = 58.90 ms / 206 tokens
eval time = 768.01 ms / 59 tokens
total time = 826.91 ms / 265 tokens
[openai/gpt-oss-20b] Model generated tool calls: []
"content": "{\"summary\":\"社内ポータルのパスワード再設定メール不着\",\"category\":\"account\",...}"
このログで確認できたこと
-
response_formatは途中で落ちていないサーバーログの受信リクエストにも
response_formatとjson_schemaがそのまま残っていました。つまり、少なくとも LM Studio の OpenAI 互換エンドポイントまでは、クライアントが意図した Structured Output 指定が届いています。 -
今回のリクエストは 2 メッセージで処理されている
Running chat completion on conversation with 2 messages.と出ているので、実際に処理されたのは system と user の 2 件です。今回は再試行が発生していないことも読み取れます。ここが重要で、チャット履歴として数えられているのはmessagesだけ です。ログ上では別途response_formatが見えているので、JSON Schema 本体はチャット履歴の中ではなく、リクエストの別フィールドとして送られていることが分かります。 -
システムプロンプト とは別に指定した構造が、その通りの形で返ってきている
受信リクエストには
response_format.json_schemaとしてsummary、category、priority、affectedUsers、needsSameDayResponse、escalationTargetが定義されていました。一方、生成結果のmessage.contentにも同じキーを持つ JSON が返ってきています。このことから Structured Output で指定した スキーマに沿って返答が組み立てられた と確認できます。 -
Tool Calling とは別物として処理されている
Model generated tool calls: []となっているため、Structured Output を使っても Tool Calling が暗黙に発動するわけではありません。今回の応答は、あくまで通常の chat completion のmessage.contentに JSON が入って返ってきています。 -
Structured Output でも
message.contentは文字列サーバーログの
Generated predictionを見ると、contentはオブジェクトではなく JSON 文字列です。そのため、クライアント側では通常の OpenAI 互換レスポンスを受け取ったあと、さらにmessage.contentをJsonSerializer.Deserialize<T>()する必要があります。 -
今回の実行コストもざっくり把握できる
ログでは
prompt_tokens = 206、completion_tokens = 59、total_tokens = 265でした。Structured Output を使うとスキーマ定義のぶんだけプロンプトトークンは増えますが、その代わり後段のパースやバリデーションが単純になります。 -
max_tokens = 14000を送っていても、実際の生成は 59 トークンで止まっている今回の出力は短い JSON なので、上限を 14000 にしていても使い切ってはいません。これは「余裕を持たせた上限値」であり、毎回そのトークン数を消費するわけではない、という理解で大丈夫です。
ログと実行結果を合わせて分かること
今回の Structured Output は、サーバーまでスキーマ指定が届き、JSON 文字列として返却され、クライアント側で型へ変換できている という点では期待通りに動いています。
一方で、affectedUsers が 3 ではなく 1 になっているので、問題は「形式が崩れた」ことではなく「意味解釈がずれた」ことです。ここからも、Structured Output が強いのは 構文の安定化 であって、意味の正確性保証ではない と分かります。
HTTPパケットから読み取れること
今回のパケット観察で押さえたい点は以下の通りです。
- リクエストボディに
response_formatが追加される - レスポンス全体は通常の
chat.completion形式のまま - 実際の構造化データは
choices[0].message.contentの文字列として返る - クライアント側で JSON をもう一段デシリアライズする必要がある
つまり、Structured Output だからといって HTTP の箱が変わるわけではありません。箱は従来通り、箱の中身だけを厳密化する のがポイントです。
まとめ
Structured Output を使うと、LLM の返答を自然文ではなく、アプリが扱いやすい型付きデータとして受け取れるようになります。今回のように response_format と json_schema を明示すれば、category や priority のような列挙値を安定して取り出しやすくなります。LM Studio のログでも、その指定がサーバーまで届き、通常の chat.completion として処理されたあとに message.content へ JSON 文字列が格納されて返ることを確認できました。
一方で、JSON にセットされた値がずれることがあります。したがって、実運用では Structured Output + アプリ側検証 をセットで考えるのが現実的です。今回のログでは affectedUsers の解釈ずれも確認できたので、今後は再試行条件の強化やコンテキストの見直しも検討していきたいところです。
LLM の動き 一覧
LLM の動き〜HTTPパケット確認〜
LLM の動き〜Tool Calling確認〜
LLM の動き〜マルチモーダルモデルの確認〜
LLM の動き〜Structured Output確認〜
ログ
実行結果
=== Structured Output デモ ===
エンドポイント: http://localhost:1234
モデル: openai/gpt-oss-20b
--- ユーザー入力 ---
社内ポータルでパスワード再設定メールが届きません。午前中から3人が同じ症状で、今日中に経費精算を締めたいです。
--- Structured Output リクエスト送信 ---
>>> HTTP リクエスト <<<
メソッド: POST
URL: http://localhost:1234/v1/chat/completions
リクエストボディ:
{
"model": "openai/gpt-oss-20b",
"messages": [
{
"role": "system",
"content": "あなたは問い合わせ本文から値を抽出するデータ抽出器です。説明文、Markdown、コードフェンス、メタ発言は禁止です。summary は問い合わせ内容だけを日本語で20文字以上40文字以下で要約してください。affectedUsers は本文から読める人数を整数で返し、読み取れない場合は 1 にしてください。"
},
{
"role": "user",
"content": "以下の問い合わせを分類してください。\n---\n社内ポータルでパスワード再設定メールが届きません。午前中から3人が同じ症状で、今日中に経費精算を締めたいです。\n---"
}
],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "inquiry_triage",
"strict": true,
"schema": {
"type": "object",
"properties": {
"summary": {
"type": "string",
"description": "問い合わせ内容を一文で要約した文章"
},
"category": {
"type": "string",
"enum": [
"account",
"billing",
"bug",
"feature_request",
"other"
],
"description": "問い合わせカテゴリ"
},
"priority": {
"type": "string",
"enum": [
"low",
"medium",
"high",
"critical"
],
"description": "対応優先度"
},
"affectedUsers": {
"type": "integer",
"minimum": 1,
"maximum": 1000,
"description": "影響を受けている人数"
},
"needsSameDayResponse": {
"type": "boolean",
"description": "当日中の優先対応が必要か"
},
"escalationTarget": {
"type": "string",
"enum": [
"helpdesk",
"identity_team",
"portal_ops",
"finance_system",
"other"
],
"description": "主担当にすべきチーム"
}
},
"required": [
"summary",
"category",
"priority",
"affectedUsers",
"needsSameDayResponse",
"escalationTarget"
],
"additionalProperties": false
}
}
},
"temperature": 0.1,
"max_tokens": 14000
}
<<< HTTP レスポンス >>>
ステータス: OK
ヘッダー:
X-Powered-By: Express
ETag: W/"32a-oBQ2Em24W3ZRlYIM4v/B8tcbVuE"
Date: Sun, 10 May 2026 11:51:50 GMT
Connection: keep-alive
Keep-Alive: timeout=5
レスポンスボディ:
{
"id": "chatcmpl-luptj6n8ureosrtqemo7o",
"object": "chat.completion",
"created": 1778413909,
"model": "openai/gpt-oss-20b",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "{\"summary\":\"社内ポータルのパスワード再設定メール不着\",\"category\":\"account\",\"priority\":\"high\",\"affectedUsers\":1,\"needsSameDayResponse\":true, \"escalationTarget\":\"identity_team\"}",
"reasoning": "",
"tool_calls": []
},
"logprobs": null,
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 206,
"completion_tokens": 59,
"total_tokens": 265,
"completion_tokens_details": {
"reasoning_tokens": 0
}
},
"stats": {},
"system_fingerprint": "openai/gpt-oss-20b"
}
--- Structured Output 解析結果 ---
finish_reason: stop
summary: 社内ポータルのパスワード再設定メール不着
category: account
priority: high
affectedUsers: 1
needsSameDayResponse: True
escalationTarget: identity_team
--- 生JSON ---
{"summary":"社内ポータルのパスワード再設定メール不着","category":"account","priority":"high","affectedUsers":1,"needsSameDayResponse":true, "escalationTarget":"identity_team"}
=== デモ完了 ===
LM Studio
2026-05-10 20:51:49 [DEBUG]
Received request: POST to /v1/chat/completions with body {
"model": "openai/gpt-oss-20b",
"messages": [
{
"role": "system",
"content": "あなたは問い合わせ本文から値を抽出するデータ抽出器です。説明文、Markdown、コードフェンス、メ... <Truncated in logs> ...ffectedUsers は本文から読める人数を整数で返し、読み取れない場合は 1 にしてください。"
},
{
"role": "user",
"content": "以下の問い合わせを分類してください。\n---\n社内ポータルでパスワード再設定メールが届きません。午前中から3人が同じ症状で、今日中に経費精算を締めたいです。\n---"
}
],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "inquiry_triage",
"strict": true,
"schema": {
"type": "object",
"properties": {
"summary": {
"type": "string",
"description": "問い合わせ内容を一文で要約した文章"
},
"category": {
"type": "string",
"enum": [
"account",
"billing",
"bug",
"feature_request",
"other"
],
"description": "問い合わせカテゴリ"
},
"priority": {
"type": "string",
"enum": [
"low",
"medium",
"high",
"critical"
],
"description": "対応優先度"
},
"affectedUsers": {
"type": "integer",
"minimum": 1,
"maximum": 1000,
"description": "影響を受けている人数"
},
"needsSameDayResponse": {
"type": "boolean",
"description": "当日中の優先対応が必要か"
},
"escalationTarget": {
"type": "string",
"enum": [
"helpdesk",
"identity_team",
"portal_ops",
"finance_system",
"other"
],
"description": "主担当にすべきチーム"
}
},
"required": [
"summary",
"category",
"priority",
"affectedUsers",
"needsSameDayResponse",
"escalationTarget"
],
"additionalProperties": false
}
}
},
"temperature": 0.1,
"max_tokens": 14000
}
2026-05-10 20:51:49 [INFO]
[openai/gpt-oss-20b] Running chat completion on conversation with 2 messages.
2026-05-10 20:51:49 [DEBUG]
LlamaV4::predict slot selection: session_id=<empty> server-selected (LCP/LRU)
2026-05-10 20:51:49 [DEBUG]
slot get_availabl: id 3 | task -1 | selected slot by LCP similarity, sim_best = 0.583 (> 0.100 thold), f_keep = 0.500
2026-05-10 20:51:49 [DEBUG]
slot launch_slot_: id 3 | task -1 | sampler chain: logits -> penalties -> ?dry -> ?top-n-sigma -> top-k -> ?typical -> top-p -> min-p -> ?xtc -> temp-ext -> dist
slot launch_slot_: id 3 | task 237 | processing task, is_child = 0
slot update_slots: id 3 | task 237 | new prompt, n_ctx_slot = 14080, n_keep = 206, task.n_tokens = 206
slot update_slots: id 3 | task 237 | n_past = 120, slot.prompt.tokens.size() = 240, seq_id = 3, pos_min = 0, n_swa = 128
slot update_slots: id 3 | task 237 | forcing full prompt re-processing due to lack of cache data (likely due to SWA or hybrid/recurrent memory, see https://github.com/ggml-org/llama.cpp/pull/13194#issuecomment-2868343055)
slot update_slots: id 3 | task 237 | n_tokens = 0, memory_seq_rm [0, end)
slot init_sampler: id 3 | task 237 | init sampler, took 0.03 ms, tokens: text = 206, total = 206
slot update_slots: id 3 | task 237 | prompt processing done, n_tokens = 206, batch.n_tokens = 206
2026-05-10 20:51:49 [INFO]
[openai/gpt-oss-20b] Prompt processing progress: 0.0%
2026-05-10 20:51:49 [INFO]
[openai/gpt-oss-20b] Prompt processing progress: 100.0%
2026-05-10 20:51:50 [DEBUG]
slot print_timing: id 3 | task 237 |
prompt eval time = 58.90 ms / 206 tokens ( 0.29 ms per token, 3497.69 tokens per second)
eval time = 768.01 ms / 59 tokens ( 13.02 ms per token, 76.82 tokens per second)
total time = 826.91 ms / 265 tokens
slot release: id 3 | task 237 | stop processing: n_tokens = 264, truncated = 0
srv update_slots: all slots are idle
2026-05-10 20:51:50 [DEBUG]
LlamaV4: server assigned slot 3 to task 237
2026-05-10 20:51:50 [INFO]
[openai/gpt-oss-20b] Model generated tool calls: []
2026-05-10 20:51:50 [INFO]
[openai/gpt-oss-20b] Generated prediction: {
"id": "chatcmpl-luptj6n8ureosrtqemo7o",
"object": "chat.completion",
"created": 1778413909,
"model": "openai/gpt-oss-20b",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "{\"summary\":\"社内ポータルのパスワード再設定メール不着\",\"category\":\"account\",\"priority\":\"high\",\"affectedUsers\":1,\"needsSameDayResponse\":true, \"escalationTarget\":\"identity_team\"}",
"reasoning": "",
"tool_calls": []
},
"logprobs": null,
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 206,
"completion_tokens": 59,
"total_tokens": 265,
"completion_tokens_details": {
"reasoning_tokens": 0
}
},
"stats": {},
"system_fingerprint": "openai/gpt-oss-20b"
}
Discussion