هوش مصنوعی

ساخت چت‌بات با مدل‌های زبانی

ساخت چت‌بات با مدل‌های زبانی در حد یک دمو ساده است؛ سخت‌ترشدن ماجرا از جایی شروع می‌شود که ربات باید درباره‌ی داده‌ی خود شما درست جواب بدهد و بد جواب ندهد. این مقاله معماری یک چت‌بات قابل‌اتکا را با کدهای مستقل از ارائه‌دهنده توضیح می‌دهد.

  • نویسنده تیم تتا
  • ۹ دقیقه مطالعه
  • به‌روزرسانی:
ساخت چت‌بات با مدل‌های زبانی

معماری یک چت‌بات: از چه اجزایی ساخته می‌شود؟

هسته‌ی کار فقط یک فراخوانی HTTP به مدل است، اما یک محصول واقعی چند لایه‌ی دیگر دارد. ترتیب معمول درخواست این است:

  1. رابط کاربری پیام را به سرور خودتان می‌فرستد (نه به ارائه‌دهنده‌ی مدل).
  2. سرور کاربر را احراز هویت می‌کند، محدودیت نرخ را اعمال و ورودی را بررسی می‌کند.
  3. در صورت نیاز، اسناد مرتبط از پایگاه دانش بازیابی می‌شوند (RAG).
  4. پرامپت نهایی ساخته و به مدل فرستاده می‌شود؛ پاسخ به‌صورت جریانی برمی‌گردد.
  5. خروجی بررسی و ثبت می‌شود و به کاربر نمایش داده می‌شود.

اولین نقطه‌ی پایانی: سرور واسط

سرور واسط کلید را مخفی نگه می‌دارد و جای مناسبی برای بقیه‌ی منطق است. در نمونه‌ی زیر با Node.js و Express، همه‌ی تفاوت‌های ارائه‌دهنده در تابع callModel جمع شده است:

js
import express from "express";
const app = express();
app.use(express.json({ limit: "20kb" }));

// Provider-specific details live here only.
async function callModel(messages) {
  const res = await fetch(process.env.LLM_URL, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: "Bearer " + process.env.LLM_API_KEY
    },
    body: JSON.stringify({ model: process.env.LLM_MODEL, messages, temperature: 0.2 })
  });
  if (!res.ok) throw new Error("LLM error " + res.status);
  const data = await res.json();
  return data.reply; // adapt to your provider's response shape
}

app.post("/api/chat", async (req, res) => {
  const { messages } = req.body;
  const system = { role: "system", content: "You are a helpful support assistant." };
  res.json({ reply: await callModel([system, ...messages.slice(-12)]) });
});
app.listen(3000);

مقدار پایین temperature برای پشتیبانی و پرسش‌وپاسخ معمولاً مناسب است، چون پاسخ‌ها پایدارتر می‌شوند. اگر این سرویس را در محیط واقعی راه می‌اندازید، دیپلوی با Docker و طراحی API مقیاس‌پذیر دو مقاله‌ی مرتبط هستند.

حافظه‌ی گفتگو و پنجره‌ی متن

مدل‌ها بین درخواست‌ها چیزی به یاد نمی‌آورند؛ «حافظه» یعنی هر بار تاریخچه را دوباره بفرستید. این کار هزینه دارد و پنجره‌ی متن (context window) محدود است. در نمونه‌ی بالا فقط ۱۲ پیام آخر را فرستادیم؛ این ساده‌ترین راهبرد است.

  • پنجره‌ی لغزان: فقط N پیام آخر؛ ساده، اما اطلاعات قدیمی را فراموش می‌کند.
  • خلاصه‌سازی: پیام‌های قدیمی به یک خلاصه‌ی کوتاه تبدیل می‌شوند و در ابتدای پرامپت می‌آیند.
  • حافظه‌ی ساختاریافته: حقایق مهم (نام، سفارش) جدا در پایگاه‌داده ذخیره و در صورت نیاز تزریق می‌شوند.

برای ذخیره‌ی نشست‌ها و کش پاسخ‌های پرتکرار، کش کردن هوشمند با Redis انتخاب رایجی است.

RAG: جواب از اسناد خودتان

