반응형

 

 

지난 시간에는 LCEL Get-started를 통해서 LCEL의 간단하면서 다양한 예시를 알아보았습니다.

이번 시간에는 LCEL을 사용하는 이유에 대해서 알아보도록 하겠습니다. 즉, 사용하면 무엇이 좋은지 어떤 가치가 있는지 알아보겠습니다. 참고로 LCEL Get-started를 먼저 보고 오시면 이해에 도움이 됩니다. 혹시 안보셨다면 먼저 읽어보시기를 추천 드립니다.

 

LCEL은 간단한 콤포넌트 부터 복잡한 체인을 쉽게 만들 수 있게 합니다. 바로 다음과 같은 것들을 이용해서 만듭니다.

1. 통일된 인터페이스:

모든 LCEL 객체는 Runnable 인터페이스를 구현합니다. Runnable은 호출하는 메소드들의 공통 집합을 정의합니다.(invoke, batch, stream, ainvoke,...등과 같은...)  이것은 LCEL객체가 자동적으로 이러한 호출들을 지원할 수 있게 만듭니다. 즉, LCEL 객체로 만든 모든 체인은 하나의 LCEL객체라는 것을 말합니다. 

2. 원시도구들의 구성:

LCEL의 가치를 이해하기 위해서, LCEL 없이 비슷한 기능을 어떻게 개발하는 지 생각해보고 동작하는 지를 살펴보는 것은 도움이 됩니다. LCEL은 콤포넌트들을 병렬화하고, fallbacks을 추가하고, 체인 내부의 구성을 동적으로 바꾸는 등을 통해서 체인을 쉽게 구성하게 해줍니다. 이번 포스팅에서는 지난 번 LCEL Get-started의 내용에 있던 기본 예시를 다루어 보겠습니다. 간단한 프롬프트와 모델이 결합된 체인을 다루어 볼껀데요, 이것은 이미 많은 기능들이 정의되어 있습니다. 이것을 재생성하는 데 필요한 것이 무엇인지 살펴보겠습니다.

준비

먼저 공통적으로 필요한 체인 하나를 만들어 보겠습니다.

%pip install –upgrade –quiet langchain-core langchain-openai

관련된 패키지를 설치 합니다.

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser


prompt = ChatPromptTemplate.from_template("Tell me a short joke about {topic}")
model = ChatOpenAI(model="gpt-3.5-turbo")
output_parser = StrOutputParser()

chain = prompt | model | output_parser

프롬프트, 모델, 아웃풋 파서를 연결한 체인을 만듭니다.

이제부터 다음 내용을 구현할 때 두가지 경우의 코드를 비교하겠습니다. 즉, LCEL 없이 구현할 때와 LCEL로 구현할 때의 코드를 보겠습니다.

  • invoke, stream, batch, async, changing model provider, runtime configuration, logging, Fallback

 

 

Invoke

제일 간단한 사용예를 보면, 토픽(주제) 문자열을 전달해 주 농담 문자열을 받는 것 입니다. 아래는 이러한 내용을 LCEL 없이 개발했을 때와 LCEL로 개발했을 때의 차이를 보여 줍니다. 

LCEL 없이 만드는 경우

from typing import List

import openai


prompt_template = "Tell me a short joke about {topic}"
client = openai.OpenAI()

def call_chat_model(messages: List[dict]) -> str:
    response = client.chat.completions.create(
        model="gpt-3.5-turbo", 
        messages=messages,
    )
    return response.choices[0].message.content

def invoke_chain(topic: str) -> str:
    prompt_value = prompt_template.format(topic=topic)
    messages = [{"role": "user", "content": prompt_value}]
    return call_chat_model(messages)

invoke_chain("ice cream")

LCEL로 만드는 경우

from langchain_core.runnables import RunnablePassthrough


prompt = ChatPromptTemplate.from_template(
    "Tell me a short joke about {topic}"
)
output_parser = StrOutputParser()
model = ChatOpenAI(model="gpt-3.5-turbo")
chain = (
    {"topic": RunnablePassthrough()} 
    | prompt
    | model
    | output_parser
)

chain.invoke("ice cream")

비교 이미지

 

Stream

만약에 결과로 스트림을 원한다면 위에서 LCEL 없이 만든 함수의 내용을 수정해야 합니다.

LCEL 없이 만드는 경우

from typing import Iterator


def stream_chat_model(messages: List[dict]) -> Iterator[str]:
    stream = client.chat.completions.create(
        model="gpt-3.5-turbo",
        messages=messages,
        stream=True,
    )
    for response in stream:
        content = response.choices[0].delta.content
        if content is not None:
            yield content

def stream_chain(topic: str) -> Iterator[str]:
    prompt_value = prompt.format(topic=topic)
    return stream_chat_model([{"role": "user", "content": prompt_value}])


for chunk in stream_chain("ice cream"):
    print(chunk, end="", flush=True)

LCEL로 만드는 경우

for chunk in chain.stream("ice cream"):
    print(chunk, end="", flush=True)

LCEL로 만드는 경우는 그저 두 줄의 코드면 충분합니다.

 

Batch

병렬적으로 여러 입력을 배치로 실행하기 원한다면 또 다시 새로운 함수가 필요합니다.

LCEL 없이 만드는 경우

from concurrent.futures import ThreadPoolExecutor


def batch_chain(topics: list) -> list:
    with ThreadPoolExecutor(max_workers=5) as executor:
        return list(executor.map(invoke_chain, topics))

batch_chain(["ice cream", "spaghetti", "dumplings"])

LCEL로 만드는 경우

chain.batch(["ice cream", "spaghetti", "dumplings"])

 

Async

비동기 버전을 원한다면,

LCEL 없이 만드는 경우

async_client = openai.AsyncOpenAI()

async def acall_chat_model(messages: List[dict]) -> str:
    response = await async_client.chat.completions.create(
        model="gpt-3.5-turbo", 
        messages=messages,
    )
    return response.choices[0].message.content

async def ainvoke_chain(topic: str) -> str:
    prompt_value = prompt_template.format(topic=topic)
    messages = [{"role": "user", "content": prompt_value}]
    return await acall_chat_model(messages)

LCEL로 만드는 경우

chain.ainvoke("ice cream")

 

Chat Model 대신 LLM 사용

chat 엔드포인트 대신에 completion 엔드포인트를 사용하기 원한다면,

LCEL없이 만드는 경우

def call_llm(prompt_value: str) -> str:
    response = client.completions.create(
        model="gpt-3.5-turbo-instruct",
        prompt=prompt_value,
    )
    return response.choices[0].text

def invoke_llm_chain(topic: str) -> str:
    prompt_value = prompt_template.format(topic=topic)
    return call_llm(prompt_value)

invoke_llm_chain("ice cream")

LCEL로 만드는 경우

from langchain_openai import OpenAI

llm = OpenAI(model="gpt-3.5-turbo-instruct")
llm_chain = (
    {"topic": RunnablePassthrough()} 
    | prompt
    | llm
    | output_parser
)

llm_chain.invoke("ice cream")

 

모델 제공자가 다른 경우

OpenAI 대신에 Anthropic을 사용하기 원하면,

LCEL 없이 만드는 경우

import anthropic

anthropic_template = f"Human:\n\n{prompt_template}\n\nAssistant:"
anthropic_client = anthropic.Anthropic()

def call_anthropic(prompt_value: str) -> str:
    response = anthropic_client.completions.create(
        model="claude-2",
        prompt=prompt_value,
        max_tokens_to_sample=256,
    )
    return response.completion    

def invoke_anthropic_chain(topic: str) -> str:
    prompt_value = anthropic_template.format(topic=topic)
    return call_anthropic(prompt_value)

invoke_anthropic_chain("ice cream")

LCEL로 만드는 경우

from langchain_community.chat_models import ChatAnthropic

anthropic = ChatAnthropic(model="claude-2")
anthropic_chain = (
    {"topic": RunnablePassthrough()} 
    | prompt 
    | anthropic
    | output_parser
)

anthropic_chain.invoke("ice cream")

 

실행 환경 설정(Runtime configurability)

LCEL 없이 만드는 경우

def invoke_configurable_chain(
    topic: str, 
    *, 
    model: str = "chat_openai"
) -> str:
    if model == "chat_openai":
        return invoke_chain(topic)
    elif model == "openai":
        return invoke_llm_chain(topic)
    elif model == "anthropic":
        return invoke_anthropic_chain(topic)
    else:
        raise ValueError(
            f"Received invalid model '{model}'."
            " Expected one of chat_openai, openai, anthropic"
        )

