Skip to main content
Summary

ID는 의미가 아니다

지난 장에서 우리는 텍스트가 토큰으로 쪼개지고, 각 토큰이 어휘 사전의 ID로 바뀌는 과정을 살펴보았습니다.

나는 커피를 마셨다.
→ ["나는", " 커피", "를", " 마셨", "다", "."]
→ [1024, 8153, 317, 22019, 91, 13]

여기까지 보면 왠지 모델이 숫자를 읽을 준비를 마친 것 같습니다. 하지만 아직 문제가 남아 있습니다. 1024라는 숫자 자체가 "나는"이라는 뜻을 담고 있을까요? 8153이 317보다 크다고 해서 "커피"가 "를"보다 더 크거나 중요한 의미를 가질까요?

전혀 그렇지 않습니다.

토큰 ID는 의미가 아니라 주소입니다. 도서관에서 책에 붙은 청구기호와 비슷합니다. 책 번호가 1024라고 해서 책 내용이 1024쪽이라는 뜻도 아니고, 번호가 큰 책이 더 훌륭하다는 뜻도 아닙니다. 그 번호는 단지 "이 책은 이 위치에서 찾을 수 있다"는 표시입니다.

LLM도 마찬가지입니다. 토큰 ID는 모델에게 "이 번호에 해당하는 벡터를 가져오라"고 알려주는 주소입니다. 그리고 그 주소를 따라가면 나오는 숫자 묶음이 바로 임베딩입니다.

임베딩은 토큰을 모델이 계산할 수 있는 벡터로 바꿉니다. 벡터는 여러 숫자를 한 줄로 모아놓은 것입니다. 예를 들면 이런 식입니다.

"커피" → [0.12, -0.48, 1.03, 0.07, ...]

실제 LLM에서는 이 숫자가 몇 개가 아니라 수백 개, 수천 개일 수 있습니다. 중요한 것은 숫자의 개수가 많다는 사실이 아니라, 이 숫자들이 모델 안에서 토큰의 특징을 표현한다는 점입니다. 토큰 ID가 단순한 주소라면, 임베딩 벡터는 그 주소에 저장된 계산 가능한 의미의 출발점입니다.

임베딩: 토큰을 벡터로 바꾸는 법

원-핫 표현으로는 부족하다

토큰을 숫자로 표현하는 가장 단순한 방법부터 생각해보겠습니다. 어휘 사전에 토큰이 6개만 있다고 가정해봅시다.

ID  토큰
0   나는
1   커피
2   차
3   마셨다
4   읽었다
5   .

이때 "커피"를 표현하려면 두 번째 위치만 1이고 나머지는 모두 0인 벡터를 만들 수 있습니다.

나는   → [1, 0, 0, 0, 0, 0]
커피   → [0, 1, 0, 0, 0, 0]
차     → [0, 0, 1, 0, 0, 0]
마셨다 → [0, 0, 0, 1, 0, 0]

이런 표현을 원-핫 인코딩이라고 합니다. 하나의 위치만 뜨겁게 켜져 있다는 뜻입니다. 구조는 매우 명확합니다. "커피"는 두 번째 칸, "차"는 세 번째 칸, "마셨다"는 네 번째 칸입니다.

하지만 원-핫 표현에는 치명적인 한계가 있습니다.

첫째, 어휘가 커질수록 벡터가 너무 길어집니다. 실제 LLM의 어휘 사전은 수만 개에서 수십만 개의 토큰을 가질 수 있습니다. 어휘가 10만 개라면 토큰 하나를 표현하기 위해 10만 칸짜리 벡터가 필요합니다. 그중 실제로 1인 값은 단 하나이고 나머지는 모두 0입니다. 대부분이 비어 있는 셈입니다.

둘째, 원-핫 벡터는 토큰 사이의 관계를 표현하지 못합니다. 사람은 "커피"와 "차"가 비슷한 종류의 단어라는 것을 압니다. 둘 다 마실 수 있는 음료입니다. 반면 "커피"와 "읽었다"는 전혀 다른 역할을 합니다. 하지만 원-핫 벡터에서는 이 차이가 드러나지 않습니다.