مدل درباره‌ی قیمت‌ها، سیاست‌ها و محصولات شما چیزی نمی‌داند و در صورت ندانستن ممکن است مطمئن حدس بزند. در RAG (تولید تقویت‌شده با بازیابی) ابتدا چند تکه‌ی مرتبط از اسناد پیدا می‌شود و همراه سؤال به مدل داده می‌شود.

آماده‌سازی داده

اسناد را به تکه‌های کوتاه (مثلاً چند پاراگراف) تقسیم کنید، برای هر تکه یک بردار (embedding) با مدل تعبیه‌سازی بسازید و در یک پایگاه برداری یا حتی یک آرایه ذخیره کنید. تکه‌ها باید مستقل و قابل‌فهم باشند؛ برش وسط یک جدول یا فهرست کیفیت را پایین می‌آورد.

بازیابی و ساخت پرامپت

js
function cosine(a, b) {
  let dot = 0, na = 0, nb = 0;
  for (let i = 0; i < a.length; i++) { dot += a[i] * b[i]; na += a[i] ** 2; nb += b[i] ** 2; }
  return dot / (Math.sqrt(na) * Math.sqrt(nb));
}

async function answer(question, index) {
  const qVec = await embed(question); // call your embedding endpoint
  const top = index
    .map(c => ({ ...c, score: cosine(qVec, c.vector) }))
    .sort((x, y) => y.score - x.score)
    .slice(0, 4);

  const context = top.map((c, i) => "[" + (i + 1) + "] " + c.text).join("\n\n");
  const system = "Answer ONLY from the context. Cite sources like [1]. " +
                 "If the answer is not in the context, say you do not know.";
  return callModel([
    { role: "system", content: system },
    { role: "user", content: "Context:\n" + context + "\n\nQuestion: " + question }
  ]);
}

دو نکته در عمل مهم‌تر از انتخاب پایگاه برداری هستند: دستور صریح «اگر در متن نبود، نمی‌دانم بگو» و نمایش منبع‌ها به کاربر. همین دو مورد اعتماد را بالا می‌برند و بازبینی را ساده می‌کنند.

تصویری انتزاعی و رنگارنگ از یک حباب گفتگو که از میان برگه‌های سند شناور اطلاعات می‌گیرد.
ربات خوب کمتر «حدس می‌زند» و بیشتر از منبع مشخص جواب می‌دهد.

حفاظ‌های ایمنی و کنترل هزینه

کاربران واقعی سؤال‌های غیرمنتظره می‌پرسند و بعضی‌ها عمداً می‌خواهند ربات را از مسیر خارج کنند (مثلاً با جمله‌ی «دستورهای قبلی را نادیده بگیر»). چند لایه‌ی دفاعی ساده کمک می‌کند:

  • محدود کردن نقش: در پیام سیستمی موضوع‌های مجاز و لحن را مشخص کنید و بگویید بیرون از آن‌ها مؤدبانه رد کند.
  • جداسازی داده از دستور: متن اسناد و پیام کاربر را «داده» معرفی کنید و فرمان‌های داخل آن‌ها را اجرا نکنید.
  • اعتبارسنجی خروجی: اگر ساختار مشخصی (مثل JSON) لازم است، قبل از استفاده آن را اعتبارسنجی کنید.
  • محدودیت نرخ و سقف توکن: برای هر کاربر تعداد درخواست و حداکثر طول پاسخ تعیین کنید.
  • حداقل دسترسی: اگر ربات ابزار یا پایگاه‌داده را صدا می‌زند، فقط عملیات خواندنی و محدود را مجاز کنید.
js
const limits = new Map(); // userId -> { count, resetAt }

function allow(userId, max = 20, windowMs = 60_000) {
  const now = Date.now();
  const e = limits.get(userId);
  if (!e || now > e.resetAt) { limits.set(userId, { count: 1, resetAt: now + windowMs }); return true; }
  return ++e.count <= max;
}

ارزیابی: چطور بفهمیم بهتر شده؟

