Những gì chúng tôi đang xây dựng
Hai bot nhỏ, một bộ não chia sẻ. Một tệp có tên llm.py chịu trách nhiệm mọi thứ liên quan đến API: nhân vật, trình giới hạn tốc độ, logic thử lại. Bot Discord và bot Telegram sau đó chỉ làm nhiệm vụ chuyển đổi sự kiện nền tảng thành danh sách tin nhắn và ngược lại. Thêm nền tảng thứ ba sau này và bạn chỉ cần viết trình kết nối.
Cài đặt các phụ thuộc với pip install httpx discord.py python-telegram-bot. Xuất API_KEY từ trang tài khoản của bạn, cùng với DISCORD_TOKEN và/hoặc TELEGRAM_TOKEN từ thiết lập bot của từng nền tảng. Cả hai bot bên dưới đều giả định Python 3.10 trở lên.
Định dạng yêu cầu là dạng chat-completions thuần của OpenAI, được trình bày trong hướng dẫn nhanh đa ngôn ngữ. Không có gì ở đây cần đến SDK.
Lõi chia sẻ: nhân vật, bộ giới hạn và thử lại
Đặt phần nguy hiểm vào một nơi. Mô-đun bên dưới duy trì cửa sổ trượt của dấu thời gian yêu cầu và chờ đợi khi nó gần đạt 240 lệnh gọi mỗi phút, một biên độ cố định dưới mức 300 yêu cầu mỗi phút cho phép cho mỗi khóa. Một semaphore giới hạn độ đồng thời ở mức bốn để một kênh bận rộn không thể kích hoạt hai mươi yêu cầu cùng một lúc.
# llm.py - shared by both bots
import asyncio
import os
import time
from collections import deque
import httpx
URL = "https://api.unrestrictedaiapi.com/v1/chat/completions"
PERSONA = "You are Pip, a sardonic bar-room storyteller. Keep replies under 150 words."
_stamps = deque() # timestamps of recent calls
_gate = asyncio.Semaphore(4) # at most 4 requests in flight
_client = httpx.AsyncClient(timeout=60.0)
async def _wait_for_slot(limit=240, window=60.0):
# Stay below the 300 requests/minute per-key limit with headroom.
while True:
now = time.monotonic()
while _stamps and now - _stamps[0] > window:
_stamps.popleft()
if len(_stamps) < limit:
_stamps.append(now)
return
await asyncio.sleep(window - (now - _stamps[0]) + 0.05)
async def chat(history):
"""history: list of {"role","content"} dicts, oldest first."""
messages = [{"role": "system", "content": PERSONA}] + list(history)
async with _gate:
for attempt in range(3):
await _wait_for_slot()
r = await _client.post(
URL,
headers={"Authorization": f"Bearer {os.environ['API_KEY']}"},
json={"model": "uncensored", "messages": messages, "max_tokens": 400},
)
if r.status_code in (429, 503):
await asyncio.sleep(2 ** attempt)
continue
if r.status_code == 403:
return "I can't continue with that."
r.raise_for_status()
return r.json()["choices"][0]["message"]["content"]
return "The service is busy. Try again in a moment."Xem cách xử lý trạng thái. Mã 429 hoặc 503 sẽ chờ 1, 2 rồi 4 giây và thử lại. Mã 403 có nghĩa là bộ lọc nội dung đã kích hoạt, vì vậy bot sẽ trả lời bằng một dòng trung tính thay vì thử lại. Bất kỳ lỗi nào khác sẽ được ném ra, thư viện nền tảng của bạn sẽ ghi nhật ký lỗi đó. Nếu bạn thấy mã 402, số dư của bạn đã hết hoặc thời gian dùng thử đã kết thúc; hãy nạp tiền vào số dư trả trước.
Bộ điều hợp Discord
Bot Discord cần bật ý định message-content trong cổng dành cho nhà phát triển cũng như trong mã, nếu không msg.content sẽ đến trống rỗng. Bot chỉ trả lời khi được đề cập, điều này giữ cho bot không đọc mọi tin nhắn trong một máy chủ bận rộn.
# discord_bot.py
import os
import time
from collections import defaultdict, deque
import discord
from llm import chat
intents = discord.Intents.default()
intents.message_content = True # also enable it in the developer portal
bot = discord.Client(intents=intents)
HISTORY_TURNS = 10 # user+assistant messages kept per channel
history = defaultdict(lambda: deque(maxlen=HISTORY_TURNS))
last_use = {} # user id -> last request time
COOLDOWN = 5.0 # seconds between requests per user
def chunks(text, size=1900):
return [text[i:i + size] for i in range(0, len(text), size)] or [""]
@bot.event
async def on_message(msg: discord.Message):
if msg.author.bot or bot.user not in msg.mentions:
return
# Age gate: guild text channels must be flagged NSFW. No DMs.
if not isinstance(msg.channel, discord.TextChannel) or not msg.channel.is_nsfw():
await msg.reply("I only chat in channels marked age-restricted (NSFW).")
return
now = time.monotonic()
if now - last_use.get(msg.author.id, 0) < COOLDOWN:
await msg.add_reaction("\u23f3")
return
last_use[msg.author.id] = now
text = msg.clean_content.replace(f"@{bot.user.display_name}", "").strip()
if not text:
return
h = history[msg.channel.id]
h.append({"role": "user", "content": f"{msg.author.display_name}: {text}"})
async with msg.channel.typing():
answer = await chat(h)
h.append({"role": "assistant", "content": answer})
for part in chunks(answer):
await msg.channel.send(part)
bot.run(os.environ["DISCORD_TOKEN"])Lịch sử là một deque với maxlen=10 được đánh dấu theo id kênh, vì vậy mỗi kênh là một cuộc trò chuyện riêng và các lượt tin nhắn cũ sẽ tự động bị loại bỏ. Tiền tố mỗi dòng người dùng với tên hiển thị của người nói cho phép mô hình phân biệt mọi người trong một phòng chia sẻ. Mười tin nhắn là mặc định bảo thủ; cửa sổ ngữ cảnh là 100.000 token, vì vậy bạn có thể tăng nó lên rất nhiều trước khi nó trở nên quan trọng.
Bộ điều hợp Telegram
python-telegram-bot v20 trở lên là bất đồng bộ, phù hợp tự nhiên với lõi chia sẻ. chat_data là một từ điển theo cuộc trò chuyện mà thư viện duy trì cho bạn, vì vậy lịch sử và trạng thái thời gian chờ không cần mã lưu trữ bổ sung.
# telegram_bot.py (python-telegram-bot v20+)
import os
import time
from collections import deque
from telegram import Update
from telegram.ext import (ApplicationBuilder, CommandHandler, ContextTypes,
MessageHandler, filters)
from llm import chat
async def adult(update: Update, context: ContextTypes.DEFAULT_TYPE):
context.chat_data["adult"] = True
await update.message.reply_text("Noted. Say something.")
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text(
"This bot produces adult fiction. Send /adult only if you are 18 or older."
)
async def talk(update: Update, context: ContextTypes.DEFAULT_TYPE):
cd = context.chat_data
if not cd.get("adult"):
await update.message.reply_text("Send /adult to confirm you are 18+.")
return
if time.monotonic() - cd.get("last", 0) < 4:
return # per-chat cooldown
cd["last"] = time.monotonic()
h = cd.setdefault("history", deque(maxlen=10))
h.append({"role": "user", "content": update.message.text})
await context.bot.send_chat_action(update.effective_chat.id, "typing")
answer = await chat(h)
h.append({"role": "assistant", "content": answer})
for i in range(0, len(answer), 4000): # Telegram limit is 4096 chars
await update.message.reply_text(answer[i:i + 4000])
app = ApplicationBuilder().token(os.environ["TELEGRAM_TOKEN"]).build()
app.add_handler(CommandHandler("start", start))
app.add_handler(CommandHandler("adult", adult))
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, talk))
app.run_polling()run_polling() là cách đơn giản nhất để bắt đầu và không cần URL công khai. Hãy nhớ rằng chat_data tồn tại trong bộ nhớ, vì vậy một lần khởi động lại sẽ xóa nó. Nếu bạn cần lịch sử tồn tại sau khi khởi động lại, hãy lưu nội dung deque của riêng bạn vào tệp hoặc cơ sở dữ liệu.
Kiểm soát độ tuổi và quy tắc kênh NSFW
API này chỉ dành cho người trưởng thành, và mọi thứ bạn xây dựng trên đó đều thừa hưởng trách nhiệm đó. Hãy xem kiểm soát độ tuổi là một tính năng, không phải suy nghĩ sau.
- Discord: chỉ trả lời trong các kênh guild được đánh dấu là giới hạn độ tuổi. Kiểm tra
channel.is_nsfw()không tốn gì và bot từ chối ở những nơi khác, bao gồm cả tin nhắn trực tiếp. - Telegram: yêu cầu xác nhận rõ ràng
/adultcho mỗi cuộc trò chuyện trước khi tạo nội dung, và nêu rõ quy tắc trong văn bản chào mừng. - Cả hai: không cung cấp bot trong các không gian hướng đến giới trẻ và thu hồi quyền truy cập ngay lập tức nếu bạn biết người dùng dưới 18 tuổi.
Lệnh xác nhận là một cổng kiểm soát, không phải bằng chứng về độ tuổi. Nội dung khiêu dâm liên quan đến trẻ vị thành niên luôn bị API chặn bằng mã 403, bao gồm cả tiểu thuyết và nhập vai, và bot của bạn không bao giờ nên cố gắng vượt qua phản hồi đó.
Chạy và kiểm tra mà không cần đám đông
Khởi động mỗi bot trong một terminal riêng với python discord_bot.py hoặc python telegram_bot.py. Đối với lần chạy đầu tiên, hãy tạo máy chủ kiểm tra riêng hoặc một cuộc trò chuyện chỉ có bạn. Đi qua một danh sách kiểm tra ngắn và bạn sẽ bắt được hầu hết các lỗi cơ bản.
- Đề cập đến bot trong một kênh không phải NSFW và xác nhận rằng nó từ chối. Điều này chứng minh cổng hoạt động.
- Đề cập đến nó trong một kênh được đánh dấu và gửi hai tin nhắn nhanh chóng. Tin nhắn thứ hai nên nhận được phản ứng đồng hồ cát, điều này chứng minh thời gian chờ hoạt động.
- Hãy giữ một cuộc trò chuyện bốn tin nhắn, sau đó tham khảo lại tin nhắn đầu tiên. Nếu bot ghi nhớ, lịch sử đã được kết nối đúng cách.
- Tạm thời đặt khóa API thành một giá trị sai và xác nhận lỗi xuất hiện trong nhật ký của bạn thay vì biến mất một cách im lặng.
- Dán một khối văn bản dài và xem phản hồi được chia thành nhiều tin nhắn dưới giới hạn độ dài của nền tảng.
Nếu các phản hồi có vẻ chung chung, nhân vật thường là vấn đề chứ không phải mã. Thay đổi một câu của PERSONA, khởi động lại và so sánh. Vì nhân vật sống trong mô-đun chia sẻ, một lần chỉnh sửa sẽ thay đổi cả hai bot cùng lúc, đó chính là lý do tại sao cấu trúc này đáng giá hơn một tệp bổ sung.
Khi bạn sẵn sàng triển khai, bất kỳ trình quản lý tiến trình luôn chạy nào cũng được. Các bot giữ kết nối dài hạn với nền tảng của chúng và thực hiện các lệnh gọi HTTPS ngắn đến API, vì vậy chúng không cần cổng vào và hầu như không tốn bộ nhớ. Thêm chính sách khởi động lại để một lần sập không khiến cộng đồng của bạn nói chuyện với một bot im lặng.
Giới hạn tốc độ, kích thước lịch sử và chi phí
Ba nút điều khiển cách bot hoạt động dưới tải. Thời gian chờ theo người dùng ngăn một người chiếm độc quyền khóa. Cửa sổ toàn bộ bảo vệ trần 300 yêu cầu mỗi phút. Semaphore làm mượt các đợt bùng nổ. Nếu bạn chạy nhiều tiến trình bot trên một khóa, chúng chia sẻ trần đó, vì vậy hãy giảm limit của mỗi tiến trình tương ứng.
Ước tính chi tiêu trước khi mời đám đông. Giả định: mỗi lệnh gọi gửi khoảng 1.200 token đầu vào (nhân vật cộng mười lượt) và trả về 250 token. Một lệnh gọi tốn 1.200 x $0,25 / 1.000.000 + 250 x $1,00 / 1.000.000 = $0,0003 + $0,00025 = $0,00055. Máy chủ tạo ra 2.000 phản hồi mỗi ngày sẽ chi khoảng $1,10 mỗi ngày theo những giả định đó. Số liệu của bạn sẽ khác, vì vậy hãy ghi nhật ký trường usage trong một tuần và tính toán lại.
Để tinh chỉnh chính nhân vật, hãy đọc hướng dẫn prompt. Giá được liệt kê trên trang giá cả.
Tăng cường độ cứng trước khi chia sẻ bot
Giữ token ra khỏi kiểm soát nguồn; đọc chúng từ môi trường như đã hiển thị. Giới hạn độ dài tin nhắn trước khi gửi nó đến API để một bản dán khổng lồ không thể đốt cháy số dư của bạn. Ghi nhật ký lỗi nhưng không ghi văn bản tin nhắn, vì người dùng của bạn sẽ mong đợi hợp lý rằng các cuộc trò chuyện của họ sẽ được giữ bí mật. Thêm lệnh /reset xóa deque của kênh, đây là cách sửa chữa nhanh nhất khi một cuộc trò chuyện đi chệch hướng. Cuối cùng, đặt một ghi chú rõ ràng trong hồ sơ bot nói rằng nó viết tiểu thuyết người lớn và các phản hồi của nó được tạo ra.
Một lựa chọn thiết kế khác xứng đáng một câu: các bot chờ phản hồi đầy đủ thay vì truyền phát nó. Chỉnh sửa tin nhắn trò chuyện từng token chạy thẳng vào giới hạn chỉnh sửa nền tảng và đối với các phản ứng được giới hạn gần 150 từ, thời gian chờ là ngắn. Nếu sau này bạn muốn gõ trực tiếp trong giao diện web của riêng mình, hãy bật stream: true ở đó, nơi bạn kiểm soát phương tiện truyền tải. Cho đến lúc đó, chỉ báo đang nhập bạn đã gửi là cách rẻ nhất để cho bot hoạt động và nó giữ cho mã ngắn đủ để đọc trong một lần và kiểm toán trước khi mời bất cứ ai vào.