LLM 출력 파싱: 함수 호출 vs. LangChain

LLM Output Parsing: Function Calling vs. LangChain

LLM을 이용한 도구 생성은 벡터 데이터베이스, 체인, 에이전트, 문서 분할 도구 등 많은 새로운 도구들을 필요로 합니다.

하지만, 가장 중요한 구성 요소 중 하나는 LLM의 출력 파싱입니다. LLM에서 구조적인 응답을 받지 못한다면, 생성된 결과물을 다루는 것이 어려워집니다. LLM에 단일 호출로 여러 가지 정보를 출력하고자 할 때 이 문제는 더욱 도드라집니다.

이 문제를 가상의 시나리오로 설명해보겠습니다:

우리는 LLM에서 단일 호출로 특정 레시피의 재료와 만드는 방법을 출력하길 원합니다. 그러나 우리 시스템의 다른 부분에서 사용하기 위해 이 두 항목을 따로 받고 싶습니다.

import openai

recipe = 'Fish and chips'
query = f"""{recipe}의 레시피는 무엇입니까? 
재료 목록과 단계를 별도로 반환해주세요."""

response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613",
    messages=[{"role": "user", "content": query}])

response_message = response["choices"][0]["message"]
print(response_message['content'])

위의 코드는 LLM에 특정 레시피의 재료와 조리법을 단일 호출로 요청하는 방법을 보여줍니다. 만약 LLM이 재료와 조리법을 구분된 형태로 제공하지 않으면, 시스템의 다른 부분에 적절하게 적용하기 어려울 것입니다.

This returns the following:

Ingredients for fish and chips:
- 1 pound white fish fillets (such as cod or haddock)
- 1 cup all-purpose flour
- 1 teaspoon baking powder
- 1 teaspoon salt
- 1/2 teaspoon black pepper
- 1 cup cold beer
- Vegetable oil, for frying
- 4 large russet potatoes
- Salt, to taste

Steps to make fish and chips:

1. Preheat the oven to 200°C (400°F).
2. Peel the potatoes and cut them into thick, uniform strips. Rinse the potato strips in cold water to remove excess starch. Pat them dry using a clean kitchen towel.
3. In a large pot or deep fryer, heat vegetable oil to 175°C (350°F). Ensure there is enough oil to completely submerge the potatoes and fish.
4. In a mixing bowl, combine the flour, baking powder, salt, and black pepper. Whisk in the cold beer gradually until a smooth batter forms. Set the batter aside.
5. Take the dried potato strips and fry them in batches for about 5-6 minutes or until golden brown. Remove the fries using a slotted spoon and place them on a paper towel-lined dish to drain excess oil. Keep them warm in the preheated oven.
6. Dip each fish fillet into the prepared batter, ensuring it is well coated. Let any excess batter drip off before carefully placing the fillet into the hot oil.
7. Fry the fish fillets for 4-5 minutes on each side or until they turn golden brown and become crispy. Remove them from the oil using a slotted spoon and place them on a paper towel-lined dish to drain excess oil.
8. Season the fish and chips with salt while they are still hot.
9. Serve the fish and chips hot with tartar sauce, malt vinegar, or ketchup as desired.

Enjoy your homemade fish and chips!

이것은 매우 큰 문자열이며, LLM이 약간 다른 구조로 결과를 반환할 수 있기 때문에 이를 파싱하는 것은 어렵습니다. 항상 “Ingredients:”와 “Steps:”를 반환하도록 프롬프트에서 요청하는 것이 해결책이 될 수 있다고 주장할 수 있으며, 그 주장은 틀린 것이 아닙니다. 이 방법은 작동할 수 있지만, 여전히 문자열을 수동으로 처리해야 하며, 이벤트 변동 및 환상(잘못된 정보)에 대한 가능성을 열어 두어야 합니다.

해결책

