TH ▾
Uncensored Chatbot APIการเข้าถึง API โดยตรงไปยัง LLM แบบไม่เซ็นเซอร์หนึ่งโมเดลรับคีย์ API

Uncensored Chatbot APIเว็บแชท

มาสร้างเว็บแชทแบบสตรีม (Express Proxy + JS ธรรมดา)

ในบทช่วยสอนนี้ คุณจะได้สร้างหน้าแชทขนาดเล็กด้วยโค้ดประมาณ 100 บรรทัด เซิร์ฟเวอร์ Express ขนาดเล็กจะเก็บคีย์ลับของคุณและส่งต่อคำขอไปยัง Uncensored Chatbot API ในขณะที่ส่วนหน้าแบบ JavaScript ธรรมดาจะอ่านการตอบกลับแบบสตรีมมิงทีละคำ ไม่มีเฟรมเวิร์ก ไม่มีขั้นตอนการ build และคีย์ของคุณจะไม่ถูกส่งไปยังเบราว์เซอร์

อัปเดตเมื่อ

จุดสำคัญ

  • อย่าเรียก API จากโค้ดเบราว์เซอร์เลย เส้นทางเซิร์ฟเวอร์จะเก็บคีย์ให้เป็นส่วนตัวและอนุญาตให้คุณตรวจสอบอินพุต
  • โปรxies ต้องการเพียงส่งเหตุการณ์ที่ส่งโดยเซิร์ฟเวอร์ผ่าน; หน้าเว็บจะแยกวิเคราะห์บรรทัดข้อมูลจนกว่าจะถึง [DONE]
  • ใช้ textContent ขณะสตรีมและทำเครื่องหมายให้ปลอดภัยก่อนเรนเดอร์มาร์กดาวน์
  • จำกัดความยาวประวัติและ max_tokens บนเซิร์ฟเวอร์เพื่อให้ผู้ใช้หนึ่งรายไม่สามารถใช้เงินคงเหลือทั้งหมดของคุณได้

สิ่งที่เรากำลังสร้าง และเหตุใดจึงต้องการโปรxies

จินตนาการถึงหน้าเว็บที่มีช่องข้อความและบันทึกการสนทนาแบบเลื่อนดูได้ คุณพิมพ์ข้อความ หน้าเว็บจะส่งการสนทนาไปยัง /api/chat บนเซิร์ฟเวอร์ของคุณ และเซิร์ฟเวอร์ของคุณจะส่งต่อไปยัง upstream โดยแนบคีย์จริงไปด้วย การตอบกลับจะถูกสตรีมกลับ และแต่ละส่วนจะปรากฏทันทีที่ได้รับ เราจะกำหนดบุคลิกเล็กน้อยให้กับบอท โดยเป็นยามประภาสชื่อ Wren เพื่อให้การสาธิตดูมีชีวิตชีวา

คุณต้องมี Node 18 ขึ้นไป (เพื่อใช้ fetch ในตัว) และคีย์ API หากคุณยังไม่มี ให้รับเครดิตทดลองใช้ฟรีจากหน้า key: บัญชีใหม่จะได้รับเครดิต $0.50 เป็นเวลา 7 วัน โดยไม่ต้องใส่รายละเอียดการชำระเงิน Base URL คือ https://api.uncensoredchatbotapi.com/v1 และ id ของโมเดลคือ uncensored

เหตุใดคุณจึงต้องการโปรxies

tempting ที่จะวางคีย์ลงในโค้ด front-end และเรียก API โดยตรง กรุณาอย่าทำ สิ่งใดๆ ที่ส่งไปยังเบราว์เซอร์สามารถอ่านได้โดยทุกคนที่เปิดเครื่องมือพัฒนา และคีย์ที่รั่วไหลหมายถึงยอดเงินหมด โปรxies แก้ไขปัญหานี้และให้ประโยชน์เพิ่มเติมสามประการ

  • ความลับ คีย์จะอยู่ในตัวแปรสภาพแวดล้อมบนเซิร์ฟเวอร์
  • การควบคุม คุณตัดสินใจพรอมต์ระบบ ความยาวประวัติและ max_tokens ดังนั้นผู้ใช้ไม่สามารถแทนที่ค่าเหล่านั้นได้
  • ที่สำหรับกฎเกณฑ์ ขีดจำกัดต่อผู้ใช้ เกณฑ์อายุ และนโยบายการบันทึกข้อมูลทั้งหมดอยู่ในเลเยอร์นี้ คู่มือ ความปลอดภัย สร้างบนเส้นทางนี้

