카테고리 없음

Langflow에 한국어 임베딩(jhgan/ko-sroberta-multitask) 커스텀 컴포넌트 붙이기 – venv 헤맨 후기

임마누엘한 2026. 5. 14. 15:42

들어가며

Langflow 로 한국어 RAG (Retrieval-Augmented Generation) 파이프라인을 만드려고 임베딩 모델을 골라야 했다. OpenAI 의 text-embedding-3-small 같은 상용 임베딩은 품질은 좋지만 비용 폐쇄망 운영 불가능이라는 두 가지 큰 제약이 있다. 그래서 처음부터 로컬에서 돌아가는 한국어 임베딩을 붙이기로 했다.

후보를 두 개로 좁혔다.

모델출처차원한국어 품질운영

jhgan/ko-sroberta-multitask HuggingFace 768 ⭐⭐⭐⭐⭐ (한국어 특화 학습) sentence-transformers 로 CPU 추론 가능
nomic-embed-text Ollama 768 ⭐⭐⭐ (다국어, 한국어는 평이) Ollama 데몬으로 호스팅

한국어 검색·RAG 품질만 보면 ko-sroberta-multitask 가 압도적이다. SBERT 계열의 한국어 fine-tuned 모델로, KorNLI / KorSTS 등으로 multi-task 학습되어 있다. 그래서 이걸로 가기로 했다.

문제는 Langflow 캔버스에서 이 모델을 어떻게 노출시키냐였다. Langflow 에는 "HuggingFace Embeddings" 라는 빌트인 컴포넌트가 있긴 한데, 저장된 venv 문제, type system 호환성, 그래프 와이어링 검증 세 가지가 차례로 발목을 잡았다. 같은 함정에 빠지지 않게 디테일하게 정리한다.

핵심 키워드: Langflow, HuggingFace Embeddings, ko-sroberta-multitask, custom component, venv, RAG.

시스템 컨텍스트

  • macOS 로컬에서 langflow 설치 (pip 으로 글로벌 설치).
  • Langflow 가 실행되면 별도 venv ~/.langflow/.langflow-venv/ 를 생성해서 그 안에서 컴포넌트를 import 한다.
  • 캔버스 흐름: 텍스트 입력 → 한국어 임베딩 컴포넌트 → Chroma 벡터스토어 → 검색.
  • 같은 흐름에 Neo4j tool 도 끼우려는 계획 (다음 날 포스트에서 다룬다).

문제 상황

langchain_huggingface.HuggingFaceEmbeddings 를 Langflow Custom Component 로 감싸서 캔버스에 올렸더니, build 단계에서 다음과 같이 두 가지 에러가 번갈아 떴다.

에러 1 — Type Convert IndexError

Flow build failed
7.7s
Error building Component Type Convert:
list index out of range

빌드는 시작되는데 "Type Convert" 라는 내부 컴포넌트에서 list index out of range 로 죽었다. 처음엔 내 코드의 버그라고 생각해서 한참 print 디버깅을 했다.

에러 2 — Output 이 임베딩 타입으로 안 잡힘

위 에러를 해결하고 나니 다른 문제가 보였다. 내 한국어 임베딩 컴포넌트의 output 이 임베딩 타입으로 인식되지 않아서, 다운스트림인 Chroma 벡터스토어와 연결이 안 됐다. UI 상으로 와이어를 끌어다 놓을 수가 없는 상태. (Langflow 는 type 이 맞는 컴포넌트끼리만 연결 가능)

에러 3 — Neo4j 모듈 import 실패 (부수효과)

같은 흐름에 Neo4j tool 을 추가했더니:

ModuleNotFoundError: No module named 'neo4j'

분명히 pip install neo4j 를 했는데 안 잡혔다. 이게 사실 가장 시간 잡아먹은 함정의 단서였다.

시도한 것들 (실패한 가설 포함)

시도 1 — 시스템 파이썬에 패키지 설치

가장 먼저 시도한 건 평소 하듯이 글로벌 pip 으로 설치.

pip install langchain-huggingface sentence-transformers neo4j

설치는 성공. 그런데 Langflow 캔버스에서는 여전히 ModuleNotFoundError. "분명히 깔았는데 왜?" 라는 의문. 시스템 파이썬에서는 잘 import 된다:

python -c "import langchain_huggingface; print(langchain_huggingface.__file__)"
# /usr/local/lib/python3.11/site-packages/langchain_huggingface/__init__.py

이때 의심해야 할 건 "Langflow 가 같은 파이썬을 쓰고 있는가" 였는데, 그걸 한참 안 의심했다.

시도 2 — Langflow 가 자체 venv 를 쓰는 걸 발견