커피   → [0, 1, 0, 0, 0, 0]
차     → [0, 0, 1, 0, 0, 0]
읽었다 → [0, 0, 0, 0, 1, 0]

컴퓨터가 보기에는 "커피"와 "차"도 서로 다른 위치에 1이 있을 뿐이고, "커피"와 "읽었다"도 서로 다른 위치에 1이 있을 뿐입니다. 둘 사이의 의미적 가까움이 표현되지 않습니다.

셋째, 원-핫 표현은 학습에 비효율적입니다. 모델은 언어를 배울 때 "비슷한 상황에서 등장하는 토큰은 어느 정도 비슷하게 다루면 된다"는 힌트를 활용해야 합니다. 예를 들어 "커피를 마셨다"를 많이 본 모델은 "차를 마셨다"도 자연스럽게 이해할 수 있어야 합니다. 그런데 원-핫 표현만으로는 "커피"와 "차"가 비슷하다는 출발점이 없습니다.

그래서 필요한 것이 밀집 벡터 표현입니다.

밀집 벡터는 의미를 담을 공간을 만든다

밀집 벡터는 대부분의 값이 0이 아닌 숫자로 채워진 벡터입니다. 원-핫 벡터처럼 아주 길고 텅 빈 표현 대신, 더 짧고 정보가 꽉 찬 표현을 사용합니다.

예를 들어 가상의 임베딩을 보겠습니다.

커피 → [0.21, -0.13, 0.88, 0.04]
차   → [0.18, -0.10, 0.81, 0.09]
책   → [-0.42, 0.77, -0.05, 0.31]

이 예시에서 "커피"와 "차"의 벡터는 서로 비슷해 보입니다. 첫 번째 값도 비슷하고, 두 번째 값도 비슷하고, 세 번째 값도 비슷합니다. 반면 "책"은 조금 다른 방향에 있습니다. 실제 모델의 임베딩은 이렇게 사람이 보기 좋게 네 개 숫자로 끝나지 않습니다. 수백 또는 수천 차원의 복잡한 공간에 놓입니다. 하지만 기본 직관은 같습니다.

비슷한 맥락에서 자주 쓰이는 토큰은 학습 과정에서 비슷한 벡터를 갖도록 조정될 수 있습니다.

여기서 "의미를 담는다"는 말을 조심해서 이해해야 합니다. 임베딩 벡터의 첫 번째 칸이 "음료 여부", 두 번째 칸이 "따뜻함", 세 번째 칸이 "격식"처럼 사람이 붙인 라벨과 정확히 대응되는 것은 아닙니다. 모델은 그런 식으로 사전을 만드는 것이 아닙니다. 대신 엄청난 양의 텍스트를 보며, 다음 토큰을 잘 예측하는 데 도움이 되는 방향으로 숫자들을 조금씩 바꿉니다.

그 결과 벡터 공간 안에 흥미로운 구조가 생깁니다. 음식과 관련된 표현, 프로그래밍과 관련된 표현, 감정과 관련된 표현, 질문에 자주 등장하는 표현들이 서로 다른 패턴을 갖게 됩니다. 모델이 명시적으로 "이것은 음식 카테고리다"라고 적어두는 것은 아니지만, 학습을 거치며 비슷한 쓰임새를 가진 토큰들이 가까운 방향으로 모이는 경향이 생깁니다.

비유하자면 임베딩 공간은 거대한 도시 지도와 같습니다. "커피", "차", "라떼"는 서로 가까운 동네에 있을 가능성이 큽니다. "파이썬", "함수", "변수"도 또 다른 동네에 모일 수 있습니다. "슬프다", "우울하다", "눈물"은 감정의 골목 어딘가에서 서로 가까울 수 있습니다. 물론 현실의 지도처럼 2차원 평면에 예쁘게 놓이는 것은 아니고, 훨씬 많은 차원을 가진 공간에서 벌어지는 일입니다.

임베딩 테이블은 거대한 조회표다

그렇다면 토큰 ID는 어떻게 임베딩 벡터로 바뀔까요? 핵심 장치는 임베딩 테이블입니다.

