Minbook
EN
Self-Tuning Loop 직접 만들기 — 레퍼런스 구현 가이드

Self-Tuning Loop 직접 만들기 — 레퍼런스 구현 가이드

MJ · · 3 분 소요

Self-Tuning Loop 4단계(Generate → Capture → Analyze → Evolve)를 범용 모듈로 추출. Supabase DDL, diff 캡처 유틸, 분석/진화 프롬프트 전문, 이메일/블로그 적용 예시, GitHub 레퍼런스 구현.

1편에서 개념을 소개했고, 2편에서 실제 운영 시스템을 해부했습니다. 이번 편에서는 Self-Tuning Loop의 핵심 패턴을 직접 만들 수 있는 모듈로 추출합니다.

이 글의 코드와 프롬프트는 모두 GitHub에 공개되어 있습니다.


모듈 구조

Self-Tuning Loop은 4개의 독립적인 컴포넌트로 구성됩니다.

self-tuning-loop/
├── schema.sql                 # Supabase DDL
├── src/
│   ├── capture.ts             # diff 추출 유틸
│   ├── analyze.ts             # 주기적 패턴 분석 실행
│   └── evolve.ts              # 가이드라인 자동 패치
├── prompts/
│   ├── analyze-diffs.md       # 범용 분석 프롬프트
│   └── evolve-guidelines.md   # Safe/Risky 진화 프롬프트
├── guidelines/
│   └── example-email.md       # 진화하는 가이드라인 예시
└── examples/
    ├── email/                 # 이메일 튜닝 데모
    └── blog/                  # 블로그 톤 튜닝 데모

각 컴포넌트는 독립적으로 교체 가능합니다. Supabase 대신 다른 DB를 쓸 수도 있고, 분석 프롬프트만 가져가서 기존 시스템에 끼워 넣을 수도 있습니다.


Step 1: 스키마 — 초안과 최종본을 저장하는 테이블

Self-Tuning Loop의 첫 번째 전제: 초안과 최종본이 같은 곳에 저장되어야 합니다.

-- schema.sql
create table drafts (
  id uuid default gen_random_uuid() primary key,
  domain text not null,              -- 'email', 'blog', 'linkedin' 등
  created_at timestamptz default now(),

  -- Generate 단계
  input text,                        -- 사용자가 요청한 것
  ai_draft text not null,            -- AI가 생성한 초안
  guidelines_version int default 1,  -- 어떤 버전의 가이드라인으로 생성했는지

  -- Capture 단계
  human_final text,                  -- 사용자가 수정한 최종본
  finalized_at timestamptz,          -- 최종본 확정 시각
  diff_summary text,                 -- LLM이 생성한 diff 요약

  -- 메타
  feedback_rating smallint,          -- 1=👎, 5=👍 (선택)
  feedback_comment text              -- 자유 코멘트 (선택)
);

-- Analyze 결과 저장
create table analysis_runs (
  id uuid default gen_random_uuid() primary key,
  domain text not null,
  analyzed_at timestamptz default now(),
  draft_count int,                   -- 분석에 사용된 draft 수
  patterns jsonb,                    -- 추출된 패턴 배열
  applied boolean default false      -- Evolve 단계에서 반영 완료 여부
);

-- 가이드라인 버전 관리
create table guidelines (
  id serial primary key,
  domain text not null,
  version int not null,
  content text not null,             -- 마크다운 가이드라인 전문
  created_at timestamptz default now(),
  source text,                       -- 'manual' | 'auto_evolve' | 'review_suggestion'
  analysis_run_id uuid references analysis_runs(id)
);

3개 테이블입니다.

테이블역할쓰기 주체
drafts초안 + 최종본 + diff앱 (Generate/Capture)
analysis_runs패턴 분석 결과Analyze 크론
guidelines가이드라인 버전 이력Evolve 크론

Step 2: Capture — diff 추출

사용자가 AI 초안을 수정하여 최종본을 확정하면, diff를 추출합니다.

// src/capture.ts
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_KEY!
);

