태그 보관물: JSON

LangChain: OutputParser로 LLM 출력 다루기

앞에서 ChatGPT와 OpenAI API를 이용해 json 형식의 문자열을 출력하는 예제를 살펴봤습니다. LangChain에는 이렇게 원하는 형식의 출력을 얻을 수 있도록 OutputParser 기능을 제공합니다. 앞에서 봤던 예제를 OutputParser를 이용해 구현해보겠습니다.

OutputParser

LangChain의 OutputParser에는 미리 만들어져있는 parser들도 있지만 여기서는 직접 만들어보겠습니다. 아래 예제에서 ResponseSchema는 특정 항목이 어떤 형식으로 출력되어야 하는지 지정하는데 사용하고, StructuredOutputParser는 전체 schema들을 모아 parser를 생성하는데 사용합니다. 여기서는 하나의 항목만 ‘output’이라는 이름으로 추출하려고 합니다. 이 항목의 내용은 {"사람 이름": "영어 색깔 이름"}의 json 형식(파이썬 딕셔너리)으로 만들고 싶습니다.

from langchain.output_parsers import ResponseSchema
from langchain.output_parsers import StructuredOutputParser

schema = ResponseSchema(name='output', description="json object, key=name, value=color in English")

response_schemas = [schema]

output_parser = StructuredOutputParser.from_response_schemas(response_schemas)

이렇게 만든 output_parser에서는 get_format_instructions라는 메소드와 parse 메소드를 지원합니다. get_format_instructions는 출력 형식 지정 프롬프트를 만들어주는 메소드입니다. 형식 지정 프롬프트는 LLM에 보낼 전체 프롬프트에 포함시킵니다. parse 메소드는 LLM의 출력이 문자열로 나오니 최종적으로 원하는 형식으로 바꿔주는 메소드입니다. format_instructions를 확인해보죠.

format_instructions: 출력 형식 지정 프롬프트

format_instructions = output_parser.get_format_instructions()
print(format_instructions)

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

```json
{
	"<strong>output</strong>": string  // json object, key=name, value=color in English
}
```

위와 같이 json 형식의 문자열을 포함한 마크다운 코드를 출력하라는 내용은 StructuredOutputParser에 미리 입력되어 있고, ResponseSchema에 적은 내용이 json 항목 설명으로 들어가게 됩니다. 그럼 전체 프롬프트를 만들어보죠.

from langchain.prompts import ChatPromptTemplate

template="""
주어진 text로부터 다음 정보를 추출하세요.

"사람 이름": "color in English"

text: ```{text}```

{format_instructions}
"""

prompt = ChatPromptTemplate.from_template(template)

messages = prompt.format_messages(text="영희는 파란색을 좋아하고 영수는 분홍색을 좋아한다.",
format_instructions=format_instructions)

최종 프롬프트 messages의 내용은 다음과 같습니다.

[HumanMessage(content='\n주어진 text로부터 다음 정보를 추출하세요.\n\n"사람 이름": "color in English"\n\ntext: ```영희는 파란색을 좋아하고 영수는 분홍색을 좋아한다.```\n\nThe output should be a markdown code snippet formatted in the following schema, including the leading and trailing "```json" and "```":\n\n```json\n{\n\t"output": string  // json object, key=name, value=color in English\n}\n```\n', additional_kwargs={}, example=False)]

이 메시지를 LLM에 보내 응답을 받습니다.

from langchain.chat_models import ChatOpenAI
chat = ChatOpenAI(temperature=0.0, model='gpt-3.5-turbo')
response = chat(messages)

response는 다음과 같습니다.

AIMessage(content='```json\n{\n\t"output": {\n\t\t"영희": "blue",\n\t\t"영수": "pink"\n\t}\n}\n```', additional_kwargs={}, example=False)

response.content는 문자열입니다. 최종적으로 원하는 결과를 얻기 위해 이 내용을 output_parser에 전달합니다.

parser: 최종 결과 출력

output = output_parser.parse(response.content)

### 결과
{'output': {'영희': 'blue', '영수': 'pink'}}

그 결과로 딕셔너리를 얻었고, output['output']에 원하는 결과를 얻었습니다.

ChatGPT에서 JSON 출력하기

ChatGPT에서의 프롬프트 엔지니어링

ChatGPT에서 원하는 출력을 얻기 위해서는 시행착오가 필요합니다. 원하는 출력을 얻는 프롬프트를 찾아내는 과정을 프롬프트 엔지니어링이라고 하죠. 문자열을 해석해서 컴퓨터에서 작업하기 좋도록 json 형식으로 만들고 싶습니다. 그래서 ChatGPT에게 다음과 같이 요청했습니다. One-shot learning을 위한 예시도 넣었죠.

문자열에서 정보를 추출해서 json으로 바꿔줘.