임베딩 테이블은 토큰 ID마다 하나의 벡터를 저장해둔 큰 표입니다. 어휘 사전에 토큰이 50,000개 있고, 각 토큰을 768차원 벡터로 표현한다면 임베딩 테이블은 대략 이런 모양입니다.

토큰 ID 개수: 50,000
임베딩 차원: 768
임베딩 테이블 크기: 50,000 × 768

표로 상상하면 다음과 같습니다.

ID     임베딩 벡터
0      [0.02, -0.11, 0.34, ..., 0.08]
1      [-0.45, 0.19, 0.07, ..., -0.12]
2      [0.31, 0.04, -0.28, ..., 0.55]
...
8153   [0.12, -0.48, 1.03, ..., 0.21]

토큰 ID 8153이 들어오면 모델은 임베딩 테이블의 8153번째 행을 가져옵니다. 이것이 "토큰을 벡터로 바꾼다"의 실제 의미입니다.

토큰: "커피"
토큰 ID: 8153
임베딩 테이블 조회
임베딩 벡터: [0.12, -0.48, 1.03, ..., 0.21]

여기서 중요한 점은 임베딩 테이블의 숫자가 사람이 직접 정한 값이 아니라는 것입니다. 처음에는 대개 작은 무작위 숫자로 시작합니다. 모델은 학습 데이터에서 다음 토큰을 맞히는 훈련을 하면서 이 숫자들을 계속 조정합니다. "이 토큰은 이런 상황에서 자주 나오네", "저 토큰은 이 토큰과 비슷한 문맥에서 쓰이네" 같은 패턴이 손실을 줄이는 방향으로 반영됩니다.

결국 임베딩 테이블은 학습된 결과물입니다. 사람이 "커피와 차는 비슷하게 해라"라고 하나하나 알려주지 않아도, 텍스트 속 사용 패턴을 통해 벡터들이 조정됩니다.

문장은 벡터들의 줄로 들어간다

토큰 하나가 벡터 하나로 바뀐다면, 문장은 어떻게 될까요? 문장은 토큰 여러 개의 시퀀스이므로, 모델 안에서는 벡터 여러 개의 시퀀스가 됩니다.

나는 커피를 마셨다.
→ [1024, 8153, 317, 22019, 91, 13]

각 ID를 임베딩 테이블에서 찾으면 다음처럼 바뀝니다.

1024  → [0.10, -0.02, 0.55, ...]
8153  → [0.12, -0.48, 1.03, ...]
317   → [-0.31, 0.08, 0.14, ...]
22019 → [0.44, 0.71, -0.26, ...]
91    → [0.03, -0.19, 0.62, ...]
13    → [-0.07, 0.22, 0.01, ...]

이제 문장은 하나의 긴 숫자 덩어리가 아니라, 토큰 개수만큼의 벡터가 순서대로 놓인 형태가 됩니다. 만약 문장이 6개 토큰이고 임베딩 차원이 768이라면, 모델의 입력은 6 × 768 모양의 숫자 배열이 됩니다.

이 배열이 Transformer 안으로 들어갑니다. 다음 부에서 자세히 배울 Attention은 이 벡터들 사이의 관계를 계산합니다. "나는"이라는 벡터, "커피"라는 벡터, "마셨다"라는 벡터가 서로 어떤 관련이 있는지 살피면서 다음 토큰을 예측합니다.

여기서 한 가지 질문이 생길 수 있습니다.

"토큰 임베딩만 있으면 문장의 의미를 알 수 있을까?"

아직은 부족합니다. "커피를 내가 마셨다"와 "내가 커피를 마셨다"는 토큰은 비슷하지만 순서가 다릅니다. "개가 사람을 물었다"와 "사람이 개를 물었다"는 같은 단어가 들어 있어도 의미가 완전히 달라집니다. 그래서 모델은 토큰 임베딩에 더해 위치 정보도 필요로 합니다. 위치 임베딩은 뒤에서 별도 장으로 다룰 예정입니다. 지금은 문장이 먼저 토큰 벡터들의 줄로 바뀐다는 점만 붙잡으면 됩니다.

가까운 벡터는 비슷한 의미를 가질 수 있다

임베딩을 설명할 때 자주 등장하는 말이 있습니다.