interface CaptureInput {
  draftId: string;
  humanFinal: string;
}

export async function captureFinal({ draftId, humanFinal }: CaptureInput) {
  // 1. 원본 초안 조회
  const { data: draft } = await supabase
    .from('drafts')
    .select('ai_draft')
    .eq('id', draftId)
    .single();

  if (!draft) throw new Error(`Draft ${draftId} not found`);

  // 2. LLM으로 diff 요약 생성
  const diffSummary = await generateDiffSummary(draft.ai_draft, humanFinal);

  // 3. 저장
  await supabase
    .from('drafts')
    .update({
      human_final: humanFinal,
      finalized_at: new Date().toISOString(),
      diff_summary: diffSummary,
    })
    .eq('id', draftId);

  return diffSummary;
}

async function generateDiffSummary(
  aiDraft: string,
  humanFinal: string
): Promise<string> {
  // Claude/GPT 등 어떤 LLM이든 사용 가능
  const prompt = `
아래 두 텍스트를 비교하여 사용자의 편집 패턴을 요약하세요.

## AI 초안
${aiDraft}

## 사용자 최종본
${humanFinal}

## 출력 형식
- 변경 1: [무엇을 어떻게 바꿨는지]
- 변경 2: ...
- 추정 의도: [왜 이렇게 바꿨는지]
`;

  // LLM API 호출 (구현은 사용하는 SDK에 따라)
  return callLLM(prompt);
}

핵심은 diff_summary입니다. 텍스트 diff 알고리즘(Myers diff 등)으로 줄 단위 변경을 추출할 수도 있지만, LLM에게 “의미 단위”로 요약시키는 것이 Analyze 단계에서 훨씬 유용합니다.

// 줄 단위 diff (기계적)
- "AI 기술의 발전은 우리 사회에 큰 변화를 가져오고 있습니다."
+ "GPT-4o 출시 3개월 만에 기업 도입률이 34% 증가했습니다."

// 의미 단위 diff (LLM 요약)
변경: 추상적 서술 → 구체적 수치 + 고유명사
추정 의도: 도입부에 즉각적인 신뢰 시그널 제공

후자가 패턴 분석에 유의미한 입력입니다.


Step 3: Analyze — 패턴 추출 프롬프트

일정 기간 diff가 쌓이면 패턴을 분석합니다. 이 프롬프트가 Self-Tuning Loop의 핵심입니다.

# analyze-diffs.md — 범용 분석 프롬프트

## 역할
당신은 사용자의 편집 패턴을 분석하는 전문가입니다.

## 입력
아래는 최근 {N}건의 AI 초안 → 사용자 최종본 diff 요약입니다.
도메인: {domain}

{diff_summaries}

## 분석 지시

### 1. 반복 패턴 추출
같은 유형의 편집이 3회 이상 반복되는 것을 패턴으로 식별하세요.
각 패턴에 대해:
- **패턴명**: 한 줄 요약
- **빈도**: N건 중 몇 건 (백분율)
- **구체적 변경**: 무엇이 어떻게 바뀌었는지
- **추정 근거**: 사용자가 왜 이렇게 바꾸는지

### 2. 분류
각 패턴을 분류하세요:
- **톤/스타일**: 문체, 격식, 이모지 사용 등
- **구조**: 단락 순서, 도입부/결론 방식
- **콘텐츠**: 포함/제외하는 정보 유형
- **포맷**: 길이, 리스트 사용, 헤딩 등

### 3. Safe/Risky 판정
- **Safe** (자동 반영 가능): 빈도 70% 이상 AND 톤/스타일/포맷 관련
- **Risky** (제안만): 빈도 70% 미만 OR 구조/콘텐츠 변경

