Skip to content

Commit d3fa3ec

Browse files
add post '[Spring AI] Structured Output using Function Calling'
1 parent 517227f commit d3fa3ec

File tree

3 files changed

+119
-10
lines changed

3 files changed

+119
-10
lines changed
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
---
2+
layout: "post"
3+
title: "[Spring AI] Structured Output using Function Calling"
4+
description:
5+
"Spring AI를 활용한 Function Calling을 통해 모델이 JSON 형식으로 구조화된 출력을 생성하는 방법을\
6+
\ 설명합니다. 예시 코드에서는 사용자의 선호에 맞는 도시 목록을 반환하는 함수를 정의하고, Spring AI의 BeanOutputConverter를\
7+
\ 이용해 자동으로 원하는 형태로 데이터를 매핑합니다. 이 과정은 안정적인 데이터 형식을 제공하여 API의 정상 작동을 보장합니다."
8+
categories:
9+
- "스터디-자바"
10+
tags:
11+
- "Spring"
12+
- "Spring AI"
13+
- "Structured Output"
14+
- "Function Calling"
15+
- "Agents"
16+
- "AI Agents"
17+
- "Json"
18+
- "BeanOutputConverter"
19+
date: "2025-03-17 00:00:00 +0000"
20+
toc: true
21+
image:
22+
path: "/assets/thumbnails/2025-03-17-spring-ai-function-calling.jpg"
23+
---
24+
25+
# Structured Output using Function Calling
26+
27+
[Google Agents Whitepapers](https://www.kaggle.com/whitepaper-agents) 를 보면 Function Calling 에 대해서 다음과 같이 설명하는 부분이 있다.
28+
29+
> With Function Calling, we can teach a model to format this output in a structured style (like JSON) that’s more convenient for another system to parse.
30+
31+
그와 함께 아래와 같은 예시를 보여준다.
32+
33+
```python
34+
def display_cities(cities: list[str], preferences: Optional[str] = None):
35+
"""Provides a list of cities based on the user's search query and preferences.
36+
Args:
37+
preferences (str): The user's preferences for the search, like skiing,
38+
beach, restaurants, bbq, etc.
39+
cities (list[str]): The list of cities being recommended to the user.
40+
Returns:
41+
list[str]: The list of cities being recommended to the user.
42+
"""
43+
return cities
44+
```
45+
46+
```python
47+
from vertexai.generative_models import GenerativeModel, Tool, FunctionDeclaration
48+
49+
model = GenerativeModel("gemini-1.5-flash-001")
50+
51+
display_cities_function = FunctionDeclaration.from_func(display_cities)
52+
tool = Tool(function_declarations=[display_cities_function])
53+
54+
message = "I’d like to take a ski trip with my family but I’m not sure where to go."
55+
56+
res = model.generate_content(message, tools=[tool])
57+
58+
print(f"Function Name: {res.candidates[0].content.parts[0].function_call.name}")
59+
print(f"Function Args: {res.candidates[0].content.parts[0].function_call.args}")
60+
61+
> Function Name: display_cities
62+
> Function Args: {'preferences': 'skiing', 'cities': ['Aspen', 'Vail', 'Park City']}
63+
```
64+
65+
이 코드를 Spring AI 에서 구현한다면 어떨까 생각이 들어 직접 구현해보았다.
66+
67+
## Spring AI 에서 적용시켜보기
68+
69+
참고: [Structured Output](https://docs.spring.io/spring-ai/reference/api/structured-output-converter.html)
70+
71+
Spring AI 에서는 손쉽게 Structured Ouput 으로 변환할 수 있도록 컨버터들을 제공해주고 있다.
72+
73+
이 부분을 Spring AI를 기준으로 변경해본다면 다음과 같이 변경해볼 수 있을 것이다.
74+
75+
```java
76+
CitiesByPreferences citiesByPreferences = ChatClient.builder(chatModel).build()
77+
.prompt()
78+
.user("I'd like to take a ski trip with my family but I'm not sure where to go.")
79+
.call()
80+
.entity(CitiesByPreferences.class);
81+
```
82+
83+
```java
84+
@JsonPropertyOrder({"preferences", "cities"})
85+
record CitiesByPreferences(String preferences, List<String> cities) {}
86+
```
87+
88+
실행시켜보면 다음과 같이 결과가 나온다.
89+
90+
```json
91+
{
92+
"preferences": "Family-friendly ski resorts with a variety of slopes and activities.",
93+
"cities": [
94+
"Aspen", "Colorado", "Park City", "Utah", "Vail", ...
95+
]
96+
}
97+
```
98+
99+
## 내부 동작
100+
101+
우리는 `entity()` 메소드를 통해 타겟 타입(`CitiesByPreferences.class`)만 제공해줬을 뿐인데, 자동으로 우리가 원하는 형태로 값이 채워져서 반환받았다.
102+
103+
그렇게 될 수 있었던 이유는 `Spring AI``BeanOutputConverter` 클래스 내부에서 타겟 타입에 대한 JSON 스키마를 만들어 prompt에 제공하기 때문이다.
104+
105+
그 결과, model은 structured output을 반환하고 Spring AI 에서는 나온 결과 값을 다시 object mapper 를 이용하여 객체에 매핑시킨다.
106+
107+
## 마무리
108+
109+
개발자에게는 안정된 형태의 데이터가 중요하다. 스키마가 다르면 API가 정상적으로 동작하지 않을 가능성이 높기 때문이다. 그런 의미에서 `structured output` 기능을 잘 사용한다면, 개발자에게 필요한 형태로 데이터를 추출할 수 있을 것이다.

_sass/_main.scss

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
}
1010

1111
body {
12-
font-family: Inter, SF Pro, Segoe UI, Roboto, Oxygen, Ubuntu, Helvetica Neue, Helvetica, Arial, sans-serif;
12+
font-family: Inter, SF Pro, Segoe UI, Roboto, Oxygen, Ubuntu, Helvetica Neue,
13+
Helvetica, Arial, sans-serif;
1314
-webkit-font-smoothing: antialiased;
1415
text-rendering: optimizeLegibility;
1516
line-height: 1.7;
@@ -151,8 +152,11 @@ h5 {
151152
}
152153

153154
section h1:first-child {
155+
font-size: 2.125em;
156+
font-weight: 800;
154157
margin-top: 0;
155-
line-height: 36px;
158+
margin-bottom: 0.5em;
159+
line-height: 1.5;
156160
}
157161

158162
strong,
@@ -361,12 +365,6 @@ figcaption {
361365
}
362366
}
363367

364-
.post > h1 {
365-
font-size: 2.125em;
366-
margin-bottom: .5em;
367-
line-height: 36px;
368-
}
369-
370368
.content {
371369
--content-heading-weight: var(--weight-extrabold);
372370
--content-heading-line-height: 1.125;
@@ -524,7 +522,9 @@ figcaption {
524522
}
525523
.content table tbody tr:last-child td,
526524
.content table tbody tr:last-child th {
527-
border-bottom-width: var(--content-table-body-last-row-cell-border-bottom-width);
525+
border-bottom-width: var(
526+
--content-table-body-last-row-cell-border-bottom-width
527+
);
528528
}
529529
.content .tabs li + li {
530530
margin-top: 0;
@@ -549,4 +549,4 @@ figcaption {
549549
--weight-semibold: 600;
550550
--weight-bold: 700;
551551
--weight-extrabold: 800;
552-
}
552+
}
1.46 MB
Loading

0 commit comments

Comments
 (0)