"비슷한 의미는 벡터 공간에서 가깝다."

이 말은 아주 중요하지만, 동시에 너무 단순하게 받아들이면 오해가 생길 수 있습니다. 먼저 직관부터 잡아보겠습니다.

가상의 2차원 공간을 상상해봅시다. 실제 임베딩은 훨씬 높은 차원이지만, 그림을 그리기 위해 2차원으로 줄여 생각하는 것입니다.

음료 쪽
  커피     차     라떼

동물 쪽
  고양이   강아지   토끼

프로그래밍 쪽
  함수     변수     클래스

만약 "커피"와 "차"가 비슷한 문맥에서 자주 등장한다면 두 벡터는 가까운 위치에 놓일 수 있습니다. "고양이"와 "강아지"도 반려동물, 동물, 귀여움, 키우다 같은 문맥을 공유하므로 가까워질 수 있습니다. "함수"와 "변수"도 프로그래밍 문맥에서 자주 함께 등장하므로 가까운 영역에 자리 잡을 수 있습니다.

이 가까움을 측정할 때는 보통 유사도를 사용합니다. 대표적으로 코사인 유사도라는 방법이 있습니다. 이름은 어렵지만 직관은 간단합니다. 두 벡터가 같은 방향을 가리키면 비슷하고, 전혀 다른 방향을 가리키면 덜 비슷하다고 보는 방식입니다.

벡터를 화살표라고 생각해봅시다. 두 화살표가 거의 같은 방향을 향하면 "이 둘은 비슷한 의미나 역할을 가질 가능성이 높다"고 볼 수 있습니다. 길이가 조금 달라도 방향이 비슷하면 유사도가 높게 나옵니다.

하지만 임베딩 공간에서의 가까움이 사람이 생각하는 의미와 항상 완벽하게 일치하는 것은 아닙니다. "사과"는 과일이기도 하고, 잘못을 인정하는 행동이기도 합니다. "은행"은 돈을 맡기는 곳일 수도 있고, 나무 이름일 수도 있습니다. 토큰 하나의 임베딩은 이런 여러 쓰임을 어느 정도 함께 품고 출발합니다. 이후 Transformer 층을 지나며 문맥에 맞는 표현으로 바뀌어 갑니다.

즉, 처음의 토큰 임베딩은 고정된 출발점이고, 문맥을 반영한 진짜 이해는 그다음 계산에서 점점 만들어집니다.

임베딩은 고정된 사전이 아니라 학습되는 좌표다

임베딩을 처음 배우면 이런 식으로 오해하기 쉽습니다.

"아, 임베딩은 단어의 뜻을 숫자로 저장한 사전이구나."

절반은 맞고 절반은 부족한 설명입니다. 임베딩은 토큰의 의미적 힌트를 담은 숫자 표현이지만, 국어사전처럼 뜻풀이가 적혀 있는 것은 아닙니다. 더 정확히 말하면 임베딩은 모델이 학습 과정에서 만들어낸 좌표입니다.

좌표라는 비유를 써보겠습니다. 도시 지도에서 어떤 가게의 위치는 위도와 경도로 표현할 수 있습니다. 위도와 경도 숫자 자체를 읽는다고 가게의 분위기를 바로 알 수는 없습니다. 하지만 좌표를 많이 모아보면 패턴이 보입니다. 카페가 몰린 거리, 회사가 많은 구역, 학교 주변 상권이 드러납니다.

임베딩도 비슷합니다. 벡터의 각 숫자 하나하나를 사람이 쉽게 해석하기는 어렵습니다. 하지만 전체 공간을 보면 비슷한 쓰임을 가진 토큰들이 비슷한 방향과 위치를 갖는 경향이 나타납니다.

학습 전의 임베딩은 거의 무작위 좌표입니다. "커피"와 "차"가 우연히 가까울 수도 있고 멀 수도 있습니다. 모델은 문장을 읽고 다음 토큰을 예측하면서 틀릴 때마다 내부 숫자를 조금씩 고칩니다. 이 과정에서 임베딩 테이블도 함께 바뀝니다.

예를 들어 모델이 이런 문장을 많이 본다고 해봅시다.

