본문 바로가기
dev/spring

Spring AI를 사용한 AI 어시스턴트 구현 - Part 1 (with OpenAI)

by igooo 2024. 10. 1.
728x90

개요

Spring AI를 활용하여 사용자가 자연어 사용을 통한 애플리케이션과 상호작용할 수 있도록 AI 어시스턴스 기능을 구현한다. Spring AI 예제는 Spring Petclinic을 사용하여 구현했지만, 아래 예제에서는 심플한 TODO 애플리케이션을 구현하여 AI 어시스턴트 기능 구현에 대하여 설명한다.

 

 

사용된 기술

Spring Petclinic 코드를(https://github.com/spring-projects/spring-petclinic/tree/spring-ai) 기반으로 예제를 작성할 예정이라 AI 기능을 제외한 기능에 대해서는 Spring Petclinic AI 코드를 참고한다.

Spring AI는 다양한 LLM 모델을 지원하지만 예제에서는 OpenAI를 사용하여 구현한다.

(OpenAI를 사용한 이유는 테스트한다고 결제했던 비용이 남아서...)

  • Frontend UI : Thymeleaf
  • Database : Spring Data JPA
  • Spring AI : OpenAI
OpenAI 사용을 위한 키 발급은 https://platform.openai.com/docs/overview에서 발급 가능하다.

 

Getting started

Project 설정

Spring Starter Project를 사용하여 프로젝트를 생성한다.

연동할 AI 모델에 따라서 의존성을 추가한다. (ex org.springframework.ai:spring-ai-openai-spring-boot-starter)

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.3.4'
	id 'io.spring.dependency-management' version '1.1.6'
}

group = 'org.igooo'
version = '0.0.1-SNAPSHOT'

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(21)
	}
}

repositories {
	mavenCentral()
	maven { url 'https://repo.spring.io/milestone' }
}

