나는 고의로 Bad Decision System을 구축했다 (당신이 직접 만들지 않게)
I’m happy to translate the article for you, but I need the full text of the post (excluding the source line you already provided) in order to do so. Could you please paste the content you’d like translated? Once I have the text, I’ll keep the source link at the top and translate the rest into Korean while preserving all formatting, markdown, and technical terms.
작업: 동일한 문제, 두 구현
두 시스템 모두 정확히 동일한 문제를 해결합니다:
입력 텍스트 → 키워드 추출 → 점수 계산 → 행동 추천
행동 공간은 의도적으로 작게 설정되었습니다:
WAIT_AND_SEEBUY_MORE_STOCKPANIC_REORDER
작업을 간단하게 유지함으로써 모델 품질이 아니라 시스템 동작에 전적으로 집중할 수 있습니다.
벤치마크 아이디어
벤치마크는 의도적으로 최소화되었습니다:
- 단일, 고정된 입력 텍스트를 사용한다.
- 시스템을 여러 번 실행한다.
- 출력이 안정적인지 관찰한다.
왜 중요한가: 한 번만 작동하는 시스템은 시스템이 아니다 — 우연일 뿐이다. 같은 입력이 다른 출력을 만든다면, 시스템 수준에서 근본적인 문제가 있다.
벤치마크 결과: BAD vs GOOD
다음 결과는 동일한 입력을 두 시스템 모두에서 다섯 번 실행하여 얻은 것입니다.
BAD 시스템 출력 (발췌)
BAD 시스템은 점진적으로 결정이 강화됩니다:
| 실행 | 점수 | 동작 |
|---|---|---|
| 1 | 14 | WAIT_AND_SEE |
| 3 | 42 | BUY_MORE_STOCK |
| 5 | 74 | PANIC_REORDER |
동일한 입력. 동일한 키워드. 완전히 다른 결정.
집계된 벤치마크 요약
BAD system
- 실행 횟수: 5
- 고유 점수: 5 →
[14, 28, 42, 58, 74] - 고유 동작: 3
GOOD system
- 실행 횟수: 5
- 고유 점수: 1 →
[14, 14, 14, 14, 14] - 고유 동작: 1
GOOD 시스템은 순수 함수처럼 동작합니다. BAD 시스템은 메모리 누수처럼 동작합니다.
Source: …
Failure Taxonomy: How the BAD System Breaks
BAD 시스템은 한 가지 명확한 방식으로만 실패하지 않습니다. 대신 실제 AI 및 데이터 시스템에서 흔히 나타나는 여러 상호작용하는 실패 모드를 보여줍니다. 이러한 실패 모드에 이름을 붙이면 감지하기 쉬워지고, 실수로 배포되는 것을 방지할 수 있습니다.
1️⃣ Drift
- 정의: 입력이 정확히 동일한 상태에서도 시스템의 출력이 시간이 지남에 따라 변합니다.
- 근본 원인: 실행마다 누적되는 전역 점수; 리셋 없이 단조롭게 증가하는 상태.
- 위험성:
- 명시적인 변경 없이 비즈니스 로직이 변형됩니다.
- 과거 실행 순서가 현재 결정에 영향을 미칩니다.
- 값이 “합리적”으로 보이기 때문에 모니터링 대시보드가 문제를 놓치는 경우가 많습니다.
Drift는 특히 학습처럼 보이지만 실제로는 그렇지 않기 때문에 위험합니다.
2️⃣ Non‑determinism
- 정의: 동일한 입력이 서로 다른 출력을 생성합니다.
- 근본 원인: 점수 산정에 삽입된 무작위 노이즈; 실행 이력에 대한 암묵적 의존.
- 위험성:
- 버그를 신뢰성 있게 재현할 수 없습니다.
- 테스트 실패가 불안정해지고 신뢰를 잃습니다.
- A/B 실험이 통계적 의미를 상실합니다.
결정을 재현할 수 없으면 디버깅도 할 수 없습니다.
3️⃣ Hidden State
- 정의: 함수가 인터페이스나 입력에 보이지 않는 데이터를 사용합니다.
- 근본 원인:
CURRENT_SCORE,LAST_TEXT,RUN_COUNT와 같은 전역 변수. - 위험성:
- 코드를 로컬에서 이해할 수 없습니다.
- 리팩터링 시 비정상적인 방식으로 동작이 바뀝니다.
- 새로운 기여자가 무의식적으로 회귀를 도입합니다.
숨겨진 상태는 모든 함수 호출을 추측 게임으로 만듭니다.
4️⃣ Silent Corruption
- 정의: 시스템은 오류 없이 계속 실행되지만, 결정이 점점 더 잘못됩니다.
- 근본 원인: 명시적인 실패 신호가 없으며, 불변식이나 정상성 검사가 없습니다.
- 위험성:
- 잘못된 출력이 하위 시스템으로 전파됩니다.
- 문제는 비즈니스 영향으로만 드러납니다.
- 롤백이 어렵거나 불가능해집니다.
시끄러운 실패는 고쳐지지만, 조용한 실패는 배포됩니다.
왜 이 분류 체계가 중요한가
이러한 실패 모드는 거의 단독으로 나타나지 않는다. BAD 시스템에서는 서로를 강화한다:
- 숨겨진 상태가 드리프트를 가능하게 한다.
- 드리프트가 비결정성을 증폭시킨다.
- 비결정성이 무음 손상을 숨긴다.
이러한 패턴을 이해하는 것이 단일 버그를 수정하는 것보다 더 가치 있다—왜냐하면 동일한 분류 체계가 훨씬 크고 복잡한 AI 시스템에도 적용되기 때문이다.
단일 지표: 안정성 점수
시스템 동작을 요약하기 위해 단일 지표를 사용했습니다:
stability_score = 1 - (unique_scores / runs)
- 1.0 → 완벽히 안정적
- 0.0 → 완전히 불안정
안정성 결과
| 시스템 | 안정성 점수 |
|---|---|
| BAD | 0.0 |
| GOOD | 0.8 |
이 한 숫자만으로도 어느 시스템을 신뢰할 수 있는지 알 수 있습니다.
최소 수정: 모든 것을 바꾸는 네 개의 작은 패치
이것은 재작성이 아닙니다. 수술적 변경입니다. 각 패치는 새로운 추상화나 프레임워크를 도입하지 않고 전체 클래스의 실패 모드를 제거합니다.
패치 1 — 전역 상태 제거
Before (BAD):
# global mutation + history dependence
GS.CURRENT_SCORE += base
return GS.CURRENT_SCORE
After (GOOD):
def score_keywords(keywords, text):
return sum(len(w) % 7 for w in keywords) + len(text) % 13
이것이 해결하는 문제
- 점수 드리프트를 없앱니다.
- 숨겨진 히스토리 의존성을 제거합니다.
- 함수가 결정적이고 테스트 가능해집니다.
전역 상태에 의존하는 함수는 함수가 아니라 메모리 누수입니다.
패치 2 — 부작용을 경계로 이동
Before (BAD):
def extract_keywords(text):
print("Extracting keywords...")
open("log.txt", "a").write(text)
return tokens[:k]
After (GOOD):
def extract_keywords(text):
# Pure computation – no I/O, no printing
return tokenize(text)[:k]
이것이 해결하는 문제
- 실행을 비결정적으로 만드는 숨겨진 I/O 부작용을 제거합니다.
- 로깅을 핵심 로직과 분리합니다(예: 데코레이터나 래퍼를 통해).
패치 3 — 불변 조건 강제
Before (BAD):
def compute_score(keywords):
# No sanity checks
return sum(len(k) for k in keywords) * random.random()
After (GOOD):
def compute_score(keywords):
assert all(isinstance(k, str) for k in keywords), "Keywords must be strings"
base = sum(len(k) for k in keywords)
return base # deterministic, no random factor
이것이 해결하는 문제
- 손상된 입력을 조기에 감지합니다.
- 점수가 예상 범위 내에 머물도록 보장합니다.
패치 4 — 실행당 상태 초기화
Before (BAD):
RUN_COUNT += 1 # global counter never reset
CURRENT_SCORE += 5 # accumulates across runs
After (GOOD):
def run_pipeline(text):
# Local state only
keywords = extract_keywords(text)
score = compute_score(keywords)
action = decide_action(score)
return {"score": score, "action": action}
이것이 해결하는 문제
- 각 호출이 독립적임을 보장합니다.
- 실행 간 드리프트와 숨겨진 상태를 제거합니다.
Additional Patch Details
Patch 3 — Make Dependencies Explicit
Before (BAD):
if GS.LAST_TEXT is not None:
base += len(GS.LAST_TEXT) % 13
After (GOOD):
def score_keywords(keywords, text):
base = sum(len(w) % 7 for w in keywords)
return base + (len(text) % 13)
What this fixes
- 숨겨진 입력이 없음.
- 데이터 흐름이 명확함.
- 안전한 리팩터링.
Patch 4 — Name the Magic Numbers
Before (BAD):
if score > 42:
action = "PANIC_REORDER"
After (GOOD):
@dataclass(frozen=True)
class Config:
panic_threshold: int = 42
if score > cfg.panic_threshold:
action = "PANIC_REORDER"
What this fixes
- 결정이 설명 가능해짐.
- 매개변수가 검토 가능해짐.
- 동작 변경이 의도적으로 이루어짐.
요약
- 숨겨진 상태 제거
- 비결정성 제거
- 행동을 설명 가능하게 만들기
- 시스템에 대한 신뢰 회복
최종 요약
The BAD system works. That’s the problem.
It fails in the most dangerous way possible: plausibly and quietly.
The GOOD system is boring, predictable, and easy to reason about — which is exactly what you want in production.
Working code is not the same as a working system.
코드 및 재현성
이 글에서 사용된 모든 코드 — 의도적으로 깨진 시스템, 깔끔한 구현, 그리고 벤치마크를 포함 — 는 GitHub에서 확인할 수 있습니다:
👉 https://github.com/Ertugrulmutlu/I-Intentionally-Built-a-Bad-Decision-System-So-You-Don-t-Have-To
결과를 재현하고 싶다면 다음을 실행하세요:
python compare.py
벤치마크는 동일한 입력을 두 시스템에 여러 번 실행하고, 몇 줄의 출력으로 예측 가능성이 화려한 추상화보다 왜 더 중요한지를 보여줍니다.