Skip to main content
ai2025년 10월 5일7분 소요

기존 제품에 AI 기능 구축하기

이미 사용자가 있는 제품에 AI를 통합하기 위한 실용적인 플레이북 — 올바른 모델 선택부터 프로덕션 배포 패턴까지.

aillmintegration
기존 제품에 AI 기능 구축하기

이미 사용자가 있는 제품에 AI를 추가하는 것은 AI-퍼스트 스타트업을 구축하는 것과는 근본적으로 다릅니다. 기존 인프라, 확립된 UX 패턴, 실제 사용자 기대치, 그리고 가장 중요하게는 고장 날 수 있는 것들이 있습니다. 이 게시물은 여러 프로덕션 애플리케이션에 AI 기능을 배포한 후 제가 신뢰할 수 있다고 판단한 패턴들을 다룹니다.

모델 제공업체 선택

모델 제공업체 결정은 영구적이지 않으며, 그렇게 취급되어서도 안 됩니다. 각 제공업체는 프로덕션 환경에서 중요한 고유한 강점을 가지고 있습니다.

**OpenAI (GPT-4o, GPT-4.1)**는 범용 텍스트 생성에 있어 가장 검증된 옵션으로 남아 있습니다. API는 안정적이고, 문서는 철저하며, 주변 도구 생태계는 성숙합니다. 함수 호출, 구조화된 JSON 출력 또는 광범위한 다국어 지원이 필요한 경우 OpenAI는 안전한 기본값입니다.

**Anthropic (Claude)**는 미묘한 지시 따르기 및 긴 컨텍스트 작업에 탁월합니다. 기능이 대규모 문서를 처리하거나, 복잡한 시스템 프롬프트를 유지하거나, 모델이 환각을 일으키기보다 "모르겠다"고 말해야 하는 작업을 처리할 때 Claude는 더 나은 성능을 보이는 경향이 있습니다. Claude 모델의 사고/추론 능력은 다단계 분석 작업에도 강력합니다.

Google Gemini는 기능이 멀티모달 입력을 포함할 때 고려할 가치가 있습니다. 특히 동일한 요청 내에서 텍스트와 함께 이미지, 비디오 또는 오디오를 처리해야 할 때 그렇습니다. Gemini의 네이티브 멀티모달 아키텍처는 텍스트 우선 모델의 비전 기능에서 느껴지는 덧붙여진 느낌을 피합니다. 고처리량 사용 사례에 대한 가격도 경쟁력이 있습니다.

실용적인 답변: 팀이 가장 잘 아는 제공업체로 시작하되, 전환할 수 있도록 시스템을 설계하세요. 첫날 "잘못된" 모델을 선택하는 것이 아니라, 제공업체 종속이 진정한 위험입니다.

API 래퍼 패턴

모든 AI 통합은 추상화 계층 뒤에 있어야 합니다. 제공업체를 확실히 전환할 것이기 때문이 아니라, 로깅, 캐싱, 속도 제한 및 폴백 로직을 확실히 추가해야 할 것이기 때문입니다. 그리고 이를 40개의 다른 장소에서 하고 싶지는 않을 것입니다.

interface AIProvider {
  generateText(prompt: string, options?: GenerateOptions): Promise<AIResponse>;
  generateStream(prompt: string, options?: GenerateOptions): AsyncGenerator<string>;
  generateStructured<T>(prompt: string, schema: z.ZodSchema<T>, options?: GenerateOptions): Promise<T>;
}

interface GenerateOptions {
  model?: string;
  temperature?: number;
  maxTokens?: number;
  systemPrompt?: string;
}

interface AIResponse {
  content: string;
  usage: { promptTokens: number; completionTokens: number };
  model: string;
  latencyMs: number;
}

주어진 제공업체에 대한 구체적인 구현은 얇게 유지됩니다:

class AnthropicProvider implements AIProvider {
  private client: Anthropic;

  constructor(apiKey: string) {
    this.client = new Anthropic({ apiKey });
  }

