Trong bài đăng này, tôi nói về cách tôi xây dựng Chatbot AI nội dung tùy chỉnh nguồn mở với Upstash, Next.js, LangChain và Fly.io. Upstash đã giúp tôi lên lịch đào tạo mô hình, đưa ra cách giới hạn tỷ lệ rộng rãi và lưu các phản hồi API OpenAI vào bộ nhớ đệm.

Những gì chúng tôi sẽ sử dụng
- Next.js (Front-end và Back-end)
- LangChain (khuôn khổ phát triển ứng dụng được hỗ trợ bởi các mô hình ngôn ngữ)
- Upstash (Mô hình đào tạo lập lịch thông qua QStash, Giới hạn tỷ lệ và phản hồi OpenAI trong bộ nhớ đệm)
- CSS Tailwind (Tạo kiểu)
- Fly.io (Triển khai)
Những gì bạn cần
- Node.js 18
- Tài khoản Upstash
- Tài khoản OpenAI (dành cho Khóa API OpenAI)
Thiết lập Upstash Redis
Khi bạn đã tạo tài khoản Upstash và đăng nhập, bạn sẽ chuyển tới tab Redis và tạo cơ sở dữ liệu.


Sau khi tạo cơ sở dữ liệu, bạn sẽ chuyển đến tab Chi tiết. Cuộn xuống cho đến khi bạn tìm thấy phần Kết nối cơ sở dữ liệu của bạn. Sao chép nội dung và lưu ở nơi an toàn.

Ngoài ra, hãy cuộn xuống cho đến khi bạn tìm thấy phần API REST và chọn nút .env. Sao chép nội dung và lưu ở nơi an toàn.

Thiết lập QStash Upstash
Sau khi đăng nhập, bạn sẽ chuyển đến tab QStash và lấy QSTASH_URL , QSTASH_TOKEN , QSTASH_CURRENT_SIGNING_KEY , và QSTASH_NEXT_SIGNING_KEY . Sao chép nội dung và lưu ở nơi an toàn.

Thiết lập dự án
Để thiết lập, chỉ cần sao chép kho ứng dụng và làm theo hướng dẫn này để tìm hiểu mọi thứ có trong đó. Để phân nhánh dự án, hãy chạy:
git clone https://github.com/rishi-raj-jain/custom-content-ai-chatbot
cd custom-content-ai-chatbot
npm install Khi bạn đã sao chép kho lưu trữ, bạn sẽ tạo một tệp .env. Bạn sẽ thêm các mục chúng tôi đã lưu từ các phần trên.
Nó sẽ trông giống như thế này:
# .env
# Obtained from the steps as above
# Upstash Redis Secrets
UPSTASH_REDIS_REST_URL="https://....upstash.io"
UPSTASH_REDIS_REST_TOKEN="..."
# Upstash QStash Secrets
QSTASH_URL="https://qstash.upstash.io/v1/publish/"
QSTASH_TOKEN="..."
QSTASH_CURRENT_SIGNING_KEY="sig_..."
QSTASH_NEXT_SIGNING_KEY="sig_..."
# OpenAI Key
OPENAI_API_KEY="sk-..."
# Admin Access Key
# Used to verify a training request as to be done only by an admin
ADMIN_KEY="..." Sau các bước này, bạn sẽ có thể khởi động môi trường cục bộ bằng lệnh sau:
npm run dev Cấu trúc kho lưu trữ
Đây là cấu trúc thư mục chính của dự án. Tôi đã đánh dấu màu đỏ các tệp sẽ được thảo luận thêm trong bài đăng này liên quan đến việc quản lý kho vectơ, tạo các Tuyến API để trò chuyện với AI được đào tạo về nội dung tùy chỉnh của bạn (với bộ nhớ đệm phản hồi) và lập lịch trình đào tạo mô hình.

Luồng dữ liệu và hoạt động cấp cao
Đây là sơ đồ cấp cao về cách dữ liệu được truyền đi và các hoạt động diễn ra 👇🏻