ext {
	set('springAiVersion', "1.0.0-M2")
	set('springModulithVersion', "1.2.4")
	set('webjarsBootstrapVersion', "5.3.3")
	set('webjarsFontAwesomeCersion', "4.7.0")
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-validation'
	implementation 'org.springframework.boot:spring-boot-starter-actuator'
	
	// front
	runtimeOnly "org.webjars.npm:bootstrap:${webjarsBootstrapVersion}"
	runtimeOnly "org.webjars.npm:font-awesome:${webjarsFontAwesomeCersion}"

	// database
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	runtimeOnly 'com.h2database:h2'
	
	// ai
	implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter'

	// dev
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	
	// modulith
	implementation 'org.springframework.modulith:spring-modulith-starter-core'
	runtimeOnly 'org.springframework.modulith:spring-modulith-actuator'
	runtimeOnly 'org.springframework.modulith:spring-modulith-observability'
	
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.springframework.modulith:spring-modulith-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

dependencyManagement {
	imports {
		mavenBom "org.springframework.modulith:spring-modulith-bom:${springModulithVersion}"
		mavenBom "org.springframework.ai:spring-ai-bom:${springAiVersion}"
	}
}

tasks.named('test') {
	useJUnitPlatform()
}

 

Web UI는 Spring PetClinic을 그대로 사용할 하니까 Spring PetClinic 파일을 가져와서 사용한다. (Web Chat UI 사용을 위해서)

@Controller
class HomeController {
	@GetMapping("/")
	public String home(Model model) {
		model.addAttribute("welcome", "안녕하세요");
		return "welcome";
	}
}

 

기본적인 Web 구성이 완료되면 아래와 같이 Spring Petclinic 화면과 AI와 대화를 할 수 있는 채팅 UI를 볼 수 있다.

 

 

 

비즈니스 로직 구현

사용자와 TODO를 관리하는 간단한 기능을 구현한다.

테스트를 위해서 임의로 데이터를 추가했다.

@Controller
@RequestMapping("/users")
class UserController {
	private final UserRepository userRepository;

	public UserController(UserRepository userRepository) {
		this.userRepository = userRepository;
	}

	@GetMapping("/find")
	String find(Model model) {
		model.addAttribute("user", new User());
		return "users/findUsers";
	}

	@GetMapping
	String list(@RequestParam(name = "name", defaultValue = "") String name, Model model) {
		var pageable = PageRequest.of(0, 20);
		model.addAttribute("users", this.userRepository.findByName(name, pageable));
		return "users/userList";
	}

	@PutMapping("")
	String list(@RequestParam(name = "name") String name) {
		var user = new User();
		user.setName(name);
		user.setCreatedAt(ZonedDateTime.now());

		this.userRepository.save(user);
		return "redirect:/users";
	}

}

@Entity
@Table(name = "users")
public class User {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	@Column(name = "name")
	@NotBlank
	private String name;

	private ZonedDateTime createdAt;

	@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
	@JoinColumn(name = "user_id")
	@OrderBy("createdAt DESC")
	private List<Todo> todo = new ArrayList<>();

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public ZonedDateTime getCreatedAt() {
		return createdAt;
	}

	public void setCreatedAt(ZonedDateTime createdAt) {
		this.createdAt = createdAt;
	}

	public List<Todo> getTodo() {
		return todo;
	}

	public void setTodo(List<Todo> todo) {
		this.todo = todo;
	}

}

 

 

AI assistant 작성하기

사용자와 TODO 관리 기능을 구현했으니 AI 어시스턴트를 구성하는 방법을 알아본다.

ChatClient 생성

AI와 통신할 ChatClient를 정의합니다. AI와 대화 맥락을 유지하도록(이전 메시지도 전송) 채팅 메모리 기능과(InMemoryChatMemory) 개발하는 애플리케이션의 어시스턴트로 동작하기 위한 시스템 텍스트를(.defaultSystem(...)) 추가하여 ChatClient를 생성한다.

@Configuration
class AIChatClientConfig {
	@Bean
	ChatMemory chatMemory() {
		return new InMemoryChatMemory();
	}

	@Bean
	VectorStore vectorStore(EmbeddingModel embeddingModel) {
		return new SimpleVectorStore(embeddingModel);
	}

	@Bean
	ChatClient chatClient(ChatClient.Builder builder, ChatMemory chatMemory) {
		return builder
				.defaultSystem(
						"""
								You are a friendly AI assistant designed to help with the management of a user's todo called igooo.
								Your job is to answer questions about and to perform actions on the user's behalf, mainly around
								users, and users' todo.
								You are required to answer an a professional manner. If you don't know the answer, politely tell the user
								you don't know the answer, then ask the user a followup question to try and clarify the question they are asking.
								If you do know the answer, provide the answer but do not provide any additional followup questions.
								When dealing with users, if the user is unsure about the returned results, explain that there may be additional data that was not returned.
								Only if the user is asking about the total number of all users, answer that there are a lot and ask for some additional criteria.
								For todo - provide the correct data.
								""")
				.defaultAdvisors(
						// Chat memory helps us keep context when using the chatbot for up to 10
						// previous messages.
						new MessageChatMemoryAdvisor(chatMemory,
								AbstractChatMemoryAdvisor.DEFAULT_CHAT_MEMORY_CONVERSATION_ID, 10), // CHAT MEMORY
						new SimpleLoggerAdvisor())
				.build();
	}
}

 

OpenAI 연동을 위한 설정 정보를 추가한다.

spring:
  ai:
    openai:
      api-key: << API-KEY >>
      chat:
        options:
          model: gpt-4o
          temperature: 0.7

 

UI에서 사용자 프롬프트를 입력받을 API를 추가하고 ChatClient로 프롬프트를 전송하고, AI 모델에서 응답받아 결과를 UI로 전달하는 API를 추가해 준다. 

@RestController
class AIController {
	private final ChatClient chatClient;

	AIController(ChatClient chatClient) {
		this.chatClient = chatClient;
	}

	@PostMapping("/chatclient")
	String exchange(@RequestBody String query) {
		return this.chatClient.prompt().user((u) -> u.text(query)).call().content();
	}
}

 

기본적인 설정이 완료되었으니 애플리케이션을 실행하고 간단하게 테스트를 해본다.

  • ChatClient에 시스템 텍스트를 추가한 내용처럼 사용자 수에 대한 질문은 사용자가 많다고 응답한다.
  • 실제 '키보드'라는 이름의 사용자는 DB에 없지만 AI는 '키보드'라는 이름의 사용자가 있다고 응답한다.

AI 기능은 동작하지만 아직 애플리케이션의 정보가 부족하다. 이어서 애플리케이션에서 지원하는 기능을 AI에 알려주어 더 정확한 대답을 할 수 있도록 추가적으로 구성한다.

 

 핵심 기능 식별(Identifying Core Functionality)

애플리케이션에서 제공하는 기능을(사용자, TODO 관리) Spring AI에서 사용할 수 있는 함수로 등록하기 위해서 java.util.function.Function 객체를 Bean으로 등록한다.

 

사용자 목록 제공 기능

사용자 조회 기능에 대하여 Function을 만들고 Bean으로 등록한다. 추가적으로 @Description을 사용하여 AI에게 어떤 기능을 제공하는지 알려준다.

@Service
class AIDataProvider {
	private final UserRepository userRepository;

	public AIDataProvider(UserRepository userRepository) {
		this.userRepository = userRepository;
	}

	public List<User> findAllUser() {
		return this.userRepository.findAll();
	}
}

@Configuration
class AIFunctionConfig {
	@Bean
	@Description("List the users that the user's todo has")
	Function<UserRequest, UserResponse> listUsers(AIDataProvider dataProvier) {
		return (request) -> new UserResponse(dataProvier.findAllUser());
	}
}

record UserRequest(User user) {

}

record UserResponse(List<User> users) {

}

 

Bean으로 등록한 사용자 조회 기능에(listUser) 대하여 설정을(functions) 추가한다.

  ai:
    openai:
      api-key: << API KEY >>
      chat:
        options:
          model: gpt-4o
          temperature: 0.7
          functions:
          - listUsers

 

사용자 정보를 제공하고 다시 AI에게 동일한 질문과 전체 사용자에 대한 추가 질문을 하면 AI는 올바른 응답을 할 수 있다.

 

사용자 추가 제공

사용자 추가하는 기능도 AI에게 제공하면 사용자 추가도 AI 어시스턴트를 사용하여 추가가 가능하다.

@Service
class AIDataProvider {
	......

	public AddUserResponse addUser(AddUserRequest request) {
		var user = new User();
		user.setName(request.name());
		user.setCreatedAt(ZonedDateTime.now());

		var savedUser = this.userRepository.save(user);
		return new AddUserResponse(savedUser);
	}

}


@Configuration
class AIFunctionConfig {
	......

	@Bean
	@Description("Add a new user to the user's todo.  The User must include a name as  least 2 characters")
	Function<AddUserRequest, AddUserResponse> addUser(AIDataProvider dataProvier) {
		return request -> {
			return dataProvier.addUser(request);
		};
	}
}
......

record AddUserRequest(String name) {

}

record AddUserResponse(User users) {

}

 

spring:
  ai:
    openai:
      .....
          functions:
          - listUsers
          - addUser

 

AI 어시스턴트를 사용하여 사용자를 추가해 달라고 요청하고 새로고침하면 키보드라는 사용자가 추가된 것을 볼 수 있다.

 

 

 

Part 2에서는 RAG(Retrieval-Augmented Generation)을 사용한 TODO 검색 기능에 대하여 구현한다.

 

 

참고