Chatbot Oluşturmak İçin Büyük Model API'si: FastAPI Backend ve Akış Frontend
Klavye yazma efektli bir sohbet botu yapmak için yalnızca üç bileşen gerekir: isteği yönlendiren bir arka uç, akışı okuyan bir web sayfası ve konuşmayı hatırlayan bir depolama. Bu makalede FastAPI arka uç olarak kullanılır, tarayıcı native fetch ile akış okunur ve konuşma geçmişi kendi SQLite veritabanınıza kaydedilir. Tüm süreç yüz satırdan az kod gerektirir ve yetişkin kullanıcılar için ürün prototipi olarak uygundur.
Güncelleme:
Önemli Noktalar
- API anahtarı yalnızca backend'de bulunur; tarayıcı asla temas etmez. Frontend yalnızca kendi /chat uç noktanızla iletişim kurar.
- Model sizin için konuşmayı hatırlamaz: Her istekte, son birkaç konuşma geçmişini depolamanızdan alıp uç noktaya göndermeniz gerekir.
- Akış çıktısının anahtarı, arka ucun StreamingResponse ile yönlendirme yapması ve ön ucun reader.read() döngüsüyle metni birleştirerek yazmasıdır.
- Yetişkinlere yönelik ürünlerde giriş noktasında yaş doğrulaması yapılmalı ve geçmiş uzunluğu ile tek seferlik girdi uzunluğu sınırlandırılmalıdır.
Önce bir şema çizin: Üç rol, üç sorumluluk
Chatbot'u bir restoran olarak düşünün: Tarayıcı müşteri, backend garson, LLM API'si ise mutfaktır. Müşteri doğrudan mutfağa girmez; tüm siparişler garson tarafından iletilir. İşte API anahtarının frontend'de neden bulunmaması gerektiğinin nedeni budur.
| Rol | Sorumluluk | Yapmaması Gerekenler |
|---|---|---|
| Tarayıcı | Görüntüleme, giriş toplama, akışı okuma | API anahtarını tutma |
| FastAPI Backend | Geçmişi kaydet, mesajları birleştir, isteği yönlendir | API anahtarını frontend'e döndürme |
| Model API'si | Mesajlara göre yanıt oluşturma | — Her seferinde yalnızca size gönderilenleri işler |
Bir konuşmanın akışı şöyledir: web sayfası bir mesaj gönderir, arka uç bunu kaydeder, yakın geçmiş yüklenir, system mesajı birleştirilir, uç noktaya istek gönderilir, yanıt akarken web sayfasına iletilir ve tamamlanınca tam yanıt da kaydedilir.
Neden ön uç yerine arka uç üzerinden yönlendirme seçmelisiniz? Anahtar güvenliğinin yanı sıra iki pratik neden vardır: Birincisi, yönlendirme sırasında geçmiş eklemeniz, hız limiti uygulamanız ve filtreleme yapmanız gerekir; bunlar yalnızca arka uçta yapılabilir. İkincisi, gelecekte model veya hizmet değiştirdiğinizde yalnızca arka uçta bir değişiklik yapmanız yeterlidir, ön uç tamamen değişmez.
Backend: FastAPI ve SQLite
Önce bağımlılıkları yükleyin ve başlatın:
pip install fastapi uvicorn openai
export API_KEY=你的密钥
uvicorn server:app --reload --port 8000Ardından server.py dosyasını yazın. Kodda dikkat etmeniz gereken dört nokta var:
# 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")load()yalnızca son 20 mesajı alır ve konuşmanın 100.000 token bağlam penceresini aşmasını engeller.gen()是异步生成器,每收到一小段内容就 yield 出去,前端因此能逐字显示。- Akışın sonunda yalnızca kullanım istatistiklerini içeren bir veri bloğu gelir; choices alanı boş olduğundan bu bloğu atlayın.
finallybloğunda tam yanıtı kaydedin; hata olsa bile oluşturulan kısım kaybolmaz.
Veritabanıyla uğraşmak istemiyorsanız load ve save işlevlerini sözlük okuma/yazma ile değiştirebilirsiniz; diğer kodlar değişmez. Aksine, trafik arttığında SQLite yerine tanıdığınız herhangi bir veritabanına geçebilirsiniz; arayüz bu iki işlevin yapısını koruyacaktır. Bir not: örnek senkron sqlite3 kullanır, yazma hızlıdır ve prototip için yeterlidir; yüksek eşzamanlı istekler için asenkron sürücüleri değerlendirin.
Frontend: fetch ile akış okuma
Tarayıcıda herhangi bir kütüphaneye gerek yoktur. resp.body.getReader() ile bir okuyucu alın, read() döngüsünde her adımda bir bayt bloğu alın, TextDecoder ile decode edin ve sayfaya ekleyin. Aşağıdaki içeriği index.html olarak kaydedin ve server.py ile aynı dizinde tutun:
<!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>İki detay: decode ederken { stream: true } ekleyin, çünkü bir karakterin baytları iki bloğa bölünebilir ve bu olmadan bozuk karakterler oluşur. Ayrıca, gönderme mantığı istek süresince butonu devre dışı bırakmalıdır; örnek kısa olması için bunu yazmadı, yayına alırken lütfen ekleyin.
Çalıştırdıktan sonra tarayıcıda 8000 portunu açın, yaş onayını işaretleyin ve bir mesaj göndererek deneyin. Metin tek parça halinde aniden beliriyorsa ve harf harf yazılmıyorsa, büyük olasılıkla bir ara sunucu yanıtı önbelleğe alıyor demektir; önce doğrudan yerel porta bağlanarak bunu hariç tutun. Nginx arkasına dağıtıldığında, bu yol için yanıt önbelleğini kapatmanız gerekir.
Konuşma Geçmişi: Neden kendi sunucunuzda saklamalısınız
Sohbet API'si "durumsuz"dur; sanki her seferinde size uzattığınız notu yalnızca okuyan bir görevli gibidir: Notta ne varsa, o kadarını bilir. Bu nedenle "bağlamı korumak" sizin sorumluluğunuzdadır ve bunu yapmanın üç seviyesi vardır:
- Basit:Sadece bellekte sözlük ile saklanır. Yeniden başlatma ile kaybolur; hata ayıklama için uygundur.
- Yaygın:Örnekte gösterildiği gibi SQLite kullanın; her mesajı ayrı bir satır olarak kaydedin ve session_id ile son N mesajı sorgulayın.
- İleri düzey: Geçmiş çok uzun olduğunda, daha eski içeriklerin bir özetini modelle oluşturun ve system mesajından sonra yerleştirin; yakın mesajlar orijinal metin olarak korunur.
Veriyi kendi arka ucunda tutmanın bir diğer avantajı da saklama süresini tamamen kontrol etmenizdir. "Konuşma geçmişini temizle" butonu sağlamanız, ilgili session kaydını gerçekten silmeniz ve gizlilik politikanda neyi kaydettiğinizi açıkça belirtmeniz önerilir.
Özet yönteminin tetiklenme koşulu basit olabilir: Tahmini geçmiş token sayısı on bini aştığında, en eski mesajların yarısını modele özetletip 300 kelimeyi geçmeyen bir parçaya dönüştürün, depolamaya yazın ve orijinal mesajları silin. Bu sayede bağlam korunur ve her istekteki veri hacmi kontrol edilebilir sınırlarda kalır. Unutmayın: özet de model tarafından üretilir ve detayları atlayabilir; önemli ayarlar (örneğin kullanıcı takma adı, yasaklı konular) özetin korunmasına güvenmek yerine system mesajında sabitlenmelidir.
Yetişkinlere yönelik ürün: Giriş ve sınırlar
Bu hizmet yalnızca 18 yaş ve üzeri yetişkin kullanıcılar içindir; uygulamanız da öyle olmalıdır. Örnek ön uçta en basit onay giriş alanı bulunur; daha sıkı ürünler daha kapsamlı bir yaş doğrulama akışı ekleyebilir. İşte bazı uygulama önerileri:
- Giriş sayfasında yaş sınırını açıkça belirtin; onaylanmadığında sohbet arayüzünü göstermeyin.
- Ürünü öğrencilere veya gençlere pazarlamayın ve onların kullanımına yönelik ortamlara dahil etmeyin.
- Reşit olmayan kullanıcıların cinsel içerikleri kurgusal olsa bile API tarafından engellenir ve 403 döndürülür; arka ucunuz bu durumu algılamalı ve kullanıcıya ham hatayı göstermek yerine dostça bir uyarı göstermelidir.
Ürün tasarımında, kullanıcılara ayarlardan bir takma ad ve üslup tercihi belirlemelerine de izin verebilirsiniz. Bunları system mesajına yazmak sohbeti kişiselleştirir ve modelin tahmin etmesine gerek kalmaz.
Yayınlamadan önce güçlendirme ve genişletme
- Girdi sınırlaması:Örnekte tek bir mesaj 4000 karakterle sınırlandırılmıştır; ihtiyaca göre ayarlayın.
- Hız limitini uygulayın: Her anahtar için dakikada 300 istek; kullanıcı veya IP bazlı olarak arka uçta önceden hız limiti uygulayın.
- Hata gösterimi:Arka uç istisnaları yakalayıp ön uca kısa bir uyarı döndürmelidir; yığın izini (stack trace) dışarıya vermeyin.
- Kullanım izleme:Token sayısını istatistik olarak almak istiyorsanız, non-streaming isteklerde usage alanını okuyun veya akışın son bloğunda okuyun. Detaylar için token ve faturalandırma bölümüne bakın.
- Çerçeve değiştirme:Ön uç Vue veya React'e geçildiğinde akış okuma mantığı aynı kalır. Arka uç Express'e geçildiğinde StreamingResponse, ilgili akış yazımına dönüştürülür.
Daha kapsamlı parametre açıklamaları için API parametre detayları bölümüne bakın; daha fazla soru için SSS bölümüne başvurun.
Son olarak yayınlamadan önce bir öz-denetim yapın: Anahtar yalnızca sunucu ortam değişkenlerinde mi? Tek girdi ve geçmiş uzunluğu için bir üst sınır var mı? Hatalar için dostça uyarılar gösteriliyor mu? Sohbet temizlendiğinde kayıtlar gerçekten siliniyor mu? Yaş onayı giriş alanında mı? Bu beş koşulu sağlıyorsanız, bu prototipi gerçek kullanıcılarla denemeye hazırdır.
Sıkça Sorulan Sorular
Neden tarayıcının doğrudan uç noktayı çağırmasına izin verilmiyor?
Çünkü API anahtarı web sayfasında görünür hale gelir ve herkes kopyalayıp çalabilir. Tarayıcının yalnızca kendi backend'inize erişmesini sağlamak ve anahtarı backend'de tutmak daha güvenli bir yaklaşımdır.
Kaç konuşma geçmişi saklanmalı?
Senaryoya bağlıdır; örnekte son 20 mesaj alınır. Maliyeti ve bağlam uzunluğunu kontrol etmek için, birkaç mesajı daha az dahil etmek ve ana fikirleri özetle korumak daha iyidir.
Akış çıktısı sırasında hata olursa ne yapılır?
Arka uç istisnayı yakalar ve ön uca bir uyarı gösterir; ayrıca oluşturulan kısmı kaydeder. Ön uç yeniden deneme butonu sağlayabilir ve son kullanıcı mesajını yeniden gönderebilir.
Bu bot gençlere yönelik olabilir mi?
Hayır. Hizmet yalnızca 18 yaş ve üzeri yetişkinler içindir; uygulamanız giriş alanında yaş doğrulaması yapmalıdır.
Anahtarı almak için formu doldurmanız yeterlidir
Hesap oluşturun, anahtarı kopyalayın, Base URL'yi değiştirin. Yapılandırma bu kadar basit.