ขั้นตอนที่ 1: ตั้งค่าโครงการ

สร้างโฟลเดอร์ เริ่มต้นเป็นโครงการ ES-module และติดตั้ง Express สิ่งอื่น ๆ สร้างขึ้นแล้ว

mkdir lantern-chat && cd lantern-chat
npm init -y
npm pkg set type=module
npm install express
mkdir public

คุณจะได้สองสิ่ง: server.js ที่รากและโฟลเดอร์ public ที่เก็บหน้าเว็บและสคริปต์

ขั้นตอนที่ 2: เขียน proxy Express

เซิร์ฟเวอร์ให้บริการไฟล์แบบสแตติกและเปิดใช้งานเส้นทาง POST หนึ่งเส้นทาง อ่านโค้ดนี้สามส่วน ส่วนแรกจะทำความสะอาดประวัติที่ได้รับ: เฉพาะบทบาท user และ assistant เท่านั้นที่ผ่านได้ แต่ละข้อความจะถูกตัดให้เหลือ 4,000 ตัวอักษร และเก็บเฉพาะการสนทนา 30 ครั้งล่าสุด ส่วนที่สอง จะเพิ่มข้อความระบบของคุณซึ่งเบราว์เซอร์ไม่สามารถเปลี่ยนแปลงได้ ส่วนที่สาม จะเรียกใช้เอนด์พอยต์ upstream ด้วย stream: true และส่งข้อมูลดิบกลับตรงๆ

import express from "express";

const app = express();
app.use(express.json({ limit: "256kb" }));
app.use(express.static("public"));

const UPSTREAM = "https://api.uncensoredchatbotapi.com/v1/chat/completions";
const SYSTEM = "You are Wren, a dry-witted night-shift lighthouse keeper. Stay in character.";

app.post("/api/chat", async (req, res) => {
  const history = Array.isArray(req.body.messages) ? req.body.messages : [];
  // Keep only well-formed turns and cap the history we forward.
  const turns = history
    .filter((m) => ["user", "assistant"].includes(m.role) && typeof m.content === "string")
    .slice(-30)
    .map((m) => ({ role: m.role, content: m.content.slice(0, 4000) }));

  if (turns.length === 0) return res.status(400).json({ error: "no messages" });

  const upstream = await fetch(UPSTREAM, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      model: "uncensored",
      messages: [{ role: "system", content: SYSTEM }, ...turns],
      stream: true,
      max_tokens: 500,
      temperature: 0.8,
    }),
  });

  if (!upstream.ok) {
    const info = await upstream.json().catch(() => ({}));
    return res.status(upstream.status).json(info);
  }

  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache");
  for await (const chunk of upstream.body) res.write(chunk);
  res.end();
});

app.listen(3000, () => console.log("http://localhost:3000"));

มีสองทางเลือกที่ควรอธิบาย การส่งข้อมูลดิบผ่านหมายความว่าคุณไม่จำเป็นต้องเข้าใจรูปแบบเหตุการณ์บนเซิร์ฟเวอร์เลย และการส่งสถานะ upstream กลับเมื่อเกิดข้อผิดพลาดทำให้หน้าเว็บสามารถตอบสนองได้อย่างเหมาะสม เช่น รหัส 402 หมายถึงยอดเงินของคุณหมด และรหัส 429 หมายถึงมีคนส่งคำขอเร็วเกินไป

ขั้นตอนที่ 3: หน้าเว็บและตัวอ่านสตรีม

