Prompt Hub 구축하기

dongne-labdongne-lab
4 min read

LangGraph를 사용하면서 편하게 사용했던 기능은 바로 LangSmith hub에서 제공하는 prompt pull 기능이었어요.

github과 같이 hub에서 prompt를 관리할 수 있어서 수정을 해도 재배포 없이 바로 반영이 되고, tag로 관리가 돠어서 환경에 따라 다르게 설정할 수 있었거든요.

Mastra를 사용해서 vercel에 배포를 하며 겪게된 이슈는 prompt 관리가 편하지 않다는거였어요. 코딩을 어떻게 하느냐에에 따라 개발자 도구로 확인이 될 수 있다는 것도 있고요. 그래서 나만의 hub를 만들어서 prompt 관리를 하도록 구현을 해봤습니다.

실제로 서비스에 적용하기 위해서는 추가적인 edge case를 비롯하여 다듬어야 하는 부분이 많을 꺼라 생각합니다. 따라서 단지 참고용으로만 활용을 해주세요.

아울러 이 방법보다 더 효율적인 방법은 항상 있다고 생각을 합니다. 새로운 의견은 언제나 환영합니다!

핵심 아이디어

Prompt를 데이터베이스에 저장하고 버전별로 관리하는 시스템입니다. Prompt를 코드에서 분리해서 관리할 수 있어, 코드 배포 없이도 Prompt를 수정하고 실험할 수 있습니다.

주요 특징:

  • Prompt를 데이터베이스에서 동적으로 load

  • 버전별 관리로 안전한 rollback 가능

  • Template 변수를 통한 동적 prompt 생성

데이터베이스 설정

PostgreSQL을 사용하는 경우 다음 스키마를 생성합니다:

-- Prompt Management System Schema
CREATE TABLE prompts (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  name VARCHAR(100) NOT NULL,
  content TEXT NOT NULL,
  version VARCHAR(20) DEFAULT '1.0.0',
  is_active BOOLEAN DEFAULT true,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),

  -- Ensure unique name+version combinations
  CONSTRAINT prompts_name_version_unique UNIQUE (name, version)
);

-- Indexes for efficient querying
CREATE INDEX idx_prompts_name_created_at ON prompts (name, created_at DESC);
CREATE INDEX idx_prompts_active ON prompts (is_active) WHERE is_active = true;

PromptManager 구현

이제 prompt manager 클래스를 구현해보겠습니다:

export class PromptManager {
  private promptTemplate: string = '';
  private isLoaded: boolean = false;
  private templateVersion: string = '1.0.0';
  private templateId: string = '';
  private promptName: string = 'default_prompt';
  private db: any; // Database client

  constructor(dbClient: any, promptName?: string) {
    this.db = dbClient;
    this.promptName = promptName || process.env.PROMPT_NAME || 'default_prompt';
  }

  /**
   * Load prompt template from database
   * Supports: 'prompt_name' (latest) or 'prompt_name:uuid' (specific version)
   */
  async loadPromptTemplate(): Promise<void> {
    if (this.isLoaded) return;

    try {
      let query = this.db
        .from('prompts')
        .select('id, content, version, name')
        .eq('is_active', true);

      // Parse name:uuid format for specific version
      if (this.promptName.includes(':')) {
        const [name, uuid] = this.promptName.split(':');
        query = query.eq('name', name).eq('id', uuid);
      } else {
        // Get latest version
        query = query
          .eq('name', this.promptName)
          .order('created_at', { ascending: false })
          .limit(1);
      }

      const { data, error } = await query.single();

      if (error || !data) {
        throw new Error(`Prompt not found: ${this.promptName}`);
      }

      this.promptTemplate = data.content;
      this.templateVersion = data.version;
      this.templateId = data.id;
      this.promptName = data.name;
      this.isLoaded = true;

    } catch (error) {
      throw new Error(`Failed to load prompt: ${error}`);
    }
  }

  /**
   * Generate complete prompt with user input
   */
  async generatePrompt(userInput: any): Promise<string> {
    if (!this.isLoaded) {
      await this.loadPromptTemplate();
    }

    // Simple input processing
    const inputText = typeof userInput === 'string' 
      ? userInput 
      : JSON.stringify(userInput);

    return this.promptTemplate.replace('{user_input}', inputText);
  }

  /**
   * Get template information
   */
  getTemplateInfo() {
    return {
      name: this.promptName,
      version: this.templateVersion,
      id: this.templateId,
      isLoaded: this.isLoaded
    };
  }
}

버전 관리 방식

두 가지 방식으로 prompt를 load할 수 있습니다:

최신 버전 사용

// 'analysis_prompt'의 최신 버전을 자동으로 로드
const manager = new PromptManager(dbClient, 'analysis_prompt');

데이터베이스에서 created_at 기준으로 가장 최근에 생성된 버전을 가져옵니다.

특정 버전 고정

// 특정 UUID 버전을 고정해서 사용
const manager = new PromptManager(
  dbClient, 
  'analysis_prompt:550e8400-e29b-41d4-a716-446655440000'
);

name:uuid 형태로 지정하면 특정 버전을 고정해서 사용할 수 있습니다. 이는 Production 환경에서 안정성을 보장할 때 유용합니다.

동작 과정

1. 지연 로딩 (Lazy Loading)

PromptManager는 생성 시점에는 데이터베이스 연결만 설정하고, 실제 프롬프트 로딩은 generatePrompt() 호출 시까지 지연합니다. 이를 통해 불필요한 데이터베이스 호출을 방지합니다.

2. 템플릿 변수 치환

Load된 prompt에서 {user_input} 같은 template 변수를 실제 사용자 입력으로 치환합니다:

// 데이터베이스의 템플릿
"다음 데이터를 분석해주세요: {user_input}"

// 치환 후 결과  
"다음 데이터를 분석해주세요: 2024년 매출 데이터"

3. 캐싱

한 번 load된 prompt는 isLoaded flag로 관리되어 중복 loading을 방지합니다.

사용 예시

기본 사용법

const manager = new PromptManager(dbClient, 'analysis_prompt');

// 프롬프트 생성 (자동으로 DB에서 로드)
const completedPrompt = await manager.generatePrompt({
  query: "분기별 매출 분석",
  context: "전년 대비 성장률 포함"
});

버전 정보 확인

await manager.loadPromptTemplate();
const info = manager.getTemplateInfo();

console.log(`Using ${info.name} v${info.version} (${info.id})`);
// Output: Using analysis_prompt v2.1.0 (550e8400-e29b-41d4-a716-446655440000)

..

0
Subscribe to my newsletter

Read articles from dongne-lab directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

dongne-lab
dongne-lab