우와한 개발자

[LangChain] LangChain이란? — LCEL, Prompt Template, Few-shot, Output Parser

by 우와한개발자

1. LangChain과 LangGraph

1) LangChain

(1) 개념

  • LangChain은 LLM 기반 애플리케이션을 구축하기 위한 프레임워크이다.
  • Prompt, Model, Parser, Retriever, Tool, Memory 등을 연결하여 하나의 파이프라인으로 구성할 수 있다.
  • 가장 큰 사용 이유는 LLM 애플리케이션에 필요한 생태계를 통합하기 위해서이다.

(2) 주요 기능

  • LLM, Prompt, Memory를 연결하는 체인 구성
  • Vector DB와 연동한 RAG 지원
  • 외부 API와 함수를 연결하는 Tool 통합
  • 다양한 모델, DB, 프레임워크와의 생태계 통합

(3) 기본 파이프라인

Prompt → Model → Output

(4) 주요 구성요소

  • Model: LLM 또는 Chat Model을 연결한다.
  • Prompt: 모델에 전달할 입력을 설계한다.
  • Parser: 모델 출력을 구조화한다.
  • Tool: 외부 API 및 함수를 연결한다.
  • Retriever: Vector DB에서 관련 문서를 검색한다.
  • Agent: 도구 선택 및 작업 자동화를 수행한다.

 

2) LangGraph

(1) 개념

  • LangGraph는 Agent 워크플로우를 그래프 구조로 설계하기 위한 프레임워크이다.
  • LangChain보다 복잡한 상태 관리, 조건 분기, 반복 실행, Multi-Agent 구조를 설계하는 데 적합하다.

(2) 그래프 기반 설계

  • 노드와 엣지로 Agent 흐름을 명확히 정의한다.
  • 각 노드는 하나의 작업 단계를 의미하고, 엣지는 다음 실행 흐름을 의미한다.

(3) 상태 관리

  • 공유 상태 객체를 기반으로 복잡한 흐름을 제어한다.
  • 이전 단계의 결과를 다음 단계에서 사용할 수 있다.

(4) Multi-Agent 지원

  • 여러 Agent 간 협업과 오케스트레이션을 구현할 수 있다.
  • 예를 들어 검색 Agent, 분석 Agent, 보고서 작성 Agent를 분리하여 협업하게 만들 수 있다.

(5) 디버깅 용이

  • 실행 과정을 시각화하고 단계별로 추적할 수 있다.
  • 복잡한 Agent 흐름에서 어느 단계에서 문제가 발생했는지 확인하기 쉽다.

 

2. LangChain 구현 기초

1) 라이브러리 설치

(1) requirements.txt 예시

langchain
langchain-openai
langchain-community
langchain-core
langchain-chroma
langgraph
chromadb
pypdf
tiktoken
python-dotenv
streamlit
duckduckgo-search

 

2) 기본 구조

  • prompt에 중괄호 {}로 작성한 변수는 invoke 시 Python 딕셔너리 형태로 값을 할당한다.
  • prompt | llm은 해당 프롬프트를 LLM에게 전달한다는 의미이다.
  • LangChain에서는 | 연산자를 사용하여 Prompt, Model, Parser 등을 연결한다.
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate

load_dotenv()

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)

prompt = ChatPromptTemplate.from_template(
    """
당신은 AI 전문가입니다.

질문:
{question}
"""
)

chain = prompt | llm

result = chain.invoke(
    {"question": "LangChain을 설명해주세요"}
)

print(result.content)

 

3) LCEL(LangChain Expression Language)

(1) 개념

  • LCEL은 LangChain Expression Language의 약자이다.
  • LangChain의 파이프라인을 선언적으로 정의할 수 있는 표준 문법이다.
  • | 연산자로 구성요소를 연결한다.

(2) 예시

chain = prompt | llm | parser

(3) 특징

  • 비동기 실행을 지원한다.
  • 스트리밍을 지원한다.
  • 배치 처리를 지원한다.
  • Spring AI에서 메서드를 .으로 연결해 chaining하는 것과 비슷하게 이해할 수 있다.

 

3. Prompt Engineering

1) 작성 방법

(1) 구체적이고 간결하게 지시

  • 모델이 해야 할 일을 명확하게 작성한다.
  • 애매한 표현보다 원하는 역할, 조건, 출력 형식을 구체적으로 제시한다.

(2) 역할 부여

  • 모델에게 특정 역할을 부여하면 답변의 방향이 명확해진다.
  • 예를 들어 “당신은 Python 전문가입니다.”처럼 작성할 수 있다.

(3) 출력 형식 명시

  • JSON, 표, 목록 등 원하는 출력 형식을 명시한다.
  • 애플리케이션에서 결과를 활용해야 한다면 구조화된 형식이 중요하다.

