OpenAI Assistants API로 대화 내용 저장하기

OpenAI에서 11월 6일 새로운 API들을 발표했는데, 그 중 Assistants API가 있었습니다.

Assistants API의 Thread를 이용하면 LangChain에서 Memory를 이용해 대화 내용을 저장했던 것처럼 대화를 저장할 수 있습니다.

Assistants API의 Tools를 이용하면 LangChain Agent처럼 도구를 사용할 수 있습니다. 이전에도 함수 호출 기능이 있었지만, 이번 업데이트를 통해 Code Interpreter와 Retrieval 도구도 사용할 수 있게 되었습니다.

이 글에서는 Threads를 이용한 대화 저장 방법을 살펴보겠습니다.

Assistant, Thread, Message, and Run

먼저 API에서 사용하는 기본 용어들을 정리하겠습니다.

  • Assistant: 도구들을 사용할 수 있는 거대 언어 모델(GPT 모델들)입니다.
  • Thread: Message들을 저장하는 대화입니다.
  • Message: 대화에서 주고 받는 메시지들로, 텍스트 외에도 이미지나 파일을 포함할 수 있습니다.
  • Run: Assistant가 Thread에 저장된 Message들을 읽고 LLM의 응답 Message를 추가하는 과정입니다.
  • Run Step: Run을 실행할 때 단순히 LLM의 응답을 추가하는 경우도 있지만, 외부 도구들(Tools)을 사용한 후 결과를 이용해 응답하는 경우도 있습니다. 어떤 세부 단계를 거쳐 응답했는지 확인하고 싶을 때 Run Step을 이용합니다.

Run 상태

LLM에 질문을 던지면 응답을 받는데 시간이 걸립니다. 그래서 Run을 실행한다고 바로 Thread에 응답이 추가되지 않습니다. Run을 실행하면 queued → in_progress → completed 과정을 거쳐 응답이 생성됩니다. 만약 사용자가 제공한 Tool을 사용할 경우에는 사용자가 실행 결과를 다시 LLM에 제공해야 합니다. 이 때 결과를 기다리는 동안 Run은 requires_action 상태가 됩니다. 정리해보면,

  • 기본 응답: queued → in_progress → completed
  • Function calling 사용시: queued → in_progress → requires_action → (함수 호출 결과 입력 후 다시 Run) → queued → in_progress → completed
  • 응답 시간이 오래 걸리면: queued → in_progress → expired
  • 응답 실패시: queued → in_progress → failed
  • 사용자가 취소할 경우: queued → in_progress → (사용자가 취소) → cancelling → cancelled

따라서, Run 상태가 queued나 in_progress라면 응답을 얻기 위해 기다려야 합니다. 코드로 확인해보겠습니다.

Assistant 만들기

from openai import OpenAI
client = OpenAI(api_key="sk-")

assistant = client.beta.assistants.create(
  name="똑똑한 비서",
  description="당신은 똑똑한 비서입니다.",
  model="gpt-4-1106-preview"
)
assistant

## 출력
Assistant(id='asst_...', created_at=1699421567, description='당신은 똑똑한 비서입니다.', file_ids=[], instructions=None, metadata={}, model='gpt-4-1106-preview', name='똑똑한 비서', object='assistant', tools=[])

모델 이름이 “gpt-4-1106-preview”입니다. 아마 얼마 안 지나면 “gpt-4-turbo”로 바뀔 것입니다. API 호출 부분을 보면 beta가 들어가 있는데 이것도 얼마 안 있어 바뀌겠죠.

이렇게 생성한 assistant는 OpenAI의 서버에 저장되고, 고유한 id를 가지고 있습니다. 우리는 이 assistant를 id를 이용해 참조하게 됩니다.

Thread 만들기

thread = client.beta.threads.create(
  messages=[
    {
      "role": "user",
      "content": "링컨의 게티스버스 연설 내용을 한 문장으로 요약해주세요."
    }
  ]
)
thread

## 출력
Thread(id='thread_...', created_at=1699421567, metadata={}, object='thread')

Thread를 만들면서 Message도 넣었는데, Message는 나중에 따로 추가할 수도 있습니다. Assistant와 Thread를 만들었으니 이제 Run을 실행할 수 있습니다.

Run, 상태 확인

run = client.beta.threads.runs.create(
  thread_id=thread.id,
  assistant_id=assistant.id,
  #model="gpt-4-1106-preview",
  #instructions="additional instructions",
  #tools=[{"type": "code_interpreter"}, {"type": "retrieval"}]
)
run

## 출력
Run(id='run_...', assistant_id='asst_...', cancelled_at=None, completed_at=None, created_at=1699421567, expires_at=1699422167, failed_at=None, file_ids=[], instructions=None, last_error=None, metadata={}, model='gpt-4-1106-preview', object='thread.run', required_action=None, started_at=None, status='queued', thread_id='thread_...', tools=[])

