본 루프는 self-hosted 오픈웨이트 모델(Qwen3, Llama 4, GLM-5 등) 전용이다. AgentCore의 Claude/Nova 등 관리형 폐쇄 모델은 자가 학습 불가이므로 스코프에서 제외한다.
실 운영 적용 전에 스코프·자동화 경계·데이터 거버넌스·롤백 기준에 대한 합의가 필요하다. 자세한 합의 대상은 ADR — Self-Improving Agent Loop 도입 의사결정을 참조.
Self-Improving Agent Loop (Autosearch)
Autosearch 담론과 엔터프라이즈 해석
Karpathy의 핵심 주장
Andrej Karpathy는 LLM이 단순한 "next token prediction" 기계를 넘어 자가 탐색(autosearch) 시스템으로 진화할 것이라고 주장했다. 핵심 메커니즘:
- Tool-use Rollout: LLM이 도구(코드 실행, 웹 검색, 계산기 등)를 사용하며 여러 추론 경로를 탐색
- Success as Signal: 성공한 경로(정답 도달, 작업 완료)가 다음 학습의 시그널이 됨
- Self-Supervised Loop: 인간 라벨링 없이 자체 성공·실패 데이터를 축적하고 강화학습으로 재학습
- Compound Growth: 더 강해진 모델이 더 많은 성공 trace를 생성 → 더 강해지는 선순환
예시: 수학 문제 해결 Agent
- Rollout: "53 × 47 = ?"에 대해 5가지 접근(직접 계산, Python 실행, Wolfram Alpha, 근사 추정, 분해 계산)
- Success: Python 실행과 분해 계산이 정답 2491에 도달
- Training: 성공 경로를 preferred 샘플로, 실패 경로를 rejected 샘플로 DPO 학습
- Next Iteration: 모델이 복잡한 계산 시 Python 실행을 먼저 시도하도록 bias 증가
엔터프라이즈 환경의 제약
Karpathy의 이상론을 기업 환경에 적용하려면 다음 제약을 고려해야 한다:
| 제약 | 설명 | 해결 방향 |
|---|---|---|
| 데이터 거버넌스 | 프로덕션 trace에 PII, 기밀 정보 포함 가능 | Presidio PII 스캐너, k-anonymity, consent 추적 |
| 비용 | Rollout마다 LLM 호출 N배 증가 (N=탐색 경로 수) | 비용·품질 trade-off 최적화, 저비용 모델 우선 사용 |
| Reward 모델링 | "성공"의 정의가 모호(고객 만족? 정확도? latency?) | 복합 reward: LLM-as-judge + Ragas + 유저 피드백 |
| Mode Collapse | 특정 패턴만 반복 생성 (diversity 손실) | Entropy regularization, diverse sampling |
| Regulatory | 모델 변경마다 감사 로그, 모델 카드 업데이트 필요 | 버전 관리, audit trail, Agent 버전관리 연동 |
Self-improving loop는 **"완전 자동화"가 아니라 "인간 감독 하의 자동 강화"**로 해석해야 한다. 매 iteration마다 품질 게이트와 휴먼-인-루프 검증이 필수다.
5-Stage Loop 아키텍처
전체 아키텍처 다이어그램
Stage 1: Rollout — 프로덕션 트래픽 수집
목표: 실제 사용자 요청에 대한 Agent 실행 trace를 수집한다.
실행 주기: 연속(Real-time)
입력: 사용자 요청, 컨텍스트, Agent 상태
출력: Trace (프롬프트, 도구 호출, 중간 추론, 최종 응답, latency, 토큰 수)
수집 메커니즘:
from langfuse import Langfuse
langfuse = Langfuse()
@trace_agent_call # 데코레이터로 자동 trace
def execute_agent(user_query: str, context: dict):
trace = langfuse.trace(name="agent-execution", metadata={"user_id": context["user_id"]})
with trace.span(name="retrieval"):
docs = vector_db.search(user_query)
with trace.span(name="reasoning"):
response = llm.generate(prompt=build_prompt(user_query, docs))
with trace.span(name="tool-execution"):
if response.requires_tool:
tool_result = execute_tool(response.tool_name, response.tool_args)
trace.event(name="completion", metadata={"tokens": response.token_count})
return response
다양성 확보: 동일 요청에 대해 temperature 변화(0.7/0.9/1.1)로 3가지 응답 생성 → diversity 증가
실패 복구: Trace 수집 실패해도 사용자 응답은 정상 반환 (async logging)
Stage 2: Score — Reward 계산
목표: 각 trace에 0-1 점수를 부여하여 "얼마나 좋은 응답인가"를 정량화한다.
실행 주기: 시간별(Hourly) 배치
입력: Langfuse trace ID 배치
출력: {trace_id: reward_score} 테이블
복합 Reward 공식:
reward_score = (
w1 * llm_judge_score + # LLM-as-Judge (0-1)
w2 * ragas_faithfulness + # Ragas faithfulness (0-1)
w3 * ragas_context_recall + # Ragas context recall (0-1)
w4 * user_feedback_score + # Thumbs up=1, down=0, neutral=0.5
w5 * latency_penalty # P99 초과 시 감점
)
# 기본 가중치 (실험으로 조정)
w1, w2, w3, w4, w5 = 0.3, 0.25, 0.2, 0.2, 0.05
LLM-as-Judge 프롬프트:
judge_prompt = f"""
다음 Agent 응답을 평가하세요:
**질문**: {question}
**컨텍스트**: {context}
**응답**: {answer}
평가 기준:
1. 정확성: 컨텍스트 기반 사실 정확성
2. 완전성: 질문의 모든 측면을 다루는가
3. 명확성: 사용자가 이해하기 쉬운가
4. 간결성: 불필요한 정보 없이 핵심만 전달하는가
0-1 사이 점수와 근거를 JSON으로 반환하세요.
{{"score": 0.85, "reasoning": "정확하고 완전하나 약간 장황함"}}
"""
judge_response = cheap_llm.generate(judge_prompt) # Qwen3-7B 사용 (비용 절감)
Ragas 평가:
from ragas.metrics import faithfulness, context_recall
eval_data = {
"question": [question],
"answer": [answer],
"contexts": [contexts],
"ground_truth": [ground_truth] if available else None
}
ragas_result = evaluate(Dataset.from_dict(eval_data), metrics=[faithfulness, context_recall])
User Feedback 통합:
# Langfuse에서 사용자 피드백 조회
feedback = langfuse.get_scores(trace_id=trace_id, name="user-feedback")
user_score = 1.0 if feedback.value == "positive" else 0.0 if feedback.value == "negative" else 0.5
비용 최적화:
- LLM-as-Judge는 저비용 모델(Qwen3-7B, Llama 4 Scout) 사용
- Ragas는 캐싱(동일 question+context 조합 재사용)
- 유저 피드백 우선 — 피드백 있으면 LLM-as-Judge 스킨
Stage 3: Filter — 데이터 큐레이션 & PII 게이트
목표: 고품질 trace만 학습 데 이터로 선별하고, 민감 정보를 제거한다.
실행 주기: 시간별(Hourly) 배치
입력: Scored traces
출력: Clean training dataset (S3 Iceberg 테이블)
품질 게이트:
def filter_traces(scored_traces):
filtered = []
for trace in scored_traces:
# 1. 최소 점수 임계값
if trace.reward_score < 0.7:
continue
# 2. Latency 이상치 제거 (P99 > 30초)
if trace.latency > 30:
continue
# 3. 에러 발생 trace 제외
if trace.error_count > 0:
continue
# 4. 중복 제거 (동일 question+answer 조합)
if is_duplicate(trace):
continue
filtered.append(trace)
return filtered
PII 스캐닝 (Presidio):
from presidio_analyzer import AnalyzerEngine
from presidio_anonymizer import AnonymizerEngine
analyzer = AnalyzerEngine()
anonymizer = AnonymizerEngine()
def scan_and_anonymize(text: str) -> tuple[str, bool]:
"""PII 탐지 후 익명화. (익명화된 텍스트, PII 발견 여부) 반환"""
results = analyzer.analyze(text=text, language='ko')
if not results:
return text, False # PII 없음
# PII 발견 → 익명화
anonymized = anonymizer.anonymize(text=text, analyzer_results=results)
return anonymized.text, True
# Trace 처리
for trace in filtered_traces:
trace.question, q_has_pii = scan_and_anonymize(trace.question)
trace.answer, a_has_pii = scan_and_anonymize(trace.answer)
if q_has_pii or a_has_pii:
trace.metadata["pii_detected"] = True
k-Anonymity 체크 (동일 query 패턴이 k명 이상 있어야 학습 데이터로 사용):
def check_k_anonymity(traces, k=5):
"""동일 패 턴이 k건 미만이면 제거"""
query_counts = defaultdict(int)
for trace in traces:
query_pattern = extract_pattern(trace.question) # 엔티티 제거 후 패턴 추출
query_counts[query_pattern] += 1
return [t for t in traces if query_counts[extract_pattern(t.question)] >= k]
저장소 — S3 + Iceberg:
import pyiceberg
catalog = pyiceberg.catalog.load_catalog("training_data")
table = catalog.load_table("agent_traces")
# Iceberg 테이블에 append
table.append([
{"trace_id": t.id, "question": t.question, "answer": t.answer,
"reward": t.reward_score, "timestamp": t.timestamp}
for t in filtered_traces
])
규제 준수:
- GDPR/PIPA: 사용자 동의 없이 학습 데이터 사용 시 opt-out 메커니즘 필수
- 데이터 보관 기간: 학습 완료 후 90일 이내 삭제 (정책 설정)
- Audit Log: 모든 PII 탐지·익명화 이벤트를 CloudTrail/Audit DB에 기록
Stage 4: Train — Preference Tuning
목표: 고품질 trace를 사용해 모델을 강화학습으로 재학습한다.
실행 주기: 주간(Weekly) 또는 월간(Monthly)
입력: S3 Iceberg 테이블 (preference pairs)
출력: Candidate 모델 체크포인트
Preference Pair 구성:
Self-improving loop는 "동일 질문에 대한 여러 응답" 중 reward가 높은 것을 preferred, 낮은 것을 rejected로 사용한다.
def build_preference_pairs(traces):
"""동일 question에 대한 trace들을 묶어 pair 생성"""
grouped = defaultdict(list)
for trace in traces:
grouped[trace.question].append(trace)
pairs = []
for question, trace_list in grouped.items():
if len(trace_list) < 2:
continue # pair 불가
# Reward 기준 정렬
sorted_traces = sorted(trace_list, key=lambda t: t.reward_score, reverse=True)
# Top 1 vs Bottom 1 pair
preferred = sorted_traces[0]
rejected = sorted_traces[-1]
# Reward 차이가 충분히 커야 유의미한 pair
if preferred.reward_score - rejected.reward_score < 0.2:
continue
pairs.append({
"prompt": question,
"chosen": preferred.answer,
"rejected": rejected.answer,
"reward_diff": preferred.reward_score - rejected.reward_score
})
return pairs
학습 방법 선택 가이드:
| 방법 | 데이터 요구량 | GPU-hours (7B 모델) | 수렴 안정성 | 적합 시나리오 |
|---|---|---|---|---|
| GRPO | 1k+ pairs | ~50 (4×H100) | ⭐⭐⭐ | 초기 self-improvement, 빠른 iteration |
| DPO | 5k+ pairs | ~200 (8×H100) | ⭐⭐⭐⭐ | 충분한 데이터 확보 후, 안정적 학습 |
| RLAIF | 10k+ pairs + reward model | ~500 (8×H100) | ⭐⭐ | 복잡한 reward 모델링 필요 시 |
| RFT | 10k+ high-quality traces | ~300 (8×H100) | ⭐⭐⭐⭐⭐ | Supervised 학습 가능한 golden dataset 확보 시 |
- 초기 (데이터 <2k pairs): GRPO — 가장 빠르고 적은 데이터로 효과
- 중기 (데이터 5k-10k pairs): DPO — 안정성과 효과의 균형
- 성숙기 (데이터 >10k): RLAIF 또는 RFT — 복잡한 reward 모델링
GRPO 학습 예시 (NeMo-RL):
from nemo.collections.nlp.models.language_modeling import MegatronGPTSFTModel
from nemo_aligner.algorithms.grpo import GRPOTrainer
# Base model 로드
model = MegatronGPTSFTModel.restore_from("qwen3-7b-base.nemo")
# GRPO 설정
grpo_config = {
"num_rollouts": 4, # 질문당 4개 응답 생성
"kl_coef": 0.05, # KL divergence penalty (policy drift 방지)
"clip_range": 0.2,
"learning_rate": 1e-6,
"batch_size": 16,
"gradient_accumulation": 4,
}
trainer = GRPOTrainer(model=model, config=grpo_config)
# 학습 실행
trainer.fit(train_dataset=preference_pairs, val_dataset=golden_dataset)
# 체크포인트 저장
model.save_to("qwen3-7b-grpo-2026-04-18.nemo")
DPO 학습 예시 (TRL):
from transformers import AutoModelForCausalLM, AutoTokenizer
from trl import DPOTrainer, DPOConfig
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-7B-Instruct")
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-7B-Instruct")
dpo_config = DPOConfig(
beta=0.1, # Temperature for DPO loss
learning_rate=5e-7,
per_device_train_batch_size=2,
gradient_accumulation_steps=8,
max_length=2048,
num_train_epochs=1,
)
trainer = DPOTrainer(
model=model,
args=dpo_config,
train_dataset=preference_dataset,
tokenizer=tokenizer,
)
trainer.train()
model.save_pretrained("qwen3-7b-dpo-2026-04-18")
학습 모니터링:
# Wandb 연동으로 실시간 메트릭 추적
import wandb
wandb.init(project="self-improving-agent", name="grpo-2026-04-18")
# 추적 메트릭
- Reward mean/std (배치별)
- KL divergence (base model 대비 policy drift)
- Loss curve
- Validation accuracy (golden dataset)
- Training time per epoch
비용 추정 (Qwen3-7B, 5k pairs, DPO):
- GPU: 8× H100 × 25시간 = 200 GPU-hours
- 클라우드 비용 (p5.48xlarge, $98.32/hr): ~$2,458
- 비교: 매주 학습 시 월 $10k, 월간 학습 시 월 $2.5k
Stage 5: Deploy — 회귀 검증 & 점진 배포
목표: 새로 학습된 모델이 기존 대비 퇴화하지 않았는지 검증 후 프로덕션에 배포한다.
실행 주기: 학습 완 료 후 1회
입력: Candidate 모델 체크포인트
출력: 프로덕션 배포 또는 롤백
Golden Dataset 평가:
from ragas import evaluate
from datasets import Dataset
# Golden Dataset (도메인 전문가가 검증한 100-200개 QA)
golden_data = load_golden_dataset("s3://golden-eval/agent-qa-v2.jsonl")
# Baseline 모델 평가
baseline_results = evaluate_model(baseline_model, golden_data)
# Candidate 모델 평가
candidate_results = evaluate_model(candidate_model, golden_data)
# 통계 비교
from scipy.stats import ttest_rel
t_stat, p_value = ttest_rel(baseline_results, candidate_results)
if p_value < 0.05 and mean(candidate_results) > mean(baseline_results):
print("✅ Candidate 모델이 통계적으로 유의하게 우수")
decision = "PROCEED_TO_SHADOW"
elif mean(candidate_results) < mean(baseline_results) * 0.95:
print("❌ 5% 이상 퇴화 감지 → 롤백")
decision = "ROLLBACK"
else:
print("⚠️ 유의미한 차이 없음 → 추가 검증 필요")
decision = "MANUAL_REVIEW"
Shadow Test (5% 트래픽):
# Inference Gateway 설정 (LiteLLM + Feature Flag)
from ldclient import LDClient, Context
ld_client = LDClient(sdk_key="sdk-key")
def select_model(user_id: str) -> str:
context = Context.builder(user_id).kind("user").build()
variant = ld_client.get_variant("agent-model-shadow-test", context)
# 95% baseline, 5% candidate (shadow)
return "qwen3-7b-baseline" if variant.name == "control" else "qwen3-7b-candidate"
# Shadow 응답은 로깅만, 사용자에게는 baseline 반환
async def execute_with_shadow(query: str, user_id: str):
baseline_task = agent_call(model="qwen3-7b-baseline", query=query)
candidate_task = agent_call(model="qwen3-7b-candidate", query=query, shadow=True)
baseline_resp, candidate_resp = await asyncio.gather(baseline_task, candidate_task)
# 비교 로깅
log_shadow_comparison(query, baseline_resp, candidate_resp)
return baseline_resp # 사용자에게는 baseline만
회귀 모니터링 (24시간):
# Prometheus 쿼리: Candidate vs Baseline 에러율
rate(agent_errors_total{model="candidate"}[1h]) / rate(agent_requests_total{model="candidate"}[1h])
vs
rate(agent_errors_total{model="baseline"}[1h]) / rate(agent_requests_total{model="baseline"}[1h])
# Latency P99
histogram_quantile(0.99, rate(agent_latency_bucket{model="candidate"}[1h]))
vs
histogram_quantile(0.99, rate(agent_latency_bucket{model="baseline"}[1h]))
# User Feedback 비율
sum(rate(user_feedback_positive{model="candidate"}[1h])) / sum(rate(user_feedback_total{model="candidate"}[1h]))
자동 롤백 트리거:
# Prometheus AlertManager
- alert: CandidateModelRegression
expr: |
(rate(agent_errors_total{model="candidate"}[30m])
/ rate(agent_requests_total{model="candidate"}[30m]))
> 1.5 *
(rate(agent_errors_total{model="baseline"}[30m])
/ rate(agent_requests_total{model="baseline"}[30m]))
for: 30m
annotations:
summary: "Candidate 모델 에러율 1.5배 증가 → 자동 롤백"
# Webhook → Lambda → LaunchDarkly API (variant weight를 0%로 변경)
Canary 배포 (Shadow 성공 시):
# LaunchDarkly 콘솔에서 점진적 비율 증가
# Day 1: 5% (shadow) → 5% (live)
# Day 2: 25%
# Day 3: 50%
# Day 4: 100%
# 각 단계마다 24시간 모니터링 → 회귀 없으면 다음 단계