def stream_configurable_chain(
    topic: str, 
    *, 
    model: str = "chat_openai"
) -> Iterator[str]:
    if model == "chat_openai":
        return stream_chain(topic)
    elif model == "openai":
        # Note we haven't implemented this yet.
        return stream_llm_chain(topic)
    elif model == "anthropic":
        # Note we haven't implemented this yet
        return stream_anthropic_chain(topic)
    else:
        raise ValueError(
            f"Received invalid model '{model}'."
            " Expected one of chat_openai, openai, anthropic"
        )

def batch_configurable_chain(
    topics: List[str], 
    *, 
    model: str = "chat_openai"
) -> List[str]:
    # You get the idea
    ...

async def abatch_configurable_chain(
    topics: List[str], 
    *, 
    model: str = "chat_openai"
) -> List[str]:
    ...

invoke_configurable_chain("ice cream", model="openai")
stream = stream_configurable_chain(
    "ice_cream", 
    model="anthropic"
)
for chunk in stream:
    print(chunk, end="", flush=True)

# batch_configurable_chain(["ice cream", "spaghetti", "dumplings"])
# await ainvoke_configurable_chain("ice cream")

LCEL로 만드는 경우

from langchain_core.runnables import ConfigurableField


configurable_model = model.configurable_alternatives(
    ConfigurableField(id="model"), 
    default_key="chat_openai", 
    openai=llm,
    anthropic=anthropic,
)
configurable_chain = (
    {"topic": RunnablePassthrough()} 
    | prompt 
    | configurable_model 
    | output_parser
)



configurable_chain.invoke(
    "ice cream", 
    config={"model": "openai"}
)
stream = configurable_chain.stream(
    "ice cream", 
    config={"model": "anthropic"}
)
for chunk in stream:
    print(chunk, end="", flush=True)

configurable_chain.batch(["ice cream", "spaghetti", "dumplings"])

# await configurable_chain.ainvoke("ice cream")

 

로깅(Logging)

LCEL 없이 만드는 경우

def invoke_anthropic_chain_with_logging(topic: str) -> str:
    print(f"Input: {topic}")
    prompt_value = anthropic_template.format(topic=topic)
    print(f"Formatted prompt: {prompt_value}")
    output = call_anthropic(prompt_value)
    print(f"Output: {output}")
    return output

invoke_anthropic_chain_with_logging("ice cream")

LCEL 로 만드는 경우

import os

os.environ["LANGCHAIN_API_KEY"] = "..."
os.environ["LANGCHAIN_TRACING_V2"] = "true"

anthropic_chain.invoke("ice cream")

 

Fallbacks

LCEL 없이 만드는 경우

def invoke_chain_with_fallback(topic: str) -> str:
    try:
        return invoke_chain(topic)
    except Exception:
        return invoke_anthropic_chain(topic)

async def ainvoke_chain_with_fallback(topic: str) -> str:
    try:
        return await ainvoke_chain(topic)
    except Exception:
        # Note: we haven't actually implemented this.
        return ainvoke_anthropic_chain(topic)

async def batch_chain_with_fallback(topics: List[str]) -> str:
    try:
        return batch_chain(topics)
    except Exception:
        # Note: we haven't actually implemented this.
        return batch_anthropic_chain(topics)

invoke_chain_with_fallback("ice cream")
# await ainvoke_chain_with_fallback("ice cream")
batch_chain_with_fallback(["ice cream", "spaghetti", "dumplings"]))

LCEL 로 만드는 경우

fallback_chain = chain.with_fallbacks([anthropic_chain])

fallback_chain.invoke("ice cream")
# await fallback_chain.ainvoke("ice cream")
fallback_chain.batch(["ice cream", "spaghetti", "dumplings"])

 

 

전체 코드 비교

이렇게 단순한 경우에도 LCEL 체인이 많은 기능들을 잘 압축해서 제공합니다.  체인들이 더 복잡해 줄 수록 이런 간단함은 더욱 특별한 가치를 가지게 됩니다.

LCEL 없이 만드는 경우

from concurrent.futures import ThreadPoolExecutor
from typing import Iterator, List, Tuple

import anthropic
import openai


prompt_template = "Tell me a short joke about {topic}"
anthropic_template = f"Human:\n\n{prompt_template}\n\nAssistant:"
client = openai.OpenAI()
async_client = openai.AsyncOpenAI()
anthropic_client = anthropic.Anthropic()

def call_chat_model(messages: List[dict]) -> str:
    response = client.chat.completions.create(
        model="gpt-3.5-turbo", 
        messages=messages,
    )
    return response.choices[0].message.content

def invoke_chain(topic: str) -> str:
    print(f"Input: {topic}")
    prompt_value = prompt_template.format(topic=topic)
    print(f"Formatted prompt: {prompt_value}")
    messages = [{"role": "user", "content": prompt_value}]
    output = call_chat_model(messages)
    print(f"Output: {output}")
    return output

def stream_chat_model(messages: List[dict]) -> Iterator[str]:
    stream = client.chat.completions.create(
        model="gpt-3.5-turbo",
        messages=messages,
        stream=True,
    )
    for response in stream:
        content = response.choices[0].delta.content
        if content is not None:
            yield content

def stream_chain(topic: str) -> Iterator[str]:
    print(f"Input: {topic}")
    prompt_value = prompt.format(topic=topic)
    print(f"Formatted prompt: {prompt_value}")
    stream = stream_chat_model([{"role": "user", "content": prompt_value}])
    for chunk in stream:
        print(f"Token: {chunk}", end="")
        yield chunk

def batch_chain(topics: list) -> list:
    with ThreadPoolExecutor(max_workers=5) as executor:
        return list(executor.map(invoke_chain, topics))

def call_llm(prompt_value: str) -> str:
    response = client.completions.create(
        model="gpt-3.5-turbo-instruct",
        prompt=prompt_value,
    )
    return response.choices[0].text

def invoke_llm_chain(topic: str) -> str:
    print(f"Input: {topic}")
    prompt_value = promtp_template.format(topic=topic)
    print(f"Formatted prompt: {prompt_value}")
    output = call_llm(prompt_value)
    print(f"Output: {output}")
    return output

def call_anthropic(prompt_value: str) -> str:
    response = anthropic_client.completions.create(
        model="claude-2",
        prompt=prompt_value,
        max_tokens_to_sample=256,
    )
    return response.completion   

def invoke_anthropic_chain(topic: str) -> str:
    print(f"Input: {topic}")
    prompt_value = anthropic_template.format(topic=topic)
    print(f"Formatted prompt: {prompt_value}")
    output = call_anthropic(prompt_value)
    print(f"Output: {output}")
    return output

async def ainvoke_anthropic_chain(topic: str) -> str:
    ...

def stream_anthropic_chain(topic: str) -> Iterator[str]:
    ...

def batch_anthropic_chain(topics: List[str]) -> List[str]:
    ...

def invoke_configurable_chain(
    topic: str, 
    *, 
    model: str = "chat_openai"
) -> str:
    if model == "chat_openai":
        return invoke_chain(topic)
    elif model == "openai":
        return invoke_llm_chain(topic)
    elif model == "anthropic":
        return invoke_anthropic_chain(topic)
    else:
        raise ValueError(
            f"Received invalid model '{model}'."
            " Expected one of chat_openai, openai, anthropic"
        )

def stream_configurable_chain(
    topic: str, 
    *, 
    model: str = "chat_openai"
) -> Iterator[str]:
    if model == "chat_openai":
        return stream_chain(topic)
    elif model == "openai":
        # Note we haven't implemented this yet.
        return stream_llm_chain(topic)
    elif model == "anthropic":
        # Note we haven't implemented this yet
        return stream_anthropic_chain(topic)
    else:
        raise ValueError(
            f"Received invalid model '{model}'."
            " Expected one of chat_openai, openai, anthropic"
        )

def batch_configurable_chain(
    topics: List[str], 
    *, 
    model: str = "chat_openai"
) -> List[str]:
    ...

async def abatch_configurable_chain(
    topics: List[str], 
    *, 
    model: str = "chat_openai"
) -> List[str]:
    ...

def invoke_chain_with_fallback(topic: str) -> str:
    try:
        return invoke_chain(topic)
    except Exception:
        return invoke_anthropic_chain(topic)

async def ainvoke_chain_with_fallback(topic: str) -> str:
    try:
        return await ainvoke_chain(topic)
    except Exception:
        return ainvoke_anthropic_chain(topic)

