Skip to content
This page has been auto-translated and may contain errors.View in English

टूल का उपयोग

docs.scrimba.com

टूल का उपयोग (अक्सर फ़ंक्शन कॉलिंग कहा जाता है) यह है कि एक भाषा मॉडल अपने स्वयं के पाठ बॉक्स से परे कैसे पहुंचता है: आप इसे फ़ंक्शन की एक सूची देते हैं जिसे वह चलाने के लिए कह सकता है, और यह तय करता है कि उन्हें कब उपयोग करना है। मॉडल कभी भी स्वयं कुछ नहीं चलाता है, यह केवल कॉल का अनुरोध करता है, और आपका कोड काम करता है।

एक मॉडल अपने आप एक बॉक्स में बंद है। यह आज का मौसम नहीं देख सकता, आपके डेटाबेस में एक उपयोगकर्ता को नहीं देख सकता, या एक ईमेल नहीं भेज सकता। यह केवल जो सीखा है उससे पाठ का अनुमान लगाता है, प्रशिक्षण समय पर जमे हुए, एलएलएम कैसे काम करते हैं से समान चित्र। टूल का उपयोग यह है कि आप इसे बॉक्स से बाहर पहुंचने देते हैं, और यह वह आधार है जिस पर एजेंट बनाए गए हैं।

याद रखने योग्य शब्द "पूछना" है। मॉडल आपको बताता है कि कौन सा फ़ंक्शन वह चाहता है और किन तर्कों के साथ। आपका कोड इसे चलाता है और परिणाम वापस देता है। आप पूरे समय नियंत्रण में रहते हैं।

एक मॉडल केवल पाठ उत्पन्न करता है, इसलिए यह अपने आप लाइव डेटा प्राप्त नहीं कर सकता, आपके डेटाबेस को छू नहीं सकता, या कोई कार्रवाई ट्रिगर नहीं कर सकता। टूल का उपयोग उस अंतराल को बंद करता है: आप फ़ंक्शन के एक सेट का वर्णन करते हैं, मॉडल एक संरचित अनुरोध जारी करता है एक को कॉल करने के लिए, आपका कोड इसे चलाता है, और आप परिणाम वापस करते हैं। मॉडल समन्वय करता है, आपका कोड निष्पादित करता है।

यह अध्याय हर एजेंट के तहत तंत्र है जो आप बनाएंगे। यहां लूप, टूल स्कीमा, और त्रुटि पथ को सही करें, और एजेंट इसी पैटर्न को बार-बार चलाने के लिए बन जाते हैं।

टूल का उपयोग वह सीम है जहां एक संभाव्य पाठ जनरेटर आपके नियतात्मक कोड से मिलता है, और अधिकांश उत्पादन दर्द सीधे उस सीम पर रहता है। मॉडल एक संरचित कॉल की भविष्यवाणी करता है, आपका कोड इसे निष्पादित करता है, और परिणाम अधिक संदर्भ के रूप में वापस दिए जाते हैं। सब कुछ जो इसे कठिन बनाता है, अविश्वसनीय तर्कों को मान्य करना, इडेम्पोटेंसी, अतिरिक्त राउंड-ट्रिप, भ्रमपूर्ण कॉल, प्रेक्षणीयता, उस एक हैंडऑफ से निकलता है।

यहां कोई नया गणित नहीं है। यह एक स्टोकेस्टिक सिस्टम को आपके सिस्टम में वास्तविक साइड इफेक्ट ट्रिगर करने देने के चारों ओर इंजीनियरिंग अनुशासन है, और यह पूरे एजेंट अध्याय का आधार है।

लूप

टूल का उपयोग एक आगे-पीछे है, एकल कॉल नहीं:

  1. आप उपयोगकर्ता का संदेश साथ ही टूल की एक सूची भेजते हैं जिसका मॉडल उपयोग कर सकता है।
  2. मॉडल दो तरीकों में से एक में उत्तर देता है: एक सामान्य उत्तर, या एक टूल को कॉल करने का अनुरोध, जिसमें वह तर्क चाहता है।
  3. अगर इसने एक टूल के लिए कहा, तो आपका कोड उस फ़ंक्शन को चलाता है और परिणाम को मॉडल को वापस भेज देता है।
  4. मॉडल परिणाम का उपयोग करके अपना अंतिम उत्तर लिखता है।

बस यही संपूर्ण पैटर्न है। मॉडल पूछता है, आपका कोड चलाता है। चरण 2 और 3 दोहराए जा सकते हैं यदि मॉडल को कई टूल की आवश्यकता है, जो एजेंट की शुरुआत है कुछ अध्याय आगे।

Junoलूप टूल का उपयोग एक लूप है: आप एक संदेश और टूल की एक सूची भेजते हैं, मॉडल या तो उत्तर देता है या एक टूल को कॉल करने का अनुरोध करता है, आपका कोड फ़ंक्शन को चलाता है, और आप परिणाम को वापस भेजते हैं मॉडल के लिए उपयोग करने के लिए। मॉडल कभी भी कोड नहीं चलाता है, यह केवल कॉल का अनुरोध करता है, इसलिए आप नियंत्रण में रहते हैं। मध्य चरणों को दोहराएं और आपके पास एजेंट की शुरुआत है।

लूप के चार बीट हैं: संदेश साथ ही टूल सूची भेजें, मॉडल उत्तर देता है या कॉल का अनुरोध करता है, आपका कोड फ़ंक्शन चलाता है, परिणाम वापस जाता है, मॉडल जारी रहता है। शुरुआती लोग अक्सर जो छोड़ते हैं वह यह है कि यह शायद ही कभी एक दौर है। मॉडल एक टूल का अनुरोध कर सकता है, आपका परिणाम पढ़ सकता है, फिर वह जो देखा है उसके आधार पर दूसरे टूल का अनुरोध कर सकता है, और इसी तरह।

तो असली आकार एक बहु-मोड़ लूप है: मॉडल को कॉल करते रहें और जो भी टूल के लिए वह पूछता है उसे चलाएं जब तक कि कोई प्रतिक्रिया वापस न आए जिसमें कोई टूल कॉल न हो। वह अंतिम टूल-मुक्त प्रतिक्रिया उपयोगकर्ता के लिए उत्तर है।