에러 스택 trace 를 자세히 보니 다음과 같이 찍혀 있었다.

File "/Users/dev/.langflow/.langflow-venv/lib/python3.11/site-packages/langflow/...

~/.langflow/.langflow-venv/ 라는 별도 venv 에서 import 가 일어나고 있었다. Langflow 는 컴포넌트 격리를 위해 자체 venv 를 만든다. 시스템 파이썬에 깐 건 그 venv 에서 안 보이는 게 당연한 상황.

이걸 알고 나니 첫 번째 해결책이 명확해진다.

시도 3 — Type Convert 에러 추적

list index out of range 가 정확히 어디서 났는지 보려고 Langflow 의 debug 로그를 켰다 (LANGFLOW_LOG_LEVEL=DEBUG). 보니까 내 컴포넌트의 build 메서드가 호출되기 전에 "Type Convert" 단계에서 죽고 있었다.

원인 후보:

  • (a) 내 컴포넌트의 Output type 선언이 잘못됨
  • (b) 입력으로 빈 list/None 이 들어옴
  • (c) 캔버스 와이어링이 실제로 끊겨 있는데 UI 만 연결처럼 보임

(c) 가 정답이었다. 내가 끌어다 놓은 와이어가 시각적으로는 붙어 보였지만 type 불일치로 실제로는 binding 이 안 되어 있었고, build 시 입력이 비어 있어서 list slice 에서 죽었다.

시도 4 — Output 타입 명시

(c) 의 근본 원인을 추적했더니, 내 Output  types 를 명시 안 한 게 문제였다. Langflow custom component 는 다음과 같이 output 의 타입을 명시해야 한다.

outputs = [
    Output(
        display_name="Embeddings",
        name="embeddings",
        method="build_embeddings",
        types=["Embeddings"],   # ← 이 줄이 결정적
    )
]

types=["Embeddings"] 를 빼면 Langflow 의 type system 이 "이 컴포넌트의 출력 타입은 알 수 없음" 으로 판정해서, Chroma 벡터스토어의 embedding 입력(타입: Embeddings)과 매칭이 안 된다. UI 에서 와이어 끌기는 되지만 type guard 에서 silent fail.

시도 5 — Chroma 와 진짜 연결되었는지 검증

Output type 을 고치고 나니 와이어가 살아 있는 게 확인됐다. 검증 방법:

  1. 입력 텍스트 1개 ("안녕하세요") 를 흘려보낸다.
  2. Chroma 의 collection 에 벡터가 1개 들어갔는지 본다.
import chromadb
client = chromadb.PersistentClient(path="/tmp/chroma")
col = client.get_collection("test_col")
print(col.count())  # 1 이 나와야 함

1 이 나왔으면 성공. 0 이거나 collection 이 없다는 에러가 나오면 임베딩이 흘러가지 않은 것.

원인 정리

세 가지가 겹쳐 있었다.

  1. Langflow 는 별도 venv (~/.langflow/.langflow-venv/) 에서 실행된다. 시스템 파이썬에 설치한 패키지는 안 보인다.
  2. Custom Component 의 Output(types=[...]) 가 Langflow 의 type system 의 핵심. 누락하면 후속 노드와 매칭 안 됨. UI 에서 연결처럼 보여도 silent fail.
  3. 빈 입력이 흐르면 Type Convert 단계의 list slice 에서 IndexError. 와이어링이 끊긴 게 진짜 원인인데, 죽는 위치는 다른 곳.

해결

1) Langflow 가 사용하는 venv 찾고 패키지 설치

에러 스택 trace 에서 venv 경로 확인. 일반적으로는 다음 위치.

~/.langflow/.langflow-venv

여기에 직접 설치한다.

source ~/.langflow/.langflow-venv/bin/activate
pip install langchain-huggingface sentence-transformers neo4j
deactivate

또는 venv 활성화 없이 직접 venv 의 pip 을 부른다.

~/.langflow/.langflow-venv/bin/pip install \
  langchain-huggingface sentence-transformers neo4j

2) Custom Component 코드 (제대로 된 버전)

from langflow.custom import Component
from langflow.io import StrInput, Output
from langchain_huggingface import HuggingFaceEmbeddings


class KoEmbeddings(Component):
    display_name = "Ko-SRoberta (Local)"
    description = "Korean sentence embeddings using jhgan/ko-sroberta-multitask."
    icon = "type"

    inputs = [
        StrInput(
            name="model_name",
            display_name="Model name",
            value="jhgan/ko-sroberta-multitask",
            advanced=False,
        ),
        StrInput(
            name="device",
            display_name="Device",
            value="cpu",       # GPU 사용 시 "cuda" 또는 "mps"
            advanced=True,
        ),
    ]

    outputs = [
        Output(
            display_name="Embeddings",
            name="embeddings",
            method="build_embeddings",
            types=["Embeddings"],   # ← 결정적
        )
    ]

    def build_embeddings(self) -> HuggingFaceEmbeddings:
        return HuggingFaceEmbeddings(
            model_name=self.model_name,
            model_kwargs={"device": self.device},
            encode_kwargs={"normalize_embeddings": True},
        )

