
Assistants API란?
간단하게 말하자면 AI 도우미를 쉽게 만들고 사용할 수 있도록 도와주는 API 정도로 볼 수 있을 것 같다. ChatGPT API를 사용할 땐 설정이나 대화 내용이 유지되지 않기 때문에 매번 지시문도 같이 요청을 보내야 하지만 어시스턴트는 설정과 대화 내용이 유지된다.
지금 만들고 있는 프로젝트의 경우, '제목 + 요약된 내용' 이라는 형식을 가지고 응답을 반환해주어야 해기 때문에 지시문이 유지되는 어시스턴트 API를 사용해보았다.
흐름
Assistants API를 사용하는 방법은 대충 아래와 같다.
우선 어시스턴트를 생성하고, 그 안에 스레드도 생성해준다. 그리고 스레드 안에 메시지를 생성한다. 메시지 생성은 말하자면 내가 AI에 던질 질문, 요청을 만드는 것이다. 그 다음, Create Run을 통해 실행을 하게 된다. Retrieve Run을 통해 실행 상태나 결과를 조회할 수 있다. 그리고 List Messages를 통해 메시지 목록을 받아와서 AI의 응답 메시지를 확인할 수 있다.
구현
https://platform.openai.com/docs/api-reference/assistants/createAssistant
공식 문서를 참고해 API를 사용하면 된다.
https://platform.openai.com/api-keys
위 주소로 가서 API Key를 발급받자.
발급받은 API Key는 application.properties 등에 잘 적어놓으면 된다.
openai.api.key={API_Key}
나는 요청이 들어올 때마다 매번 어시스턴트를 생성하지 않고, 하나의 어시스턴트를 생성해서 그 안에 여러 스레드를 생성하는 방식으로 구현했다. 그래서 코드 내에서 어시스턴트를 생성하진 않았고
https://platform.openai.com/playground/assistants
위 링크의 플레이그라운드에서 바로 어시스턴트를 생성했다.

Instructions에 지시문을 입력해줬다.

그리고 나는 Response format을 json_object로 바꿔 응답을 json으로 받았다. 지시문에는 응답을 output이라는 이름의 json으로 반환해달라고 적었다. 참고로 나는 gpt-4o 모델 사용했는데 돈을 아끼고 싶다면 다른 모델 선택하기. 난 걍 제일 좋은 모델 써보고 싶었음.
어시스턴트를 생성한 후 application.properties에 어시스턴트 id를 저장했다.
openai.assistant.id={Assistant_id}
나는 GPTClient 클래스를 만들어서 어시스턴트 API는 그 안에 만들어 놓았다. 일단 Api Key와 Assistant Id 값을 application.properties에서 가져왔다.

