문서가 곧 명세서였다: 지친 부모와 대화하는 AI 앱을 위한 안전 계층 구축하기
요약
AI 웰니스 앱 FamNest를 개발하며 겪은 경험을 바탕으로, AI의 예측 가능성을 높이기 위한 안전 계층 구축 방법을 설명합니다. 모델의 유연함 대신 명확한 규칙과 검토 프로세스를 도입하여 사용자에게 신뢰할 수 있는 서비스를 제공하는 엔지니어링 원칙을 다룹니다.
핵심 포인트
- AI 제품에서 예측 가능성은 지연 시간보다 중요함
- 모델의 '영리함'은 통제되지 않을 경우 리스크가 될 수 있음
- 안전 검토자(Safety Reviewer) 모델을 통한 3단계 판결 시스템 도입
- 불연속적인 결과값(ok, revise, crisis)을 통한 명확한 제어 구조 설계
"AI 코치를 유연하게 설계하는 것을 멈추고, 글로 써 내려갈 수 있을 만큼 예측 가능하게 설계하기 시작했을 때 어떤 일이 일어났는가."
지난달, 나는 내 AI 앱에서 세 가지 기능을 삭제했습니다. 아무도 나에게 요청하지 않았습니다.
내가 그것들을 제거한 이유는 명확하게 설명할 수 없었기 때문입니다. 그리고 나는 이것을 강력한 신호로 받아들이기 시작했습니다. 만약 어떤 기능이 어떻게 작동하는지를 지친 부모도 이해할 수 있는 평이한 언어로 설명할 수 없다면, 그것은 완성된 것이 아니라 리스크(liability)입니다.
이 포스트는 그 규칙으로부터 나온 엔지니어링에 관한 것입니다. 이는 구체적이며, 코드를 포함하고 있으며, AI 제품을 만들 때 아무도 데모에서 보여주지 않는 부분입니다.
컨텍스트 (The context)
나는 부모를 위한 AI 웰니스 도구인 FamNest를 만들고 있습니다. 내가 설계하는 사용자는 새벽 3시에 수유 기록기를 새로고침하며, 한 손으로는 잠든 아기를 안고 다른 한 손으로는 "이게 정상인가요"를 구글링하는 사람입니다.
그러한 사용자는 당신의 엔지니어링 우선순위를 바꿉니다. 예측 가능성(predictability)이 지연 시간(latency)보다 더 중요합니다. "영리함(Clever)"은 기능이 아니라 리스크입니다. 그리고 당신의 AI가 그런 상태에 있는 사람에게 미묘하게 틀린 말을 하는 순간, 당신은 사용자를 놓치게 되며, 어쩌면 실제적인 해를 끼칠 수도 있습니다.
그래서 내가 계속해서 되풀이했던 질문은 "모델이 무엇을 할 수 있는가?"가 아니었습니다. 그것은 "모델이 절대로 즉흥적으로 행동해서는 안 되는 것은 무엇인가?"였습니다.
문제 1: 너무 많은 답변을 가진 코치
첫 번째 버전에서, 나의 AI 코치는 동일한 질문에 대해 수십 가지의 약간씩 다른 방식으로 응답할 수 있었습니다. 똑똑하게 느껴졌습니다. 심지어 유연해 보이기까지 했습니다.
하지만 그것은 사실 악몽이었습니다. 문서를 작성하기 위해 앉았을 때, 나는 시스템이 무엇을 할 것인지 부모에게 약속할 수 없었습니다. 그리고 내가 약속할 수 없다면, 그들이 왜 그것을 신뢰해야 할까요?
해결책은 사용자 앞에 두 번째 모델인 안전 검토자(safety reviewer)를 배치하고, 그것에게 정확히 세 가지 결과만을 부여하는 것이었습니다. 신뢰도 점수(confidence score)도 아니고, 자유 형식의 비평도 아닙니다. 세 가지의 불연속적인 판결(discrete verdicts)입니다.
type Verdict = "ok" | "revise" | "crisis";
interface ReviewResult {
verdict: Verdict;
reason: string; // 로깅용, 사용자에게는 절대 노출되지 않음
revisedDraft?: string; // verdict === "revise"인 경우에만 존재
}
코치(coach)가 초안을 생성합니다. 리뷰어(reviewer)가 이를 판단합니다. 전체 계약(contract)은 단 네 줄로 구성되며, 이것이 핵심입니다. 저는 이를 한 문장으로 문서화할 수 있습니다: 리뷰어는 초안을 승인하거나, 다시 쓰거나, 위기 대응(crisis response)으로 에스컬레이션(escalate)합니다.
async function generateReply(userMessage: string): Promise<string> {
const draft = await coach.respond(userMessage);
const review = await safetyReviewer.review(userMessage, draft);
switch (review.verdict) {
case "ok":
return draft;
case "revise":
return review.revisedDraft ?? draft;
case "crisis":
return CRISIS_RESPONSE; // 아래 참조 — 이는 생성되지 않음
}
}
문제 2: 위기 바닥(the crisis floor)
여기 제가 가장 확신하는 결정이 있습니다.
메시지가 위기를 암시할 때, 시스템은 응답을 생성하지 않습니다. 대신 고정된, 사람이 검토한 텍스트를 반환합니다. 매번, 단 한 바이트도 틀리지 않고 말입니다.
// 이 문자열은 사람이 검토하며, 버전 관리(version-controlled)되고,
// 사용자용 문서에 글자 그대로 기록됩니다.
const CRISIS_RESPONSE = `지금 정말 힘든 시간을 보내고 계신 것 같군요.
혼자 감당하지 않으셔도 됩니다. 만약 즉각적인 위험에 처해 있다면,
지역 응급 번호로 연락해 주세요...`.trim();
이는 우리가 생성형 AI(generative AI)에 대해 보통 생각하는 방식과는 정반대입니다. LLM(Large Language Model)의 매력은 새로운 무언가를 구성한다는 점에 있습니다. 하지만 부모가 응답이 정확하기를 가장 필요로 하는 상황은, 역설적으로 제가 모델의 창의성을 가장 원하지 않는 상황입니다.
저는 이를 '위기 바닥(crisis floor)'이라고 부릅니다. 시스템이 생성 과정을 통해 결코 그 아래로 내려갈 수 없는 결정론적(deterministic)인 기준선입니다. 모델은 이 바닥 위에서는 경험을 더 좋게 만들 수 있습니다. 하지만 바닥에서 일어나는 일에는 결코 관여할 수 없습니다.
미묘하지만 중요한 세부 사항이 있습니다: 이 바닥(floor)은 영리한 경로(clever path)를 거친 후가 아니라, 그 이전에 확인됩니다.
async function handleMessage(userMessage: string): Promise { // LLM과 독립적으로, 결정론적 가드레일 (Deterministic guardrail)이 먼저 실행됩니다. if (crisisFloor.matches(userMessage)) { return CRISIS_RESPONSE; } return generateReply(userMessage); } 만약 생성 파이프라인 (generative pipeline)이 다운되거나, 오작동하거나, 환각 (hallucinating)을 일으키더라도, 바닥 (floor)은 여전히 유지됩니다. 이는 시스템에서 가장 실패할 가능성이 높은 부분에 의존하지 않기 때문입니다.
문제 3: 제공자 (provider)가 무너질 때는 어떻게 되는가
LLM API는 다운될 수 있습니다. 속도 제한 (Rate limits)에 걸릴 수도 있습니다. 특정 지역에 문제가 생길 수도 있습니다. 대부분의 앱에게 이는 단순한 번거로움일 뿐입니다. 하지만 새벽 3시에 부모가 의지하는 앱에게, 빈 화면은 깨진 약속과 같습니다.
따라서 모든 외부 의존성 (external dependency)에는 알려진 폴백 (fallback)이 있어야 하며, "모델을 사용할 수 없음"은 스택 트레이스 (stack trace)로 떠오르는 예외 사항이 아니라, 문서화된 상태 (documented state)여야 합니다.
async function coachRespond(message: string): Promise { try { return await llm.complete(buildPrompt(message)); } catch (err) { logger.warn("LLM provider unavailable, degrading gracefully", { err }); // 안전하고 일반적인, 미리 작성된 답변입니다. 에러가 아닙니다. return GRACEFUL_FALLBACK; } } ext{} ext{} ext{}
사용자는 결코 끝나지 않는 로딩 스피너 대신, 차분하고 정직한 메시지를 받게 됩니다. 우아한 성능 저하 (Graceful degradation)는 여기서 있으면 좋은 기능이 아닙니다. 그것은 신뢰 계약 (trust contract)의 일부입니다.
내가 실제로 배운 것
세 가지 모두를 관통하는 패턴은 이것입니다: 문서화는 엔지니어링 후에 작성하는 것이 아니었습니다. 문서화 자체가 바로 엔지니어링이었습니다.
문서가 곧 명세서 (spec)가 되었습니다. 어떤 동작을 깔끔하게 글로 적을 수 없을 때 — 세 가지 판결, 고정된 위기 대응 텍스트, 이름이 지정된 폴백 상태 등 — 그것은 글쓰기의 문제가 아니라 설계가 잘못되었다는 신호였습니다.
"문서화할 수 있을 정도로 예측 가능하다"는 것은 훌륭한 설계 제약 조건임이 드러났습니다. 이는 당신을 작고 분리된 계약 (discrete contracts)을 향하도록 유도하며, 데모는 잘 되지만 실제 배포 시에는 엉망이 되는 식의 개방적이고 영리한 (open-ended cleverness) 방식으로부터 멀어지게 합니다.
많은 AI 제품들이 스스로 완전히 설명할 수 없는 동작을 출시하면서, 그 혼란을 조용히 "지능 (intelligence)"이라고 부르고 있다고 생각합니다. 제가 만드는 사용자들에게는 명확함 (clarity)이 곧 기능입니다.
문서화할 수 없다면, 아직 그것을 이해하지 못한 것입니다.
저는 프로덕션 (production) AI 시스템을 구축하고 문서화하는 것에 대해 글을 씁니다. 만약 여러분이 자랑스럽게 생각하는 가드레일 (guardrail)이나 폴백 (fallback)을 출시했거나, 혹은 그것 때문에 곤혹을 치른 경험이 있다면 — 저는 진심으로 그 이야기를 듣고 싶습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기