아침에 커피를 마셨다.
아침에 차를 마셨다.
따뜻한 커피가 좋다.
따뜻한 차가 좋다.

"커피"와 "차"는 비슷한 주변 단어를 자주 만납니다. "마셨다", "따뜻한", "잔", "향" 같은 단어와 함께 등장합니다. 모델은 이런 패턴을 이용해 다음 토큰을 더 잘 예측하려고 합니다. 그 결과 "커피"와 "차"의 벡터가 어떤 면에서 비슷해질 수 있습니다.

물론 두 토큰이 완전히 같아지지는 않습니다. "커피"는 "카페인", "에스프레소", "원두"와 더 자주 연결될 수 있고, "차"는 "녹차", "홍차", "찻잎"과 더 자주 연결될 수 있습니다. 임베딩은 같음과 다름을 동시에 담으려는 압축 표현입니다.

유명한 벡터 산술은 왜 가능해 보일까

임베딩을 소개할 때 빠지지 않는 유명한 예가 있습니다.

왕 - 남자 + 여자 ≈ 여왕

이 문장은 임베딩 공간에서 어떤 의미 관계가 방향으로 표현될 수 있다는 직관을 보여줍니다. "왕"에서 "남자"와 관련된 방향을 빼고 "여자"와 관련된 방향을 더하면 "여왕"에 가까운 벡터가 나올 수 있다는 이야기입니다.

이 예시는 임베딩의 매력을 잘 보여줍니다. 벡터가 단순한 ID 변환을 넘어, 관계와 패턴을 어느 정도 담을 수 있다는 뜻이기 때문입니다. "파리 - 프랑스 + 일본 ≈ 도쿄"처럼 수도와 국가의 관계를 떠올리게 하는 예도 자주 언급됩니다.

하지만 이 예시를 마법처럼 받아들일 필요는 없습니다. 모든 단어 관계가 이렇게 깔끔하게 계산되는 것은 아닙니다. 언어는 복잡하고, 단어는 여러 의미를 가지며, 학습 데이터에는 편향과 불균형도 있습니다. 벡터 산술은 임베딩 공간에 구조가 생길 수 있음을 보여주는 좋은 직관이지, 모든 의미가 단순한 덧셈과 뺄셈으로 해결된다는 뜻은 아닙니다.

이 책에서 우리가 붙잡아야 할 핵심은 더 실용적입니다.

임베딩은 토큰이나 문장을 숫자 공간의 점으로 바꿉니다. 그리고 그 공간에서 가까움, 방향, 거리 같은 계산을 할 수 있게 해줍니다. 이 능력이 LLM 내부 계산뿐 아니라 검색, 추천, RAG 같은 서비스의 기반이 됩니다.

입력 임베딩과 검색용 임베딩은 다르다

이제 중요한 구분을 해야 합니다. LLM을 공부하다 보면 "임베딩"이라는 단어가 두 가지 맥락에서 등장합니다.

하나는 지금까지 설명한 언어 모델의 입력 임베딩입니다. 토큰 ID를 Transformer에 넣기 전 벡터로 바꾸는 단계입니다. "나는 커피를 마셨다"라는 문장이 토큰 ID 배열이 되고, 각 ID가 임베딩 테이블을 통해 벡터로 바뀌는 과정입니다.

다른 하나는 서비스 개발에서 자주 쓰는 검색용 문장 임베딩입니다. 이것은 문장, 문단, 문서 조각 전체를 하나의 벡터로 바꾸는 데 사용됩니다.

예를 들어 다음 두 문장이 있다고 해봅시다.

환불은 구매 후 7일 이내에 가능합니다.
구입한 지 일주일 안이면 환불할 수 있습니다.

두 문장은 겉으로 쓰인 단어가 꽤 다릅니다. 첫 문장은 "구매", "7일", "가능"을 쓰고, 두 번째 문장은 "구입", "일주일", "할 수"를 씁니다. 하지만 의미는 매우 비슷합니다. 검색용 임베딩 모델은 이런 문장들을 벡터로 바꾸어, 의미가 비슷하면 가까운 위치에 놓이도록 학습됩니다.

