티스토리 뷰

반응형

LangChain 체인 연결하기 — Prompt | Model | Parser를 부품처럼 조립하는 법

여기서부터 슬슬 “아, 이래서 LangChain을 쓰는구나” 하는 감각이 옵니다.

이전 글에서는 PromptTemplate을 만들고, structured output으로 결과를 안정적으로 받는 흐름까지 봤죠.
그런데 아직은 약간 이런 느낌이 남아 있습니다.

“프롬프트 만들고, 모델 호출하고, 결과 출력하고… 결국 그냥 순서대로 코드 쓰는 거 아닌가?”

맞아요. 처음엔 그렇게 보입니다.
근데 LangChain의 진짜 재미는 각 단계를 부품처럼 연결해서 재사용 가능한 파이프라인으로 만든다는 데 있어요. LangChain은 모델, 프롬프트, 툴 등을 연결해 애플리케이션과 에이전트를 만드는 프레임워크로 소개되고, 이런 연결 가능한 구성요소들은 Runnable 인터페이스를 기반으로 조합됩니다. (LangChain Docs)

오늘은 바로 그 감각을 잡겠습니다.


왜 체인 연결이 중요한가

주니어 때는 보통 한 파일 안에서 이렇게 많이 짭니다.

prompt = ...
response = model.invoke(prompt)
text = response.content
result = parse_text(text)
print(result)

이 방식은 간단한 실험에는 괜찮아요.
문제는 기능이 늘어날수록입니다.

  • 같은 프롬프트를 다른 모델에도 써보고 싶다
  • 출력 후처리를 다른 곳에서도 재사용하고 싶다
  • 중간 단계를 로그로 확인하고 싶다
  • 입력/출력 형식을 바꿔가며 테스트하고 싶다

이럴 때 각 단계를 하나의 독립 부품으로 보고 연결할 수 있어야 편합니다. LangChain의 modern composition은 prompt | model | parser 같은 파이프 형태를 중심으로 설명되고, RunnableSequence/RunnableParallel 같은 조합형 runnable로 확장됩니다. (LangChain API)


오늘 글에서 얻어갈 것

이번 글에서는 아래 4가지를 확실히 가져가면 됩니다.

  • prompt | model | parser가 정확히 어떤 흐름인지
  • 왜 | 연산자가 중요한지
  • 문자열 파서와 구조화 파서의 역할 차이
  • 나중에 RAG나 Agent로 갈 때 이 감각이 왜 계속 필요한지

솔직히 말하면, 여기서 감각을 못 잡으면 뒤에 가서 계속 헷갈립니다.
반대로 여기만 잡히면 이후 글들이 훨씬 덜 복잡하게 느껴져요.


1. | 는 그냥 문법이 아니라 “데이터 흐름”이다

지난 글에서 잠깐 봤던 이 코드요.

chain = prompt | model

처음 보면 약간 마법처럼 보이죠.
근데 실제로는 꽤 직관적입니다.

  • prompt가 입력값을 받아 프롬프트 메시지를 만든다
  • 그 결과를 model이 받아 응답을 만든다
  • 그 응답을 parser가 받아 최종 형태로 바꾼다

즉, | 는
앞 단계의 출력을 다음 단계의 입력으로 넘기는 파이프라인 연결자라고 생각하면 됩니다. LangChain의 runnable 조합은 이런 형태를 기본으로 하고, 각 단계는 공통 인터페이스를 통해 invoke 같은 방식으로 실행됩니다. (LangChain API)

이걸 이해하면, LangChain 코드가 갑자기 덜 추상적으로 보여요.
결국 “연결”일 뿐이니까요.


2. 가장 기본적인 체인: Prompt | Model

아주 작은 코드부터 보겠습니다.

pip install -U "langchain[openai]"
import os
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate


def main() -> None:
    if not os.environ.get("OPENAI_API_KEY"):
        raise ValueError("OPENAI_API_KEY 환경변수가 설정되어 있지 않습니다.")

    model = init_chat_model("gpt-5-nano", model_provider="openai")

    prompt = ChatPromptTemplate.from_messages(
        [
            ("system", "너는 친절한 Python 멘토다."),
            ("human", "{topic}을 주니어 개발자 눈높이로 설명해줘."),
        ]
    )

    chain = prompt | model

    response = chain.invoke({"topic": "LangChain chain"})
    print(response.content)


if __name__ == "__main__":
    main()

여기서 chain.invoke(...)를 호출하면 실제로는:

  1. {topic} 값이 prompt에 들어가고
  2. prompt가 메시지 리스트를 만들고
  3. model이 그 메시지를 받아 응답을 생성합니다