## 출력 형식 (JSON)
```json
{
  "patterns": [
    {
      "name": "도입부 구체화",
      "frequency": "8/10 (80%)",
      "category": "구조",
      "change": "추상적 서술 → 구체적 수치/사례로 시작",
      "reason": "독자의 즉각적 관심 유도",
      "classification": "safe"
    }
  ],
  "summary": "전체 편집 경향 한 줄 요약",
  "confidence": "high | medium | low"
}

### 실행 코드

```typescript
// src/analyze.ts
export async function analyzeDiffs(domain: string, lookbackDays = 7) {
  // 1. 최근 N일간 finalized된 draft 조회
  const since = new Date();
  since.setDate(since.getDate() - lookbackDays);

  const { data: drafts } = await supabase
    .from('drafts')
    .select('diff_summary, feedback_rating, feedback_comment')
    .eq('domain', domain)
    .not('human_final', 'is', null)
    .gte('finalized_at', since.toISOString())
    .order('finalized_at', { ascending: false });

  if (!drafts || drafts.length < 3) {
    console.log(`${domain}: ${drafts?.length ?? 0}건 — 최소 3건 필요, 스킵`);
    return null;
  }

  // 2. diff 요약들을 모아서 분석 프롬프트에 주입
  const diffSummaries = drafts
    .map((d, i) => `### Draft ${i + 1}\n${d.diff_summary}\n${
      d.feedback_comment ? `코멘트: ${d.feedback_comment}` : ''
    }`)
    .join('\n\n');

  const prompt = analyzePromptTemplate
    .replace('{N}', String(drafts.length))
    .replace('{domain}', domain)
    .replace('{diff_summaries}', diffSummaries);

  // 3. LLM 호출 + 결과 저장
  const result = await callLLM(prompt);
  const patterns = JSON.parse(result);

  await supabase.from('analysis_runs').insert({
    domain,
    draft_count: drafts.length,
    patterns,
  });

  return patterns;
}

Step 4: Evolve — 가이드라인 자동 패치

분석 결과의 Safe 패턴을 가이드라인에 반영합니다.

# evolve-guidelines.md — 진화 프롬프트

## 역할
당신은 작성 가이드라인을 업데이트하는 편집자입니다.

## 입력

### 현재 가이드라인
{current_guidelines}

### 반영할 패턴 (Safe 판정된 것만)
{safe_patterns}

## 규칙

1. **추가만 합니다.** 기존 규칙을 삭제하거나 수정하지 마세요.
2. 새 규칙은 기존 형식과 동일한 스타일로 작성하세요.
3. 이미 기존 규칙과 중복되는 패턴은 무시하세요.
4. 각 추가 규칙 끝에 `(auto: {날짜})`를 표기하세요.

## 출력
업데이트된 가이드라인 전문을 마크다운으로 출력하세요.
추가된 부분만 `+ ` 접두사로 표시하세요.

진화 실행 코드

// src/evolve.ts
export async function evolveGuidelines(domain: string) {
  // 1. 최신 미반영 분석 결과 조회
  const { data: run } = await supabase
    .from('analysis_runs')
    .select('*')
    .eq('domain', domain)
    .eq('applied', false)
    .order('analyzed_at', { ascending: false })
    .limit(1)
    .single();

  if (!run) return null;

  // 2. Safe 패턴만 필터
  const safePatterns = run.patterns.patterns
    .filter((p: any) => p.classification === 'safe');

  if (safePatterns.length === 0) {
    console.log(`${domain}: Safe 패턴 없음, 스킵`);
    await supabase
      .from('analysis_runs')
      .update({ applied: true })
      .eq('id', run.id);
    return null;
  }

  // 3. 현재 가이드라인 조회
  const { data: current } = await supabase
    .from('guidelines')
    .select('*')
    .eq('domain', domain)
    .order('version', { ascending: false })
    .limit(1)
    .single();

  // 4. LLM으로 가이드라인 업데이트
  const prompt = evolvePromptTemplate
    .replace('{current_guidelines}', current?.content ?? '(초기 가이드라인 없음)')
    .replace('{safe_patterns}', JSON.stringify(safePatterns, null, 2));

  const updatedContent = await callLLM(prompt);

  // 5. 새 버전 저장
  const newVersion = (current?.version ?? 0) + 1;
  await supabase.from('guidelines').insert({
    domain,
    version: newVersion,
    content: updatedContent,
    source: 'auto_evolve',
    analysis_run_id: run.id,
  });

  // 6. 분석 결과를 반영 완료로 표시
  await supabase
    .from('analysis_runs')
    .update({ applied: true })
    .eq('id', run.id);

  return { version: newVersion, addedPatterns: safePatterns.length };
}