이 문제를 해결할 수 있는 몇 가지 방법이 있습니다. 위에서 하나를 언급했지만, 테스트된 몇 가지 방법이 더 나을 수 있습니다. 이 글에서는 두 가지 옵션을 소개하겠습니다:

  1. Open AI 함수 호출
  2. LangChain 출력 파서.

이 두 가지 방법 각각의 장점과 단점을 살펴보며, 어떤 상황에서 각각이 더 유용한지 알아보겠습니다.

Open AI 함수 호출

이 방법은 제가 시도해 본 방법 중 가장 일관된 결과를 가져온 방법입니다. Open AI API의 함수 호출 기능을 사용하여 모델이 응답을 구조화된 JSON 형태로 반환하게 합니다.

이 기능의 목적은 LLM에게 JSON 형태의 입력을 제공함으로써 외부 함수를 호출하는 능력을 제공하는 것입니다. 모델은 주어진 함수를 사용할 때 언제 그것을 사용해야 하는지 이해하기 위해 미세 조정되었습니다. 이의 예는 현재 날씨에 대한 함수입니다. 만약 GPT에게 현재 날씨를 물으면 알려줄 수 없지만, 이것을 수행하는 함수를 제공하고 GPT에 전달하면 어떤 입력이 주어지면 접근할 수 있다는 것을 알게 됩니다.

이 기능에 대해 더 깊이 알고 싶다면, Open AI의 공지를 확인하십시오. 그리고 이것은 훌륭한 기사입니다.

그렇다면 주어진 문제를 기반으로 코드에서 이것이 어떻게 보일지 살펴보겠습니다. 코드를 분해해봅시다:

functions = [
    {
        "name": "return_recipe",
        "description": "Return the recipe asked",
        "parameters": {
            "type": "object",
            "properties": {
                "ingredients": {
                    "type": "string",
                    "description": "The ingredients list."
                },
                "steps": {
                    "type": "string",
                    "description": "The recipe steps."
                },
            },
            },
            "required": ["ingredients","steps"],
        }
]

우리가 해야 할 첫 번째 일은 LLM에 사용할 수 있는 함수를 선언하는 것입니다. 모델이 함수를 사용해야 할 때 이해할 수 있도록 이름과 설명을 제공해야 합니다. 여기서 이 함수는 요청된 레시피를 반환하는 데 사용된다고 알려줍니다.

그런 다음 매개변수로 이동합니다. 먼저 object 유형이며 사용할 수 있는 속성은 ingredients와 steps라고 말합니다. 이 두 가지 모두 LLM의 출력을 안내하기 위해 설명과 유형을 가지고 있습니다. 마지막으로 함수를 호출하기 위해 필요한 속성 중 어떤 것을 지정하는지 지정합니다 (이것은 LLM이 사용하고 싶은 경우 선택적 필드를 가질 수 있다는 것을 의미합니다).

이와 같이, 함수 호출 기능을 통해 LLM에게 명확한 구조와 필요한 정보를 전달할 수 있게 되어, 예측 가능하고 일관된 출력을 얻을 수 있게 됩니다.

이제 LLM에 호출을 해봅시다:

import openai

recipe = 'Fish and chips'
query = f"What is the recipe for {recipe}? Return the ingredients list and steps separately."

response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613",
    messages=[{"role": "user", "content": query}],
    functions=functions,
    function_call={'name':'return_recipe'}
)
response_message = response["choices"][0]["message"]

print(response_message)
print(response_message['function_call']['arguments'])

우리는 먼저 API에 대한 쿼리를 생성하여 기본 프롬프트를 변수 입력(recipe)으로 포맷팅합니다. 그런 다음 “gpt-3.5-turbo-0613”을 사용하여 API 호출을 선언하고, 메시지 인수에서 쿼리를 전달하며, 이제 우리의 함수를 전달합니다.