  async generateText(prompt: string, options?: GenerateOptions): Promise<AIResponse> {
    const start = Date.now();
    const response = await this.client.messages.create({
      model: options?.model ?? "claude-sonnet-4-20250514",
      max_tokens: options?.maxTokens ?? 1024,
      temperature: options?.temperature ?? 0.7,
      system: options?.systemPrompt,
      messages: [{ role: "user", content: prompt }],
    });

    const textBlock = response.content.find((b) => b.type === "text");
    return {
      content: textBlock?.text ?? "",
      usage: {
        promptTokens: response.usage.input_tokens,
        completionTokens: response.usage.output_tokens,
      },
      model: response.model,
      latencyMs: Date.now() - start,
    };
  }

  // ... generateStream, generateStructured
}

그런 다음 서비스 계층이 횡단 관심사를 처리합니다:

class AIService {
  constructor(
    private provider: AIProvider,
    private cache: CacheStore,
    private logger: Logger,
    private fallbackProvider?: AIProvider
  ) {}

  async generate(prompt: string, options?: GenerateOptions): Promise<AIResponse> {
    const cacheKey = this.buildCacheKey(prompt, options);
    const cached = await this.cache.get<AIResponse>(cacheKey);
    if (cached) return cached;

    try {
      const response = await this.provider.generateText(prompt, options);
      this.logger.info("ai_generation", {
        model: response.model,
        tokens: response.usage,
        latencyMs: response.latencyMs,
      });
      await this.cache.set(cacheKey, response, { ttl: 3600 });
      return response;
    } catch (error) {
      if (this.fallbackProvider) {
        this.logger.warn("ai_fallback_triggered", { error: String(error) });
        return this.fallbackProvider.generateText(prompt, options);
      }
      throw error;
    }
  }
}

이 패턴은 첫 주 안에 그 가치를 증명합니다. OpenAI에 장애가 발생하면 (그리고 발생할 것입니다), 폴백 제공업체로 전환합니다. 프로덕션 프롬프트 문제를 디버깅해야 할 때, 로그는 이미 거기에 있습니다.

프로덕션 환경에서의 프롬프트 엔지니어링

프로덕션 환경의 프롬프트는 소스 코드의 문자열이 아닙니다. 이는 버전 관리, 테스트 및 관찰 가능성이 필요한 별도의 관심사입니다.

제가 사용하는 템플릿 시스템은 간단합니다:

interface PromptTemplate {
  id: string;
  version: number;
  system: string;
  user: string;
  variables: string[];
}

const LISTING_DESCRIPTION: PromptTemplate = {
  id: "listing-description",
  version: 3,
  system: `You are a professional copywriter for a restaurant platform.
Write compelling menu item descriptions.
Rules:
- Max 2 sentences
- Mention key ingredients
- Never use the word "delicious" or "mouth-watering"
- Match the restaurant's tone: {{tone}}`,
  user: `Write a description for: {{itemName}}
Category: {{category}}
Ingredients: {{ingredients}}`,
  variables: ["tone", "itemName", "category", "ingredients"],
};

function renderPrompt(
  template: PromptTemplate,
  vars: Record<string, string>
): { system: string; user: string } {
  let system = template.system;
  let user = template.user;

  for (const key of template.variables) {
    const value = vars[key];
    if (!value) throw new Error(`Missing variable: ${key}`);
    system = system.replaceAll(`{{${key}}}`, value);
    user = user.replaceAll(`{{${key}}}`, value);
  }

  return { system, user };
}

버전 번호는 중요합니다. 프롬프트를 변경할 때 버전을 올리고 모든 요청과 함께 기록하세요. 사용자가 AI 출력이 변경되었다고 보고하면, 정확한 프롬프트 버전으로 추적할 수 있습니다. 프롬프트 템플릿은 하드코딩하지 말고 데이터베이스나 설정 파일에 저장하여 재배포 없이 업데이트할 수 있도록 하세요.

코드를 테스트하듯이 프롬프트를 테스트하세요. 입력/출력 픽스처 세트를 유지하세요. 프롬프트를 변경할 때 픽스처를 실행하고 차이점을 수동으로 검토하세요. 자동화된 평가는 개선되고 있지만, 프롬프트 변경에 대한 사람의 검토는 여전히 지표가 놓치는 문제를 잡아냅니다.

UX를 위한 스트리밍 응답

사용자는 완전한 응답을 위해 3초를 기다리는 것은 참을 수 있습니다. 하지만 15초 동안 스피너를 쳐다보는 것은 참지 못할 것입니다. 스트리밍이 이 문제를 해결합니다.