python
messages = [{"role": "user", "content": user_text}]

while True:
    response = client.chat.completions.create(
        model=MODEL, messages=messages, tools=tools,
    )
    msg = response.choices[0].message
    messages.append(msg)              # रिकॉर्ड करें कि मॉडल ने क्या कहा

    if not msg.tool_calls:            # कोई टूल नहीं चाहा: यह उत्तर है
        return msg.content

    for call in msg.tool_calls:       # हर अनुरोधित कॉल चलाएं, केवल पहला नहीं
        args = json.loads(call.function.arguments)
        result = tool_impls[call.function.name](args)
        messages.append({
            "role": "tool",
            "tool_call_id": call.id,
            "content": json.dumps(result),
        })

लूप स्थिति सुरक्षा प्रश्न है। एक मॉडल जो टूल के लिए पूछता रहता है स्वयं बाहर नहीं निकलता है, इसलिए उत्पादन में पुनरावृत्तियों को सीमित करें और स्पष्ट त्रुटि के साथ रोकें यदि आप सीमा तक पहुंचें। लूप तब समाप्त होता है जब मॉडल कोई टूल कॉल नहीं करता है।

Junoलूप लूप टूल भेजें, मॉडल उत्तर देता है या कॉल के लिए पूछता है, आप इसे चलाते हैं, आप परिणाम को वापस करते हैं, दोहराते हैं। यहां यह चाल है: यह तब तक जारी रहता है जब तक मॉडल कोई टूल कॉल के बिना प्रतिक्रिया नहीं करता है, इसलिए इसे एक वास्तविक लूप के रूप में लिखें, एक if-स्टेटमेंट नहीं। और पुनरावृत्तियों को सीमित करें, क्योंकि एक मॉडल जो टूल के लिए पूछता रहता है आपके खर्चे पर खुश होकर हमेशा के लिए घूमेगा।

लूप यांत्रिक रूप से छोटा है: मॉडल को टूल के साथ कॉल करें, यह जो कुछ पूछता है उसे चलाएं, परिणाम जोड़ें, दोहराएं जब तक यह कोई टूल कॉल के साथ प्रतिक्रिया नहीं करता है। दिलचस्प हिस्सा यह है कि हर पुनरावृत्ति एक पूर्ण मॉडल राउंड-ट्रिप है, और राउंड-ट्रिप वह जगह है जहां आपकी लेटेंसी और टोकन बजट चली जाती है।

हर मोड़ पूरे बढ़ते संदेश इतिहास को पुनः भेजता है, इसलिए एक कार्य जिसे पांच टूल कॉल की आवश्यकता है पाँच बार प्रॉम्प्ट के लिए भुगतान करता है, साथ ही इनपुट हर मोड़ बढ़ता है क्योंकि परिणाम जमा होते हैं। लीवर:

  • राउंड-ट्रिप की संख्या को कम करें ऐसे टूल देकर जो प्रति कॉल अधिक करते हैं।
  • टूल परिणामों को कॉम्पैक्ट रखें ताकि पुनः भेजा गया इतिहास विस्फारित न हो।
  • स्वतंत्र टूल कॉल को समवर्ती रूप से चलाएं क्रमिक रूप से नहीं ताकि दीवार-घड़ी लेटेंसी सबसे धीमी कॉल को ट्रैक करे, न कि उनके योग को।
python
MAX_TURNS = 8

for turn in range(MAX_TURNS):
    response = client.chat.completions.create(
        model=MODEL, messages=messages, tools=tools,
    )
    msg = response.choices[0].message
    messages.append(msg)

    if not msg.tool_calls:
        return msg.content

    # मॉडल एक मोड़ में कई कॉल का अनुरोध कर सकता है; उन्हें चलाएं, फिर जारी रखें
    for call in msg.tool_calls:
        messages.append(run_tool(call))   # सत्यापित, लॉग किया गया, त्रुटि-लपेटा हुआ

raise RuntimeError("tool loop did not converge within MAX_TURNS")

कठोर सीमा वैकल्पिक नहीं है। इसके बिना, एक भ्रमित मॉडल अनिश्चित काल तक लूप कर सकता है, और विफलता मोड एक धीमी, महंगी अनुरोध है न कि एक स्वच्छ त्रुटि। इसे बाध्य करें, और जब आप सीमा तक पहुंचें, तो जोर से विफल करें और ट्रेस को लॉग करें ताकि आप देख सकें कि मॉडल क्या पीछा कर रहा था।

Junoलूप लूप लिखने के लिए तुच्छ है और चलाने के लिए महंगा है: हर मोड़ पूरे पुनः भेजे गए इतिहास पर एक पूर्ण मॉडल कॉल है, इसलिए पांच टूल कॉल का मतलब पाँच बार प्रॉम्प्ट के लिए भुगतान करना है। परिणामों को कॉम्पैक्ट रखें, स्वतंत्र कॉल को समवर्ती रूप से चलाएं, और कई बातूनी लोगों पर कई चिकने टूल पसंद करें। और मोड़ों को कठिन-सीमा करें, क्योंकि जिस दिन कोई मॉडल हमेशा के लिए लूप करने का निर्णय लेता है, आप अपने लॉग में एक स्वच्छ त्रुटि चाहते हैं, पाँच-अंकीय बिल नहीं।

वास्तव में क्या हो रहा है

टूल का उपयोग ऐसा लग सकता है कि मॉडल को नई शक्तियां मिलीं, और उसे ऐसा नहीं हुआ। हुड के तहत, मॉडल वह एक काम कर रहा है जो वह हमेशा करता है: पाठ की भविष्यवाणी करना। इसके बॉक्स में वह कुछ नहीं बदला है।

