Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Redis

Tăng hiệu suất:Bộ nhớ đệm LLM theo yêu cầu với LangChain, Redis và QStash

Trong một số ứng dụng, việc truy vấn LLM khi người dùng yêu cầu có thể không khả thi. Quá trình gửi lời nhắc tới API và chờ phản hồi của nó có thể đặc biệt tốn thời gian. Đối với các tác vụ phức tạp hơn liên quan đến LangChain, như xử lý dữ liệu từ tệp PDF hoặc âm thanh và cung cấp dữ liệu đó cho LLM, độ trễ thậm chí còn ảnh hưởng lớn hơn đến trải nghiệm người dùng.

Trong khi phát trực tuyến một giải pháp phù hợp trong nhiều trường hợp, có thể thuận tiện hơn khi thực hiện mọi thứ tại một số điểm tùy ý trước khi người dùng truy cập ứng dụng của bạn. Bằng cách này, người dùng có thể nhận được phản hồi được lưu trong bộ nhớ đệm gần như ngay lập tức mà không cần phải đợi LLM tạo ra phản hồi đó. Điều này đặc biệt hữu ích cho các ứng dụng sử dụng cùng một thông tin đầu vào cho mọi người dùng, chẳng hạn như trình tóm tắt sách hoặc tin tức. Nhưng nếu không có thông tin đầu vào từ người dùng, làm cách nào chúng tôi có thể khiến LangChain lưu vào bộ nhớ đệm phản hồi?

Chúng ta sẽ cần một vi dịch vụ mà chúng ta có thể gọi theo yêu cầu, có khả năng gửi lời nhắc tới LangChain và lưu phản hồi vào bộ nhớ đệm bằng Upstash Redis. Chúng tôi cũng sẽ sử dụng SDK giới hạn tốc độ của Upstash để đảm bảo rằng chúng tôi không vô tình vượt quá giới hạn tốc độ của mình. QStash là cách linh hoạt nhất để gọi microservice của chúng tôi—chúng tôi có thể thiết lập nó để phản hồi vào bộ đệm bất cứ khi nào chúng tôi muốn, thậm chí chuyển một công việc định kỳ nếu ứng dụng của chúng tôi yêu cầu. Nó cũng cung cấp một bảng thông tin mà chúng ta có thể sử dụng để theo dõi việc sử dụng microservice của mình. Trong trường hợp điểm cuối của chúng tôi bị quá tải hoặc có sự cố khác, QStash sẽ thử lại yêu cầu HTTP và đảm bảo rằng tin nhắn của chúng tôi được gửi.

Dịch vụ vi mô của chúng tôi sẽ được xây dựng bằng Hono.js, một khung web nhẹ và nhanh dành cho biên. Trong bản demo này, chúng tôi sẽ sử dụng Cloudflare Worker để lưu trữ vi dịch vụ của mình. Tuy nhiên, nhờ có Upstash, nó có thể được triển khai hầu như ở mọi nơi, kể cả các thời gian chạy biên/không có máy chủ khác.

Bạn có thể tìm thấy mã nguồn đầy đủ cho bản demo này tại đây.

Điều kiện tiên quyết

  • Cơ sở dữ liệu Upstash Redis
  • Biến môi trường QStash
  • Khóa API OpenAI

Bắt đầu

Tạo dự án

Không giống như nhiều create-<package> khác gói npm, create-hono yêu cầu bạn phải ở trong một thư mục trống. Đầu tiên, tạo một thư mục trống mới cho dự án của bạn và điều hướng đến nó:

mkdir langchain-qstash
cd langchain-qstash

Với bản phát hành v1.0.0 gần đây, Bun sẽ được sử dụng để xây dựng dự án này. Tuy nhiên, cũng có thể sử dụng create-hono với npm , pnpm , và yarn . Đảm bảo chọn cloudflare-workers khi được nhắc về mẫu. Bạn có thể thấy bun dưới dạng một tùy chọn riêng biệt nhưng đây không phải là mẫu được sử dụng trong bản demo.

bun create hono@latest

Cài đặt phụ thuộc

Bây giờ, bạn có thể chạy lệnh sau với trình quản lý gói mà bạn chọn để cài đặt các phần phụ thuộc còn lại:

bun install @upstash/qstash @upstash/ratelimit @upstash/redis langchain openai

Định cấu hình dự án

