gpt-oss で JSON 構造化出力を安定させる:スキーマ提示・検証リトライ・自動修復

AI モデルから確実に JSON を取得したい。そんな開発者の願いに応えるべく、OpenAI は gpt-oss などのモデルで構造化出力機能を提供しています。しかし、実際には「余計なコメントが混入する」「途中でオブジェクトが切れる」といった問題が頻発しているのです。
本記事では、gpt-oss で JSON 構造化出力を安定させるための 3 つの柱——スキーマ提示・検証リトライ・自動修復について、実践的なコード例とともに詳しく解説します。TypeScript と Zod を使った型安全な実装から、エラーハンドリングまで、確実に動く仕組みを構築しましょう。
背景
gpt-oss とは
gpt-oss は OpenAI が開発したオープンソースの言語モデルです。Ollama や Hugging Face、Fireworks AI などのプラットフォームで利用でき、ローカル環境でも動作させられる点が大きな特徴となっています。
20B(200 億パラメータ)という比較的コンパクトなサイズながら、高度な推論能力を持ち、さまざまなタスクに対応できます。
JSON 構造化出力が求められる理由
AI モデルの出力をプログラムで扱うには、決まった形式のデータが必要です。特に以下のようなケースでは、JSON による構造化出力が不可欠でしょう。
- Web API との連携: バックエンドが期待する型に合わせたレスポンス
- データベース保存: スキーマに準拠したオブジェクト
- UI 表示: フロントエンドコンポーネントで扱いやすい形式
- 自動処理パイプライン: 後続の処理で型チェックやバリデーションを行う
従来は自然言語の回答をパースする必要があり、正規表現や文字列操作で無理やり抽出していました。構造化出力機能を使えば、この手間を大幅に削減できます。
以下の図は、従来の方式と構造化出力の違いを示したものです。
mermaidflowchart LR
user["開発者"] -->|プロンプト| model["AI モデル"]
model -->|自然言語| parse["パース処理"]
parse -->|正規表現・分割| extract["データ抽出"]
extract -->|不安定| app["アプリケーション"]
user2["開発者"] -->|スキーマ + プロンプト| model2["構造化出力<br/>モデル"]
model2 -->|JSON| validate["スキーマ検証"]
validate -->|型安全| app2["アプリケーション"]
style model fill:#ffcccc
style parse fill:#ffcccc
style extract fill:#ffcccc
style model2 fill:#ccffcc
style validate fill:#ccffcc
従来方式では複数の処理ステップが必要で、各段階でエラーが発生しやすくなっています。一方、構造化出力では最初から JSON として取得できるため、パイプラインがシンプルかつ安定します。
gpt-oss における構造化出力の現状
OpenAI の公式 API では response_format
パラメータで JSON Schema を指定できますが、gpt-oss ではいくつかの制約があるのです。
Ollama や vllm などのランタイムでは、完全な Structured Outputs 機能がまだサポートされていません。そのため、以下のような問題が報告されています。
# | 問題 | 具体例 |
---|---|---|
1 | 余計なコメントの混入 | JSON の前後に説明文が付く |
2 | 不完全なオブジェクト | トークン制限で途中で切れる |
3 | スキーマ違反 | 必須フィールドが欠落 |
4 | Boolean 値の誤り | true の代わりに True |
5 | 推論トレースの挿入 | Harmony 形式で思考過程が混入 |
これらの問題に対処するには、スキーマ提示・検証リトライ・自動修復という 3 つのアプローチを組み合わせる必要があります。
課題
課題 1: スキーマを提示しても守られない
response_format
で JSON Schema を指定しても、gpt-oss は必ずしもそれに従いません。
以下は典型的な失敗例です。
typescript// スキーマを指定しているのに...
const schema = {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'number' },
},
required: ['name', 'age'],
};
期待する出力は以下のような JSON です。
json{
"name": "田中太郎",
"age": 30
}
しかし、実際には以下のようなレスポンスが返ってくることがあります。
textこの質問にお答えします。
{
"name": "田中太郎",
"age": 30
}
以上が回答です。
JSON 以外のテキストが混入しているため、JSON.parse()
が失敗してしまいます。これは gpt-oss が「ユーザーに親切に説明しよう」とする傾向があるためです。
課題 2: トークン制限による切断
gpt-oss には最大出力トークン数の制限があります。長い JSON を生成する際、途中で切れてしまうことがあるのです。
json{
"users": [
{ "id": 1, "name": "太郎" },
{ "id": 2, "name": "花子" },
{ "id": 3, "name": "次郎" },
{ "id": 4, "nam
このような不完全な JSON は当然パースできません。配列やオブジェクトの閉じ括弧が欠けているため、構文エラーとなります。
課題 3: 型の不一致
スキーマで number
を指定しても、文字列で返されることがあります。
json{
"age": "30"
}
期待していたのは数値型ですが、文字列として返ってきました。後続の計算処理でエラーが発生する原因となります。
課題 4: 必須フィールドの欠落
スキーマで required
を指定しても、フィールドが省略されることがあるのです。
json{
"name": "田中太郎"
}
age
フィールドが欠けています。これをそのままアプリケーションで使おうとすると、undefined
エラーが発生する可能性があります。
以下の図は、これらの課題がどのように発生するかを示したものです。
mermaidsequenceDiagram
participant Dev as 開発者
participant API as OpenAI API
participant Model as gpt-oss
participant App as アプリケーション
Dev->>API: スキーマ + プロンプト
API->>Model: 生成リクエスト
Model->>API: 余計なテキスト付きJSON
API->>App: レスポンス
App->>App: JSON.parse() 失敗
App-->>Dev: エラー: Unexpected token
Dev->>API: 再リクエスト
API->>Model: 生成リクエスト
Model->>API: トークン制限で途中切断
API->>App: 不完全なJSON
App->>App: JSON.parse() 失敗
App-->>Dev: エラー: Unexpected end
このように、複数の要因でパースエラーが発生します。単純にスキーマを渡すだけでは不十分で、検証とリトライの仕組みが必要です。
解決策
これらの課題を解決するには、以下の 3 つのステップを実装します。
解決策 1: スキーマの明示的な提示
まず、JSON Schema を API に渡すだけでなく、プロンプト内にもスキーマを明記します。これによりモデルが期待される形式を理解しやすくなるのです。
基本的なスキーマ提示
typescriptimport OpenAI from 'openai';
const client = new OpenAI({
baseURL: 'https://api.fireworks.ai/inference/v1',
apiKey: process.env.FIREWORKS_API_KEY,
});
Fireworks AI を経由して gpt-oss にアクセスする設定です。baseURL
を変更することで、他のプロバイダーでも利用できます。
typescriptconst schema = {
type: 'json_schema',
json_schema: {
name: 'person_schema',
schema: {
type: 'object',
properties: {
name: { type: 'string' },
city: { type: 'string' },
profession: { type: 'string' },
},
required: ['name', 'city', 'profession'],
},
},
};
JSON Schema を定義しています。required
配列で必須フィールドを明示することが重要です。
typescriptconst systemPrompt = `You are a helpful assistant designed to output JSON.
You MUST respond with valid JSON only, without any additional text or explanation.
The JSON must conform to this schema:
{
"name": "string",
"city": "string",
"profession": "string"
}`;
システムプロンプトで「JSON のみを返す」ことを強調しています。スキーマも再度提示することで、モデルの理解を深めます。
typescriptconst completion = await client.chat.completions.create({
model: 'accounts/fireworks/models/gpt-oss-20b',
messages: [
{ role: 'system', content: systemPrompt },
{
role: 'user',
content: '東京在住のエンジニアについて教えてください',
},
],
response_format: schema,
});
response_format
パラメータでスキーマを指定します。これにより、モデルは JSON 形式で出力しようと試みます。
typescriptconst content = completion.choices[0].message.content;
console.log(content);
取得した内容を表示します。この時点ではまだパースしていません。
この方法により、スキーマ違反のリスクが減少しますが、完全ではありません。次のステップで検証を行います。
解決策 2: Zod によるスキーマ検証とリトライ
次に、Zod を使って厳密な型チェックを行い、エラー時にはリトライする仕組みを構築します。
Zod のインストール
bashyarn add zod openai zod-to-json-schema
必要なパッケージをインストールします。zod-to-json-schema
は Zod スキーマを JSON Schema に変換するライブラリです。
Zod スキーマの定義
typescriptimport { z } from 'zod';
const PersonSchema = z.object({
name: z.string().describe('人物の名前'),
city: z.string().describe('居住都市'),
profession: z.string().describe('職業'),
});
type Person = z.infer<typeof PersonSchema>;
Zod でスキーマを定義します。describe()
メソッドで各フィールドの説明を追加でき、これが JSON Schema に反映されます。
z.infer
で TypeScript の型を自動生成できるため、型安全性が向上します。
JSON Schema への変換
typescriptimport { zodToJsonSchema } from 'zod-to-json-schema';
const jsonSchema = zodToJsonSchema(
PersonSchema,
'person_schema'
);
Zod スキーマを OpenAI API が理解できる JSON Schema 形式に変換します。
リトライ機能付き補完関数
typescriptasync function completionWithRetry(
prompt: string,
maxRetries: number = 3
): Promise<Person> {
let lastError: Error | null = null;
for (let i = 0; i < maxRetries; i++) {
try {
// API リクエストを実行
const response = await client.chat.completions.create(
{
model: 'accounts/fireworks/models/gpt-oss-20b',
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: prompt },
],
response_format: {
type: 'json_schema',
json_schema: jsonSchema,
},
}
);
return response;
} catch (error) {
lastError = error as Error;
console.log(`試行 ${i + 1} 失敗: ${error.message}`);
// エクスポネンシャルバックオフ
await new Promise((resolve) =>
setTimeout(resolve, Math.pow(2, i) * 1000)
);
}
}
throw lastError;
}
この関数はリトライロジックを実装しています。失敗時には指数バックオフで待機時間を増やし、API の負荷を分散させます。
次に、取得した JSON を検証する部分を実装します。
typescriptasync function getValidatedPerson(
prompt: string
): Promise<Person> {
const response = await completionWithRetry(prompt);
const content = response.choices[0].message.content;
// JSON パース
let parsed: unknown;
try {
parsed = JSON.parse(content);
} catch (error) {
throw new Error(`JSON パースエラー: ${error.message}`);
}
// Zod で検証
const result = PersonSchema.safeParse(parsed);
if (!result.success) {
throw new Error(
`スキーマ検証エラー: ${JSON.stringify(
result.error.issues
)}`
);
}
return result.data;
}
safeParse()
を使うことで、検証失敗時に例外をスローせず、エラー情報を取得できます。これにより、詳細なエラーメッセージをログに記録できるのです。
使用例
typescripttry {
const person = await getValidatedPerson(
'大阪在住のデザイナーについて教えてください'
);
console.log(person.name);
console.log(person.city);
console.log(person.profession);
} catch (error) {
console.error('エラー:', error.message);
}
この実装により、型安全な形でデータを取得できます。検証エラーが発生した場合は、エラー内容を確認してプロンプトやスキーマを調整しましょう。
解決策 3: 自動修復メカニズム
最後に、パースエラーや検証エラーが発生した際に、AI に自己修正を促す仕組みを実装します。
gpt-json ライブラリによる自動修復
gpt-json
や zod-gpt
といったライブラリは、自動修復機能を提供しています。ここでは zod-gpt の仕組みを参考に、自前の修復ロジックを構築します。
typescriptasync function completionWithAutoRepair(
prompt: string,
maxAttempts: number = 3
): Promise<Person> {
let messages: Array<{ role: string; content: string }> = [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: prompt },
];
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const response = await client.chat.completions.create({
model: 'accounts/fireworks/models/gpt-oss-20b',
messages,
response_format: {
type: 'json_schema',
json_schema: jsonSchema,
},
});
const content = response.choices[0].message.content;
// 次のブロックに続く
return await validateAndRepair(
content,
messages,
attempt
);
}
throw new Error('最大試行回数を超えました');
}
この関数は会話履歴を保持しながら、複数回の試行を行います。エラーが発生した場合は、その情報を次のリクエストに含めます。
typescriptasync function validateAndRepair(
content: string,
messages: Array<{ role: string; content: string }>,
attempt: number
): Promise<Person> {
// まず JSON として抽出を試みる
const extracted = extractJSON(content);
let parsed: unknown;
try {
parsed = JSON.parse(extracted);
} catch (parseError) {
// JSON パースエラー時の修復プロンプト
const repairPrompt = `前回の出力が無効な JSON でした:
${content}
エラー: ${parseError.message}
有効な JSON のみを出力してください。`;
messages.push(
{ role: 'assistant', content },
{ role: 'user', content: repairPrompt }
);
throw new Error('JSON パース失敗、修復を試行');
}
// 次のブロックに続く
return await validateSchema(parsed, content, messages);
}
JSON の抽出とパース処理を行います。エラー時には、エラー内容を含めた修復プロンプトを生成します。
typescriptfunction extractJSON(text: string): string {
// ``` で囲まれている場合
const codeBlockMatch = text.match(
/```(?:json)?\s*\n?([\s\S]*?)\n?```/
);
if (codeBlockMatch) {
return codeBlockMatch[1].trim();
}
// { で始まる部分を抽出
const jsonMatch = text.match(/\{[\s\S]*\}/);
if (jsonMatch) {
return jsonMatch[0];
}
// そのまま返す
return text.trim();
}
余計なテキストを除去し、JSON 部分だけを抽出します。コードブロックや説明文が混入している場合にも対応できます。
typescriptasync function validateSchema(
parsed: unknown,
content: string,
messages: Array<{ role: string; content: string }>
): Promise<Person> {
const result = PersonSchema.safeParse(parsed);
if (!result.success) {
// スキーマ検証エラー時の修復プロンプト
const errors = result.error.issues
.map(
(issue) =>
`- ${issue.path.join('.')}: ${issue.message}`
)
.join('\n');
const repairPrompt = `前回の出力がスキーマに適合しませんでした:
${content}
エラー:
${errors}
以下のスキーマに完全に適合する JSON を出力してください:
${JSON.stringify(PersonSchema.shape, null, 2)}`;
messages.push(
{ role: 'assistant', content },
{ role: 'user', content: repairPrompt }
);
throw new Error('スキーマ検証失敗、修復を試行');
}
return result.data;
}
スキーマ検証を行い、エラー時には具体的なエラー箇所を示して修正を促します。どのフィールドがどう間違っているかを明示することで、モデルが適切に修正しやすくなります。
Boolean 値の自動修正
gpt-oss は true
の代わりに True
を返すことがあります。これを自動で修正する関数を追加しましょう。
typescriptfunction fixBooleanValues(jsonString: string): string {
return jsonString
.replace(/:\s*True\b/g, ': true')
.replace(/:\s*False\b/g, ': false');
}
正規表現で Python スタイルの Boolean 値を JavaScript 形式に変換します。
typescriptasync function validateAndRepair(
content: string,
messages: Array<{ role: string; content: string }>,
attempt: number
): Promise<Person> {
let extracted = extractJSON(content);
// Boolean 値を修正
extracted = fixBooleanValues(extracted);
// 以降は前述の処理
// ...
}
抽出後すぐに修正を適用することで、パースエラーを防ぎます。
トークン切断への対処
JSON が途中で切れた場合、閉じ括弧を補完します。
typescriptfunction repairTruncatedJSON(jsonString: string): string {
let fixed = jsonString.trim();
// { の数と } の数をカウント
const openBraces = (fixed.match(/\{/g) || []).length;
const closeBraces = (fixed.match(/\}/g) || []).length;
// 不足分を補完
const missingBraces = openBraces - closeBraces;
if (missingBraces > 0) {
fixed += '\n' + '}'.repeat(missingBraces);
}
// [ の数と ] の数をカウント
const openBrackets = (fixed.match(/\[/g) || []).length;
const closeBrackets = (fixed.match(/\]/g) || []).length;
// 不足分を補完
const missingBrackets = openBrackets - closeBrackets;
if (missingBrackets > 0) {
fixed += '\n' + ']'.repeat(missingBrackets);
}
return fixed;
}
括弧の数を数えて、不足分を追加します。完璧ではありませんが、多くのケースで有効です。
typescriptasync function validateAndRepair(
content: string,
messages: Array<{ role: string; content: string }>,
attempt: number
): Promise<Person> {
let extracted = extractJSON(content);
extracted = fixBooleanValues(extracted);
extracted = repairTruncatedJSON(extracted);
// 以降は前述の処理
// ...
}
複数の修復処理を組み合わせることで、さまざまなエラーに対応できます。
以下の図は、自動修復のフローを示したものです。
mermaidflowchart TD
start["API レスポンス取得"] --> extract["JSON 抽出"]
extract --> fixBool["Boolean 値修正"]
fixBool --> fixTrunc["切断修復"]
fixTrunc --> parse["JSON.parse()"]
parse --> validate["Zod 検証"]
parse -->|パース失敗| repair1["修復プロンプト生成"]
validate -->|検証失敗| repair2["修復プロンプト生成"]
repair1 --> retry["再試行"]
repair2 --> retry
validate -->|成功| done["データ返却"]
retry --> start
style parse fill:#ffffcc
style validate fill:#ffffcc
style repair1 fill:#ffcccc
style repair2 fill:#ffcccc
style done fill:#ccffcc
この図が示すように、エラー時には具体的な修復プロンプトを生成し、再試行することで成功率を高めます。
具体例
実際のアプリケーションでこれらの技術を組み合わせた例を見ていきましょう。
具体例 1: ユーザープロフィール抽出
SNS の投稿文からユーザー情報を抽出するケースです。
スキーマ定義
typescriptimport { z } from 'zod';
const UserProfileSchema = z.object({
name: z.string().describe('ユーザー名'),
age: z.number().min(0).max(150).describe('年齢'),
location: z.string().describe('居住地'),
interests: z.array(z.string()).describe('興味・関心'),
isActive: z.boolean().describe('アクティブかどうか'),
});
type UserProfile = z.infer<typeof UserProfileSchema>;
配列やブール値を含む、やや複雑なスキーマです。min()
や max()
でバリデーションルールも定義しています。
抽出関数
typescriptasync function extractUserProfile(
text: string
): Promise<UserProfile> {
const prompt = `以下のテキストからユーザープロフィールを抽出してください:
${text}
JSON 形式で出力してください。`;
return await completionWithAutoRepair(prompt);
}
先ほど実装した completionWithAutoRepair
を使うことで、自動修復機能が有効になります。
使用例
typescriptconst post = `
こんにちは!東京在住の田中太郎です。
28歳のエンジニアで、最近は機械学習とWeb開発に興味があります。
毎日GitHubにコミットしているアクティブユーザーです。
`;
try {
const profile = await extractUserProfile(post);
console.log('名前:', profile.name);
console.log('年齢:', profile.age);
console.log('居住地:', profile.location);
console.log('興味:', profile.interests.join(', '));
console.log('アクティブ:', profile.isActive);
} catch (error) {
console.error('抽出失敗:', error.message);
}
期待される出力は以下のとおりです。
text名前: 田中太郎
年齢: 28
居住地: 東京
興味: 機械学習, Web開発
アクティブ: true
型安全に各フィールドにアクセスできます。
具体例 2: エラー情報の構造化
ログメッセージからエラー情報を構造化するケースです。
スキーマ定義
typescriptconst ErrorInfoSchema = z.object({
errorCode: z.string().describe('エラーコード'),
errorMessage: z.string().describe('エラーメッセージ'),
severity: z
.enum(['low', 'medium', 'high', 'critical'])
.describe('深刻度'),
affectedComponent: z
.string()
.describe('影響を受けたコンポーネント'),
possibleCauses: z
.array(z.string())
.describe('考えられる原因'),
suggestedFixes: z
.array(z.string())
.describe('推奨される修正方法'),
});
type ErrorInfo = z.infer<typeof ErrorInfoSchema>;
z.enum()
で固定値の選択肢を定義しています。これにより、型安全性がさらに向上します。
抽出関数
typescriptasync function analyzeError(
logMessage: string
): Promise<ErrorInfo> {
const prompt = `以下のエラーログを分析し、構造化された情報を抽出してください:
${logMessage}
深刻度は low, medium, high, critical のいずれかを選択してください。
JSON 形式で出力してください。`;
return await completionWithAutoRepair(prompt);
}
プロンプト内で enum の選択肢を明示することで、モデルが適切な値を返しやすくなります。
使用例
typescriptconst log = `
[ERROR] 2025-01-15 10:23:45
Database connection failed: ECONNREFUSED
Component: UserService
Stack trace:
at Connection.connect (/app/db/connection.js:42)
at UserService.fetchUser (/app/services/user.js:18)
`;
try {
const errorInfo = await analyzeError(log);
console.log('エラーコード:', errorInfo.errorCode);
console.log('メッセージ:', errorInfo.errorMessage);
console.log('深刻度:', errorInfo.severity);
console.log(
'コンポーネント:',
errorInfo.affectedComponent
);
console.log('\n考えられる原因:');
errorInfo.possibleCauses.forEach((cause, i) => {
console.log(` ${i + 1}. ${cause}`);
});
console.log('\n推奨される修正:');
errorInfo.suggestedFixes.forEach((fix, i) => {
console.log(` ${i + 1}. ${fix}`);
});
} catch (error) {
console.error('分析失敗:', error.message);
}
期待される出力例です。
textエラーコード: ECONNREFUSED
メッセージ: Database connection failed
深刻度: high
コンポーネント: UserService
考えられる原因:
1. データベースサーバーが起動していない
2. ネットワーク接続の問題
3. 接続設定の誤り
推奨される修正:
1. データベースサーバーの起動を確認
2. 接続文字列の設定を確認
3. ファイアウォール設定を確認
このように、非構造化されたログから有用な情報を抽出できます。
具体例 3: 複数オブジェクトのバッチ処理
複数のアイテムを一度に処理する場合の実装です。
スキーマ定義
typescriptconst ActionItemSchema = z.object({
id: z.number().describe('アイテムID'),
description: z.string().describe('説明'),
dueDate: z
.string()
.nullable()
.describe('期限(YYYY-MM-DD形式)'),
owner: z.string().nullable().describe('担当者'),
priority: z
.enum(['low', 'medium', 'high'])
.describe('優先度'),
});
const ActionItemsSchema = z.object({
items: z
.array(ActionItemSchema)
.describe('アクションアイテムのリスト'),
});
type ActionItems = z.infer<typeof ActionItemsSchema>;
ネストした構造のスキーマです。配列の中に複数のオブジェクトが含まれます。
抽出関数
typescriptasync function extractActionItems(
text: string
): Promise<ActionItems> {
const prompt = `以下の会議メモからアクションアイテムを抽出してください:
${text}
各アイテムには ID、説明、期限、担当者、優先度を含めてください。
期限が不明な場合は null を使用してください。
JSON 形式で出力してください。`;
return await completionWithAutoRepair(prompt);
}
null
の扱いをプロンプトで明示しています。
使用例
typescriptconst meetingNotes = `
今日の会議で決まったこと:
- 来週までにログイン機能を実装する(担当: 田中)【高優先度】
- デザインのレビューを今月中に完了(担当: 佐藤)【中優先度】
- パフォーマンステストを実施(期限未定)【低優先度】
`;
try {
const actionItems = await extractActionItems(
meetingNotes
);
console.log(
`\n抽出されたアクションアイテム: ${actionItems.items.length}件\n`
);
actionItems.items.forEach((item) => {
console.log(`ID: ${item.id}`);
console.log(`説明: ${item.description}`);
console.log(`期限: ${item.dueDate || '未定'}`);
console.log(`担当: ${item.owner || '未割当'}`);
console.log(`優先度: ${item.priority}`);
console.log('---');
});
} catch (error) {
console.error('抽出失敗:', error.message);
}
期待される出力例です。
text抽出されたアクションアイテム: 3件
ID: 1
説明: ログイン機能を実装する
期限: 2025-01-22
担当: 田中
優先度: high
---
ID: 2
説明: デザインのレビューを完了
期限: 2025-01-31
担当: 佐藤
優先度: medium
---
ID: 3
説明: パフォーマンステストを実施
期限: 未定
担当: 未割当
優先度: low
---
配列を含む複雑なスキーマでも、適切に抽出できます。
具体例 4: エラーハンドリングとログ記録
本番環境で使う際の堅牢なエラーハンドリング例です。
typescriptimport * as fs from 'fs/promises';
interface CompletionLog {
timestamp: string;
prompt: string;
response: string;
attempts: number;
success: boolean;
error?: string;
}
ログの型定義です。デバッグやモニタリングに活用できます。
typescriptasync function logCompletion(
log: CompletionLog
): Promise<void> {
const logEntry = JSON.stringify(log) + '\n';
await fs.appendFile('completions.log', logEntry);
}
NDJSON 形式でログファイルに追記します。
typescriptasync function safeCompletion<T>(
prompt: string,
schema: z.ZodType<T>,
options: {
maxRetries?: number;
onError?: (error: Error, attempt: number) => void;
} = {}
): Promise<T> {
const startTime = Date.now();
let lastError: Error | null = null;
let attempts = 0;
try {
const result = await completionWithAutoRepair(
prompt,
options.maxRetries
);
await logCompletion({
timestamp: new Date().toISOString(),
prompt,
response: JSON.stringify(result),
attempts: attempts + 1,
success: true,
});
return result;
} catch (error) {
lastError = error as Error;
if (options.onError) {
options.onError(error as Error, attempts);
}
await logCompletion({
timestamp: new Date().toISOString(),
prompt,
response: '',
attempts: attempts + 1,
success: false,
error: error.message,
});
throw error;
}
}
成功・失敗を問わずログを記録し、エラー時にはコールバックを実行します。
typescript// 使用例
try {
const profile = await safeCompletion(
'東京在住のエンジニアについて',
UserProfileSchema,
{
maxRetries: 5,
onError: (error, attempt) => {
console.warn(
`試行 ${attempt} 失敗: ${error.message}`
);
// アラート送信やメトリクス記録など
},
}
);
console.log('成功:', profile);
} catch (error) {
console.error('最終的に失敗:', error.message);
// フォールバック処理
}
このように、本番環境では詳細なログとエラーハンドリングが不可欠です。
以下の図は、エラーハンドリングとログ記録のフローを示したものです。
mermaidflowchart TD
start["補完リクエスト開始"] --> attempt["API 呼び出し"]
attempt --> success{成功?}
success -->|Yes| log_success["成功ログ記録"]
log_success --> return["データ返却"]
success -->|No| callback["エラーコールバック実行"]
callback --> log_error["エラーログ記録"]
log_error --> check{リトライ可能?}
check -->|Yes| wait["バックオフ待機"]
wait --> attempt
check -->|No| throw["例外スロー"]
style success fill:#ffffcc
style log_success fill:#ccffcc
style log_error fill:#ffcccc
style return fill:#ccffcc
style throw fill:#ffcccc
この図が示すように、各ステップで適切なログを記録し、リトライ可能な場合は再試行、不可能な場合は例外をスローします。
まとめ
gpt-oss で JSON 構造化出力を安定させるには、以下の 3 つの柱が重要です。
1. スキーマの明示的な提示
response_format
パラメータで JSON Schema を指定- システムプロンプト内にもスキーマを明記
- プロンプトで「JSON のみを返す」ことを強調
2. Zod によるスキーマ検証とリトライ
- Zod で型安全なスキーマを定義
safeParse()
で検証エラーを取得- エクスポネンシャルバックオフでリトライ
- 会話履歴を保持して段階的に修正
3. 自動修復メカニズム
- JSON 部分の抽出(コードブロックや説明文の除去)
- Boolean 値の自動修正(
True
→true
) - トークン切断時の括弧補完
- 検証エラー時の修復プロンプト生成
これらを組み合わせることで、gpt-oss の構造化出力の成功率を大幅に向上させられます。特に本番環境では、詳細なログ記録とエラーハンドリングを実装することが不可欠です。
gpt-oss はまだ発展途上のモデルで、Ollama や vllm などのランタイムでの完全な Structured Outputs サポートは今後の課題となっています。しかし、本記事で紹介した技術を活用すれば、現時点でも十分実用的なシステムを構築できるでしょう。
型安全で信頼性の高い AI アプリケーション開発に、ぜひこれらのテクニックを活用してください。
関連リンク
- article
gpt-oss で JSON 構造化出力を安定させる:スキーマ提示・検証リトライ・自動修復
- article
gpt-oss のモデルルーティング設計:サイズ別・ドメイン別・コスト別の自動切替
- article
gpt-oss プロンプト設計チートシート:指示・制約・出力フォーマットの即戦力例 100
- article
gpt-oss を最短デプロイ:CPU/単一 GPU/マルチ GPU 別インフラ設計テンプレ
- article
gpt-oss の全体像と導入判断フレーム:適用領域・制約・成功条件を一挙解説
- article
gpt-oss 技術ロードマップ 2025:機能進化と対応エコシステムの見取り図
- article
CI/CD で更新を自動化:GitHub Actions と WordPress の安全デプロイ
- article
NestJS クリーンアーキテクチャ:UseCase/Domain/Adapter を疎結合に保つ設計術
- article
WebSocket プロトコル設計:バージョン交渉・機能フラグ・後方互換のパターン
- article
MySQL 読み書き分離設計:ProxySQL で一貫性とスループットを両立
- article
Motion(旧 Framer Motion)アニメオーケストレーション設計:timeline・遅延・相互依存の整理術
- article
WebRTC で遠隔支援:画面注釈・ポインタ共有・低遅延音声の実装事例
- blog
iPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
- blog
Googleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
- blog
【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
- blog
Googleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
- blog
Pixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
- blog
フロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来