## 예시
“수연이는 파란색을 좋아하고, 철호는 빨간색을 좋아한다” => {“수연”: “blue”,”철호”: “red”}

## 입력
“영희는 노란색을 좋아하고, 민수는 초록색을 좋아한다” =>

그랬더니 직접 json을 만들어주는 대신 json으로 변환하는 코드를 만들어 줬습니다.(결과는 물어볼 때마다 달라질 수 있습니다.)

입력된 문자열에서 정보를 추출하고 JSON 형식으로 바꾸려면 Python 프로그래밍 언어를 사용하여 다음과 같이 할 수 있습니다.

import re
import json

# 입력 문자열
input_string = "영희는 노란색을 좋아하고, 민수는 초록색을 좋아한다"

# 이름과 색상 정보 추출
name_color_pattern = r'(\w+)는\s(\w+)색을\s좋아한다'
matches = re.findall(name_color_pattern, input_string)

# 결과를 JSON 형식으로 변환
result = {name: color for name, color in matches}

# JSON 문자열로 변환
json_result = json.dumps(result, ensure_ascii=False)

print(json_result)


위 코드는 입력된 문자열에서 이름과 색상 정보를 추출하고 이를 JSON 형식으로 변환합니다. 코드를 실행하면 원하는 결과가 출력됩니다.

그러나 이 코드는 예시에서 보여준 것처럼 색깔을 영어로 만들지 못하죠. 그래서 “키는 이름, 값은 색깔을 영어로” 만들어 달라고 프롬프트에 추가했는데 앞에서와 비슷한 대답을 했습니다. 이번엔 코드를 작성하지 말고 직접 바꿔달라고 했습니다.

입력 문자열에서 정보를 추출해서 아래와 같이 json으로 바꿔줘. 키는 이름, 값은 색깔을 영어로. 코드를 작성하지 말고 직접 바꿔서 결과를 보여줘.

## 예시
“수연이는 파란색을 좋아하고, 철호는 빨간색을 좋아한다” => {“수연”: “blue”,”철호”: “red”}

## 입력
“영희는 노란색을 좋아하고, 민수는 초록색을 좋아한다” =>

그랬더니 아래와 같이 대답했습니다.

입력 문자열을 파싱하여 아래와 같이 JSON 형식으로 변환합니다.

입력 문자열:
"영희는 노란색을 좋아하고, 민수는 초록색을 좋아한다"

JSON 결과:
{
"영희": "yellow",
"민수": "green"
}

원하는 결과가 마지막에 나오긴 했지만 앞에 불필요한 설명이 들어가 있습니다. 그래서 다시 프롬프트를 수정했습니다.

당신은 json 변환기입니다. 보기처럼 문장을 입력 받으면 json 형식으로 출력하세요. 설명 없이 json만 출력하세요.

## 보기
“수연이는 파란색을 좋아하고, 철호는 빨간색을 좋아한다” => {“수연”: “blue”, “철호”: “red”}

## 문장
“민수는 노란색을 좋아하고, 은정이는 분홍색을 좋아한다”

이제 원하는 답이 나옵니다.

{"민수": "yellow", "은정이": "pink"}

입력 문장을 바꿔가며 실행해봐도 결과가 잘 나옵니다. 몇 번 실행해봤을 때 결과가 잘 나왔으나 항상 잘 나온다는 보장은 없습니다. 응답이 확률적으로 나오므로 언제 부연 설명이 들어가게 될지 알 수 없죠. 앞에 설명이 들어가면 {} 기호를 이용해 json 부분만 추출할 수도 있으나 결과가 항상 json 형식으로 나온다면 더 좋겠죠?

사실 json 출력은 ChatGPT에서 대화할 때 보다는 API를 사용할 때 더 유용합니다. LLM의 응답을 바로 파이썬에서 딕셔너리로 바꿔서 사용할 수 있기 때문이죠. 위의 프롬프트를 가지고 API를 사용할 수도 있지만, 함수 호출 기능을 이용하면 더 확실하게 부연 설명 없는 json 결과물을 얻을 수 있습니다.

API에서 function_call을 이용해 json 출력하기

OpenAI API 호출시 functions 인자를 이용해 함수 사용법을 알려줄 수 있습니다. LLM은 필요하다고 판단될 경우 응답의 function_call을 통해 호출할 함수 이름과 호출할 때 사용할 인자(arguments)를 알려줍니다. 이 때 인자를 json object 형식(파이썬 딕셔너리에 해당)으로 받아서 출력하면 됩니다. 이를 위해 사용할 functions 인자는 다음과 같습니다.

functions = [
    {
        "name": "name_and_color",
        "description": "사람 이름과 좋아하는 색깔의 영어 이름을 매칭시킴",
        "parameters": {
            "type":"object",
            "properties": {
                "match": {
                    "type": "object",
                    "description": "key=name, value=color"
                }
            },
            "required": ["match"]
        }
    }
]