이를 통해, API는 우리가 지정한 함수를 사용하여 요청을 처리하게 됩니다. 이 경우 ‘return_recipe’ 함수는 레시피의 재료 목록과 조리법을 별도로 반환하는 것을 지시합니다. 함수 호출을 사용하면 LLM의 출력을 보다 구조화된 형식으로 받을 수 있게 되므로 파싱 및 처리가 훨씬 쉬워집니다.

함수에 관한 두 가지 인자가 있습니다. 첫 번째는 위에서 보여준 형식으로 객체 리스트를 전달하며, 이는 모델이 접근할 수 있는 함수들입니다. 두 번째 인자인 “function_call”에서는 모델이 이러한 함수를 어떻게 사용할 것인지 지정합니다. 세 가지 선택지가 있습니다:

  1. “Auto” -> 모델은 사용자 응답과 함수 호출 사이에서 선택합니다.
  2. “none” -> 모델은 함수를 호출하지 않고 사용자의 응답만 반환합니다.
  3. {“name”: “my_function_name”} -> 함수의 이름을 지정하면, 모델은 해당 함수를 사용하도록 강제됩니다.

이렇게 다양한 옵션을 통해 개발자는 LLM과의 상호작용을 더욱 효과적으로 제어할 수 있게 됩니다.

You can find the official documentation here.

우리의 경우 출력 파싱을 사용하기 위해 다음과 같이 선택했습니다:

function_call={'name':'return_recipe'}

이제 우리의 응답을 살펴보겠습니다. 이 필터 [“choices”][0][“message”] 후에 우리가 얻는 응답은:

{
  "role": "assistant",
  "content": null,
  "function_call": {
    "name": "return_recipe",
    "arguments": "{...}"
  }
}

이를 “function_call”로 더 파싱하면 우리가 원하는 구조화된 응답을 볼 수 있습니다:

{
  "ingredients": "For the fish:\n- 1 lb white fish fillets\n- 1 cup all-purpose flour\n... (이하 생략)",
  "steps": "1. Start by preparing the fish. In a shallow dish, combine the flour, baking powder, salt, and black pepper.\n... (이하 생략)"
}

이렇게 function_call을 사용하면 LLM의 출력을 명확하게 구조화된 형식으로 얻을 수 있어, 후속 처리나 다른 시스템과의 통합이 더 쉬워집니다.

함수 호출에 대한 결론
Open AI API에서 바로 함수 호출 기능을 사용할 수 있습니다. 이를 통해 LLM을 호출할 때마다 동일한 키를 가진 딕셔너리 형식의 응답을 받을 수 있습니다.

이를 사용하는 것은 매우 간단합니다. 작업에 중점을 둔 name, description 및 properties를 지정하여 함수 객체를 선언하기만 하면 되지만 (description에서) 이것이 모델의 응답이어야 한다고 지정해야 합니다. 또한 API를 호출할 때 모델에 우리의 함수를 사용하도록 강제함으로써 더욱 일관성 있게 만들 수 있습니다.

이 방법의 주요 단점은 모든 LLM 모델과 API에서 지원되지 않는다는 것입니다. 따라서 Google PaLM API를 사용하려면 다른 방법을 사용해야 합니다.

LangChain 출력 파서

모델에 구애받지 않는 대안 중 하나는 LangChain을 사용하는 것입니다.

먼저 LangChain이 무엇인지 알아보겠습니다.

LangChain은 언어 모델로 구동되는 애플리케이션을 개발하기 위한 프레임워크입니다.

이것이 LangChain의 공식 정의입니다. 이 프레임워크는 최근에 만들어졌으며 이미 LLM으로 구동되는 도구를 만드는 데 있어 업계 표준으로 사용되고 있습니다.

우리의 사용 사례에 아주 적합한 “출력 파서”라는 기능이 있습니다. 이 모듈에는 LLM 호출로부터 다양한 형식을 반환하고 파싱할 수 있는 여러 객체가 포함되어 있습니다. 먼저 형식을 선언하고 이를 LLM에 프롬프트로 전달함으로써 이를 달성합니다. 그런 다음 이전에 만든 객체를 사용하여 응답을 파싱합니다.