async def batch_chain_with_fallback(topics: List[str]) -> str:
    try:
        return batch_chain(topics)
    except Exception:
        return batch_anthropic_chain(topics)

LCEL로 만드는 경우

import os

from langchain_community.chat_models import ChatAnthropic
from langchain_openai import ChatOpenAI
from langchain_openai import OpenAI
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough, ConfigurableField

os.environ["LANGCHAIN_API_KEY"] = "..."
os.environ["LANGCHAIN_TRACING_V2"] = "true"

prompt = ChatPromptTemplate.from_template(
    "Tell me a short joke about {topic}"
)
chat_openai = ChatOpenAI(model="gpt-3.5-turbo")
openai = OpenAI(model="gpt-3.5-turbo-instruct")
anthropic = ChatAnthropic(model="claude-2")
model = (
    chat_openai
    .with_fallbacks([anthropic])
    .configurable_alternatives(
        ConfigurableField(id="model"),
        default_key="chat_openai",
        openai=openai,
        anthropic=anthropic,
    )
)

chain = (
    {"topic": RunnablePassthrough()} 
    | prompt 
    | model 
    | StrOutputParser()
)

 

요약

이번 시간에는 LCEL을 실제로 사용할 때와 안했을 때의 차이 비교를 통해서 LCEL이 제공하는 가치에 대해서 알아보았습니다.  LCEL이 통일된 인터페이스를 제공하고 많은 원시 구성 요소들을 제공하기 때문에 간단한 메소드 호출 만으로 많은 기능을 대신할 수 있음을 사례로 살표 보았습니다. 이러한 사례를 통해서 단순하고 간결한 코드 작성으로 구현이 가능함을 확인 하였습니다. 

 

 

 

Next Steps

LCEL에 대한 학습을 계속하려면 다음을 권장합니다:

여기서 부분적으로 다룬 LCEL 인터페이스에 대해 전체 내용을 자세히 읽어보세요.

LCEL이 제공하는 추가적인 구성 기본 요소에 대해 학습하기 위해서 How-to 섹션을 탐험하세요.

공통 사용 사례에 대한 LCEL 동작을 보려면 Cookbook 섹션을 확인하세요. 그 다음에 살펴볼 좋은 사용 사례는 검색 증강 생성입니다.

 

 

반응형
반응형

 

LangChain 표현 언어 또는 LCEL은 체인을 쉽게 조립하는 선언적인 방법입니다. LCEL은 상용 서비스부터 프로토 타입을 구현하는 데 코드 변경 없이 지원하도록 처음부터 설계되었습니다. 가장 간단한 "프롬프트 + LLM" 체인에서부터 가장 복잡한 체인까지(사람들이 100개 이상의 단계로 구성된 LCEL 체인을 성공적으로 운영했습니다) 모두 지원합니다. LCEL을 사용하려는 이유 중에서 몇 가지를 강조해보겠습니다:

  1. 스트리밍 지원: LCEL로 체인을 빌드하면 첫 번째 출력이 나올 때까지의 경과 시간 최대한 줄일 수 있습니다. 일부 체인의 경우, LLM에서 스트리밍 출력 파서로 토큰을 직접 전송하고, LLM이 원시 토큰을 출력하는 속도와 동일한 속도로 파싱된 증분 출력 묶음(청크)를 얻을 수 있습니다.
  2. 비동기 지원: LCEL로 작성된 모든 체인은 동기식 API(예: 프로토타입을 만들 때 Jupyter 노트북에서) 및 비동기식 API(예: LangServe 서버에서)로 호출할 수 있습니다. 이는 프로토타입 및 프로덕션에서 동일한 코드를 사용하여 훌륭한 성능을 내고, 동일한 서버에서 많은 동시 요청을 처리할 수 있게 해줍니다.
  3. 최적화된 병렬 실행: LCEL 체인에 병렬로 실행할 수 있는 단계가 있는 경우(예: 여러 Retriver에서 문서를 가져오는 경우), 대기 시간을 최소화 하기 위해서 동기 및 비동기 인터페이스를 자동으로 실행됩니다.
  4. 재시도 및 예비 기능: LCEL 체인의 어떤 부분이든 재시도 및 예비 기능을 구성할 수 있습니다. 이것은 규모가 큰 체인을 더 신뢰할 수 있게 만드는 좋은 방법입니다. 현재 재시도/예비 기능에 대한 스트리밍 지원을 추가 중이므로 나중에는 추가된 신뢰성을 얻을 수 있으면서도 동시에 추가 대기 시간이 필요 없습니다.
  5. 중간 결과에 대한 액세스: 더 복잡한 체인의 경우 종종 최종 출력이 생성되기 전에 중간 단계의 결과에 액세스하는 것이 매우 유용합니다. 이는 최종 사용자에게 무언가가 발생하고 있음을 알리거나 체인을 디버그하는 데 사용될 수 있습니다. 중간 결과를 스트리밍할 수 있으며 모든 LangServe 서버에서 사용 가능합니다.
  6. 입력 및 출력 스키마: 입력 및 출력 스키마는 각 LCEL 체인에 대한 Pydantic 및 JSONSchema 스키마를 제공하며 체인의 구조에서 유추됩니다. 이는 입력 및 출력의 유효성 검사에 사용될 수 있으며 LangServe의 중요한 부분입니다.
  7. 원활한 LangSmith 추적 통합: 체인이 더 복잡해질수록 모든 단계를 정확히 이해하는 것이 점점 중요해집니다. LCEL을 사용하면 모든 단계가 최대의 가시성과 디버깅 가능성을 위해 자동으로 LangSmith에 로그됩니다.
  8. 원활한 LangServe 배포 통합: LCEL로 생성된 모든 체인은 LangServe를 사용하여 쉽게 배포할 수 있습니다.

 

 

 

LCEL 시작하기 : Get started

LCEL은 기본 콤포넌트를 가지고 복잡한 체인을 쉽게 만들 수 있게 합니다.  그리고 로깅과 병렬처리, 스트리밍과 같은 외부 기능으로의 확대를 지원 합니다.

기본 예제 : 프롬프트 + 모델 + 출력 파서

가장 기본적이고 일반적인 사용 예제는 프롬프트 템플릭과 모델을 함께 체인으로 묶는 것 입니다. 이것이 어떻게 동작하는지 보기 위해서 하나의 토픽을 받아서 하나의 농담을 생성하는 체인을 만들어 보시지요.

from langchain.chat_models import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

prompt = ChatPromptTemplate.from_template("tell me a short joke about {topic}")
model = ChatOpenAI()
output_parser = StrOutputParser()

chain = prompt | model | output_parser

chain.invoke({"topic": "ice cream"})
"Why did the ice cream go to therapy?\n\nBecause it had too many toppings and couldn't find its cone-fidence!"

 

아래의 코드를 통해서 우리는 다른 콤포넌트들을 LCEL을 이용해서 하나로 묶을 수 있습니다.

chain = prompt | model | output_parser

 

여기서 | 기호는 유닉스의 파이프 연산자 입니다. 이것은 하나의 콤포넌트에서 나온 아웃풋을 다음 콤포넌트의 입력으로 제공하여 체인을 만듭니다.

이 체인의 입력은 프롬프트 템플릿으로 전달 됩니다. 그리고 그 프롬프트 템플릿의 출력은 모델로 전달 됩니다. 그리고 모델의 출력은 아웃풋 파서에게 전달 됩니다. 무슨일이 벌어지는지 정확히 이해하기 위해서 콤포넌트들을 각각 알아보시죠.

 

1. Prompt

프롬프트는 BasePromptTemplate입니다. 이것은 템플릿 변수로 사전(dictionary)을 받아서 PromptValue를 생성합니다. PromptValue는 (입력으로 문자열을 받는) LLM 이나 (입력으로 연속된 메시지를 받는) ChatModel에 전달될 수 있는 완성된 프롬프트로 만드는 랩퍼입니다. 이것은 BaseMessages나 문자열을 만드는 로직을 정의하기 때문에 두 언어모델과 함께 사용 할 수 있습니다.

prompt_value = prompt.invoke({"topic": "ice cream"})
prompt_value
# 출력 내용
ChatPromptValue(messages=[HumanMessage(content='tell me a short joke about ice cream')])
prompt_value.to_messages()
# 출력 내용 : HumanMessage 객체
[HumanMessage(content='tell me a short joke about ice cream')]
prompt_value.to_string()
# 출력 내용: 문자열
'Human: tell me a short joke about ice cream'

 

2. Model

