들어가며
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개 ("안녕하세요") 를 흘려보낸다.
- Chroma 의 collection 에 벡터가 1개 들어갔는지 본다.
import chromadb
client = chromadb.PersistentClient(path="/tmp/chroma")
col = client.get_collection("test_col")
print(col.count()) # 1 이 나와야 함
1 이 나왔으면 성공. 0 이거나 collection 이 없다는 에러가 나오면 임베딩이 흘러가지 않은 것.
원인 정리
세 가지가 겹쳐 있었다.
- Langflow 는 별도 venv (~/.langflow/.langflow-venv/) 에서 실행된다. 시스템 파이썬에 설치한 패키지는 안 보인다.
- Custom Component 의 Output(types=[...]) 가 Langflow 의 type system 의 핵심. 누락하면 후속 노드와 매칭 안 됨. UI 에서 연결처럼 보여도 silent fail.
- 빈 입력이 흐르면 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초 안에 정상 동작을 검증할 수 있다.
- 캔버스 좌측 사이드바 → "Embeddings" 카테고리에 Ko-SRoberta (Local) 가 있는지.
- 끌어다 놓고, 입력 텍스트 한 줄 + Chroma 벡터스토어 + 검색 컴포넌트 까지 와이어.
- 캔버스 우상단 Run.
- Chroma 의 persist 디렉토리에 chroma.sqlite3 가 생기는지, 그 안에 1개 벡터가 들어갔는지 (위 검증 코드).
- 같은 쿼리로 검색했을 때 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 까지만.
참고
- LangChain HuggingFaceEmbeddings: https://python.langchain.com/docs/integrations/text_embedding/huggingfacehub
- ko-sroberta-multitask 모델 카드: https://huggingface.co/jhgan/ko-sroberta-multitask
- Langflow custom components 가이드: https://docs.langflow.org/components-custom-components
- sentence-transformers normalize 설명: https://www.sbert.net/docs/usage/semantic_textual_similarity.html