왜 같은 걸 두 번 만들었나
1편에서 만든 파이프라인은 코드로 짜여 있어서, 파라미터 하나 바꾸려면 코드를 열고 수정하고 재배포해야 했습니다. 이 파이프라인을 운영하는 게 저 혼자가 아니라면 — 예를 들어 청킹 크기나 리랭킹 후보 수를 현업 담당자가 직접 튜닝하고 싶어 한다면 — 코드 기반 구조는 접근성이 떨어집니다. Langflow 같은 로우코드 도구로 같은 파이프라인을 다시 만들어보면 이 트레이드오프가 어디서 갈리는지 명확해집니다.
전체 구조 비교
| 단계 | 1편 (LangChain 코드) | Langflow |
|---|---|---|
| 인제스천 | ConfluenceLoader 직접 호출 |
Custom Component로 동일 로더 래핑 |
| 청킹 | RecursiveCharacterTextSplitter |
Split Text 컴포넌트 (파라미터 동일 매핑) |
| 임베딩(dense) | FastEmbed 직접 호출 | Embedding Model 컴포넌트 |
| 임베딩(sparse) + RRF 융합 | 커스텀 함수 | 네이티브 컴포넌트 없음 → Custom Component로 구현 |
| 벡터 저장 | Qdrant 클라이언트 | Vector Store 컴포넌트 (Qdrant 지원) |
| 리랭킹 | Cohere/bge-reranker 직접 호출 | Reranker 컴포넌트 (지원 프로바이더 한정) |
| 배포 | FastAPI로 직접 감쌈 | Flow as API로 즉시 엔드포인트 노출 |
표에서 보이듯 절반은 컴포넌트를 드래그해서 연결하는 것만으로 끝나고, 나머지 절반(특히 하이브리드 검색의 융합 로직)은 결국 코드로 내려가야 했습니다. 아래에서 단계별로 정리합니다.
1. 인제스천 — Custom Component로 기존 로더 재사용
Langflow에는 Confluence 전용 컴포넌트가 기본 제공되지 않습니다. 대신 Custom Component 블록 안에 1편에서 썼던 ConfluenceLoader 호출 코드를 그대로 넣으면 됩니다. Langflow의 커스텀 컴포넌트는 입력/출력 타입만 Langflow 규격(Data, Message 등)에 맞추면 내부 로직은 순수 Python이라, 기존 코드를 거의 그대로 재사용할 수 있었습니다.
from langflow.custom import Component
from langflow.io import Output
from langflow.schema import Data
from langchain_community.document_loaders import ConfluenceLoader
class ConfluenceIngestComponent(Component):
display_name = "Confluence Ingest"
outputs = [Output(display_name="Documents", name="docs", method="load_docs")]
def load_docs(self) -> Data:
loader = ConfluenceLoader(url=self.confluence_url, token=self.api_token)
docs = loader.load(space_key=self.space_key, include_attachments=True, limit=50)
return Data(data={"documents": docs})
증분 동기화(version.when 비교) 로직도 이 컴포넌트 안에 그대로 넣었습니다. Langflow가 대신해주는 건 “이 컴포넌트를 캔버스 위에 올려두고 다음 컴포넌트와 선으로 연결하는 것”뿐, 동기화 전략 자체는 여전히 직접 설계해야 합니다.
2. 청킹 — Split Text 컴포넌트로 파라미터만 노출
이 단계는 로우코드가 확실히 이깁니다. Split Text 컴포넌트는 chunk_size, chunk_overlap, 분할 기준(문자/토큰/구분자)을 UI 필드로 그대로 노출합니다. 1편에서 코드로 넣었던 값(chunk_size=1024, chunk_overlap=128)을 필드에 입력하는 것만으로 끝났습니다.
여기서 얻는 실무적 이득은 값 하나 바꿀 때마다 배포 파이프라인을 돌릴 필요가 없다는 점입니다. 현업 담당자가 “요즘 답변이 문맥을 놓친다”고 하면, 개발자 없이도 캔버스에서 chunk_overlap 값을 올려보고 바로 테스트할 수 있습니다.
3. 임베딩과 하이브리드 검색 — 여기서 로우코드의 한계가 드러남
Dense 임베딩은 Embedding Model 컴포넌트로 문제없이 처리됩니다. 문제는 sparse 임베딩과 RRF(Reciprocal Rank Fusion) 융합입니다.
Langflow의 Vector Store 컴포넌트는 dense 벡터 검색을 전제로 설계돼 있고, dense+sparse를 동시에 질의해서 RRF로 합치는 하이브리드 검색은 네이티브 컴포넌트로 노출돼 있지 않았습니다. 일부 벡터 DB 연동 컴포넌트(예: Astra DB)는 하이브리드 검색 + 리랭커 조합을 사전 설정으로 제공하지만, Qdrant 기준으로는 1편에서 쓴 RRF 융합 쿼리를 Custom Component로 다시 넣어야 했습니다.[1]
class HybridSearchComponent(Component):
display_name = "Hybrid Search (Dense+Sparse RRF)"
def search(self) -> Data:
dense_hits = self.qdrant.query(collection, using="dense", query=dense_vec, limit=50)
sparse_hits = self.qdrant.query(collection, using="sparse", query=sparse_vec, limit=50)
fused = reciprocal_rank_fusion(dense_hits, sparse_hits, k=60)
return Data(data={"results": fused[:10]})
즉 “하이브리드 검색이 필요 없는 단순 RAG”라면 Langflow 기본 컴포넌트만으로 충분하지만, 1편에서 다룬 수준의 정교한 검색을 그대로 옮기려면 결국 코드 한 조각은 남습니다. 로우코드 도구가 없애주는 건 보일러플레이트(연결, 로깅, API 서빙)지, 검색 품질을 결정하는 핵심 로직이 아니라는 걸 이 단계에서 체감했습니다.
4. 리랭킹 — 컴포넌트는 있지만 프로바이더가 제한적
Reranker 컴포넌트는 존재하지만, 지원하는 리랭커 프로바이더 목록이 정해져 있습니다. 1편에서 쓴 오픈소스 bge-reranker를 자체 호스팅해서 쓰려면 이것도 Custom Component로 HTTP 호출을 감싸야 했습니다. Cohere처럼 이미 지원 목록에 있는 프로바이더를 쓴다면 이 단계는 필드 몇 개 채우는 것으로 끝납니다 — 즉 “어떤 리랭커를 쓰느냐”가 로우코드 여부를 가르는 실질적 기준이었습니다.
5. Flow as API — 배포는 확실히 편해진다
파이프라인을 다 연결한 뒤 Flow as API 기능으로 즉시 REST 엔드포인트를 얻었습니다.[2] 1편에서는 FastAPI로 직접 서버를 감싸는 코드를 따로 작성했는데, 이 부분은 Langflow가 확실히 시간을 아껴줬습니다. 사내 챗봇 UI나 Slack 봇에서 이 엔드포인트를 호출하기만 하면 되므로, “파이프라인을 프로덕션에 연결하는” 마지막 단계는 로우코드 쪽이 명백히 유리했습니다.
코드 vs 로우코드, 언제 뭘 쓸까
| 기준 | LangChain 코드 | Langflow |
|---|---|---|
| 검색 로직이 이미 정교하게 정해져 있음(하이브리드+RRF 등) | 적합 | 커스텀 컴포넌트로 우회 필요 |
| 파라미터를 비개발자가 자주 튜닝해야 함 | 배포 필요 | 캔버스에서 즉시 조정 |
| 빠른 프로토타입, PoC | 초기 설정 부담 | 빠름 |
| API 서빙까지 직접 설계·통제하고 싶음 | 적합 | Flow as API로 충분한 경우가 많음 |
| 버전 관리(Git diff로 리뷰) | 자연스러움 | Flow 파일(JSON) diff는 가독성이 떨어짐 |
자주 하는 실수
- 모든 로직을 로우코드로 밀어붙이려 하기: 하이브리드 검색처럼 이미 코드로 검증된 로직이 있다면, 억지로 네이티브 컴포넌트에 끼워 맞추기보다 Custom Component로 그대로 옮기는 게 낫습니다.
- Flow JSON을 형상관리 없이 UI에서만 관리하기: Langflow의 Flow는 JSON으로 export할 수 있습니다. 이걸 Git에 커밋해두지 않으면, 누가 캔버스에서 뭘 바꿨는지 추적할 방법이 없습니다.
- 프로바이더 지원 여부를 먼저 확인하지 않고 설계하기: 리랭커·벡터DB처럼 “이미 통합돼 있는지”가 작업량을 크게 좌우하는 컴포넌트는, 파이프라인을 그리기 전에 지원 목록부터 확인하는 게 시간을 아낍니다.
정리
- 파이프라인의 뼈대(인제스천 → 청킹 → 임베딩 → 검색 → 리랭킹 → 생성)는 Langflow 컴포넌트로 대부분 표현됩니다.
- 다만 1편 수준의 정교한 하이브리드 검색·RRF 융합은 네이티브 컴포넌트만으로는 안 되고, Custom Component(Python)로 빠져야 했습니다.
- 반대로 API 배포는 Langflow가 확실히 더 빠릅니다.
- 결론: “검색 로직은 코드로 확정하고, 그 위의 조립·운영·배포는 로우코드로” 가져가는 하이브리드 구성이 가장 현실적이었습니다.