모델 호출의 기본은 invoke()이고, prompt는 ChatPromptTemplate.from_messages(...) 같은 방식으로 구성할 수 있습니다. 이전 공식 가이드들과 레퍼런스도 이 패턴을 반복적으로 보여줍니다. (LangChain Docs)


3. 그런데 왜 Parser가 필요한가

여기서 많은 분이 한 번 묻습니다.

“이미 response.content가 있는데, 파서가 왜 필요하죠?”

좋은 질문이에요.
파서는 쉽게 말하면 모델 응답을 내가 원하는 최종 형태로 바꾸는 단계입니다.

예를 들어 모델은 보통 AIMessage 같은 객체 형태로 응답을 줍니다.
그런데 우리 애플리케이션은 상황에 따라 원하는 게 다르죠.

  • 그냥 문자열만 필요할 수도 있고
  • JSON 문자열이 필요할 수도 있고
  • 리스트가 필요할 수도 있고
  • Pydantic 객체가 필요할 수도 있습니다

그 차이를 정리해주는 게 parser입니다.


4. 가장 쉬운 Parser: 문자열 파서

반응형

먼저 제일 단순한 것부터요.
모델이 준 응답에서 텍스트만 꺼내기입니다.

import os
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser


def main() -> None:
    if not os.environ.get("OPENAI_API_KEY"):
        raise ValueError("OPENAI_API_KEY 환경변수가 설정되어 있지 않습니다.")

    model = init_chat_model("gpt-5-nano", model_provider="openai")

    prompt = ChatPromptTemplate.from_messages(
        [
            ("system", "너는 친절한 기술 블로그 멘토다."),
            ("human", "{topic}을 3문장으로 설명해줘."),
        ]
    )

    parser = StrOutputParser()

    chain = prompt | model | parser

    result = chain.invoke({"topic": "LangChain LCEL"})
    print(result)
    print(type(result))


if __name__ == "__main__":
    main()

이 코드에서 result는 이제 str입니다.
즉, response.content를 직접 꺼내는 대신 체인 끝에서 문자열로 정리된 최종 결과를 받는 거예요.

이게 왜 좋냐면, 이후 코드가 훨씬 단순해집니다.

  • 모델 메시지 객체를 신경 안 써도 됨
  • 후속 함수 입력 타입이 명확해짐
  • 테스트가 쉬워짐

5. prompt | model | parser 를 말로 풀어보면

이 흐름을 진짜 익히려면, 코드가 아니라 한국말로도 풀 수 있어야 합니다.

예를 들어:

chain = prompt | model | parser

이건 사실 이런 뜻이에요.

  • Prompt: “입력값을 받아, 모델이 이해할 메시지로 바꾼다”
  • Model: “그 메시지를 받아, 응답을 생성한다”
  • Parser: “그 응답을 애플리케이션이 쓰기 좋은 형태로 바꾼다”

여기까지 오면 이제 LangChain이 왜 단순 SDK 호출과 다른지 느껴집니다.
단순 호출은 “한 번 요청하고 끝”에 가깝고, 체인 연결은 “입출력 변환 단계를 조립하는 방식”에 가깝거든요.


6. 실무형 예제: 블로그 소제목 생성 체인

그냥 개념만 보면 재미없으니까, 조금 현실적인 예제를 보죠.

목표

주제를 입력하면, 소제목 5개를 줄바꿈 문자열로 받는다.

import os
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser


def main() -> None:
    if not os.environ.get("OPENAI_API_KEY"):
        raise ValueError("OPENAI_API_KEY 환경변수가 설정되어 있지 않습니다.")

    model = init_chat_model("gpt-5-nano", model_provider="openai")

    prompt = ChatPromptTemplate.from_messages(
        [
            ("system", "너는 IT 블로그 기획을 돕는 에디터다."),
            (
                "human",
                "주제: {topic}\n"
                "대상 독자: {reader}\n"
                "요청: 블로그 소제목 5개를 줄바꿈으로만 작성해줘.",
            ),
        ]
    )

    chain = prompt | model | StrOutputParser()

    result = chain.invoke(
        {
            "topic": "LangChain output parser",
            "reader": "주니어 백엔드 개발자",
        }
    )

    print(result)


if __name__ == "__main__":
    main()

이 예제는 아주 단순하지만, 벌써 체인의 장점이 보입니다.

  • prompt는 입력 구조를 담당
  • model은 생성 담당
  • parser는 결과 정리 담당

역할이 딱 나뉘죠.


7. 그런데 Parser는 만능이 아니다

여기서 하나 솔직하게 짚고 갈게요.

