uttapen

خروجی ساخت‌یافته (JSON)

گرفتن JSON معتبر از مدل با response_format از نوع json_object و json_schema، نمونهٔ Python و Node، و اشتباه‌های رایج با متن و اعداد فارسی.

به‌روزرسانی: ۱۶ شهریور ۱۴۰۵

وقتی خروجی مدل قرار است وارد کد شود (پر کردن فرم، استخراج فیلد از فاکتور، دسته‌بندی)، متن آزاد کافی نیست. response_format به مدل می‌گوید فقط JSON برگرداند و در حالت json_schema شکل دقیق آن را هم تحمیل می‌کند. یوتاپن این فیلد را عیناً عبور می‌دهد.

کدام مدل‌ها

نشان json در صفحهٔ مدل‌ها یعنی مدل response_format را می‌فهمد؛ در GET /v1/models معادلش "response_format" و برای اسکیمای سخت‌گیرانه "structured_outputs" در supported_parameters است. مدلی که structured_outputs ندارد ممکن است json_schema را با 400 رد کند یا فقط به‌صورت «حالت JSON» رفتار کند.

حالت json_object

ساده‌ترین شکل: مدل تضمین می‌کند خروجی یک JSON معتبر است، ولی اسکیما را تو در prompt توضیح می‌دهی.

import json
from openai import OpenAI

client = OpenAI(base_url="https://api.uttapen.ir/v1", api_key="sk-up-...")

resp = client.chat.completions.create(
    model="openai/gpt-5-mini",
    messages=[
        {"role": "system", "content": "فقط JSON برگردان با کلیدهای name، city و phone. مقدار نامعلوم null."},
        {"role": "user", "content": "علی رضایی از اصفهان، شماره ۰۹۱۳۱۲۳۴۵۶۷"},
    ],
    response_format={"type": "json_object"},
)
data = json.loads(resp.choices[0].message.content)
print(data["city"])

قانون providerها: کلمهٔ «JSON» باید جایی در پیام‌ها آمده باشد وگرنه درخواست رد می‌شود. همان system prompt بالا کافی است.

حالت json_schema

اسکیمای JSON Schema می‌دهی و مدل خروجی را دقیقاً با آن تطبیق می‌دهد. با strict: true هیچ فیلد اضافه یا کم نمی‌آید.

schema = {
    "type": "object",
    "properties": {
        "name": {"type": "string"},
        "city": {"type": ["string", "null"]},
        "phone": {"type": ["string", "null"], "description": "با ارقام لاتین و بدون فاصله"},
        "intent": {"type": "string", "enum": ["order", "complaint", "question"]},
    },
    "required": ["name", "city", "phone", "intent"],
    "additionalProperties": False,
}

resp = client.chat.completions.create(
    model="openai/gpt-5-mini",
    messages=[{"role": "user", "content": "علی رضایی از اصفهان زنگ زد و از تأخیر سفارش شاکی بود. ۰۹۱۳۱۲۳۴۵۶۷"}],
    response_format={
        "type": "json_schema",
        "json_schema": {"name": "contact", "strict": True, "schema": schema},
    },
)
contact = json.loads(resp.choices[0].message.content)

در حالت strict همهٔ فیلدها باید در required باشند و additionalProperties: false الزامی است؛ برای فیلد اختیاری از نوع ["string", "null"] استفاده کن. اگر SDK Python را با Pydantic به کار می‌بری، client.chat.completions.parse(..., response_format=ContactModel) همین اسکیما را از کلاس می‌سازد و خروجی را در resp.choices[0].message.parsed می‌دهد.

Node.js با Zod

import OpenAI from "openai";
import { z } from "zod";
import { zodResponseFormat } from "openai/helpers/zod";

const Contact = z.object({
  name: z.string(),
  city: z.string().nullable(),
  intent: z.enum(["order", "complaint", "question"]),
});

const client = new OpenAI({ baseURL: "https://api.uttapen.ir/v1", apiKey: process.env.UTTAPEN_API_KEY });
const resp = await client.chat.completions.parse({
  model: "openai/gpt-5-mini",
  messages: [{ role: "user", content: "علی رضایی از اصفهان، سؤال دربارهٔ گارانتی" }],
  response_format: zodResponseFormat(Contact, "contact"),
});
console.log(resp.choices[0].message.parsed);

اشتباه‌های رایج با متن فارسی

  • ارقام فارسی. مدل‌ها گاهی "phone": "۰۹۱۳..." می‌دهند که برای int() یا regex تو بی‌معنی است. در description فیلد بنویس «ارقام لاتین» و در کد هم یک نرمال‌ساز ساده بگذار: جایگزینی ۰-۹ و ٠-٩ با 0-9.
  • عدد به‌صورت رشته. مبلغ «۱٬۲۵۰٬۰۰۰ تومان» ممکن است رشته برگردد. نوع را در اسکیما number بگذار و در prompt بگو بدون جداکنندهٔ هزارگان.
  • Unicode escape. بعضی مدل‌ها فارسی را به شکل علی می‌نویسند. این JSON کاملاً معتبر است و json.loads درستش می‌کند؛ فقط در لاگ خام زشت است. لازم نیست کاری بکنی.
  • کاراکترهای نامرئی. نیم‌فاصله () و نشانه‌های جهت () در مقدار رشته‌ها می‌آیند و مقایسهٔ دقیق را خراب می‌کنند. قبل از مقایسه با مقادیر ثابت (مثل نام شهر)، از enum استفاده کن یا نرمال‌سازی کن.
  • ی و ک عربی. «ي» (U+064A) و «ك» (U+0643) در ورودی کاربر رایج‌اند و در خروجی مدل برمی‌گردند. اگر مقادیر را در دیتابیس جستجو می‌کنی، هر دو طرف را به «ی» و «ک» فارسی تبدیل کن.
  • متن قبل یا بعد از JSON. در حالت json_object بعضی مدل‌ها بلوک ```json می‌گذارند. json_schema این مشکل را ندارد. اگر مجبوری از مدلی بدون آن استفاده کنی، اولین { تا آخرین } را ببُر.

وقتی مدل json_schema ندارد

با tool calling و tool_choice اجباری، آرگومان‌های تابع در عمل JSON مطابق اسکیمای تو هستند. برای مدل‌های متن‌باز که structured_outputs ندارند ولی tools دارند، این راه قابل‌اعتمادتر از prompt است.

هزینه

اسکیما جزو prompt نیست ولی providerها معمولاً آن را به‌صورت grammar روی decoder اعمال می‌کنند و بعضی برای بار اول کمی کندترند. خروجی JSON فشرده‌تر از متن آزاد است و توکن کمتری مصرف می‌کند. max_tokens را متناسب با بزرگ‌ترین خروجی ممکن بگذار؛ JSON بریده‌شده (finish_reason: "length") قابل parse نیست و پولش را داده‌ای.