예제 1: 이메일 자동 튜닝

가장 보편적인 적용 사례입니다. 매일 AI로 이메일 답장 초안을 만들고, 수정 후 발송하는 사람.

초기 가이드라인

## 이메일 작성 가이드라인 v1
- 톤: 비즈니스 캐주얼
- 인사: "안녕하세요, {이름}님" 으로 시작
- 분량: 3-5문장
- 서명: "감사합니다, {내 이름}" 으로 마무리

2주 후 자동 진화된 가이드라인

## 이메일 작성 가이드라인 v4 (자동 진화)
- 톤: 비즈니스 캐주얼
- 인사: "안녕하세요, {이름}님" 으로 시작
- 분량: 3-5문장
- 서명: "감사합니다, {내 이름}" 으로 마무리
+ - 첫 문장에 상대방의 이전 메일 핵심을 요약하여 시작 (auto: 2026-04-07)
+ - "확인 부탁드립니다" 대신 구체적 액션 명시 (auto: 2026-04-07)
+ - 요청 사항이 2개 이상이면 번호 리스트 사용 (auto: 2026-04-14)
+ - 외부 미팅 관련은 격식체("~드립니다"), 내부는 반말체 유지 (auto: 2026-04-14)

사용자는 이 가이드라인을 직접 작성한 적이 없습니다. 매일 이메일을 수정한 것만으로, 시스템이 패턴을 포착하여 자동으로 추가했습니다.


예제 2: 블로그 톤 진화

블로그 포스트 초안을 AI로 만들고 편집하는 경우.

diff 패턴 분석 결과 (실제)

{
  "patterns": [
    {
      "name": "도입부 축소",
      "frequency": "9/12 (75%)",
      "category": "구조",
      "change": "3-4문장 도입부 → 1문장으로 축소",
      "reason": "독자의 스크롤을 줄이고 본론 진입 속도 향상",
      "classification": "safe"
    },
    {
      "name": "데이터 우선 배치",
      "frequency": "10/12 (83%)",
      "category": "콘텐츠",
      "change": "주장 → 근거 순서를 근거(수치/인용) → 주장으로 변경",
      "reason": "데이터가 먼저 와야 주장에 신뢰가 생김",
      "classification": "safe"
    },
    {
      "name": "결론 제거",
      "frequency": "7/12 (58%)",
      "category": "구조",
      "change": "요약형 결론 삭제, 마지막 섹션으로 자연 종료",
      "reason": "불필요한 반복 회피",
      "classification": "risky"
    }
  ]
}

“도입부 축소”와 “데이터 우선 배치”는 Safe (70% 이상, 자동 반영). “결론 제거”는 Risky (58%, 제안만) — 이런 구조적 변경은 사람이 확인 후 반영합니다.


운영 팁: 실전에서 배운 것들

최소 시작 데이터

Self-Tuning Loop은 3건의 diff부터 작동합니다. 10건이면 패턴이 명확해지고, 30건이면 Safe 판정의 신뢰도가 높아집니다. 첫 주는 수동으로 프롬프트를 고치고, 2주차부터 루프를 켜는 것을 권장합니다.

도메인 분리

이메일과 블로그를 같은 domain으로 묶지 마세요. 이메일에서 “격식체 유지”가 Safe로 판정되면 블로그 가이드라인에도 격식체가 추가될 수 있습니다. 도메인별로 독립된 가이드라인을 유지하세요.

가이드라인 비대화 방지