- Khi người dùng đặt câu hỏi thông qua chatbot, IP của người dùng sẽ được kiểm tra theo giới hạn tốc độ và phản hồi, nếu không được lưu vào bộ nhớ đệm qua Upstash Redis, sẽ được tìm kiếm từ API OpenAI (sau đó được lưu vào bộ nhớ đệm) và truyền trực tuyến đến người dùng
- Khi quản trị viên yêu cầu đào tạo mô hình hiện có trên một nhóm URL nhất định, với sự trợ giúp của QStash của Upstash, yêu cầu POST sẽ được thực hiện trong serverless sau một khoảng thời gian trễ nhất định để tìm nạp nội dung trong các URL đã cho và cập nhật mô hình (ở chế độ nền)
Thiết lập các tuyến API trò chuyện và đào tạo trong Next.js
Trong phần này, chúng tôi nói về cách thiết lập tuyến đường:pages/api/chat.js để kích hoạt các Yêu cầu có nguồn gốc chéo, Giới hạn tỷ lệ các lệnh gọi API trò chuyện, Phản hồi bộ đệm và luồng cho người dùng, đồng thời đưa ra phương pháp lên lịch đào tạo nội dung trên các URL cụ thể và pages/api/train.js chỉ thực hiện đào tạo trên các URL nhất định nhưng ở chế độ nền.
1. Kích hoạt CORS
Sử dụng cors gói, chúng tôi đã kích hoạt CORS trong ứng dụng để sử dụng chatbot ở nhiều nơi, chẳng hạn như bot trên trang web của bạn. Ngay sau khi Tuyến API được khởi tạo, chúng tôi sẽ chạy thiết lập cors như bên dưới 👇🏻
// File: pages/api/chat.js
// Reference Function to cors
import { runMiddleware } from '@/lib/cors'
export default async function (req, res) {
try {
// Run the middleware
await runMiddleware(req, res)
// ...
catch (e) {
console.log(e.message || e.toString())
}
return res.end()
}
// Cors Function
// File: lib/cors.js
import Cors from 'cors'
// Initializing the cors middleware
// You can read more about the available options here: https://github.com/expressjs/cors#configuration-options
const cors = Cors({
methods: ['POST', 'OPTIONS', 'HEAD'],
})
// Helper method to wait for a middleware to execute before continuing
// And to throw an error when an error happens in a middleware
export function runMiddleware(req, res, fn = cors) {
return new Promise((resolve, reject) => {
fn(req, res, (result) => {
if (result instanceof Error) return reject(result)
return resolve(result)
})
})
} 2. Lên lịch (các) Yêu cầu đào tạo nội dung trên các URL nhất định
Với Upstash QStash, người ta có thể tạo các API giống như lửa và quên. Bạn không cần phải chủ động đợi chức năng chính hoàn thành mới nhận được phản hồi mà nên thực hiện ở chế độ nền (tùy chọn, sau một số độ trễ nhất định). Nó giống như một cron-job nhưng chạy theo từng yêu cầu và không thường xuyên theo lịch trình.
Trong cùng một tuyến đường api trò chuyện, chúng tôi chấp nhận yêu cầu có admin-key tiêu đề và nếu nó khớp với bí mật phía máy chủ (ADMIN_KEY ), chúng tôi lên lịch đào tạo nội dung về tập hợp các URL được chuyển trong nội dung yêu cầu sau một thời gian trì hoãn (ở đây 10s ). Yêu cầu đào tạo nội dung sau thời gian trễ đã đặt sẽ được gửi đến một điểm cuối nhất định (tại đây:https://custom-content-ai-chatbot.fly.dev/api/train )
// File: pages/api/chat.js
// If the headers contain an `admin-key` header
if (req.headers['admin-key'] === process.env.ADMIN_KEY) {
// If `urls` is not in body, return with `Bad Request`
if (!req.body.urls) return res.status(400).send('No urls to train on.')
// Hit QStash API to train on this set of URLs after 10 seconds from now
await qstashClient.publishJSON({
delay: 10,
body: { urls: req.body.urls },
url: 'https://custom-content-ai-chatbot.fly.dev/api/train'
})
return res.status(200).end()
}
Bây giờ, hãy cùng tìm hiểu những gì có trong lộ trình API train (pages/api/train.js ) 👇🏻
// File: pages/api/train.js
import train from '@/lib/train'
import * as dotenv from 'dotenv'
import { redis } from '@/lib/redis'
import { runMiddleware } from '@/lib/cors'
import { verifySignature } from '@upstash/qstash/nextjs'
dotenv.config()
// Disabling converting request body to JSON directly
// More on https://nextjs.org/docs/pages/building-your-application/routing/api-routes#custom-config
export const config = {
api: {
bodyParser: false,
},
}
async function handler(req, res) {
try {
// Run the middleware
await runMiddleware(req, res)
// If method is not POST, return with `Forbidden Access`
if (req.method !== 'POST') return res.status(403).send('No other methods allowed.')
// If `urls` is not in body, return with `Bad Request`
if (!req.body.urls) return res.status(400).send('No urls to train on.')
// Train on the particular URLs
await train(req.body.urls)
// Once saved, clear all the responses in Upstash
let allKeys = await redis.keys('*')
if (allKeys) {
// Filter out the keys to not have the ratelimiter ones
allKeys = allKeys.filter((i) => !i.includes('@upstash/ratelimit:'))
const p = redis.pipeline()
// Create a pipeline to clear out all the keys
allKeys.forEach((i) => p.del(i))
// Execute the pipeline commands in a transaction
await p.exec()
console.log('Cleaned cached responses in Upstash.')
}
return res.status(200).end()
} catch (e) {
console.log(e.message || e.toString())
}
return res.end()
}
// Verify the incoming request to be a valid
// QStash Scheduled POST request with Upstash-Signature
export default verifySignature(handler) Trong đoạn mã trên, chúng tôi đang thực hiện ba hành động quan trọng:
- Thực hiện xác minh yêu cầu đến bằng
verifySignaturecủa QStash phương pháp. Phần bên dưới này tìm kiếmUpstash-Signaturetiêu đề và xác minh nó bằng phần thân thô nhận được. - Gọi tới
trainchức năng tìm nạp nội dung URL và thêm vào kho vectơ hiện có (và lưu nó). - Xóa các phản hồi được lưu trong bộ nhớ đệm trong Upstash Redis sau khi lọc ra các khóa liên quan đến việc triển khai giới hạn tốc độ thông qua Giao dịch Redis.
3. Giới hạn tỷ lệ
Để triển khai giới hạn tốc độ, chúng tôi sử dụng ứng dụng khách cơ sở dữ liệu Upstash Redis và thư viện giới hạn tốc độ có tên @upstash/ratelimit .
// File: lib/redis.js
// Reference Function to ratelimiting
import * as dotenv from 'dotenv'
import { Redis } from '@upstash/redis'
import { Ratelimit } from '@upstash/ratelimit'
// Load environment variables
dotenv.config()
// Initialize Upstash Redis
export const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL,
token: process.env.UPSTASH_REDIS_REST_TOKEN,
})
// Initialize Upstash Rate Limiter
export const ratelimit = {
chat: new Ratelimit({
redis,
// Limit requests to 30 questions per day per IP Address
limiter: Ratelimit.slidingWindow(30, '86400s'),
}),
}
Bằng cách sử dụng tính năng Giới hạn tỷ lệ, tôi có thể sử dụng dịch vụ - hoàn toàn miễn phí và công khai! Điều này cho phép tôi giới thiệu những lợi ích của hệ thống, tức là phản hồi trò chuyện. Theo nghĩa đen, bất kỳ ai cũng có thể hỏi 30 câu hỏi mỗi ngày thông qua trang web. Chúng tôi có thể thực thi giới hạn tỷ lệ 30 câu hỏi mỗi ngày dựa trên IP address làm chìa khóa.
// File: pages/api/chat.js
import requestIp from 'request-ip'
import { ratelimit } from '@/lib/redis'
// ...
// Get the client IP
const detectedIp = requestIp.getClientIp(req)
// If no IP detected, return with a `Bad Request`
if (!detectedIp) return res.status(400).send('Bad request.')
// Check the Rate Limit
const result = await ratelimit.chat.limit(detectedIp)
// If rate limited, return with the same
if (!result.success) return res.status(400).send('Rate limit exceeded.')
// Continue with serving the chat responses 4. Tải kho lưu trữ vectơ được lập chỉ mục đã lưu và yêu cầu OpenAI phản hồi
Sau khi hoàn tất tất cả các bước kiểm tra, chúng tôi hiện đang bắt đầu công việc chính - gọi API OpenAI bằng nội dung tùy chỉnh của chúng tôi và gửi phản hồi cho người dùng. Để đơn giản hóa mọi thứ, chúng tôi sẽ chia phần này thành các phần khác:
- 3.1:Truy xuất kho vectơ đã lưu
// File: pages/api/chat.js
// Reference Function to loadVectorStore
import { loadVectorStore } from '@/lib/vectorStore'
// Load the trained model
const vectorStore = await loadVectorStore()
// ...
// Vectore Store Function
// File: lib/vectorStore.js
import { join } from 'path'
import { existsSync } from 'fs'
import { Document } from 'langchain/document'
import { FaissStore } from 'langchain/vectorstores/faiss'
import { OpenAIEmbeddings } from 'langchain/embeddings/openai'
export async function loadVectorStore() {
const directory = join(process.cwd(), 'loadedVectorStore')
const docStoreJSON = join(process.cwd(), 'loadedVectorStore', 'docstore.json')
if (existsSync(docStoreJSON)) {
// If the directory is found, load the vector store saved by Faiss integration
return await FaissStore.load(directory, new OpenAIEmbeddings())
} else {
// If no content is there, load the vector store with just `Hey` for starters
return await FaissStore.fromDocuments([new Document({ pageContent: 'Hey' })], new OpenAIEmbeddings())
}
} - 3.2:Thêm nguyên tắc nhắc nhở vào truy vấn của người dùng
Bằng cách sử dụng NhắcTemplate của LangChain, với truy vấn của người dùng, chúng tôi chuyển hướng dẫn về cách thực hiện và AI sẽ trả lời câu hỏi theo cách nào:
// File: pages/api/chat.js
import { z } from 'zod'
import { PromptTemplate } from 'langchain/prompts'
import { RetrievalQAChain } from 'langchain/chains'
import { OutputFixingParser, StructuredOutputParser } from 'langchain/output_parsers'
// Load the trained model
// ...
// Create a prompt specifying for OpenAI what to write
const outputParser = StructuredOutputParser.fromZodSchema(
z.object({
answer: z.string().describe('answer to question in HTML friendly format, use all of the tags wherever possible and including reference links'),
}),
)
// ...
// Create an instance of output parser class to help refine the response of OpenAI
const outputFixingParser = OutputFixingParser.fromLLM(model, outputParser)
// Create a prompt specifying for OpenAI how to process on the input
const prompt = new PromptTemplate({
template: `Answer the user's question as best and be as detailed as possible:\n{format_instructions}\n{query}`,
inputVariables: ['query'],
partialVariables: {
format_instructions: outputFixingParser.getFormatInstructions(),
},
})
// Pass the prompt to the query with the model to OpenAI API
const chain = RetrievalQAChain.fromLLM(model, vectorStore.asRetriever(), prompt) - 3.3:Phản hồi truyền trực tuyến và bộ nhớ đệm
Để lưu vào bộ nhớ đệm các phản hồi bằng Upstash Redis, chúng tôi sẽ sử dụng UpstashRedisCache thư viện bộ đệm của LangChain. Chúng tôi chuyển phiên bản Redis hiện có dưới dạng máy khách và chuyển trình xử lý bộ nhớ đệm tới ChatOpenAI trình bao bọc để sử dụng nó vào bộ đệm sau khi phản hồi được gửi:
// File: pages/api/chat.js
import { redis } from '@/lib/redis'
import { ChatOpenAI } from 'langchain/chat_models/openai'
import { UpstashRedisCache } from 'langchain/cache/upstash_redis'
// Load the trained model
// ...
// Create Upstash caching
const upstashRedisCache = new UpstashRedisCache({ client: redis })
// A flag to detect if response was not cached
let doesToken = false
const model = new ChatOpenAI({
// Enable streaming to return responses to user as quickly possible
streaming: true,
// Cache responses using Upstash Redis cache client
cache: upstashRedisCache,
callbacks: [
{
handleLLMNewToken(token) {
// Set the flag to true if we receive stream from OpenAI
doesToken = true
// Stream the token to the user
res.write(token)
},
},
],
})
// Create a LLM QA Chain
// ...
// Store the output to refer to in case cached
const chainOutput = await chain.call({ query: req.body.input })
// If no tokens received implies that the content is cached
// Return the cached response as is
if (!doesToken) return res.status(200).send(chainOutput.text) Đó là rất nhiều học hỏi! Bây giờ bạn đã hoàn tất.
Triển khai lên Fly.io
Kho lưu trữ đi kèm với thiết lập sẵn có cho Fly.io, đặc biệt liên quan đến:
- Tệp Docker
- fly.toml
- .dockerignore
Việc triển khai cần có tài khoản trên Fly.io. Sau khi có tài khoản, bạn có thể tạo ứng dụng trong Fly.io bằng cách chạy lệnh sau trong thư mục gốc của dự án:
# Create an app based on the baked-in configuration in your account
# This will result only in the change of app name in existing fly.toml
fly launch và triển khai qua 👇🏻
# Deploy the app based on the configuration created above
fly deploy Bây giờ chúng ta đã hoàn tất việc triển khai! Vâng, chỉ vậy thôi.
Kết luận
Tóm lại, dự án này đã cung cấp kinh nghiệm quý báu trong việc triển khai bộ nhớ đệm phản hồi OpenAI, giới hạn tốc độ và các yêu cầu API được lên lịch để đào tạo mô hình, đồng thời sử dụng một dịch vụ có thể điều chỉnh theo nhu cầu của bạn, tức là Upstash.
Next.js , Redis , TailwindCSS , LangChain , Serverless Scheduling