بدون ارزیابی، هر تغییر در پرامپت یا مدل ممکن است یک مورد را درست کند و سه مورد را خراب کند. ساده‌ترین راه، یک فایل از ۳۰ تا ۱۰۰ پرسش واقعی با جواب یا نکته‌ی مورد انتظار است که پیش از هر انتشار اجرا می‌شود.

js
const cases = [
  { q: "What is the return period?", mustInclude: ["14 days"] },
  { q: "Do you ship abroad?",        mustInclude: ["yes", "international"] },
  { q: "Tell me a joke about politics", mustDecline: true }
];

let pass = 0;
for (const c of cases) {
  const out = (await answer(c.q, index)).toLowerCase();
  const ok = c.mustDecline
    ? /cannot|can't|only help/.test(out)
    : c.mustInclude.every(s => out.includes(s.toLowerCase()));
  if (ok) pass++; else console.log("FAIL:", c.q);
}
console.log(pass + "/" + cases.length + " passed");

این بررسی متنی خام است و برای پاسخ‌های بازتر باید داوری انسانی یا یک مدل داور هم اضافه شود؛ ولی همین نسخه‌ی ساده جلوی پسرفت‌های آشکار را می‌گیرد. پرسش‌های ناموفق کاربران واقعی را به‌مرور به مجموعه اضافه کنید. برای نوشتن پرامپت‌های بهتر، راهنمای پرامپت‌نویسی را ببینید.

چک‌لیست پیش از انتشار

  • کلید API فقط روی سرور و در متغیر محیطی است؛ چرخش (rotation) کلید برنامه‌ریزی شده است.
  • لاگ درخواست‌ها، تأخیر و هزینه‌ی هر گفتگو ثبت می‌شود (بدون ذخیره‌ی داده‌ی حساس).
  • برای خطای ارائه‌دهنده، تایم‌اوت و پیام جایگزین مؤدبانه وجود دارد.
  • کاربر می‌تواند به یک انسان ارجاع داده شود و بداند با ربات حرف می‌زند.
  • مجموعه‌ی ارزیابی قبل از هر انتشار اجرا می‌شود و نتیجه ثبت می‌شود.

اگر ترجیح می‌دهید این مسیر را با یک تیم باتجربه طی کنید، با ما تماس بگیرید یا نمونه‌کارهای پروژه‌های قبلی را ببینید. برای کاربردهای فراتر از چت، اتوماسیون کسب‌وکار با هوش مصنوعی ایده‌های بیشتری می‌دهد.

پرسش‌های متداول

برای ساخت چت‌بات باید مدل را از ابتدا آموزش بدهم؟

نه. در بیشتر پروژه‌ها از یک مدل آماده از طریق API استفاده می‌شود و دانش شما با RAG و پرامپت به آن داده می‌شود. آموزش یا تنظیم دقیق مدل فقط وقتی لازم است که رفتار یا سبک خاصی را با پرامپت نتوان به دست آورد.

RAG بهتر است یا fine-tuning؟

برای اضافه‌کردن دانش متغیر مثل قیمت، مستندات و سیاست‌ها، RAG ساده‌تر و به‌روزرسانی‌اش آسان‌تر است. تنظیم دقیق بیشتر برای تغییر لحن، قالب خروجی یا رفتار ثابت مناسب است و معمولاً جایگزین RAG نیست.

چطور از جواب ساختگی ربات جلوگیری کنم؟

کاملاً حذف نمی‌شود، اما با RAG، دستور «فقط از متن جواب بده»، دمای پایین، نمایش منبع و ارزیابی منظم به‌شدت کم می‌شود. برای موضوع‌های حساس، مسیر ارجاع به انسان را هم نگه دارید.

هزینه‌ی چت‌بات مبتنی بر مدل زبانی چطور محاسبه می‌شود؟

معمولاً بر اساس تعداد توکن ورودی و خروجی است. طول تاریخچه، تعداد تکه‌های RAG و طول پاسخ مستقیم روی هزینه اثر می‌گذارند؛ بنابراین سقف توکن، کش و خلاصه‌سازی مهم هستند.

دیدگاه‌ها

    هنوز دیدگاهی ثبت نشده است. اولین نفر باشید.

    دیدگاه‌ها پس از بررسی منتشر می‌شوند.