일부 응용 프로그램에서는 사용자가 요청할 때 LLM을 쿼리하는 것이 불가능할 수 있습니다. API에 프롬프트를 보내고 응답을 기다리는 프로세스는 특히 시간이 많이 걸릴 수 있습니다. PDF 또는 오디오 파일의 데이터를 처리하고 이를 LLM에 공급하는 등 LangChain과 관련된 보다 복잡한 작업의 경우 지연이 사용자 경험에 더욱 큰 타격을 줍니다.
스트리밍하는 동안 은 많은 경우에 적합한 솔루션이므로 사용자가 애플리케이션을 방문하기 전에 임의의 지점에서 모든 작업을 수행하는 것이 더 편리할 수 있습니다. 이렇게 하면 LLM이 응답을 생성할 때까지 기다릴 필요 없이 사용자에게 거의 즉시 캐시된 응답을 제공할 수 있습니다. 이는 책이나 뉴스 요약기와 같이 모든 사용자에게 동일한 입력을 사용하는 애플리케이션에 특히 유용합니다. 하지만 사용자의 입력 없이 어떻게 LangChain이 응답을 캐시하도록 할 수 있을까요?
우리는 필요에 따라 호출할 수 있고 LangChain에 프롬프트를 보내고 Upstash Redis를 사용하여 응답을 캐싱할 수 있는 마이크로서비스가 필요합니다. 또한 실수로 속도 제한을 초과하지 않도록 Upstash의 속도 제한 SDK를 사용할 것입니다. QStash는 마이크로서비스를 호출하는 가장 다재다능한 방법입니다. 원할 때마다 응답을 캐시하도록 설정할 수 있으며 애플리케이션에서 요구하는 경우 크론 작업을 전달할 수도 있습니다. 또한 마이크로서비스 사용량을 모니터링하는 데 사용할 수 있는 대시보드도 제공합니다. 엔드포인트가 오버로드되거나 다른 문제가 발생하는 경우 QStash는 HTTP 요청을 다시 시도하고 메시지가 전달되는지 확인합니다.
우리의 마이크로서비스는 엣지를 위한 가볍고 빠른 웹 프레임워크인 Hono.js를 사용하여 구축될 것입니다. 이 데모에서는 Cloudflare Worker를 사용하여 마이크로서비스를 호스팅하겠습니다. 하지만 Upstash 덕분에 다른 엣지/서버리스 런타임을 포함하여 거의 모든 곳에 배포할 수 있습니다.
여기에서 이 데모의 전체 소스 코드를 찾을 수 있습니다.
전제조건
- Upstash Redis 데이터베이스
- QStash 환경 변수
- OpenAI API 키
시작하기
프로젝트 생성
다른 많은 create-<package>과 달리 npm 패키지, create-hono 빈 디렉터리에 있어야 합니다. 먼저, 프로젝트를 위한 새로운 빈 디렉토리를 생성하고 해당 디렉토리로 이동하세요:
mkdir langchain-qstash
cd langchain-qstash
v1.0.0의 최근 릴리스에서는 Bun이 이 프로젝트의 기반이 되는 데 사용될 것입니다. 그러나 create-hono을 사용하는 것도 가능합니다. npm로 , pnpm 및 yarn . cloudflare-workers을 선택하세요. 템플릿을 묻는 메시지가 표시되면 bun이 표시될 수 있습니다. 별도의 옵션으로 제공되지만 데모에 사용된 템플릿은 아닙니다.
bun create hono@latest 종속성 설치
이제 선택한 패키지 관리자로 다음 명령을 실행하여 나머지 종속성을 설치할 수 있습니다.
bun install @upstash/qstash @upstash/ratelimit @upstash/redis langchain openai 프로젝트 구성
작성 당시 create-hono .gitignore에 잠금 파일이 포함되어 있습니다. 기본적으로. 꼭 필요한 것은 아니지만 .gitignore를 업데이트할 수 있습니다. 다음과 같이 잠금 파일을 제외하려면:
node_modules
dist
.wrangler
.dev.vars
wrangler.toml
이제 전제 조건에서 환경 변수를 설정할 수 있습니다. wrangler.toml에 추가할 수 있습니다. , 이미 소스 제어에서 제외되어야 합니다.
[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-********"
마지막으로 src/index.ts를 수정하세요. 이전의 환경 변수에 대한 입력을 추가하려면:
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 }>(); 두 개의 서명 키를 갖는 이점은 환경 변수를 업데이트하지 않고도 한 번 롤링할 수 있다는 사실에서 비롯됩니다. 왜냐하면 QStash는 현재 서명 키가 실패할 경우 자동으로 다음 서명 키를 사용하려고 시도하기 때문입니다.
개발
이 단계에서는 wrangler 환경 변수를 읽고 프로젝트를 배포할 수 있어야 합니다. 이렇게 하려면 원하는 패키지 관리자로 다음 명령을 실행하면 됩니다:
bun run dev
wrangler Hono.js 서버를 가동하고 프로젝트를 테스트하기 위해 포트 8787에 로컬 URL을 제공합니다. 로컬로 테스트하는 대신 Edge 미리보기 세션을 직접 시작하려면 개발자 package.json를 수정하세요. 스크립트는 다음과 같습니다:
"dev": "wrangler dev src/index.ts --remote", Cloudflare 작업자의 구성과 로그를 보려면 먼저 작업자를 배포해야 합니다.
bun run deploy Cloudflare에 로그인하여 인증 후 처음으로 자동으로 프로젝트를 배포하는 단계를 안내합니다.
미들웨어 생성
디버깅 목적으로 QStash에서 받은 요청을 기록하는 것이 유용합니다. Begin log stream를 누르면 Cloudflare 작업자 대시보드의 "로그" 탭에 표시됩니다. . Hono.js는 라우터에 추가할 수 있는 로거 미들웨어를 제공합니다:
import { Hono } from "hono";
import { logger } from "hono/logger";
// snip
const app = new Hono<{ Bindings: Bindings }>();
app.use("*", logger());
// snip