위에서 함수 이름은 중요하지 않습니다. 실제로는 존재하지 않는 함수입니다. 중요한 것은 함수 설명과 함수 인자 설명입니다. 함수 설명에는 우리의 목적에 대한 설명을 넣어서 LLM에서 호출해야 하는 함수로 판단할 수 있도록 합니다. 인자는 object 형식으로 만들고 인자 설명에 key는 사람 이름, value는 색깔을 넣도록 했습니다.

messages = [{"role": "system", "content": "You are a helpful assistant."},
           {"role": "user", "content": "수연이는 파란색을 좋아하고 철호는 빨간색을 좋아한다."}]
response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo",
    messages=messages,
    functions=functions
)
print(response["choices"][0]['message']["function_call"]["arguments"])

### 결과
{
  "수연": "blue",
  "철호": "red"
}

원하는대로 json 문자열이 결과로 나왔습니다. 문장을 바꿔도 잘 나옵니다. 이제 부연 설명이 나올지 안 나올지 걱정할 필요가 없죠. json.loads 함수를 이용해 파이썬 딕셔너리로 바꿔서 사용하면 이후 과정을 편리하게 진행할 수 있습니다.

파이썬에서 json 파일 다루기

JSON 형식

JSON (JavaScript Object Notation) 파일은 인터넷에서 서버와 클라이언트 사이 통신에 많이 사용하는 텍스트 표기 형식입니다. REST API에서도 많이 사용하고, 설정 파일로 사용하기도 하죠. 다음과 같이 사람과 컴퓨터가 읽을 수 있는 텍스트 형식입니다.

{
    "key": "value",
    "list": [
        {"str": "text string"},
        {
            "number": {"int": 1234, "float": 12.34}
        },
        {"True": true},
        {"False": false},
        {"None": null}
    ]
}

JSON에는 { }로 표기하는 객체(object), [ ]로 표기하는 배열(array), 큰 따옴표로 감싸는 문자열(string), 숫자(number), 불리언(boolean), null 자료형이 있습니다. 객체는 키와 값으로 이루어지는데, 키는 문자열이고, 값은 위의 6가지 자료형 중 하나가 될 수 있습니다. 객체 안에 객체를 넣는 것도 가능합니다. 배열에도 여러 자료형들이 함께 들어갈 수 있습니다.

파이썬에서 JSON 읽기

파이썬에서 JSON 파일을 읽으려면 다음과 같이 json 모듈load 함수를 사용하면 됩니다.

import json
with open('data.json') as fj:
    data = json.load(fj)

문자열(str 또는 bytes, bytearray)에 json 내용이 들어있을 경우 loads를 이용합니다.

import json
js = '{"key": "value"}'
data = json.loads(js)

이렇게 읽어들인 data는 파이썬에서 dict 자료형이 됩니다. JSON 자료형은 읽었을 때 아래와 같이 파이썬 자료형으로 변환됩니다.

JSONPython
objectdict
arraylist
stringstr
number (integer)int
number (real)float
trueTrue
falseFalse
nullNone
JSON to Python

파이썬에서 JSON 쓰기

파이썬에서 JSON 파일을 쓰려면 dump, 문자열에 쓰려면 dumps 함수를 이용합니다.

import json
data = ["list", {'key': 'value'}]

with open('data.json', 'w') as fj:
    json.dump(data, fj)

js = json.dumps(data, indent=4)

dumps에서 indent 옵션을 넣으면 출력시 보기 좋게 만들어줍니다. 파이썬 자료형은 아래와 같이 JSON 자료형으로 변환됩니다.

PythonJSON
dictobject
list, tuplearray
strstring
int, float, (int, float 열거형)number
Truetrue
Falsefalse
Nonenull
Python to JSON

JSON 파일에 한글 쓰기

파이썬 문자열에 한글이 포함되어 있을 경우 위의 dump, dumps를 이용해 JSON 파일이나 문자열을 쓰면 한글이 바로 표시되지 않습니다. 그래도 load, loads로 읽으면 제대로 읽어지기는 합니다. 파일이나 문자열로 썼을 때 한글이 바로 표시되게 만들고 싶으면 dump, dumpsensure_ascii=False 옵션을 주면 됩니다.

import json
data = ["list", {'key': '한글'}]

with open('data.json', 'w') as fj:
    json.dump(data, fj, ensure_ascii=False)

js = json.dumps(data, indent=4, ensure_ascii=False)

제어 문자가 들어간 문자열

\n, \t, \r, \0과 같은 제어 문자가 들어간 문자열을 json.loads로 읽어들이려고 하면 JSONDecodeError가 발생합니다. 제어 문자를 포함하고 싶으면 strict=False 옵션을 주면 됩니다.

js = """
["list",
{"key": "        
한글        
"}]
"""

data = json.loads(js, strict=False)