async function* streamAIResponse(
  provider: AIProvider,
  prompt: string,
  options?: GenerateOptions
): AsyncGenerator<string> {
  const stream = provider.generateStream(prompt, options);

  for await (const chunk of stream) {
    yield chunk;
  }
}

// In your API route (Next.js example)
export async function POST(request: Request) {
  const { prompt, options } = await request.json();

  const encoder = new TextEncoder();
  const stream = new ReadableStream({
    async start(controller) {
      try {
        for await (const chunk of streamAIResponse(aiProvider, prompt, options)) {
          controller.enqueue(encoder.encode(`data: ${JSON.stringify({ text: chunk })}\n\n`));
        }
        controller.enqueue(encoder.encode("data: [DONE]\n\n"));
        controller.close();
      } catch (error) {
        controller.enqueue(
          encoder.encode(`data: ${JSON.stringify({ error: "Generation failed" })}\n\n`)
        );
        controller.close();
      }
    },
  });

  return new Response(stream, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      Connection: "keep-alive",
    },
  });
}

클라이언트 측에서는 스트림을 소비하고 UI를 점진적으로 업데이트합니다. 인지되는 성능 차이는 극적입니다. 사용자는 전체 생성을 기다리는 대신 200-400ms 내에 콘텐츠가 나타나는 것을 보게 됩니다.

중요한 구현 세부 사항 하나: 클라이언트에서 부분 단어를 버퍼링하세요. 일부 제공업체는 단어 중간에 분할되는 토큰을 보냅니다. 작은 버퍼를 축적하고 완전한 단어만 렌더링하여 시각적 떨림을 피하세요.

비용 제어 및 캐싱 전략

AI API 비용은 예상치 못하게 높을 수 있습니다. 캐싱을 고려하지 않았다면 스테이징 환경에서 하루 2달러가 드는 기능이 프로덕션 환경에서는 하루 2,000달러가 들 수 있습니다.

의미론적 캐싱은 가장 높은 레버리지 최적화입니다. 두 사용자가 기능적으로 동일한 질문을 하면 캐시된 응답을 제공합니다. 이를 위해 벡터 데이터베이스가 필요하지 않습니다. 정규화된 입력에 대한 정확히 일치하는 캐싱으로 시작하세요. 프롬프트(변수 주입 후)를 해싱하고 TTL과 함께 응답을 저장하세요.

계층형 모델 라우팅은 품질 저하 없이 비용을 절감합니다. 모든 요청에 가장 비싼 모델이 필요한 것은 아닙니다. 간단한 분류 작업은 더 작은 모델로 라우팅하고, 복잡한 생성 작업에는 큰 모델을 예약하세요:

function selectModel(task: AITask): string {
  switch (task.complexity) {
    case "classification":
    case "extraction":
      return "gpt-4o-mini"; // fast, cheap
    case "generation":
      return "claude-sonnet-4-20250514"; // balanced
    case "reasoning":
      return "claude-opus-4-20250514"; // maximum quality
  }
}

엄격한 예산 한도를 설정하세요. 대부분의 제공업체는 API 키 수준에서 사용량 제한을 지원합니다. 이를 활용하세요. 또한 사용자별 애플리케이션 수준 속도 제한을 구현하세요. 한 명의 악성 사용자가 오후에 월간 예산을 모두 소진해서는 안 됩니다.

총 지출뿐만 아니라 기능별 비용을 추적하세요. 모든 API 호출에 해당 호출을 트리거한 기능을 태그하세요. 청구서가 도착했을 때, 총 X달러를 지출했다는 것뿐만 아니라 "SEO 설명 자동 생성" 기능이 지출의 60%를 차지한다는 것을 알아야 합니다.

우아한 성능 저하

AI 기능은 중단될 수 있습니다. 제공업체 장애가 발생하고, 속도 제한에 도달하며, 네트워크 요청이 시간 초과될 수 있습니다. 제품은 계속 작동해야 합니다.

원칙: AI 기능은 경험을 향상시켜야 하며, 방해해서는 안 됩니다. AI 기반 검색을 사용할 수 없는 경우 키워드 검색으로 폴백하세요. AI 콘텐츠 생성이 실패하면 사용자에게 수동 입력 양식을 보여주세요. 우회 경로가 없는 중요한 경로에 AI를 절대 두지 마세요.

