Skip to content
게으른 엔지니어의 기술 블로그
Go back

임베딩 모델을 바꾸니 순서가 뒤집혔다

Read in English →

AI 검증이 놓치는 것들 · 5/9편

만들고 있는 Obsidian 플러그인에는 기사 한 편을 넣으면 LLM이 그 안의 개념·인물·회사를 각각 별도 노트로 나눠 위키에 저장하는 기능(ingest)이 있다. 4편에서는 이렇게 같은 기사에서 함께 만들어진 노트들(형제 노트)이 서로 같은 문단을 그대로 베끼는 버그를 찾았다. 이번엔 고치는 쪽으로 방향을 틀었다. 프롬프트 문구를 손봐서 몇 라운드 재현 테스트를 돌려봤는데, 재현율은 줄었지만 완전히 사라지지는 않았다. 그래서 더 근본적인 구조를 만들기로 했다 — ingest가 형제 노트 여러 개를 만들 때마다, 쓰기 전에 서로 겹치는 문단이 있는지 자동으로 검사하고 고치는 후처리 파이프라인.

설계는 두 단계였다. 먼저 문단들을 벡터로 바꿔서 수학적으로 “얼마나 비슷한지” 점수를 매기고, 점수가 높은 후보 쌍만 골라 그 둘을 실제로 다시 읽고 판단할 수 있는 쪽에 넘긴다. 이 얘기를 하다가 이런 질문을 받았다. “그거 결국 또 LLM 쓰는 거 아니야?” 맞기도 하고 틀리기도 한 질문이었다. 둘 다 API로 호출하는 AI 모델이긴 한데, 실제로는 완전히 다른 일을 하는 물건이었다.

채팅형 LLM은 “답을 만드는” 모델이다 — LLM과 임베딩 모델의 차이

이 플러그인이 노트를 쓰고 판단하는 데 쓰는 gpt-5.6-luna 같은 모델은 프롬프트를 주면 그에 이어질 것 같은 다음 단어를 하나씩 예측해서 이어 붙이는 방식으로 답을 만든다. “이 두 문단이 같은 내용을 중복해서 담고 있는가”라고 물으면, 그 질문에 대한 답을 문장(또는 JSON)으로 새로 지어내서 돌려준다. 결과물이 텍스트라는 게 핵심이다. 판단의 과정도, 결과도 전부 “말”로 나온다.

임베딩 모델은 “위치를 알려주는” 모델이다 — 코사인 유사도

반면 임베딩 모델에게 문단 하나를 주면, 그 모델은 문장을 만들어 돌려주지 않는다. 대신 숫자 수백 개짜리 배열(벡터) 하나를 뱉는다. 예를 들어 text-embedding-3-small은 문단 하나를 최대 1536개의 숫자로 바꿔서 돌려준다. 이 숫자들 자체는 사람이 읽어서 무슨 뜻인지 알 수 있는 게 아니다. 대신 이 벡터는 좌표 공간 안의 한 점이라고 생각하면 된다 — 그리고 그 모델은 의미가 비슷한 문장일수록 그 점들이 서로 가깝게 찍히도록 학습되어 있다. “오픈AI는 풀스택 조직이다”와 “오픈AI는 컴퓨팅부터 제품까지 전 과정을 보유한 조직이다”를 각각 벡터로 바꾸면, 그 두 점은 좌표 공간 안에서 서로 가까이 찍힌다. 이 “가까움”을 코사인 유사도라는 값으로 계산해 숫자 하나로 뽑아내는 게(이론상 -1에서 1 사이지만, 이런 텍스트 임베딩에서는 보통 0에서 1 사이로 나온다) 이번에 계속 이야기한 그 “유사도 점수”다. 임베딩 모델은 판단하지 않는다. 그저 위치를 알려줄 뿐이고, 그 위치를 보고 “비슷한가 아닌가”를 정하는 건 그 점수를 받아 든 쪽(이번 경우엔 코드, 최종적으로는 LLM)의 몫이다.