Tính đến thời điểm viết bài, create-hono bao gồm các tệp khóa trong .gitignore của nó theo mặc định. Mặc dù không bắt buộc nghiêm ngặt nhưng bạn có thể cập nhật .gitignore của mình để loại trừ các lockfiles như sau:

node_modules
dist
.wrangler
.dev.vars
wrangler.toml

Bây giờ, chúng ta có thể đặt các biến môi trường từ các điều kiện tiên quyết. Chúng có thể được thêm vào wrangler.toml của bạn , vốn đã được loại trừ khỏi kiểm soát nguồn.

[vars]
QSTASH_CURRENT_SIGNING_KEY="sig_********"
QSTASH_NEXT_SIGNING_KEY="sig_********"
UPSTASH_REDIS_REST_URL="https://********.upstash.io"
UPSTASH_REDIS_REST_TOKEN="********"
OPENAI_API_KEY="sk-********"

Cuối cùng, sửa đổi src/index.ts của bạn để thêm kiểu gõ cho các biến môi trường từ trước đó:

type Bindings = {
 QSTASH_CURRENT_SIGNING_KEY: string;
 QSTASH_NEXT_SIGNING_KEY: string;
 UPSTASH_REDIS_REST_URL: string;
 UPSTASH_REDIS_REST_TOKEN: string;
 OPENAI_API_KEY: string;
};
 
const app = new Hono<{ Bindings: Bindings }>();

Lợi ích của việc có hai khóa ký đến từ việc bạn có thể cuộn chúng một lần mà không cần phải cập nhật các biến môi trường, vì QStash sẽ tự động thử và sử dụng khóa ký tiếp theo nếu khóa hiện tại bị lỗi.

Phát triển

Ở giai đoạn này, wrangler sẽ có thể đọc các biến môi trường và triển khai dự án của bạn. Để làm như vậy, bạn có thể chạy lệnh sau bằng trình quản lý gói ưa thích của mình:

bun run dev

wrangler sẽ khởi động máy chủ Hono.js của bạn và cung cấp cho bạn URL cục bộ trên cổng 8787 để kiểm tra dự án của bạn. Nếu bạn muốn trực tiếp bắt đầu phiên xem trước cạnh thay vì thử nghiệm cục bộ, hãy sửa đổi dev package.json của bạn kịch bản như sau:

"dev": "wrangler dev src/index.ts --remote",

Để xem cấu hình và nhật ký của Cloudflare Worker, trước tiên bạn phải triển khai nhân viên của mình:

bun run deploy

Điều này sẽ hướng dẫn bạn các bước đăng nhập vào Cloudflare, tự động triển khai dự án của bạn lần đầu tiên sau khi xác thực.

Tạo phần mềm trung gian

Vì mục đích gỡ lỗi, việc ghi lại các yêu cầu chúng tôi nhận được từ QStash sẽ rất hữu ích. Chúng sẽ hiển thị trong bảng điều khiển dành cho Cloudflare Worker của chúng tôi trong tab "Nhật ký" sau khi bạn nhấn Begin log stream . Hono.js cung cấp phần mềm trung gian ghi nhật ký mà chúng tôi có thể thêm vào bộ định tuyến của mình:

import { Hono } from "hono";
import { logger } from "hono/logger";
 
// snip
 
const app = new Hono<{ Bindings: Bindings }>();
app.use("*", logger());
 
// snip

Tăng hiệu suất:Bộ nhớ đệm LLM theo yêu cầu với LangChain, Redis và QStash

Kết nối QStash với Cloudflare Workers

Trước khi có thể bắt đầu phát triển điểm cuối API bằng Hono.js, chúng tôi cần một cách để chặn các tin nhắn do QStash gửi và loại bỏ yêu cầu nếu nó có chữ ký không hợp lệ. Rất may, Hono.js cung cấp một cách để triển khai phần mềm trung gian tùy chỉnh của riêng chúng tôi, phần mềm này luôn chạy trước trình xử lý. Nó đủ mạnh để chúng tôi có thể sắp xếp phần mềm trung gian của mình thành các tệp riêng biệt mà chúng tôi thấy phù hợp.

Trong phần mềm trung gian của chúng tôi, chúng tôi có thể sử dụng bộ thu của QStash. Hãy tạo một tệp mới có tên src/middleware/verify.ts và xuất hàm được nhập bằng MiddlewareHandler :

