[LCEL 시리즈] invoke, batch, stream

안녕하세요.

9기 HuggingFace 파트너 겸 뾰족한 개발자 청강생 정정민입니다. 😁


8기에서 랭체인을 공부하면서,

LCEL을 깊이 있게 이해해야 함을 느끼게 되었습니다. (LCEL이 미래다!!)

얼마나 될지 모르겠지만 LCEL을 알아가는 과정을 담아보려 합니다.


오늘은 첫 시간으로, LCEL를 찾아보면 항상 맨 앞에 나오는 말이죠.

invoke, batch, stream을 사용해보고 각각의 특징을 살펴보려고 합니다.



LCEL 설명 페이지를 찾아보면…

이런 이야기가 있습니다.


앞으로 살펴 볼 많은 이야기가 담겨있네요.

일단 오늘 살펴볼 내용만 초점을 맞춰보면,

LCEL을 활용해 구성한 chain은 일반적으로 많이 사용하는 명령 함수를 사용할 수 있다고 하네요.

거기에는 아래의 것이 포함됩니다.

  • invoke

  • batch

  • stream

이게 좋은지, 어떤 장점이 있는지, 어떤 특징이 있는지 등등을 알아야

LCEL을 이해할 수 있겠네요.

(참고로, 앞에 a가 붙은 비동기 명령문(ainvoke, abatch, astream)들도 있습니다만, 이는 추후에 살펴보죠!)


이 포스트에서 사용할 간단한 chain을 만들어보죠. (공식 페이지의 예시 코드를 활용)

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

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

invoke

invoke란, 구성한 chain에 단 하나의 입력을 넣어 출력을 반환하는 함수입니다.

저는 invoke 라는 용어를 사용해본 경험이 없었는데요..

네이버 영어 사전을 보니 invoke의 6번째 의미를 이용한 것 같네요 ^^;;


위 chain에 입력으로 ‘ice cream’을 넣어볼까요?

이때 사용하는 메소드가 invoke 입니다.

chain.invoke('ice cream')

결과는 아래와 같습니다.

Why did the ice cream go to the gym?
Because it wanted to be a little "cone"dy!


시간은 얼마나 걸릴까요?

시간 측정 함수를 작성해 결과를 살펴보면 아래와 같습니다.

def measure_execution_time(func, *args, **kwargs):
    start_time = time.time()  
    result = func(*args, **kwargs) 
    end_time = time.time() 
    execution_time = end_time - start_time  
    print(f"{func.__name__} 실행 시간: {execution_time:.3f}초")
    return result

measure_execution_time(chain.invoke, "ice cream") # 1.294 초 

사용하는 모델과 인터넷 상황에 따라 다르겠지만 대략 1.294초가 걸렸네요.



batch

batch란, 구성한 chain에 복수의 입력을 넣어 각 입력에 따른 출력을 반환하는 함수입니다.


위 예를 사용했을 때,

ice cream에 따른 결과 뿐 아니라

chocolate의 결과도 그리고 vanilla 결과도 같이 보고싶으면 어떻게 할까요??

가장 쉽게 생각할 수 있는 것은 앞서 살펴본 invoke를 원하는 입력 수 만큼 넣어주면 되겠네요.

마치 아래와 같은 pseudo code 처럼요!

results = [] 
for 입력 in 원하는 입력들 : 
    res = chat.invoke(입력) 
    results.append()


하지만 여기에는 문제(?)가 있는데,

직렬화 방식으로 인한 작업 시간의 증가입니다.

s = time.time()
for item in ["ice cream", "chocolate", "vanilla"]: 
    chain.invoke(item)
duration = time.time() - s 
print(f"Duration: {duration:.3f} sec") # 3.097 sec 

앞선 invoke 에서 약 1.3초 걸렸는데

3개 했을 때 3.1초 정도 걸렸네요.

어느정도 선형적으로 증가하네요.


좀 더 실험적으로 보면

선형성이 더 잘 보이네요.

(4개 무슨일이죠?? ㅎㅎ;;)


batch 메소드는

복수개의 입력을 한번에 넣을 수 있는 기능을 제공합니다.

코드를 쭉 따라가보면 (링크)

invoke 함수를 map 함수로 감싸 병렬로 처리를 하네요

with get_executor_for_config(configs[0]) as executor:
    return cast(List[Output], list(executor.map(invoke, inputs, configs)))


chatGPT의 답에 따르면 executor.map은 이런 함수라고 합니다.

» executor.map이 무슨 함수야?

» executor.map 함수는 concurrent.futures 모듈의 Executor 클래스(또는 그 하위 클래스인 ThreadPoolExecutor와 ProcessPoolExecutor)의 메서드입니다. 이 메서드는 여러 작업을 동시에 실행하려고 할 때 사용됩니다. Python의 map 함수와 유사하게, executor.map은 주어진 함수를 순회 가능한(iterable) 각 요소에 대해 병렬로 적용합니다. 하지만 executor.map은 작업을 병렬로 실행하여 CPU 사용을 최적화하거나 I/O 작업을 비동기적으로 수행할 때 이점을 제공합니다.




3개의 입력을 동시에 처리하면 아래와 같은 결과와 시간이 걸립니다.

measure_execution_time(chain.batch, ["ice cream", "chocolate", "vanilla"]) # 1.33초
>> ['Why was the ice cream so bad at tennis? \n\nBecause it had a soft serve!', 'Why did the chocolate chip cookie go to the doctor?\n\nBecause it was feeling crumby!', "Why did the vanilla go to therapy?\n\nBecause it had serious identity issues - it couldn't decide if it wanted to be ice cream or a candle scent!"]

결과도 원하는 방식으로 잘 나왔네요!

직렬 처리 시 3.1초가 걸린 것에 비해 매우 짧게 걸렸네요!

마치 1개를 처리하는 시간과 비슷합니다!!


어느 정도의 fluctuate가 존재하지만

invoke를 활용한 for문보다는

시간 측면에서 훨씬 이득이 많네요.



Stream

우리가 chatGPT를 쓰다보면

한 단어, 한 단어씩 글이 만들어집니다.


이 효과(?)의 장점은

  • 답변을 실시간으로 만드는 것 같은 느낌

  • 작업 속도를 간접적으로 확인

  • 답변의 내용을 답변 중에 습득 가능

  • 등등

이 있어보입니다.


이런 방식을 streaming 이라고 하고

이는 특히, 답변에 긴 경우 유용하다고 합니다.


LCEL에서도 이 기능을 제공하며

아래와 같은 코드로 구현 가능하다고 합니다.

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

chunk는 LLM에서 제공하는 token 의 단위입니다.

즉, 하나 하나의 token이 출력되고 그것을 바로 보여줍니다.


아래는 chunk 단위를 구분하는 구분자를 넣어 출력한 결과입니다.

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

>> !! !! Why!!  did!!  the!!  ice!!  cream!!  go!!  to!!  therapy!! ?!!  Because!!  it!!  had!!  a!!  rocky!!  road!! !!! %


구현할 서비스에 따라 streaming 방식이 유용할 것 같네요!


#9기랭체인


작성자 : 정정민

블로그 : 링크

3
1개의 답글

(채용) 유튜브 PD, 마케터, AI엔지니어, 디자이너

지피터스의 콘텐츠 플라이휠로 고속 성장할 팀원을 찾습니다!

👉 이 게시글도 읽어보세요