LLM 할루시네이션을 방지하는 애플리케이션 상태 관리와 외재화 고민하기

궁금하면 500원·2026년 4월 3일

AI 미생지능

목록 보기
91/94

LLM 시스템의 한계를 극복하는 에이전트 아키텍처와 상태 관리 고민하기

최근 생성형 AI와 대형 언어 모델을 활용한 서비스 개발이 급격하게 늘어나고 있습니다.

하지만 단순한 챗봇 단계를 넘어 복잡한 비즈니스 로직을 처리하는 'AI 솔루션'을 구축하다 보면 반드시 한계에 부딪히게 됩니다.

모델의 무상태성, 컨텍스트 제한, 그리고 할루시네이션 때문입니다.

이러한 문제를 애플리케이션 계층에서 어떻게 엔지니어링 측면으로 해결하고, 더 나아가 자율적인 'AI 에이전트'로 확장할 수 있는지 그 구조적 해법을 상세히 살펴보겠습니다.


1. 모델의 무상태성과 상태 관리의 주체

기본적으로 대형 언어 모델은 구조상 무상태 구조를 가집니다.
즉, 모델 자체는 이전 요청이나 대화 내용을 전혀 기억하지 못합니다.

멀티턴 대화의 실제 구조

우리가 LLM과 주고받는 연속적인 대화는 모델이 기억력을 가지고 있어서 유지되는 것이 아닙니다. 클라이언트나 서버가 매 요청마다 기존의 모든 대화 기록을 포함하여 다시 전송하기 때문에 마치 상태가 유지되는 것처럼 보일 뿐입니다.

  • 매 요청은 상태 없이 항상 독립적으로 처리됩니다.
  • 이 과정에서 효율적인 처리를 위해 인프라 수준에서 KV 캐시 전략이 필수로 요구됩니다.

상태 유지의 주체와 API 스펙의 변화

결국 대화나 작업의 '상태'를 유지하는 것은 LLM 자체의 기능이 아니라, 모델을 감싸고 있는 애플리케이션 계층의 구현법에 달려 있습니다.
이는 크게 두 가지 방식으로 나뉩니다.

1) LLM 서버가 상태를 유지하는 경우

최근의 발전된 API 스펙 예: Assistants API나 Responses API 계열은 서버 측에서 대화 세션을 관리하는 방식을 지원합니다.

  • 클라이언트가 대화 세션을 개설하면 서버로부터 sessionID를 발급받습니다.

  • 이후 요청에서는 전체 히스토리를 보낼 필요 없이 sessionID와 이번 턴의 입력값만 전송합니다.

  • 장점: 서버측 컨텍스트 관리 로직을 개입시킬 수 있으며, 프리픽스 캐시 적중률을 극대화할 수 있습니다.

  • 단점: 서버 측에서 세션별 히스토리를 모두 보관해야 하므로 인프라 및 서버 측의 부담이 크게 증가합니다.

2) 클라이언트가 상태를 유지하는 경우

기존의 전통적인 chat/completions 스펙을 포함한 1세대 API 대부분이 채택하는 방식입니다.

  • 대화 턴이 쌓일수록 매 요청에 포함되는 텍스트의 크기가 단조 증가합니다.

  • 단점: 대화가 길어질수록 페이로드가 커져 네트워크 비용 부담이 증가합니다.
    다만, LLM의 응답 속도 자체가 본질적으로 느리기 때문에, 현대의 네트워크 대역폭 관점에서는 이 네트워크 비용이 아주 치명적인 문제로 작용하지는 않는 편입니다.

  • 장점: 서버 구현체와 무관하게 클라이언트 사이드에서 독립적인 컨텍스트 관리 정책 예: 슬라이딩 윈도우, 요약본 대체 등 을 수립할 수 있습니다.


2. 할루시네이션 방지와 외재화

단순 대화 기록을 넘어 애플리케이션의 특정 변수나 시스템 상태를 LLM의 컨텍스트 안에서 관리하려고 하면 심각한 문제가 발생합니다.

프롬프트 내 상태관리의 취약점

예를 들어 프롬프트 내에 "지금 변수 a의 상태는 13이다."라고 명시한 뒤, 복잡한 대화가 전개되면서 "현재는 15다."를 거쳐 "현재 a의 상태를 반영하여 x를 전개하라"는 식의 요청을 보낸다고 가정해 보겠습니다.