왜 “임베딩”이라고 부르나 — word2vec

이 이름은 원래 수학 용어다. 어떤 구조(예: 낮은 차원의 도형)를 그보다 더 큰 공간(더 높은 차원) 안에, 원래 갖고 있던 관계(거리나 형태)를 최대한 보존한 채로 “박아 넣는” 것을 embedding이라고 부른다. 텍스트를 다루는 AI 쪽에서는 단어를 벡터 공간에 박아 넣어 의미가 비슷한 단어끼리 가깝게 배치하는 표현을 “word embedding”이라고 불러왔고, 2013년 구글의 word2vec이 이 방식을 널리 퍼뜨리면서 이름이 그대로 굳어졌다. “문장을 벡터 공간에 임베드한다” — 문장의 의미라는, 원래는 좌표로 잴 수 없던 것을 좌표로 잴 수 있는 공간 안에 밀어 넣는다는 뜻이다. 그러니 “임베딩 모델”은 이름 그대로 “텍스트를 좌표 공간에 박아 넣어주는 모델”이고, 채팅형 LLM과는 애초에 만들어진 목적 자체가 다르다.

그런데 왜 그냥 LLM한테 매번 물어보면 안 되나 — LLM 호출 비용과 임베딩

이론적으로는 문단 두 개를 놓고 LLM한테 “이거 겹쳐?”라고 물어보는 것만 반복해도 같은 결과를 얻을 수 있다. 문제는 규모다. 기사 한 편에서 형제 노트가 10개쯤 나오면, 그 안의 문단은 수십 개가 되고, 겹치는지 확인해야 할 모든 쌍의 개수는 문단 수의 제곱에 비례해서 늘어난다. 문단 50개면 쌍은 천 개가 넘는다. 이 쌍 하나하나를 LLM에게 물어보면, 이번에 실측한 대로 판단 호출 하나에 4초 가까이 걸리니 순서대로만 처리해도 한 시간이 넘게 걸린다. 반면 임베딩은 문단 50개를 한 번에 배치로 묶어 보내면 1초도 안 걸려서 벡터 50개를 받는다. 그다음 “어느 쌍이 진짜 의심스러운가”는 그 벡터들끼리 곱셈 몇 번(코사인 유사도 계산)만 하면 끝나는, API 호출도 필요 없는 순수 계산이다. 그렇게 걸러진 몇 개(보통 청크당 한 자릿수에서 많아야 십몇 개)만 LLM에게 넘겨서 진짜 판단을 맡긴다. 임베딩은 정답을 내주는 도구가 아니라, 값비싼 LLM 판단을 아무 데나 낭비하지 않도록 “이거 후보야”라고 미리 걸러주는 1차 필터인 셈이다.

같은 회사 임베딩 모델도 등급이 있다 — text-embedding-3-small vs large

OpenAI 임베딩 모델에도 text-embedding-3-small과 text-embedding-3-large 두 등급이 있다. 채팅형 모델에 “미니”와 “풀사이즈” 등급이 있는 것과 같은 구도로, 학습에 들인 규모와 파라미터 수가 다르고 그만큼 벡터가 담아내는 의미의 해상도도 다르다. 이번 파이프라인은 처음에 small을, 그것도 256차원으로 줄여서(OpenAI 임베딩 API는 벡터를 원래 크기보다 짧게 잘라서 받는 옵션을 제공한다 — 저장 비용을 줄이려는 목적이다) 쓰고 있었다. 쌓인 노트에서 중복을 찾아 합치자고 제안하는 기존 위키 정리 기능(refactor)이 이미 그렇게 쓰고 있었고, 그 설정에 맞춰 임계값(코사인 유사도 0.80)까지 실측으로 정해둔 상태였기 때문이다.