여기서 차이를 정리하면 다음과 같습니다.

  • 입력 임베딩은 LLM 내부에서 토큰 ID를 처리하기 위한 벡터입니다.

  • 검색용 임베딩은 문장이나 문서의 의미를 비교하기 위한 벡터입니다.

  • 입력 임베딩은 다음 토큰 예측을 위한 계산의 출발점이고, 검색용 임베딩은 유사도 검색과 추천에 자주 쓰입니다.

둘 다 텍스트를 벡터로 바꾼다는 점에서는 비슷합니다. 하지만 목적이 다릅니다. 입력 임베딩은 모델 내부의 부품에 가깝고, 검색용 임베딩은 애플리케이션에서 "비슷한 내용을 찾기 위한 도구"에 가깝습니다.

이 차이를 이해하면 나중에 RAG를 배울 때 훨씬 덜 헷갈립니다. RAG에서 문서를 벡터 데이터베이스에 저장할 때 사용하는 임베딩은 보통 검색용 임베딩입니다. 반면 LLM이 최종 답변을 생성할 때 내부적으로 사용하는 것은 해당 모델의 입력 임베딩과 Transformer 계산입니다.

문장 임베딩은 의미 검색의 엔진이다

검색용 임베딩이 왜 강력한지 조금 더 살펴보겠습니다. 전통적인 검색은 주로 단어가 겹치는지를 봅니다. 사용자가 "환불 기간"이라고 검색하면 문서 안에 "환불"과 "기간"이라는 단어가 있는지 확인합니다.

이 방식은 빠르고 유용하지만 한계가 있습니다. 사용자가 이렇게 물으면 어떨까요?

결제 취소는 언제까지 돼요?

문서에는 "환불은 구매 후 7일 이내에 가능합니다"라고 적혀 있습니다. "결제 취소"와 "환불"은 비슷한 뜻으로 쓰일 수 있지만 단어가 정확히 같지는 않습니다. "언제까지"와 "7일 이내"도 표현이 다릅니다. 단어 겹침만 보는 검색은 이 문서를 놓칠 수 있습니다.

문장 임베딩을 사용하면 접근이 달라집니다. 질문과 문서를 각각 벡터로 바꾼 뒤, 벡터 공간에서 가까운 문서를 찾습니다.

질문: 결제 취소는 언제까지 돼요?
→ 질문 벡터

문서: 환불은 구매 후 7일 이내에 가능합니다.
→ 문서 벡터

두 벡터가 가까우면 관련 문서로 판단

이것을 의미 검색이라고 부를 수 있습니다. 정확히 같은 단어가 없어도 의미가 비슷한 문서를 찾는 방식입니다.

물론 의미 검색도 완벽하지 않습니다. 문장이 짧거나 모호하면 엉뚱한 문서가 가까워질 수 있습니다. 숫자, 날짜, 고유명사처럼 정확한 일치가 중요한 정보는 키워드 검색과 함께 쓰는 편이 더 안정적일 때가 많습니다. 하지만 LLM 애플리케이션에서 사용자의 자연스러운 질문을 문서와 연결하는 데 임베딩 검색은 매우 강력한 도구입니다.

추천 시스템도 임베딩을 좋아한다

임베딩은 언어 모델만의 기술이 아닙니다. 추천 시스템에서도 오래전부터 비슷한 아이디어가 사용되어 왔습니다. 사용자의 취향, 상품의 특징, 영화의 분위기, 음악의 장르를 벡터로 표현하면 "가까운 것"을 찾을 수 있기 때문입니다.

예를 들어 영화 추천을 생각해봅시다. 어떤 사용자가 SF 영화와 철학적인 분위기의 영화를 자주 봅니다. 그러면 이 사용자의 취향 벡터는 그런 영화들이 모인 방향에 가까워질 수 있습니다. 새 영화의 벡터가 그 방향과 가깝다면 추천 후보가 됩니다.

쇼핑몰에서도 비슷합니다. "무선 키보드"를 본 사람이 "블루투스 마우스", "노트북 거치대", "USB-C 허브"에 관심을 가질 수 있습니다. 상품 이름이 정확히 같지 않아도, 사용 패턴과 설명 문맥이 비슷하면 벡터 공간에서 가까워질 수 있습니다.