QStash를 Cloudflare 작업자에 연결
Hono.js를 사용하여 API 엔드포인트 개발을 시작하기 전에 QStash에서 보낸 메시지를 가로채서 서명이 잘못된 경우 요청을 삭제하는 방법이 필요합니다. 다행히도 Hono.js는 항상 핸들러보다 먼저 실행되는 자체 사용자 정의 미들웨어를 구현하는 방법을 제공합니다. 우리가 적절하다고 생각하는 대로 미들웨어를 별도의 파일로 구성할 수 있을 만큼 강력합니다.
미들웨어에서는 QStash의 수신기를 활용할 수 있습니다. src/middleware/verify.ts라는 새 파일을 만들어 보겠습니다. , 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가 ctx를 전달합니다. (컨텍스트) 객체를 각 미들웨어 및 핸들러에 할당합니다. 이는 Cloudflare Workers의 ExecutionContext와 직접적으로 동일하지 않습니다. —하지만 동일한 정보가 포함되어 있습니다. 호노의 ctx 개체는 기본 Cloudflare Workers fetch에 전달된 요청 개체, 환경, 실행 컨텍스트와 거의 동일합니다. 핸들러는 모두 하나의 개체로 결합됩니다.
또한 전역 Response을 수정합니다. 사용자 정의 locals를 포함하는 인터페이스 . Express와 달리 Hono는 res.locals를 생성하지 않습니다. 기본적으로 개체입니다. 나중에 이를 사용하여 쿼리를 처리기에 전달합니다. 다음으로, 앞서 입력한 환경 변수에 액세스하여 수신기를 구성합니다.
이제 수신자를 사용하여 요청 서명을 확인할 수 있습니다:
// 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();
먼저 컨텍스트에서 요청 개체를 가져옵니다. Hono.js는 fetch과 같은 Web Standard API만 편리하게 사용합니다. , URL , Request 및 Response . 이 데모에서는 Cloudflare Workers에서 사용하고 있지만 이를 통해 엣지/서버리스 환경을 포함한 수많은 다른 환경에서도 실행할 수 있습니다.
요청 객체에서 요청 본문과 Upstash-Signature를 텍스트로 읽습니다. 사용자 정의 JWT가 포함된 헤더입니다. 수신자를 사용하여 요청의 서명과 본문을 전달하여 JWT에 포함된 서명을 확인할 수 있습니다. .catch에서 핸들러를 사용하면 오류를 기록하고 false을 반환해야 합니다. 서명이 유효하지 않음을 나타냅니다.
요청 본문을 사용하고 있기 때문에 나중에 실제 처리기에서 해당 내용에 액세스할 수 없습니다. 본문이 ReadableStream이기 때문입니다. 한 번만 먹을 수 있는 것입니다. 대신 쿼리를 핸들러에 전달하기 위해 locals에 추가할 수 있습니다. 응답에 대해 이의를 제기합니다. 마지막으로 서명이 유효하지 않으면 401 Unauthorized 응답을 반환합니다. 그렇지 않으면 next()를 호출합니다. 다음 미들웨어나 핸들러로 계속 진행합니다.
속도 제한 추가
이 엔드포인트를 개인 OpenAI API 키에 연결할 예정이므로 실수로 속도 제한을 초과하지 않도록 하는 것이 중요합니다. 다행히 Upstash는 엔드포인트에 속도 제한을 쉽게 추가하는 데 사용할 수 있는 속도 제한 SDK를 제공합니다. 요청 수가 지정된 임계값을 초과하는 경우 429 요청이 너무 많음 응답을 반환합니다.
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();
};
여기서는 Upstash Redis 데이터베이스를 Rate Limiting SDK에 연결합니다. Redis을 만들어야 합니다. ctx.env의 환경 변수를 사용하여 수동으로 인스턴스 , Redis.fromEnv()이기 때문입니다. Cloudflare Workers를 사용할 때 자동으로 읽지 못합니다. analytics일 때 true인 경우 SDK는 자동으로 Redis를 호출하여 각 식별자에 대한 호출 캐시를 유지합니다. @upstash/ratelimit을 사용합니다. 기본적으로 접두사로 사용됩니다.
SDK는 Map를 사용하는 임시 캐시 사용도 지원합니다. 극도의 부하에서 시간과 리소스를 절약할 수 있는 Redis 대신:
const cache = new Map(); // outside of the middleware handler
// snip
const ratelimit = new Ratelimit({
ephemeralCache: cache,
// snip
});
limiter도 제공합니다. SDK에. 이는 SDK에 요청 속도 제한 방법을 알려주는 함수입니다. 우리는 slidingWindow를 사용하고 있습니다 10초마다 최대 10개의 요청을 허용하도록 구성된 제한기입니다.
또한 SDK를 사용하면 각 요청에 대한 식별자를 제공할 수 있으며 특정 기간 내에 이루어진 요청 수를 자동으로 추적합니다. 실제 애플리케이션에서는 사용자의 IP 주소를 식별자로 사용할 수 있지만 이 데모에서는 상수 문자열을 사용하여 단일 속도 제한으로 모든 요청을 제한하겠습니다.
// snip
const identifier = "openai";
const { success } = await ratelimit.limit(identifier);
if (!success) {
return new Response("Too many requests", { status: 429 });
}
await next();
// snip
요청의 속도가 제한된 경우 429 요청이 너무 많음 응답을 반환합니다. 그렇지 않으면 next()를 호출합니다. 다음 미들웨어나 핸들러로 계속 진행합니다. 여러 Redis 데이터베이스를 SDK에 연결한 경우에는 이들 데이터베이스 간에 동기화를 수행해야 합니다. 이로 인해 Vercel Edge 및 Cloudflare Workers에 대한 약속이 중단될 수 있으며, 이를 다음과 같이 처리할 수 있습니다.
const { pending, success } = await ratelimit.limit(identifier);
ctx.event.waitUntil(pending);
이를 수행하는 방법은 사용 중인 라이브러리에 따라 다를 수 있습니다. Hono는 Web Standard API를 사용하므로 event.waitUntil를 사용할 수 있습니다. Promise가 해결될 때까지 기다리는 메서드입니다. 하지만 이 데모에서는 단일 Redis 데이터베이스만 사용하므로 약속이 지켜지는 것에 대해 걱정할 필요가 없습니다.
QStash에서 메시지 수신
QStash에 HTTP 요청을 보낼 때 우리가 지정하는 대상은 Cloudflare Workers 엔드포인트의 URL이 됩니다. Hono.js를 사용하여 이 엔드포인트에 대한 핸들러를 생성할 수 있습니다. src/index.ts에 새 항목을 추가해 보겠습니다. , 이전의 미들웨어를 활성화하면서:
// snip
import { ratelimit } from "./middleware/ratelimit";
import { verify } from "./middleware/verify";
// snip
app.post("/api/announce", ratelimit, verify, async (ctx) => {});
// snip 이 핸들러에서는 LangChain을 사용하여 주어진 프롬프트에 대한 응답을 생성하고 Upstash Redis를 사용하여 결과를 캐시할 수 있습니다.
// 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 여기에서는 LangChain에 대한 캐싱을 설정하는 데 필요한 클래스를 가져옵니다. 그런 다음 OpenAI 모델과 Upstash Redis 캐시를 사용하여 체인을 구성하고 Upstash Redis 인스턴스를 캐시에 전달합니다. OpenAI API 키는 Cloudflare Workers에서 자동으로 읽을 수 없으므로 수동으로 모델에 전달해야 합니다.
그런 다음 이전에 res.locals에 저장된 쿼리에 액세스합니다. , 이를 사용하여 모델을 호출하고 결과를 기록합니다. 마지막으로 결과를 응답으로 반환하거나 문제가 발생한 경우 빈 문자열을 반환합니다. 이 단계에서는 엔드포인트에 POST 요청을 보내고 응답을 확인하여 엔드포인트를 테스트할 수 있습니다. 먼저 deploy을 다시 실행하세요. package.json의 스크립트:
bun run deploy
Cloudflare Workers 엔드포인트의 URL을 검색한 후 curl를 사용하여 POST 요청을 보낼 수 있습니다. :
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는 이러한 요청을 보다 쉽게 보내는 데 사용할 수 있는 QStash 콘솔을 제공합니다. 요청을 반복적으로 실행하기 위해 QStash에 cron 작업을 제공하는 것도 마찬가지로 간단합니다. QStash는 요청 본문을 있는 그대로 우리 끝점에 전달합니다. 다음 JSON 페이로드를 보냅니다. 여기서 query 모델에 대한 프롬프트입니다:
{
"query": "What's the derivative of e^x?"
}

결론
Rate Limiting SDK는 식별자당 호출 수를 성공적으로 캐시합니다.

마찬가지로 LangChain에서 생성된 콘텐츠는 Upstash Redis 데이터베이스에 성공적으로 캐시됩니다.

마지막으로 응답은 Cloudflare Worker 로그에 기록됩니다.