import { Receiver } from "@upstash/qstash";
import { type MiddlewareHandler } from "hono";
 
declare global {
 interface Response {
 locals: {
 query: string;
 };
 }
}
 
export const verify: MiddlewareHandler = async (ctx, next) => {
 const receiver = new Receiver({
 currentSigningKey: ctx.env.QSTASH_CURRENT_SIGNING_KEY,
 nextSigningKey: ctx.env.QSTASH_NEXT_SIGNING_KEY,
 });
};

Hono.js vượt qua ctx (ngữ cảnh) đối tượng cho từng phần mềm trung gian và trình xử lý. Điều này không hoàn toàn giống với ExecutionContext của Cloudflare Workers —nhưng nó chứa thông tin tương tự. ctx của Hono đối tượng gần như tương đương với đối tượng yêu cầu, môi trường và bối cảnh thực thi được chuyển đến Cloudflare Workers fetch mặc định trình xử lý, tất cả được kết hợp thành một đối tượng.

Chúng tôi cũng sửa đổi Response toàn cầu giao diện để bao gồm locals tùy chỉnh . Không giống như Express, Hono không tạo res.locals đối tượng theo mặc định. Chúng tôi sẽ sử dụng điều này để chuyển truy vấn đến trình xử lý sau. Tiếp theo, chúng ta truy cập vào các biến môi trường đã nhập trước đó để xây dựng bộ thu.

Bây giờ chúng ta có thể sử dụng người nhận để xác minh chữ ký của yêu cầu:

// snip
 
const body = await ctx.req.text();
ctx.res.locals = {
 query: JSON.parse(body).query,
};
 
const isValid = await receiver
 .verify({
 signature: ctx.req.headers.get("Upstash-Signature")!,
 body,
 })
 .catch((err) => {
 console.error(err);
 return false;
 });
 
if (!isValid) {
 return new Response("Invalid signature", { status: 401 });
}
 
await next();

Đầu tiên, chúng ta lấy đối tượng yêu cầu từ ngữ cảnh. Hono.js chỉ sử dụng các API tiêu chuẩn web một cách thuận tiện, như fetch , URL , Request , và Response . Mặc dù chúng tôi đang sử dụng nó trên Cloudflare Workers trong bản demo này, nhưng điều này cho phép nó chạy trên vô số môi trường khác, bao gồm cả môi trường biên/không có máy chủ.

Từ đối tượng yêu cầu, chúng tôi đọc nội dung của yêu cầu dưới dạng văn bản cũng như Upstash-Signature tiêu đề chứa JWT tùy chỉnh. Chúng ta có thể sử dụng người nhận để xác minh chữ ký có trong JWT bằng cách chuyển cho nó chữ ký và nội dung của yêu cầu. Trong .catch xử lý, chúng tôi đảm bảo ghi lại lỗi cũng như trả về false để chỉ ra rằng chữ ký không hợp lệ.

Vì chúng ta đang sử dụng phần nội dung của yêu cầu nên sau này chúng ta sẽ không có quyền truy cập vào yêu cầu đó trong trình xử lý thực tế của mình. Điều này là do phần thân là ReadableStream mà chỉ có thể được tiêu thụ một lần. Thay vào đó, để chuyển truy vấn tới trình xử lý, chúng ta có thể thêm truy vấn đó vào locals đối tượng trên phản hồi. Cuối cùng, nếu chữ ký không hợp lệ, chúng tôi sẽ trả về phản hồi trái phép 401. Nếu không, chúng tôi gọi next() để tiếp tục tới phần mềm trung gian hoặc trình xử lý tiếp theo.

Thêm giới hạn tỷ lệ

Vì chúng tôi sẽ kết nối điểm cuối này với các khóa API OpenAI cá nhân của mình nên điều quan trọng là phải đảm bảo rằng chúng tôi không vô tình vượt quá giới hạn tốc độ của mình. Rất may, Upstash cung cấp SDK giới hạn tỷ lệ mà chúng tôi có thể sử dụng để dễ dàng thêm giới hạn tỷ lệ vào điểm cuối của mình. Nếu số lượng yêu cầu vượt quá ngưỡng nhất định, chúng tôi sẽ trả về phản hồi 429 Quá nhiều yêu cầu.

