OpenAI Assistant를 이용한 Retrieval

앞에서 LangChain을 이용한 Retrieval Augmented Generation을 살펴봤습니다. OpenAI의 새로운 Assistant API를 이용하면 좀 더 쉽게 Retrieval을 구현할 수 있습니다.

구현 순서는 다음과 같습니다.

  1. Retrieval을 수행할 파일을 OpenAI 서버에 올리고 파일 ID를 저장합니다.
  2. Assistant를 생성합니다.
  3. Thread를 생성합니다.
  4. 파일을 포함하는 Message를 생성합니다. Message에는 파일의 내용과 관련된 질문을 넣습니다.
  5. Run 객체를 만들어 실행하면 파일에서 찾은 내용을 바탕으로 한 응답을 얻을 수 있습니다.

이 때 파일은 Message에 포함시킬 수도 있지만, Assistant에 포함시킬 수도 있습니다. Message는 Thread 생성 후에 만들 수도 있고, Thread를 생성하면서 동시에 만들 수도 있습니다. 최종 응답에는 파일의 어떤 내용을 참조했는지에 대한 정보도 포함되어 있습니다. 코드를 보겠습니다.

OpenAI 라이브러리

import openai
openai.__version__
# 결과: '1.2.3'

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

파일 올리기

import requests

url="https://www.gutenberg.org/cache/epub/4/pg4.txt"
r = requests.get(url)

with open("GettysburgAddress.txt",'w') as fo:
  fo.write(r.text)

my_file = client.files.create(
    file = open("GettysburgAddress.txt",'rb'),
    purpose='assistants'
)
my_file

## 결과
FileObject(id='file-UdQTPO55auQ5CEEW562Ji9er', bytes=22030, created_at=1699803306, filename='GettysburgAddress.txt', object='file', purpose='assistants', status='processed', status_details=None)

구텐베르크 프로젝트 페이지에서 링컨의 게티스버그 연설을 받아서 파일로 저장하고 OpenAI 서버에 올렸습니다. 뒤에서 이 연설 내용에 관한 질문을 던지려고 합니다. 이 파일에는 게티스버그 연설문 앞 뒤로 프로젝트 소개, 안내와 같은 내용들도 들어 있습니다.

위 결과를 보면 파일 객체에 id가 있습니다. 파일 뿐 아니라 Assistant, Thread, Message, Run 객체 모두 id를 가지고 있씁니다. 앞으로 이 id를 이용해 각각의 객체들을 참조하게 됩니다.

Assistant, Thread, Message 생성

Retrieval을 위해서는 Assistant 생성시 tools에 retrieval 도구를 추가해줍니다.

assistant = client.beta.assistants.create(
  name="연설전문가",
  description="당신은 연설 전문가입니다.",
  model="gpt-4-1106-preview",
  tools=[{"type": "retrieval"}],
  #file_ids=[my_file.id]
)
assistant

## 결과
Assistant(id='asst_UojnLxCEe5wfEL0IZrHW43vu', created_at=1699803311, description='당신은 연설 전문가입니다.', file_ids=[], instructions=None, metadata={}, model='gpt-4-1106-preview', name='연설전문가', object='assistant', tools=[ToolRetrieval(type='retrieval')])

Assistant에 파일을 추가해주고 싶을 경우 위의 주석 처리한 file_ids 줄처럼 추가하면 됩니다.

이번엔 Thread를 생성하면서 Message도 추가해줬습니다. Message에 file_ids를 이용해 앞에서 올린 파일을 attach 했습니다.

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

## 결과
Thread(id='thread_U4msr0xRagC356A7ZmYJB6sJ', created_at=1699803314, metadata={}, object='thread')

Run

Assistant와 Thread를 만든 후 Run을 실행해 응답을 받습니다. API를 이용해 Run 객체를 생성하면 LLM 모델이 응답을 생성할 때까지 기다리지 않고 바로 Run 객체를 반환하기 때문에 status를 확인해서 응답이 나올 때가지 기다립니다.

import time

def run_and_wait(client, 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
    )
    print(run_check.status)
    if run_check.status in ['queued','in_progress']:
      time.sleep(2)
    else:
      break
  return run

run_and_wait(client, assistant, thread)