LLM은 본질적으로 텍스트 확률 모델이기 때문에 대화가 길어지고 복잡해질수록 문맥 속에서 실제 a의 정확한 최종 상태가 무엇인지 파악하는 데 실패하기 쉽습니다.
즉, 할루시네이션에 매우 취약해집니다.

도구를 통한 상태의 외재화

이 문제를 해결하려면 LLM의 텍스트 컨텍스트 내부에 상태를 가두지 말고, 외부 변수 및 도구 사용을 통해 상태를 외재화해야 합니다.

  • 도구 호출의 활용: LLM에게 setVariables, getVariables와 같은 도구를 제공하여, 모델이 직접 텍스트 상태를 바꾸는 것이 아니라 엄격한 코드 로직을 통해 외부 저장소의 변수를 조작하게 만듭니다.

  • 별도의 상태관리자 주입: 시스템 프롬프트나 메시지 배열 상단에 항상 현재의 정확한 상태 값을 애플리케이션 계층에서 강제로 주입해 주는 방식입니다.

{
  "role": "system",
  "content": "current variables: a=17, b=15"
}
  • 구조화된 응답: LLM이 작업을 마친 뒤 변경된 변수 상태를 신뢰할 수 있는 형태로 반환하도록 JSON 스키마를 강제하는 기법입니다.
{
  "type": "object",
  "properties": {
    "response": {"type": "string"},
    "a": {"type": "number"},
    "b": {"type": "number"}
  }
}

이렇게 아키텍처를 격리해야 모델의 확률적 불확실성에 시스템의 상태가 오염되는 것을 막을 수 있습니다.


3. 모델과 코드의 지분 균형

LLM 기반 솔루션을 설계할 때 가장 중요한 개념적 이정표는 '에이전트'의 도입입니다.

  • 에이전트의 정의: 전체 AI 솔루션 아키텍처에서 LLM이 아닌, 일반적인 소스 코드로 이루어진 프로그램 영역을 의미합니다.

  • 에이전틱 AI: 솔루션 내부에서 LLM에 대한 의존성은 상대적으로 낮추고, 결정론적인 코드의 의존성과 제어권을 높인 구조적 개념입니다.
    현실의 그 어떤 엔터프라이즈 솔루션도 모델 단독으로만 존재할 수는 없습니다.

결국 시스템 설계자는 애플리케이션 내부에서 '에이전트의 지분'과 '모델의 지분'을 어떻게 조율할지 결정해야 합니다.

1) 솔루션에서 에이전트 지분이 커질 때

  • 특징: LLM의 자율성 범위가 줄어들고, 사전에 정의되고 한정된 상황에만 대응하도록 설계됩니다.

  • 효과: 코드의 엄격함 덕분에 시스템의 정확성과 예측 가능성이 극대화됩니다.
    반복 구조나 상태 관리 구조를 명확히 설계하여 복잡한 비즈니스 업무를 안전하게 분할 정복할 수 있습니다.

  • 발전성: 모델 자체는 고정되어 불변일지라도, 이를 감싸는 에이전트의 프롬프트가 지속적으로 증강되고 제어 흐름이 고도화되므로 시스템 전체가 계속 발전하는 솔루션을 구축할 수 있습니다.

2) 솔루션에서 모델 지분이 커질 때

  • 특징: 전반적인 시스템의 성능과 커버리지가 사용하는 LLM 자체의 스펙과 추론 능력에 완전히 종속됩니다.

  • 효과: 최신 고성능 모델들은 과거 코드로 짜야 했던 에이전트 기능 자체를 내재적으로 학습하고 있으므로, 복잡한 파이프라인 없이 단순 멀티턴만으로 문제를 해결하기도 합니다.

  • 확장성: 특히 에이전트가 처리하던 기능들을 '도구 사용' 형태로 치환하여 모델에 넘기면, 모델이 자율적으로 도구 사용 학습을 기반으로 판단 내릴 수 있습니다.

더 나아가 모델에게 쉘 명령어 실행 권한이나 파일 시스템 접근 권한을 부여하게 되면, 사실상 제어의 외재화마저 자율적으로 활용하는 고도의 독립적 에이전트가 완성됩니다.


4. Task 중심의 아키텍처 설계와 비동기 제어

에이전트가 복잡한 업무를 수행하기 위해 핵심적으로 다루어야 하는 단위가 바로 Task입니다. Task는 애플리케이션이 처리해야 할 모든 작업 단위를 추상화한 것입니다.

