जिसने भी एजेंट उत्पाद जारी किया है, वह इस अनुभव को जानता है: डेमो चालू करना तेज़ है, लेकिन उसे ऐसी चीज़ में बदलना कठिन है जिस पर उपयोगकर्ता हर दिन भरोसा करे—जो उसकी अपनी मशीन पर दिन भर बिना ठप हुए चले। और कठिन हिस्सा मॉडल को जोड़ना नहीं है। कठिनाई मॉडल के चारों ओर मौजूद पूरी परत में है।
इस परत के कई नाम हैं; मैं इसे Agent Harness कहूँगा। यह “बड़े मॉडल” और “उत्पाद की सुविधाओं” के बीच बैठती है और यही वास्तविक रनटाइम है: यह उपयोगकर्ता के एक अनुरोध को मॉडल के साथ बातचीत के कई दौर में बदलती है, बीच-बीच में टूल कॉल जोड़ती है, परिणाम वापस भेजती है, संदर्भ भरने से पहले उसे संक्षिप्त करती है, नेटवर्क की रुकावटों पर दोबारा प्रयास करती है और प्रक्रिया क्रैश होने के बाद भी बातचीत बहाल करती है। मॉडल सोचता है; हार्नेस उस सोच को कार्रवाइयों की भरोसेमंद शृंखला में बदलता है।
Orkas उपयोगकर्ता की अपनी मशीन पर चलने वाला डेस्कटॉप एजेंट ऐप है और उसका हार्नेस पूरी तरह क्लाइंट पर रहता है। यह लेख बताता है कि वह परत कैसे बनाई गई है: उसे परतों में कैसे बाँटा गया है, रन लूप कैसा है, टूल और मॉडल को कैसे अमूर्त किया गया है और स्मृति व सत्र कैसे संभाले जाते हैं। कोड के विवरण से पहचान योग्य जानकारी हटाकर उन्हें सामान्यीकृत किया गया है, लेकिन इंजीनियरिंग संरचना वास्तविक है।
परतें
किसी एजेंट उत्पाद को खोलकर देखें तो मोटे तौर पर ये परतें मिलती हैं, नीचे से ऊपर क्रम में:
┌─────────────────────────────────────────┐
│ उत्पाद की सुविधाएँ (चैट / कौशल / कनेक्टर / सिंक) │
├─────────────────────────────────────────┤
│ Agent Harness (रन लूप / टूल / सत्र) │
├─────────────────────────────────────────┤
│ प्रदाता अमूर्तन (कई LLM विक्रेताओं को एकरूप बनाना) │
├─────────────────────────────────────────┤
│ आधारभूत संरचना (टाइप / त्रुटियाँ / लॉगिंग / कॉन्फ़िगरेशन) │
└─────────────────────────────────────────┘यहाँ एक महत्वपूर्ण निर्णय पहले से निहित है: मॉडल का सारा अनुमान-प्रक्रमण क्लाइंट पर होता है. डेस्कटॉप ऐप कोई हल्का क्लाइंट नहीं है—उसमें हार्नेस स्वयं मौजूद है और वह सीधे मॉडल को कॉल करता है। सर्वर केवल खाते, बहु-डिवाइस सिंक और बिलिंग संभालता है; वह एजेंट बिल्कुल नहीं चलाता। इस निर्णय ने आगे की लगभग हर चीज़ को आकार दिया: सत्र स्थानीय डिस्क पर सहेजे जाते हैं, टूल सीधे उपयोगकर्ता की कार्य डायरेक्टरी में काम करते हैं और संवेदनशील डेटा कभी मशीन से बाहर नहीं जाता।
हार्नेस स्वयं कुछ हिस्सों में बँटा है: रन लूप (रनर), सत्र, टूल, प्रदाता परत और स्मृति। आइए इन्हें एक-एक करके देखें।
रन लूप: एक स्ट्रीमिंग जनरेटर
हार्नेस का केंद्र रनर है। एक वाक्य में उसका काम है: मॉडल से बार-बार बात करना, जब तक वह न कहे “मेरा काम पूरा हो गया।”
इसे असिंक्रोनस जनरेटर के रूप में लागू किया गया है और यह चुनाव महत्वपूर्ण है। एजेंट का एक रन “अनुरोध भेजें, परिणाम की प्रतीक्षा करें” से कहीं अधिक है। बीच में बहुत कुछ होता है—मॉडल टोकन भेज रहा है, वह टूल कॉल करना चाहता है, टूल का काम पूरा हुआ, संदर्भ इतना लंबा हो गया कि संक्षेपण शुरू करना पड़ा, नेटवर्क विफल हुआ और हम दोबारा प्रयास कर रहे हैं। कॉलबैक या साधारण Promise के साथ इन बीच की अवस्थाओं को कॉल करने वाले के सामने साफ़ ढंग से लाना कठिन होता है। जनरेटर में ये सभी घटनाओं की एक स्ट्रीम बन जाती हैं, जिन्हें yield के माध्यम से भेजा जाता है:
type AgentRunEvent =
| { type: "text_delta"; text: string } // model emitting tokens
| { type: "tool_start"; name: string; input: unknown } // a tool starts executing
| { type: "tool_end"; name: string; result: string } // a tool finished
| { type: "compaction"; tokensBefore: number; tokensAfter: number } // context compacted
| { type: "retry"; attempt: number; reason: string } // error, retrying
| { type: "done"; result: AgentRunResult } // terminalUI इस घटना स्ट्रीम की सदस्यता लेता है और मॉडल का आउटपुट व टूल निष्पादन वास्तविक समय में दिखाता है। गैर-स्ट्रीमिंग प्रवेश बिंदु आंतरिक रूप से केवल “स्ट्रीम को अंत तक पढ़ो, अंतिम परिणाम लो done” है—दोनों प्रवेश बिंदु एक ही कार्यान्वयन साझा करते हैं, इसलिए तालमेल बिगड़ने वाला कोई दूसरा कोड पथ नहीं है।
एक बारी के भीतर क्या होता है
विस्तार से देखें तो एक बारी मोटे तौर पर ऐसी होती है:
- उपयोगकर्ता का संदेश, जिसमें चित्र भी हो सकते हैं, सत्र के इतिहास में जोड़ें।
- वर्तमान में उपलब्ध टूल, कौशल सूचकांक आदि जोड़ते हुए सिस्टम प्रॉम्प्ट तैयार करें।
- मॉडल स्ट्रिंग को पार्स करें और उससे विशिष्ट प्रदाता व मॉडल ID निर्धारित करें।
- सभी टूल को मॉडल की समझ में आने वाली परिभाषाओं में बदलें और इतिहास के साथ भेजें।
- मॉडल की प्रतिक्रिया स्ट्रीम पढ़ें,
yieldके माध्यम से पाठ को एक-एक टोकन करके भेजते हुए मॉडल की सभी टूल कॉल इकट्ठी करें। - स्ट्रीम समाप्त होने पर मॉडल के रुकने का कारण देखें:
- यदि वह है
tool_use, तो मॉडल टूल कॉल करना चाहता है—टूल चलाएँ, फिर चरण 5 पर लौटकर मॉडल से दोबारा पूछें। - अन्यथा बारी पूरी हो गई है—परिणाम तैयार करें,
yield done, और लौट जाएँ।
यहाँ एक नियम हमेशा कायम रखना ज़रूरी है: मॉडल की हर टूल कॉल के तुरंत बाद इतिहास में उससे मेल खाता टूल परिणाम होना चाहिए. मॉडल API इस जोड़ी को सख़्ती से लागू करती है—इसे तोड़ें तो अगला अनुरोध या तो त्रुटि देगा या अटक जाएगा। सत्र के अपने आप ठीक होने की चर्चा में हम इस पर फिर लौटेंगे।
टूल कॉल को सही टूल तक कैसे पहुँचाया जाता है
मॉडल खुद टूल नहीं चलाता; वह केवल कहता है “मैं कॉल करना चाहता हूँ read_file इन आर्ग्युमेंट के साथ।” रनर को यह आशय मिलते ही:
for (const call of toolUseBlocks) {
yield { type: "tool_start", name: call.name, input: call.input };
const tool = this.tools.get(call.name);
const ctx = { workingDir, signal, state: { sandboxEnv } };
const result = await tool.execute(call.input, ctx);
// append the result to the session as a tool-result message
session.addToolResult(call.id, result);
yield { type: "tool_end", name: call.name, result: result.content };
}टूल क्रम से चलते हैं, उनके परिणाम इतिहास में उसी क्रम में लिखे जाते हैं जिसमें मॉडल ने उन्हें घोषित किया था, और फिर उन परिणामों के साथ मॉडल से दोबारा पूछा जाता है। परिणाम देखकर मॉडल कोई और टूल कॉल कर सकता है या अपना अंतिम उत्तर दे सकता है। यही "पूछें → कॉल करें → उत्तर दें → फिर पूछें" चक्र एजेंट को कई चरणों वाले कार्य पूरे करने देता है।
एक बात का अलग से उल्लेख ज़रूरी है: कुछ टूल छवियाँ लौटाते हैं — स्क्रीनशॉट, बनाई गई तस्वीरें। लेकिन कई मॉडल टूल-परिणाम चैनल में छवियाँ स्वीकार नहीं करते। Orkas इसे संभालने के लिए छवि को एक अलग उपयोगकर्ता संदेश में रखता है, जो रखा जाता है बाद में टूल के परिणाम के — मॉडल पहले पढ़ता है कि "टूल ने यह पाठ लौटाया," फिर ठीक अगले संवाद-चरण में उससे जुड़ी छवि देखता है। यह छोटा-सा समझौता प्रदाताओं की क्षमताओं के अंतर से पार पाने का तरीका है।
संदर्भ सीमा पार होने वाली हो तो क्या करें
लंबे कार्यों में सबसे आम बाधा संदर्भ विंडो होती है। Orkas उसके भरने का इंतज़ार नहीं करता — वह तय करता है 60% की सीमा: टूल के हर दौर के बाद वह अनुमान लगाता है कि वर्तमान टोकन विंडो का कितना हिस्सा घेरते हैं, और जैसे ही यह 60% से आगे जाता है, वह पहले ही संक्षेपण शुरू कर देता है।
संक्षेपण में मॉडल से पिछली बातचीत का सार बनाने को कहा जाता है, फिर पुराने संदेशों की जगह वह सार रख दिया जाता है और केवल सबसे हाल का अंतिम हिस्सा बचता है। सुनने में सरल है, लेकिन एक पेच है: इस बदलाव के बाद बचा हुआ हिस्सा किसी "बिना कॉल वाले टूल परिणाम" से शुरू नहीं होना चाहिए — "मेल खाने वाली कॉल के बिना परिणाम" नहीं हो सकता, वरना जोड़ी बनाए रखने का अनिवार्य नियम फिर टूट जाता है। इसलिए संक्षेपण का तर्क सुनिश्चित करता है कि काटने की जगह एक साफ़ सीमा पर हो।
यहाँ एक और दिलचस्प चुनाव समझने लायक है: "60% पर पूरे हिस्से का सार बना दें" जैसा मोटा तरीका क्यों, कोई अधिक बारीक तरीका क्यों नहीं — जैसे हर संदेश को अंक देकर महत्व के अनुसार छाँटना, टूल आउटपुट से संरचित जानकारी निकालना, या कई स्तरों वाला स्मृति-वृक्ष बनाए रखना? शोधपत्रों में ये तरीके बहुत अच्छे लगते हैं, लेकिन हमने जानबूझकर वह रास्ता नहीं चुना, तीन कारणों से।
पहला, कैशिंग। मॉडल का प्रॉम्प्ट कैश शुरुआती हिस्से के आधार पर काम करता है: जब तक इतिहास का शुरुआती हिस्सा नहीं बदलता, वह हिस्सा कैश से मिलता है, जिससे लागत और विलंब दोनों बचते हैं। बारीक संक्षेपण इतिहास के बीच के हिस्से को लगातार दोबारा लिखता है, जिससे कैश में रखा शुरुआती हिस्सा बार-बार टूटता है — हर संपादन बड़े हिस्से को फिर से प्रोसेस करने पर मजबूर करता है। "उसे रहने दें, फिर सीमा आने पर एक बार संक्षिप्त करें" रणनीति अधिकांश संवाद-चरणों में शुरुआती हिस्से को स्थिर रखती है; केवल वह एक संक्षेपण उसे अमान्य करता है। यह कैश के लिए कहीं बेहतर है।
दूसरा, जटिलता। "हर टूल कॉल की जोड़ी होनी चाहिए" वाला अनिवार्य नियम, जिस पर हम बार-बार ज़ोर दे रहे हैं — आप इतिहास को जितनी बारीकी से छाँटते हैं, किसी कोने में उसे तोड़ने की संभावना उतनी ही बढ़ती है। पूरे हिस्से के सार को केवल एक साफ़ कट-बिंदु सुरक्षित रखना होता है; गलती होने की जगहें लगभग दस गुना कम हो जाती हैं। अपवाद की एक श्रेणी कम होने का मतलब है वास्तविक उपयोग में गड़बड़ी की एक श्रेणी कम होना।
तीसरा, बेहतर मॉडलों का लाभ लेना। पिछले कुछ वर्षों में संदर्भ विंडो लगातार बड़ी हुई हैं, और मॉडल लंबे संदर्भ को बेहतर से बेहतर संभाल रहे हैं। आज किसी विस्तृत संक्षेपण एल्गोरिदम पर बहुत मेहनत लगाना मूलतः ऐसी समस्या से लड़ना है जो छोटी हो रही है — संभव है कि आप उसे ठीक से समायोजित करना पूरा करें और तभी अगली पीढ़ी अपनी विंडो दोगुनी कर दे, जिससे आपकी जटिलता केवल बोझ बनकर रह जाए। इसके उलट, सार बनाने का काम मॉडल को सौंपने पर मॉडल के सुधरने के साथ यह अपने आप बेहतर होता है: वह महत्वपूर्ण बातें जितनी अच्छी तरह चुनता है, सार की गुणवत्ता उतनी बढ़ती है, और हमें एक पंक्ति भी नहीं बदलनी पड़ती। जिस जटिलता को मॉडल आपके लिए संभाल सकता है, उसे आपको खुद नहीं संभालना चाहिए।
टोकन के अनुमान में एक बात आसानी से छूट जाती है: चीनी भाषा। अंग्रेज़ी की समझ के आधार पर चीनी का अनुमान लगाएँगे — लगभग हर कुछ अक्षरों पर एक टोकन — तो गिनती बहुत कम आएगी। Orkas अपने अनुमान में CJK अक्षरों को अलग भार देता है; वरना पूरी तरह चीनी में हुई बातचीत के लिए सीमा का आकलन गलत होगा और संक्षेपण सही समय पर शुरू नहीं होगा।
त्रुटियाँ और पुनः प्रयास
उपयोगकर्ता की मशीन पर चलते हुए और बाहरी मॉडल API पर निर्भर रहते हुए त्रुटियाँ अपवाद नहीं, सामान्य बात हैं। रनर उन्हें कुछ श्रेणियों में बाँटता है और हर श्रेणी को अलग तरह से संभालता है:
- पुनः प्रयास योग्य: दर सीमाएँ, समय-सीमा समाप्त होना, कनेक्शन टूटना, 5xx। बेतरतीब बदलाव के साथ घातीय रूप से बढ़ता प्रतीक्षा समय, अधिकतम 30 सेकंड; अगर दर सीमा लगी है और सर्वर ने भेजा है
retry-after, तो उसका पालन करें। - पुनः प्रयास के अयोग्य: जैसे प्रमाणीकरण विफल होना — कितने भी पुनः प्रयास मदद नहीं करेंगे, इसलिए तुरंत त्रुटि लौटाएँ।
- विशेष: संदर्भ सीमा पार होना। पहले संक्षेपण आज़माएँ, उसके बाद एक बार पुनः प्रयास करें, और तभी त्रुटि लौटाएँ जब वह भी विफल हो।
एक और श्रेणी है: "टूल खुद विफल हो गया।" इससे पूरा संवाद-चरण विफल नहीं होता — टूल की विफलता भी मॉडल के लिए जानकारी है; "वह कमांड त्रुटि के साथ रुकी" देखकर वह कोई दूसरा तरीका आज़मा सकता है। हार्नेस टूल की इन अस्थायी त्रुटियों को वास्तविक खराबियों से अलग पहचानता है: न वह प्रवाह रोकता है, न उन्हें खोता है — वे बाद के आँकड़ों में दिखाई देती हैं। (यही डेटा आगे स्व-विकास तंत्र में काम आता है, जो अगले लेख का विषय है।)
बाहरी रद्दीकरण संकेत (AbortSignal) हर महत्वपूर्ण बिंदु पर जाँचा जाता है। उपयोगकर्ता "रोकें" दबाता है और वर्तमान संवाद-चरण तुरंत रुक जाता है — कोई नया पुनः प्रयास शुरू नहीं होता।
टूल अमूर्तन: इतना सरल कि विस्तार आसान हो
टूल इंटरफ़ेस जानबूझकर छोटा रखा गया है:
interface AgentTool {
readonly name: string;
readonly description: string; // shown to the model
readonly inputSchema: Record<string, unknown>; // JSON Schema to constrain inputs
execute(input: Record<string, unknown>, ctx: ToolContext): Promise<ToolResult>;
}टूल बस "एक नाम + मॉडल के लिए विवरण + इनपुट स्कीमा + निष्पादन फ़ंक्शन" है। अंतर्निर्मित टूल — फ़ाइल पढ़ना, फ़ाइल लिखना, शेल कमांड चलाना, वेब खोजना और सामग्री लाना — सभी इसी इंटरफ़ेस को लागू करते हैं। डेस्कटॉप परत इसके ऊपर स्थानीय उपयोग के कई टूल जोड़ती है (ज्ञान-आधार में खोज, छवि निर्माण, बाहरी कनेक्टर कॉल करना), लेकिन इंटरफ़ेस वही रहता है।
छोटे इंटरफ़ेस का लाभ यह है कि रनर के लिए टूल का स्रोत मायने नहीं रखता: अंतर्निर्मित हो, उपयोगकर्ता का बनाया हो, या किसी कौशल से लोड किया गया हो — सब एक ही तरह की चीज़ हैं, जो एक ही संरचना में पंजीकृत होते हैं: Map<string, AgentTool> और हर संवाद-चरण में मॉडल द्वारा पढ़ी जा सकने वाली परिभाषाओं में बदले जाते हैं।
शेल कमांड जैसे बाहरी स्थिति बदलने वाले टूल एक अलग निष्पादक से गुजरते हैं: समय-सीमाएँ, आउटपुट लंबाई की सीमाएँ, निषिद्ध कमांडों की सूची, और परिवेश चर प्रक्रिया के वैश्विक परिवेश को बदलने के बजाय अलग से दिए जाते हैं — वैश्विक परिवेश बदलने पर वे अनेक उप-प्रक्रियाओं में फैल जाएँगे और Electron जैसी बहु-प्रक्रिया संरचना में आसानी से शुरू होने की प्रक्रिया बिगाड़ सकते हैं।
प्रदाता परत: कई मॉडलों को एक इंटरफ़ेस में समेटना
मॉडल को लेकर उपयोगकर्ताओं की पसंद बहुत अलग-अलग होती है, और कोई उत्पाद एक ही विक्रेता से बँधा नहीं रह सकता। हार्नेस के नीचे Orkas एक प्रदाता अमूर्तन रखता है, जो अलग-अलग विक्रेताओं के मॉडलों को एक इंटरफ़ेस के पीछे एकजुट करता है:
interface LLMProvider {
readonly id: string;
complete(params: CompletionParams): Promise<CompletionResult>;
stream(params: CompletionParams): AsyncIterable<StreamEvent>;
validateAuth(): Promise<boolean>;
}ऊपर का रनर केवल इसी इंटरफ़ेस से बात करता है; उसे पता नहीं होता कि पीछे कौन-सा विक्रेता है। एक रजिस्ट्री मॉडल स्ट्रिंग के आधार पर रूटिंग संभालती है: स्पष्ट provider/model रूप को सीधे विभाजित किया जाता है; केवल मॉडल का नाम होने पर उसके उपसर्ग से प्रदाता तय होता है। प्रमाणीकरण (API कुंजी या OAuth टोकन) भी यहीं संभाला जाता है, और समाप्त हो चुके OAuth टोकन को अपने आप ताज़ा किया जाता है।
कई मॉडलों को एक रूप देने में असली सिरदर्द पाठ पूरा करना नहीं है — वे कोने हैं जहाँ विक्रेताओं के व्यवहार के अर्थ अलग होते हैं। दो उदाहरण जिन्होंने हमें परेशान किया।
एक है अलग-अलग विक्रेताओं के बीच चिंतन खंडों को सुरक्षित रखना। तर्क करने वाले मॉडल "चिंतन" सामग्री का एक हिस्सा देते हैं; कुछ विक्रेता उसे एन्क्रिप्ट करते हैं और चाहते हैं कि आप उसे ज्यों का त्यों वापस भेजें, जबकि दूसरे उसे अलग फ़ील्डों से दर्शाते हैं। अगर उपयोगकर्ता बातचीत के बीच विक्रेता A से विक्रेता B पर जाता है, तो इतिहास में उस चिंतन हिस्से के हस्ताक्षर का मेल नहीं रहता। समाधान है इतिहास के हर संदेश पर "इसे किस मॉडल ने बनाया" का चिह्न लगाना, ताकि रूपांतरण परत तय कर सके कि उसे ज्यों का त्यों रखना है या नहीं: वही मॉडल हो तो रखें; अलग मॉडल हो तो नियमों के अनुसार सरल रूप में बदलें।
दूसरा है प्रॉम्प्ट कैश। एक सत्र के संवाद-चरणों में शुरुआती हिस्सा बहुत दोहराया जाता है, और उसे कैश करने से लागत और विलंब में अच्छी बचत होती है। कार्यान्वयन इसे समर्थन देने वाले विक्रेताओं को सत्र ID कैश कुंजी के रूप में भेजता है और साथ ही हर विक्रेता की कुंजी लंबाई सीमा संभालता है (जैसे बहुत लंबी होने पर काटना या हैश करना)।
यह सब मेहनत वाला साधारण काम है — लेकिन इसी काम की यह परत ऊपर के रनर को ऐसे काम करने देती है जैसे "मॉडल केवल एक तरह का है।"
स्मृति: दो तंत्र, दोनों का अपना काम
Orkas में "स्मृति" वास्तव में दो समानांतर तंत्र हैं, जो दो बिल्कुल अलग समस्याएँ हल करते हैं। एक खोज-आधारित ज्ञान-आधार है, उस बड़ी सामग्री के लिए जिसे आप "ज़रूरत पड़ने पर ढूँढते हैं।" दूसरा सत्रों के बीच बनी रहने वाली स्मृति है, उन कुछ मुख्य तथ्यों के लिए जो "हमेशा ध्यान में होने चाहिए।" कई उत्पाद इन दोनों को मिला देते हैं; इन्हें अलग रखने से बातें अधिक स्पष्ट होती हैं।
ज्ञान-आधार: मिश्रित खोज
पहला तंत्र ऐसी सामग्री के लिए है जो बड़ी है, लेकिन कभी-कभी ही प्रासंगिक होती है — उपयोगकर्ता के दस्तावेज़, पुराने नोट्स, क्षेत्र-विशेष का ज्ञान। यह वेक्टर खोज वाला स्थानीय ज्ञान-आधार है, जिसके दो बैकएंड हैं: पूरी तरह मेमोरी में चलने वाला हल्का संस्करण (परीक्षण और अस्थायी उपयोग के लिए), और स्थानीय डेटाबेस में स्थायी रूप से रखा जाने वाला संस्करण (वास्तविक उपयोग के लिए, पूर्ण-पाठ अनुक्रमण और वेक्टरों के साथ)।
डेटा इस रास्ते से आता है:
दस्तावेज़ → पंक्तियों की सीमाओं पर खंड बनाएँ (ओवरलैप के साथ) → दोहरा अनुक्रमण
├─ पूर्ण-पाठ अनुक्रमणिका (कीवर्ड, एम्बेडिंग की कोई लागत नहीं)
└─ वेक्टर अनुक्रमणिका (अगर एम्बेडिंग मॉडल कॉन्फ़िगर किया गया हो)खंड पंक्तियों की सीमाओं पर काटे जाते हैं और उनके बीच थोड़ा ओवरलैप रखा जाता है, ताकि अर्थ के किसी पूरे हिस्से को बीच से न काटा जाए। खोज होती है मिश्रित: एक वेक्टर खोज (अर्थ में निकट) और एक कीवर्ड खोज (शाब्दिक मेल), जिनके दोनों परिणाम समूह RRF (पारस्परिक रैंक संलयन) से मिलाए जाते हैं:
score = Σ 1 / (k + rank_i)किसी खोज में परिणाम की रैंक जितनी ऊँची होती है, उसका योगदान उतना अधिक होता है; दोनों खोजों के योगदान जोड़ने से अर्थ की प्रासंगिकता भी बनी रहती है और सटीक शाब्दिक मेल भी नहीं छूटते। वेक्टर और कीवर्ड के भार बदले जा सकते हैं; डिफ़ॉल्ट रूप से अर्थ को प्राथमिकता मिलती है। मिलाने के बाद "(दस्तावेज़, शुरुआती पंक्ति)" के आधार पर दोहराव हटाया जाता है, हर स्थान के लिए केवल सबसे अच्छा परिणाम रखा जाता है, फिर सीमा से नीचे के परिणाम हटाकर शीर्ष-K लौटाए जाते हैं।
केवल वेक्टरों पर भरोसा क्यों नहीं? क्योंकि वेक्टर खोज अक्सर व्यक्तिवाचक नामों, कोड प्रतीकों और सटीक शाब्दिक स्ट्रिंगों पर चूक जाती है — ऐसी क्वेरी जिनका अर्थ विशेष नहीं होता, लेकिन शब्दों का हूबहू मेल बहुत मायने रखता है — जबकि केवल कीवर्ड "एक ही अर्थ, अलग शब्दावली" नहीं पकड़ सकते। दोनों को चलाना खोज की गुणवत्ता और लागत के बीच बहुत व्यावहारिक संतुलन है।
सत्रों के बीच स्मृति: उपयोगकर्ता को ध्यान में रखना
ज्ञान-आधार "इतनी सामग्री कि सब याद न रख सकें" की समस्या हल करता है। लेकिन एक और तरह की बातें हैं — मात्रा में बहुत छोटी, फिर भी हर समय ध्यान में रहनी चाहिए: यह उपयोगकर्ता कौन है, उसकी पसंद क्या है, पिछली बार क्या तय हुआ था। इन्हें खोज के दौरान "संयोग से याद आ जाने" पर निर्भर नहीं होना चाहिए — इन्हें हर संवाद-चरण में मौजूद होना चाहिए।
इसके लिए Orkas सत्रों के बीच बनी रहने वाली स्मृति की अलग परत बनाता है, जिसे सामग्री के आधार पर दो हिस्सों में बाँटा गया है:
- उपयोगकर्ता प्रोफ़ाइल: स्थिर तथ्य, जो संबंधित हैं व्यक्ति से — भूमिका, पसंद, संवाद शैली, तकनीकी स्टैक।
- तथ्य नोट्स: लंबे समय तक उपयोगी तथ्य, जो संबंधित हैं काम से — निर्णय, पड़ाव, परियोजना की परंपराएँ।
दोनों छोटे हैं, और प्रत्येक पर कुछ हज़ार अक्षरों की सख्त सीमा है, जिससे उनमें केवल लंबे समय तक सचमुच उपयोगी बातें ही रखी जाती हैं। वे खोज से नहीं गुजरते; इसके बजाय हर संवाद-चरण की शुरुआत में उन्हें सीधे सिस्टम प्रॉम्प्ट में स्थिर रूप से शामिल किया जाता है — यानी एजेंट ये बातें बस "जानता" है, उसे उन्हें खोजने जाना याद नहीं रखना पड़ता। यह ज्ञान-आधार से ठीक उलटा रुख है: ज्ञान-आधार है "ज़रूरत पड़ने पर लाएँ, बाद में हटा दें," जबकि सत्रों के बीच स्मृति है "हमेशा मौजूद, हमेशा दिखाई देने वाली।"
लिखने का काम एक समर्पित स्मृति टूल से होता है, जिसे मॉडल बातचीत के बीच यह तय करने पर कॉल करता है कि "यह लंबे समय तक याद रखने लायक है।" इसमें जोड़ना, उप-स्ट्रिंग बदलना और हटाना समर्थित है। क्या सहेजना है और क्या नहीं, यह टूल के विवरण में स्पष्ट है: उपयोगकर्ता के सुधार और पसंद सर्वोच्च प्राथमिकता हैं; लंबे समय तक लागू निर्णय और परंपराएँ सहेजी जाती हैं; लेकिन वर्तमान कार्य की अस्थायी स्थिति, एकबारगी डिबग जानकारी और आसानी से दोबारा खोजी जा सकने वाली बातें नहीं — स्मृति "उपयोगकर्ता और परियोजना के टिकाऊ तथ्यों" के लिए है, "इस बार मैं कहाँ तक पहुँचा" के लिए नहीं।
एक विवरण आसानी से अनदेखा हो सकता है, लेकिन काफी महत्वपूर्ण है: हर बार लिखने से पहले सुरक्षा जाँच चलती है। यह सामग्री ज्यों की त्यों सिस्टम प्रॉम्प्ट में जाती है और लंबे समय तक सत्रों के बीच बनी रहती है — यानी यह प्रभावी रूप से स्थायी इंजेक्शन का माध्यम है। इसलिए डिस्क पर लिखी जाने वाली हर स्मृति में पहले संदिग्ध पैटर्न जाँचे जाते हैं — प्रॉम्प्ट इंजेक्शन की आम भाषा ("पिछले सभी निर्देशों को अनदेखा करें" जैसी), कुंजियाँ बाहर भेजने की कोशिश करने वाले कमांड, पाठ में छिपे अदृश्य यूनिकोड अक्षर — और मेल मिलने पर उसे सीधे अस्वीकार कर दिया जाता है। साथ में दोहराव हटाने और सीमा से अधिक सामग्री छाँटने से यह स्मृति परत जोखिम बने बिना उपयोगी रहती है।
दोनों तंत्र मिलकर दोनों छोर संभालते हैं — "विशाल लेकिन कभी-कभार" और "छोटा लेकिन लगातार": ज्ञान-आधार पहला संभालता है, सत्रों के बीच स्मृति दूसरा। इसमें एजेंट की समझ जोड़ दें खुद के बारे में (जो अगले लेख का विषय है), तो Orkas एजेंट एक साथ तीन प्रकार की स्मृति लेकर आता है — सामग्री के बारे में, उपयोगकर्ता के बारे में और अपने बारे में।
सत्र: क्रैश सहने और उबरने के लिए बने
सत्र संदेशों का इतिहास संभालता है। इसका मूल संस्करण मेमोरी में संदेशों की एक सरणी है, जिसमें इतिहास की छँटाई और संक्षेपण होता है। लेकिन उपयोगकर्ता की मशीन पर चलने वाली किसी भी चीज़ को यह मानना चाहिए कि वह किसी भी समय बंद की जा सकती है — उपयोगकर्ता ऐप बंद कर दे, सिस्टम फिर से चालू हो, या वॉचडॉग की समय-सीमा प्रक्रिया समाप्त कर दे। इसलिए वास्तविक उपयोग में स्थायी सत्र काम आता है, जिसे स्थानीय JSONL फ़ाइल में लिखा जाता है, हर पंक्ति पर एक संदेश।
लिखने की दो रणनीतियाँ हैं: नया संदेश जोड़ने के लिए परमाण्विक जोड़ का उपयोग होता है; पूरी फ़ाइल दोबारा लिखने वाले काम (संक्षेपण, साफ़ करना) "अस्थायी फ़ाइल लिखें + परमाण्विक नाम बदलें" का उपयोग करते हैं। इस तरह लिखते समय बिजली जाने पर भी कोई आधा-अधूरा दूषित रिकॉर्ड पीछे नहीं छूटता।
सबसे दिलचस्प हिस्सा है बिना परिणाम वाली टूल कॉलों की मरम्मत। उसी जोड़ी के अनिवार्य नियम पर लौटें: मॉडल टूल कॉल करता है, हार्नेस उसे चलाता है, परिणाम वापस लिखा जाता है — इन तीन चरणों में कहीं भी रुकावट आने पर डिस्क पर "परिणाम के बिना कॉल" छूट जाती है। अगली बार उस सत्र को लोड करके वैसे ही मॉडल को भेजेंगे, तो API उसे अस्वीकार करेगी या अटक जाएगी।
हर बार डिस्क से सत्र लोड होने पर मरम्मत का तर्क चलता है, और उसे दोहराने से परिणाम नहीं बदलता:
- सभी सहायक संदेशों को जाँचें और उनकी टूल-कॉल ID इकट्ठी करें।
- आगे के संदेशों में मेल खाने वाले टूल परिणाम खोजें।
- जिस कॉल का मेल खाता परिणाम न मिले, उसके लिए "बाधित" चिह्न वाला परिणाम बनाएँ।
- साथ ही परिणामों का क्रम कॉल की घोषणा के क्रम से मिलाएँ, और वे परिणाम हटा दें जिनकी कोई मेल खाती कॉल नहीं है।
इस जाँच के बाद सत्र निश्चित रूप से ऐसी स्थिति में होता है जो API की जोड़ी संबंधी आवश्यकता पूरी करता है और भेजने के लिए सुरक्षित है। यह तंत्र साधारण दिखता है, लेकिन यही सुरक्षा जाल सुनिश्चित करता है कि "केवल एक क्रैश के कारण उपयोगकर्ता की बातचीत हमेशा के लिए न अटक जाए।"
कुछ निर्णय जिनकी अहमियत बाद में स्पष्ट हुई
इन सबको जोड़कर देखने पर कुछ निर्णय बाद में विशेष रूप से मूल्यवान लगते हैं।
प्रमुख इंटरफ़ेस के रूप में जनरेटर। स्ट्रीमिंग और गैर-स्ट्रीमिंग एक ही कार्यान्वयन साझा करते हैं, बीच की स्थिति स्वाभाविक रूप से सामने आती है, और UI जितना चाहे उतना विवरण दिखा सकता है। इससे असंगति वाले बगों की वह पूरी श्रेणी बच गई जो "पहले गैर-स्ट्रीमिंग लागू करें, बाद में स्ट्रीमिंग जोड़ें" से पैदा होती।
60% पर संक्षिप्त करें, पूरा भरने पर नहीं। इससे संक्षेपण के लिए भी जगह बचती है (जिसमें भी एक मॉडल कॉल लगती है) और अंतिम क्षण की हड़बड़ी से बचाव होता है।
जोड़ी का अनिवार्य नियम हर जगह लागू होता है। संक्षेपण के कट-बिंदु से लेकर डिस्क पर लिखने और लोड करते समय मरम्मत तक, सत्र को छूने वाली हर जगह एक ही नियम का पालन करती है। एक नियम होने से किसी हिस्से को अपना अलग सुधार-तर्क नहीं बनाना पड़ता।
प्रदाता परत में केंद्रित मेहनत वाला साधारण काम। अलग-अलग विक्रेताओं की सारी पेचीदगियाँ — चिंतन खंड, कैश कुंजियाँ, क्षमताओं के अंतर — इसी एक परत में संभल जाती हैं, जिससे ऊपर का रनर साफ़ रहता है। भविष्य में कोई नया मॉडल विक्रेता जोड़ें तो बदलाव मुश्किल से इस परत के बाहर जाता है।
समापन
Orkas के हार्नेस में कोई चौंकाने वाला एल्गोरिदम नहीं है। इसकी उपयोगिता "एजेंट को वास्तविक परिवेश में भरोसेमंद ढंग से चलाने" को स्पष्ट सीमाओं वाले मॉड्यूलों में बाँटने में है, जिनमें हर एक एक हिस्सा संभालता है: रनर चक्र और पुनः प्रयास संभालता है, टूल क्षमताएँ संभालते हैं, प्रदाता परत कई मॉडलों को एक रूप देती है, स्मृति खोज संभालती है, सत्र स्थायी भंडारण और मरम्मत संभालता है। अपने आप में कोई जटिल नहीं है; साथ मिलकर ही वे ऐसी चीज़ को टिकाए रखते हैं जिसे लोग हर दिन इस्तेमाल करते हैं।
अगर कुछ सीख साथ ले जानी हो: रन लूप को स्ट्रीमिंग जनरेटर बनाएँ, तो बीच की स्थिति संभालना बहुत आसान हो जाता है; एक बार मूल अनिवार्य नियम तय हो जाए (जैसे "टूल कॉलों की जोड़ी होनी चाहिए"), तो संक्षेपण, डिस्क पर लेखन और लोडिंग में उसे लगातार लागू करें — किसी कोने को अपवाद न बनने दें; अलग-अलग विक्रेताओं से जुड़ा साधारण लेकिन मेहनत वाला काम एक परत में केंद्रित करें और उसे व्यावसायिक तर्क से बाहर रखें; और — सबसे सीधी बात — मानकर चलें कि आपकी प्रक्रिया सबसे खराब समय पर बंद होगी, और उस पल की मरम्मत पहले ही लिख दें।
अगला लेख Orkas के और दिलचस्प हिस्से पर जाता है: यह एजेंट अपने उपयोग से कैसे सीखता है, अनुभव को दोबारा इस्तेमाल किए जा सकने वाले कौशलों में कैसे ढालता है, और धीरे-धीरे खुद को अधिक उपयोगी कैसे बनाता है।