जब आप अनुरोध में टूल परिभाषाएं शामिल करते हैं, तो आप उन्हें संदर्भ में जोड़ रहे हैं जिससे मॉडल भविष्यवाणी करता है। मॉडल को ऐसे उदाहरणों पर प्रशिक्षित किया गया था जहां, इस तरह के टूल दिए गए, सही निरंतरता कभी-कभी एक विशेष रूप से स्वरूपित संदेश होता है जिसका अर्थ है "कॉल get_weather शहर दिल्ली के साथ" एक सामान्य वाक्य के बजाय। इसलिए जब आपका प्रश्न एक टूल को उपयोगी दिखाता है, तो सबसे संभावित अगला आउटपुट वह संरचित टूल-कॉल संदेश है, और एपीआई इसे आपको tool_calls के रूप में सतह देता है।

मॉडल किसी चीज़ तक पहुंचा नहीं है और कुछ नहीं चलाया है। इसने भविष्यवाणी की है कि एक टूल कॉल सही कदम है और एक प्रारूप में एक अनुरोध लिखा है जिसे आपका कोड पढ़ना जानता है।

फिर आप, मॉडल नहीं, फ़ंक्शन चलाते हैं। आप वास्तविक परिणाम लेते हैं और इसे संदेशों में अधिक संदर्भ के रूप में वापस रखते हैं, और मॉडल उस विस्तारित संदर्भ से अपना अंतिम उत्तर भविष्यवाणी करता है। पूरी सुविधा दो साधारण भविष्यवाणियां हैं जिसमें आपका कोड बीच में वास्तविक काम करता है।

आप मॉडल के पाठ बॉक्स और वास्तविक दुनिया के बीच पुल हैं। उस चित्र को पकड़ें और अध्याय का बाकी हिस्सा, एजेंट शामिल हैं, रहस्यमय महसूस करना बंद कर देंगे: हर कार्रवाई जो एआई लेता है वह एक भविष्यवाणीयुक्त अनुरोध है जिसे आपका कोड चलाने के लिए चुनता है।

Junoवास्तव में क्या हो रहा है टूल का उपयोग अभी भी शुद्ध भविष्यवाणी है: मॉडल को इस तरह प्रशिक्षित किया गया था कि इसके संदर्भ में टूल परिभाषाएं दी गई हों, संभावित आउटपुट कभी-कभी एक संरचित "इस फ़ंक्शन को इन तर्कों के साथ कॉल करें" संदेश होता है, जिसे एपीआई आपको tool_calls के रूप में सौंपता है। मॉडल कभी कुछ नहीं चलाता है, यह एक अनुरोध की भविष्यवाणी करता है। आपका कोड फ़ंक्शन चलाता है और परिणाम को अगली भविष्यवाणी के लिए अधिक संदर्भ के रूप में वापस करता है, इसलिए आप मॉडल और वास्तविक दुनिया के बीच पुल हैं।

टूल का उपयोग एलएलएम कैसे काम करते हैं से नई क्षमता नहीं है, यह एक संरचित लक्ष्य की ओर इशारा की गई समान अगली-टोकन भविष्यवाणी है। टूल परिभाषाएं संदर्भ में जाती हैं, और मॉडल को इस तरह प्रशिक्षित किया गया था कि जब एक टूल फिट बैठता है, तो उच्चतम-संभावना निरंतरता गद्य के बजाय एक स्वरूपित कॉल होता है। एपीआई उस निरंतरता को पार्स करता है और इसे आपको tool_calls के रूप में सौंपता है।

उपयोगी परिणाम: एक टूल कॉल सीमित पीढ़ी है, आउटपुट मॉडल उत्पन्न करता है एक स्कीमा से मेल खाने के लिए जो आपने आपूर्ति की थी। यह संरचित आउटपुट के समान तंत्र है, जहां आप मॉडल को आपके द्वारा परिभाषित एक आकृति में जेसन लौटाने के लिए कहते हैं।

उनके बीच की रेखा इरादा है। संरचित आउटपुट "मुझे इस आकार में डेटा दें और रुकें"। टूल का उपयोग "मुझे इस आकार में डेटा दें ताकि मैं कुछ चला सकूं और वापसी से प्रतिक्रिया दे सकूं"।

यदि आपको मॉडल से केवल एक पार्स किया गया वस्तु चाहिए, तो संरचित आउटपुट के लिए पहुंचें। टूल का उपयोग करने के लिए पहुंचें जब मॉडल को कार्य करने की आवश्यकता हो और फिर अपना कोड लौटाता है से प्रतिक्रिया दें।

क्योंकि कॉल भविष्यवाणी की जाती है, निष्पादित नहीं, आपका कोड एकमात्र चीज़ है जो वास्तव में आपके सिस्टम को छूता है। मॉडल प्रस्ताव देता है, आपका कोड निपटाता है, और वह सीमा वह जगह है जहां आप कुछ भी वास्तविक होने से पहले हर जांच डालते हैं।

Junoवास्तव में क्या हो रहा है एक टूल कॉल समान अगली-टोकन भविष्यवाणी है, गद्य के बजाय एक स्कीमा को लक्षित किया गया: एपीआई इसे tool_calls में एक संरचित कॉल में डिकोड करता है। यह संरचित आउटपुट, समान मशीनरी के समान सीमित पीढ़ी है। अंतर इरादा है: संरचित आउटपुट आपको डेटा देता है और रुकता है, टूल का उपयोग आपको डेटा देता है ताकि आप कुछ चला सकें और प्रतिक्रिया दे सकें। किसी भी तरह से मॉडल केवल प्रस्ताव देता है, आपका कोड एकमात्र चीज़ है जो कुछ भी वास्तविक को छूता है।

एक टूल कॉल भविष्यवाणी की गई पाठ है, निष्पादन नहीं, और यह सीधा रखना वह है जो आपको सुरक्षित करता है। मॉडल संदर्भ में टूल दिए गए उच्चतम-संभावना निरंतरता उत्सर्जित करता है, एपीआई इसे एक संरचित कॉल में डिकोड करता है, और आपका कोड यह तय करता है कि इसको सम्मानित करें या नहीं। यांत्रिकी रूप से यह संरचित आउटपुट है एक साइड इफेक्ट संलग्न के साथ।

वह फ्रेमिंग विश्वास सीमा को सटीक रूप से सेट करता है। एक टूल कॉल में तर्क मॉडल आउटपुट हैं, जिसका अर्थ है कि वे अविश्वसनीय इनपुट हैं: पाठ जो एक संभाव्य प्रणाली ने उत्पन्न किया है, किसी भी चीज़ के समान स्टैंडिंग के साथ एक उपयोगकर्ता ने एक फॉर्म में टाइप किया है। उस तरह उन्हें मानें।