그리고나서 PromptValue 는 모델에게 전달 됩니다. 이번에 우리가 사용한 모델이 ChatModel이기 때문에 아웃풋은 BaseMessage일 껍니다.

message = model.invoke(prompt_value)
message
# 출력 내용
AIMessage(content="Why did the ice cream go to therapy? \n\nBecause it had too many toppings and couldn't find its cone-fidence!")

만약에 우리의 모델이 LLM이라면 이것의 출력은 문자열일 껍니다.

from langchain.llms import OpenAI

llm = OpenAI(model="gpt-3.5-turbo-instruct")
llm.invoke(prompt_value)
 

3. Output parser

그리고 마지막으로 우리는 우리의 모델 아웃풋을 아웃풋 파서에 전달 합니다. 아웃풋 파써가 BaseOutputParser라는 것은 문자열이나 BaseMessage를 입력으로 받는 다는 것을 의미합니다.

output_parser.invoke(message)
# 출력 내용
"Why did the ice cream go to therapy? \n\nBecause it had too many toppings and couldn't find its cone-fidence!"

 

4. 전체 Pipeline

다음의 단계를을 따라 갑니다.:

  1. {"topic": "ice cream"}과 같이 원하는 토픽을 사용자 입력으로 전달합니다.
  2. 프롬프트는 사용자 입력을 받는데 이것은 토픽을 이용해서 프롬프트를 만든 다음 PromptValue를 생성하는데 사용합니다.
  3. 모델 콤포넌트는 생성된 프롬프트를 받아서 OpenAI LLM 모델에 전달합니다. 이 모델의 출력으로 생성된 것은 ChatMessage 오브젝트입니다. 
  4. 마지막으로, output_parser 콤포넌트는 ChatMessage 오브젝트를 입력 받고 파이썬 문자열로 변환하여 invoke 메소드로 반환합니다. 

만약에 어떤 콤포넌트든지 출력에 대해서 궁금한 점이 있다면 이 체인의 작은 버전을 언제든지 테스트 할 수 있습니다. 예를 들면 중간 결과를 보기 위해서 prompt 또는 prompt | model 같은 것을 테스트 할 수 있습니다. 

input = {"topic": "ice cream"}

# 프롬프트만 실행
prompt.invoke(input)
# > ChatPromptValue(messages=[HumanMessage(content='tell me a short joke about ice cream')])

# 프롬프트와 모델만 실행
(prompt | model).invoke(input)
# > AIMessage(content="Why did the ice cream go to therapy?\nBecause it had too many toppings and couldn't cone-trol itself!")

 

RAG 검색 예시

다음 예시로 우리는 질문에 대답할 때 어떤 내용을 추가하는 retrieval-augmented generation 체인을 만들기 원합니다.

# Requires:
# pip install langchain docarray tiktoken

from langchain.chat_models import ChatOpenAI
from langchain.embeddings import OpenAIEmbeddings
from langchain.prompts import ChatPromptTemplate
from langchain.vectorstores import DocArrayInMemorySearch
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableParallel, RunnablePassthrough

vectorstore = DocArrayInMemorySearch.from_texts(
    ["harrison worked at kensho", "bears like to eat honey"],
    embedding=OpenAIEmbeddings(),
)
retriever = vectorstore.as_retriever()

template = """Answer the question based only on the following context:
{context}

Question: {question}
"""
prompt = ChatPromptTemplate.from_template(template)
model = ChatOpenAI()
output_parser = StrOutputParser()

setup_and_retrieval = RunnableParallel(
    {"context": retriever, "question": RunnablePassthrough()}
)
chain = setup_and_retrieval | prompt | model | output_parser

chain.invoke("where did harrison work?")

 

이 경우에 구성된 체인은 다음과 같습니다.

chain = setup_and_retrieval | prompt | model | output_parser
이 것을 설명하려면 우리는 먼저 프롬프트에서 제공되는 값으로, 콘텍스트와 질문을 받아들이는 프롬프트 템플릿에 대해서 알아볼 수 있습니다.  프롬프트 템플릿을 만들기 전에 우리는 관련된 문서를 가져오고 검색해서 이렇게 가져온 것들을 컨텍스트의 일부로 포함하기를 원합니다.

제일 첫번째 단계로 우리는 메모리 저장소를 이용해서 retriever를 설정할 수 있습니다. 이 retriever는 질문에 기반해서 문서를 가져올 수 있습니다. 이것은 다른 콤포넌트들과 함께 묶여질 수 있을 뿐만 아니라 실행될 수 있는 콤포넌트입니다. 뿐만아니라 여러분은 retriever만 분리해서 실행 할 수도 있습니다. 

retriever.invoke("where did harrison work?")
그리고나서 우리는 사용자 입력 뿐만 아니라 가져온 문서 전체를 프롬프트의 기대 입력값으로 준비하기 위해서 RunnableParallel을 사용할 수 있습니다. 즉, 문서 검색을 위해 retriever를 사용하고 사용자 질문을 RunnablePassthrough를 사용해서 전달합니다.
setup_and_retrieval = RunnableParallel(
    {"context": retriever, "question": RunnablePassthrough()}
)
다시 정리하면, 완성된 체인은 다음과 같습니다.
setup_and_retrieval = RunnableParallel(
    {"context": retriever, "question": RunnablePassthrough()}
)
chain = setup_and_retrieval | prompt | model | output_parser

흐름은 다음과 같습니다:

  1. 첫 번째 단계에서는 두 항목을 포함하는 RunnableParallel 객체를 만듭니다. 첫 번째 항목인 "context"에는 retriever에 의해 가져온 문서 결과가 포함됩니다. 두 번째 항목인 "question"에는 사용자의 원래 질문이 포함됩니다. 질문을 전달하기 위해 RunnablePassthrough를 사용하여 복사합니다.
  2. 위 단계에서 생성된 사전을 프롬프트 컴포넌트에 전달합니다. 그런 다음 사용자 입력인 "question" 과 검색된 문서인 "context"를 사용하여 프롬프트를 구성하고 PromptValue를 출력합니다.
  3. 모델 컴포넌트는 생성된 프롬프트를 가져와 OpenAI LLM 모델에 전달하여 실행 평가합니다. 모델에서 생성된 출력은 ChatMessage 객체입니다.
  4. 마지막으로 output_parser 구성 요소는 ChatMessage를 입력으로 받아서 이를 Python 문자열로 변환하고, invoke 메서드에 반환합니다.

Next steps

동일한 기능을 LCEL로 구현할 때의 코드와 LCEL 없이 구현할 때의, 두 코드를 나란히 놓고 비교하는 내용이 있는 '왜 LCEL을 사용하는지'를 읽어 보시기 바랍니다. 

 

이번 시간을 통해서 LCEL이 무엇인지 알아 보았습니다. 그리고 prompt + model  + output_parsert 형태의 기본 예제를 만들고 이해했으며, 마지막에는 RAG 예제를 통해서 관련된 문서를 입력 프롬프트에서 함께 사용하도록 하는 방법에 대해서 알아 보았습니다.

 

 

반응형
반응형

목차  LangChain - Get started

  1. Introduction
  2. Installation
  3. Quickstart
  4. Security (이번 포스팅 내용)

 

동영상 설명은 아래 링크를 참고해 주세요. 

https://youtu.be/pSjEz7PD_6s

 

 

이전 포스팅까지 잘 따라오셨다면 LangChain에 대해서 이미 많은 것을 이해하고 기본적인 내용을 활용할 수 있게 되었다고 할 수 있겠습니다. 직전 내용인 Quickstart의 내용이 살짝 많은 것 처럼 생각될 수 있는데, 이것은 다양한 형태의 사용을 설명하다 보니 조금 복잡해 진것 같습니다. 하여간 집중해서 보셨다면 이제 LangChain을 이용해서 어느정도 간단한 어플리케이션을 개발/ 구성을 하실 수 있게 됐을 것 같습니다.

이번 시간에는 보안(Security)에 대해서 다루어 보겠습니다.

 

보안(Security)

LangChain은 로컬 및 원격 파일 시스템, API 및 데이터베이스와 같은 다양한 외부 리소스와 통합된 대규모 생태계를 보유하고 있습니다. 이러한 통합은 개발자가 LLM의 강력한 기능을 활용하면서 외부 리소스(서비스)에 액세스하고 상호 작용 할 수 있는 다양한 응용 프로그램을 만들 수 있게 합니다.