Parser는 편하지만, 항상 제일 안정적인 해법은 아닙니다.
LangChain 문서의 OUTPUT_PARSING_FAILURE 안내도 출력 파서가 모델 응답을 기대한 형식으로 처리하지 못할 수 있다고 설명하고, 가능한 경우에는 tool calling이나 structured output 같은 더 안정적인 방식을 고려하라고 권장합니다. (LangChain Docs)

이 말은 무슨 뜻이냐면:

  • 단순 문자열 후처리에는 parser가 좋다
  • 하지만 엄격한 JSON/스키마가 필요하면 structured output이 더 낫다

즉, parser는 여전히 유용하지만
정확한 계약이 필요한 서비스 응답에서는 structured output 쪽이 더 믿을 만한 경우가 많습니다. 이건 지난 글에서 본 내용과도 이어집니다. (LangChain Docs)


8. 그럼 Structured Output이 있는데 Parser를 왜 배우나

이건 꽤 중요한 포인트입니다.

“어차피 structured output이 더 안정적이면, parser는 안 배워도 되는 거 아닌가요?”

그건 아닙니다.
왜냐면 parser는 단지 “JSON 만들기” 용도가 아니거든요.

parser가 유용한 상황은 많습니다.

  • 결과를 단순 문자열로 정리할 때
  • 모델 응답에서 특정 형식만 추출할 때
  • 중간 단계 후처리를 가볍게 할 때
  • structured output까지 갈 정도로 무겁지 않은 기능일 때

즉, parser는 가벼운 후처리용 도구로 생각하면 좋아요.
반면 structured output은 엄격한 스키마 계약용 도구에 더 가깝습니다.

이 둘은 경쟁 관계라기보다, 용도가 다른 편입니다.


9. 체인이 좋다는 건 결국 “부품 교체가 쉽다”는 뜻이다

이제부터 중요한 감각 하나.

체인이 좋은 이유는 멋있어 보여서가 아닙니다.
교체가 쉽기 때문이에요.

예를 들어 지금 코드가 이렇게 있다고 해볼게요.

chain = prompt | model | StrOutputParser()

여기서 나중에 바꾸고 싶을 수 있습니다.

  • 모델만 더 좋은 걸로 바꾸기
  • parser만 구조화 쪽으로 바꾸기
  • prompt만 더 엄격하게 바꾸기

즉, 전체를 다시 짜는 게 아니라
한 부품만 갈아끼울 수 있다는 게 핵심입니다.

이 감각이 쌓이면 나중에 RAG도 그렇게 보이기 시작해요.

  • retriever 추가
  • 문서 포맷터 추가
  • reranker 추가
  • parser 교체

결국 전부 같은 철학입니다.


10. 한 단계 더: 입력을 함수처럼 생각해보기

체인을 제대로 쓰려면, 입력도 함수 인자처럼 봐야 합니다.

예를 들어 이런 체인이 있다고 합시다.

chain = prompt | model | StrOutputParser()

그러면 chain.invoke({...}) 는 거의 이런 함수처럼 느껴져야 해요.

def generate_blog_outline(topic: str, reader: str) -> str:
    ...

이렇게 느껴지기 시작하면 LangChain 코드가 훨씬 덜 어지럽습니다.
체인도 결국 입력을 받아 출력을 반환하는 컴포넌트라는 뜻이니까요.


11. 실무 감각 예제: 체인을 함수처럼 감싸기

이제 실제 코드로 그렇게 만들어볼게요.

import os
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser


def build_outline_chain():
    model = init_chat_model("gpt-5-nano", model_provider="openai")

    prompt = ChatPromptTemplate.from_messages(
        [
            ("system", "너는 기술 블로그 기획 전문가다."),
            (
                "human",
                "주제: {topic}\n"
                "독자: {reader}\n"
                "요청: 소제목 5개와 각 소제목의 한 줄 설명을 작성해줘.",
            ),
        ]
    )

    return prompt | model | StrOutputParser()


def generate_outline(topic: str, reader: str) -> str:
    chain = build_outline_chain()
    return chain.invoke({"topic": topic, "reader": reader})


def main() -> None:
    if not os.environ.get("OPENAI_API_KEY"):
        raise ValueError("OPENAI_API_KEY 환경변수가 설정되어 있지 않습니다.")

    result = generate_outline(
        topic="LangChain chain",
        reader="생성형 AI를 처음 배우는 주니어 개발자",
    )
    print(result)


if __name__ == "__main__":
    main()

이렇게 해두면 나중에 진짜 서비스 코드에 넣기 쉬워집니다.
FastAPI든, CLI든, 배치 스크립트든 재사용하기 편해지죠.


12. 체인을 쓰다 보면 만나게 되는 오류

이건 미리 아는 게 좋습니다.

1) 입력 변수 누락