가이드라인이 계속 커지면 LLM의 instruction following이 떨어집니다. 경험적으로 20개 규칙 이내가 적정선입니다. 분기별로 오래된 규칙을 리뷰하고, 더 이상 유효하지 않은 것을 정리하세요.

Evolve 프롬프트의 “추가만” 규칙

Evolve 단계에서 기존 규칙을 수정하거나 삭제하지 않도록 설계한 이유가 있습니다. 자동 시스템이 기존 규칙을 건드리면 사용자가 의도적으로 넣은 규칙이 사라질 수 있습니다. 삭제/수정은 항상 사람이 합니다.


Quick Start

1. Supabase 프로젝트 생성

무료 티어로 충분합니다. SQL Editor에서 schema.sql을 실행하세요.

2. 초기 가이드라인 등록

insert into guidelines (domain, version, content, source)
values (
  'email',
  1,
  '## 이메일 작성 가이드라인 v1
- 톤: 비즈니스 캐주얼
- 분량: 3-5문장',
  'manual'
);

3. 앱에서 Generate + Capture 연동

AI로 초안을 생성할 때 drafts 테이블에 ai_draft를 저장하고, 사용자가 수정을 완료하면 captureFinal()을 호출합니다.

4. 주간 Analyze + Evolve 크론

# cron 또는 Claude Desktop 스케줄 태스크
npx tsx src/analyze.ts --domain email
npx tsx src/evolve.ts --domain email

또는 Claude Desktop 스케줄 태스크로 등록:

매주 일요일 09:00에 실행.
1. drafts 테이블에서 최근 7일간 email 도메인의 finalized 항목을 조회
2. diff_summary를 모아서 분석 프롬프트 실행
3. Safe 패턴을 가이드라인에 반영
4. 결과를 요약하여 알림

확장 가능성

팀 레벨 적용

개인 가이드라인 위에 팀 공통 가이드라인 레이어를 추가할 수 있습니다. 팀원 개인의 diff는 개인 가이드라인에만 반영하고, 전체 팀에서 공통으로 나타나는 패턴은 팀 가이드라인에 반영합니다.

팀 가이드라인 (공통) + 개인 가이드라인 (고유) = 최종 프롬프트

크로스 도메인 러닝

이메일에서 학습된 “수치 먼저 제시” 패턴이 보고서에도 적용 가능할 수 있습니다. 분석 단계에서 도메인 간 공통 패턴을 탐지하고, 관련 도메인에 제안하는 확장이 가능합니다.

A/B 테스팅

새 가이드라인 버전을 만들 때 기존 버전과 병행하여 어느 쪽 초안에 사용자가 수정을 적게 하는지(diff 크기 비교) 측정할 수 있습니다. 이것은 프롬프트 레벨의 A/B 테스팅입니다.


마무리: 프롬프트는 코드다

이 시리즈에서 이야기한 것을 한 문장으로 요약하면:

프롬프트는 정적인 설정이 아니라, 사용자와 함께 진화하는 코드입니다.

코드에는 버전 관리가 있고, 테스트가 있고, 배포 파이프라인이 있습니다. 프롬프트도 그래야 합니다. Self-Tuning Loop은 프롬프트에 **버전 관리(guidelines 테이블) + 자동 테스트(Analyze) + 자동 배포(Evolve)**를 부여하는 시스템입니다.

다만 코드와 한 가지 다른 점이 있습니다. 코드의 테스트는 자동화된 assertion이지만, Self-Tuning Loop의 테스트는 사용자의 실제 편집입니다. 이것이 더 느리지만, 더 정직한 피드백입니다.

쓸수록 당신에게 맞춰지는 AI. 그 시작은 “초안과 최종본을 둘 다 저장하는 것”만큼 단순합니다.


참고 자료

  • Self-Tuning Loop GitHub: (레포 공개 시 링크 추가)
  • Supabase Quick Start: https://supabase.com/docs/guides/getting-started
  • 1편: 매일 버려지는 학습 시그널
  • 2편: 크론 + Telegram + Claude로 만든 자기 개선 시스템
공유

관련 글