모델은 기억하지 않습니다. 당신이 기억해야 합니다.
요약
LLM의 작동 원리 중 컨텍스트 관리 방식에 대해 설명합니다. 모델 자체는 상태를 저장하지 않는 Stateless 방식이므로, 개발자가 대화 기록(History)을 배열 형태로 직접 관리하여 매 요청마다 전달해야 함을 강조합니다.
핵심 포인트
- LLM 모델은 자체적인 메모리가 없는 Stateless 방식임
- 대화의 맥락을 유지하려면 개발자가 전체 기록을 배열로 관리해야 함
- SDK의 추상화 없이 Raw Fetch를 통해 API 요청/응답 구조를 이해하는 것이 중요함
- 컨텍스트 관리는 결국 이전 메시지들과 최신 쿼리를 포함한 배열을 구성하는 과정임
서론 (Introduction)
LLM (Large Language Model)이 어떻게 작동하는지 깊이 파고들기 전에는, 각 채팅이 자체적으로 메모리나 컨텍스트 (Context)를 저장한다고 가정했습니다. 그것이 단지 모든 메시지가 추가된 하나의 배열 (Array)일 뿐이라는 것을 깨달은 순간, 저는 통제권을 얻은 듯한 기분이 들었습니다. 더 일찍 알았더라면 좋았을 것입니다. 이는 채팅 세션에서는 보이지 않습니다. Claude와 OpenAI는 정확한 컨텍스트 응답을 끌어내기 위해 수많은 실타래를 당깁니다. 이러한 실타래에 대해 먼저 알기 위해서는 SDK (Software Development Kit) 없이 raw fetch를 사용하여 LLM API와 작업하며, 요청/응답 (Request/Response) 사이클을 이해해야 했습니다.
파고들기 (Digging in)
우리는 탄탄한 기초를 쌓기를 원하므로, Anthropic SDK를 사용하지 않는 것은 우리가 인지하지 못할 수도 있는 추상화 (Abstraction)로부터 우리를 자유롭게 해줍니다. SDK는 관용적인 인터페이스 (Idiomatic interfaces), 타입 안정성 (Type safety), 그리고 스트리밍 (Streaming), 재시도 (Retries), 에러 핸들링 (Error handling)에 대한 내장 지원을 제공합니다. SDK가 없다면 아무것도 추상화되지 않습니다. 모든 결정이 눈에 보이며, 이것이 바로 핵심입니다.
보통 SDK를 사용하여 API를 호출하려면 다음과 같은 스크립트를 추가해야 합니다:
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
...
그리고 raw fetch를 사용하려면 헤더 (Headers)와 바디 (Body)를 직접 관리해야 합니다:
const URL = `https://api.anthropic.com/v1/messages`;
const res = await fetch(URL, {
...
놀랍게도 이 경로를 택하고자 할 때 문서화된 내용이 거의 없는데, 이유는 명확하지만 여전히 궁금한 부분입니다. 그리고 글쎄요, 이것은 단지 기본적인 요청 및 응답 역학에 관한 것입니다. 쿼리 (Query)를 보내고, LLM으로부터 응답을 받으면 그것으로 끝입니다. Messages API는 상태가 없는 (Stateless) 방식이므로, 요청을 보낼 때마다 항상 전체 대화 기록 (Conversation history)을 다시 보내야 합니다. 우리는 여러 번의 대화 턴 (Conversational turns)을 달성하고자 할 것입니다.
메모리에 대한 깨달음 (The memory realization)
우리가 관리해야 하는 이 "기록 (History)"에 대해 잠시 생각해보겠습니다. 여기서 여러분은 LLM 개발에서 가장 중요한 개념을 배우게 됩니다. 모델은 메모리가 없습니다. 기록을 유지하고 매번 그것을 다시 보내는 것은 여러분의 책임입니다. 우리의 모델은 우리가 모델에 보내고 있는 것만 인지합니다. 그 외의 모든 것은 잊혀집니다.
루프 개발 과정을 거치면서 우리의 "기억 (memory)"은 단지 이전 메시지들과 최신 쿼리(query)가 포함된 배열 (array)일 뿐이라는 사실을 알게 되었습니다. 네, 이것이 LLM이 컨텍스트 (context)를 관리하는 방식입니다. 저는 모델이 이를 스스로 관리하고 있다고 생각했기에 이 사실은 저에게 큰 충격이었으며, 이 배열을 이토록 미세한 수준까지 제어할 수 있다는 점은 기분 좋은 놀라움이었습니다. 두 번째 쿼리 이후의 우리의 "기억 (memory)"은 아래 스니펫 (snippet)과 같은 모습일 것입니다.
messages: [
{ role: "user", content: "Hello, Claude" },
{ role: "assistant", content: "Hello! How can I help you today?" },
...
만약 모델과 진정한 주고받는 대화를 하고 싶다면 어떻게 해야 할까요? 우선, 다음과 같은 요구 사항들이 필요합니다: 터미널 (terminal)에서 사용자 입력을 읽고, 모델에 전달하기 위해 이전 메시지에 새로운 메시지를 추가(append)하며, 응답을 출력한 뒤 다시 1단계로 돌아가고, 마지막으로 깔끔한 종료 옵션을 제공하는 것입니다.
기본적인 루프 채팅의 전체 구현을 확인하고 싶다면, 이 단계가 추가된 raw-claude-chat 리포지토리의 이 스크립트를 확인해 보세요.
이 단순한 배열 (array)은 나중에 실제로 "기억하는" 기능적인 채팅을 구현하는 데 필수적인 슬라이딩 윈도우 (sliding window), RAG, 그리고 시맨틱 검색 (semantic search)과 같은 많은 컨텍스트 (context) 전략의 씨앗이 됩니다.
다음 단계
채팅과 상호작용할 때, 우리가 원하는 한 가지는 단순히 메시지를 보내는 것이 아니라 모델에게 무언가를 하라고 명령하는 것입니다. 이는 도구 사용 (tool use), 즉 모델이 실제로 실행하도록 지시받은 것을 실행하고, 작업을 차례대로 수행하며, 필요할 때 어떤 도구를 실행할지 올바르게 선택하는 단계로 이어집니다. 우리는 서버 관점에서 도구인 gitstoria를 구축했습니다. 이제 그 상대방인 클라이언트 측 (client side)을 이해함으로써 이 지식을 보완해 나갈 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기