Run을 생성하면서 Thread의 id와 Assistant의 id를 입력합니다. 필요할 경우 Assistant의 model, instruction, tools를 덮어쓸 수도 있습니다. 이렇게 Run을 생성하고 바로 내용을 확인해보면 status='queued'라고 나오는 것을 볼 수 있습니다. 바로 Thread를 확인해보면 응답이 아직 추가되어 있지 않으므로 Run 상태가 바뀌는 것을 확인한 후에 Thread의 Message를 확인해야 합니다.

import time

while True:
  run_check = client.beta.threads.runs.retrieve(
    thread_id=thread.id,
    run_id=run.id
  )
  if run_check.status in ['queued','in_progress']:
    time.sleep(2)
  else:
    break

Run 상태가 바뀌면 위 루프가 끝납니다. 이후에 Thread의 Message를 확인합니다.

Thread에 추가된 응답 Message 확인

thread_messages = client.beta.threads.messages.list(thread.id)
for msg in thread_messages.data:
  print(f"{msg.role}: {msg.content[0].text.value}")

## 출력
assistant: 링컨의 게티스버스 연설은 민주주의의 이상을 재확인하고, .....
user: 링컨의 게티스버스 연설 내용을 한 문장으로 요약해주세요.

이렇게 Assistant의 응답이 추가된 것을 확인할 수 있습니다. 이전의 대화 내용은 빼고 마지막 응답만 확인하고 싶다면 위 코드에서 list 함수 호출시 limit=1 옵션을 주면 됩니다.

새로운 메시지 추가

대화를 계속해 나가려면 Thread에 새로운 사용자 Message를 추가해주고 Run을 실행해서 Assistant Message를 확인하는 과정을 반복하면 됩니다. Thread에 새로운 Message를 추가하려면 Message를 생성하며 Thread id를 입력하면 됩니다.

new_message = client.beta.threads.messages.create(
  thread_id = thread.id,
  role="user",
  content="링컨은 어떤 사람이지?",
)
new_message

## 출력
ThreadMessage(id='msg_...', assistant_id=None, content=[MessageContentText(text=Text(annotations=[], value='링컨은 어떤 사람이지?'), type='text')], created_at=1699421965, file_ids=[], metadata={}, object='thread.message', role='user', run_id=None, thread_id='thread_...')

Run 반복

사용자 Message를 추가했으니 다시 Run을 실행해서 Assistant의 응답을 Thread에 추가합니다.

run = client.beta.threads.runs.create(
  thread_id=thread.id,
  assistant_id=assistant.id
)
while True:
  run_check = client.beta.threads.runs.retrieve(
    thread_id=thread.id,
    run_id=run.id
  )
  run_check
  if run_check.status not in ['queued','in_progress']:
    break
  else:
    time.sleep(2)

thread_messages = client.beta.threads.messages.list(thread.id)
for msg in thread_messages.data:
  print(f"{msg.role}: {msg.content[0].text.value}")

## 출력
assistant: 에이브러햄 링컨(Abraham Lincoln, 1809-1865)은 ...
user: 링컨은 어떤 사람이지?
assistant: 링컨의 게티스버스 연설은 ...
user: 링컨의 게티스버스 연설 내용을 한 문장으로 요약해주세요.

참고로, 이렇게 생성한 Assistant는 OpenAI Platform에서도 확인할 수 있습니다.

Assistant와 Thread 삭제

Assistant와 Thread는 OpenAI 서버에 저장되어 있는데, 더이상 필요가 없으면 삭제해줍니다.

# delete thread
response = client.beta.threads.delete(thread.id)
print(response)

# delete assistant
response = client.beta.assistants.delete(assistant.id)
print(response)

이 외에도 다양한 함수들이 있는데 API Reference를 확인하면 되겠습니다.

LangChain과 비교

LangChain에서는 Memory를 이용해 대화 내용을 저장했는데, 토큰 제한을 고려하여 최근 대화 내용만 남기거나 이전 대화 내용을 요약하는 등의 방식으로 분량을 줄였습니다. OpenAI API에서는 Thread를 통해 OpenAI 서버에 대화 내용을 저장해주고 토큰 제한에 맞추는 과정도 알아서 진행해주기 때문에 대화 진행이 간편해졌습니다.

대신 대화 내용 저장시 프로그래머가 컨트롤할 수 있는 부분은 별로 없습니다. 현재는 대화 내용을 최대한 보존하는 방식(토큰 소모가 많은 방식)인데, 앞으로 다른 방식을 사용할 수 있도록 옵션을 추가할 계획이라고 합니다.

댓글 남기기