예를 들면 아이언맨에 나오는 자비스 같은 것을 말합니다. 자비스는 랭체인을 기반으로 구현할 수 있는 언어 모델 어플리케이션의 좋은 예라고 할 수 있겠습니다. (LLM 이) 사용자(토니)의 말을 잘 알아 듣고 대답을 생성하며, 필요하면 (LangChain 이 하는 것 처럼) 외부의 시스템/서비스에 접속해서 필요한 정보를 가져오거나 필요한 동작을 실행 합니다. 이런 어플리케이션의 특징이 있기 때문에 전통적인 보안의 관점에서 좀더 확장된 시각의 접근이 필요한 것 같습니다. 왜냐하면 하나의 서비스 완성을 위해서 여러 기능의 연결점이 많고, 내부와 외부 서비스로 다양하기 때문입니다. 즉, LLM의 특징과 언제 어떻게 사용될 지 모르는 불확실 성에서 오는 잠재적 위험이라고 할 수 있겠습니다.

 

최적의 방법

이러한 응용 프로그램을 개발할 때 개발자는 다음과 같은 좋은 보안 관행을 따르는 것이 중요합니다.

  • 권한 제한: 응용 프로그램이 필요한 권한을 구체적으로 제한하세요. 넓거나 과도한 권한 부여는 중대한 보안 취약점이 될 수 있습니다. 이러한 취약점을 피하려면 읽기 전용 자격 증명을 사용하거나 민감한 리소스에 대한 액세스를 금지하거나 적절한 샌드박싱 기술(컨테이너 내에서 실행)을 사용하는 것을 고려하십시오. 이처럼 권한을 제한하는 것은 어찌보면 당연한 일이지만 어플리케이션의 구성이 더 복잡해 짐에 따라 더 중요한 포인트입니다. 
  • 잠재적인 남용 예측: 인간이 실수할 수 있듯이 대형 언어 모델(LLM)도 실수 할 수 있습니다. 어떤 시스템 액세스 또는 자격 증명이든지 간에 할당된 권한이 허용하는 모든 방식으로 사용될 수 있다고 가정하십시오. 예를 들어 데이터베이스 자격 증명이 데이터 삭제를 허용한다면 해당 자격 증명을 사용할 수 있는 모든 LLM이 실제로 데이터를 삭제할 수 있다고 가정하는 것이 가장 안전합니다.
  • 깊이 있는 방어: 어떤 보안 기술도 완벽하지 않습니다. Fine-tuning 및 좋은 체인 디자인은 대형 언어 모델(LLM)이 실수를 할 확률을 줄일 수 있지만 완전히 제거할 수는 없습니다. 보안을 보장하기 위해 단일 방어 계층에 의존하는 대신 여러 겹의 보안 접근 방식을 결합하는 것이 가장 좋습니다. 예를 들어 읽기 전용 권한과 샌드박싱을 모두 사용하여 LLM이 명시적으로 사용하도록 허용된 데이터에만 액세스할 수 있도록 보장할 수 있습니다.

보안 관행을 따르지 않으면 다음과 같은 리스크가 발생할 수 있습니다(당연하지만 이보다 더할 수 있습니다.):

  • 데이터 손상 또는 손실.
  • 기밀 정보에 대한 무단 액세스.
  • 핵심 리소스의 성능 또는 가용성에 대한 손상.

예시 시나리오 및 완화 전략:

  • 사용자가 파일 시스템에 액세스 권한이 있는 에이전트에게 삭제해서는 안 될 파일을 삭제하거나 민감한 정보가 포함된 파일의 내용을 읽도록 요청할 수 있습니다. 완화를 위해 에이전트를 특정 디렉토리만 사용하도록 제한하고 읽거나 쓸 수 있는 안전한 파일만 허용하도록 합니다. 에이전트를 컨테이너 내에서 실행하여 추가로 샌드박스 처리하는 것도 고려합니다.
  • 사용자가 외부 API에 쓰기 권한이 있는 에이전트에게 악의적인 데이터를 API에 기록하거나 해당 API에서 데이터를 삭제할 수 있습니다. 완화를 위해 에이전트에게 읽기 전용 API 키를 부여하거나 이미 그런 남용에 대한 견딜 수 있는 엔드포인트만 사용하도록 제한할 수 있습니다.
  • 사용자가 데이터베이스에 액세스할 수 있는 에이전트에게 테이블을 삭제하거나 스키마를 변형할 수 있습니다. 완화를 위해 에이전트가 액세스해야 하는 테이블에 권한을 제한하고 읽기 전용 자격 증명을 발급하는 것을 고려합니다.

만약 여러분이 회사에서 외부 리소스에 액세스하는 응용 프로그램을 개발 중이라면 회사의 보안 팀과 상의하여 응용 프로그램을 최적으로 설계하고 보안을 강화하는 방법을 결정하는 것이 좋습니다.

 

취약점 보고하기

보안 취약점이 있다면 security@langchain.dev 로 이메일을 통해 신고해 주세요. 이렇게 하면 문제가 즉각적으로 처리되고 필요한 대응이 이루어집니다.  

 

기업 솔루션

LangChain은 추가적인 보안 요구사항을 가지고 있는 고객을 위해 기업 솔루션을 제공할 수 있습니다. sales@langchain.dev로 연락주시기 바랍니다.

 

기타

가장 기본적이면서, 중요하면서, 잊기 쉬운 것이 바로 Key 관리 아닌가 싶습니다. 자체 모델과 서비스를 이용해서 통합하고 어플리케이션을 만들면 Key 관리 Risk가 적겠지만, 쉽고, 빠르고, 높은 품질의 어플리케이션을 만들려면 외부 서비스와의 통합이 필수적이게 되고 이때 Key와 Credential을 사용하게됩니다. 그래서 위의 예시 시나리오에서도 나왔지만 API 사용시 적절한 Key 관리가 매우 중요합니다. 코드에 Key를 포함하지 않는 것은 기본이고, 용도와 최소한의 권한을 부여한 Key를 사용하고 주기적으로 점검하고 관리하는 것이 중요합니다. 

 

이것으로 LangChain을 사용할 때 보안 관점에서의 필요성과 보안 모범 사례를 알아보았고,

예상 시나리오에 대한 적절한 완화 전략과 기타 보안 관련 내용에 대해서 이야기해 보았습니다.

 

동영상 설명 : https://youtu.be/pSjEz7PD_6s

 

반응형
반응형

이 포스팅은 2024년 1월 기준으로 새롭게 변경된 LangChain Quickstart에 대한 내용입니다. 이전 버전의 Quickstart 내용보다 상위 수준에서 설명하면서도 동시에 구체적인 예제 코드를 제시하고 있습니다. 예를 들어 이전버전에서는 가장 간단한 prompte + LLM 버전의 체인을 만드는 것에 집중했다면 새로운 Quickstart에서는 LLM Chain, Retrieval Chain, Conversation Retrieval Chain, Agent 등의 내용과 예제를 제공합니다. 다루는 폭이 넓어지고 다양해 졌다고 할 수 있겠네요.

이전 포스팅을 통해서 OpenAI API를 사용하는 방법에 대해서 배웠으며, 랭체인이 무엇인지를 알아보았고, 그리고 랭체인을 설치하는 방법에 대해서 알아보았습니다.

이번 시간에는 랭체인을 이용해서 간단한 어플리케이션을 만들고 서빙을(배포를) 해 보겠습니다. 다시 말하면, (이전의 내용들을 잘 따라오셨다면) 랭체인을 이용해서 간단한 언어 모델 어플리케이션을 만들 수 있다는 의미 입니다. 아주 멋지고 유용한 어플리케이션을 만들기 위해서 먼저, 간단한 어플리케이션을 만들어보는 과정이라고 하겠습니다. 

주된 내용은 다음을 참고했고 추가로 설명을 작성하였습니다.  https://python.langchain.com/docs/get_started/quickstart

 

Quickstart | 🦜️🔗 Langchain

In this quickstart we'll show you how to:

python.langchain.com

 

본 포스팅의 친절한 설명 동영상과 실제 동작하는 코드 내용은 제일 하단에 있는 링크를 참고해 주세요. 

 

 

 



이번 포스팅(Quickstart)에서는 다음과 같은 것들을 보실 수 있습니다.

  • LangChain, LangSmith, LangServe 설정방법
  • 랭체인에서 가장 기본적이고 공통적인 요소의 사용법: prompt templates, models, 그리고 output parser 같은 것들
  • LCEL(LangChain Expression Language)의 사용법: LCEL은 LangChain이 구축된 프로토콜로, 구성 요소들을 체이닝(묶는 것)을 용이하게 합니다.
  • 랭체인을 이용한 간단한 어플리케이션 개발 방법
  • 랭스미스를 이용한 어플리케이션 추적 방법(이 내용은 많이 없습니다.)
  • 랭서브를 이용한 어플리케이션 서빙(배포) 방법

