पिछले मंगलवार मैं Flashcards Open Source App के एजेंट दस्तावेज़ पढ़ रहा था, और वही परिचित बैकएंड इंजीनियर वाला क्षण फिर सामने आ गया: सब कुछ सुथरा, टाइप-सुरक्षित, साफ़ और थोड़ा असहनीय लग रहा था।
मेरे पास एजेंटों के लिए 17 अलग-अलग टूल कॉल थे। list_cards, get_cards, search_cards, list_due_cards, create_cards, update_cards, delete_cards, फिर decks के लिए वही ढांचा, और उसके बाद टैग, शेड्यूलर की सेटिंग्स, वर्कस्पेस का संदर्भ और रिव्यू इतिहास। कुछ भी टूटा हुआ नहीं था। यही सबसे खीझ पैदा करने वाली बात थी। सब ठीक से चल रहा था।
मुश्किल बस यह थी कि पूरा इंटरफ़ेस ठीक उसी तरह शोरभरा हो गया था, जैसे LLM API अक्सर हो जाती हैं। कोई मानव इंजीनियर दस्तावेज़ एक बार सरसरी तौर पर पढ़कर क्लाइंट बना सकता है और आगे बढ़ सकता है। LLM के पास यह सुविधा नहीं होती। उसे उदाहरणों, विवरणों और त्रुटियों के सहारे उसी बाहरी सतह को बार-बार फिर सीखना पड़ता है। अगर आप एक साधारण इरादे को बहुत सारे टूलों में बाँट देते हैं, तो मॉडल हर बार उसकी कीमत चुकाता है।
यह वही एजेंट परत है जो flashcards-open-source-app.com के पीछे काम करती है, इसलिए मेरे लिए यह अहम था कि उसका बाहरी इंटरफ़ेस सीखना आसान हो, सिर्फ तकनीकी रूप से सही होना काफी नहीं था।
इसीलिए मैंने पूरी व्यवस्था को एक SQL-जैसी DSL में समेट दिया।
Raw PostgreSQL नहीं। मैं उतना साहसी नहीं हूँ।