public String createThread() {
String threadId = null;
try{
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
String url = "https://api.openai.com/v1/threads";
String json = "{}";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.header("OpenAI-Beta", "assistants=v2")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("thread 생성 응답 코드: " + response.statusCode());
System.out.println("thread 생성 응답 본문: " + response.body());
String responseBody = response.body();
ObjectMapper objectMapper = new ObjectMapper();
JsonNode rootNode = objectMapper.readTree(responseBody);
// thread id 추출
threadId = rootNode.path("id").asText();
System.out.println("Thread ID: " + threadId);
} catch (Exception e) {
e.printStackTrace();
return null;
}
return threadId;
}
처음에 url을 https://api.openai.com/v1/threads/라고 잘못 적었었는데 계속 403이 떠서 한참 고생했었다. 엔드포인트를 제대로 적었는지 잘 확인하는 게 좋을 듯.
공식문서가 시키는 대로 헤더 붙이고, 데이터 넣어서 요청 보내면 된다.
그리고 나는 응답에서 Thread ID를 추출해서 이걸 리턴하도록 만들었다.
public int createMessage(String input, String threadId) {
int result = 0;
try{
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
String url = "https://api.openai.com/v1/threads/" + threadId + "/messages";
String json = "{"
+ "\"role\": \"user\","
+ "\"content\": \"" + input + "\""
+ "}";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.header("OpenAI-Beta", "assistants=v2")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
result = response.statusCode();
System.out.println("message 생성 응답 코드: " + response.statusCode());
System.out.println("message 생성 본문: " + response.body());
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
방법은 스레드를 생성했던 것과 같다.
나는 사용자가 보낸 글을 input으로 받아서 어시스턴트에 요청을 보낼 때 content로 사용했다.
String json = "{"
+ "\"role\": \"user\","
+ "\"content\": \"" + input + "\""
+ "}";
여기서 input 부분에 AI에게 보낼 요청을 적으면 된다.
public String createRun(String threadId){
String runId, runStatus = "";
try {
HttpClient client = HttpClient.newHttpClient();
String url = "https://api.openai.com/v1/threads/" + threadId + "/runs";
String json = "{"
+ "\"assistant_id\": \"" + assistantId + "\""
+ "}";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.header("OpenAI-Beta", "assistants=v2")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> runResponse = client.send(request, HttpResponse.BodyHandlers.ofString());
String responseBody = runResponse.body();
ObjectMapper objectMapper = new ObjectMapper();
JsonNode rootNode = objectMapper.readTree(responseBody);
// "id" 필드 값 추출
runId = rootNode.path("id").asText();
System.out.println("Run ID: " + runId);
//상태 체크
int checkInterval = 500;
int maxTimeout = 30000;
int elapsedTime = 0;
runStatus = retrieveRun(threadId, runId);
while (!runStatus.equals("completed") && elapsedTime < maxTimeout) {
Thread.sleep(checkInterval);
elapsedTime += checkInterval;
runStatus = retrieveRun(threadId, runId);
System.out.println("Run Status: " + runStatus);
}
if (!runStatus.equals("completed")) {
System.out.println("Create run Time Out");
return null;
}
System.out.println("run 응답 코드: " + runResponse.statusCode());
System.out.println("run 응답 본문: " + runResponse.body());
} catch (Exception e) {
e.printStackTrace();
return null;
}
return runId;
}
run의 상태가 completed 일 때 메시지를 받아와야 제대로된 응답 결과를 얻을 수 있기 때문에 중간에 retrieveRun을 통해 run의 상태를 확인했다. completed이 아닌 상태에서 메시지를 받아와버리면 공백이나 내가 요청한 텍스트를 그대로 반환하는 등의 문제가 있을 수 있다. 그리고 결과로 Run ID 값을 리턴하도록 했다.
public String retrieveRun(String threadId, String runId) {
String runStatus = "";
try {
HttpClient client = HttpClient.newHttpClient();
String url = "https://api.openai.com/v1/threads/" + threadId + "/runs/" + runId;
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + apiKey)
.header("OpenAI-Beta", "assistants=v2")
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("retrieve 응답 코드: " + response.statusCode());
System.out.println("retrieve 응답 본문: " + response.body());
ObjectMapper objectMapper = new ObjectMapper();
JsonNode statusRootNode = objectMapper.readTree(response.body());
runStatus = statusRootNode.path("status").asText();
} catch (Exception e) {
e.printStackTrace();
}
return runStatus;
}
retrieve run. 말 그대로 run을 검색한다. run id를 통해 run을 찾고, 그 run의 상태를 확인할 수 있다. createRun에서 run의 상태가 completed인지 확인하기 위해 사용했다.
처음엔 retrieve run이 뭔지 제대로 이해하지 못해서 create run 이후 무조건 5초동안 쉬도록 했는데 이제서야 retrieve run을 활용해서 run의 상태에 따라 다음으로 진행하도록 고칠 수 있었다...
public String[] listMessages(String threadId) {
String title, content;
try {
HttpClient client = HttpClient.newHttpClient();
String url = "https://api.openai.com/v1/threads/" + threadId + "/messages";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.header("OpenAI-Beta", "assistants=v2")
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("list 응답 코드: " + response.statusCode());
System.out.println("list 응답 본문: " + response.body());
String responseBody = response.body();
ObjectMapper objectMapper = new ObjectMapper();
JsonNode rootNode = objectMapper.readTree(responseBody);
// "data" 배열의 첫 번째 객체 가져오기
JsonNode firstMessageNode = rootNode.path("data").get(0);
// "content" 배열의 첫 번째 객체 가져오기
JsonNode contentNode = firstMessageNode.path("content").get(0);
// "value" 필드에서 JSON 형식의 문자열 추출
String contentValue = contentNode.path("text").path("value").asText();
// JSON 형식의 문자열을 다시 파싱하여 title과 content 추출
JsonNode parsedContent = objectMapper.readTree(contentValue);
title = parsedContent.path("output").path("제목").asText();
content = parsedContent.path("output").path("일기 내용").asText();
// 결과 출력
System.out.println("Title: " + title);
System.out.println("Content: " + content);
} catch (Exception e) {
e.printStackTrace();
return null;
}
return new String[]{title, content};
}
AI가 보낸 응답을 받아올 수 있다. 내가 보낸 요청과 AI가 보낸 응답을 모두 확인할 수 있는데 거기서 AI가 보낸 응답 중 내가 사용할 제목과 일기 내용을 추출해 리턴하도록 했다.
public int deleteThread(String threadId) {
int result = 0;
try{
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
String url = "https://api.openai.com/v1/threads/" + threadId;
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.header("OpenAI-Beta", "assistants=v2")
.DELETE()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
result = response.statusCode();
System.out.println("thread 삭제 응답 코드: " + response.statusCode());
System.out.println("thread 삭제 응답 본문: " + response.body());
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
필요한 작업을 마쳤으면 스레드를 삭제했다. 챗봇 같은 걸 개발하는 중이라 같은 스레드를 계속 사용해야 한다면 굳이 삭제할 필요 없겠지만 나는 이전 대화 기록은 필요 없어서 스레드를 삭제해주었다.
//thread 생성
threadId = gptClient.createThread();
if (threadId == null)
return GPTResponseDto.GPTFail();
//messages 생성
if (gptClient.createMessage(dto.getInput(), threadId) != 200)
return GPTResponseDto.GPTFail();
//run 생성
runId = gptClient.createRun(threadId);
if (runId == null)
return GPTResponseDto.GPTFail();
//메시지 가져오기
String[] result = gptClient.listMessages(threadId);
if (result == null)
return GPTResponseDto.GPTFail();
else
{
title = result[0];
content = result[1];
//DB 저장
try {
Diary diary = new Diary();
diary.setTitle(title);
diary.setContent(content);
diary.setDate(dto.getDate());
diary.setUser(user);
diary.setCreatedAt(LocalDateTime.now());
diary.setUpdatedAt(LocalDateTime.now());
diaryRepository.save(diary);
user.setCoin(coin - 1);
userRepository.save(user);
} catch (Exception e) {
e.printStackTrace();
return GPTResponseDto.databaseError();
}
}
//thread 삭제
if (gptClient.deleteThread(threadId) != 200)
return GPTResponseDto.GPTFail();
최종적으로 서비스에서 이렇게 사용했다.
마무리
처음에 엄청 헤맸는데 아래 유튜브가 한 줄기 빛이 되어 주었다. 이 유튜브 보면 이해 바로 됨.
https://www.youtube.com/watch?v=kKhWkG5Di5s
그리고 나는 백엔드 개발이 처음이라 내 코드 쓰레기일 수 있음. 주의.
저 왕초보니까 만약 지나가다 이 글을 보신다면 조언해주세요.