살짝 많아보이긴 하지만 매우 기본적이고 중요한 내용입니다. 가보실까요!~~~

전체 내용은 이렇습니다. 먼저 환경 및 필요한 내용을 Setup 하고, LangChain을 만들어보고, LangServe로 서빙해 보겠습니다.

 

Setup

Jupyter Notebook

이 페이지에 있는 다른 자료를 포함해서 이 가이드에서도 Jupyter notebooks(주피터 노트북)을 이용하고 여러분도 그럴 것이라고 가정합니다. 주피터 노트북은 LLM 시스템이 어떻게 동작하는지 배우기에 완벽한 도구입니다. 왜냐하면 기대하지 않던 출력이나 API 다운 과 같이 잘못될 때가 종종 있기 때문입니다. 그래서 이와 같은 상호 작용하는 환경에서 가이드를 진행하는 것이 LLM시스템을 더 잘 이해하는데 좋은 방법입니다.

이 가이드를 반드시 주피터 노트북에서 진행할 필요는 없지만 그렇게 할 것을 추천 드립니다. 노트북을 어떻게 설치하는지 궁금하시면 여기를 참고해주세요.

Installation

이 내용은 랭체인 설치 방법으로 바로 지난 포스팅에서 다룬 내용입니다. 아래의 명령어를 실행합니다.

pip install langchain

conda 환경에서는 아래를 실행합니다.

conda install langchain -c conda-forge

LangSmith

여러분이 만드시는 많은 어플리케이션은 LLM(LargeLanguageModel) 호출을 여러번 실행하는 멀티 스텝을 갖게 될 것입니다. 이러한 어플리케이션이 점점더 복잡해 짐에 따라서 여러분의 체인과 에이전트가 정확하게 무엇을 하고 있는지 조사하는 것 또한 점점 더 중요해 집니다. 이를 위해 가장 좋은 방법이 바로 LangSmith를 이용하는 것 입니다.

LangSmith가 반드시 필요한 것은 아닙니다. 그러나 도움이 됩니다. 바로 위의 링크를 통해 LangSmith에 접속해서 LangSmith Key를 받고, 아래와 같이 설정해서 사용할 수 있습니다. 참고로 LangSmith는 베타버전의 서비스 입니다. 따라서 품질에 대한 보증은 어렵습니다. 그래도 방금 페이지에 접속해보니 사용을 위해서는 대기해야할 정도로 인기가 있네요.

export LANGCHAIN_TRACING_V2="true"
export LANGCHAIN_API_KEY="..."

참고로 LangSmith는 베타버전의 서비스 입니다. 따라서 품질에 대한 보증은 어렵습니다. 그래도 방금 페이지에 접속해보니 사용을 위해서는 대기해야할 정도로 인기가 있네요.

 

Building with LangChain

LangChain은 개발하는 어플리케이션이 데이터의 외부 소스와 LLMs 모델의 처리를 연결 할 수 있게 합니다. 이 가이드에서 우리는 이와 같은 연결을 하는 몇가지 다른 방법을 진행할 것입니다. 우리는 먼저 간단한 LLM 체인으로 시작할 것인데  이 체인은 프롬프트 템플릿에 있는 정보에 의존해서 반응(회신)합니다. 그 다음에 retrieval chain(회수 체인)을 만들껀데요, 이것은 분리된 데이터 베이스로 부터 데이터를 가져와서 프롬프트 템플릿으로 전달합니다. 우리는 그리고나서 chat history를 추가할 것인데요, 이것은 이전 질문들을 기억하는 것입니다. 마지막으로는 에이전트를 만들 예정입니다. 이 에이전트는 질문에 대답하기 위해서 데이터를 가져와야 하는지 아닌지를 결정하기 위해서 LLM을 이용합니다. 우리는 이와 같은 내용을 상위 수준에서 다룰 예정이지만 사실 많은 상세 내용들이 있습니다. 이런 상세 내용은 링크로 연결/제공하겠습니다.

 

LLM Chain

공식 문서 가이드에서는 두가지 옵션을 제공니다. 하나는 OpenAI를 이용하는 것이고 다른 하나는 로컬에 저장된 오픈소스모델을 이용하는 것입니다. 그러나 이 문서에서는 쉬운 사용법 이해를 위해 OpenAI를 이용하도록 하겠습니다. 로컬 모델을 이용하실 분은 공식문서를 참고해주세요.

OpenAI 이용을 위해서는 아래와 같이 파이썬 패키지 설치가 필요합니다.

pip install openai