मॉडल को लूप के बीच में पढ़ी गई सामग्री से भी निर्देशित किया जा सकता है, इसलिए यदि एक टूल किसी वेबपेज या दस्तावेज़ से पाठ लौटाता है, तो प्रॉम्प्ट इंजेक्शन उस पाठ में छिपा हुआ मॉडल को विभिन्न टूल के लिए अनुरोध करने में बात सकता है आक्रमणकारी-चुने गए तर्कों के साथ। रक्षा संरचनात्मक है, सिस्टम संकेत में शिष्टाचार निर्देश नहीं: एक कठोर स्कीमा के विरुद्ध तर्कों को सत्यापित करें, प्रत्येक टूल को सबसे संकीर्ण अनुमति तक सीमित करें जिसे इसकी आवश्यकता है, और कभी भी एक टूल की प्राधिकार को कम से कम विश्वसनीय पक्ष को अनुदान के लिए अपने इनपुट को प्रभावित कर सकते हैं।

तो मानसिक मॉडल एक अनुरोध राउटर है, सहकर्मी नहीं। मॉडल प्रस्तावित तर्कों के साथ एक फ़ंक्शन के लिए मार्गों की राह देता है। आपका कोड प्रामाणिकता, अनुमति, सत्यापन, और केवल तब निष्पादन करता है। "केवल तब" के डाउनस्ट्रीम सब कुछ वह जगह है जहां यह अध्याय इसके रखते हैं।

Junoवास्तव में क्या हो रहा है एक टूल कॉल भविष्यवाणी की गई पाठ है जिसे एपीआई आपके लिए डिकोड करता है, एक साइड इफेक्ट के साथ संरचित आउटपुट, मॉडल कभी भी कुछ नहीं चलाता है। तो तर्क अविश्वसनीय इनपुट हैं, एक किसी ने फॉर्म फील्ड भरे हुए के समान स्टैंडिंग, और एक टूल के लौटाए गए पाठ में छिपा प्रॉम्प्ट इंजेक्शन अगली कॉल को पुनः निर्देशित कर सकता है। सीमा पर सत्यापन, दायरा, और अनुमति। मॉडल अनुरोध को रूट करता है, आपका कोड एकमात्र चीज़ है जिसका हाथ लीवर पर है।

एक टूल को परिभाषित करना

आप प्रत्येक टूल को मॉडल के लिए वर्णन करते हैं: इसका नाम, यह क्या करता है, और इसे किन तर्कों की आवश्यकता है। विवरण आपके लिए दस्तावेज़ नहीं है, यह टूल के बारे में मॉडल को एक निर्देश है, इसलिए इसे एक प्रॉम्प्ट की तरह लिखें।

python
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get the current weather for a city. Use when the user asks about weather.",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "The city name, e.g. 'Delhi'"},
                },
                "required": ["city"],
            },
        },
    },
]

parameters ब्लॉक एक स्कीमा है, संरचित आउटपुट के समान विचार: यह तर्कों को परिभाषित करता है जिसे मॉडल को उत्पादन करना चाहिए। जब मॉडल get_weather को कॉल करने का निर्णय लेता है, तो यह इस आकृति से मेल खाने वाली एक city तर्क भेजता है। एक अस्पष्ट विवरण ("मौसम प्राप्त करता है") मॉडल को गलत क्षणों में टूल का उपयोग करने के लिए जन्म देता है। एक स्पष्ट ("जब उपयोगकर्ता मौसम के बारे में पूछता है तो उपयोग करें") इसे अच्छी तरह से गाइड करता है।

Junoएक टूल को परिभाषित करना एक टूल परिभाषा का एक नाम, एक विवरण, और इसके तर्कों के लिए parameters स्कीमा है। विवरण वास्तव में एक प्रॉम्प्ट है: यह मॉडल को बताता है कि टूल को कब कॉल करना है, इसलिए सावधानी से इसे लिखें। पैरामीटर स्कीमा संरचित आउटपुट के समान तंत्र है, मॉडल भेजता है कि तर्कों को सीमित करता है।

एक टूल परिभाषा में मॉडल के तीन भाग हैं: नाम, विवरण, और parameters स्कीमा। विवरण वह भाग है जो लोग लिखते हैं। यह आपके सहकर्मियों के लिए एक टिप्पणी नहीं है, यह एक प्रॉम्प्ट है जिसे मॉडल टूल को कॉल करने का निर्णय लेने के लिए उपयोग करता है, इसलिए इसे एक की तरह लिखें: कहें कि टूल क्या करता है, कब इसका उपयोग करना है, और कब नहीं।

python
tools = [
    {
        "type": "function",
        "function": {
            "name": "search_orders",
            "description": (
                "Look up a customer's orders by their account email. "
                "Use only when the user asks about an existing order. "
                "Do not use for product questions or refunds."
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "email": {"type": "string", "description": "Account email to search"},
                    "status": {
                        "type": "string",
                        "enum": ["pending", "shipped", "delivered", "cancelled"],
                        "description": "Optional status filter",
                    },
                },
                "required": ["email"],
                "additionalProperties": False,
            },
        },
    },
]

स्कीमा को कसें और आप मॉडल को कसते हैं:

  • required टूल को नहीं चला सकता उन तर्कों को बलपूर्वक करता है।
  • एक enum एक क्षेत्र को एक निश्चित सेट तक सीमित करता है, इसलिए मॉडल पाँचवां स्थिति आविष्कार नहीं कर सकता।
  • additionalProperties: false फील्ड को अस्वीकार करता है जिसे आपने परिभाषित नहीं किया।

स्कीमा आपकी पहली पंक्ति रक्षा है: यह जितना संकीर्ण है, उतने कम तरीके मॉडल के पास टूल को गलत तरीके से कॉल करने का है।

यह टूल आकार OpenAI-शैली है; सटीक चाबियां प्रदाता द्वारा भिन्न होती हैं (Anthropic और अन्य स्कीमा को अलग तरीके से नेस्ट करते हैं), लेकिन तीन भाग, नाम, विवरण, स्कीमा, सर्वत्र समान हैं।

