HI ▾
API कुंजी प्राप्त करें

बड़े मॉडल API से चैटबॉट बनाएं: FastAPI बैकएंड और स्ट्रीमिंग फ्रंटएंड

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

अंतिम अद्यतन:

मुख्य बिंदु

  • API कुंजी केवल बैकएंड पर रखें, ब्राउज़र कभी भी API कुंजी तक नहीं पहुँचता, फ्रंटएंड केवल आपके /chat एंडपॉइंट से बात करता है।
  • मॉडल आपके लिए चैट इतिहास नहीं याद रखता। हर अनुरोध में आपको अपने स्टोरेज से पिछले संदेश निकालकर एंडपॉइंट पर भेजने होते हैं।
  • स्ट्रीमिंग आउटपुट की मुख्य बात यह है कि बैकएंड StreamingResponse का उपयोग करके आगे बढ़ाता है, और फ्रंटएंड reader.read() लूप का उपयोग करके जोड़ता है।
  • वयस्कों के लिए उत्पाद में प्रवेश द्वार पर आयु सत्यापन होना चाहिए, और चैट इतिहास की लंबाई व एकल इनपुट की सीमा निर्धारित करनी चाहिए।

पहले एक छोटा चित्र बनाएं: तीन भूमिकाएं, तीन काम

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

भूमिकाजिम्मेदारीक्या नहीं करना चाहिए
ब्राउज़रप्रदर्शन, इनपुट एकत्र करना, स्ट्रीम पढ़नाAPI कुंजी रखना
FastAPI बैकएंडइतिहास सेव, messages बनाएं, अनुरोध आगे बढ़ाएंAPI कुंजी फ्रंटएंड को वापस करना
API इंटरफ़ेसmessages के आधार पर उत्तर उत्पन्न करें—यह केवल वही संसाधित करता है जो आप भेजते हैं

एक संवाद का प्रवाह इस प्रकार है: वेबपेज एक वाक्य भेजता है, बैकएंड उसे स्टोर करता है, हाल ही के इतिहास को निकालता है, system को जोड़ता है, request interface को अनुरोध करता है, प्राप्त करते समय आगे बढ़ाता है, और प्राप्त करने के बाद पूरा उत्तर भी स्टोर करता है।

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

बैकएंड: FastAPI और SQLite

पहले निर्भरताएँ इंस्टॉल करें और सर्वर चलाएं:

pip install fastapi uvicorn openai
export API_KEY=你的密钥
uvicorn server:app --reload --port 8000

फिर server.py लिखें। कोड में चार बिंदुओं पर रुककर देखें:

# server.py
import os
import sqlite3
import uuid

from fastapi import FastAPI
from fastapi.responses import FileResponse, StreamingResponse
from pydantic import BaseModel
from openai import AsyncOpenAI

app = FastAPI()
client = AsyncOpenAI(base_url="https://api.apidamoxing.com/v1", api_key=os.environ["API_KEY"])

SYSTEM = {"role": "system", "content": "你是『小墨』,一个说话简洁、爱用比喻的聊天伙伴。回答控制在 200 字内。"}
KEEP = 20  # 每次只带最近 20 条消息

db = sqlite3.connect("chat.db", check_same_thread=False)
db.execute("create table if not exists msg(id integer primary key autoincrement, sid text, role text, content text)")

def load(sid, n=KEEP):
    rows = db.execute("select role, content from msg where sid=? order by id desc limit ?", (sid, n)).fetchall()
    return [{"role": r, "content": c} for r, c in reversed(rows)]

def save(sid, role, content):
    db.execute("insert into msg(sid, role, content) values (?,?,?)", (sid, role, content))
    db.commit()

class ChatIn(BaseModel):
    session_id: str
    message: str

@app.get("/")
def index():
    return FileResponse("index.html")

@app.post("/session")
def new_session():
    return {"session_id": uuid.uuid4().hex}