코드 한 줄씩 의미:

  • display_name → 캔버스에서 보이는 이름. 카테고리는 폴더로 결정 (뒤에 설명).
  • inputs  StrInput 두 개. advanced=True 인 항목은 기본 UI 에서 숨겨지고 "Advanced" 토글로 펼친다.
  • outputs  method="build_embeddings" 가 호출되어 그 반환값을 다음 컴포넌트로 흘려보낸다. types=["Embeddings"] 가 type matching 키.
  • normalize_embeddings=True → 코사인 유사도 검색 시 L2 normalize 가 있으면 dot product 만으로도 동작. Chroma 의 default 와 잘 맞는다.

3) Langflow 백그라운드 실행

langflow run 명령이 안 잡히면 venv 안의 binary 를 직접 가리키거나 nohup 으로 띄운다.

nohup ~/.langflow/.langflow-venv/bin/langflow run --port 7860 \
  > ~/langflow.log 2>&1 &
echo $!  # PID 기록해두면 나중에 kill 편함

langflow.log 의 첫 30줄에 어떤 venv 에서 실행 중인지, 어떤 컴포넌트 path 가 로드되는지 출력된다. 셋업 확인은 이 로그를 보는 게 가장 빠르다.

4) (선택) 사이드바에 영구 등록

캔버스에 만든 컴포넌트는 그 flow 안에서만 산다. 다른 flow 에서도 쓰려면 파일로 등록해야 한다.

export LANGFLOW_COMPONENTS_PATH=$HOME/langflow-components
mkdir -p $HOME/langflow-components/embeddings_korean
# 그 안에 ko_embeddings.py 저장

폴더 이름이 사이드바의 카테고리 가 되고, 파일 이름이 컴포넌트 이름이 된다. 이 구조를 깨면 사이드바에 안 뜨거나 카테고리가 이상하게 잡힌다.

검증 절차

배포 후 다음 순서로 확인하면 30초 안에 정상 동작을 검증할 수 있다.

  1. 캔버스 좌측 사이드바 → "Embeddings" 카테고리에 Ko-SRoberta (Local) 가 있는지.
  2. 끌어다 놓고, 입력 텍스트 한 줄 + Chroma 벡터스토어 + 검색 컴포넌트 까지 와이어.
  3. 캔버스 우상단 Run.
  4. Chroma 의 persist 디렉토리에 chroma.sqlite3 가 생기는지, 그 안에 1개 벡터가 들어갔는지 (위 검증 코드).
  5. 같은 쿼리로 검색했을 때 cosine similarity 가 0.99 이상 (자기 자신 검색이라면) 나오는지.

배운 점

  • Langflow custom component 의 의존성은 Langflow venv 에 설치한다. 시스템 파이썬 ≠ 캔버스 런타임이라는 것을 처음부터 알고 있으면 30분이 줄어든다. 에러 스택 trace 의 경로를 한 번 보면 venv 가 어디 있는지 즉시 보인다.
  • Output(types=[...]) 는 캔버스 연결 가능 여부를 결정한다. 누락 시 와이어가 시각적으로는 그려져도 binding 이 안 되고 build 단계에서 IndexError 등 엉뚱한 곳에서 죽는다. 디버깅 진입점을 잘못 잡기 쉬운 함정.
  • 타입 명시는 SBERT 임베딩만의 문제가 아니다. Vector store, LLM, Document 등 모든 type 에서 동일하게 적용된다. 새 컴포넌트 만들 때 첫 줄을 types=[...] 부터 적는 게 안전하다.
  • 한국어 임베딩 시작점: jhgan/ko-sroberta-multitask 가 무난한 기본. RAG quality 가 부족하면 BAAI/bge-m3 로 옮겨도 768차원이라 벡터스토어 재생성 없이 시도 가능. intfloat/multilingual-e5-large 도 비교 후보.

다음 단계

  • 한국어 임베딩 + 일반 텍스트 retrieval 까지는 OK. 다음은 그래프 RAG: Neo4j 와 함께 엮어서 entity 관계까지 사용하는 흐름. (다음 날 포스트로 이어진다)
  • production 으로 가려면 임베딩 모델을 컨테이너 + GPU 로 분리. CPU 단일 호스트는 PoC 까지만.

참고