Junoएक टूल को परिभाषित करना नाम, विवरण, और एक parameters स्कीमा, यही एक टूल है। विवरण एक डॉकस्ट्रिंग नहीं, एक प्रॉम्प्ट है: मॉडल को बताएं कि इसे कब कॉल करना है और कब नहीं, या यह गलत क्षणों में कॉल करेगा। required, enum, और अतिरिक्त संपत्ति के साथ स्कीमा को कसें, क्योंकि एक संकीर्ण स्कीमा मॉडल के लिए टूल को गलत तरीके से कॉल करने का कम तरीका है। सटीक JSON कुंजियां प्रदाता द्वारा शिफ्ट होती हैं, लेकिन वे तीन भाग नहीं।

एक टूल परिभाषा दो प्रॉम्प्ट है जो एक टोपी पहन रहे हैं। विवरण कब मॉडल टूल को कॉल करता है यह चलाता है, स्कीमा क्या तर्क यह भेज सकता है को सीमित करता है, और दोनों मॉडल-सामना निर्देश हैं, आंतरिक डॉक्स नहीं। स्कीमा वह जगह है जहां आप कड़ी होना पाते हैं, और यहां कड़कता आपके पास सबसे सस्ता भ्रम नियंत्रण है।

python
{
    "name": "issue_refund",
    "description": (
        "Issue a refund for a specific order line. "
        "Use only after confirming the order ID and amount with the user. "
        "Never call for amounts above the order total."
    ),
    "parameters": {
        "type": "object",
        "properties": {
            "order_id": {"type": "string", "pattern": "^ord_[0-9]{8}$"},
            "amount_cents": {"type": "integer", "minimum": 1, "maximum": 100000},
            "reason": {"type": "string", "enum": ["damaged", "wrong_item", "late"]},
        },
        "required": ["order_id", "amount_cents", "reason"],
        "additionalProperties": False,
    },
}

एक सीमित क्षेत्र (एक enum, pattern, minimum/maximum, या required सेट के साथ) मॉडल को उत्सर्जित कर सकता है को सीमित करता है, लेकिन सीमा को स्पष्ट रूप से पढ़ें: कई प्रदाता स्कीमा की संरचना को सत्यापित करते हैं, फिर भी मान भविष्यवाणी से आते हैं, इसलिए एक कड़ी स्कीमा गलत आकार वाली कॉल को कम करती है लेकिन यह साबित नहीं करता कि कॉल सही या सुरक्षित है। order_id एक पैटर्न से मेल खाना का मतलब यह नहीं है कि वह आदेश मौजूद है या इस उपयोगकर्ता का है। तो स्कीमा एक फिल्टर है, अनुमति चेक नहीं।

आप अभी भी हर तर्क को वास्तविक स्थिति के विरुद्ध सत्यापित करते हैं, आदेश मौजूद है, राशि कुल अधिक नहीं है, कॉलर इसे साइड इफेक्ट चलाने से पहले आपके कोड में स्वामित्व देता है। स्कीमा सत्यापन गलत आकार को पकड़ता है; केवल आपका कोड गलत कार्रवाई को पकड़ता है। (स्कीमा कुंजियां और कितनी कड़ी तरह से हर प्रदाता उन्हें प्रवर्तन करता है, तो मान लेने के बजाय आपके प्रदाता के व्यवहार की पुष्टि करें सीमा प्रवर्तित है।)

एक और लीवर: टूल गणना। लगभग एक दर्जन टूल के बाद, चयन सटीकता गिरता है और मॉडल गलत एक के लिए पहुंचना शुरू करता है, और हर परिभाषा हर कॉल पर संदर्भ जलना टोकन में बैठता है। सक्रिय टूल सेट को छोटा और कार्य के लिए प्रासंगिक रखें बजाय आपकी पूरी एपीआई सतह को एक बार में उजागर करने के।

Junoएक टूल को परिभाषित करना विवरण नियंत्रण करता है कि मॉडल कब कॉल करता है, स्कीमा नियंत्रण करता है कि यह क्या भेज सकता है, और दोनों प्रॉम्प्ट हैं। enum, pattern, और सीमाओं के साथ स्कीमा को लॉक करें, लेकिन एक वैध आकार को एक वैध कार्रवाई के लिए गलती न करें: एक अच्छी तरह से गठित order_id अभी भी अविश्वसनीय है, इसलिए साइड इफेक्ट से पहले वास्तविक स्थिति के विरुद्ध पुनः-सत्यापन करें। और टूल सेट को छोटा रखें, क्योंकि एक दर्जन से परे मॉडल गलत को चुनता है और हर परिभाषा आपके संदर्भ को कर रही है। स्कीमा फिल्टर करता है, आपका कोड अनुमति देता है।

कॉल को संभालना

जब मॉडल एक टूल चाहता है, तो उत्तर में tool_calls होते हैं बजाय एक अंतिम उत्तर के। आप अनुरोधित फ़ंक्शन और तर्कों को पढ़ते हैं, वास्तविक फ़ंक्शन को चलाते हैं, और परिणाम को tool संदेश के रूप में वापस भेजते हैं। फिर आप मॉडल को फिर से कॉल करते हैं इसलिए यह समाप्त कर सकता है।

python
import json

# टूल का वास्तविक कार्यान्वयन
def get_weather(city):
    # एक वास्तविक ऐप में यह मौसम एपीआई को कॉल करता है; यहां हम इसे नकली करते हैं
    return {"city": city, "tempC": 18, "condition": "cloudy"}

messages = [{"role": "user", "content": "What's the weather in Delhi?"}]

# 1. पहली कॉल: टूल की पेशकश करें
response = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
tool_calls = response.choices[0].message.tool_calls
tool_call = tool_calls[0] if tool_calls else None

