์๋ ํ์ธ์ dev_writer์ ๋๋ค.
์ด๋ฒ ์๊ฐ์๋ Spring AI ๊ณต์ ๋ฌธ์ ์ค Chat Client API์ ๋ํ ๋ด์ฉ์ ๋ฒ์ญํ ๋ด์ฉ์ ์ ๋ฌํด ๋๋ฆฌ๊ฒ ์ต๋๋ค.
Chat Client API
ChatClient๋ AI ๋ชจ๋ธ๊ณผ์ ํต์ ์ ์ํ ์ ์ฐฝํ (fluent) API๋ฅผ ์ ๊ณตํฉ๋๋ค. ์ด API๋ ๋๊ธฐ (synchronous) ๋ฐฉ์๊ณผ ์คํธ๋ฆฌ๋ฐ (streaming) ๋ฐฉ์์ ํ๋ก๊ทธ๋๋ฐ ๋ชจ๋ธ์ ๋ชจ๋ ์ง์ํฉ๋๋ค.
์ด fluent API๋ AI ๋ชจ๋ธ์ ์ ๋ ฅ์ผ๋ก ์ ๋ฌ๋๋ Prompt๋ฅผ ๊ตฌ์ฑํ๋ ์์๋ค์ ๋จ๊ณ์ ์ผ๋ก ์กฐ๋ฆฝํ ์ ์๋ ๋ฉ์๋๋ฅผ ์ ๊ณตํฉ๋๋ค. Prompt๋ AI ๋ชจ๋ธ์ ์ถ๋ ฅ๊ณผ ๋์์ ์ ๋ํ๊ธฐ ์ํ ์ง์๋ฌธ (instructional text)์ ํฌํจํ๋ฉฐ, API ๊ด์ ์์ ๋ณผ ๋ Prompt๋ ๋ฉ์์ง๋ค์ ์งํฉ์ผ๋ก ๊ตฌ์ฑ๋ฉ๋๋ค.
AI ๋ชจ๋ธ์ ์ฃผ๋ก ๋ ๊ฐ์ง ์ ํ์ ๋ฉ์์ง๋ฅผ ์ฒ๋ฆฌํฉ๋๋ค: ์ฌ์ฉ์ ๋ฉ์์ง (user message)์ ์์คํ ๋ฉ์์ง (system messages)์ ๋๋ค. ์ฌ์ฉ์ ๋ฉ์์ง๋ ์ฌ์ฉ์๊ฐ ์ง์ ์ ๋ ฅํ ๋ด์ฉ์ ์๋ฏธํ๊ณ , ์์คํ ๋ฉ์์ง๋ ๋ํ์ ํ๋ฆ์ ์๋ดํ๊ธฐ ์ํด ์์คํ ์์ ์์ฑ๋ ๋ฉ์์ง์ ๋๋ค.
์ด๋ฌํ ๋ฉ์์ง๋ค์๋ ์ข ์ข ํ๋ ์ด์คํ๋ (placeholder)๊ฐ ํฌํจ๋์ด ์์ผ๋ฉฐ, ์ด๋ ์คํ ์์ ์ ์ฌ์ฉ์ ์ ๋ ฅ์ ๋ฐ๋ผ ๋์ฒด๋์ด AI ๋ชจ๋ธ์ ์๋ต์ ์ฌ์ฉ์ ๋ง์ถคํ์ผ๋ก ์กฐ์ ํ๋ ๋ฐ ํ์ฉ๋ฉ๋๋ค.
ํ๋ ์ด์คํ๋ (placeholder)์ ์ฌ์ฉ ์ ์ค ์ผ๋ถ์ธ PromptTemplate ์ ๋ํด ์๊ณ ์ถ์ผ์ ๋ถ๋ค์ ์๋ ๊ธ์ ์ฐธ๊ณ ํ์๊ธฐ ๋ฐ๋๋๋ค.
- [Spring AI ๐ค] Spring AI์์ ํ๋กฌํํธ๋ฅผ ๋์ฑ ํจ๊ณผ์ ์ผ๋ก ์์ฑํ๋ ๋ฐฉ๋ฒ (feat. PromptTemplate)
๋ํ, ์ฌ์ฉํ AI ๋ชจ๋ธ์ ์ด๋ฆ์ด๋ ์์ฑ๋๋ ์๋ต์ ๋ฌด์์์ฑ ๋๋ ์ฐฝ์์ฑ์ ์กฐ์ ํ๋ temperature ์ค์ ๊ณผ ๊ฐ์ Prompt ์ต์ ๋ ํจ๊ป ์ง์ ํ ์ ์์ต๋๋ค.
ChatClient ์์ฑํ๊ธฐ
ChatClient๋ ChatClient.Builder ๊ฐ์ฒด๋ฅผ ํตํด ์์ฑ๋ฉ๋๋ค. Spring Boot์ ์๋ ๊ตฌ์ฑ ๊ธฐ๋ฅ์ ์ฌ์ฉํ๋ ChatModel์ ๋ํด ์๋์ผ๋ก ๊ตฌ์ฑ๋ ChatClient.Builder ์ธ์คํด์ค๋ฅผ ์ป๊ฑฐ๋, ์ง์ ํ๋ก๊ทธ๋๋ฐ ๋ฐฉ์์ผ๋ก ์์ฑํ ์ ์์ต๋๋ค.
์๋ ๊ตฌ์ฑ๋ ChatClient.Builder ์ฌ์ฉํ๊ธฐ
๊ฐ์ฅ ๊ฐ๋จํ ์ฌ์ฉ ์ฌ๋ก์์๋, Spring AI๊ฐ ์ ๊ณตํ๋ Spring Boot ์๋ ๊ตฌ์ฑ ๊ธฐ๋ฅ์ ํตํด ChatClient.Builder์ ํ๋กํ ํ์ ๋น (bean)์ด ์๋์ผ๋ก ์์ฑ๋๋ฉฐ, ์ด๋ฅผ ์ฌ๋ฌ๋ถ์ ํด๋์ค์ ์ฃผ์ ํ์ฌ ์ฌ์ฉํ ์ ์์ต๋๋ค.
๋ค์์ ์ฌ์ฉ์์ ๊ฐ๋จํ ์์ฒญ์ ๋ํด ๋ฌธ์์ด ์๋ต์ ๋ฐ๋ ์์ ์ ๋๋ค.
@RestController
class MyController {
private final ChatClient chatClient;
public MyController(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
@GetMapping("/ai")
String generation(String userInput) {
return this.chatClient.prompt()
.user(userInput)
.call()
.content();
}
}
์ด ๊ฐ๋จํ ์์ ์์ ์ฌ์ฉ์ ์ ๋ ฅ์ user message์ ๋ด์ฉ์ ์ค์ ํฉ๋๋ค. call() ๋ฉ์๋๋ AI ๋ชจ๋ธ์ ์์ฒญ์ ์ ์กํ๋ฉฐ, content() ๋ฉ์๋๋ AI ๋ชจ๋ธ์ ์๋ต์ ๋ฌธ์์ด (String)๋ก ๋ฐํํฉ๋๋ค.
ChatClient๋ฅผ ํ๋ก๊ทธ๋๋ฐ ๋ฐฉ์์ผ๋ก ์์ฑํ๊ธฐ
spring.ai.chat.client.enabled=false๋ก ์์ฑ์ ์ค์ ํ๋ฉด, ChatClient.Builder์ ์๋ ๊ตฌ์ฑ์ ๋นํ์ฑํํ ์ ์์ต๋๋ค. ์ด ๋ฐฉ์์ ์ฌ๋ฌ ๊ฐ์ ์ฑ ๋ชจ๋ธ์ ํจ๊ป ์ฌ์ฉํ๋ ๊ฒฝ์ฐ์ ์ ์ฉํฉ๋๋ค.
์ด ๊ฒฝ์ฐ, ํ์ํ ๊ฐ ChatModel์ ๋ํด ํ๋ก๊ทธ๋๋ฐ ๋ฐฉ์์ผ๋ก ChatClient.Builder ์ธ์คํด์ค๋ฅผ ์ง์ ์์ฑํ๋ฉด ๋ฉ๋๋ค:
ChatModel myChatModel = ... // ์ผ๋ฐ์ ์ผ๋ก ์๋ ์ฃผ์
๋จ
ChatClient.Builder builder = ChatClient.builder(this.myChatModel);
// ๋๋ ๊ธฐ๋ณธ ๋น๋ ์ค์ ์ผ๋ก ChatClient ์์ฑ:
ChatClient chatClient = ChatClient.create(this.myChatModel);
ChatClient Fluent API
ChatClient์ Fluent API๋ ์ค๋ฒ๋ก๋ฉ๋ prompt ๋ฉ์๋๋ฅผ ํตํด ์ธ ๊ฐ์ง ๋ฐฉ์์ผ๋ก Prompt๋ฅผ ์์ฑํ ์ ์๋๋ก ์ง์ํฉ๋๋ค. ๊ฐ ๋ฐฉ์์ Fluent API์ ์์์ ์ญํ ์ ํฉ๋๋ค.
- prompt(): ์ธ์๋ฅผ ๋ฐ์ง ์๋ ์ด ๋ฉ์๋๋ Fluent API๋ฅผ ์์ํ๋ ๊ธฐ๋ณธ์ ์ธ ๋ฐฉ๋ฒ์ ๋๋ค. ์ฌ์ฉ์ ๋ฉ์์ง, ์์คํ ๋ฉ์์ง ๋ฑ Prompt์ ์ฌ๋ฌ ๊ตฌ์ฑ ์์๋ฅผ ์ฒด๊ณ์ ์ผ๋ก ์์ฑํ ์ ์๋๋ก ์ง์ํฉ๋๋ค.
- prompt(Prompt prompt): ์ด ๋ฉ์๋๋ Prompt ์ธ์คํด์ค๋ฅผ ์ธ์๋ก ๋ฐ์ ์ฌ์ฉ์๊ฐ Prompt์ ๋น-Fluent API๋ฅผ ์ฌ์ฉํ์ฌ ๋ฏธ๋ฆฌ ๊ตฌ์ฑํ Prompt ๊ฐ์ฒด๋ฅผ ๊ทธ๋๋ก ์ ๋ฌํ ์ ์๊ฒ ํฉ๋๋ค.
- prompt(String context): ์ด ๋ฉ์๋๋ ์ฌ์ฉ์ ์ ๋ ฅ ํ ์คํธ๋ฅผ ๋ฐ๋ก ์ ๋ฌํ ์ ์๋๋ก ํ ํธ์์ฑ ๋ฉ์๋์ ๋๋ค. ์์ ์ค๋ช ํ ์ค๋ฒ๋ก๋ ๋ฐฉ์๊ณผ ์ ์ฌํ๋, ๋ฌธ์์ด ํ๋๋ง์ผ๋ก Prompt๋ฅผ ๊ฐ๋จํ ๊ตฌ์ฑํ ์ ์์ต๋๋ค.
ChatClient ์๋ต ์ฒ๋ฆฌ
ChatClient, Prompt, Generations, ChatResponse์ ๋ํด ๋ ์๊ณ ์ถ์ผ์ ๋ถ๋ค์ ์๋ ๊ธ์ ์ฐธ๊ณ ํ์๊ธฐ ๋ฐ๋๋๋ค.
- [Spring AI ๐ค] Spring AI๋ฅผ ์ ์ฉํด๋ณด์!
ChatClient API๋ API ๋ชจ๋ธ๋ก๋ถํฐ ๋ฐ์ ์๋ต์ ๋ค์ํ ๋ฐฉ์์ผ๋ก ๊ฐ๊ณตํ ์ ์๋๋ก ์ง์ํฉ๋๋ค.
ChatResponse ๋ฐํ
AI ๋ชจ๋ธ์ ์๋ต์ ChatResponse ํ์ ์ผ๋ก ์ ์๋ ํ๋ถํ ๊ตฌ์กฐ๋ฅผ ๊ฐ๊ณ ์์ต๋๋ค. ์ด ์๋ต์๋ ์๋ต์ด ์ด๋ป๊ฒ ์์ฑ๋์๋์ง์ ๋ํ ๋ฉํ๋ฐ์ดํฐ๊ฐ ํฌํจ๋๋ฉฐ, ๊ฐ๊ฐ์ ๋ฉํ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ง ์ฌ๋ฌ ๊ฐ์ ์๋ต (Generations)๋ ํฌํจ๋ ์ ์์ต๋๋ค. ๋ฉํ๋ฐ์ดํฐ์๋ ์๋ต์ ์์ฑํ๋ ๋ฐ ์ฌ์ฉ๋ ํ ํฐ ์๊ฐ ํฌํจ๋์ด ์์ผ๋ฉฐ, ๊ฐ ํ ํฐ์ ๋๋ต ๋จ์ด์ 3/4 ์ ๋์ ํด๋นํฉ๋๋ค. ์ด ์ ๋ณด๋ ์ค์ํฉ๋๋ค. ์๋ํ๋ฉด ํธ์คํ ๋ AI ๋ชจ๋ธ์ ์์ฒญ ๋น ์ฌ์ฉ๋ ํ ํฐ ์๋ฅผ ๊ธฐ์ค์ผ๋ก ์๊ธ์ ๋ถ๊ณผํ๊ธฐ ๋๋ฌธ์ ๋๋ค.
์๋๋ call() ๋ฉ์๋ ์ดํ์ chatResponse()๋ฅผ ํธ์ถํ์ฌ ๋ฉํ๋ฐ์ดํฐ๋ฅผ ํฌํจํ ChatResponse ๊ฐ์ฒด๋ฅผ ๋ฐํํ๋ ์์ ์ ๋๋ค.
ChatResponse chatResponse = chatClient.prompt()
.user("Tell me a joke")
.call()
.chatResponse();
์ํฐํฐ ๋ฐํ
AI ๋ชจ๋ธ๋ก๋ถํฐ ๋ฐํ๋ ๋ฌธ์์ด์ ๋งคํํ์ฌ ์ํฐํฐ ํด๋์ค๋ก ๋ฐํํ๊ณ ์ ํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์ต๋๋ค. entity() ๋ฉ์๋๋ ์ด๋ฌํ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค.
์๋ฅผ ๋ค์ด, ๋ค์๊ณผ ๊ฐ์ Java record๊ฐ ์์ ๋:
record ActorFilms(String actor, List<String> movies) {}
AI ๋ชจ๋ธ์ ์ถ๋ ฅ ๊ฒฐ๊ณผ๋ฅผ ์๋์ ๊ฐ์ด ํด๋น record๋ก ์์ฝ๊ฒ ๋งคํํ ์ ์์ต๋๋ค:
ActorFilms actorFilms = chatClient.prompt()
.user("Generate the filmography for a random actor.")
.call()
.entity(ActorFilms.class);
๋ํ, ์ ๋ค๋ฆญ ๋ฆฌ์คํธ์ ๊ฐ์ ํ์ ์ ์ง์ ํ ์ ์๋๋ก ์ค๋ฒ๋ก๋ฉ๋ entity ๋ฉ์๋ entity(ParameterizedTypeReference<T> type) ๋ ์ ๊ณตํฉ๋๋ค:
List<ActorFilms> actorFilms = chatClient.prompt()
.user("Generate the filmography of 5 movies for Tom Hanks and Bill Murray.")
.call()
.entity(new ParameterizedTypeReference<List<ActorFilms>>() {});
์คํธ๋ฆฌ๋ฐ ์๋ต ๋ฐํ
stream() ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋ฉด ๋น๋๊ธฐ ๋ฐฉ์์ผ๋ก AI ์๋ต์ ๋ฐ์ ์ ์์ต๋๋ค. ์๋๋ ๊ทธ ์์์ ๋๋ค:
Flux<String> output = chatClient.prompt()
.user("Tell me a joke")
.stream()
.content();
๋ํ Flux<ChatResponse> ํํ๋ก ChatResponse ์ ์ฒด๋ฅผ ์คํธ๋ฆฌ๋ฐ ํ ์๋ ์์ต๋๋ค. ํฅํ์๋ stream() ๋ฉ์๋๋ฅผ ์ฌ์ฉํ ๋๋ Java ์ํฐํฐ๋ฅผ ๋ฐ๋ก ๋ฐํํ ์ ์๋ ํธ์ ๋ฉ์๋๊ฐ ์ ๊ณต๋ ์์ ์ ๋๋ค. ํ์ฌ๋ ์๋ต์ ์์งํ ํ ๋ช ์์ ์ผ๋ก Structured Output Converter๋ฅผ ์ฌ์ฉํ์ฌ ๋ณํํด์ผ ํฉ๋๋ค. ์๋๋ ๊ทธ ์์์ด๋ฉฐ, ์ด ๊ณผ์ ์ ์ถํ ์ค๋ช ๋ Fluent API์ ํ๋ผ๋ฏธํฐ ์ฌ์ฉ ์์๋ ํจ๊ป ๋ณด์ฌ์ค๋๋ค.
๊ฐ์ธ์ ์ผ๋ก ํ์ตํ๋ BeanOutputConverter (StructuredOutputConverter์ ๊ตฌํ์ฒด)์ ๋ํด ์๊ณ ์ถ์ผ์ ๋ถ๋ค์ ์๋ ๊ธ์ ์ฐธ๊ณ ํ์๊ธฐ ๋ฐ๋๋๋ค.
- [Spring AI ๐ค] ์ด๋ฏธ์ง๋ฅผ ๋ถ์ํ๊ณ JSON ํฌ๋งทํ ์ ํด๋ณด์! (feat. EatToFit)
var converter = new BeanOutputConverter<>(new ParameterizedTypeReference<List<ActorsFilms>>() {});
Flux<String> flux = this.chatClient.prompt()
.user(u -> u.text("""
Generate the filmography for a random actor.
{format}
""")
.param("format", this.converter.getFormat()))
.stream()
.content();
String content = this.flux.collectList().block().stream().collect(Collectors.joining());
List<ActorFilms> actorFilms = this.converter.convert(this.content);
call() ๋ฐํ ๊ฐ
call() ๋ฉ์๋๋ฅผ ํธ์ถํ ์ดํ์๋ ๋ค์ํ ํํ๋ก ์๋ต์ ๋ฐํ๋ฐ์ ์ ์์ต๋๋ค:
- String content(): ์๋ต์ ๋ด์ฉ์ String ํํ๋ก ๋ฐํํฉ๋๋ค.
- ChatResponse chatResponse(): ์ฌ๋ฌ ๊ฐ์ ์์ฑ ๊ฒฐ๊ณผ (Generations)์ ์๋ต ๋ฉํ๋ฐ์ดํฐ (์: ์ฌ์ฉ๋ ํ ํฐ ์ ๋ฑ)๋ฅผ ํฌํจํ๋ ChatResponse ๊ฐ์ฒด๋ฅผ ๋ฐํํฉ๋๋ค.
- entity(): ์๋ต์ Java ํ์ ์ ์ํฐํฐ๋ก ๋ฐํํฉ๋๋ค.
- entity(ParameterizedTypeReference<T> type): ์ ๋ค๋ฆญ ์ปฌ๋ ์ ํ์ ์ ์ํฐํฐ๋ฅผ ๋ฐํํ ๋ ์ฌ์ฉํฉ๋๋ค. ์: List<MyEntity>
- entity(Class<T> type): ๋จ์ผ ์ํฐํฐ ํ์ ์ ๋ฐํํ ๋ ์ฌ์ฉํฉ๋๋ค.
- entity(StructuredOutputConverter<T> structuredOutputConverter): ๋ฌธ์์ด์ ์ํฐํฐ ํ์ ์ผ๋ก ๋ณํํ๊ธฐ ์ํ StructuredOutputConverter ์ธ์คํด์ค๋ฅผ ์ง์ ํฉ๋๋ค.
๋ํ, call() ๋์ stream() ๋ฉ์๋๋ฅผ ์ฌ์ฉํ ์๋ ์์ต๋๋ค.
stream() ๋ฐํ ๊ฐ
stream() ๋ฉ์๋๋ฅผ ํธ์ถํ ์ดํ์๋ ๋ค์๊ณผ ๊ฐ์ ๋ฐฉ์์ผ๋ก ์๋ต์ ๋ฐ์ ์ ์์ต๋๋ค:
- Flux<String> content(): AI ๋ชจ๋ธ์ด ์์ฑํ๋ ๋ฌธ์์ด์ ์ค์๊ฐ์ผ๋ก ์คํธ๋ฆฌ๋ฐ ํํ(Flux)๋ก ๋ฐํํฉ๋๋ค.
- Flux<ChatResponse> chatResponse(): ์๋ต ๋ฉํ๋ฐ์ดํฐ๋ฅผ ํฌํจํ๋ ChatResponse ๊ฐ์ฒด๋ฅผ ์คํธ๋ฆฌ๋ฐ ํํ(Flux)๋ก ๋ฐํํฉ๋๋ค.
๊ธฐ๋ณธ๊ฐ ์ฌ์ฉ
@Configuration ํด๋์ค ๋ด์์ ์์คํ ํ ์คํธ์ ๊ธฐ๋ณธ๊ฐ์ ์ค์ ํ์ฌ ChatClient๋ฅผ ์์ฑํ๋ฉด ๋ฐํ์ ์ฝ๋๊ฐ ๊ฐ๊ฒฐํด์ง๋๋ค.
๊ธฐ๋ณธ๊ฐ์ ์ค์ ํ๋ฉด ๋งค ์์ฒญ๋ง๋ค ์์คํ ํ ์คํธ๋ฅผ ์ง์ ํ ํ์ ์์ด ์ฌ์ฉ์ ํ ์คํธ๋ง ์ง์ ํ๋ฉด ๋ฉ๋๋ค.
๊ธฐ๋ณธ ์์คํ ํ ์คํธ
๋ค์ ์์๋ ์์คํ ํ ์คํธ๋ฅผ ํญ์ ํด์ ์ ๋งํฌ๋ก ์๋ตํ๋๋ก ์ค์ ํ๋ ๋ฐฉ๋ฒ์ ๋ณด์ฌ์ค๋๋ค. ์ด ์ค์ ์ @Configuration ํด๋์ค ์์์ ChatClient ์ธ์คํด์ค๋ฅผ ์ ์ํจ์ผ๋ก์จ, ๋ฐํ์ ์ฝ๋์์ ์ค๋ณต ์ค์ ์ ํผํ ์ ์์ต๋๋ค.
@Configuration
class Config {
@Bean
ChatClient chatClient(ChatClient.Builder builder) {
return builder.defaultSystem("You are a friendly chat bot that answers question in the voice of a Pirate")
.build();
}
}
๊ทธ๋ฆฌ๊ณ ์ด๋ฅผ ํธ์ถํ๋ @RestController๋ ์๋์ ๊ฐ์ต๋๋ค.
@RestController
class AIController {
private final ChatClient chatClient;
AIController(ChatClient chatClient) {
this.chatClient = chatClient;
}
@GetMapping("/ai/simple")
public Map<String, String> completion(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
return Map.of("completion", this.chatClient.prompt().user(message).call().content());
}
}
curl๋ก ํธ์ถํ๋ฉด ์๋์ ๊ฐ์ด ๋์ต๋๋ค.
โฏ curl localhost:8080/ai/simple
{"completion":"Why did the pirate go to the comedy club? To hear some arrr-rated jokes! Arrr, matey!"}
ํ๋ผ๋ฏธํฐ๊ฐ ํฌํจ๋ ๊ธฐ๋ณธ ์์คํ ํ ์คํธ
๋ค์ ์์ ์์๋ ์์คํ ํ ์คํธ์ ํ๋ ์ด์คํ๋ { voice }๋ฅผ ์ฌ์ฉํ์ฌ, ๋์์ธ ํ์์ด ์๋ ๋ฐํ์์ ๋งํฌ๋ฅผ ์ง์ ํ ์ ์๋๋ก ๊ตฌ์ฑํฉ๋๋ค.
@Configuration
class Config {
@Bean
ChatClient chatClient(ChatClient.Builder builder) {
return builder.defaultSystem("You are a friendly chat bot that answers question in the voice of a {voice}")
.build();
}
}
์ปจํธ๋กค๋ฌ์์๋ ์๋์ ๊ฐ์ด ์์คํ ํ ์คํธ์ ๋์ ์ผ๋ก ํ๋ผ๋ฏธํฐ๋ฅผ ์ฃผ์ ํฉ๋๋ค:
@RestController
class AIController {
private final ChatClient chatClient;
AIController(ChatClient chatClient) {
this.chatClient = chatClient;
}
@GetMapping("/ai")
Map<String, String> completion(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message, String voice) {
return Map.of("completion",
this.chatClient.prompt()
.system(sp -> sp.param("voice", voice))
.user(message)
.call()
.content());
}
}
httpie๋ฅผ ์ฌ์ฉํ ์คํ ๊ฒฐ๊ณผ ์์๋ ์๋์ ๊ฐ์ต๋๋ค.
http localhost:8080/ai voice=='Robert DeNiro'
{
"completion": "You talkin' to me? Okay, here's a joke for ya: Why couldn't the bicycle stand up by itself? Because it was two tired! Classic, right?"
}
๊ธฐํ ๊ธฐ๋ณธ๊ฐ ์ค์
Client.Builder ์์ค์์ ํ๋กฌํํธ์ ๊ธฐ๋ณธ ๊ตฌ์ฑ์ ์ง์ ํ ์ ์์ต๋๋ค.
- defaultOptions(ChatOptions chatOptions): ChatOptions ํด๋์ค์์ ์ ์๋ ๊ณตํต ์ต์ ๋๋ OpenAiChatOptions์ ๊ฐ์ ๋ชจ๋ธ๋ณ ์ต์ ์ ์ค์ ํ ์ ์์ต๋๋ค. ๋ชจ๋ธ๋ณ ChatOptions ๊ตฌํ์ ๋ํ ์์ธํ ๋ด์ฉ์ JavaDocs๋ฅผ ์ฐธ๊ณ ํ์ญ์์ค.
- defaultFunction(String name, String description, java.util.function.Function<I, O> function): ์ฌ์ฉ์ ํ ์คํธ์์ ์ฐธ์กฐ๋ ํจ์ ์ด๋ฆ, ํจ์์ ๋ชฉ์ ์ ์ค๋ช ํ๋ ์ค๋ช , ๊ทธ๋ฆฌ๊ณ ์ค์ ์คํ๋ Java ํจ์ ์ธ์คํด์ค๋ฅผ ์ค์ ํฉ๋๋ค. AI ๋ชจ๋ธ์ ์ด ์ ๋ณด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ ์ ํ ํจ์๋ฅผ ์ ํํด ์คํํ ์ ์์ต๋๋ค.
- defaultFunctions(String... functionNames): ์ ํ๋ฆฌ์ผ์ด์ ์ปจํ ์คํธ์ ์ ์๋ Function ๋น์ ์ด๋ฆ๋ค์ ์ง์ ํฉ๋๋ค.
- defaultUser(String text), defaultUser(Resource text), defaultUser(Consumer<UserSpec> userSpecConsumer): ์ฌ์ฉ์ ์ ๋ ฅ ํ ์คํธ์ ๊ธฐ๋ณธ๊ฐ์ ์ค์ ํ ์ ์์ต๋๋ค. ํนํ Consumer<UserSpec>์ ๋๋ค ํํ์์ ํตํด ํ ์คํธ์ ๊ธฐ๋ณธ ํ๋ผ๋ฏธํฐ๋ฅผ ํจ๊ป ์ ์ํ ์ ์๊ฒ ํด ์ค๋๋ค.
- defaultAdvisors(Advisor... advisor): ํ๋กฌํํธ๋ฅผ ์์ฑํ๋ ๋ฐ ์ฌ์ฉ๋๋ ๋ฐ์ดํฐ๋ฅผ ์์ ํ๊ฑฐ๋ ๋ณด๊ฐํ๋ Advisor๋ฅผ ์ค์ ํฉ๋๋ค. ์: QuestionAnswerAdvisor๋ ์ฌ์ฉ์ ์ ๋ ฅ์ ๊ธฐ๋ฐํ ๊ด๋ จ ๋ฌธ๋งฅ ์ ๋ณด๋ฅผ ํ๋กฌํํธ์ ์ถ๊ฐํ์ฌ RAG(Retrieval-Augmented Generation, ๊ฒ์ ์ฆ๊ฐ ์์ฑ)๋ฅผ ๊ตฌํํฉ๋๋ค.
- defaultAdvisors(Consumer<AdvisorSpec> advisorSpecConsumer): AdvisorSpec์ ํ์ฉํ์ฌ ์ฌ๋ฌ Advisor๋ฅผ ๋๋ค ๋ฐฉ์์ผ๋ก ๋ฑ๋กํ ์ ์๋๋ก ์ง์ํฉ๋๋ค.
์ด๋ฌํ ๊ธฐ๋ณธ๊ฐ๋ค์ ๋ฐํ์์์ ํด๋นํ๋ ๋ฉ์๋๋ฅผ ํตํด ์ฌ์ ์ํ ์ ์์ต๋๋ค. (default ์ ๋์ฌ ์์ด ์ฌ์ฉ)
- options(ChatOptions chatOptions)
- function(...), functions(...)
- user(...)
- advisors(...), advisors(Consumer<AdvisorSpec>)
Advisors
Advisors API๋ Spring ์ ํ๋ฆฌ์ผ์ด์ ์์ AI ๊ธฐ๋ฐ ์ํธ์์ฉ์ ๊ฐ๋ก์ฑ๊ณ , ์์ ํ๋ฉฐ, ํ์ฅํ ์ ์๋ ์ ์ฐํ๊ณ ๊ฐ๋ ฅํ ์๋จ์ ์ ๊ณตํฉ๋๋ค.
AI ๋ชจ๋ธ์ ์ฌ์ฉ์ ์ ๋ ฅ์ผ๋ก ํธ์ถํ ๋, ์ผ๋ฐ์ ์ธ ํจํด ์ค ํ๋๋ ํ๋กฌํํธ์ ๋ฌธ๋งฅ ์ ๋ณด๋ฅผ ์ถ๊ฐํ๊ฑฐ๋ ๋ณด๊ฐํ๋ ๊ฒ์ ๋๋ค.
์ด๋ฌํ ๋ฌธ๋งฅ ์ ๋ณด (contextual data)๋ ๋ค์ํ ํํ์ผ ์ ์์ผ๋ฉฐ, ์ผ๋ฐ์ ์ผ๋ก ๋ค์ ๋ ๊ฐ์ง ์ ํ์ด ์์ฃผ ์ฌ์ฉ๋ฉ๋๋ค.
- ์ฌ์ฉ์ ์ ์ ๋ฐ์ดํฐ: AI ๋ชจ๋ธ์ด ํ์ตํ์ง ์์ ์์ฒด ๋ฐ์ดํฐ์ ๋๋ค. ๋ชจ๋ธ์ด ์ ์ฌํ ๋ฐ์ดํฐ๋ฅผ ํ์ตํ๋๋ผ๋, ์ถ๊ฐ๋ ๋ฌธ๋งฅ ๋ฐ์ดํฐ๊ฐ ์๋ต ์์ฑ ์ ์ฐ์ ์ ์ผ๋ก ๋ฐ์๋ฉ๋๋ค.
- ๋ํ ์ด๋ ฅ (Conversational History): ์ฑ ๋ชจ๋ธ API๋ ์ํ๋ฅผ ์ ์งํ์ง ์๊ธฐ ๋๋ฌธ์, ์๋ฅผ ๋ค์ด AI ๋ชจ๋ธ์ ์ด๋ฆ์ ์๋ ค์ค๋ ์ดํ ์์ฒญ์์ ๊ธฐ์ตํ์ง ๋ชปํฉ๋๋ค. ๋ฐ๋ผ์ ์ด์ ๋ํ ๋ด์ฉ์ ๋ฐ์ํ๊ณ ์ถ๋ค๋ฉด, ๋งค ์์ฒญ๋ง๋ค ํด๋น ์ด๋ ฅ์ ํจ๊ป ์ ์กํด์ผ ํฉ๋๋ค.
ChatClient์์์ Advisor ์ค์
ChatClient์ Fluent API๋ Advisor ๊ตฌ์ฑ์ ์ํ AdvisorSpec ์ธํฐํ์ด์ค๋ฅผ ์ ๊ณตํฉ๋๋ค. ์ด ์ธํฐํ์ด์ค๋ ํ๋ผ๋ฏธํฐ๋ฅผ ์ถ๊ฐํ๊ฑฐ๋, ์ฌ๋ฌ ๊ฐ์ ํ๋ผ๋ฏธํฐ๋ฅผ ํ ๋ฒ์ ์ค์ ํ๊ฑฐ๋, ํ๋ ์ด์์ Advisor๋ฅผ ์ฒด์ธ์ ์ถ๊ฐํ ์ ์๋ ๋ฉ์๋๋ค์ ์ ๊ณตํฉ๋๋ค.
interface AdvisorSpec {
AdvisorSpec param(String k, Object v);
AdvisorSpec params(Map<String, Object> p);
AdvisorSpec advisors(Advisor... advisors);
AdvisorSpec advisors(List<Advisor> advisors);
}
์ค์
Advisor๊ฐ ์ฒด์ธ์ ์ถ๊ฐ๋๋ ์์๋ ๋งค์ฐ ์ค์ํฉ๋๋ค.
์ด ์์๋ ๊ฐ Advisor์ ์คํ ์์๋ฅผ ๊ฒฐ์ ํ๋ฉฐ, ๊ฐ Advisor๋ ํ๋กฌํํธ๋ ๋ฌธ๋งฅ์ ์ด๋ค ๋ฐฉ์์ผ๋ก๋ ์์ ํฉ๋๋ค.
ํ๋์ Advisor๊ฐ ์ํํ ๋ณ๊ฒฝ ์ฌํญ์ ์ฒด์ธ์ ๋ค์ Advisor๋ก ์ ๋ฌ๋์ด ์ฐ์์ ์ผ๋ก ์ ์ฉ๋ฉ๋๋ค.
ChatClient.builder(chatModel)
.build()
.prompt()
.advisors(
new MessageChatMemoryAdvisor(chatMemory),
new QuestionAnswerAdvisor(vectorStore)
)
.user(userText)
.call()
.content();
์ด ๊ตฌ์ฑ์์๋ MessageChatMemoryAdvisor๊ฐ ๋จผ์ ์คํ๋์ด ๋ํ ๊ธฐ๋ก์ ํ๋กฌํํธ์ ์ถ๊ฐํฉ๋๋ค. ๊ทธ๋ฐ ๋ค์ QuestionAnswerAdvisor๋ ์ฌ์ฉ์์ ์ง๋ฌธ๊ณผ ์ถ๊ฐ๋ ๋ํ ๊ธฐ๋ก์ ๊ธฐ๋ฐ์ผ๋ก ๊ฒ์์ ์ํํ์ฌ ์ ์ฌ์ ์ผ๋ก ๋ ๊ด๋ จ์ฑ์ด ๋์ ๊ฒฐ๊ณผ๋ฅผ ์ ๊ณตํฉ๋๋ค.
Question Answer Advisor์ ๋ํด ์์๋ณด๊ธฐ
Retrieval Augmented Generation (RAG)
Retrieval Augmented Generation ๊ฐ์ด๋๋ฅผ ์ฐธ๊ณ ํ์ธ์.
Chat Memory
ChatMemory ์ธํฐํ์ด์ค๋ ์ฑํ ๋ํ ์ด๋ ฅ์ ์ ์ฅํ๊ธฐ ์ํ ์ ์ฅ์๋ฅผ ๋ํ๋ ๋๋ค. ์ด ์ธํฐํ์ด์ค๋ ๋ค์๊ณผ ๊ฐ์ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค:
- ๋ํ์ ๋ฉ์์ง๋ฅผ ์ถ๊ฐํ๋ ๋ฉ์๋
- ๋ํ๋ก๋ถํฐ ๋ฉ์์ง๋ฅผ ์กฐํํ๋ ๋ฉ์๋
- ๋ํ ์ด๋ ฅ์ ์ด๊ธฐํ(์ญ์ ) ํ๋ ๋ฉ์๋
ํ์ฌ ChatMemory์ ๊ตฌํ์ฒด๋ ๋ ๊ฐ์ง๊ฐ ์์ต๋๋ค:
- InMemoryChatMemory: ๋ฉ๋ชจ๋ฆฌ ๊ธฐ๋ฐ์ ํ๋ฐ์ฑ ์ ์ฅ ๋ฐฉ์
- CassandraChatMemory: TTL (์ ํจ ๊ธฐ๊ฐ)์ ๊ฐ์ง ์ง์์ฑ ์ ์ฅ ๋ฐฉ์
์๋ฅผ ๋ค์ด, ํ๋ฃจ ๋์ ์ ํจํ CassandraChatMemory ์ธ์คํด์ค๋ฅผ ์์ฑํ๋ ค๋ฉด ๋ค์๊ณผ ๊ฐ์ด ์์ฑํ ์ ์์ต๋๋ค:
CassandraChatMemory.create(
CassandraChatMemoryConfig.builder()
.withTimeToLive(Duration.ofDays(1))
.build()
);
๋ค์ Advisor ๊ตฌํ์ฒด๋ค์ ChatMemory๋ฅผ ํ์ฉํ์ฌ ๋ํ ์ด๋ ฅ์ ํ๋กฌํํธ์ ๋ฐ์ํฉ๋๋ค. ๊ฐ Advisor๋ ๋ฉ๋ชจ๋ฆฌ๋ฅผ ํ๋กฌํํธ์ ์ฝ์ ํ๋ ๋ฐฉ์์์ ์ฐจ์ด๊ฐ ์์ต๋๋ค:
- MessageChatMemoryAdvisor: ๋ํ ์ด๋ ฅ์ ๋ฉ์์ง์ ์ปฌ๋ ์ ํํ๋ก ํ๋กฌํํธ์ ์ถ๊ฐํฉ๋๋ค.
- PromptChatMemoryAdvisor: ๋ํ ์ด๋ ฅ์ ์์คํ ํ ์คํธ ์์ ์ฝ์ ํ์ฌ ํ๋กฌํํธ๋ฅผ ๋ณด๊ฐํฉ๋๋ค.
- VectorStoreChatMemoryAdvisor: ์์ฑ์์ ๋ค์๊ณผ ๊ฐ์ ๊ตฌ์ฑ ์์๋ฅผ ํฌํจํฉ๋๋ค.
VectorStoreChatMemoryAdvisor(
VectorStore vectorStore,
String defaultConversationId,
int chatHistoryWindowSize,
int order
)
์ด ์์ฑ์๋ฅผ ์ฌ์ฉํ๋ฉด ๋ค์๊ณผ ๊ฐ์ ์์ ์ ์ํํ ์ ์์ต๋๋ค.
- ๋ฌธ์๋ฅผ ๊ด๋ฆฌํ๊ณ ์ง์ํ๊ธฐ ์ํ VectorStore ์ธ์คํด์ค๋ฅผ ์ง์ ํ ์ ์์ต๋๋ค.
- ์ปจํ ์คํธ์ ๋ํ ID๊ฐ ์ ๊ณต๋์ง ์์์ ๊ฒฝ์ฐ ์ฌ์ฉํ ๊ธฐ๋ณธ ๋ํ ID๋ฅผ ์ค์ ํ ์ ์์ต๋๋ค.
- ๋ํ ์ด๋ ฅ์ ์กฐํํ ์๋์ฐ ํฌ๊ธฐ๋ฅผ ํ ํฐ ๋จ์๋ก ์ ์ํ ์ ์์ต๋๋ค.
- Chat Advisor ์์คํ ์์ ์ฌ์ฉํ ์์คํ ํ ์คํธ๋ฅผ ์ ๊ณตํ ์ ์์ต๋๋ค.
- ์ด Advisor์ ์ฒด์ธ ๋ด ์ฐ์ ์์ (์คํ ์์)๋ฅผ ์ค์ ํ ์ ์์ต๋๋ค.
VectorStoreChatMemoryAdvisor.builder() ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋ฉด ๋ค์ ํญ๋ชฉ๋ค์ ์ค์ ํ ์ ์์ต๋๋ค:
- ๊ธฐ๋ณธ ๋ํ ID (defaultConversationId)
- ๋ํ ์ด๋ ฅ์ ์กฐํํ ์๋์ฐ ํฌ๊ธฐ (ํ ํฐ ๋จ์)
- ๋ํ ์ด๋ ฅ์ ์กฐํ ์์ (order)
์ด์ ์ฌ๋ฌ Advisor๋ฅผ ์ฌ์ฉํ๋ ์์ @Service ๊ตฌํ์ด ์๋์ ๋์ต๋๋ค:
import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY;
import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY;
@Service
public class CustomerSupportAssistant {
private final ChatClient chatClient;
public CustomerSupportAssistant(ChatClient.Builder builder, VectorStore vectorStore, ChatMemory chatMemory) {
this.chatClient = builder
.defaultSystem("""
You are a customer chat support agent of an airline named "Funnair". Respond in a friendly,
helpful, and joyful manner.
Before providing information about a booking or cancelling a booking, you MUST always
get the following information from the user: booking number, customer first name and last name.
Before changing a booking you MUST ensure it is permitted by the terms.
If there is a charge for the change, you MUST ask the user to consent before proceeding.
""")
.defaultAdvisors(
new MessageChatMemoryAdvisor(chatMemory), // ๋ํ ๋ฉ๋ชจ๋ฆฌ ๋ณด๊ฐ
new QuestionAnswerAdvisor(vectorStore), // RAG ๋ณด๊ฐ
new SimpleLoggerAdvisor() // ๋ก๊น
)
.defaultFunctions("getBookingDetails", "changeBooking", "cancelBooking") // ํจ์ ํธ์ถ ์ค์
.build();
}
public Flux<String> chat(String chatId, String userMessageContent) {
return this.chatClient.prompt()
.user(userMessageContent)
.advisors(a -> a
.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)
.param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 100))
.stream()
.content();
}
}
Question Answer Advisor์ ๋ํด ์์๋ณด๊ธฐ
๋ก๊น (Logging)
SimpleLoggerAdvisor๋ ChatClient์ ์์ฒญ ๋ฐ ์๋ต ๋ฐ์ดํฐ๋ฅผ ๋ก๊ทธ๋ก ๊ธฐ๋กํ๋ Advisor์ ๋๋ค. ์ด ๊ธฐ๋ฅ์ AI์์ ์ํธ์์ฉ์ ๋๋ฒ๊น ํ๊ฑฐ๋ ๋ชจ๋ํฐ๋งํ ๋ ์ ์ฉํ๊ฒ ์ฌ์ฉํ ์ ์์ต๋๋ค.
ํ
Spring AI๋ LLM๊ณผ Vector Store ๊ฐ์ ์ํธ์์ฉ์ ๋ํ ๊ฐ์์ฑ (Obserability)์ ์ง์ํฉ๋๋ค.
์์ธํ ๋ด์ฉ์ Obserability ๊ฐ์ด๋๋ฅผ ์ฐธ๊ณ ํ์๊ธฐ ๋ฐ๋๋๋ค.
๋ก๊น ์ ํ์ฑํํ๋ ค๋ฉด ChatClient๋ฅผ ์์ฑํ ๋ SimpleLoggerAdvisor๋ฅผ Advisor ์ฒด์ธ์ ์ถ๊ฐํด์ผ ํฉ๋๋ค. ์ด Advisor๋ ์ฒด์ธ์ ๋ง์ง๋ง์ฏค์ ์ถ๊ฐํ๋ ๊ฒ์ด ๊ถ์ฅ๋ฉ๋๋ค.
ChatResponse response = ChatClient.create(chatModel).prompt()
.advisors(new SimpleLoggerAdvisor())
.user("Tell me a joke?")
.call()
.chatResponse();
๋ก๊ทธ๋ฅผ ํ์ธํ๋ ค๋ฉด application.properties ๋๋ application.yml ํ์ผ์ ๋ค์๊ณผ ๊ฐ์ด ๋ก๊น ๋ ๋ฒจ์ ์ค์ ํฉ๋๋ค:
logging.level.org.springframework.ai.chat.client.advisor=DEBUG
AdvisedRequest์ ChatResponse์์ ์ด๋ค ์ ๋ณด๋ฅผ ๋ก๊ทธ๋ก ์ถ๋ ฅํ ์ง ์ง์ ์ปค์คํฐ๋ง์ด์ง ํ ์๋ ์์ต๋๋ค. ๋ค์ ์์ฑ์๋ฅผ ์ฌ์ฉํฉ๋๋ค:
SimpleLoggerAdvisor(
Function<AdvisedRequest, String> requestToString,
Function<ChatResponse, String> responseToString
)
์ฌ์ฉ ์:
SimpleLoggerAdvisor customLogger = new SimpleLoggerAdvisor(
request -> "Custom request: " + request.userText(),
response -> "Custom response: " + response.getResult()
);
์ด๋ ๊ฒ ํ๋ฉด ๋ก๊น ๋๋ ๋ด์ฉ์ ์ํฉ์ ๋ง๊ฒ ์กฐ์ ํ ์ ์์ต๋๋ค.
ํ
์ด์ ํ๊ฒฝ์์๋ ๋ฏผ๊ฐํ ์ ๋ณด๋ฅผ ๋ก๊น ํ์ง ์๋๋ก ๊ฐ๋ณํ ์ฃผ์ํด์ผ ํฉ๋๋ค.
Reference
'๐ ๊ณต์ ๋ฌธ์ ๋ฒ์ญ > Spring AI (2025 Renewal)' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[Spring AI 2025 Renewal #4] Chat Models & Image Models (1) | 2025.04.12 |
---|---|
[Spring AI 2025 Renewal #3] Advisors API (0) | 2025.04.06 |
[Spring AI 2025 Renewal #1] Spring AI๋ ๋ฌด์์ธ๊ฐ? (0) | 2025.04.04 |