## 결과
Run(id='run_FGObi8liSR7RhfodFQBFYFQ8', assistant_id='asst_UojnLxCEe5wfEL0IZrHW43vu', cancelled_at=None, completed_at=None, created_at=1699803317, expires_at=1699803917, 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_U4msr0xRagC356A7ZmYJB6sJ', tools=[ToolAssistantToolsRetrieval(type='retrieval')])

결과 보기

응답은 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: 에이브러햄 링컨의 게티스버그 연설은 미국의 남북전쟁 중에 "전사들의 희생을 기리며, 자유를 위한 그들의 헌신이 헛되지 않았음을 다짐하고, '민주주의 정부가 이 땅에서 사라지지 않을 것'이라는 메시지를 전달한 것입니다【9†source】.
user: 링컨의 게티스버스 연설 내용을 한 문장으로 요약해주세요.

위의 결과를 보면 【9†source】 이런 참조 기호가 있습니다. 이건 Retrieval 도구에서 파일을 참조했다는 표시입니다. 파일의 어떤 내용을 참조했는지는 응답의 annotations에서 찾을 수 있습니다.

File Citation

응답에서 Annotations 부분에 어떤 내용이 있나 봅시다.

thread_messages.data[0].content[0].text.annotations

## 결과

[TextAnnotationFileCitation(end_index=130, file_citation=TextAnnotationFileCitationFileCitation(file_id='file-UdQTPO55auQ5CEEW562Ji9er', quote='Four score and seven years ago, our fathers brought forth\r\nupon this continent a new nation:  conceived in liberty, and\r\ndedicated to the proposition that all men are created equal.\r\n\r\nNow we are engaged in a great civil war. . .testing whether\r\nthat nation, or any nation so conceived and so dedicated. . .\r\ncan long endure.  We are met on a great battlefield of that war.\r\n\r\nWe have come to dedicate a portion of that field as a final resting place\r\nfor those who here gave their lives that this nation might live.\r\nIt is altogether fitting and proper that we should do this.\r\n\r\nBut, in a larger sense, we cannot dedicate. . .we cannot consecrate. . .\r\nwe cannot hallow this ground.  The brave men, living and dead,\r\nwho struggled here have consecrated it, far above our poor power\r\nto add or detract.  The world will little note, nor long remember,\r\nwhat we say here, but it can never forget what they did here.\r\n\r\nIt is for us the living, rather, to be dedicated here to the unfinished\r\nwork which they who fought here have thus far so nobly advanced.\r\nIt is rather for us to be here dedicated to the great task remaining\r\nbefore us. . .that from these honored dead we take increased devotion\r\nto that cause for which they gave the last full measure of devotion. . .\r\nthat we here highly resolve that these dead shall not have died in vain. . .\r\nthat this nation, under God, shall have a new birth of freedom. . .\r\nand that government of the people. . .by the people. . .for the people. . .\r\nshall not perish from this earth'), start_index=120, text='【9†source】', type='file_citation')]

위 내용을 보면 리스트 안에 TextAnnotationFileCitation 객체가 있고, text 항목에 인용 기호, quote에 인용한 내용이 있습니다. 인용 내용을 출력해봅시다.

print(thread_messages.data[0].content[0].text.annotations[0].file_citation.quote)

## 결과

Four score and seven years ago, our fathers brought forth
upon this continent a new nation:  conceived in liberty, and
dedicated to the proposition that all men are created equal.

Now we are engaged in a great civil war. . .testing whether
that nation, or any nation so conceived and so dedicated. . .
can long endure.  We are met on a great battlefield of that war.

We have come to dedicate a portion of that field as a final resting place
for those who here gave their lives that this nation might live.
It is altogether fitting and proper that we should do this.

But, in a larger sense, we cannot dedicate. . .we cannot consecrate. . .
we cannot hallow this ground.  The brave men, living and dead,
who struggled here have consecrated it, far above our poor power
to add or detract.  The world will little note, nor long remember,
what we say here, but it can never forget what they did here.

It is for us the living, rather, to be dedicated here to the unfinished
work which they who fought here have thus far so nobly advanced.
It is rather for us to be here dedicated to the great task remaining
before us. . .that from these honored dead we take increased devotion
to that cause for which they gave the last full measure of devotion. . .
that we here highly resolve that these dead shall not have died in vain. . .
that this nation, under God, shall have a new birth of freedom. . .
and that government of the people. . .by the people. . .for the people. . .
shall not perish from this earth

전체 문서 중에서 링컨 대통령의 연설 부분만 잘 추출해서 인용했습니다. 문서 생성시 LLM의 응답 메시지에서 위 text에 지정된 인용 기호 부분을 각주/미주 기호로 바꾸고 각주/미주 부분에 quote의 인용문을 넣으면 각주/미주를 만들 수 있습니다.

OpenAI RAG vs LangChain RAG

LangChain으로 RAG를 수행했을 때와 다른 부분을 생각해봅시다.

  • 문서를 벡터 데이터베이스에 넣고 검색하는 부분이 없습니다. OpenAI에서 알아서 짧은 문서는 텍스트 검색을 이용해 인용해주고, 긴 문서는 임베딩 벡터를 만들어 벡터 데이터베이스에 넣었다가 벡터 검색을 이용해 찾아서 인용해줍니다. 프로그래밍이 편해진 대신 임베딩 방법, 벡터 데이터베이스 종류와 검색 방법은 선택할 수 없습니다.
  • 길이 제한에 맞춰 이전 대화와 인용문을 프롬프트에 넣는 부분이 사라졌습니다. OpenAI에서 알아서 프롬프트 길이 제한에 맞춰줍니다. 역시 프로그래밍이 편해진 대신 이전 대화 내용과 인용문을 어떻게, 얼마나 줄여서 프롬프트에 넣을 것인지는 선택할 수 없습니다.

댓글 남기기