이런 관점에서 임베딩은 "세상을 숫자 공간에 배치하는 기술"이라고 볼 수 있습니다. 단어, 문장, 문서, 이미지, 사용자, 상품까지도 벡터로 표현할 수 있습니다. 벡터로 바꿀 수 있으면 가까운 것을 찾고, 묶고, 추천하고, 비교할 수 있습니다.

LLM 시대에 임베딩이 더 중요해진 이유도 여기에 있습니다. 우리는 이제 단순히 문서를 저장하는 것이 아니라, 문서의 의미를 찾고 연결해야 합니다. 사용자가 어떤 표현으로 질문하든 관련 정보를 찾아야 하고, 그 정보를 LLM에게 전달해 답변하게 해야 합니다. 이 연결의 바닥에 임베딩이 있습니다.

RAG는 임베딩 위에 세워진다

앞으로 배울 RAG는 LLM 애플리케이션에서 매우 중요한 구조입니다. RAG는 Retrieval-Augmented Generation의 줄임말로, 검색으로 찾은 외부 지식을 LLM의 답변 생성에 붙이는 방식입니다.

간단히 말하면 이런 흐름입니다.

사용자 질문
→ 질문을 임베딩 벡터로 변환
→ 벡터 데이터베이스에서 비슷한 문서 조각 검색
→ 검색된 문서를 LLM 프롬프트에 함께 넣음
→ LLM이 근거를 바탕으로 답변 생성

예를 들어 회사 내부 규정을 답변하는 챗봇을 만든다고 해봅시다. LLM은 기본적으로 회사의 최신 휴가 규정을 알지 못합니다. 모델이 학습된 이후에 바뀐 규정일 수도 있고, 애초에 외부에 공개되지 않은 내부 문서일 수도 있습니다.

이때 모든 규정을 모델에 다시 학습시키는 것은 비용이 크고 관리도 어렵습니다. 대신 규정 문서를 조각으로 나누고, 각 조각을 임베딩 벡터로 바꾸어 벡터 데이터베이스에 저장합니다. 사용자가 "배우자 출산휴가는 며칠인가요?"라고 묻는다면, 질문도 벡터로 바꾸고 가까운 문서 조각을 찾습니다. 그런 다음 찾은 규정 내용을 LLM에게 함께 보여주며 답하게 합니다.

여기서 임베딩은 질문과 문서를 연결하는 다리입니다. 사용자가 문서에 적힌 표현 그대로 질문하지 않아도, 의미가 가까우면 관련 문서를 찾을 수 있습니다.

문서 표현: 배우자 출산휴가는 10일 부여한다.
사용자 질문: 아내가 출산하면 남편은 며칠 쉴 수 있나요?

단어는 많이 다르지만 의미는 연결됩니다. 임베딩 검색은 이런 연결을 가능하게 합니다.

RAG를 잘 만들려면 단순히 LLM API를 호출하는 것만으로는 부족합니다. 문서를 어떻게 자를지, 어떤 임베딩 모델을 쓸지, 몇 개의 문서를 가져올지, 검색 결과를 어떻게 다시 정렬할지, 답변에 근거를 어떻게 반영할지 고민해야 합니다. 하지만 그 출발점은 하나입니다.

텍스트를 벡터로 바꾸면 의미를 계산할 수 있습니다.

임베딩이 모든 것을 이해하는 것은 아니다

임베딩이 강력하다고 해서 모든 문제가 해결되는 것은 아닙니다. 벡터 공간에서 가깝다는 것은 "관련 있을 가능성이 높다"는 뜻이지, 항상 정답이라는 뜻은 아닙니다.

예를 들어 "아이폰 15 케이스"와 "아이폰 14 케이스"는 벡터 공간에서 매우 가까울 수 있습니다. 둘 다 스마트폰 액세서리이고 표현도 비슷합니다. 하지만 실제 쇼핑 검색에서는 이 차이가 중요합니다. 모델명이 다르면 호환되지 않을 수 있기 때문입니다.