직렬성과 병렬성의 조합

대다수의 비즈니스 로직은 순차적으로 처리해야 하는 직렬성 작업과, 동시에 처리할 수 있는 병렬성 작업의 조합으로 구성됩니다.

  • 일반적으로 복잡한 직렬 흐름의 단계 중 일부가 병렬화되었다가, 처리가 완료되면 다시 하나의 직렬 흐름으로 복귀하는 패턴을 가집니다.

  • 고도화된 에이전트 시스템에서는 이것이 단순한 선형 파이프라인을 넘어 복잡한 그래프 형태의 작업 관리 구조로 확장됩니다.

비동기 작업 관리자와 에이전트 루프

LLM의 추론을 기다리는 동기식 대기는 결국 CPU 블로킹을 유발하여 자원 낭비를 초래합니다.
따라서 비동기 대기 구조가 훌륭한 대안이 됩니다.

  • 구조: 비동기적인 작업 관리자는 직렬 및 병렬로 분류된 작업들을 작업 매니저에 등록합니다. 매니저는 등록된 비동기 작업들이 전부 완료되었는지 여부를 추적하고 애플리케이션에 알립니다.

  • 에이전트 루프: 에이전트 내부의 실행 루프는 작업 관리자가 등록된 모든 Task를 완료 처리했다고 보고할 때까지 해당 턴을 종료하지 않고 유지하며 제어권을 쥡니다.

  • 모델 훈련: 이 수준에 도달하려면 에이전트에 사용되는 LLM 역시 범용 모델을 넘어 도구 목록 중 특히 Task 계열의 도구를 적절한 시점에 호출하고 다루는 방법에 대해 사전 학습 또는 파인튜닝이 되어 있어야 합니다.


5. Task의 실제 활용과 프롬프트 해체

실무에서 이러한 Task 아키텍처를 어떻게 활용할 수 있을까요? 거대하고 복잡한 프롬프트 하나로 모든 문제를 해결하려 하지 말고, 단계를 쪼개어 접근해야 합니다.

[복잡한 지시사항 수신]
        │
        ▼ (프롬프트 해체)
┌───────────────────────────────┐
│ Task 매니저 (비동기 큐 관리)  │
└───────┬───────────────┬───────┘
        │ (직렬 처리)    │ (병렬 처리)
        ▼               ▼
   [직렬 Task 1]    [병렬 Task A] ──▶ 여러 폴더 동시 검색
        │           [병렬 Task B] ──▶ 여러 웹사이트 동시 조회
        ▼               │
   [직렬 Task 2]        ▼
                    (결과 취합/Reduce)
  1. 프롬프트 해체: 복잡한 단일 지시가 들어오면 단계별로 처리할 수 있도록 프롬프트를 해체합니다. 각 단계를 독립적인 직렬 Task로 등록하여 순차적으로 해결해 나갑니다.

  2. 병렬 작업의 외부 위임: 모델이 직접 여러 사이트를 뒤적거리게 하지 않고, '여러 폴더를 동시에 검색'하거나 '여러 웹사이트의 정보를 동시에 스크랩'하는 작업을 병렬 큐 구조의 Task로 등록합니다.
    Task 매니저 수준에서 병렬 큐에 한 번에 등록된 Task들의 결과를 비동기로 수집 및 취합하는 기능을 내장하여 모델에 최종 전달합니다.

  3. 거대 단위의 위임: 단순히 개별 I/O 작업을 넘어서 더 큰 규모의 아키텍처 레이어도 하나의 Task로 다룰 수 있습니다.
    예컨대 특정 서브 에이전트를 구동하거나 서브 시스템을 실행하는 행위 자체가 하나의 병렬 Task가 되며, 넓게는 완전히 독립적인 코딩 에이전트 실행 전체 프로세스를 하나의 Task로 래핑하여 구동할 수도 있습니다.


6. 컨텍스트 제한 극복과 안정적인 인프라 구축 테스트

에이전트가 스스로 작업을 분해하고 Task를 생성하며 이를 감시하는 구조는 매우 강력하지만, 한 가지 치명적인 맹점이 있습니다. 바로 컨텍스트 제한에 의한 작업 파괴와 할루시네이션에 의한 실행 무시입니다.

