러너블에 폴백 추가하기
러너블에 폴백 추가하기 (How to add fallbacks to a runnable)
언어 모델을 다루다 보면 rate limit이나 서버 다운처럼 기반 API 쪽에서 오는 문제를 꽤 자주 만나요. LLM 애플리케이션을 프로덕션으로 옮길수록 이런 문제로부터 지키는 게 중요해지죠. 그래서 LangChain에는 폴백(fallback) 개념이 있어요. 폴백은 비상 상황에 쓸 수 있는 대체 플랜이라고 보면 됩니다.
출처: 공식문서
중요한 점은 폴백을 LLM 레벨뿐 아니라 전체 러너블 레벨에도 적용할 수 있다는 거예요. 모델마다 필요한 프롬프트가 다른 경우가 많기 때문이죠. OpenAI 호출이 실패했다고 해서 같은 프롬프트를 그대로 Anthropic에 보내고 싶지는 않을 거예요. 다른 프롬프트 템플릿을 써서 다른 버전을 보내는 게 자연스럽습니다.
LLM API 오류에 대한 폴백
폴백의 가장 흔한 사용 사례예요. LLM API 요청은 API 다운, rate limit 도달 등 다양한 이유로 실패할 수 있어요. 폴백은 그런 상황으로부터 보호해 줍니다.
중요: 기본적으로 많은 LLM 래퍼가 오류를 잡아서 재시도해요. 폴백을 쓸 때는 이 재시도를 끄는 게 좋아요. 그렇지 않으면 첫 번째 래퍼가 실패하지 않고 계속 재시도할 거예요.
%pip install --upgrade --quiet langchain langchain-openai
from langchain_anthropic import ChatAnthropic
from langchain_openai import ChatOpenAI
먼저 OpenAI에서 RateLimitError가 나는 상황을 mock으로 만들어 볼게요.
from unittest.mock import patch
import httpx
from openai import RateLimitError
request = httpx.Request("GET", "/")
response = httpx.Response(200, request=request)
error = RateLimitError("rate limit", response=response, body="")
# Note that we set max_retries = 0 to avoid retrying on RateLimits, etc
openai_llm = ChatOpenAI(model="gpt-4o-mini", max_retries=0)
anthropic_llm = ChatAnthropic(model="claude-3-haiku-20240307")
llm = openai_llm.with_fallbacks([anthropic_llm])
with_fallbacks([anthropic_llm])로 OpenAI가 실패하면 Anthropic 모델로 넘어가도록 폴백 체인을 만들었어요. max_retries=0으로 재시도를 끈 점도 확인하세요.
먼저 OpenAI만 써서 오류가 나는 걸 보여줄게요.
# Let's use just the OpenAI LLm first, to show that we run into an error
with patch("openai.resources.chat.completions.Completions.create", side_effect=error):
try:
print(openai_llm.invoke("Why did the chicken cross the road?"))
except RateLimitError:
print("Hit error")
이제 폴백을 붙인 상태로 다시 시도해 볼게요.
# Now let's try with fallbacks to Anthropic
with patch("openai.resources.chat.completions.Completions.create", side_effect=error):
try:
print(llm.invoke("Why did the chicken cross the road?"))
except RateLimitError:
print("Hit error")
"폴백이 붙은 LLM"은 일반 LLM처럼 그냥 쓰면 돼요.
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"You're a nice assistant who always includes a compliment in your response",
),
("human", "Why did the {animal} cross the road"),
]
)
chain = prompt | llm
with patch("openai.resources.chat.completions.Completions.create", side_effect=error):
try:
print(chain.invoke({"animal": "kangaroo"}))
except RateLimitError:
print("Hit error")
시퀀스(체인)에 대한 폴백
폴백 자체가 시퀀스인 경우에도 시퀀스 폴백을 만들 수 있어요. 다른 두 모델로 만들어 볼게요. 하나는 ChatOpenAI(채팅 모델), 다른 하나는 일반 OpenAI(채팅 모델 아님)예요. OpenAI는 채팅 모델이 아니므로 아마 다른 프롬프트를 쓰고 싶을 거예요.
# First let's create a chain with a ChatModel
# We add in a string output parser here so the outputs between the two are the same type
from langchain_core.output_parsers import StrOutputParser
chat_prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"You're a nice assistant who always includes a compliment in your response",
),
("human", "Why did the {animal} cross the road"),
]
)
# Here we're going to use a bad model name to easily create a chain that will error
chat_model = ChatOpenAI(model="gpt-fake")
bad_chain = chat_prompt | chat_model | StrOutputParser()
# Now lets create a chain with the normal OpenAI model
from langchain_core.prompts import PromptTemplate
from langchain_openai import OpenAI
prompt_template = """Instructions: You should always include a compliment in your response.
Question: Why did the {animal} cross the road?"""
prompt = PromptTemplate.from_template(prompt_template)
llm = OpenAI()
good_chain = prompt | llm
# We can now create a final chain which combines the two
chain = bad_chain.with_fallbacks([good_chain])
chain.invoke({"animal": "turtle"})
각 시퀀스의 출력 타입을 맞추기 위해 StrOutputParser를 붙인 점에 주목하세요. 이렇게 하면 두 체인을 폴백 관계로 안전하게 묶을 수 있어요.
긴 입력에 대한 폴백
LLM의 큰 제약 중 하나가 컨텍스트 윈도우예요. 보통은 LLM에 보내기 전에 프롬프트 길이를 세고 추적할 수 있지만, 그게 어렵거나 복잡한 상황에서는 더 긴 컨텍스트 길이를 가진 모델로 폴백할 수 있어요.
short_llm = ChatOpenAI()
long_llm = ChatOpenAI(model="gpt-3.5-turbo-16k")
llm = short_llm.with_fallbacks([long_llm])
inputs = "What is the next number: " + ", ".join(["one", "two"] * 3000)
try:
print(short_llm.invoke(inputs))
except Exception as e:
print(e)
try:
print(llm.invoke(inputs))
except Exception as e:
print(e)
짧은 컨텍스트 모델(short_llm)은 입력이 너무 길어 실패하지만, 폴백이 붙은 llm은 더 긴 컨텍스트를 가진 16k 모델로 넘어가서 처리돼요.
더 나은 모델로 폴백
모델에게 특정 형식(예: JSON)으로 출력을 요청하는 경우가 많아요. GPT-3.5 같은 모델도 어느 정도는 잘 하지만 가끔 어려워해요. 이런 상황은 자연스럽게 폴백으로 이어집니다. 더 빠르고 저렴한 GPT-3.5로 시도해 보고, 파싱이 실패하면 GPT-4를 쓰는 방식이죠.
from langchain.output_parsers import DatetimeOutputParser
prompt = ChatPromptTemplate.from_template(
"what time was {event} (in %Y-%m-%dT%H:%M:%S.%fZ format - only return this value)"
)
# In this case we are going to do the fallbacks on the LLM + output parser level
# Because the error will get raised in the OutputParser
openai_35 = ChatOpenAI() | DatetimeOutputParser()
openai_4 = ChatOpenAI(model="gpt-4") | DatetimeOutputParser()
only_35 = prompt | openai_35
fallback_4 = prompt | openai_35.with_fallbacks([openai_4])
try:
print(only_35.invoke({"event": "the superbowl in 1994"}))
except Exception as e:
print(f"Error: {e}")
try:
print(fallback_4.invoke({"event": "the superbowl in 1994"}))
except Exception as e:
print(f"Error: {e}")
이 경우 오류는 DatetimeOutputParser에서 발생하므로, LLM과 출력 파서를 묶은 수준에서 폴백을 걸었어요. GPT-3.5 조합이 파싱에 실패하면 GPT-4 조합으로 자동으로 넘어갑니다.