if tool_call:
    # 2. मॉडल के तर्कों के साथ अनुरोधित फ़ंक्शन चलाएं
    args = json.loads(tool_call.function.arguments)
    result = get_weather(args["city"])

    # 3. मॉडल के अनुरोध और आपका परिणाम वापस भेजें
    messages.append(response.choices[0].message)         # सहायक का टूल अनुरोध
    messages.append({
        "role": "tool",
        "tool_call_id": tool_call.id,
        "content": json.dumps(result),
    })

    # 4. फिर कॉल करें ताकि मॉडल परिणाम का उपयोग करके उत्तर दे सके
    response = client.chat.completions.create(model=MODEL, messages=messages)

print(response.choices[0].message.content)
# "It's currently 18 degrees and cloudy in Delhi."

तर्क JSON स्ट्रिंग के रूप में आते हैं, इसलिए आप उन्हें एक वास्तविक वस्तु में json.loads करते हैं, फिर उपयोग करने से पहले उन्हें जांचते हैं। आप इतिहास पर दो संदेश धक्का देते हैं: मॉडल का टूल अनुरोध और आपका tool परिणाम, tool_call_id से जुड़ा हुआ। अंतिम कॉल कच्चे मौसम डेटा को एक प्राकृतिक वाक्य में बदल देता है।

Junoकॉल को संभालना जब मॉडल एक टूल चाहता है, तो इसकी प्रतिक्रिया में tool_calls होते हैं पाठ के बजाय। आप JSON स्ट्रिंग से तर्क को पार्स करते हैं, वास्तविक फ़ंक्शन चलाते हैं, और दो संदेश वापस धक्का देते हैं: मॉडल का अनुरोध और आपका परिणाम, tool_call_id से जुड़ा हुआ। एक अंतिम मॉडल कॉल आपके कच्चे परिणाम को एक प्राकृतिक उत्तर में बदल देता है।

जब मॉडल एक टूल चाहता है, तो इसकी प्रतिक्रिया content के बजाय tool_calls लेकर जाती है। हर कॉल आपको एक फ़ंक्शन नाम, id, और JSON स्ट्रिंग के रूप में तर्क देता है। आप पार्स करते हैं, चलाते हैं, और एक tool संदेश जोड़ते हैं जो मेल खाने वाली tool_call_id के साथ टैग किया गया है ताकि मॉडल जाने कि कौन सा परिणाम कौन सा अनुरोध का उत्तर देता है।

सही पाने के लिए लायक हिस्सा विफलता है। आपका टूल थ्रो करेगा: एक बुरी तर्क, एक 404, एक समय सीमा। सहज ज्ञान अपवाद बुलबुला करने के लिए है, लेकिन यह पूरे मोड़ को समाप्त करता है। बेहतर चाल है त्रुटियों को डेटा के रूप में लौटाना: त्रुटि को पकड़ें और इसे टूल परिणाम के रूप में वापस कर दें, इसलिए मॉडल यह पढ़ सकता है कि क्या गलत हुआ और पुनः प्राप्त करें, ठीक किए गए तर्क के साथ पुनः प्रयास करें, या उपयोगकर्ता को बताएं कि वह चीज़ नहीं कर सकता।

python
def run_tool(call):
    try:
        args = json.loads(call.function.arguments)
        result = tool_impls[call.function.name](args)
        content = json.dumps(result)
    except Exception as err:
        # विफलता को डेटा के रूप में वापस हाथ, अपवाद नहीं
        content = json.dumps({"error": str(err)})
    return {"role": "tool", "tool_call_id": call.id, "content": content}

त्रुटियों को टूल संदेशों के रूप में लौटाना यह है कि एक टूल लूप लचीला के बजाय नाजुक बनाता है। एक मॉडल जो एक गलत ईमेल के साथ search_orders को कॉल करता है वह {"error": "invalid email"} वापस पाता है और उपयोगकर्ता से इसे सही करने के लिए कह सकता है, बजाय आपका पूरा अनुरोध 500-ing करने के। प्रतिक्रिया आकार (tool_calls, tool_call_id, tool भूमिका) OpenAI-शैली है और प्रदाता द्वारा भिन्न होता है, लेकिन पैटर्न, पार्स, चलाएं, परिणाम-या-त्रुटि लौटाएं, सभी में समान रहता है।

Junoकॉल को संभालना प्रतिक्रिया tool_calls लेकर जाती है: JSON तर्कों को पार्स करें, फ़ंक्शन चलाएं, मेल खाने वाली tool_call_id के साथ tool संदेश जोड़ें। विफलताओं पर यहां यह चाल है: अपवाद को मोड़ को समाप्त करने और हाथ न करें `{"error": ...}` टूल परिणाम के रूप में वापस ताकि मॉडल पुनः प्राप्त कर सके या पुनः प्रयास कर सके। सटीक फील्ड नाम प्रदाता द्वारा भिन्न होता है, लेकिन पार्स, चलाएं, परिणाम-या-त्रुटि सर्वत्र समान है।

तर्कों को पार्स और डिस्पैच करना बोरिंग भाग है। खतरनाक भाग तर्कों को प्राप्त करने और साइड इफेक्ट चलाने के बीच सब कुछ है, क्योंकि तर्क मॉडल आउटपुट हैं और फ़ंक्शन कुछ वास्तविक करता है।

तीन आदतें एक डेमो से एक टूल को अलग करती हैं जो आप उत्पादन में चला सकते हैं:

  • पहला, निष्पादन से पहले सत्यापन करें, वास्तविक स्थिति के विरुद्ध, स्कीमा नहीं: स्कीमा पहले से ही पारित किया गया था या आप यहां नहीं होते, तो अब पुष्टि करें कि आदेश मौजूद है, उपयोगकर्ता इसे मालिकाना है, राशि सीमा में है।
  • दूसरा, इडेम्पोटेंसी के लिए बिल्ड करें, संपत्ति यह कि एक ही ऑपरेशन दो बार चलाना एक बार चलाने जैसा ही प्रभाव है। मॉडल पुनः प्रयास करते हैं, लूप दोबारा आग लगाते हैं, नेटवर्क डुप्लिकेट करते हैं, इसलिए एक लिखें टूल को एक इडेम्पोटेंसी कुंजी की आवश्यकता होती है (अक्सर tool_call_id से व्युत्पन्न) इसलिए एक पुनरावृत्ति दोगुनी चार्ज या दोगुनी भेज नहीं करता है।
  • तीसरा, त्रुटियों को डेटा के रूप में लौटाएं, संरचित पर्याप्त कि मॉडल उन पर कार्य कर सके, इसलिए एक पुनः प्राप्त करने योग्य विफलता एक पुनः प्रयास बन जाता है और एक अपुनः प्राप्त करने योग्य एक उपयोगकर्ता के लिए स्वच्छ संदेश बन जाता है।