작업이 장기화되면서 컨텍스트가 가득 차면 시스템 프롬프트에 정의된 중요한 실행 규칙이 밀려나 유실되거나, 모델이 중간 단계를 마음대로 누락하거나 허위로 완료되었다고 꾸며낼 위험이 있습니다.

코드 기반 검증의 병행

이 문제를 방지하려면 LLM의 자율적 프롬프트 수행에만 의존하지 말고, 강제로 집행되는 코드 기반의 제어 장치를 병행해야 합니다.

  • 스킬과 도구의 격리: 수행 능력을 정의하는 것은 프롬프트이지만, 그 프롬프트의 지시사항이 특정 도구를 거쳐 실제 시스템 쉘 레벨에서 엄격하게 실행되도록 구조적으로 가두어야 합니다.

  • 룰 기반 검증: 모델의 결과물이나 중간 과정이 올바른지 검증할 때 LLM에게 "이게 맞니?"라고 다시 물어보는 방식을 지양하고, 실제 애플리케이션 코드나 정규식을 이용한 룰 검증 단계를 파이프라인 중간에 강제로 끼워 넣어야 합니다.

실제 AI 테스트 시나리오 설계 자동화

이러한 에이전트 아키텍처의 견고함을 테스트하기 위한 단위 테스트 및 인수 테스트 시나리오 역시 고도화가 필요합니다.
과거 AI가 없던 시절의 단위 테스트는 고정된 입력과 기댓값을 비교하는 구형 테스트 방식에 머물렀습니다. 하지만 에이전트 환경에서는 단위 테스트부터 서비스의 최종 인수 테스트까지 전 구간의 테스트 지시를 LLM을 통해 유연하게 내릴 수 있습니다.

여기서도 테스트 항목이 방대해지면 모델이 검증을 누락하거나 결과를 꾸며내는 문제가 발생하므로 다음과 같은 아키텍처적 순환 구조를 설계하여 문제를 해결합니다.

┌────────────────────────────────────────────────────────┐
│                   애플리케이션 계층                    │
│  (Node.js / Python 등 런타임 환경)                     │
└──────────────────────────┬─────────────────────────────┘
                           │ 1. 테스트 항목 생성 요청
                           ▼
┌────────────────────────────────────────────────────────┐
│                        LLM                             │
│  - 테스트 항목 카테고리별 분할                         │
│  - 표준화된 JSON 배열 응답 생성                       │
└──────────────────────────┬─────────────────────────────┘
                           │ 2. JSON 구조화 데이터 반환
                           ▼
┌────────────────────────────────────────────────────────┐
│                   애플리케이션 계층                    │
│  - 반환된 JSON 배열 순회 (Loop)                        │
│  - 순차적으로 개별 테스트 지시를 LLM에 비동기 의뢰   │
└────────────────────────────────────────────────────────┘
  • 1단계: LLM에게 테스트하고 싶은 대략적인 요구사항을 준 뒤, 수행해야 할 구체적인 테스트 항목들을 카테고리별로 분류하여 표준화된 JSON 배열 형태로 생성하게 합니다.

  • 2단계: Node.js나 Python 등의 애플리케이션 런타임 환경에서 해당 JSON 데이터를 받아 옴으로써 데이터를 확정 짓습니다.

  • 3단계: 코드가 이 JSON 배열을 순회하면서, 한 번에 하나씩 구체적인 테스트 지시를 LLM에게 비동기적으로 의뢰합니다.
    이 루프는 정의된 테스트 항목이 모두 소진될 때까지 명확하게 반복됩니다.

이렇게 설계하면 컨텍스트 유실로 인한 테스트 누락을 원천 차단할 수 있으며, 아무리 복잡하고 거대한 엔터프라이즈 급 시나리오 시스템이라도 안정적이고 결정론적인 제어 구조 하에 완수할 수 있게 됩니다.


결국 고도화된 AI 서비스를 지탱하는 핵심은 LLM 모델 그 자체가 아니라, 모델의 무상태성과 불확실성을 보완하기 위해 애플리케이션 계층에서 촘촘하게 설계한 에이전트 아키텍처와 비동기 Task 관리 능력에 있습니다.
모델과 코드의 지분을 영리하게 나누는 시스템 디자인이야말로 프로덕션 환경에서 동작하는 안정적인 솔루션을 만드는 열쇠입니다.

profile
공부할게 많아졌어요

0개의 댓글