ตอนนี้ส่วนหน้า บันทึกเป็น public/index.html; มันเปลือยเปล่าโดยเจตนาเพื่อให้คุณสามารถปรับแต่งได้ในภายหลัง

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Lantern Chat</title>
  <style>
    body { font: 16px system-ui; max-width: 640px; margin: 2rem auto; padding: 0 1rem; }
    #log { min-height: 320px; border: 1px solid #ccc; padding: 1rem; white-space: pre-wrap; }
    .me { color: #234; font-weight: 600; }
    form { display: flex; gap: .5rem; margin-top: 1rem; }
    input { flex: 1; padding: .6rem; }
  </style>
</head>
<body>
  <h1>Lantern Chat</h1>
  <div id="log"></div>
  <form id="form">
    <input id="text" autocomplete="off" placeholder="Say something...">
    <button>Send</button>
  </form>
  <script src="chat.js"></script>
</body>
</html>

ถัดไปคือ public/chat.js ส่วนที่น่าสนใจคือลูปอ่าน ข้อมูลเครือข่ายไม่เคารพขอบเขตบรรทัด ดังนั้นเราจึงเก็บ buffer แยกตามบรรทัดใหม่ และเก็บบรรทัดบางส่วนสุดท้ายไว้จนกว่าข้อมูลจะมาถึง บรรทัดที่สมบูรณ์แต่ละบรรทัดที่ขึ้นต้นด้วย data: เป็น JSON ยกเว้นเครื่องหมาย [DONE] สุดท้าย

const log = document.getElementById("log");
const form = document.getElementById("form");
const input = document.getElementById("text");
const history = [];

function addLine(cls, text) {
  const div = document.createElement("div");
  div.className = cls;
  div.textContent = text;          // textContent, never innerHTML, for untrusted text
  log.appendChild(div);
  return div;
}

form.addEventListener("submit", async (e) => {
  e.preventDefault();
  const text = input.value.trim();
  if (!text) return;
  input.value = "";
  history.push({ role: "user", content: text });
  addLine("me", "You: " + text);
  const bubble = addLine("bot", "");

  const res = await fetch("/api/chat", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ messages: history }),
  });
  if (!res.ok) {
    bubble.textContent = "(the chat is unavailable right now)";
    history.pop();
    return;
  }

  const reader = res.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "", reply = "";
  for (;;) {
    const { value, done } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split("\n");
    buffer = lines.pop();            // keep a partial line for the next read
    for (const line of lines) {
      if (!line.startsWith("data: ")) continue;
      const data = line.slice(6).trim();
      if (data === "[DONE]") continue;
      const json = JSON.parse(data);
      const piece = json.choices?.[0]?.delta?.content;
      if (piece) {
        reply += piece;
        bubble.textContent = reply;
      }
    }
  }
  history.push({ role: "assistant", content: reply });
});

สังเกตอาร์เรย์ history API ไม่มีความจำระหว่างการเรียกใช้ ดังนั้นหน้าเว็บจะส่งการสนทนาทั้งหมดใหม่ทุกครั้ง และเซิร์ฟเวอร์จะตัดให้สั้นลง เริ่มต้นแอปและเปิดในเบราว์เซอร์:

export API_KEY="paste-your-key-here"
node server.js

บันทึกเกี่ยวกับการเรนเดอร์มาร์กดาวน์

โมเดลแชทชอบเครื่องหมายดอกจัน รายการ และโค้ดบล็อกเป็นครั้งคราว ตัวอย่างของเราแสดงข้อความดิบซึ่งปลอดภัย เมื่อคุณต้องการผลลัพธ์ที่สวยงาม ให้เรนเดอร์ markdown ด้วยไลบรารี แต่ให้ปฏิบัติตามสองกฎ รัน HTML ผ่านตัวกรองเนื้อหา ก่อนแทรกด้วย innerHTML เนื่องจากโมเดลหรือผู้ใช้ที่หลอกมันสามารถปล่อยแท็กและแอตทริบิวต์ที่คุณไม่ได้ตั้งใจ และ เรนเดอร์แบบเพิ่มทีละส่วนอย่างระมัดระวัง: การวิเคราะห์ข้อความตอบกลับทั้งหมดใหม่ในทุกส่วนนั้นใช้ได้สำหรับข้อความสั้น ๆ แต่ markdown ที่ยังไม่เสร็จอาจกระพริบ ดังนั้นผู้สร้างบางรายจึงแสดงข้อความธรรมดาขณะสตรีม และสลับไปแสดงผลแบบจัดรูปแบบเมื่อสตรีมสิ้นสุด

การจัดรูปแบบ Roleplay เป็นข้อผิดพลาดเฉพาะตัว ตัวละครจำนวนมากห่อหุ้มการกระทำด้วยเครื่องหมายดอกจัน เช่น *ปรับโคมไฟ* ตัดสินใจว่าแอปของคุณจะจัดรูปแบบตัวเอียงเหล่านั้นหรือไม่ และบอกตัวละครในพรอมต์ระบบให้ปฏิบัติตามข้อตกลงใดข้อตกลงหนึ่ง คู่มือ การออกแบบบุคลิก แสดงคำพูดพรอมต์สำหรับสิ่งนั้น