코드를 분석해봅시다:

from langchain.prompts import ChatPromptTemplate
from langchain.output_parsers import ResponseSchema, StructuredOutputParser
from langchain.llms import GooglePalm, OpenAI

ingredients = ResponseSchema(
        name="ingredients",
        description="레시피의 재료를 단일 문자열로 표시합니다.",
    )
steps = ResponseSchema(
        name="steps",
        description="레시피를 준비하는 단계를 단일 문자열로 표시합니다.",
    )

output_parser = StructuredOutputParser.from_response_schemas(
    [ingredients, steps]
)

response_format = output_parser.get_format_instructions()
print(response_format)

prompt = ChatPromptTemplate.from_template("레시피는 {recipe}에 대한 것이며, 재료 목록과 단계를 별도로 반환하십시오. \n {format_instructions}")

우선, 파서의 입력이 될 Response Schema를 만듭니다. 재료와 단계 각각에 대해 하나씩 만들며, 사전의 키가 될 이름과 LLM 응답을 안내하는 설명을 포함합니다.

그런 다음 이러한 응답 스키마로부터 StructuredOutputParser를 생성합니다. 파서의 다양한 스타일로 이 작업을 수행하는 여러 가지 방법이 있습니다. 그들에 대해 더 자세히 알아보려면 해당 문서를 참조하십시오.

마지막으로, 우리는 형식 지침을 얻고 레시피 이름과 형식 지침을 입력으로 사용하는 프롬프트를 정의합니다. 형식 지침은 다음과 같습니다:

"""
The output should be a markdown code snippet formatted in the following schema, including the leading and trailing "```json" and "```":

```json
{
 "ingredients": string  // The ingredients from recipe, as a unique string.
 "steps": string  // The steps to prepare the recipe, as a unique string.
}  
"""
출력은 다음 스키마로 포맷된 마크다운 코드 스니펫이어야 하며, 선행 및 후행 "```json" 및 "```"를 포함합니다:

json
{
“ingredients”: string // 레시피의 재료를 단일 문자열로 표시합니다.
“steps”: string // 레시피를 준비하는 단계를 단일 문자열로 표시합니다.
}
“`

이 형식 지침은 LLM에게 어떻게 응답을 포맷해야 하는지 구체적으로 알려주기 위한 것입니다. 이러한 지침을 사용하면, LLM은 제공된 형식을 준수하여 응답을 반환합니다. 그런 다음 StructuredOutputParser는 해당 형식에 따라 응답을 파싱하고, 그 결과로 사용자는 일관된 형식의 구조화된 응답을 얻을 수 있습니다.

Now what we have left is just calling the API. Here I will demonstrate both the Open AI API and with Google PaLM API.

llm_openai = OpenAI()
llm_palm = GooglePalm()

recipe = 'Fish and chips'

formated_prompt = prompt.format(**{"recipe":recipe, "format_instructions":output_parser.get_format_instructions()})

response_palm = llm_palm(formated_prompt)
response_openai = llm_openai(formated_prompt)

print("PaLM:")
print(response_palm)
print(output_parser.parse(response_palm))

print("Open AI:")
print(response_openai)
print(output_parser.parse(response_openai))

위의 코드는 LangChain 프레임워크가 제공하는 모델 중립적인 기능을 보여줍니다. 이를 다음과 같이 분석할 수 있습니다:

  1. 모델 초기화:
    • OpenAI와 Google PaLM을 위한 두 개의 다른 LLMs (언어 학습 모델)이 초기화됩니다. 이것은 LangChain 프레임워크를 사용하여 다양한 모델을 쉽게 통합할 수 있음을 보여줍니다.
  2. 프롬프트 형성:
    • 이전에 정의된 프롬프트 템플릿은 원하는 레시피 이름(“Fish and chips”)과 특정 형식 지침으로 채워집니다. 이를 통해 언어 모델이 응답을 파서가 해석하기 쉬운 방식으로 어떻게 형식화해야 하는지 알 수 있습니다.
  3. 모델 호출:
    • 동일한 형식화된 프롬프트가 Google PaLM 및 OpenAI 모델에 전달됩니다. 프롬프팅과 파싱에 표준화된 접근 방식이 있으면 다양한 모델 간에 이렇게 원활하고 교환 가능한 프로세스를 가질 수 있음을 이 행동이 보여줍니다.
  4. 출력 파싱:
    • 두 모델로부터의 응답을 받으면 StructuredOutputParser가 이러한 응답을 더 구조화된 형식으로 파싱합니다. 이것은 두 모델의 응답 모두에 대해 수행됩니다.
  5. 결과 표시:
    • PaLM과 OpenAI 모델 모두의 원시 응답과 파싱된 결과가 출력됩니다. 이를 통해 두 모델의 출력을 비교하거나 대조하는 것이 쉬워집니다.

본질적으로, LangChain 프레임워크는 특히 출력 파서를 통해 다양한 모델 간에 출력을 처리하는 구조화되고 일관된 방법을 제공합니다. 이것은 다양한 LLMs를 활용하려는 개발자들에게 일관된 상호 작용 패턴을 유지하면서 매우 간단한 프로세스를 제공합니다. OpenAI의 GPT 시리즈나 Google의 PaLM과 같은 것을 사용하든 간에, 이런 표준화된 접근 방식을 사용하면 개발자들은 효율적으로 그 사이를 전환하거나 비교 또는 앙상블 목적으로 나란히 실행할 수 있습니다.

This generated the following output:

LangChain 출력 파싱 결론:

LangChain의 출력 파싱 방식은 그 특징으로서의 유연성으로 매우 유용합니다. 우리는 다양한 모델과 함께 쉽게 조합하고 사용할 수 있는 Response Schema, Output Parser, 및 Prompt Templates와 같은 몇 가지 구조를 생성합니다. 또한 여러 출력 형식을 지원한다는 점이 큰 장점입니다.

주요 단점은 프롬프트를 통해 형식 지침을 전달하는 것입니다. 이로 인해 무작위 오류나 환상(무작위로 부정확한 출력)이 발생할 수 있습니다. 실제 예로는 응답 스키마의 설명에 “유니크한 문자열로”라고 명시해야 했던 경우를 들 수 있습니다. 이것을 명시하지 않으면 모델이 단계와 지침을 문자열 목록으로 반환했고, 이로 인해 Output Parser에서 파싱 오류가 발생했습니다.

따라서 LangChain을 사용할 때는 항상 이러한 세부 사항에 주의를 기울여야 하며, 의도한 출력 형식에 맞게 프롬프트와 설명을 신중하게 작성해야 합니다.

결론:

LLM 기반 애플리케이션에 출력 파서를 사용하는 방법은 여러 가지가 있습니다. 그러나 주어진 문제에 따라 선택이 달라질 수 있습니다. 저 개인적으로는 다음과 같은 아이디어를 따르기를 좋아합니다:

LLM에서 단 하나의 출력만 있더라도 항상 출력 파서를 사용합니다. 이를 통해 출력을 통제하고 명세할 수 있습니다. Open AI와 작업할 때는 Function Calling을 선택합니다. 왜냐하면 이 방법이 가장 통제력이 있고, 프로덕션 애플리케이션에서 무작위 오류를 피할 수 있기 때문입니다. 그러나 다른 LLM을 사용하거나 다른 출력 형식이 필요한 경우, 제 선택은 LangChain입니다. 하지만 출력에 대한 많은 테스트를 통해 최소한의 실수로 프롬프트를 작성하기 위해 노력해야 합니다.

결국, 목적에 따라 가장 적합한 도구와 방법을 선택하는 것이 중요합니다.

The full code can be found here.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다