그런데 이번 파이프라인을 실제 데이터로 검증하던 중, 진짜 중복 사례와 그냥 우연히 비슷해 보이는 무해한 문장이 거의 같은 점수대(0.72~0.80)에 뒤섞여 있다는 걸 발견했다. 예를 들어 “Codeberg의 방식은 오픈소스 AI 정책 비교에서 다뤄지는 모델 중 하나다”와 “LLVM의 방식은 오픈소스 AI 정책 비교에서 다뤄지는 모델 중 하나다”처럼, 서로 완전히 다른 프로젝트를 말하지만 문장 틀이 비슷해서 생긴 우연한 유사도가, “카카오가 보유한 다음 운영사 AXZ 지분을 업스테이지에 이전한다”는 문장이 형제 노트 두 곳에 거의 그대로 반복된 진짜 중복과 같은 점수를 받고 있었다. 이러면 임계값을 아무리 조정해도 둘을 가를 수가 없다.

같은 문단 쌍을 text-embedding-3-large로 다시 계산해보니 순서가 뒤집혔다. 진짜 중복 사례들은 0.78~0.92로 올라갔고, 방금 그 Codeberg/LLVM 같은 우연한 유사 문장은 오히려 0.69 아래로 내려갔다. small 모델에서는 둘이 뒤섞여 있던 그 자리가, large에서는 명확한 간격으로 갈라진 것이다. 같은 코사인 유사도라는 계산식을 쓰면서도, 모델이 문장 안에 담긴 “무엇에 관한 이야기인가”라는 더 미세한 차이를 얼마나 잘 벡터에 반영하느냐에 따라 결과가 이렇게까지 달라졌다.

물론 large도 만능은 아니었다. 김성훈 대표의 발언(“사람들이 생계를 위해 억지로 하는 일에서 벗어나…”)이 형제 노트 두 곳에서 표현만 살짝 바뀐 채 거의 그대로 반복된 사례, 그리고 파라미터 수치(“1000억 개 이하 모델은… 2000억 개 이상 모델에…”)가 숫자까지 똑같이 반복된 사례는 large로 계산해도 0.68~0.74 사이에 머물러 새로 잡은 임계값을 넘지 못했다. 사람이 읽으면 거의 베낀 수준인데도, 임베딩 모델은 그 정도의 유사성은 여전히 “그럭저럭 비슷한 수준”으로만 본다는 뜻이다.

남는 생각

임베딩 모델을 바꾸는 것만으로 점수 순서가 뒤집히는 걸 직접 보고 나니, “코사인 유사도 0.8 이상이면 중복”처럼 숫자 하나로 정답을 정하려던 접근 자체가 애초에 무리였다는 게 더 분명해졌다. 임베딩은 후보를 좁혀 주는 필터로는 훌륭하지만, 그 필터가 얼마나 정밀한지는 순전히 어떤 모델을 쓰느냐에 달려 있고, 아무리 좋은 모델을 써도 결국 걸러내지 못하는 사례가 남는다. 최종 판단은 결국 문장을 실제로 읽고 이해할 수 있는 쪽 — 채팅형 LLM — 에게 넘길 수밖에 없다는 것도, 이번에 다시 확인했다.


Share this post:

AI 검증이 놓치는 것들

  1. 1. 몇 주 뒤, 그 감사 장치가 못 보는 경우를 찾았다
  2. 2. 리스트 밀도 상위 4개 중, 서브에이전트가 2개를 놓쳤다
  3. 3. 검증은 다른 쌍으로 했는데, 원래 그 사례는 다시 안 봤다
  4. 4. LLM이 만든 형제 노트끼리 같은 문단을 그대로 베끼고 있었다
  5. 5. 임베딩 모델을 바꾸니 순서가 뒤집혔다
  6. 6. 임베딩 필터를 걷어내니, 중복이 오히려 더 많이 잡혔다
  7. 7. LLM 노트 병합 규칙에서 틀린 예시는 지웠는데, 규칙은 남아 있었다
  8. 8. LLM 노트 병합 테스트는 통과했는데, 고친 코드는 한 번도 실행되지 않았다
  9. 9. LLM이 노트 중복을 지우자, 남은 문장이 가리킬 곳을 잃었다