python
def run_tool(call):
    name, args = call.function.name, json.loads(call.function.arguments)
    log.info("tool_call", name=name, args=redact(args), call_id=call.id)  # प्रेक्षणीयता

    try:
        validate(name, args)                       # वास्तविक-स्थिति जांच, खराब इनपुट पर उठाता है
        result = TOOLS[name](args, idem_key=call.id)  # पुनर्प्रयास पर इडेम्पोटेंट
        content = json.dumps(result)
    except ValidationError as err:
        content = json.dumps({"error": "invalid", "detail": str(err)})  # मॉडल पुनः प्रयास कर सकता है
    except Exception as err:
        log.exception("tool_failed", call_id=call.id)
        content = json.dumps({"error": "tool_failed"})  # आंतरिक को मॉडल में लीक न करें

    return {"role": "tool", "tool_call_id": call.id, "content": content}

उत्पादन की दो और वास्तविकताएं:

  • प्रेक्षणीयता: लॉग करें कि कौन सा टूल चलता है, किन तर्कों के साथ (गोपनीयता को छुपाएं), और यह क्या लौटाता है, क्योंकि जब एक एजेंट गलतव्यवहार करता है तो टूल कॉल का ट्रेस यह पुनर्निर्माण करने का एकमात्र तरीका है कि वह वास्तव में क्या करता है।
  • विनाशकारी टूल: किसी भी अपरिवर्तनीय के लिए, हटाना, भुगतान, ईमेल, मॉडल के कॉल को अंतिम प्राधिकार न बनाएं। पुष्टि, एक ड्राई-रन जो बताता है कि क्या होगा, या मानव अनुमति चरण के पीछे गेट करें, और क्रेडेंशियल को सीमित करें ताकि भीड़ त्रिज्या भी सीमित हो यदि मॉडल गलत है या अपहृत है।

एक स्कीमा-वैध कॉल तब तक अविश्वसनीय है जब तक आपका कोड इसे अनुमति नहीं देता है। (फील्ड नाम और पुनः प्रयास लिफाफा OpenAI-शैली हैं; अनुशासन किसी भी प्रदाता तक ले जाता है।)

Junoकॉल को संभालना पार्सिंग बोरिंग भाग है। तर्कों और साइड इफेक्ट के बीच: केवल स्कीमा नहीं वास्तविक स्थिति के विरुद्ध सत्यापन करें, tool_call_id से लिखता है इडेम्पोटेंट बनाएं क्योंकि पुनः प्रयास दो बार आग लगाएंगे, और त्रुटियों को डेटा के रूप में लौटाएं मॉडल बिना आपके आंतरिक को लीक किए कर सके। हर कॉल और इसके args को लॉग करें ताकि आप पुनर्निर्माण कर सकें कि एजेंट वास्तव में क्या करता है। और कुछ भी विनाशकारी के पीछे पुष्टि या स्कोप्ड क्रेडेंशियल गेट करें, क्योंकि एक स्कीमा-वैध रिफंड अभी भी एक अजनबी का अनुरोध है जब तक आपका कोड हाँ न कहे।

व्यवहार में

एक पुनर्प्रयोज्य फ़ंक्शन के रूप में समान प्रवाह। ध्यान दें कि मॉडल और आपके सिस्टम के बीच एकमात्र चीज़ आपके द्वारा लिखा गया कोड है और नियंत्रण:

python
import json

tool_impls = {"get_weather": lambda args: get_weather(args["city"])}

def answer_with_tools(user_text):
    messages = [{"role": "user", "content": user_text}]

    response = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
    calls = response.choices[0].message.tool_calls
    if not calls:
        return response.choices[0].message.content  # मॉडल सीधे उत्तर दिया

    call = calls[0]
    args = json.loads(call.function.arguments)
    result = tool_impls[call.function.name](args)

    messages.append(response.choices[0].message)
    messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})

    response = client.chat.completions.create(model=MODEL, messages=messages)
    return response.choices[0].message.content

यह एक टूल कॉल को संभालता है। एक मॉडल एक बार में कई भी अनुरोध कर सकता है, जिसे समानांतर टूल कॉल कहा जाता है, जिसे आप tool_calls में हर प्रविष्टि को लूप करके संभाले सकते हैं। और यदि आप मॉडल को लूप में टूल के लिए कॉल करना जारी रखने देते हैं जब तक यह किया नहीं जाता है, अपने अगले चरण का निर्णय हर बार ले रहा है, आप एक एजेंट प्राप्त करते हैं, जो ठीक वही जगह है जहां अगला अध्याय जाता है।

Junoव्यवहार में एक फ़ंक्शन में लपेटा गया, टूल का उपयोग एक टूल चुनने के लिए एक मॉडल कॉल है, आपका कोड इसे चलाता है, और परिणाम को एक उत्तर में बदलने के लिए दूसरी कॉल। मॉडल और आपके सिस्टम के बीच एकमात्र चीज़ कोड है जो आप लिखते हैं, इसलिए आप नियंत्रण में रहते हैं। मॉडल किया जा रहा है तब तक यह लूप और आपके पास एक एजेंट है।

एक वास्तविक टूल हैंडलर में आप मल्टी-मोड़ लूप, समानांतर कॉल, और त्रुटि रिटर्न को एक फ़ंक्शन में संयोजित करते हैं। मॉडल एक ही मोड़ में कई टूल के लिए पूछ सकता है, और लूप तब तक चलता है जब तक यह पूछना बंद नहीं करता है।

python
import json

MAX_TURNS = 6