(4) Few-shot 예시 제공

  • 몇 가지 예시를 제공하면 모델이 원하는 응답 패턴을 더 잘 따른다.
  • Zero-shot보다 일관되고 정확한 응답을 얻을 수 있다.

 

2) Prompt Template

(1) 개념

  • 프롬프트를 재사용 가능한 형태로 정의하는 방식이다.
  • {변수명}을 사용하여 런타임에 값을 주입할 수 있다.

(2) 특징

  • 변수 삽입: {변수명}으로 런타임 값 주입
  • 역할 분리: system / human / ai 메시지 구분
  • 재사용성: 하나의 템플릿으로 다양한 입력 처리

(3) 코드 예시

from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
    ("system", "당신은 {role}입니다."),
    ("human", "{question}")
])

chain = prompt | llm

response = chain.invoke({
    "role": "Python 전문가",
    "question": "리스트 내포란?"
})

 

3) Few-shot Prompting

(1) 개념

  • Few-shot Prompting은 모델에 몇 가지 예시를 제공하여 원하는 출력 패턴을 학습시키는 기법이다.
  • 모델을 실제로 재학습시키는 것은 아니지만, 프롬프트 안의 예시를 참고하여 비슷한 형식으로 답변하게 만든다.

(2) 코드 예시

from langchain_core.prompts import ChatPromptTemplate

examples = [
    ("서울", "대한민국"),
    ("도쿄", "일본"),
    ("파리", "프랑스")
]

messages = [
    ("system", "도시에 맞는 국가를 답하세요.")
]

for city, country in examples:
    messages.append(("human", f"{city}는 어느 나라의 도시인가요?"))
    messages.append(("ai", country))

messages.append(("human", "{city}는 어느 나라의 도시인가요?"))

prompt = ChatPromptTemplate.from_messages(messages)

chain = prompt | llm

response = chain.invoke({
    "city": "뉴욕"
})

 

4. Output Parser와 구조화 출력

1) Output Parser 

(1) 개념

  • Output Parser는 LLM의 원시 텍스트 출력을 애플리케이션이 처리하기 좋은 구조화된 형태로 변환하는 도구이다.
  • 단순 문자열이 아니라 JSON, 객체, 리스트 등의 형태로 변환할 수 있다.

(2) 필요한 이유

  • LLM의 응답은 기본적으로 자연어 텍스트이다.
  • 애플리케이션에서 사용하려면 필드명, 타입, 구조가 일정해야 한다.
  • Output Parser를 사용하면 LLM 응답을 코드에서 다루기 쉬운 형태로 변환할 수 있다.

(3) 출력 형태 예시

  • 문자열 출력: LLM의 원시 텍스트를 그대로 반환
  • JSON 출력: 구조화된 JSON 형식으로 변환
  • Pydantic 객체 출력: 타입이 정의된 객체로 변환

 

2) Pydantic

(1) 개념

  • Pydantic은 Python의 데이터 유효성 검증 및 직렬화 라이브러리이다.
  • LLM 출력을 타입 안전하고 구조화된 객체로 변환할 때 사용할 수 있다.

(2) Java와 비교

  • Java의 DTO나 record 타입처럼, 정해진 필드와 타입을 가진 데이터 객체를 정의하는 방식과 비슷하다.
  • 예를 들어 name: str, age: int처럼 필드 타입을 미리 정의하면, LLM의 출력이 해당 구조에 맞는지 검증할 수 있다.

 

3) PydanticOutputParser

(1) 개념

  • PydanticOutputParser는 LLM의 응답을 Pydantic 모델에 맞게 변환한다.
  • Parser 객체를 직접 생성하고, format_instructions를 프롬프트에 포함하여 LLM에게 출력 형식을 지시한다.

(2) 주의점

  • LLM 응답의 필드명이 Pydantic 모델의 필드명과 일치해야 매핑된다.
  • LLM 응답에 필요한 필드가 없거나 필드명이 다르면 파싱 오류가 발생할 수 있다.
  • 필드 누락을 허용하고 싶다면 Optional 또는 기본값을 지정할 수 있다.

(3) 코드 예시

from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field

load_dotenv()

class Answer(BaseModel):
    name: str = Field(description="기술 이름")
    description: str = Field(description="기술 설명")
    use_cases: str = Field(description="사용 사례")

parser = PydanticOutputParser(pydantic_object=Answer)

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)

prompt = ChatPromptTemplate.from_template(
    """
당신은 AI 전문가입니다.
다음 기술을 설명해주세요.

질문:
{question}

반드시 아래 형식으로 답변하세요.
{format_instructions}
"""
)

chain = prompt | llm | parser