17 टूल बहुत ज़्यादा थे
पुराने रूप में कुछ मुख्य संसाधनों के पढ़ने-लिखने वाले काम अलग-अलग टूलों में बँटे हुए थे:
- वर्कस्पेस का संदर्भ
- शेड्यूलर की सेटिंग्स
- टैग
- कार्ड
- बकाया कार्ड
- डेक
- रिव्यू इतिहास
बैकएंड की नज़र से देखें तो यह काफी सुथरा लगता है। हर टूल एक ही काम करता है। हर स्कीमा साफ़ है। OpenAPI भी काफ़ी सलीकेदार दिखता है। बिल्कुल वैसा कदम, जैसा कोई पारंपरिक बैकएंड इंजीनियर उठाता है।
एजेंट की नज़र से देखें तो यह बेकार की कागजी मशक्कत है।
अगर मॉडल को "अंग्रेज़ी के ऐसे कार्ड चाहिए जो हाल में अपडेट हुए हों और जल्दी पढ़े जा सकें", तो पहले उसे अंदाज़ा लगाना पड़ता है कि यह list_cards, search_cards या किसी और टूल के अंतर्गत आता है। फिर उसे अनुरोध का ढाँचा याद रखना पड़ता है। फिर पृष्ठांकन। फिर छँटाई। और अगर पढ़ने के बाद एक पंक्ति अपडेट करनी हो, तो एक और टूल चलाना पड़ता है।
इसे चलाया जा सकता है। मैंने चलाया भी।
बस मुझे यह पसंद आना बंद हो गया।
क्या बदला
नया सार्वजनिक अनुबंध सिर्फ एक टूल है:
{
"sql": "SELECT * FROM cards WHERE tags OVERLAP ('english') AND effort_level IN ('fast', 'medium') ORDER BY updated_at DESC LIMIT 20 OFFSET 0"
}
पढ़ने और सीधे-सादे बदलाव, दोनों के लिए वही एक रास्ता।
{
"sql": "UPDATE cards SET back_text = 'Updated answer' WHERE card_id = '123e4567-e89b-42d3-a456-426614174000'"
}
पूरी बात यही है। आंतरिक और बाहरी एजेंट अब एक ही इंटरफ़ेस सीखते हैं, टूल नामों के छोटे से संग्रहालय को नहीं।
पहले एजेंट को यह समझना पड़ता था कि किस काम के लिए कौन-सा टूल मौजूद है।
अब वह ज़्यादातर सीधे काम से शुरू कर सकता है:
- मुझे कार्ड दिखाओ
- टैग के आधार पर छाँटो
- अपडेट समय के हिसाब से क्रम लगाओ
- यह फ़ील्ड बदलो
- इन पंक्तियों को हटाओ
यह उस तरीके से कहीं बेहतर मेल खाता है, जिससे LLM वास्तव में सिस्टमों को टटोलते हैं। वे कुछ कोशिश करते हैं, त्रुटि पढ़ते हैं, फिर दोबारा कोशिश करते हैं। एक SQL-जैसी भाषा इस पूरे चक्र को 17 अलग-अलग टूलों की तुलना में कहीं बेहतर संभालती है।
मैंने SQL क्यों चुना, कोई और JSON संरचना क्यों नहीं
मैंने SQL इसलिए नहीं चुना कि मैं अपने ऐप को डेटाबेस क्लाइंट बनाना चाहता था।
मैंने SQL इसलिए चुना क्योंकि लगभग हर ठीक-ठाक LLM को इसका पहले से कुछ न कुछ अंदाज़ा होता है। मॉडल को मोटे तौर पर पता होता है कि SELECT, UPDATE, WHERE, ORDER BY और LIMIT से क्या होना चाहिए। इससे बहुत-सी व्याख्या बच जाती है।
अगर मैं अपनी कोई अलग JSON DSL बना देता, तो मॉडल को मेरी क्रियाएँ, मेरी परत-दर-परत संरचना, मेरे फ़िल्टर, मेरी अपवादजन्य स्थितियाँ, और उस हफ्ते नामकरण करते समय मेरा जो भी मन रहा हो, सब सीखना पड़ता। अगर मैं उसे SQL-जैसा आकार दूँ, तो वह अक्सर पहली कोशिश में ही सही जवाब के काफी करीब पहुँच जाता है।
यहाँ तक कि जब क्वेरी गलत होती है, तब भी गलती अक्सर ऐसी होती है जिससे आगे बढ़ा जा सके। आम तौर पर बात इनमें से किसी एक पर अटकती है:
- गलत कॉलम नाम
- असमर्थित क्लॉज़
ORDER BYछूट जानाLIMITबहुत बड़ा होना
यह उस स्थिति से कहीं बेहतर है जिसमें "गलत टूल चला दिया, अनुरोध की संरचना भी गलत थी, और अब आधा विनिर्देश फिर से पढ़ना पड़ेगा।"
मुझे कुछ ऐसा चाहिए था जिसे मॉडल पहले से आधा बोलना जानता हो, और फिर बार-बार कोशिश करके उसे साफ कर सके। SQL इस काम में बहुत अच्छा है।
सबसे ज़रूरी बात: यह PostgreSQL नहीं है
इस डिज़ाइन का सबसे अहम हिस्सा यह है कि यह क्या नहीं करता।
यह असली डेटाबेस पर raw SQL सीधे नहीं चलाता।
यह SQL-जैसी स्ट्रिंग को पढ़ता है, प्रकाशित व्याकरण के अनुसार उसकी जाँच करता है, और उसे उन्हीं आंतरिक कार्रवाइयों में बदल देता है जिनका ऐप पहले से इस्तेमाल करता है। वही SQL-जैसी स्ट्रिंग सार्वजनिक DSL है। यह स्टोरेज परत तक जाने वाली कोई सुरंग नहीं है।
इससे मैं वास्तविक डोमेन-व्यवहार को वहीं रख सकता हूँ, जहाँ उसे होना चाहिए:
- वर्कस्पेस की सीमा सर्वर अपने-आप जोड़ता है
- सिस्टम फ़ील्ड पढ़े जा सकते हैं, बदले नहीं जा सकते
- समन्वयन से जुड़ा मेटाडेटा अंदर ही रहता है
- डोमेन के नियम अपनी असली प्रोसेसिंग परत में ही लागू रहते हैं
- स्टोरेज परत बाद में बदली जा सकती है, बिना सार्वजनिक अनुबंध तोड़े
यही वह रेखा थी जिसे मैं पार नहीं करना चाहता था। Flashcards Open Source App ऑफ़लाइन-प्रथम है और समन्वयन को ध्यान में रखकर बनाया गया है। मैं नहीं चाहता कि एजेंट डेटाबेस की मूल तालिकाएँ बदलें और उसे उत्पाद का API समझ लें।
इसलिए अनुबंध साफ है: बाहर से SQL-जैसा, भीतर से डोमेन-सुरक्षित।
व्याकरण मेरी उम्मीद से भी छोटा निकला
पहला संस्करण जान-बूझकर छोटा है:
SELECTINSERTUPDATEDELETE
शुरू में मुझे लगा था कि मैं तार्किक संसाधनों की लंबी सूची रखूँगा। फिर मैंने उसे भी घटा दिया।
आखिर में सार्वजनिक इंटरफ़ेस को उसकी मुख्य इकाइयों के करीब रखा:
cardsdecksworkspacereview_events
इस बदलाव ने पूरी चीज़ को और साफ कर दिया।
tags_summary, due_cards या दूसरे पहले से तय सार-रूप प्रकाशित करने के बजाय, मैंने भाषा में क्वेरी करने की थोड़ी और क्षमता जोड़ दी। खास तौर पर GROUP BY और कुछ एग्रीगेट फ़ंक्शन।
इससे मॉडल सीधे सार-रूप माँग सकता है, बजाय इसके कि वह हर अलग सार-रूप के लिए कोई अलग टूल या संसाधन सीखे, जिसे मैंने पिछले महीने सार्वजनिक किया हो।
उदाहरण के लिए, अब यह संभव है:
SELECT tag, COUNT(*) AS card_count
FROM cards
GROUP BY tag
ORDER BY card_count DESC
LIMIT 20 OFFSET 0;
या:
SELECT rating, COUNT(*) AS reviews
FROM review_events
GROUP BY rating
ORDER BY reviews DESC
LIMIT 10 OFFSET 0;
हर छोटी रिपोर्टिंग ज़रूरत के लिए अलग रास्ते बनाए रखने की तुलना में यह कहीं सरल है।
यह व्याकरण अब भी सीमित है। मैं "पूरा Postgres" होने का नाटक नहीं कर रहा।
मैं जिन चीज़ों का समर्थन नहीं करता:
JOINCTEsubqueries- एक साथ कई statements चलाना
- मनमाने functions
- आंतरिक तालिकाओं तक सीधी पहुँच
- सुरक्षित सिस्टम फ़ील्ड में सीधे बदलाव
यह प्रतिबंधित लगता है क्योंकि यह सचमुच प्रतिबंधित है। अच्छा है। यही बात इसे ईमानदार और संभालने योग्य बनाए रखती है।
नए इंटरफ़ेस की कुछ क्वेरियाँ
कार्ड पढ़ना:
SELECT *
FROM cards
WHERE tags OVERLAP ('english', 'grammar')
AND effort_level IN ('fast', 'medium')
ORDER BY updated_at DESC
LIMIT 20 OFFSET 0;
टैग के आधार पर समूहित कार्ड पढ़ना:
SELECT tag, COUNT(*) AS card_count
FROM cards
GROUP BY tag
ORDER BY card_count DESC
LIMIT 20 OFFSET 0;
डेक बनाना:
INSERT INTO decks (name, effort_levels, tags)
VALUES
('Grammar', ('medium', 'long'), ('english', 'grammar'));
कार्ड अपडेट करना:
UPDATE cards
SET back_text = 'Updated answer',
tags = ('english', 'verbs')
WHERE card_id = '123e4567-e89b-42d3-a456-426614174000';
कार्ड हटाना:
DELETE FROM cards
WHERE card_id IN (
'123e4567-e89b-42d3-a456-426614174000',
'123e4567-e89b-42d3-a456-426614174001'
);
रीव्यू इवेंट की कुल संख्या देखना:
SELECT review_grade, COUNT(*) AS total_reviews
FROM review_events
GROUP BY review_grade
ORDER BY total_reviews DESC
LIMIT 10 OFFSET 0;
इससे पुराने टूल-कैटलॉग का बड़ा हिस्सा समेटा जा सकता है, बिना एजेंट से ऐप की हर इकाई और हर सार-रूप के लिए अलग रास्ता याद करवाए।
एक अच्छा अतिरिक्त लाभ यह भी है कि दस्तावेज़ छोटे हो जाते हैं। अब मुझे बीस अलग-अलग अनुरोध-ढाँचे समझाने की ज़रूरत नहीं है। मैं एक छोटा व्याकरण, दस उदाहरण दिखा सकता हूँ, और मॉडल को करके सीखने दे सकता हूँ।
वह खटकने वाली बात, जिसे मैंने फिर भी रहने दिया
यह डिज़ाइन सरल है, लेकिन कोई जादू नहीं है।
सबसे साफ़-साफ़ दिखने वाला नुकसान यह है कि जैसे ही आप "SQL-जैसा" कहते हैं, लोग असली SQL की आदतें साथ ले आते हैं। उनमें से कुछ काम करेंगी। कुछ नहीं करेंगी। तब ऐप को बहुत साफ़ ढंग से बताना पड़ता है कि क्वेरी समर्थित व्याकरण से बाहर चली गई है।
मैंने एक और समझौता किया है जिसे डेटाबेस शुद्धतावादी पसंद नहीं करेंगे: v1 कर्सर-आधारित पृष्ठांकन की जगह सीधे SQL में LIMIT और OFFSET इस्तेमाल करता है।
मुझे इसका नुकसान पता है। अनुरोधों के बीच डेटा बदल जाए तो पन्ने खिसक सकते हैं। कर्सर-आधारित पृष्ठांकन ज़्यादा सुरक्षित है।
फिर भी मैंने इस इंटरफ़ेस के लिए OFFSET चुना, क्योंकि यह एजेंट बनाने वालों के लिए आसान है, उदाहरणों में दिखाना आसान है, और मॉडल के लिए अतिरिक्त प्रोटोकॉल-ज्ञान के बिना बनाना आसान है। इस API में मेरे लिए पहली बार इस्तेमाल की सादगी, लगातार बदलते डेटा पर बिल्कुल सही पृष्ठांकन व्यवहार से ज़्यादा महत्वपूर्ण है।
अगर यह समझौता व्यवहार में परेशानी देने लगे, तो मैं बाद में प्रकाशित भाषा बदल सकता हूँ। अभी के लिए सादगी जीतती है।
असली जीत सिर्फ कम रास्ते होना नहीं थी
असली बड़ी जीत यह थी कि API अब उस तरीके से मेल खाती है, जिससे भाषा मॉडल स्वाभाविक रूप से सिस्टमों को परखते हैं।
उन्हें हर टूल का संग्रहालय-भ्रमण नहीं चाहिए। उन्हें एक ऐसी जगह चाहिए जहाँ वे कोई इरादा आज़मा सकें और गलत होने पर उपयोगी त्रुटि मिल सके।
इसीलिए यह पिछली रूपरेखा से बेहतर महसूस होता है। यह सिर्फ छोटा नहीं है। इसका अंदाज़ा लगाना आसान है।
एजेंट-आधारित उत्पादों के लिए, आसानी से समझ आ जाना अक्सर सुंदर आंतरिक संरचना से ज़्यादा काम का निकलता है।
मैं यह तरीका कहाँ नहीं अपनाऊँगा
अगर आपका उत्पाद बहुत हद तक ऐसी जटिल डोमेन-क्रियाओं पर निर्भर करता है जो CRUD-जैसी नहीं हैं, तो मैं यह तरीका इस्तेमाल नहीं करूँगा।
अगर असली काम submit_review, run_scheduler, या merge_learning_state जैसा कुछ है, तो हर चीज़ को UPDATE बनाकर दिखाना आम तौर पर API को और खराब कर देता है। ऐसे मामलों में मैं जटिल कामों के लिए स्पष्ट आदेश रखूँगा, और SQL-जैसी DSL को व्यापक पढ़ने वाली परत, CRUD और हल्की रिपोर्टिंग के लिए इस्तेमाल करूँगा।
यहीं कई टीमें उलटा कर बैठती हैं। या तो वे सीधी स्टोरेज परत खोल देती हैं, जो लापरवाही है, या हर छोटी क्रिया को किसी अलग समर्पित रास्ते में बाँध देती हैं, जो थका देने वाला है।
बीच का उपयोगी रास्ता यह है:
- व्यापक डेटा-पहुँच के लिए SQL-जैसी DSL
- डोमेन-प्रधान कार्रवाइयों के लिए स्पष्ट आदेश
यह विभाजन दोनों अतियों से कहीं ज़्यादा यथार्थवादी लगता है।
मुझे यह दिशा क्यों पसंद है
इसका छोटा सार बहुत सरल है।
मैंने टूलों की फैली हुई सूची को एक ऐसी क्वेरी भाषा से बदल दिया जिसे ज़्यादातर LLM पहले से आधा बोलना जानते हैं।
इंजीनियरिंग की भाषा में कहूँ तो बात बस थोड़ी कम रोमांचक है:
मैंने असली बैकएंड संरचना, समन्वयन से जुड़ा व्यवहार और अनिवार्य नियम वहीं रहने दिए जहाँ वे पहले थे, और ऊपर सिर्फ एक पतला, ज़्यादा सीखने योग्य अनुबंध रख दिया।
मुझे यह सही विभाजन लगता है।
अगर आप एजेंटों के लिए API बना रहे हैं, तो मैं "इंसानों के लिए सबसे साफ OpenAPI इंटरफ़ेस क्या है?" से शुरुआत नहीं करूँगा। मैं "मॉडल सबसे कम दस्तावेज़ और सबसे कम पुनः-प्रयासों के साथ जल्दी क्या समझ सकता है?" से शुरुआत करूँगा।
कई बार जवाब कोई नया API रास्ता नहीं होता।
कई बार जवाब एक बहुत छोटी भाषा होती है।
अगर आप स्वयं उत्पाद देखना चाहें, तो वह यहाँ है: flashcards-open-source-app.com
अगर आप कोड देखना चाहें, तो GitHub परियोजना यहाँ है: github.com/kirill-markin/flashcards-open-source-app। यह मेरा MIT लाइसेंस वाला मुक्त-स्रोत प्रोजेक्ट है।