실용적인 구현:

  • 타임아웃. AI 호출에 공격적인 타임아웃을 설정하세요 (최대 10-15초). 대부분의 UX 흐름에서 느린 응답은 응답이 없는 것보다 나쁩니다.
  • 서킷 브레이커. N번의 연속적인 실패 후, 쿨다운 기간 동안 제공업체 호출을 중단하세요. 이는 연쇄적인 실패를 방지하고 실패할 요청에 돈을 낭비하는 것을 피합니다.
  • 사전 생성된 폴백. 제품 설명이나 추천과 같은 기능의 경우, AI 없이 작동하는 템플릿 기반 폴백 세트를 유지하세요. 품질은 떨어지겠지만, 없는 것보다는 나을 것입니다.
  • UI 커뮤니케이션. 사용자에게 무슨 일이 일어났는지 알려주세요. "AI 제안을 일시적으로 사용할 수 없습니다"는 일반적인 오류 메시지나 무한 스피너보다 훨씬 낫습니다.

실제 사례

AI 콘텐츠 생성은 가장 일반적인 통합 지점입니다. 마케팅 플랫폼의 경우, 이는 제품 브리프를 받아 광고 문구 변형을 생성하고, 브랜드 가이드라인에 따라 점수를 매기고 (두 번째 AI 호출 사용), 상위 후보를 인간 검토자에게 제시하는 파이프라인을 구축하는 것을 의미했습니다. 핵심 통찰: AI가 생성하고, 인간이 큐레이션합니다. 사용자가 AI 출력을 편집하고 개선할 수 있도록 하는 기능은 생성 자체만큼 중요합니다.

인테리어 디자인을 위한 컴퓨터 비전은 다른 아키텍처를 필요로 합니다. 스타일 분석 및 가구 감지를 위해 방 사진을 처리하는 것은 이미지를 비전 모델로 보내고, 구조화된 출력을 파싱하며, 제품 카탈로그와 결과를 일치시키는 것을 포함합니다. 지연 시간이 더 길기 때문에 UX 패턴은 동기식 대기 및 표시 대신 푸시 알림을 통한 비동기식 처리로 전환됩니다.

지능형 검색은 전통적인 키워드 매칭을 의미론적 이해로 대체합니다. 레스토랑 플랫폼의 경우, 이는 메뉴 항목을 임베딩으로 인덱싱하여 "매콤하고 채식주의적인 것"을 검색할 때 해당 단어가 어떤 목록에도 정확히 나타나지 않더라도 관련 결과를 반환하도록 하는 것을 의미했습니다. 임베딩 생성은 쿼리 시간이 아닌 쓰기 시간(메뉴가 업데이트될 때)에 발생합니다. 이는 AI 제공업체 지연 시간과 관계없이 검색을 빠르게 유지합니다.

각 경우에 동일한 원칙이 적용됩니다: 제공업체를 래핑하고, 프롬프트 버전을 관리하며, 적극적으로 캐시하고, 항상 폴백을 준비하세요.

AI 기능 책임감 있게 배포하기

AI 데모와 프로덕션 AI 기능 사이의 격차는 엄청납니다. 데모는 캐싱, 오류 처리, 비용 제어 또는 우아한 성능 저하가 필요하지 않습니다. 프로덕션은 필요합니다. 이 게시물의 패턴은 이론적인 것이 아닙니다. 실제 사용자가 매일 의존하는 AI 기능을 배포하면서 얻은 것입니다.

AI 기반 방 재설계부터 자동화된 콘텐츠 스튜디오에 이르기까지, 저는 모바일 앱, SaaS 플랫폼 및 백엔드 시스템 전반에 걸쳐 AI 기능을 배포했습니다. 이 기술은 진정으로 강력하지만, 그 주변의 엔지니어링 규율이 사용자가 기능을 좋아할지 아니면 피하게 될지를 결정합니다.

래퍼 패턴으로 시작하고, 첫날부터 관찰 가능성을 추가하며, 캐시할 수 있는 모든 것을 캐시하고, AI를 사용할 수 없을 때 항상 사용자에게 진행할 수 있는 경로를 제공하세요. 모델은 계속해서 개선될 것입니다. 여러분의 임무는 이러한 개선을 활용할 수 있을 만큼 통합이 견고한지 확인하는 것입니다.

DU

Danil Ulmashev

Full Stack Developer

함께 일하는 데 관심이 있으신가요?