또 다른 예로 "환불 가능"과 "환불 불가"를 생각해봅시다. 두 표현은 많은 단어를 공유합니다. 임베딩 공간에서도 어느 정도 가까울 수 있습니다. 하지만 의미는 정반대입니다. 이런 경우 단순 유사도만 믿으면 위험합니다.

그래서 실전에서는 임베딩 검색에 여러 장치를 함께 사용합니다. 키워드 검색을 섞거나, 검색된 후보를 다시 정렬하거나, 숫자와 날짜 조건을 별도로 필터링합니다. 중요한 답변에서는 LLM에게 근거 문장을 확인하게 하고, 출처를 함께 보여주는 방식도 씁니다.

임베딩은 의미 검색의 강력한 엔진이지만, 브레이크와 계기판 없이 엔진만 달린 자동차를 운전할 수는 없습니다. 좋은 LLM 서비스는 임베딩, 검색, 규칙, 검증, 사용자 경험을 함께 설계합니다.

작은 숫자들이 만드는 큰 차이

여기까지 보면 임베딩은 단순한 변환처럼 보일 수도 있습니다.

토큰 ID → 벡터
문장 → 벡터
문서 → 벡터

하지만 이 변환이 LLM 세계의 많은 문을 엽니다. 토큰 ID가 벡터가 되어야 Transformer가 계산을 시작할 수 있습니다. 문장이 벡터가 되어야 의미 검색을 할 수 있습니다. 문서가 벡터가 되어야 RAG에서 필요한 지식을 찾아올 수 있습니다.

처음 LLM을 사용할 때 우리는 보통 프롬프트와 답변만 봅니다. 입력창에 질문을 넣으면 모델이 그럴듯한 문장을 돌려줍니다. 하지만 내부에서는 전혀 다른 일이 벌어집니다. 텍스트는 토큰이 되고, 토큰은 ID가 되고, ID는 임베딩 벡터가 됩니다. 그 벡터들이 여러 층의 계산을 지나며 다음 토큰의 확률로 바뀝니다.

이 흐름을 이해하면 LLM이 조금 덜 마법처럼 보입니다. 그리고 더 흥미로워집니다. 마법이 사라지는 대신, 우리가 직접 만질 수 있는 구조가 보이기 때문입니다.

텍스트
→ 토큰
→ 토큰 ID
→ 임베딩 벡터
→ Transformer 계산
→ 다음 토큰 확률
→ 생성된 문장

이 책의 뒤쪽에서 우리는 작은 GPT를 직접 만들 것입니다. 그때도 임베딩 테이블이 등장합니다. 처음에는 무작위 숫자였던 임베딩이 학습을 거치며 조금씩 조정되는 모습을 보게 될 것입니다. 물론 우리가 만드는 것은 ChatGPT가 아니라, ChatGPT의 핵심 원리를 이해하기 위한 작은 GPT입니다. 하지만 그 작은 모델 안에도 언어 모델의 중요한 부품들이 들어갑니다. 임베딩은 그중 가장 앞에서 문을 여는 부품입니다.

다음 단계: 모델은 얼마나 긴 문맥을 볼 수 있을까

이제 우리는 텍스트가 숫자가 되는 과정을 꽤 멀리 따라왔습니다. 문장은 토큰으로 나뉘고, 토큰은 ID가 되며, ID는 임베딩 벡터로 바뀝니다. 이 벡터들이 모델 안으로 들어가 계산의 재료가 됩니다.

하지만 아직 중요한 질문이 남아 있습니다.

모델은 이 벡터들을 얼마나 많이 한꺼번에 볼 수 있을까요? 대화가 길어지면 앞에서 한 말을 계속 기억할까요? 긴 문서를 통째로 넣으면 모든 내용을 똑같이 잘 참고할까요?

이 질문의 답이 바로 컨텍스트 윈도우입니다. 컨텍스트 윈도우는 LLM이 한 번에 다룰 수 있는 토큰의 범위입니다. 다음 장에서는 이 작업대의 크기가 모델의 기억처럼 보이는 능력, 비용, RAG 설계, 대화형 서비스 품질에 어떤 영향을 주는지 살펴보겠습니다.

책 목록

처음 만나는 LLM: 원리부터 Mini GPT 구현, RAG 서비스 개발까지

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기