result = chain.invoke({
    "question": "LangChain을 설명해주세요",
    "format_instructions": parser.get_format_instructions()
})

print(result)
print(result.name)
print(result.description)
print(result.use_cases)

print(result.model_dump())

 

4) with_structured_output()

(1) 개념

  • with_structured_output()은 LLM에 Pydantic 모델을 직접 연결하여 구조화된 출력을 받는 방식이다.
  • PydanticOutputParser처럼 별도의 Parser 객체를 만들지 않아도 된다.
  • format_instructions를 프롬프트에 직접 넣지 않아도 된다.

(2) 특징

  • 코드가 간단하다.
  • 구조화된 응답을 실무에서 빠르게 적용하기 좋다.
  • 모델이 지원하는 구조화 출력 방식에 따라 동작한다.

(3) 코드 예시

from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from pydantic import BaseModel, Field

load_dotenv()

class Answer(BaseModel):
    name: str = Field(description="기술 이름")
    description: str = Field(description="기술 설명")
    use_cases: str = Field(description="사용 사례")

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)

structured_llm = llm.with_structured_output(Answer)

prompt = ChatPromptTemplate.from_template(
    """
당신은 AI 전문가입니다.
다음 기술을 설명해주세요.

질문:
{question}
"""
)

chain = prompt | structured_llm

result = chain.invoke({
    "question": "LangChain을 설명해주세요"
})

print(result)
print(result.name)
print(result.description)
print(result.use_cases)

print(result.model_dump())

 

5) PydanticOutputParser와 with_structured_output() 비교

구분 PydanticOutputParser with_structured_output()
사용 방식 Parser 객체를 직접 생성해서 사용 LLM에 Pydantic 모델을 직접 연결
구조 지정 위치 PydanticOutputParser(pydantic_object=Answer) llm.with_structured_output(Answer)
프롬프트 작성 format_instructions를 프롬프트에 포함해야 함 별도의 format_instructions 작성이 필요 없음
Chain 구성 prompt llm
반환 결과 Pydantic 모델 객체 Pydantic 모델 객체
필드 접근 result.name, result.description처럼 접근 result.name, result.description처럼 접근
장점 Output Parser의 동작 흐름을 이해하기 좋음 코드가 간단하고 실무에서 사용하기 편함
주의점 LLM이 형식을 지키지 않으면 파싱 오류가 발생할 수 있음 모델이 지원하는 구조화 출력 방식에 따라 동작함
적합한 경우 Output Parser 개념을 학습하거나 출력 형식을 명시적으로 제어할 때 구조화된 응답을 간단하게 받고 싶을 때

 

9. 실행 방식

1) invoke

  • 단일 입력에 대해 한 번 실행하고 결과를 반환한다.
  • 가장 기본적인 실행 방식이다.
result = chain.invoke({
    "question": "LangChain이란?"
})

 

2) stream

  • 모델의 응답을 한 번에 받지 않고 토큰 단위 또는 chunk 단위로 순차적으로 받는다.
  • 챗봇에서 답변이 실시간으로 출력되는 것처럼 보이게 할 때 사용한다.
for chunk in chain.stream({
    "question": "LangChain을 설명해주세요."
}):
    print(chunk.content, end="")

 

3) batch

  • 여러 입력을 한 번에 처리한다.
  • 같은 체인을 여러 질문에 반복 적용할 때 사용한다.
results = chain.batch([
    {"question": "LangChain이란?"},
    {"question": "LangGraph란?"},
    {"question": "RAG란?"}
])

 

10. 응답 품질 평가

1) 평가 기준

(1) 정확성

  • 사실 관계와 기술적 내용이 맞는지 평가한다.
  • 잘못된 개념 설명이나 허위 정보가 없는지 확인한다.

(2) 완전성

  • 사용자가 요청한 내용이 모두 포함되었는지 평가한다.
  • 필수 필드, 조건, 형식이 누락되지 않았는지 확인한다.

(3) 일관성

  • 반복 실행 시 출력 형식이 일정한지 평가한다.
  • JSON, 표, 객체 구조 등이 매번 안정적으로 유지되는지 확인한다.

 

2) 일반적인 문제 패턴

(1) 필드 누락

  • 요청한 필드가 응답에 포함되지 않는 문제이다.

(2) 타입 불일치

  • 문자열이어야 하는 값이 숫자로 나오거나, 리스트여야 하는 값이 문자열로 나오는 문제이다.

(3) 중첩 구조 파싱 오류

  • JSON이나 객체 안에 또 다른 객체가 들어가는 구조에서 형식이 깨지는 문제이다.

(4) 환각

  • 실제로 존재하지 않는 정보를 그럴듯하게 생성하는 문제이다.

블로그의 정보

우와한개발자 님의 블로그

우와한개발자

활동하기