@app.post("/chat")
async def chat(body: ChatIn):
    save(body.session_id, "user", body.message[:4000])
    messages = [SYSTEM] + load(body.session_id)

    async def gen():
        parts = []
        try:
            stream = await client.chat.completions.create(
                model="uncensored", messages=messages,
                stream=True, max_tokens=800, temperature=0.8,
            )
            async for chunk in stream:
                if not chunk.choices:        # 末尾的 usage 块没有 choices
                    continue
                delta = chunk.choices[0].delta.content
                if delta:
                    parts.append(delta)
                    yield delta
        except Exception as e:
            yield f"\n[请求失败:{type(e).__name__}]"
        finally:
            if parts:
                save(body.session_id, "assistant", "".join(parts))

    return StreamingResponse(gen(), media_type="text/plain; charset=utf-8")
  1. load() केवल पिछले 20 संदेश लौटाता है, ताकि चैट लंबी न हो और 100,000 टोकन की कॉन्टेक्स्ट विंडो न फटें।
  2. gen() एक असिंक्रोनस जनरेटर है। यह हर छोटे डेटा टुकड़े पर yield करता है, जिससे फ्रंटएंड अक्षर-दर-अक्षर प्रदर्शित कर सकता है।
  3. स्ट्रीम के अंत में केवल usage डेटा वाला एक ब्लॉक होता है, जिसमें choices नहीं होते, इसलिए खाली चेक करके छोड़ दें।
  4. finally ब्लॉक में पूरा उत्तर सहेजा जाता है। यदि बीच में त्रुटि आती है, तो उत्पन्न भाग खोता नहीं है।

यदि आप डेटाबेस का उपयोग नहीं करना चाहते, तो load और save को डिकशनरी पर पढ़ने-लिखने में बदल दें। बाकी कोड वही रहेगा। भविष्य में SQLite को किसी अन्य डेटाबेस से बदला जा सकता है, जब तक कि इन दो फ़ंक्शनों का रूप बना रहे। उदाहरण में सिंक्रोनस sqlite3 का उपयोग किया गया है, जो प्रोटोटाइप के लिए पर्याप्त है; उच्च समानांतर अनुरोधों के लिए असिंक्रोनस ड्राइवर पर विचार करें।

फ्रंटएंड: fetch से स्ट्रीम पढ़ें

ब्राउज़र को किसी लाइब्रेरी की ज़रूरत नहीं है। resp.body.getReader() से एक रीडर मिलता है, read() लूप चलाएं, हर बार एक बाइट्स ब्लॉक मिलता है, TextDecoder से डिकोड करें और पेज पर जोड़ें। नीचे दिया गया कोड index.html में सेव करें, server.py के साथ ही डायरेक्टरी में:

<!doctype html>
<meta charset="utf-8">
<title>小墨</title>
<div id="gate">
  <label><input type="checkbox" id="adult"> 我已年满 18 周岁</label>
  <button id="enter">进入</button>
</div>
<div id="app" hidden>
  <div id="log" style="white-space:pre-wrap;min-height:300px"></div>
  <input id="box" placeholder="说点什么"> <button id="send">发送</button>
</div>
<script>
let sid = localStorage.getItem("sid");
const log = document.getElementById("log");

document.getElementById("enter").onclick = async () => {
  if (!document.getElementById("adult").checked) return;
  if (!sid) {
    const r = await fetch("/session", { method: "POST" });
    sid = (await r.json()).session_id;
    localStorage.setItem("sid", sid);
  }
  document.getElementById("gate").hidden = true;
  document.getElementById("app").hidden = false;
};

document.getElementById("send").onclick = async () => {
  const box = document.getElementById("box");
  const text = box.value.trim();
  if (!text) return;
  box.value = "";
  log.textContent += "\n你:" + text + "\n小墨:";
  const resp = await fetch("/chat", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ session_id: sid, message: text }),
  });
  const reader = resp.body.getReader();
  const decoder = new TextDecoder("utf-8");
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    log.textContent += decoder.decode(value, { stream: true });
  }
  log.textContent += "\n";
};
</script>

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

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

चैट इतिहास: इसे अपने बैकएंड पर ही क्यों सहेजें