ขั้นตอนที่ 4: smoke-test เส้นทาง

ก่อนโทษเบราว์เซอร์ ให้ทดสอบโปรxies โดยตรง หากเทอร์มินัลทำงาน เซิร์ฟเวอร์ก็โอเค และข้อบกพร่องที่เหลืออยู่ในหน้าเว็บ

curl -N http://localhost:3000/api/chat \
  -H "Content-Type: application/json" \
  -d '{"messages":[{"role":"user","content":"Wren, is the fog coming in?"}]}'

แฟลก -N จะปิดการบัฟเฟอร์ของ curl ทำให้คุณควรเห็นบรรทัด data: ปรากฏขึ้นทีละน้อย และลงท้ายด้วย data: [DONE] หากคุณได้รับข้อผิดพลาด JSON ให้ตรวจสอบสถานะ: 401 หมายถึงคีย์ในสภาพแวดล้อมของคุณไม่ถูกต้อง 402 หมายถึงยอดเงินหมด และ 404 หมายถึง URL upstream มีตัวพิมพ์ผิด

อาการสาเหตุที่เป็นไปได้การแก้ไข
หน้าเว็บแสดงข้อความว่าไม่สามารถใช้งานได้เซิร์ฟเวอร์ส่งสถานะที่ไม่ใช่ 200รันการทดสอบ curl และอ่านสถานะ
ข้อความปรากฏเฉพาะในตอนท้ายชั้นพร็อกซีทำการบัฟเฟอร์การตอบกลับปิดการบัฟเฟอร์สำหรับเส้นทาง
อักขระที่ผิดรูปแบบตัวถอดรหัสที่ใช้โดยไม่มี stream: trueตั้งค่าตัวเลือกบน TextDecoder.decode
การตอบตัดกลางประโยคmax_tokens ถึงขีดจำกัดเพิ่มขีดจำกัดบนเซิร์ฟเวอร์

ด้วยการแก้ไขสี่จุดนี้ คุณสามารถวินิจฉัยปัญหาส่วนใหญ่ในการรันครั้งแรกได้ภายในหนึ่งนาที เก็บคำสั่ง curl ไว้ในบันทึกของคุณ เพราะมันยังเป็นเครื่องมือตรวจสอบสุขภาพที่มีประโยชน์หลังจากที่คุณเปลี่ยน proxy ในภายหลัง

รายละเอียดเล็กๆ น้อยๆ ที่ทำให้ดูสมบูรณ์

แชทที่สตรีมมิงอยู่แล้วนั้นให้ความรู้สึกดี แต่รายละเอียดไม่กี่อย่างคือสิ่งที่แยกเดโมออกจากผลิตภัณฑ์ที่ผู้คนกลับมาใช้งานซ้ำ

  • ปิดปุ่มส่งขณะรอรับข้อความตอบกลับ การส่งซ้ำสองครั้งสร้างประวัติที่สลับซับซ้อนซึ่งทำให้ทั้งผู้ใช้และโมเดลสับสน
  • เพิ่มปุ่มหยุด สร้าง AbortController ส่งสัญญาณไปยัง fetch และเรียก abort() เมื่อคลิก บันทึกข้อความที่ได้รับจนถึงปัจจุบันเป็นการตอบกลับของผู้ช่วย
  • บันทึกบันทึกการสนทนา เก็บประวัติไว้ใน sessionStorage เพื่อให้การรีเฟรชไม่ลบการสนทนา และเสนอปุ่มล้างแชทที่ล้างข้อมูลนั้น
  • เลื่อนอัตโนมัติอย่างเหมาะสม ติดตามด้านล่างเฉพาะเมื่อผู้ใช้อยู่ใกล้แล้วเท่านั้น; มิฉะนั้นให้พวกเขาอ่านบรรทัดเก่าได้อย่างสงบสุข
  • แสดงข้อผิดพลาดอย่างสุภาพ แทนที่ฟองว่างด้วยลิงก์ลองส่งใหม่ซึ่งส่งข้อความผู้ใช้ล่าสุดอีกครั้ง