{topic}이 prompt에 있는데 invoke()에서 안 넘기면 당연히 실패합니다.

2) parser 기대 형식과 응답이 안 맞음

예를 들어 특정 형식을 가정했는데 모델이 다르게 출력하면 parsing 문제가 날 수 있습니다. LangChain도 output parsing failure를 별도 에러 문서로 안내하고 있어요. (LangChain Docs)

3) 프롬프트가 너무 느슨함

parser가 잘못한 게 아니라, 프롬프트가 출력 형식을 충분히 안내하지 못한 경우도 많습니다. 공식 문서도 parsing 오류가 날 때 formatting instructions를 더 정밀하게 주라고 권합니다. (LangChain Docs)

이건 꼭 기억해두세요.
에러가 parser에서 났다고 해서 원인이 parser만은 아닙니다.


13. 이번 글의 전체 실행 코드

복붙해서 바로 실행할 수 있게 하나로 정리해둘게요.

import os
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser


def build_chain():
    model = init_chat_model("gpt-5-nano", model_provider="openai")

    prompt = ChatPromptTemplate.from_messages(
        [
            ("system", "너는 주니어 개발자를 돕는 친절한 LangChain 멘토다."),
            (
                "human",
                "주제: {topic}\n"
                "요청: {request}\n"
                "형식: 번호 목록 3개로 작성해줘."
            ),
        ]
    )

    parser = StrOutputParser()

    return prompt | model | parser


def main() -> None:
    if not os.environ.get("OPENAI_API_KEY"):
        raise ValueError("OPENAI_API_KEY 환경변수가 설정되어 있지 않습니다.")

    chain = build_chain()

    result = chain.invoke(
        {
            "topic": "LangChain chain",
            "request": "Prompt | Model | Parser 흐름을 쉽게 설명해줘.",
        }
    )

    print("=== result ===")
    print(result)
    print(type(result))


if __name__ == "__main__":
    main()

오늘 글에서 꼭 가져가야 할 한 문장

오늘 내용을 한 줄로 줄이면 이겁니다.

LangChain 체인은 “프롬프트를 쓰는 법”이 아니라, 입력과 출력을 연결하는 부품들을 조립하는 사고방식이다.

이 감각이 생기면, 이제부터 나오는 것들이 훨씬 쉬워집니다.

  • 문서 검색 결과를 중간에 넣는 것
  • 출력 파서를 교체하는 것
  • 여러 단계를 나눠서 재사용하는 것
  • 나중에 에이전트나 RAG로 확장하는 것

전부 결국 같은 철학이에요.


마무리

저는 LangChain을 처음 봤을 때, 솔직히 문법보다 이 사고방식이 더 낯설었어요.
“왜 이렇게 잘게 나눠서 연결하지?” 싶었죠.

근데 서비스 코드를 몇 번 짜보니까 오히려 반대더라고요.
잘게 나누지 않으면 나중에 수정이 너무 힘들어졌어요.

생성형 AI 개발도 결국 개발입니다.
그리고 개발에서 오래 가는 코드는 대부분 이런 특징이 있어요.

  • 역할이 나뉘어 있고
  • 교체가 쉽고
  • 재사용이 가능하고
  • 디버깅이 가능하다

LangChain 체인은 그걸 LLM 쪽으로 가져온 느낌입니다.
저는 그게 꽤 마음에 들었습니다.


다음 글 예고

다음 글에서는
“LangChain 대화형 챗봇 만들기 — 메시지 히스토리와 문맥 유지의 기초”
로 이어가겠습니다.

이제부터는 진짜 챗봇처럼 보이기 시작합니다.
사용자와 여러 번 주고받는 흐름에서, 문맥을 어떻게 유지할지 본격적으로 들어갈 거예요.


출처

  • LangChain Overview: LangChain은 모델, 툴, 에이전트를 연결해 맞춤형 애플리케이션을 만드는 프레임워크로 설명됩니다. (LangChain Docs)
  • LangChain RunnableParallel / runnable composition 레퍼런스: runnable 기반 조합 구조와 sequence/parallel 개념의 기반이 됩니다. (LangChain API)
  • LangChain Structured Output 문서: 엄격한 구조화 응답은 structured output이 적합하며, predictable format을 제공한다고 설명합니다. (LangChain Docs)
  • LangChain OUTPUT_PARSING_FAILURE 문서: 출력 파서 실패 시 formatting instructions 강화 또는 structured output/tool calling 고려를 권장합니다. (LangChain Docs)


LangChain, LCEL, Output Parser, StrOutputParser, LangChain Python, 생성형AI, LLM개발, AI체인, 주니어개발자, Python튜토리얼

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
글 보관함
반응형