Hãy bắt đầu bằng cách tạo một phần mềm trung gian mới trong src/middleware/ratelimit.ts :

import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis/cloudflare";
import { type MiddlewareHandler } from "hono";
 
export const ratelimit: MiddlewareHandler = async (ctx, next) => {
 const redis = new Redis({
 url: ctx.env.UPSTASH_REDIS_REST_URL,
 token: ctx.env.UPSTASH_REDIS_REST_TOKEN,
 });
 
 const ratelimit = new Ratelimit({
 redis,
 limiter: Ratelimit.slidingWindow(10, "10 s"),
 analytics: true,
 });
 
 await next();
};

Ở đây, chúng tôi đang kết nối cơ sở dữ liệu Upstash Redis của mình với SDK giới hạn tỷ lệ. Chúng ta phải tạo Redis phiên bản thủ công bằng cách sử dụng các biến môi trường từ ctx.env , bởi vì Redis.fromEnv() không thể tự động đọc chúng khi sử dụng Cloudflare Workers. Khi analytics đúng, SDK sẽ tự động gọi Redis để lưu các cuộc gọi vào bộ đệm cho từng số nhận dạng. Nó sử dụng @upstash/ratelimit làm tiền tố theo mặc định.

SDK cũng hỗ trợ sử dụng bộ đệm tạm thời sử dụng Map thay vì Redis, có thể tiết kiệm thời gian và tài nguyên khi chịu tải cực lớn:

const cache = new Map(); // outside of the middleware handler
 
// snip
 
const ratelimit = new Ratelimit({
 ephemeralCache: cache,
 // snip
});

Chúng tôi cũng cung cấp limiter tới SDK. Đây là chức năng cho SDK biết cách xếp hạng các yêu cầu giới hạn. Chúng tôi đang sử dụng slidingWindow bộ giới hạn, được định cấu hình để cho phép tối đa 10 yêu cầu cứ sau 10 giây.

SDK cũng cho phép chúng tôi cung cấp mã nhận dạng cho từng yêu cầu và SDK sẽ tự động theo dõi số lượng yêu cầu được thực hiện trong một khoảng thời gian nhất định. Trong ứng dụng trong thế giới thực, chúng tôi có thể sử dụng địa chỉ IP của người dùng làm mã nhận dạng, nhưng vì lợi ích của bản demo này, chúng tôi sẽ sử dụng một chuỗi không đổi để giới hạn tất cả các yêu cầu với một giới hạn tốc độ duy nhất:

// snip
 
const identifier = "openai";
const { success } = await ratelimit.limit(identifier);
 
if (!success) {
 return new Response("Too many requests", { status: 429 });
}
 
await next();
// snip

Nếu yêu cầu bị giới hạn tỷ lệ, chúng tôi sẽ trả về phản hồi 429 Quá Nhiều Yêu cầu. Nếu không, chúng tôi gọi next() để tiếp tục tới phần mềm trung gian hoặc trình xử lý tiếp theo. Trong trường hợp chúng tôi đã kết nối nhiều cơ sở dữ liệu Redis với SDK, nó sẽ phải thực hiện đồng bộ hóa giữa chúng. Điều này sẽ dẫn đến những lời hứa hẹn không chắc chắn về Vercel Edge và Cloudflare Workers mà chúng ta có thể giải quyết như thế này:

const { pending, success } = await ratelimit.limit(identifier);
 
ctx.event.waitUntil(pending);

Cách thực hiện việc này có thể khác nhau tùy thuộc vào thư viện bạn đang sử dụng. Vì Hono sử dụng API tiêu chuẩn web nên chúng tôi có thể sử dụng event.waitUntil phương pháp chờ đợi lời hứa để giải quyết. Tuy nhiên, đối với bản demo này, chúng tôi sẽ chỉ sử dụng một cơ sở dữ liệu Redis duy nhất, vì vậy chúng tôi không phải lo lắng về những lời hứa suông.

Nhận tin nhắn từ QStash

Khi chúng tôi gửi yêu cầu HTTP tới QStash, đích đến mà chúng tôi chỉ định sẽ là URL của điểm cuối Cloudflare Workers của chúng tôi. Chúng ta có thể sử dụng Hono.js để tạo trình xử lý cho điểm cuối này. Hãy thêm một cái mới vào src/index.ts , đồng thời kích hoạt phần mềm trung gian của chúng tôi từ trước:

// snip
import { ratelimit } from "./middleware/ratelimit";
import { verify } from "./middleware/verify";
 
// snip
 
app.post("/api/announce", ratelimit, verify, async (ctx) => {});
 
// snip

Trong trình xử lý này, chúng ta có thể sử dụng LangChain để tạo phản hồi cho lời nhắc đã cho, sử dụng Upstash Redis để lưu kết quả vào bộ đệm:

// snip
 
import { Redis } from "@upstash/redis/cloudflare";
import { UpstashRedisCache } from "langchain/cache/upstash_redis";
import { OpenAI } from "langchain/llms/openai";
 
// snip
 
app.post("/api/announce", ratelimit, verify, async (ctx) => {
 const redis = new Redis({
 url: ctx.env.UPSTASH_REDIS_REST_URL,
 token: ctx.env.UPSTASH_REDIS_REST_TOKEN,
 });
 
 const cache = new UpstashRedisCache({ client: redis });
 const model = new OpenAI({
 cache,
 openAIApiKey: ctx.env.OPENAI_API_KEY,
 });
 
 const query = ctx.res.locals.query;
 const result = await model
 .call(query)
 .then((result) => {
 console.log(result);
 return result;
 })
 .catch((err) => console.error(err));
 
 return new Response(result ?? "", { status: 200 });
});
 
// snip

Tại đây, chúng tôi nhập các lớp cần thiết để thiết lập bộ nhớ đệm cho LangChain. Sau đó, chúng tôi xây dựng chuỗi bằng mô hình OpenAI và bộ đệm Upstash Redis, chuyển một phiên bản Upstash Redis vào bộ đệm. Một lần nữa, chúng ta sẽ phải chuyển Khóa API OpenAI cho mô hình theo cách thủ công vì Cloudflare Workers không thể tự động đọc khóa này.

Sau đó, chúng ta truy cập truy vấn đã được lưu trước đó vào res.locals , sử dụng nó để gọi mô hình và ghi lại kết quả. Cuối cùng, chúng tôi trả về kết quả dưới dạng phản hồi hoặc một chuỗi trống nếu có sự cố. Ở giai đoạn này, chúng tôi có thể kiểm tra điểm cuối của mình bằng cách gửi yêu cầu POST tới điểm cuối đó và kiểm tra phản hồi. Đầu tiên, chạy lại deploy tập lệnh trong package.json của bạn:

bun run deploy

Sau khi truy xuất URL của điểm cuối Cloudflare Workers, bạn có thể gửi yêu cầu POST tới điểm đó bằng curl :

curl -XPOST \
 "https://qstash.upstash.io/v2/publish/https://<YOUR_API_URL>.workers.dev/api/announce" \
 -H "Authorization: Bearer <YOUR_QSTASH_TOKEN>" \
 -H "Content-Type: application/json" \
 -d "{ \"query\": \"What's the derivative of e^x?\" }"

Upstash cung cấp bảng điều khiển QStash mà bạn có thể sử dụng để gửi những yêu cầu này dễ dàng hơn. Thật đơn giản khi cung cấp cho QStash một công việc định kỳ để chạy liên tục yêu cầu của chúng tôi. QStash chuyển nội dung yêu cầu nguyên trạng đến điểm cuối của chúng tôi. Chúng tôi đang gửi tải trọng JSON sau đây, trong đó query là lời nhắc cho mô hình:

{
 "query": "What's the derivative of e^x?"
}

Tăng hiệu suất:Bộ nhớ đệm LLM theo yêu cầu với LangChain, Redis và QStash

Kết luận

SDK giới hạn tốc độ lưu trữ thành công số lượng cuộc gọi trên mỗi số nhận dạng:

Tăng hiệu suất:Bộ nhớ đệm LLM theo yêu cầu với LangChain, Redis và QStash

Tương tự, nội dung được tạo từ LangChain sẽ được lưu vào bộ nhớ đệm thành công trong cơ sở dữ liệu Upstash Redis của chúng tôi:

Tăng hiệu suất:Bộ nhớ đệm LLM theo yêu cầu với LangChain, Redis và QStash

Và cuối cùng, phản hồi được ghi vào nhật ký Cloudflare Worker của chúng tôi:

Tăng hiệu suất:Bộ nhớ đệm LLM theo yêu cầu với LangChain, Redis và QStash