7 min read

프롬프트는 코드가 아니다

코드로 작성 된 프롬프트를 Langfuse로 분리 후 개발 사이클 개선한 썰

Author: qyinm
프롬프트는 코드가 아니다

프롬프트는 코드가 아니다

프롬프트 한 줄 고치는 데 한 시간 걸렸다.

500줄짜리 프롬프트가 TypeScript 파일에 하드코딩되어 있었다. 만약 프롬프트 수정이 있으면 코드 수정 → 빌드 → 테스트 → 배포 → 모니터링 과정을 거쳐야 했다. ‘과연 프롬프트 수정이 컨테이너 배포 사이클에 타는가?’ 라는 생각이 들었고 결국 Langfuse로 마이그레이션을 했다.

왜 프롬프트는 코드가 아닌가

코드는 배포 사이클을 타도 된다. 버그 고치려면 신중해야 하니까.

프롬프트는 다르다. 실제로 내가 만든 서비스의 프롬프트를 하루에 10번도 수정한 적이 있다. 표현을 바꿔보고, 예시를 추가하고, 톤을 조정한다. LLM 응답이 이상하면 즉시 고쳐야 한다.

배포 사이클에 태우면 실험이 불가능하다.

// 프롬프트가 코드에 하드코딩
@Injectable()
export class PromptService {
  private readonly newsletterPrompt = `You are an expert...
    [500줄의 프롬프트]
  `;
}

그래서 나는 프롬프트는 설정에 가깝다 생각했고, 코드와 분리 하고자 했다.

해결: 프롬프트를 코드에서 분리

프롬프트를 코드에서 분리하려면 관리 도구가 필요했다. Langfuse는 트레이싱과 프롬프트 관리를 한 곳에서 한다.

왜 Langfuse인가:

  1. 프롬프트 수정 → 코드 수정 → 배포 (느림)
  2. A/B 테스트 불가능
  3. 비개발자는 프롬프트 못 고침

Langfuse는 트레이싱 + 프롬프트 관리를 한다. 도구 하나 더 쓰는 게 아니라 워크플로우가 바뀐다.

// Langfuse에서 프롬프트 동적으로 가져오기
async getPrompt(name: string): Promise<PromptTemplate> {
  const prompt = await this.langfuseClient.prompt.get(name, {
    label: 'production',
  });
  return PromptTemplate.fromTemplate(prompt.getLangchainPrompt());
}

프롬프트를 고치고 프로덕션에 반영하는 과정:

  1. Langfuse UI에서 편집
  2. “Promote to Production” 클릭

코드 변경 없이 프롬프트만 바뀐다. 트레이싱 데이터는 그대로 쌓인다. 프롬프트 수정 전후 성능을 바로 비교할 수 있다.

버전 관리가 내장되어 있다

모든 프롬프트 변경이 자동으로 버전이 된다:

  • 롤백은 클릭 한 번
  • 버전별 성능 메트릭 자동 수집
  • Production/Staging/Development 레이블로 환경 분리

A/B 테스트도 쉽다. 코드 변경 없이 프롬프트만 바꿔가며 테스트한다.

LangSmith에서 Langfuse로

마이그레이션은 업보라 생각하고 노가다를 했다.

트레이싱 교체

Langfuse CallbackHandler 사용하면 된다. 그런데 ESM 방식이라 CJS 방식으로 변환해야 한다.(이거 때문에 시간 많이 씀)

프롬프트 마이그레이션

18개 프롬프트를 Langfuse로 옮겼다. 하나씩 옮기느라 노가다했다..

배운 것

1. 캐싱

Langfuse API를 캐싱하자

import { LangfuseClient } from "@langfuse/client";
 
const langfuse = new LangfuseClient();
 
// Get current `production` version and cache prompt for 5 minutes
const prompt = await langfuse.prompt.get("movie-critic", {
  cacheTtlSeconds: 300,
});

캐싱을 설정할 수 있으니 속도에서 이점을 볼 수 있다.

공식 문서

2. 프롬프트 변경을 추적한다

트레이싱 메타데이터에 프롬프트 버전을 넣으면:

metadata: {
  promptVersion: prompt.version,
  promptName: 'newsletter-simple',
}

Langfuse에서 “이 버전의 프롬프트는 평균 응답이 2초 느리네?” 같은 분석이 가능하다.

3. 트레이싱 + 프롬프트를 한 곳에서

LangSmith를 쓸 때는 프롬프트 문제를 발견해도 고치러 다른 곳으로 가야 했다. Langfuse는 같은 대시보드에서

  • 문제 발견 (트레이싱)
  • 프롬프트 수정
  • 성능 비교

모두 한다. 워크플로우가 단순해졌다.

4. ESM/CJS 호환성

Langfuse는 ESM, NestJS는 CJS. Dynamic import로 해결:

async onModuleInit() {
  const { CallbackHandler } = await import('@langfuse/langchain');
  this.CallbackHandlerClass = CallbackHandler;
}

LangSmith도 비슷한 문제가 있었지만, Langfuse 마이그레이션하면서 패턴을 정리했다. NestJS도 ESM으로 해줘…

Langfuse 설정 팁

레이블 전략

  • production: 실서비스
  • staging: 테스트 중
  • development: 실험 중

개발 → 스테이징 → 프로덕션 순서로 promote.

프롬프트 버전을 메타데이터에

트레이싱할 때 프롬프트 버전 정보를 넣으면 나중에 분석이 쉽다:

metadata: {
  promptVersion: prompt.version,
}

후회하는 것

처음부터 할 걸

프롬프트 관리 문제를 알고 있었지만 미뤘다. “나중에 고치지 뭐” 하다가 결국 관리와 개발 사이클에서 병목이 발생해 마이그레이션 하게 되었다.

모니터링 설정

트레이싱만 보고 알림을 늦게 설정했다. 처음부터 설정하자 🥹

생성 실패나 아니면 성능이 떨어졌다거나 툴 내부의 지표에서 떨어지면 알림을 꼭 설정하자

결론

프롬프트는 코드가 아니다. 설정에 가깝다.

코드처럼 관리하면:

  • 배포 사이클이 병목
  • A/B 테스트 불가능
  • 실험 속도 느림

Langfuse로 분리 후:

  • 버전별 성능 비교 자동화
  • 워크플로우 단순화

프롬프트가 자주 바뀐다면 코드에서 분리하라.

코드는 Langfuse 문서에 더 자세히 나와 있다.