LLM은 설명하고, 코드가 결정한다
요약
LLM이 직접 판단하게 하는 대신, 결정론적인 Python 코드가 운영 결정을 내리고 LLM은 그 결과를 설명하는 역할만 수행하도록 설계하는 방식의 이점을 설명합니다. 이를 통해 데이터 일관성을 유지하고 환각 현상을 방지하며 모니터링 파이프라인의 안정성을 확보할 수 있습니다.
핵심 포인트
- LLM의 판단 대신 결정론적 코드를 통한 분류 및 검증 수행
- LLM은 코드의 결정 결과를 이해하기 쉬운 문장으로 설명하는 역할에 집중
- 자유 형식의 모델 출력을 방지하여 데이터 집계 및 일관성 유지
- 모델 실패 시에도 모니터링 파이프라인의 작동 안정성 보장
현재 제가 보고 있는 대부분의 "관측 가능성을 위한 AI (AI for observability)" 작업들은 언어 모델 (Language Model)에게 판단을 맡깁니다. 저는 그것이 거꾸로 되었다고 생각합니다. 모델에게 경고 (Alert)를 제공하고, 몇 가지 메트릭 (Metrics)을 제공한 뒤, 무엇이 잘못되었는지, 무엇을 해야 하는지 묻고 모델이 판단을 내리게 하십시오. 언어 모델과 작업해 온 제 경험에 비추어 볼 때, 저는 이 과정을 뒤집는 것이 더 나은 결과를 제공한다는 결론을 내렸습니다.
요약하자면 이렇습니다. 저의 경고 파이프라인 (Alerting pipeline)에서 허용 가능한 분류 (Classifications) 집합은 결정론적인 (Deterministic) Python으로 고정되어 있으며, 모델은 그중 하나를 선택해야 합니다. LLM의 유일한 역할은 구조화된 판결을 이해하기 쉬운 문장으로 바꾸는 것입니다. 모델은 어떤 것이 나쁜지, 얼마나 나쁜지, 또는 어떤 범주의 문제인지 결코 결정하지 않습니다. 모델은 코드가 이미 잠가 놓은 결정 공간 (Decision space) 안에서 설명(Narrate)할 뿐입니다.
요약 (TL;DR): LLM이 경고의 무엇이 잘못되었는지 결정하게 하는 대신, 저는 결정론적인 Python이 모든 운영 결정을 내리게 하고 모델은 그 결과를 쉬운 영어로 설명하도록 제한합니다. 코드가 분류 (Classify), 검증 (Validate), 집계 (Aggregate)를 수행하며, LLM은 오직 설명만 합니다. 이를 통해 데이터의 일관성을 유지하고, 환각 (Hallucination)된 분류를 방지하며, 모델이 실패하더라도 모니터링 파이프라인이 계속 작동하도록 보장합니다.
문제점
저는 소규모 관리형 모니터링 서비스를 운영하고 있습니다. Alertmanager가 발화하면 웹훅 (Webhook)이 도착하는데, 역사적으로 이 웹훅은 'web-vm 호스트의 HighMemoryUsage, 심각도 warning'과 같은 한 줄을 생성했습니다. 이는 정확하긴 하지만 그다지 도움이 되지는 않습니다. 이를 읽는 사람은 여전히 HighMemoryUsage가 무엇을 의미하는지, 이 호스트가 항상 과열 상태인지, 그리고 신경을 써야 하는지 여부를 알아야 합니다. 저는 경고 전달 프로세스를 변경하지 않으면서 경고에 평이한 영어 문맥 (Context)이 첨부되기를 원했습니다.
가장 명백한 방법은 경고(alert) 전체를 LLM에 던져주고 설명을 요구하는 것이었습니다. 저는 이 실험의 첫 번째 반복(iteration) 단계에서 어느 정도 정확하되 완전히 신뢰할 수는 없을 것이라 예상하며 이를 시도해 보았는데, 결과는 실망시키지 않았습니다. 모델은 자신만만하게 일관성 없는 모습을 보였습니다. 동일한 경고를 세 번 실행했을 때, 세 가지 서로 다른 "근본 원인 (root cause)" 카테고리가 생성되었습니다. 한 번의 실행에서는 테스트 경고를 "설정 또는 구성 문제 (Configuration or setup issue)"라고 불렀고, 다음에는 "설정/테스트 (Configuration/Testing)"라고 불렀으며, 그다음에는 또 다른 무언가로 불렀습니다. 만약 나중에 어떤 종류의 집계 (aggregation)를 하기 위해 해당 출력을 저장한다면 (저는 동일한 유형의 문제를 겪는 서로 다른 고객이 같은 주에 세 명이나 발생하는지 알고 싶어서 저장합니다), 자유 형식 (free-form)의 모델 출력은 노이즈로 파편화됩니다. 모델이 무작위로 변경하는 필드를 기준으로 그룹화하는 방식은 작동하지 않을 것입니다.
저는 처음부터 놀라운 결과를 얻지는 못할 것이며, 보기보다 더 어려울 것이라는 점을 어느 정도 알고 있었습니다. 그래서 조사를 시작했고, 설계를 뒤집기로 결정했습니다. 제 머릿속 한구석의 작은 목소리가 내내 옳았습니다. 모델이 결정을 내리게 두지 마세요.
분리 (The Split)
저는 중간에 단단한 벽이 있는 파이프라인을 구축했습니다.
결정론적 (Deterministic) 측면에서는 Python이 분류를 수행합니다. 출력값은 memory_pressure, cpu_saturation, disk_pressure, service_unavailability, network_issue, configuration_error, external_dependency, unknown의 8가지 값으로 구성된 열거형 (Enum)으로 제한됩니다. 이 필드는 오직 8개의 문자열 중 하나만 가질 수 있기 때문에, 저는 이 필드를 기준으로 집계 (Aggregate)합니다. 만약 어떤 것도 일치하지 않는다면 정답은 unknown이 되며, 이는 환각 (Hallucination)이나 변형된 추측이 아닌 그 자체로 유용한 신호가 됩니다.
서사적 (Narrative) 측면에서는 LLM (llama3:8b, 제 로컬 LAN에 있는 장비에서 로컬로 실행되어 데이터/네트워크가 안전함)이 해당 고정된 8가지 값 세트 중에서 분류를 선택해야 하며, 그와 함께 두 개의 짧은 필드를 작성합니다. 하나는 경고 내용에 대한 평이한 영어 설명이고, 다른 하나는 운영상 무엇을 의미하는지에 대한 설명입니다. 코드가 답변의 형태를 정의하며, 모델은 이미 존재하는 슬롯을 채울 뿐입니다. 모델은 수정 사항을 제안하거나 원인을 지어내지 않도록 명시적으로 지시받으므로, 분석 대신 번역을 수행하게 됩니다.
프롬프트는 문법이 제약된 엄격한 JSON을 반환하므로, 저는 매번 {what, means, likely_cause_class}를 받게 되며, 열거형 (Enum) 값은 출력 과정에서 허용된 세트와 대조되어 검증됩니다. 만약 모델이 목록에 없는 값을 반환하면, 행을 저장하는 대신 버그를 포착합니다.
컨텍스트 수화 (Context hydration)
서사 단계조차도 단순하게 구현하면 지나치게 경고조인 문장이 만들어집니다. 시스템을 튜닝하던 중 DiskFillPredicted 경고를 받았는데, 겉보기에는 조사할 가치가 있어 보였습니다. 하지만 해당 호스트를 확인해보니 디스크 사용량 베이스라인이 몇 달 동안 평탄하게 유지되고 있었습니다. 그 예측은 반올림 오차에 의한 것이었으며, "디스크가 곧 가득 찰 것입니다"라는 문구는 적극적으로 오해를 불러일으키는 표현이었습니다. 모델은 경고만으로는 그 사실을 알 방법이 없었기에, 그냥 무언가를 작성해버린 것입니다.
저는 사람이 반응하기 전에 살펴보는 것과 동일한 컨텍스트 (Context)를 모델에게 제공함으로써 이 문제를 해결했습니다. LLM 호출 (LLM call) 이전에, Python은 해당 호스트의 최근 베이스라인 (Baseline)을 확인하기 위해 메트릭 백엔드 (Metrics backend)를 대상으로 빠른 조회를 수행하며, 프롬프트 (Prompt)에는 명시적인 규칙이 포함됩니다: 만약 과거의 컨텍스트가 제공된다면, 경고의 문자 그대로의 텍스트보다 이를 더 비중 있게 다루라는 것입니다. 수개월 동안 안정적인 베이스라인을 유지해 온 호스트에서 예측된 디스크 채움 (Predicted-disk-fill)은 긴급한 사항이 아니라 정보 제공 차원의 사항입니다.
해당 조회에 할당된 지연 시간 예산 (Latency budget)은 5초입니다. LLM 호출 자체는 약 80초가 소요되는데, 이는 GPU가 없는 16GB RAM을 갖춘 4세대 Intel i7과 같은 저사양 CPU 전용 하드웨어에서 의도적으로 실행되기 때문입니다. 이것은 제 제약 사항에 대한 변명이 아니라 하나의 선택입니다. 이 서비스의 전체적인 태도는 그 어떤 것도 제 LAN을 벗어나지 않는 것이며, 따라서 느린 로컬 모델이 빠른 원격 모델보다 낫습니다. 그리고 이 80초라는 시간은 경고를 받는 사람에게 결코 전달되지 않습니다. 어노테이터 (Annotator)가 기존 경로와 나란히 실행되기 때문에 (이에 대해서는 아래에서 자세히 설명하겠습니다), 원본 경고는 Slack과 이메일로 즉시 전달되며, 설명이 추가된 버전은 약 1분 뒤에 별도의 주석으로 나타납니다. 그 무엇도 모델을 기다리게 하지 않습니다. 80초의 호출 시간과 비교했을 때, 5초의 사전 가져오기 (Pre-fetch)는 7% 미만의 오버헤드이며 체감되지 않습니다. 가장 중요한 점은 메트릭 백엔드가 느리거나 접속할 수 없는 경우, 시스템이 즉시 실패하고 데이터가 보충되지 않은 (Un-hydrated) 경로로 전환되어, 정보 보강 (Enrichment) 단계가 결과 도출을 절대 차단할 수 없다는 것입니다.
Fail-Closed
그러한 폴백 (Fallback) 본능은 전체 프로세스에 흐르고 있습니다. 그 핵심 정신은 어노테이터 (Annotator)가 부가적이며, '있으면 좋은 (nice-to-have)' 존재라는 것입니다. Alertmanager는 continue: true 옵션과 함께 이를 라우팅하므로, 기존의 Slack 및 이메일 전달 프로세스와 나란히 작동하며 결코 이들을 차단할 수 없습니다. 웹훅 (Webhook)은 LLM 박스가 다운되었거나 JSON 형식이 잘못되었을 때도 항상 200을 반환하며, 대신 정적인 폴백 어노테이션 (Fallback annotation)이 사용됩니다. 최악의 경우 최종 사용자가 덜 화려한 알림을 받게 될 뿐, 알림 자체를 놓치는 일은 절대 없습니다. 내레이션 (Narration)은 그 자체에 전혀 의존하지 않는 전달 경로 위에 계층화된 편의 기능입니다.
조언 (Advice, For What it's Worth)
"모델이 분석하게 하지 말고, 설명하게 하라"는 교훈의 쉬운 버전이며, 이는 사실이지만 그것이 어려운 부분은 아닙니다. 진짜 어려운 부분은 강제 (Enforcement)하는 것입니다. 즉, 출력을 검증 가능한 고정된 집합으로 제한하고, 모델을 크리티컬 패스 (Critical path)에서 제외하여 모델의 실패가 문장력을 떨어뜨릴 수는 있어도 알림 자체를 놓치게 해서는 안 된다는 것입니다. 모델의 가치는 판단력이 아니라 유창성 (Fluency)에 있으며, 유창성은 스택 (Stack) 내에서 가장 교체하기 쉬운 요소입니다. 모든 실제 결정은 테스트 가능한 코드로 밀어 넣고, 나중에 열거형 (Enum)으로 집계할 모든 것은 제한하며, 모델을 파이프라인 (Pipeline)의 마지막이자 가장 교체하기 쉬운 단계로 취급하십시오. 만약 내일 당장 모델을 교체하더라도 데이터가 깨끗하게 유지된다면, 경계선을 올바른 곳에 그은 것입니다.
제 시스템은 몇 주 동안 실제 인프라에서 실행되었습니다. 문장은 훌륭하며, 하드웨어가 업그레이드되고 더 강력한 모델이 도입되면 더 좋아질 것이라 확신합니다. 하지만 제가 (조심스럽게) 이 시스템을 신뢰하는 이유는 문장이 작업을 수행하고 있지 않기 때문입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기