एजेंट से विश्वसनीय JSON कैसे प्राप्त करें (Schema, Constrained Decoding, Retries)
पिछले साल मैंने जो पाइपलाइन बनाई थी वो रात 2 बजे क्रैश हो गई, क्योंकि मॉडल ने अपने JSON को एक दोस्ताना प्रस्तावना में लपेटने का फैसला किया: "ज़रूर! यहाँ वो डेटा है जो आपने माँगा:" और उसके बाद एक कोड फेंस। मेरा पार्सर, जिसे उम्मीद थी कि रिस्पॉन्स बॉडी { से शुरू होगी, फेल हो गया। रिट्राई उसी दोस्ताना मॉडल से टकराई, वही दोस्ताना प्रस्तावना मिली, और फिर से फेल। तीन रिट्राई, तीन विफलताएँ, एक इंसान को जगाना पड़ा।
परेशान करने वाली बात यह है कि कोड फेंस के अंदर JSON बिल्कुल सही था। मॉडल ने मुश्किल हिस्सा — निष्कर्षण — सही तरीके से किया। बस वो पहले नमस्ते कहने से खुद को नहीं रोक सका। मैंने अपनी मेहनत प्रॉम्प्ट पर लगाई, कॉन्ट्रैक्ट पर बिल्कुल नहीं।
यही पूरी समस्या है एक किस्से में। मॉडल आमतौर पर वो डेटा देने में सक्षम होता है जो आप चाहते हैं। विफलता जोड़ों में होती है: प्रस्तावना, ट्रेलिंग कॉमा, वो फील्ड जिसे उसने रीनेम किया, वो enum वैल्यू जो उसने बना ली, वो रिस्पॉन्स जो ऑब्जेक्ट के बीच में कट गई क्योंकि आप टोकन लिमिट बढ़ाना भूल गए। ये कोई रीजनिंग विफलताएँ नहीं हैं। ये कॉन्ट्रैक्ट विफलताएँ हैं, और आप इनसे मेकेनिज्म से बचाव करते हैं, बेहतर प्रॉम्प्टिंग से नहीं।
संक्षेप में: JSON माँगें नहीं, मॉडल को इसे उत्पन्न करने के लिए constraint करें। अपने प्रोवाइडर के schema-constrained structured output मोड का उपयोग करें (अब OpenAI, Anthropic, और Google में मानक), रिजल्ट को अपने schema से validate करें, और उन मामलों के लिए एक repair-retry लूप रखें जो फिर भी फिसल जाते हैं। नीचे सबसे कमज़ोर से सबसे मज़बूत तक की सीढ़ी है, और मैं वास्तव में कहाँ रुकता हूँ।
सीढ़ी, सबसे कमज़ोर से सबसे मज़बूत तक
मोटे तौर पर पाँच पायदान हैं, और ज़्यादातर लोग जितना सोचते हैं उससे एक पायदान नीचे बैठे हैं।
पायदान एक: prompt-and-pray। आप लिखते हैं "केवल JSON में जवाब दें, कोई और टेक्स्ट नहीं" और उम्मीद करते हैं। यह डेमो में इतनी बार काम करता है कि यह खतरनाक रूप से भ्रामक है। यह प्रस्तावनाओं, कोड फेंस, ट्रेलिंग प्रोज़, और किसी भी मॉडल पर फेल होता है जो उस दिन भावुक हो। इसे शिप न करें।
पायदान दो: JSON mode। एक प्रोवाइडर फ्लैग जो गारंटी देता है कि आउटपुट वाक्य-रचनात्मक रूप से वैलिड JSON है — यह पार्स होगा। यह गारंटी नहीं देता कि आउटपुट आपके आकार से मेल खाता है। OpenAI के डॉक्स स्पष्ट हैं कि यह पुराना, कमज़ोर विकल्प है: "Structured Outputs JSON mode का विकास है। जबकि दोनों वैलिड JSON का उत्पादन सुनिश्चित करते हैं, केवल Structured Outputs schema adherence सुनिश्चित करता है" (OpenAI structured outputs guide)। JSON mode प्रस्तावना की समस्या रोकता है। यह किसी गायब फील्ड या hallucinated enum के बारे में कुछ नहीं करता।
पायदान तीन: schema-constrained structured outputs। आप प्रोवाइडर को एक JSON Schema देते हैं और वो जनरेशन को constraint करता है ताकि आउटपुट उसके अनुरूप हो — हर ज़रूरी फील्ड मौजूद, हर टाइप सही, हर enum वैल्यू आपकी लिस्ट से। डेटा निष्कर्षण के लिए यही पायदान है।
पायदान चार: tool / function calling। मॉडल एक ऐसे फंक्शन का call emit करता है जिसके parameters एक JSON Schema हैं। मेकेनिकली यह पायदान तीन के समान constraint है, response body के बजाय tool arguments पर लागू। आप इसे तब चाहते हैं जब मॉडल यह चुन रहा हो कि कौन सा एक्शन लेना है, न कि सिर्फ एक फॉर्म भरना।
पायदान पाँच: constrained / grammar-based decoding जो आप खुद चलाते हैं। ओपन-वेट मॉडल के साथ आप डीकोडर को नियंत्रित करते हैं, इसलिए आप हर कदम पर टोकन logits को मास्क कर सकते हैं ताकि grammar को तोड़ने वाला कोई भी टोकन forbidden हो। यह सबसे मज़बूत गारंटी है — मॉडल literally एक invalid टोकन emit नहीं कर सकता — और सबसे ज़्यादा काम भी।
ज़्यादातर production कोड को hosted प्रोवाइडर का उपयोग करके पायदान तीन या चार पर बैठना चाहिए। मैं दिखाता हूँ कि प्रत्येक कैसा दिखता है।
पायदान तीन: schema-constrained structured output
तीनों प्रमुख प्रोवाइडर अब यह ship करते हैं, और आकार एक बार सीखने के लिए काफी समान है।
OpenAI response_format के साथ json_schema और strict: true का उपयोग करता है। strict फ्लैग ही है जो "कृपया" को "गारंटीशुदा" में बदलता है। इसकी आवश्यकताएँ विशिष्ट हैं और आसानी से ट्रिप हो सकती हैं: additionalProperties हर ऑब्जेक्ट पर false होना चाहिए, और हर property required array में दिखनी चाहिए। वैकल्पिक फील्ड को null के साथ union के रूप में व्यक्त किया जाता है, न कि छोड़ने से (OpenAI structured outputs guide)।
Anthropic ने Claude API में वही क्षमता जोड़ी — अब generally available, beta header (structured-outputs-2025-11-13) की अब ज़रूरत नहीं। आप JSON response body के लिए output_config.format पास करते हैं, और guaranteed-valid tool inputs के लिए tool definitions पर strict: true सेट करते हैं। Schema subset OpenAI के समान है: हर ऑब्जेक्ट पर additionalProperties: false, हर property required में (Anthropic structured outputs docs)।
Google का Gemini इसे generation config के माध्यम से करता है: responseMimeType को application/json पर सेट करें और अपना schema responseSchema के रूप में पास करें। उनके SDKs आपको Python में Pydantic या TypeScript में Zod के साथ schema define करने देते हैं बजाय JSON Schema हाथ से लिखने के (Gemini structured output docs)।
यहाँ OpenAI के साथ TypeScript का आकार है। मैं Zod में schema define करता हूँ और convert करता हूँ, क्योंकि JSON Schema को हाथ से maintain करना ही schema और आपके types के अलग होने का तरीका है।
import OpenAI from "openai";
import * as z from "zod";
const Ticket = z.object({
priority: z.enum(["low", "medium", "high", "urgent"]),
category: z.enum(["billing", "bug", "feature", "other"]),
summary: z.string().max(280),
needs_human: z.boolean(),
});
const client = new OpenAI();
const res = await client.chat.completions.create({
model: "gpt-4o-2024-08-06",
messages: [
{ role: "system", content: "Classify the support ticket." },
{ role: "user", content: ticketText },
],
response_format: {
type: "json_schema",
json_schema: {
name: "ticket",
strict: true,
schema: z.toJSONSchema(Ticket),
},
},
});
const parsed = Ticket.parse(JSON.parse(res.choices[0].message.content));
z.toJSONSchema() Zod 4 में built-in है और default रूप से Draft 2020-12 को target करता है (Zod JSON Schema docs)। ध्यान दें कि priority और category fields enums हैं — strict mode चालू होने पर, मॉडल "critical" return नहीं कर सकता, भले ही वो चाहे, क्योंकि वो टोकन allowed set में नहीं है। यह single property बग की एक पूरी class को मारती है।
Constraint आपको किस चीज़ से नहीं बचाता: यह आकार की गारंटी देता है, अर्थ की नहीं। एक constrained मॉडल खुशी से उस ticket पर एक real category डाल देगा जिसे उसने गलत पढ़ा। इसलिए मैं अंत में Ticket.parse() फिर भी चलाता हूँ — आंशिक रूप से अपने types और wire format के बीच schema drift के खिलाफ, आंशिक रूप से क्योंकि मैं कभी-कभी semantic checks जोड़ता हूँ जो JSON Schema express नहीं कर सकता।
पायदान चार: tool calling, और इसे कब prefer करें
Tool calling वही constraint है जो एक अलग target पर इशारा कर रहा है। "इस response body को भरें" के बजाय, "यदि आप एक tool call करते हैं, तो आपके arguments उसके schema से मेल खाने चाहिए।" Anthropic का strict tool use इस गारंटी को स्पष्ट करता है: strict: true के साथ, "Claude के tool inputs आपके schema से exactly match करते हैं" (Anthropic structured outputs docs)।
Plain structured response पर tools को prefer करने का कारण choice है। जब मॉडल search_orders, issue_refund, और escalate में से चुनता है, प्रत्येक का अपना argument shape है, तो आप function calling चाहते हैं — कौन सा tool plus उस tool के लिए validated args, यही agent-loop primitive है। जब आपको एक call से एक fixed shape वापस चाहिए, तो structured response body सरल है और आप tool-call ceremony को skip करते हैं।
एक practical TypeScript-ish loop sketch:
// model returns tool_calls; each has a name + JSON arguments
for (const call of message.tool_calls ?? []) {
const tool = tools[call.function.name];
if (!tool) throw new Error(`hallucinated tool: ${call.function.name}`);
// strict mode guarantees this parses to the tool's shape,
// but validate anyway — providers differ, and you own the contract
const args = tool.schema.parse(JSON.parse(call.function.arguments));
const result = await tool.run(args);
// feed result back into the conversation, continue the loop
}
मैं hallucinated tool guard strict mode के साथ भी रखता हूँ, क्योंकि कौन से tools exist हैं का set और प्रति tool valid arguments का set stack के अलग-अलग हिस्सों द्वारा enforce होता है, और मैं यह assume नहीं करना चाहता कि दोनों हर प्रोवाइडर पर airtight हैं।
पायदान पाँच: constrained decoding जो आप खुद चलाते हैं
जब आप ओपन-वेट मॉडल चलाते हैं, तो आपके पास डीकोडर है, जिसका मतलब है कि आप token level पर grammar enforce कर सकते हैं: हर कदम पर, किसी भी ऐसे token को mask out करें जो partial output को आपके schema के खिलाफ un-parseable बना दे। मॉडल invalid JSON नहीं बना सकता क्योंकि invalid tokens कभी menu पर नहीं होते।
ज़्यादातर लोग Outlines से शुरू करते हैं, जो regex, JSON Schema, और EBNF में full context-free grammars को support करता है। यह एक real capability है जिसकी real cost है: early finite-state-machine approach complex schemas को compile करने में लंबा समय ले सकती है, और एक JSON-schema benchmark ने पाया कि compilation timeouts ने heavy schemas पर इसकी compliance rate को नीचे खींचा। ज़्यादातर high-throughput serving stacks जिस backend पर गए वो XGrammar है — vLLM और SGLang जैसे engines में default structured-generation backend, बहुत कम per-token overhead के साथ (XGrammar)। अगर आप self-hosting कर रहे हैं और structured output hot path पर है, ecosystem वहाँ गया।
इसे reach करने से पहले जानने योग्य एक caveat: hard grammar constraints reasoning के साथ बुरी तरह interact कर सकते हैं। Published work है जो argue करता है कि structure को बहुत जल्दी force करना model की actual answer quality को hurt कर सकता है बनाम उसे prose में सोचने देना और बाद में structure करना — constrained decoding का तथाकथित alignment tax (arXiv)। Practical read: output को constraint करें, लेकिन यदि task मुश्किल है तो मॉडल को पहले सोचने की जगह दें। मॉडल को JSON में सोचने के लिए force न करें।
Failure modes, और वास्तव में कैसे बचाव करें
Constraint shape को handle करता है। ये वो चीज़ें हैं जो यह नहीं करता, और ये वो हैं जो आपको page करती हैं।
Truncation। सबसे common। मॉडल object के बीच में output token limit तक पहुँचता है और आपको आधा JSON document मिलता है जिसे कोई constraint बचा नहीं सकता — यह रुकने तक valid था। Defenses: schema के worst case के लिए उदारता से max_tokens सेट करें, और parse failure पर blindly retry करने से पहले जाँचें कि क्या response length के कारण रुका। Same limit के साथ truncation को retry करना बस पैसे जलाना है।
Schema drift। आपके code का expected shape और वो schema जो आपने model को भेजा sync से बाहर हो जाते हैं — आमतौर पर इसलिए कि किसी ने JSON Schema को hand-edit किया। Fix यह है कि इसे कभी hand-write न करें: उसी Zod या Pydantic type से JSON Schema derive करें जिससे आप parse करते हैं, ताकि truth का एक ही source हो। Instructor exactly इसी पर lean करता है, client को wrap करके ताकि एक Pydantic model schema define करे, response validate करे, और failure पर retry करे — सब एक object में।
Hallucinated enum values। Prompt-and-pray में एक model "critical" invent करता है जब आपका enum low|medium|high हो। Strict structured output इसे पायदान तीन पर ही खत्म कर देता है — invalid token अनुपलब्ध है। यदि आप constrained mode पर नहीं हैं, तो एक validating parser इसे एक catchable error में बदल देता है बजाय downstream surprise के।
Over-nesting और silent renames। Model customer_name के नीचे सही data return करता है जब आपने name माँगा था, या सब कुछ एक extra { "result": ... } layer में wrap करता है। additionalProperties: false plus एक exhaustive required list ही वो है जो model को fields add करने या rename करने से रोकता है; यही कारण है कि OpenAI और Anthropic दोनों strict mode के लिए उन दो constraints को mandatory बनाते हैं।
Repair loop सब कुछ जोड़ता है। जब validation fail हो, तो सिर्फ same call retry न करें — error वापस feed करें:
import instructor
from pydantic import BaseModel
client = instructor.from_provider("openai/gpt-4o")
class Ticket(BaseModel):
priority: str
summary: str
# on a validation error, Instructor sends the Pydantic error message
# back to the model and asks it to fix the specific field — then re-validates
ticket = client.chat.completions.create(
response_model=Ticket,
max_retries=2,
messages=[{"role": "user", "content": ticket_text}],
)
Model को specific validation error दिखाना ("priority must be one of low, medium, high") blind retry से कहीं ज़्यादा effective है, क्योंकि आपने उसे exactly बताया कि क्या fix करना है। लेकिन retries cap करें — दो पर्याप्त हैं। एक model जो तीन बार validate करने में fail होता है वो आमतौर पर किसी ऐसे कारण से fail हो रहा है जो ज़्यादा retries solve नहीं करेंगे, और आप gracefully degrade करना चाहते हैं: एक typed error return करें, default पर fallback करें, या human को route करें। Poison input पर forever loop करना ही मेरा रात 2 बजे का page था।
Use case के हिसाब से मैं वास्तव में क्या reach करता हूँ
Hosted model के खिलाफ data extraction या classification के लिए — common case — अपने provider के strict schema-constrained mode का उपयोग करें, Zod या Pydantic से schema derive करें, और उसी type से result validate करें। यह पायदान तीन plus एक validating parser है, और यह ज़्यादातर लोगों की building को cover करता है।
Actions choose करने वाले agents के लिए, strict tool calling का उपयोग करें। Which-tool-plus-validated-args shape वो चीज़ है जो आप चाहते हैं, और strict flag argument-validation gap को बंद करता है। उन tool names के लिए guard रखें जिन्हें आप नहीं पहचानते।
Hot path पर self-hosted open-weight models के लिए, अपने serving engine के माध्यम से grammar-constrained decoding का उपयोग करें — XGrammar-class backends अब इतने fast हैं कि इसे hand-roll करने का कोई ख़ास कारण नहीं। यदि task मुश्किल है तो model को पहले prose में reason करने दें, फिर structured part को constrain करें।
और पायदान चाहे कोई भी हो: एक schema से boundary पर validate करें जो आपका हो, और graceful degradation के साथ एक bounded repair loop रखें। Constraint model को garbage emit करने से रोकता है। Validator आपकी assumptions को garbage होने से रोकता है। जब मैंने prompt पर trust करना बंद किया और contract को enforce करना शुरू किया, तब से दोनों मेरी pipeline और मेरी नींद बेहतर हो गई।