แต่ละรายการนี้ใช้โค้ด JavaScript ธรรมดาเพียงไม่กี่บรรทัด ดังนั้นจงต้านทานแรงที่จะใช้เฟรมเวิร์กจนกว่าอินเทอร์เฟซของคุณจะต้องการคอมโพเนนต์ การกำหนดเส้นทาง หรือสถานะที่แชร์จริงๆ

การทำให้แข็งแกร่งและขั้นตอนถัดไป

ตอนนี้คุณมีแชทที่ใช้งานได้ ก่อนที่ผู้ใช้จริงจะเข้ามา ให้เพิ่มมาตรการป้องกันใน server.js จำกัดจำนวนคำขอต่อ IP หรือเซสชัน เพราะคีย์หนึ่งๆ อนุญาตให้ส่งคำขอได้ 300 คำขอต่อนาที และผู้ใช้ที่กระตือรือรือคนหนึ่งอาจใช้ยอดทั้งหมดได้ จัดการข้อผิดพลาดจาก upstream อย่างชัดเจน: ข้อผิดพลาด 503 พร้อม upstream_busy ควรได้รับปุ่มลองใหม่ที่เป็นมิตร และข้อผิดพลาด 403 content_blocked ควรได้รับข้อความที่ชัดเจนแทนที่จะเป็นฟองข้อความว่างเปล่า เก็บ max_tokens ให้เหมาะสม; 500 เพียงพอสำหรับการแชท ในขณะที่ค่าสูงสุดที่อนุญาตคือ 32,000 ต่อคำขอ

จากนั้นให้พิจารณาเรื่องค่าใช้จ่าย ตัวอย่างเช่น สมมติว่าการสนทนาแต่ละรอบส่ง 1,200 โทเคนพรอมต์และรับ 250 โทเคนการเติมเต็ม นั่นคือประมาณ $0.0003 สำหรับข้อมูลนำเข้า และ $0.00025 สำหรับข้อมูลส่งออก รวมประมาณ $0.00055 ต่อการสนทนา จำนวนโทเคนเหล่านี้เป็นเพียงการสมมติ ดังนั้นควรวัดค่าจริงของคุณด้วยข้อมูล usage docs อธิบายฟิลด์นี้ และ guide to hosted uncensored LLMs อธิบายสิ่งที่ควรคาดหวังจากบริการประเภทนี้

ถาม-ตอบ

ฉันสามารถเรียก API โดยตรงจากเบราว์เซอร์ได้หรือไม่?

ในทางเทคนิคใช่ แต่คุณจะทำให้คีย์ของคุณเปิดเผยต่อผู้เยี่ยมชมทุกคน ใช้เส้นทางเซิร์ฟเวอร์เช่นในบทช่วยสอนนี้เพื่อให้คีย์อยู่ในตัวแปรสภาพแวดล้อม

เซิร์ฟเวอร์จำเป็นต้องแยกวิเคราะห์สตรีมหรือไม่?

ไม่ มันสามารถส่งต่อไบต์โดยไม่เปลี่ยนแปลง และหน้าเว็บจะอ่านบรรทัดข้อมูล แยกวิเคราะห์บนเซิร์ฟเวอร์ก็ต่อเมื่อคุณต้องการบันทึกข้อความหรือกรองเอาต์พุต

ทำไมคำตอบของฉันจึงมาถึงพร้อมกันทั้งหมด?

บางอย่างระหว่างนั้นคือการบัฟเฟอร์ ตรวจสอบว่าคำขอตั้งค่า stream เป็น true และว่า proxy ย้อนกลับหรือชั้นการบีบอัดไม่ได้กักเก็บการตอบกลับ

ฉันจะให้หน่วยความจำกับบอทได้อย่างไร?

ส่งการสนทนาในรูปแบบข้อความในทุกคำขอ โดยตัดการสนทนาที่เก่าที่สุดออกเมื่อข้อความมีจำนวนมาก หน้าต่างบริบทขนาด 100,000 โทเคนจะถูกแชร์ร่วมกับข้อความตอบกลับ

คีย์ของคุณอยู่ห่างแค่แบบฟอร์มเดียว

สร้างบัญชี คัดลอกคีย์ เปลี่ยน base URL นั่นคือการตั้งค่าทั้งหมด

รับคีย์ APIอ่านเอกสาร