def answer_with_tools(user_text):
    messages = [{"role": "user", "content": user_text}]

    for _ in range(MAX_TURNS):
        response = client.chat.completions.create(
            model=MODEL, messages=messages, tools=tools,
        )
        msg = response.choices[0].message
        messages.append(msg)

        if not msg.tool_calls:
            return msg.content

        for call in msg.tool_calls:        # सभी समानांतर कॉल को संभालें
            messages.append(run_tool(call))  # पार्स, चलाएं, परिणाम-या-त्रुटि लौटाएं

    return "Sorry, I could not complete that."  # मोड़ की सीमा मारा

तीन निर्णय यह को उत्पादन-तैयार बनाते हैं एक डेमो के बजाय। सभी पर tool_calls, [0] पर नहीं, या आप मौन रूप से कॉल को छोड़ देंगे जो मॉडल ने पूछा। MAX_TURNS सीमित करें इसलिए एक मॉडल हमेशा के लिए स्पिन नहीं कर सकता। और विफलताओं को run_tool के माध्यम से रूट करें त्रुटि संदेशों के रूप में, इसलिए एक एकल बुरी कॉल पूरे विनिमय को क्रैश नहीं करता है।

जब आप कदमों को कोडित करना बंद करते हैं और मॉडल को अपने अगले कदम को हर मोड़ का निर्णय लेने देते हैं, यह सटीक लूप एक एजेंट बन जाता है। अंतर स्वायत्तता है, वास्तुकला नहीं।

Junoव्यवहार में उत्पादन हैंडलर मल्टी-मोड़ लूप है तीन चीजें वायर्ड के साथ: tool_calls के हर प्रविष्टि पर लूप न केवल पहली, मोड़ को सीमित करें ताकि वह हमेशा के लिए स्पिन न कर सके, और विफलताओं को टूल संदेशों के रूप में लौटाएं ताकि एक बुरी कॉल पूरे चलाने को क्रैश न करे। समान लूप, अधिक स्वायत्तता, और आपके पास एक एजेंट है। आर्किटेक्चर नहीं बदलता है, मॉडल केवल अपने अगले कदम को तय करता है।

उत्पादन हैंडलर सब कुछ को ऊपर मोड़ता है: बाध्य मल्टी-मोड़ लूप, समानांतर कॉल समवर्ती चलाते हैं, सत्यापित और लॉग किए गए निष्पादन, और डेटा के रूप में त्रुटियां लौटाई गई। शेष प्रश्न वह है जो अधिकांश टीमें छोड़ते हैं: कब मॉडल को बिल्कुल टूल न दें।

एक टूल सही उत्तर है जब कार्रवाई वास्तव में प्राकृतिक भाषा पर मॉडल के निर्णय पर निर्भर करता है: कौन सा आदेश उपयोगकर्ता का मतलब है, क्या यह रिफंड केस में गिना जाता है, क्या खोज करना है। यह गलत उत्तर है जब पथ नियतात्मक होता है, समान इनपुट हमेशा एक ही कॉल उत्पन्न करता है। यदि एक अनुरोध हमेशा एक ही कॉल समान व्युत्पन्न तर्कों के साथ ट्रिगर करता है, तो इसे कोड में वायर करें और राउंड-ट्रिप को छोड़ दें; आप एक भ्रम सतह को हटाते हैं, एक लेटेंसी हॉप, और शून्य नुकसान के लिए एक टोकन लागत। मॉडल को एक टूल सौंपना लचीलापन खरीदता है और इसे अनिश्चितता में भुगतान करता है, इसलिए इसे केवल वहां खर्च करें जहां लचीलापन बिंदु है।

python
def answer_with_tools(user_text):
    messages = [{"role": "user", "content": user_text}]

    for turn in range(MAX_TURNS):
        response = client.chat.completions.create(
            model=MODEL, messages=messages, tools=tools, tool_choice="auto",
        )
        msg = response.choices[0].message
        messages.append(msg)
        if not msg.tool_calls:
            return msg.content

        results = run_tools_concurrently(msg.tool_calls)  # स्वतंत्र कॉल समानांतर में
        messages.extend(results)

    log.warning("tool_loop_unconverged", turns=MAX_TURNS)
    return fallback_answer()

दो लीवर जब आप करते हैं टूल का उपयोग करें:

  • टूल-कॉल भ्रम को सीमित करें: एक मॉडल एक ऐसे टूल को कॉल करने का आविष्कार कर सकता है जिसे आपने कभी परिभाषित नहीं किया या तर्कों को गढ़ सकते हैं, इसलिए अज्ञात टूल नाम को आउट-राइट अस्वीकार करें, और tool_choice का उपयोग करें (पैरामीटर जो बल, निषेध, या मुक्त टूल का उपयोग करता है) एक टूल की आवश्यकता होने पर बजाय हर मोड़ के लिए छोड़ने के।
  • प्रेक्षणीयता ट्रेस रखें, टूल, तर्क, परिणाम, मोड़ की गणना, क्योंकि एक स्वायत्त लूप केवल तभी डिबग करने योग्य है जब आप पुनरावृत्ति कर सकते हैं कि वह वास्तव में क्या करता है।

यह हैंडलर एक एजेंट का शाब्दिक बीज है: एजेंट यह लूप है व्यापक टूलसेट और एक लक्ष्य की ओर कॉल को श्रृंखलित करने के लिए अक्षांश के साथ, जो यह भी है कि विफलता मोड यहां वहां स्केल क्यों हैं। (एसडीके सतह OpenAI-शैली है; tool_choice और संदेश लिफाफा प्रदाता द्वारा भिन्न होते हैं।)

Junoव्यवहार में पूर्ण हैंडलर बाध्य लूप है समवर्ती, सत्यापित, लॉग किए गए टूल रन और डेटा के रूप में त्रुटियां। छोड़ा गया प्रश्न यह है कि कब टूल नहीं दें: यदि कॉल नियतात्मक है, तो इसे कोड में वायर करें और राउंड-ट्रिप, लेटेंसी, और भ्रम सतह को छोड़ दें। `tool_choice` का उपयोग करें टूल को बजाय की आशा करने के बजाय बल या निषेध करने के लिए, अज्ञात टूल कॉल को अस्वीकार करें, और ट्रेस रखें, क्योंकि यह प्रशिक्षण पहियों के साथ एक एजेंट है, और विफलता मोड वहां से बड़े ही होते हैं।