응답 캐싱

응답 캐싱 (Caching Responses)

만드는 애플리케이션의 종류에 따라 AI 프로바이더로부터 받은 응답을 적어도 임시로는 캐시하고 싶을 수 있습니다. AI SDK는 캐싱을 위해 언어 모델 미들웨어를 활용하는 방법과 라이프사이클 콜백을 활용하는 방법 두 가지를 공식적으로 제시합니다. 둘 다 모델 호출을 가로채거나 완료 시점에 응답을 저장한다는 공통점이 있지만, 세밀함은 다릅니다.

출처: 공식문서

본문

언어 모델 미들웨어 사용 (권장)

응답 캐싱의 권장 접근 방식은 언어 모델 미들웨어simulateReadableStream 함수를 함께 사용하는 것입니다.

언어 모델 미들웨어는 언어 모델에 대한 호출을 가로채고 수정해 언어 모델의 동작을 향상시키는 방법입니다. 미들웨어로 응답을 캐시하는 방법을 살펴보겠습니다.

ai/middleware.ts

import { Redis } from '@upstash/redis';
import {
  type LanguageModelV4,
  type LanguageModelV4Middleware,
  type LanguageModelV4StreamPart,
  simulateReadableStream,
} from 'ai';

const redis = new Redis({
  url: process.env.KV_URL,
  token: process.env.KV_TOKEN,
});

export const cacheMiddleware: LanguageModelV4Middleware = {
  wrapGenerate: async ({ doGenerate, params }) => {
    const cacheKey = JSON.stringify(params);

    const cached = (await redis.get(cacheKey)) as Awaited<
      ReturnType<LanguageModelV4['doGenerate']>
    > | null;

    if (cached !== null) {
      return {
        ...cached,
        response: {
          ...cached.response,
          timestamp: cached?.response?.timestamp
            ? new Date(cached?.response?.timestamp)
            : undefined,
        },
      };
    }

    const result = await doGenerate();

    redis.set(cacheKey, result);

    return result;
  },
  wrapStream: async ({ doStream, params }) => {
    const cacheKey = JSON.stringify(params);

    // Check if the result is in the cache
    const cached = await redis.get(cacheKey);

    // If cached, return a simulated ReadableStream that yields the cached result
    if (cached !== null) {
      // Format the timestamps in the cached response
      const formattedChunks = (cached as LanguageModelV4StreamPart[]).map(p => {
        if (p.type === 'response-metadata' && p.timestamp) {
          return { ...p, timestamp: new Date(p.timestamp) };
        } else return p;
      });
      return {
        stream: simulateReadableStream({
          initialDelayInMs: 0,
          chunkDelayInMs: 10,
          chunks: formattedChunks,
        }),
      };
    }

    // If not cached, proceed with streaming
    const { stream, ...rest } = await doStream();

    const fullResponse: LanguageModelV4StreamPart[] = [];

    const transformStream = new TransformStream<
      LanguageModelV4StreamPart,
      LanguageModelV4StreamPart
    >({
      transform(chunk, controller) {
        fullResponse.push(chunk);
        controller.enqueue(chunk);
      },
      flush() {
        // Store the full response in the cache after streaming is complete
        redis.set(cacheKey, fullResponse);
      },
    });

    return {
      stream: stream.pipeThrough(transformStream),
      ...rest,
    };
  },
};

이 예시는 @upstash/redis로 어시스턴트의 응답을 저장하고 조회하지만, 원하는 어떤 KV 저장소를 써도 됩니다.

이 미들웨어는 AI SDK가 구조화 출력을 검증하기 전에 원본 모델 응답을 캐시합니다. 구조화 출력을 쓸 때는 AI SDK가 검증을 통과한 응답만 캐시하도록, 검증 이후의 결과를 캐시해 두세요.

Next.js 애플리케이션에서 Redis로 캐싱하는 전체 예시는 Caching Middleware 레시피에서 볼 수 있습니다.

라이프사이클 콜백 사용

대안으로, 각 AI SDK Core 함수에는 특별한 라이프사이클 콜백이 있습니다. 유용한 것은 생성이 완료될 때 호출되는 onEnd입니다. 여기서 전체 응답을 캐시할 수 있습니다.

다음은 Upstash Redis와 Next.js를 사용해 OpenAI 응답을 1시간 동안 캐시하는 예시입니다.

app/api/chat/route.ts

import {
  convertToModelMessages,
  createUIMessageStreamResponse,
  streamText,
  toUIMessageStream,
  UIMessage,
} from 'ai';
import { Redis } from '@upstash/redis';

// Allow streaming responses up to 30 seconds
export const maxDuration = 30;

const redis = new Redis({
  url: process.env.KV_URL,
  token: process.env.KV_TOKEN,
});

export async function POST(req: Request) {
  const { messages }: { messages: UIMessage[] } = await req.json();

  // come up with a key based on the request:
  const key = JSON.stringify(messages);

  // Check if we have a cached response
  const cached = (await redis.get(key)) as string | null;
  if (cached != null) {
    return new Response(cached, {
      status: 200,
      headers: { 'Content-Type': 'text/plain' },
    });
  }

  // Call the language model:
  const result = streamText({
    model: "xai/grok-4.6",
    messages: await convertToModelMessages(messages),
    async onEnd({ text }) {
      // Cache the response text:
      await redis.set(key, text);
      await redis.expire(key, 60 * 60);
    },
  });

  // Respond with the stream
  return createUIMessageStreamResponse({
    stream: toUIMessageStream({ stream: result.stream }),
  });
}

더 알아보기