Langchain
contents
LLM(GPT-4나 Claude 같은 모델)이 인공지능의 "엔진"이라면, 랭체인은 그 엔진을 운전 가능한 자동차로 만들어주는 섀시(차대), 스티어링 휠, 그리고 GPS라고 할 수 있습니다.
1. 개요 (High-Level Overview)
랭체인은 2022년 말 해리슨 체이스(Harrison Chase)가 만든 오픈 소스 프레임워크입니다. 대형 언어 모델(LLM)을 기반으로 하는 애플리케이션 생성을 단순화하도록 설계되었습니다.
- 문제점: 독립된 LLM은 본질적으로 텍스트 예측기에 불과합니다. 상태가 없고(과거 대화를 기억하지 못함), 시간에 멈춰 있으며(학습 데이터 기준일까지만 앎), 행동을 할 수 없습니다(웹 검색이나 데이터베이스 쿼리 등 불가).
- 해결책 (랭체인): LLM을 다른 연산이나 지식 소스에 연결할 수 있는 표준 인터페이스를 제공합니다. LLM에 기억(Memory), 도구(Tools), 그리고 문맥(Context) 을 부여합니다.
- 주요 언어: 파이썬(Python) 및 자바스크립트/타입스크립트(JavaScript/TypeScript) (나중에 설명하겠지만 Java 버전도 존재합니다).
2. 핵심 개념 (구성 요소)
랭체인은 몇 가지 기초적인 추상화(Abstractions) 위에 구축되었습니다. 이들을 이해하는 것이 프레임워크를 이해하는 핵심입니다.
A. 모델 및 I/O (Models & I/O)
랭체인은 시중의 거의 모든 LLM을 위한 범용 API 래퍼(Wrapper)를 제공합니다. OpenAI, Anthropic, Google Gemini, 또는 로컬 Llama 3 모델 중 무엇을 호출하든 랭체인 코드는 정확히 동일해 보입니다.
- 프롬프트 템플릿 (Prompt Templates): 텍스트를 하드코딩하는 대신 변수가 있는 템플릿을 사용합니다 (예:
"{language_a} 텍스트를 {language_b}로 번역하세요: {text}"). - 출력 파서 (Output Parsers): LLM은 원시 문자열(Raw strings)을 출력합니다. 파서는 LLM의 출력을 구조화된 데이터(JSON이나 파이썬 객체 등)로 강제하여 백엔드에서 실제로 사용할 수 있게 해줍니다.
B. 검색 (RAG - 검색 증강 생성, Retrieval-Augmented Generation)
이것은 랭체인의 가장 인기 있는 사용 사례입니다. 모델을 재학습시키지 않고도 LLM이 개인 데이터(PDF, 데이터베이스, 웹사이트)와 대화할 수 있게 해줍니다.
- 문서 로더 (Document Loaders): S3, Notion, PDF, SQL 등에서 데이터를 읽어옵니다.
- 텍스트 분할기 (Text Splitters): 큰 문서를 작은 조각으로 나눕니다 (LLM에는 컨텍스트 제한이 있기 때문입니다).
- 임베딩 및 벡터 저장소 (Embeddings & Vector Stores): 그 조각들을 숫자의 배열(벡터)로 변환하여 Pinecone, Chroma, pgvector 같은 데이터베이스에 저장합니다.
C. 체인 (Chains)
체인은 이러한 구성 요소들을 순서대로 묶어줍니다.
- 예시:
PromptTemplate->LLM->OutputParser를 체인으로 연결합니다. - LCEL (LangChain Expression Language): 파이프 연산자(
|)를 사용하여 체인을 작성하는 현대적인 방식입니다. 리눅스 터미널의 파이프라인과 매우 유사해 보입니다.
D. 에이전트 (Agents, "두뇌")
체인이 하드코딩된 순서대로 실행되는 반면, 에이전트는 LLM을 추론 엔진으로 사용하여 어떤 행동을 어떤 순서로 취할지 결정 합니다.
- 에이전트에게 목표와 도구(Tools) 목록(예: Google 검색, 파이썬 인터프리터, SQL 데이터베이스 연결)을 줍니다.
- LLM은 ReAct(Reasoning and Acting) 같은 프레임워크를 사용하여 이렇게 사고합니다: "서울의 날씨를 알아야겠다. 날씨 도구를 사용해야지. [도구가 데이터를 반환함]. 이제 데이터를 얻었으니 최종 답변을 생성해야겠다."
3. 현대적 생태계
랭체인은 단일 라이브러리에서 거대한 엔터프라이즈 제품군으로 성장했습니다.
| 구성 요소 | 역할 |
|---|---|
| LangChain (Core) | 기초적인 추상화 및 LCEL 라우팅 아키텍처. |
| LangChain-Community | 수천 개의 서드파티 통합 (VectorDB, 문서 로더, 특정 LLM 래퍼 등). |
| LangGraph | 새로운 표준. 복잡하고 다중 액터(Multi-actor)가 참여하며 상태를 유지하는(Stateful) 에이전트를 구축하기 위한 확장 기능입니다. AI 워크플로우를 순환형 그래프로 모델링하여 루프(Loops), 메모리 영속성, 그리고 사람의 개입(Human-in-the-loop)을 허용합니다. |
| LangSmith | LLM을 위한 DevOps 플랫폼. 복잡한 체인 내부에서 정확히 무엇이 잘못되었는지 검사, 디버깅, 테스트할 수 있게 해줍니다 (예: "LLM이 왜 여기서 환각(Hallucination)을 일으켰지? 아, VectorDB가 잘못된 문서를 가져왔구나"). |
| LangServe | 랭체인 코드를 스트리밍이 내장된 FastAPI REST 서버로 즉시 래핑해주는 도구. |
4. 코드 예제 (현대적인 LCEL 파이썬)
LCEL을 사용하는 현대적인 랭체인의 모습입니다. 맨 아래의 파이프라인(chain)이 얼마나 깔끔한지 확인해 보세요.
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
# 1. 모델 정의
model = ChatOpenAI(model="gpt-4o", temperature=0)
# 2. 프롬프트 템플릿 정의
prompt = ChatPromptTemplate.from_messages([
("system", "당신은 {topic}에 대한 전문가입니다."),
("user", "{topic}에 관한 흥미로운 사실을 알려주세요.")
])
# 3. 출력 파서 정의 (깔끔한 문자열만 반환하도록 보장)
parser = StrOutputParser()
# 4. LCEL(파이프 구문)을 사용하여 체인 생성
chain = prompt | model | parser
# 5. 체인 실행
result = chain.invoke({"topic": "양자 물리학"})
print(result)
5. 요약: 장점과 단점
거대한 프레임워크가 응당 그렇듯, 랭체인도 찬사와 비판을 동시에 받습니다.
장점
- 배터리 포함 (Batteries Included): 벡터 데이터베이스, 도구, 또는 LLM이 존재한다면, 랭체인에는 이미 이를 위한 사전 구축된 통합(Integration) 코드가 있습니다.
- 빠른 출시 (Speed to Market): 50줄 미만의 코드로 완벽하게 작동하는 RAG 애플리케이션을 구축할 수 있습니다.
- 교체 용이성 (Swappability): 코드 단 한 줄만 변경하면 OpenAI에서 Google Gemini로 모델을 전환할 수 있습니다.
단점
- 무거운 추상화: 비판하는 사람들은 랭체인이 내부 동작을 너무 많이 숨긴다고 지적합니다. 에러가 발생했을 때 스택 트레이스(Stack traces)가 너무 길고 읽기 어렵기로 악명이 높습니다.
- 오버 엔지니어링: 단순한 LLM 호출을 위해 랭체인을 사용하는 것은 호두를 까기 위해 해머를 쓰는 것과 같습니다. (많은 순수주의자들은 단순한 앱에는 공식 OpenAI나 Anthropic의 SDK를 사용하는 것을 선호합니다).
- 빠른 기능 폐기 (Deprecations): AI 분야가 너무 빨리 변하기 때문에 랭체인도 매우 빠르게 진화했으며, 이로 인해 2023년의 튜토리얼 코드가 2024년이나 2025년에는 완전히 작동하지 않는 경우가 많습니다.
6. Java와의 연결 고리: LangChain4j
Java 커뮤니티에서 LangChain4j를 구축했습니다. 랭체인과 정확히 동일한 개념(Models, Memory, RAG, Chains)을 JVM 환경으로 가져왔습니다. 엔터프라이즈 환경에 고도로 최적화되어 있으며 Spring Boot 및 Quarkus와 네이티브 수준으로 통합됩니다.
예를 들어, LangChain4j를 사용하는 Spring Boot에서는 @AiService 어노테이션을 추가하는 것만으로 Java 인터페이스를 LLM 에이전트로 탈바꿈시킬 수 있습니다.
과거에는 Java로 RAG 시스템을 구축하는 작업이 매우 수동적이고 번거로웠지만, LangChain4j와 Spring Boot를 결합하면 놀랍도록 우아하게 처리할 수 있습니다. Spring의 의존성 주입(Dependency Injection)과 LangChain4j의 선언적 @AiService 인터페이스를 활용하면, 최소한의 보일러플레이트 코드만으로 프로덕션 수준의 파이프라인을 구축할 수 있습니다.
텍스트 문서를 읽고, 인메모리 벡터 데이터베이스에 저장한 다음, 엄격하게 해당 문서를 기반으로 사용자의 질문에 답변하는 기본적인 RAG 시스템 구현에 대한 단계별 가이드입니다.
1. 의존성 추가 (pom.xml)
LangChain4j Spring Boot 스타터, LLM 프로바이더(이 예제에서는 OpenAI 사용), 그리고 문서 파서가 필요합니다.
org.springframework.boot
spring-boot-starter-web
dev.langchain4j
langchain4j-open-ai-spring-boot-starter
0.30.0
dev.langchain4j
langchain4j-core
dev.langchain4j
langchain4j-document-parser-text
2. 속성 설정 (application.yml)
속성 파일에 API 키를 제공하면 Spring Boot 스타터가 ChatLanguageModel 및 EmbeddingModel 빈(Bean)을 자동으로 생성합니다.
langchain4j:
open-ai:
chat-model:
api-key: ${OPENAI_API_KEY}
model-name: gpt-4o-mini
temperature: 0.0
embedding-model:
api-key: ${OPENAI_API_KEY}
model-name: text-embedding-3-small
3. 벡터 저장소 및 RAG 빈(Bean) 설정 (RagConfig.java)
Spring Boot에 정보를 검색하는 방법을 알려주어야 합니다. LangChain4j는 대화 중에 관련 문서 조각(청크)을 가져오기 위해 ContentRetriever 인터페이스를 사용합니다.
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.rag.content.retriever.ContentRetriever;
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RagConfig {
// 1. 인메모리 벡터 데이터베이스 생성
@Bean
public EmbeddingStore embeddingStore() {
return new InMemoryEmbeddingStore<>();
}
// 2. 리트리버(검색기) 설정
// AI가 임베딩 모델을 사용하여 벡터 저장소를 검색하는 방법을 정의합니다.
@Bean
public ContentRetriever contentRetriever(EmbeddingStore embeddingStore,
EmbeddingModel embeddingModel) {
return EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.maxResults(3) // 가장 관련성이 높은 상위 3개의 단락을 가져옵니다.
.minScore(0.7) // 관련성이 매우 높은 결과만 가져옵니다.
.build();
}
}
4. 선언적 AI 서비스 생성 (CorporateAssistant.java)
이 부분이 LangChain4j의 진가가 발휘되는 곳입니다. 복잡한 API 호출 코드를 작성하는 대신, 표준 Java 인터페이스를 정의합니다. @AiService 어노테이션은 LangChain4j가 런타임에 이 인터페이스를 동적으로 구현하도록 지시합니다.
3단계에서 ContentRetriever 빈을 정의했기 때문에, LangChain4j는 이를 자동으로 주입하여 이 표준 챗봇을 RAG 기반 에이전트로 변환합니다.
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.spring.AiService;
@AiService
public interface CorporateAssistant {
@SystemMessage({
"You are a polite corporate assistant.",
"Answer the user's question using ONLY the provided context.",
"If the answer is not in the context, say 'I do not have enough information to answer that.'"
})
String chat(String userMessage);
}
5. 데이터 수집(Ingestion) 서비스 생성 (DataIngestionService.java)
AI가 질문에 답하기 전에 벡터 저장소에 문서를 로드해야 합니다. 실제 앱에서는 사용자가 PDF를 업로드할 때 이 작업이 발생할 수 있습니다. 이 예제에서는 애플리케이션 시작(Startup) 시점에 문서를 로드하도록 트리거하겠습니다.
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.loader.FileSystemDocumentLoader;
import dev.langchain4j.data.document.splitter.DocumentSplitters;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Service;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.nio.file.Paths;
@Service
public class DataIngestionService {
private final EmbeddingStore embeddingStore;
private final EmbeddingModel embeddingModel;
public DataIngestionService(EmbeddingStore embeddingStore, EmbeddingModel embeddingModel) {
this.embeddingStore = embeddingStore;
this.embeddingModel = embeddingModel;
}
@PostConstruct
public void ingestData() {
// 1. 문서 로드 (회사 내규가 적힌 텍스트 파일을 가정해 보세요)
Document document = FileSystemDocumentLoader.loadDocument(getFilePath("company-policy.txt"));
// 2. 수집기(Ingestor) 파이프라인 생성
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
.documentSplitter(DocumentSplitters.recursive(500, 50)) // 500 토큰 크기의 조각으로 나누고 50 토큰씩 겹치게 설정
.embeddingModel(embeddingModel) // 텍스트를 벡터로 변환
.embeddingStore(embeddingStore) // 데이터베이스에 저장
.build();
// 3. 수집 실행
ingestor.ingest(document);
System.out.println("벡터 저장소에 문서가 성공적으로 저장되었습니다!");
}
private Path getFilePath(String fileName) {
try {
return Paths.get(getClass().getClassLoader().getResource(fileName).toURI());
} catch (URISyntaxException | NullPointerException e) {
throw new RuntimeException("파일을 찾을 수 없습니다: " + fileName);
}
}
}
6. REST 엔드포인트 노출 (ChatController.java)
마지막으로 CorporateAssistant를 표준 Spring @RestController에 연결하여 프론트엔드에 노출합니다.
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ChatController {
private final CorporateAssistant assistant;
// Spring이 LangChain4j에 의해 생성된 동적 프록시를 주입합니다.
public ChatController(CorporateAssistant assistant) {
this.assistant = assistant;
}
@GetMapping("/api/chat")
public String chat(@RequestParam String message) {
// 이 단 한 줄의 코드는 내부적으로 다음 작업을 수행합니다:
// 1. 사용자 메시지를 임베딩(벡터화)합니다.
// 2. InMemoryEmbeddingStore에서 일치하는 상위 3개의 조각을 쿼리합니다.
// 3. 해당 조각들을 LLM 프롬프트에 주입(Context)합니다.
// 4. OpenAI GPT-4o-mini를 호출합니다.
// 5. 생성된 문자열을 반환합니다.
return assistant.chat(message);
}
}
references