그리고 API에 접근하기 위해서는 OpenAI API Key가 필요합니다. OpenAI의 API Key를 받기 위해서는 OpenAI 계정이 필요하구요. 없으신 분은 다음의 링크를 따라가서 만드실 수 있습니다.(https://platform.openai.com/account/api-keys)

OpenAI API Key에 대해서 낮설으신 분은 이전 포스팅 내용인 다음의 링크를 통해 먼저 이해하고 진행하시면 좋겠네요.(https://bigdatamaster.tistory.com/203)

키를 발급 받으신 다음에는 아래와 같은 명령어를 이용해서 환경번수에 Key 값을 설정해 주세요.

export OPENAI_API_KEY="..."

 

그리고나서 다음과 같이 모델을 초기화 할 수 있습니다.

from langchain.chat_models import ChatOpenAI

llm = ChatOpenAI()

만약에 환경변수에 설정하고 싶지 않은 경우에는 OpenAI LLM 클래스를 초기화할 때 openai_api_key라는 파라메터를 통해 직접 전달할 수 있습니다.

from langchain.chat_models import ChatOpenAI

llm = ChatOpenAI(openai_api_key="...")

여러분이 선택한 LLM을 설치하고 초기화 했다면, 이제 그 모델을 사용할 수 있습니다. 자, 이제 모델에게 LangSmith가 테스팅할 때 어떤 도움을 줄 수 있는지("how can langsmith help with testing?") 물어 보겠습니다. - 참, 이런 질문은 모델 훈련 데이터에 없었기 때문에 좋은 대답을 받기는 어려울 껍니다.

llm.invoke("how can langsmith help with testing?")

우리는 또한 프롬프트 템플릿을 가지고 이것의 응답을 가이드 할 수 있습니다. 프롬프트 템플릿은 사용자 입력 내용을 LLM에게 더 좋은 입력으로 변환 하는데 사용됩니다.

from langchain.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages([
    ("system", "You are world class technical documentation writer."),
    ("user", "{input}")
])

자, 이제 우리는 이렇게 만든 것들을 간단한 LLM 체인으로 묶을 수 있습니다.

chain = prompt | llm

이렇게 프롬프트와 LLM 모델을 하나의 체인으로 묶을 수 있습니다.  이제 우리는 이렇게 만든 체인을 invoke 시키고 동일한 질문을 할 수 있습니다. 아마 체인은 아직도 정확한 답변을 모를 껍니다. 하지만 마치 기술문서 작성자와 같은 더 적절한 톤으로 답변할 껍니다.

ChatModel의(즉, 이 체인의) 출력은 메시지 입니다. 그럼에도, 문자열을 가지고 작업하는게 훨씬 편할 때가 종종 있습니다. 그러니 chat message를 문자열로 바꾸어주는 간단한 output parser를 만들어 보시죠. 

from langchain_core.output_parsers import StrOutputParser

output_parser = StrOutputParser()

이렇게 만든 output parser를 이전에 만든 체인에 추가할 수 있습니다.

chain = prompt | llm | output_parser

자, 이제 다시 invoke 시키고 동일한 질문을 할 수 있습니다. 이 질문에 대한 답변은 ChatMessage가 아니라 하나의 문자열일 껍니다.

chain.invoke({"input": "how can langsmith help with testing?"})

Diving Deeper

우리는 지금까지 기본 LLM 체인을 성공적으로 설정했습니다. 우리는 기본적인 프로프트와 모델, 그리고 아웃풋 파서(output parser) 만을 다루었습니다. 더 깊이 있는 것들이 궁금하신 분들은 여기 문서를 참고해 주세요.

 

Retrieval Chain

앞선 질문("how can langsmith help with testing?")에 적절한 답변을 만들기 위해서 우리는 LLM에거 추가 context를 제공할 필요가 있습니다. 우리는 이것을 retrieval로 할 수 있습니다. Retrieval이란 데이터가 너무 많아서 LLM에게 직접적으로 전달하지 못할 때 유용합니다. 그래서 여러분은 가장 관련된 데이터 조각을 가져오고 모델에 전달하기 위해서 retriever를 사용할 수 있습니다.

이 처리에서 우리는 Retriever로 부터 관련된 문서를 찾아보고 프롬프트에 전달 할 껍니다. Retriever는 SQL 테이블이나 인터넷 등과 같은 것을 지원 받을 수 있습니다. 그러나 이번에는 벡터 스토어(Vector Store)를 만들고 retriever로 이용 할 껍니다. vectorstore에 대한 정보는 여기를 참고해 주세요.

먼저 인덱스가 필요한 데이터를 프로그램(메모리)에 로드할(가져올) 필요가 있습니다.

from langchain_community.document_loaders import WebBaseLoader
loader = WebBaseLoader("https://docs.smith.langchain.com/overview")

docs = loader.load()

그다음, vectorstore에 데이터를 인덱스하는 것이 필요합니다. 이것은 몇개의 콤포넌트를 필요로 하는데 가장 유명한 것이 embedding modelvectorstore입니다.

embedding models을 위해서 OpenAI와 로컬 모델을 통해서 사용하는 예시를 제공합니다. (이것도 로컬 모델을 이용하는 방법은 공식 문서를 참고해주세요)

OpenAI

LLM에서 필요로 하는 것과 같이 Openai 패키지가 잘 설치되어 있고 적절한 환경 변수가 설정되어 있는 것이 필요합니다.

from langchain_community.embeddings import OpenAIEmbeddings

embeddings = OpenAIEmbeddings()

이제 이 embedding model을 가지고 vectorstore에 문서들을 주입해 넣을 수 있습니다. 우리는 단순화를 목적으로 간단한 로컬 vectorstore인 DocArray InMemorySearch를 사용 할 예정입니다.

먼저 필요한 패키지 설치가 필요합니다.

pip install docarray

그리고나서 인텍스를 만들 수 있습니다.

from langchain_community.vectorstores import DocArrayInMemorySearch
from langchain.text_splitter import RecursiveCharacterTextSplitter


text_splitter = RecursiveCharacterTextSplitter()
documents = text_splitter.split_documents(docs)
vector = DocArrayInMemorySearch.from_documents(documents, embeddings)

이렇게 함으로써 우리는 vectorstore안에 인덱스된 데이터를 만들었습니다. 이제 retrieval chain을 만들어 보시죠. 이 체인은 들어오는 질문을 받아서, 관련된 문서들을 찾고, 그리고나서 최초 입력된 질문과 함께 이 문서들을 LLM에 전달하고, 원초 질문의 답을 LLM에게 물어볼 껍니다.

먼저 원초 질문과 회수된 문서, 그리고 답변을 생성하는 체인을 설정합니다.

from langchain.chains.combine_documents import create_stuff_documents_chain

prompt = ChatPromptTemplate.from_template("""Answer the following question based only on the provided context:

<context>
{context}
</context>

Question: {input}""")

document_chain = create_stuff_documents_chain(llm, prompt)

우리가 원한다면 문서를 직접 전달해서 이 것을 실행할 수도 있습니다.

from langchain_core.documents import Document

document_chain.invoke({
    "input": "how can langsmith help with testing?",
    "context": [Document(page_content="langsmith can let you visualize test results")]
})

그럼에도 불구하고, 우리는 우리가 설정한 것처럼 retriever로 부터 먼저온 문서들을 원합니다. 그러기 위해서 주어진 질문에 대하여 retriever를 이용해서 동적으로 가장 관련 높은 문서를 선택해서 가져오고 전달 할 수 있습니다.

from langchain.chains import create_retrieval_chain

retriever = vector.as_retriever()
retrieval_chain = create_retrieval_chain(retriever, document_chain)

우리는 이제 이 체인을 invoke 할 수 있습니다. 이 체인은 LLM으로 부터 받은 응답을 dictionary로 반환하는데 answer key를 이용해서 그 내용을 확인 할 수 있습니다.

response = retrieval_chain.invoke({"input": "how can langsmith help with testing?"})
print(response["answer"])

// LangSmith offers several features that can help with testing:...

이렇게 나온 답변은 훨씬 더 정확할 껍니다.

Diving Deeper

지금까지 기본적인 retrieval chain을 성공적으로 만들어 보았습니다. 기본적인 retrieval 만 다루었기 때문에 더 깊이있는 내용을 원하시면 여기를 참고하세요.

 

Conversation Retrieval Chain

지금까지 만들어본 체인은 하나의 질문에 답변하기 위한 것이었습니다. 사람들이 만드는 LLM어플리케이션의 주요 유형중 하나는 챗봇입니다. 그래서 어떻게 하면 하나의 체인을 이어지는 질문에 대답할 수 있게 만들 수 있을까요?

우리는 계속해서 create_retrieval_chain 함수를 이용할 것 입니다. 다만 두가지 변경이 필요합니다.

1. retrieval method는 가장 최근의 입력에 동작하지 말고 계정의 전체 기록을 받아서 동작해야 합니다.

2.최종 LLM 체인은 계정의 전체 기록과 같은 형태여야 합니다.

Updating Retrieval

retrieval을 업데이트하기 위해서 우리는 새로운 체인을 만들 겠습니다. 이 체인은 최근의 입력(input)과 대화 기록(chat_history)을 받고 검색 질문을 만들기 위해 LLM을 사용하도록 만들겠습니다.

from langchain.chains import create_history_aware_retriever
from langchain_core.prompts import MessagesPlaceholder

# First we need a prompt that we can pass into an LLM to generate this search query

prompt = ChatPromptTemplate.from_messages([
    MessagesPlaceholder(variable_name="chat_history"),
    ("user", "{input}"),
    ("user", "Given the above conversation, generate a search query to look up in order to get information relevant to the conversation")
])
retriever_chain = create_history_aware_retriever(llm, retriever, prompt)

사용자가 이어지는 질문을 물어보는 것과 같은 인스턴스를 전달해서 이 체인을 테스트해 볼 수 있습니다.

from langchain_core.messages import HumanMessage, AIMessage

chat_history = [HumanMessage(content="Can LangSmith help test my LLM applications?"), AIMessage(content="Yes!")]
retrieval_chain.invoke({
    "chat_history": chat_history,
    "input": "Tell me how"
})

당신은 이 체인이 LangSmith를 가지고 테스팅하는 것에 대한 문서들을 반환하는지 확인해야 합니다. 왜냐하면 이것은 LLM이 chat history와 이어지는 질문을 연결하는 새로운 질문을 생성하기 때문입니다.

이로써 우리는 새로운 retriever를 만들었습니다. 이제 생각했던 것과 같이 회수된 문서(retrieved documents)를 가지고 대화를 계속하는 새로운 체인을 만들 수 있습니다.

prompt = ChatPromptTemplate.from_messages([
    ("system", "Answer the user's questions based on the below context:\n\n{context}"),
    MessagesPlaceholder(variable_name="chat_history"),
    ("user", "{input}"),
])
document_chain = create_stuff_documents_chain(llm, prompt)

retrieval_chain = create_retrieval_chain(retriever_chain, document_chain)

 이제 처음부터 끝까지 테스트할 수 잇습니다.

chat_history = [HumanMessage(content="Can LangSmith help test my LLM applications?"), AIMessage(content="Yes!")]
retrieval_chain.invoke({
    "chat_history": chat_history,
    "input": "Tell me how"
})

우리는 이것이 연관된 대답을 준다는 것을 볼 수 있습니다. 이로써 우리는 우리의 retreival chain을 챗봇으로 성공적으로 변경 시켰습니다.

 

Agent

우리는 지금까지 각 단계가 미리알려진 그런 chain들의 예제를 만들어 보았습니다. 이제 마지막으로 LLM이 어떤 단계를 취해야하는지 판단하는 에이전트를 만들겠습니다.

에이전트를 만들때 해야할 것 중 가장 먼저인 것은 어떤 툴에 접속할 수 있게 할 것인지를 정하는 것입니다. 예들어면, 에이전트에게 다음과 같은 두개의 툴에 접속할 수 있도록 할 수 있습니다.

1. 방금 만들었던 retriever. 이것은 LangSmith에 대한 질문에 쉽게 답할 수 있게 합니다.

2. 검색 툴. 이것은 최신 정보가 필요한 질문에 쉽게 대답 할 수 있게 합니다.

먼저 우리가 만든 retriever를 툴로 설정해 보겠습니다.

from langchain.tools.retriever import create_retriever_tool

retriever_tool = create_retriever_tool(
    retriever,
    "langsmith_search",
    "Search for information about LangSmith. For any questions about LangSmith, you must use this tool!",
)

 검색 툴은 Tavily를 이용하겠습니다. 이 것은 별도의 API가 필요합니다.(무료 등급으로도 제공하네요) API Key를 생성한 뒤에는 환경변수에 설정이 필요합니다.

export TAVILY_API_KEY=...

API Key를 설정하고 싶지 않다면 이툴을 생성하는 것을 스킵할 수 있습니다.

from langchain_community.tools.tavily_search import TavilySearchResults

search = TavilySearchResults()

이제 우리가 동작하기 원하는 툴들의 리스트를 생성할 수 있습니다.

tools = [retriever_tool, search]

이로써 우리 tools를 만들었으니 이제 이 것들을 이용 할 에이전트를 만들 수 있습니다.  좀 빠르게 이부분을 넘어가겠습니다. 더 자세한 내용은 여기를 참고해주세요.

from langchain.chat_models import ChatOpenAI
from langchain import hub
from langchain.agents import create_openai_functions_agent
from langchain.agents import AgentExecutor

# Get the prompt to use - you can modify this!
prompt = hub.pull("hwchase17/openai-functions-agent")
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
agent = create_openai_functions_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

이제 에이전트를 불러서 어떻게 답변하는지 보겠습니다. 아래와 같이 LangSmith에 대한 질문을 할 수 있습니다.

agent_executor.invoke({"input": "how can langsmith help with testing?"})

날씨에 대해서도 물어볼 수 있습니다.

agent_executor.invoke({"input": "what is the weather in SF?"})

이런 것에 대한 대화를 할 수도 있습니다.

chat_history = [HumanMessage(content="Can LangSmith help test my LLM applications?"), AIMessage(content="Yes!")]
agent_executor.invoke({
    "chat_history": chat_history,
    "input": "Tell me how"
})

Diving Deeper

이로써 기본 에이전트를 만들어 보았습니다. 여기서는 에이전트에 대한 기본적인 내용만 다루었습니다. 더 자세한 내용은 여기를 참고해주세요. 

 

Serving with LangServe

지금까지 하나의 어플리케이션을 만들었습니다. 이제 저장하고 서빙이 필요합니다. 바로 이때 LangServe가 필요합니다. LangServe는 LangChain chain들을 REST API로 배포할 수 있도록 도와줍니다. LangChain을 사용하기 위해서 LangServe를 사용할 필요는 없지만 이 가이드에서는 LangServe를 가지고 어떻게 여러분의 앱을 배포할 수 있는지 보여드리겠습니다.

이 가이드의 초반에 주피터 노트북에서 실행을 가정했기 때문에 이제 우리는 노트북에서 나와야 합니다.  우리는 파이썬 파일을 하나 만들고 이것을 명령 라인에서 동작할 수 있도록 하겠습니다.

먼저 langserve를 설치 합니다.

pip install "langserve[all]"

Server

우리가 만든 어플리케이션을 위한 서버를 만들기 위해서 serve.py라는 파일을 만들겠습니다. 이 파일은 어플리케이션을 서빙하기위한 로직을 담게 될 것입니다. 이것은 3가지로 구성됩니다.

1. 위에서 우리가 만든 체인의 정의

2. 우리의 FastAPI 앱

3. 어디에서 어느 체인으로 서비스될 것인지에 대한 경로 정의(langserve.app_routes에 의해 정의됩니다.)

#!/usr/bin/env python
from typing import List

from fastapi import FastAPI
from langchain.prompts import ChatPromptTemplate
from langchain.chat_models import ChatOpenAI
from langchain_community.document_loaders import WebBaseLoader
from langchain_community.embeddings import OpenAIEmbeddings
from langchain_community.vectorstores import DocArrayInMemorySearch
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.tools.retriever import create_retriever_tool
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain.chat_models import ChatOpenAI
from langchain import hub
from langchain.agents import create_openai_functions_agent
from langchain.agents import AgentExecutor
from langchain.pydantic_v1 import BaseModel, Field
from langchain_core.messages import BaseMessage
from langserve import add_routes

# 1. Load Retriever
loader = WebBaseLoader("https://docs.smith.langchain.com/overview")
docs = loader.load()
text_splitter = RecursiveCharacterTextSplitter()
documents = text_splitter.split_documents(docs)
embeddings = OpenAIEmbeddings()
vector = DocArrayInMemorySearch.from_documents(documents, embeddings)
retriever = vector.as_retriever()

# 2. Create Tools
retriever_tool = create_retriever_tool(
    retriever,
    "langsmith_search",
    "Search for information about LangSmith. For any questions about LangSmith, you must use this tool!",
)
search = TavilySearchResults()
tools = [retriever_tool, search]


# 3. Create Agent
prompt = hub.pull("hwchase17/openai-functions-agent")
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
agent = create_openai_functions_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)


# 4. App definition
app = FastAPI(
  title="LangChain Server",
  version="1.0",
  description="A simple API server using LangChain's Runnable interfaces",
)

# 5. Adding chain route

# We need to add these input/output schemas because the current AgentExecutor
# is lacking in schemas.

class Input(BaseModel):
    input: str
    chat_history: List[BaseMessage] = Field(
        ...,
        extra={"widget": {"type": "chat", "input": "location"}},
    )


class Output(BaseModel):
    output: str

add_routes(
    app,
    agent_executor.with_types(input_type=Input, output_type=Output),
    path="/agent",
)

if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app, host="localhost", port=8000)

