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로 만든 자기 개선 시스템
관련 글

크론 + Telegram + Claude로 만든 자기 개선 시스템 — $0 아키텍처 해부
1편에서 소개한 Self-Tuning Loop 패턴을 실제로 운영하는 시스템의 전체 아키텍처. 데이터 수집(35개 소스), AI 큐레이션, Telegram 입력 파이프라인, 주간 자동 리뷰, Syncthing 기반 무배포 프롬프트 진화까지.

매일 버려지는 학습 시그널 — AI 초안과 최종본 사이의 간극
AI 초안을 수정할 때마다 발생하는 implicit feedback(편집 diff)을 캡처하고, 주기적으로 패턴을 분석하여 프롬프트를 자동 진화시키는 Self-Tuning Loop 패턴. 학술 연구(DSPy, TextGrad, POHF)와의 gap 분석 포함.

왜 오픈소스로 만들었나 — 체크리스트를 닫으면 안 되는 이유
MMU를 유료 SaaS 대시보드가 아닌 MIT 오픈소스 CLI로 공개한 이유 분석. 1인 빌더의 워크플로우(터미널 중심)와 비용 구조(구독 피로도)를 고려한 결정 과정, 그리고 'What(무료)-How(유료)-Auto(SaaS)'로 이어지는 3단계 수익 모델 설계 및 검증 지표 정리.