चैट API 'स्टेटलेस' है, जैसे एक कर्मचारी जो केवल उसी कागज़ को देखता है जो आप उसे देते हैं। इसलिए 'कॉन्टेक्स्ट विंडो याद रखना' आपकी जिम्मेदारी है। इसे तीन स्तरों पर लागू करें:

  1. सरलतम:केवल मेमोरी में शब्दकोश का उपयोग करके संग्रहीत करें। रीस्टार्ट करने पर डेटा हट जाता है, जो डीबगिंग के लिए उपयुक्त है।
  2. सामान्य:उदाहरण की तरह SQLite का उपयोग करें, प्रत्येक संदेश एक पंक्ति में, session_id के आधार पर नवीनतम N संदेश खोजें।
  3. उन्नत:इतिहास बहुत लंबा होने पर, पुराने संदेशों को मॉडल से सारांश में बदलवाएं और system प्रॉम्प्ट के बाद रखें, हालिया संदेश मूल रूप में रखें।

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

सारांश विधि के ट्रिगर शर्तें सरल हो सकती हैं: जब अनुमानित इतिहास टोकन 10,000 से अधिक हो जाएं, तो सबसे पुराने आधे संदेशों को मॉडल से 300 शब्दों से कम के एक सारांश में बदलवाएं, उन्हें स्टोरेज में लिखें और मूल संदेश हटा दें। इससे संदर्भ बना रहता है और प्रत्येक अनुरोध का आकार नियंत्रित सीमा में स्थिर रहता है। ध्यान रखें कि सारांश स्वयं मॉडल द्वारा उत्पन्न होता है, इसलिए विवरण छूट सकते हैं; महत्वपूर्ण सेटिंग्स (जैसे उपयोगकर्ता की उपनाम, प्रतिबंधित विषय) को system प्रॉम्प्ट में स्थिर रखना बेहतर है, न कि सारांश पर निर्भर रहना।

वयस्कों के लिए उत्पाद: प्रवेश और सीमाएँ

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

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

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

लाइव करने से पहले मजबूती और विस्तार

  • इनपुट सीमा:उदाहरण में प्रत्येक संदेश को 4000 वर्णों तक सीमित किया गया है, आवश्यकतानुसार समायोजित करें।
  • आवृत्ति सीमा:प्रति मिनट 300 अनुरोध प्रति कुंजी, उपयोगकर्ता या IP के आधार पर बैकएंड में पहले ही रेट लिमिट लागू करें।
  • त्रुटि प्रदर्शन: बैकएंड अपवाद को पकड़ता है और फ्रंटएंड को संक्षिप्त संदेश भेजता है, स्टैक ट्रेस को प्रकट नहीं करता।
  • उपयोग मॉनिटरिंग:टोकन की गणना करने के लिए, नॉन-स्ट्रीमिंग अनुरोधों में usage पढ़ें, या स्ट्रीम के अंतिम ब्लॉक में पढ़ें, विवरण के लिए टोकन और शुल्क देखें।
  • फ्रेमवर्क बदलें: जब फ्रंटएंड Vue या React में बदल दिया जाता है, तो स्ट्रीम पढ़ने की तर्कसंगतता बिल्कुल समान रहती है। बैकएंड को Express में बदलने पर केवल StreamingResponse को संबंधित स्ट्रीमिंग कोडिंग शैली में बदलना होता है।

अधिक पूर्ण पैरामीटर विवरण के लिए, इंटरफ़ेस पैरामीटर विस्तृत देखें; अधिक प्रश्नों के लिए सामान्य प्रश्न देखें।

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

सामान्य प्रश्न

ब्राउज़र को सीधे इंटरफ़ेस कॉल करने से क्यों रोकें?

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

चैट इतिहास में कितने संदेश संग्रहीत करें?

यह परिदृश्य पर निर्भर करता है, उदाहरण में नवीनतम 20 संदेश लिए गए हैं। लागत और कॉन्टेक्स्ट विंडो को नियंत्रित करने के लिए, कुछ कम संदेश लेना बेहतर है, और सारांश के साथ मुख्य बिंदुओं को बनाए रखें।

स्ट्रीमिंग आउटपुट के बीच में विफलता हो जाए तो?

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

क्या इस बॉट को किशोरों के लिए उपयोग किया जा सकता है?

नहीं। सेवा केवल 18 वर्ष और उससे अधिक आयु के वयस्कों के लिए है, आपके ऐप को भी प्रवेश बिंदु पर आयु पुष्टि करनी चाहिए।

केवल फॉर्म भरें और कुंजी प्राप्त करें

खाता बनाएं, कुंजी कॉपी करें, Base URL बदलें। कॉन्फ़िगरेशन इतना ही सरल है।

API कुंजी प्राप्त करें