이게 전부 입니다. 이제 이 파일을 실행 시킵니다.

python serve.py

 localhost:8000 에서 서빙되고 있는 우리의 체인을 볼 수 있습니다.

Playground

모든 LangServe서비스는 중간 단계를 시각화하 스트리밍 아웃풋이 있는 어플리케이션을 조작하고 부기 위해서 간단한 빌트인 UI를 제공합니다. http://localhost:8000/agent/playground/  이 주소를 통해 확인해 보세요. 이전 질문인 "how can langsmith help with testing?"을 전달해 보세요! 그리고 이전과 같은 답변을 보이는지 확인해보세요.

Client

자, 이제 우리의 서버와 자동으로 상호 작용하는 클라이언트를 만들어 보겠습니다. 우리는  [langserve.RemoteRunnable](/docs/langserve#client) 이것을 가지고 쉽게 만들 수 있습니다. 이것을 사용함으로써 서빙되는 체인을 마치 클라이언트에서 동작하는 것처럼 상호 작용 할 수 있습니다.

from langserve import RemoteRunnable

remote_chain = RemoteRunnable("http://localhost:8000/agent/")
remote_chain.invoke({"input": "how can langsmith help with testing?"})

자세한 내용은 여기를 참고해주세요.

 

Next steps

우리는 LangChain을 활용한 어플리케이션을 어떻게 만들고 LangSmith로 어떻게 그것을 다루며, 그리고 LangServe를 가지고 어떻게 서빙하는지 알아보았습니다. 여기서 다룬 것보다 이 세 가지 모두에는 훨씬 더 많은 기능이 있습니다. 계속해서 학습하려면 다음의 내용을 참고하기를 추천 합니다.:

  • 이런 콤포넌트들을 함께 묶는 방법 같은, 모든 이런 기능들은 LangChain Expression Language (LCEL)을에 의해서 지원됩니다. 사용자에게 맞는 체인을 개발하기위해 더 잘 이해하기 위해서 이 문서들을 참고하세요.
  • Model IO는 prompts, LLMS, 그리고 output parsers에 대해서 더 자세한 것들을 다룹니다.
  • Retrieval은 회수(retrieval)와 관련된 모든 자세한 것들을 다룹니다.
  • Agents는 agent과 관련된 모든 자세한 것들을 다룹니다.
  • 공통적인 end-to-end 사용 사례  템플릿 애플리케이션을 탐색하세요.
  • 디버깅, 테스트, 모니터링 등을 위한 플랫폼인 LangSmith에 대해 읽어보세요.
  • LangServe를 사용하여 애플리케이션을 서비스하는 방법에 대해 더 자세히 알아보세요.

 

 

관련 설명 동영상 : https://www.youtube.com/watch?v=qM2hzIMFhuo&t=7s

 

관련 코드 링크 : https://colab.research.google.com/drive/1_siIcfcJOjJZ3OBW5yX0worB4bubOWJq

 

langchain_quickstart.ipynb

Colaboratory notebook

colab.research.google.com

 

반응형

+ Recent posts