๐ ์ฐ๋ฆฌ๋ ์ ๋ฐ๋ช ์๋ฅผ ๋ง๋ค์ด์ผ ํ ๊น?

์ฌ์ฌํ ๋ ๋ฐ๋ช ์๊ฐ ๋์ ๋ํ๋ฅผ ํด ์ฃผ๋ฉด ์ฐธ ์ข๊ฒ ๋๋ฐ, ๋ฐ๋ช ์์๊ฒ ์น๊ตฌ๋น๋ฅผ ์ฃผ๊ธฐ์ ์ฃผ๋จธ๋ ์ฌ์ ์ด ๋๋ํ์ง ์๋ค….
ํ์ง๋ง ์ฐ๋ฆฌ์๊ฒ ์ฝ๋ฉ์ ํ ์ ์๋ ๋งฅ๋ถ๊ณผ ์ฝ๋ฉ ์ฒ์ฌ ์งํผํฐ๊ฐ ์์ผ๋, ๊ฐ์ง ๋ฐ๋ช ์๋ฅผ ๋ง๋ค์ด ์๊ธ์์กฑํ๋ฉด ๋๋ค ๐
๐ Embabel์ ๋ํด ์์๋ณด์
Embabel์๋จ์ํํ
์คํธ๋ฅผ์์ฑํ๋๊ฒ์๋์ด์ฌ์ฉ์๋ฅผ๋์ ํด์ถ๋ก ํ๊ณ ๋ชฉํ์งํฅ์ ์ผ๋กํ๋ํ๋ฉฐ๊ณํ์์ธ์ฐ๊ณ ๋๊ตฌ๋ฅผํธ์ถํ๋์ง๋ฅํ์์ด์ ํธ๋ฅผ๊ตฌ์ถํ๊ธฐ์ํJVM๊ธฐ๋ฐ์์์ด์ ํธํ๋ ์์ํฌ๋ก๊ฐ๋ฐ์๊ฐ๋ณต์กํ์ํฌํ๋ก์ฐ๋ฅผ์ผ์ผ์ด์ฝ๋ฉํ์ง์์๋์์ด์ ํธ๊ฐ์ค์ค๋ก๋ชฉํ๋ฅผ๋ฌ์ฑํ๊ธฐ์ํ์ต์ ์์ก์
์์๋ฅผ๊ฒฐ์ ํ๋๋ก๋๋๋ค.(์์ด๊ณ ์จ์ฐจ)
์ธ ์ค ์์ฝ์ ๋ ์์ฝํ์๋ฉด, ๋์ถฉ AI ์์ด์ ํธ ๊ฐ๋ฐ์ ์ํด ์ฌ์ฉํ๋ ํ๋ ์์ํฌ์ด๋ค.
โก๏ธ 1. GOAP(Goal-Oriented Action Planning)
GOAP๋ผ๋ ๋น-LLM ์๊ณ ๋ฆฌ์ฆ์ ์ฌ์ฉํด์ ๋ชฉํ ๋ฌ์ฑ์ ์ํ ์ต์ ์ ๊ฒฝ๋ก๋ฅผ ์ค์ค๋ก ์ฐพ๋ ๊ฒ์ด ํน์ง์ด๋ค. ๋ฐฑ์ค ์ค๋ฒ 3 ์ด์๋ถํฐ๋ง ์ฌ์ฉํ ์ ์๋ ํ๋ ์์ํฌ์ด๋ค.
์ดํด๋ฅผ ๋๊ธฐ ์ํด ์ค๋ช ์ ์ถ๊ฐํ์๋ฉด,
์๋ ์ฐ๋ฆฌ๋ ์ด๋ค ๋น์ฆ๋์ค ๋ก์ง์ ์คํ์ ์ํด ๋ค์๊ณผ ๊ฐ์ด ์ฝ๋๋ฅผ ์์ฑํ๋ค:
1. ์ฌ๊ณผ๋ฅผ ์ป์ด์ ์ค๋นํ๋ค.
2. ๋ฐ๋๋๋ฅผ ์ป์ด์ ์ค๋นํ๋ค.
3. ๊ผฌ์น๋ฅผ ์ค๋นํ๋ค.
4. ๊ผฌ์น์ ์ฌ๊ณผ์ ๋ฐ๋๋๋ฅผ ๊ฝ๋๋ค.
5. ์คํ์ ๋ น์ธ๋ค.
6. ๋ น์ ์คํ์ ๊ผฌ์น์ ์ง์ ํด์ ํํ๋ฃจ๋ฅผ ํ๋ โจ
๊ทธ๋ฐ๋ฐ ์ด ํ๋ ์์ํฌ๋ฅผ ์ฌ์ฉํ๊ฒ ๋๋ฉด ์ฝ๋๋ฅผ ์ด๋ ๊ฒ ์ง๊ฒ ๋๋ค:
์ต์ข ๋ชฉํ: ํํ๋ฃจ ํ๋ โจ
ํ์ฉ๋ ํ๋ ๋ชฉ๋ก
- ์ฌ๊ณผ ์ป์ด์ ์ค๋นํ๊ธฐ (์ค๋น๋ฌผ: ์์, ๊ฒฐ๊ณผ๋ฌผ: ๋ฝ๋๋ฝ๋ ์ฌ๊ณผ)
- ๋ฐ๋๋ ์ป์ด์ ์ค๋นํ๊ธฐ (์ค๋น๋ฌผ: ์์, ๊ฒฐ๊ณผ๋ฌผ: ๋ฝ๋๋ฝ๋ ๋ฐ๋๋)
- ๊ผฌ์น ์ค๋นํ๊ธฐ (์ค๋น๋ฌผ: ์์, ๊ฒฐ๊ณผ๋ฌผ: ๊ทธ๋ฅ ๊ผฌ์น)
- ๊ผฌ์น์ ์ฌ๊ณผ๋ ๋ฐ๋๋ ๊ฝ๊ธฐ (์ค๋น๋ฌผ: ๋ฝ๋๋ฝ๋ ์ฌ๊ณผ, ๋ฝ๋๋ฝ๋ ๋ฐ๋๋, ๊ทธ๋ฅ ๊ผฌ์น, ๊ฒฐ๊ณผ๋ฌผ: ์ต๊ฐ ๊ผฌ์น)
- ์คํ ๋ น์ฌ์ ์คํ๋ฌผ ๋ง๋ค๊ธฐ (์ค๋น๋ฌผ: ์์, ๊ฒฐ๊ณผ๋ฌผ: ์คํ๋ฌผ)
- ์ต๊ฐ ๊ผฌ์น์ ์คํ๋ฌผ์ ์ง์ ํด์ ํํ๋ฃจ๋ฅผ ํ๋ โจ (์ค๋น๋ฌผ: ์ต๊ฐ ๊ผฌ์น, ์คํ๋ฌผ, ๊ฒฐ๊ณผ๋ฌผ: ํ๋ ํํ๋ฃจ)
์์ ์ ๋ ฌ์ ๋์๊ฐ ์์… (ํ์ง๋ง GOAP๋ ๋ ๋ณต์กํ ๊ฒฝ๋ก ํ์์ ์ํด ๋จ์ํ ์์ ์ ๋ ฌ์ ๋์ด, ํ๋์ ๋น์ฉ๊ณผ ์ฑ๊ณต ํ๋ฅ ๊น์ง ๊ณ์ฐํด ์ต์ ์ ๊ฒฝ๋ก๋ฅผ ์ฐพ๋๋ค)
์ด๋ ๊ฒ ์์ฑ์ ํ๋ฉด embabel์ด GOAP ์๊ณ ๋ฆฌ์ฆ์ ์ฌ์ฉํด, ์์์ ์ด๋ค ํ๋์ ์คํํ ์ง, ๊ทธ๋ฆฌ๊ณ ๊ทธ ํ๋๋ค์ ์ด๋ค ์์๋ก ์คํํด์ ์ต์ข ๋ชฉํ์ธ ํํ๋ฃจ ํ๋์ ๋์ถํ ์ง ๊ฒฝ๋ก๋ฅผ ์ค์ค๋ก ์ฐพ๋ ๊ตฌ์กฐ์ด๋ค. ์ด ๊ฒฝ๋ก ์ถ๋ก ์ ํต์ฌ์ด ๋๋ ๊ฒ์ ๊ฐ ํ๋์ ์ ๋ ฅ๊ณผ ์ถ๋ ฅ ํ์ ์ด๋ค, ์ ์์์์ ๋ณธ๋ค๋ฉด '๊ผฌ์น์ ์ฌ๊ณผ๋ ๋ฐ๋๋ ๊ฝ๊ธฐ' ๋ผ๋ ํ๋์๋ ๋ฝ๋๋ฝ๋ ์ฌ๊ณผ, ๋ฝ๋๋ฝ๋ ๋ฐ๋๋, ๊ทธ๋ฅ ๊ผฌ์น๊ฐ ํ์ํ๋ ์ด ์ค๋น๋ฌผ๋ค์ ๋ง๋ค์ด ์ฃผ๋ ํ๋์ ๋จผ์ ์ํํ๋๋ก ์์๋ฅผ ์ ํ๋ ์์ด๋ค ๐
(์ ๊น ์ฟ ํค๋ฐ ์ค๋ธ๋ธ๋ ์ดํฌ ์์ ํ์๋ฉด, ์ด ๊ฒ์์๋ ์งฑ๊ท์ฌ์ด ๊ฐ๊ทคํํ๋ฃจ๋ง์ฟ ํค๊ฐ ์์ต๋๋ค ๐๐ ์ฟ ์ค๋ธ ์ ์ ๋ ์์ ์ณ์ฃผ์ธ์ ์น๊ตฌ์ฝ๋ ๋ค๊ณ ๋ ์๊ฐ๊ฒ์ ๐คธโ๏ธ)

๊ทธ๋ฐ๋ฐ ์์งํ ์ฝ๋๋ฅผ ์ง๋ ์ ์ฅ์์๋ ์ ์์ ๋ฐฉ์์ด ํจ์ฌ ์ง๊ด์ ์ด๊ณ ํ๋ฆ๋ ํ์ ํ๊ธฐ ์ฝ๊ณ , ๊ตณ์ด ์ด GOAP ์๊ณ ๋ฆฌ์ฆ์ ๋์ ํด ํ๋ ์์ํฌ์์ ์ฝ๋์ ์คํ์ ์ ์ดํ๋ ๊ฒ์ด ๋ฌด์จ ์ฅ์ ์ด ์๋ค๋ ๊ฑด์ง ์ฒ์์๋ ๋ฉ๋์ด ์ ๋๋ค ๐
๊ทธ๋ฌ๋ค ์๊ฒ ๋ ์ฌ์ค์, ์๋ ๊ณํ(GOAP)์ ์ง๊ฐ๋ ์์ธ ์ํฉ์์ ๋ํ๋๋ค๋ ๊ฒ์ด๋ค.
๋ง์ฝ์ ์ฐ๋ฆฌ๊ฐ ์ด๋ ๋ ๊ฐ์๊ธฐ ๋ฑ์ด๊ฐ ๋์ด ๋ฒ๋ ธ๋ค๊ณ ๊ฐ์ ํด ๋ณด์:

์๋ฌด๋๋ ํ ์์ ์๊ฐ๋ฝ์ด ๋ค์ฏ ๊ฐ์์ 0๊ฐ๋ก ๋ฐ๋๋ค๋ฉด ๊ผฌ์น์ ์ฌ๊ณผ๋ ๋ฐ๋๋๋ฅผ ๊ฝ๋ค๊ฐ ๋ฐ๋ฅ์ ๋จ์ดํธ๋ฆฌ๋ ์ผ์ด ์์ฃผ ์๊ธธ ๊ฒ์ด๋ค. ๊ทธ๋ฌ๋ฉด ์ด๋ฐ ํน๋ฐ๋ ์ฝ๋๊ฐ ํ์ํ๋ค:
public Tanghulu makeTanghulu() {
Apple apple = null;
Banana banana = null;
Skewer skewer = null;
SkewerWithFruit fruitSkewer = null;
SugarWater sugarWater = null;
// ์ฌ๊ณผ ์ค๋น
while (apple == null) {
try {
apple = washApple();
} catch (AppleDroppedException e) {
System.out.println("์ฌ๊ณผ ๋จ์ด์ง! ๋ค์ ์ป๋๋ค.");
}
}
// ๋ฐ๋๋ ์ค๋น
while (banana == null) {
try {
banana = washBanana();
} catch (BananaDroppedException e) {
System.out.println("๋ฐ๋๋ ๋จ์ด์ง! ๋ค์ ์ป๋๋ค.");
}
}
// ๊ผฌ์น ์ค๋น
skewer = prepareSkewer();
// ๊ณผ์ผ ๊ฝ๊ธฐ (์ฌ๊ธฐ์ ๋จ์ด์ง ์ ์์)
while (fruitSkewer == null) {
try {
fruitSkewer = stickFruit(skewer, apple, banana);
} catch (FruitDroppedException e) {
System.out.println("๊ณผ์ผ ๋จ์ด์ง! ๋ค์ ์ค๋น");
// ๋จ์ด์ง ๊ณผ์ผ ๋ค์ ์ค๋น
apple = null;
banana = null;
while (apple == null) {
try {
apple = washApple();
} catch (AppleDroppedException ignore) {}
}
while (banana == null) {
try {
banana = washBanana();
} catch (BananaDroppedException ignore) {}
}
}
}
// ์คํ ๋
น์ด๊ธฐ
sugarWater = meltSugar();
// ์ต์ข
ํํ๋ฃจ
return coatSugar(fruitSkewer, sugarWater);
}
ํต์ฌ ๋ฌธ์ ๋ ์ ์ฐจํ ์ฝ๋์์๋ ์ํ ๋ณต๊ตฌ ๋ก์ง์ด ๋ณต์กํด์ง๋ ๊ฒ์ด๋ค. ๊ทธ๋ฆฌ๊ณ ๋์ฐํ๊ฒ๋ ์คํ์ด ํ๊ฑฐ๋, ๊ผฌ์น๊ฐ ๋ถ๋ฌ์ง๊ฑฐ๋, ๋ฐ๋๋ ๊ป์ง์ ๊น ๋๋ ์ฉ์ ๋ฐ๋๋๊ฐ ๋์จ ๊ฒ๊ณผ ๊ฐ์ ๋ค์ํ ์คํจ ์ผ์ด์ค๋ค์ ์์ง ์ฝ๋์ ์ ์ฉํ์ง๋ ์์ ์ํ์ด๋ค.
๋ฌด์๋ณด๋ค ์ฐ๋ฆฌ๊ฐ ํ ์์ด์ ํธ ๊ฐ๋ฐ์ ํต์ฌ์ด ๋๋ AI๋… ํด๋ง ์ด๋ชจ์ง๋ฅผ ๋ณด์ฌ ๋ฌ๋ผ๊ณ ํ๋ฉด ๊ณ ์ฅ๋๋ ์น๋ช ์ ์ธ ๋ฒ๊ทธ๊ฐ ์๋ค ๐คฆโ๏ธ๐คฆโ๏ธ

๊ฐ๋ณต์น ChatGPT ํ๋๋ง ๋ด๋ ์ ์ ์๋ฏ, ์์ด์ ํธ ๊ฐ๋ฐ์ ์์ด ํ๋ณต ํ๋ ฅ์ฑ์ ํธ๋ค๋งํ๋ ๊ฒ์ ๋งค์ฐ ์ค์ํ๋ค.
๐ค 2. Embabel์ ํ๋ณต ํ๋ ฅ์ฑ์ ์ด๋์์ ์ค๋๊ฐ
Embabel์๋ ์์ด์ ํธ ํ๋ก์ธ์ค๊ฐ ์คํ๋๋ ๋์ ์ํ๋ฅผ ์ ์งํ๊ณ ๋ฐ์ดํฐ๋ฅผ ๊ณต์ ํ๋ ๋ธ๋๋ณด๋๋ผ๋ ์ค์ ์ ์ฅ์ ๊ฐ์ ๊ณต๊ฐ์ด ์๋ค. ์ค๋น๊ฐ ์๋ฃ๋ ๋ฝ๋๋ฝ๋ ์ฌ๊ณผ๋ ๋ฝ๋๋ฝ๋ ๋ฐ๋๋๋ฅผ ์ ์ฅํด ๋๊ณ ์๋ ๋ณด๊ดํจ ์ ๋๋ก ์๊ฐํ๋ฉด ๋๋ค! ๋ฐ๋๋๊ฐ ์ฉ์๊ฑฐ๋ ๊ผฌ์น๋ฅผ ๋จ์ดํธ๋ฆฌ๋ฉด, ์ด ๋ณด๊ดํจ์์ ์ค๋น๋ฌผ์ด ๋ถ์กฑํด์ง ๊ฒ์ Embabel์ด GOAP๋ฅผ ํตํด ํ์ ํ ์ ์๋ ๊ตฌ์กฐ์ธ ๊ฒ์ด๋ค.
Embabel์ ๋ธ๋๋ณด๋์ GOAP๋ฅผ ๊ฒฐํฉํ์ฌ ์๋์ ๊ฐ์ด ๋ฃจํ๋ฅผ ํ์ฑํ๋ค:
- Observe (๊ด์ฐฐ): ํ์ฌ ๋ธ๋๋ณด๋์ ์ํ์ ์ด์ ์ก์ ์ ๊ฒฐ๊ณผ๋ฅผ ํ์ธํ๋ค.
- Orient (๋ฐฉํฅ ์ค์ ): ๋ง์ง๋ง ๊ณํ ์๋ฆฝ ์ดํ ๋ฌด์์ด ๋ณํ๋์ง ํ์ ํ๋ค.
- Decide (๊ฒฐ์ ): GOAP ํ๋๋๊ฐ ์๋ก์ด ์ ๋ณด๋ฅผ ๋ฐํ์ผ๋ก ๊ณํ์ ์๋ฆฝํ๊ฑฐ๋ ์ ๋ฐ์ดํธํ๋ค.
- Act (์คํ): ๊ณํ๋ ๋ค์ ์ก์ ์ ์คํํ๊ณ ๊ฒฐ๊ณผ๋ฅผ ๋ธ๋๋ณด๋์ ๋ฐ์ํ๋ค.
๐จ "์์ธ ์ํฉ์์ ์ง๊ฐ๊ฐ ๋ํ๋๋ค"๋ ๋ฌธ์ฅ์ ์คํด์ ์์ง๊ฐ ์์ ๊ฒ ๊ฐ์ ๋ง๋ถ์ด์๋ฉด, ์ค์ ๋ก Embabel์ ์๋ฌ๊ฐ ๋ฌ์ ๋๋ง ๋ค์ ๊ณํ์ ์ธ์ฐ๋ ๊ฒ์ด ์๋๋ผ ๋งค ํ๋์ด ๋๋ ๋๋ง๋ค ๋ฃจํ๋ฅผ ๋๋ฉฐ ์ํ๋ฅผ ์ฌํ๊ฐํ๋ค.
๐ Embabel ๊ธฐ์ด ์๊ธฐ
embabel์ ํ์ฉํด ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์ด ๋ณด๊ณ ์ถ์ ๊ฒฝ์ฐ์๋ ์๋ ์์ repository๋ฅผ cloneํ๋ฉด ๋๋ค:
https://github.com/embabel/embabel-agent-examples
GitHub - embabel/embabel-agent-examples: Agent Framework Examples for Java and Kotlin Developers
Agent Framework Examples for Java and Kotlin Developers - embabel/embabel-agent-examples
github.com
์๋๋ Embabel ๋ฌธ์์ ์๋ Agent ํด๋์ค์ ์์ ์ฝ๋์ด๋ค.
@Agent(description = "Find news based on a person's star sign")
public class StarNewsFinder {
private final HoroscopeService horoscopeService;
private final int storyCount;
public StarNewsFinder(
HoroscopeService horoscopeService,
@Value("${star-news-finder.story.count:5}") int storyCount) {
this.horoscopeService = horoscopeService;
this.storyCount = storyCount;
}
@Action
public StarPerson extractStarPerson(UserInput userInput, OperationContext context) {
return context.ai()
.withLlm(OpenAiModels.GPT_41)
.createObject("""
Create a person from this user input, extracting their name and star sign:
%s""".formatted(userInput.getContent()), StarPerson.class);
}
@Action
public Horoscope retrieveHoroscope(StarPerson starPerson) {
// Uses regular injected Spring service - not LLM
return new Horoscope(horoscopeService.dailyHoroscope(starPerson.sign()));
}
@Action
public RelevantNewsStories findNewsStories(
StarPerson person, Horoscope horoscope, OperationContext context) {
var prompt = """
%s is an astrology believer with the sign %s.
Their horoscope for today is: %s
Given this, use web tools to find %d relevant news stories.
""".formatted(person.name(), person.sign(), horoscope.summary(), storyCount);
return context.ai().withDefaultLlm()
.withToolGroup(CoreToolGroups.WEB)
.createObject(prompt, RelevantNewsStories.class);
}
@AchievesGoal(description = "Write an amusing writeup based on horoscope and news")
@Action
public Writeup writeup(
StarPerson person, RelevantNewsStories stories, Horoscope horoscope,
OperationContext context) {
var llm = LlmOptions.fromCriteria(ModelSelectionCriteria.getAuto())
.withTemperature(0.9);
var storiesFormatted = stories.items().stream()
.map(s -> "- " + s.url() + ": " + s.summary())
.collect(Collectors.joining("\n"));
var prompt = """
Write something amusing for %s based on their horoscope and news stories.
Format as Markdown with links.
<horoscope>%s</horoscope>
<news_stories>
%s
</news_stories>
""".formatted(person.name(), horoscope.summary(), storiesFormatted);
return context.ai().withLlm(llm).createObject(prompt, Writeup.class);
}
}
@Agent
Agent๋ ํน์ ๋ชฉํ๋ฅผ ๋ฌ์ฑํ๊ธฐ ์ํด ๋๋ฉ์ธ ๋ก์ง, AI ์ญ๋, ๋๊ตฌ ์ฌ์ฉ์ ํ๋๋ก ๋ฌถ์ ๋ ๋ฆฝ์ ์ธ ๊ตฌ์ฑ ์์๋ฅผ ์ ์ํ ๋ ์ฌ์ฉ๋๋ค. ํด๋์ค ๋ ๋ฒจ์ ์ด ์ด๋ ธํ ์ด์ ์ด ๋ถ์ ๊ฒฝ์ฐ, ํด๋น ํด๋์ค๊ฐ multi-step flow๋ฅผ ์ํํ ์ ์๋ ์์ด์ ํธ์์ ํ๋ ์์ํฌ์ ์๋ฆฌ๋ ์ญํ ์ ํ๋ค. @Agent ์์ฑ ์ description ํ๋ผ๋ฏธํฐ๋ ํ์๋ก ์ ๊ณต๋์ด์ผ ํ๋๋ฐ, ์ด๋ ์ฌ๋์ด ์ฝ๊ธฐ ์ํ ์ฉ๋์ผ ๋ฟ๋ง ์๋๋ผ, LLM์ด ์ฌ์ฉ์์ ์ ๋ ฅ์ ๊ฐ์ฅ ์ ํฉํ ์์ด์ ํธ๋ฅผ ์ ํํ๊ฑฐ๋ ์์๋ฅผ ๋งค๊ธธ ๋ ์ค์ํ ๊ธฐ์ค์ผ๋ก ์ฌ์ฉ๋๋ค.
+) Spring Component ์ด๋ ธํ ์ด์ ์ ํฌํจํ๊ณ ์์ด์ ์์์ ๋น์ผ๋ก ๋ฑ๋ก๋๋ค
@Action
์์ด์ ํธ ๋ด๋ถ์ ์ฌ๋ฌ public ๋ฉ์๋์ @Action ์ด ๋ถ์ด ์๋ ๊ฒ์ ํ์ธํ ์ ์๋ค. ์ด๊ฒ ์์ ์์์์ ์ธ๊ธํ ํ๋ ๋ชฉ๋ก๋ค์ด๋ค. @Action์ ์์ด์ ํธ๊ฐ ๋ชฉํ๋ฅผ ๋ฌ์ฑํ๊ธฐ ์ํด ์ทจํ ์ ์๋ ๊ฐ๋ณ์ ์ธ ๋จ๊ณ ๋๋ ๊ธฐ๋ฅ์ ์ ์ํ๋ ์ญํ ์ ํ๋ค. ์ด ์ก์ ๋ฉ์๋์ ํ๋ผ๋ฏธํฐ์ ๋ฐํ๊ฐ์ ํ๋ ์์ํฌ๊ฐ ์คํ ๊ณํ์ ์ธ์ฐ๋ ํต์ฌ ์งํ๊ฐ ๋๋ค.

์ค์ ํ ์ ์๋ ํ๋ผ๋ฏธํฐ๊ฐ ๊ต์ฅํ ๋ค์ํ๋ฐ ์ผ๋ฌด์ง๊ฒ ์จ๋จน์ผ๋ฉด ๋ ๋ฉ์ง ์์ด์ ํธ๊ฐ ๋ง๋ค์ด์ง์ง๋…?
Non-LLM Action
@Action
public Horoscope retrieveHoroscope(StarPerson starPerson) {
// Uses regular injected Spring service - not LLM
return new Horoscope(horoscopeService.dailyHoroscope(starPerson.sign()));
}
retrieveHoroscope ๋ฉ์๋๋ฅผ ์ฝ์ด๋ณด๋ฉด ์ ์ ์๋ฏ, ๋ชจ๋ Action์ ai์ ๋ํ ํธ์ถ์ด ์์ด์ผ ํ๋ ๊ฒ์ ์๋๋ค. ์ด๋ฐ ์์ผ๋ก ์ ํต์ ์ธ ๋ฐฉ์์ ๊ฐ๋ฐ ๋ฐฉ์๊ณผ ai๋ฅผ ์ด์ฉํ ๊ฐ๋ฐ์ ์ ์ ํ๊ฒ ์ ์กฐํฉํ ์ ์๋ค๋ ๊ฒ์ ๋๋ฆ์ ์ฅ์ ์ผ๋ก ๊ผฝ์ ์ ์๋ค ๐
Multi-Input ์์กด์ฑ
@Action
public RelevantNewsStories findNewsStories(
StarPerson person, Horoscope horoscope, OperationContext context) { }
@AchievesGoal(description = "Write an amusing writeup based on horoscope and news")
@Action
public Writeup writeup(
StarPerson person, RelevantNewsStories stories, Horoscope horoscope,
OperationContext context) { }
findNewsStories ๋ฉ์๋๋ฅผ ๋ณด๋ฉด StarPerson๊ณผ Horoscope๋ฅผ ์ ๋ ฅ์ผ๋ก ๋ฐ๋๋ฐ input์ด ๋ช ๊ฐ๊ฐ ๋๋ ๊ฐ๋ฅํ ์คํ ๊ฒฝ๋ก๊ฐ ์๋ค๋ฉด Embabel์ด ์ ๊ตํ๊ฒ ์ค์ผ์คํธ๋ ์ด์ ์ ํด ์ค๋ค. ์ ๋ ฅ ๋ณต์ก๋ ์ต๊ณ ๋ด์ writeup๋ฉ์๋์ด๋ค, ๋ธ๋๋ณด๋ + GOAP๋ฅผ ์ง๋ Embabel์ ๋ฌด์ ์ด์ ๐ซจ
๋ฐ๋ปํ ์์ด์ค ์๋ฉ๋ฆฌ์นด๋ ธ ํ ์ ์ฃผ์ธ์
๋๋๊ฒ๋ ๊ทธ๋ฐ ์ฃผ๋ฌธ์ด Embabel์์๋ ์ ๋ง๋ก ๊ฐ๋ฅํ๋ค๋๋ฝ….
var llm = LlmOptions.fromCriteria(ModelSelectionCriteria.getAuto())
.withTemperature(0.9);
withTemperature๋ก ai ์๋ต์ ๋ํ ์ต์ ์ ์ค ์ ์๋๋ฐ, ์จ๋๊ฐ ๋์์๋ก ์กฐ๊ธ ๋ ์ฐฝ์์ ์ธ ๋ต๋ณ์ ์ค๋ค๊ณ ํ๋ค.
์๋ ๋ ์ค ํ๋๋ ์จ๋๋ฅผ 0.1๋ก ์ค์ ํ ๊ฑฐ๊ณ ํ๋๋ 1.0์ผ๋ก ์ค์ ํ๊ณ ๋์ผํ ์ง๋ฌธ์ ํ ๊ฒ์ด๋ค.


+) ๋ฒ์ธ, AI๋ ๋ฐ๋ปํด๋ ์ฐจ๊ฐ๋ค

1+1์ ๋ฌผ์ด๋ดค์ ๋ ๋น์ฐํ ์ฐฝ์์ ์ธ ์ชฝ์ ๊ท์๋ฏธ๋ผ๊ณ ๋๋ตํด ์ค ๊ฒ์ ๊ธฐ๋ํ๋๋ฐ… ๋์ด ๋๊ฐ์ด ๋๋ตํด์ ๊ต์ฅํ ์ค๋ง์ค๋ฌ์ ๋ค (๋ด ํ ํฐ ๋๋ ค์ค) ์ธ๊ฐ์ด ์๊ฐํ๋ ๊ฒ๋ณด๋ค๋ ๋ ์ฐฝ์์ ์ธ ๊ฒ ๊ฐ๋ค.
@AchievesGoal
์ด๊ฑด Agent์ ์ต์ข ๋ชฉํ๋ฅผ ์ ์ํ๋ ํต์ฌ ์ด๋ ธํ ์ด์ ์ผ๋ก, ์๋ ์์ด ๋ถ์ ๋ฉ์๋๋ ํด๋น ์์ด์ ํธ๊ฐ ๋ฌ์ฑํด์ผ ํ ๋ชฉ์ ์ง์ ๋๋ฌํ์์ ํ๋ ์์ํฌ์๊ฒ ์๋ฆฌ๋ ์ญํ ์ ํ๋ค.
ํน์๋ ๊ฐ๋ฐํ๋ค ๋ชฉํ ์ง์ ์ ๋นผ๋จน์๊น๋ด ๊ฑฑ์ ํ์ง ์์๋ ๋๋ค. ์๋ํ๋ฉด

์ ๋ฃ์ผ๋ฉด Embabel์ด ์ด์ฌํ ํ๋ด์ค๋ค ๐ฅ
๋ค๊ฐ ๋๊ตฌ๊น์ง ์ฌ์ฉํ๋ฉด ๋ด ๋ฐฅ๊ทธ๋ฆ์…
์ฌํ๊ฒ๋ ๋ง์น๋ฅผ ์ฅ์ฌ์ค๋ ๋จธ๋ฆฌ๋ง ๊ธ์ ๊ฑฐ๋ฆฌ๋ ์์ญ์ด๋ค๊ณผ ๋ค๋ฅด๊ฒ, AI์๊ฒ๋ ๋ง์น๋ผ๋ ๋๊ตฌ๋ฅผ ์ฃผ๊ณ ๊ทธ ์ฉ๋๋ฅผ ์๋ ค์ฃผ๋ฉด ๋๋ฑ๋๋ฑ ์ง์ ์ง๋๋ค.

๊ทธ๋ฌ๋ ์ง๊ธ๋ถํฐ ๋ฏธ๋ฆฌ๋ฏธ๋ฆฌ ๊ธฐ๊ฐ์ ์ก์๋๋๋ก ํ์ :)
return context.ai().withDefaultLlm()
.withToolGroup(CoreToolGroups.WEB)
.createObject(prompt, RelevantNewsStories.class);
์๋ฌดํผ ์ฝ๋ ๋ด์์๋ ์ด๋ฐ ์์ผ๋ก ์ฌ์ฉํ ๋๊ตฌ๋ฅผ ์ง์ ์ง์ ํด ์ค ์๋ ์๋ค.
UserInput
Embabel์์๋ UserInput ํด๋์ค๋ฅผ ๋ค์๊ณผ ๊ฐ์ด ์ค๋ช ํ๋ค.

UserInput์ ํ๋ ์์ํฌ๊ฐ ์ด ๋ฐ์ดํฐ๋ ์ฌ์ฉ์๋ก๋ถํฐ ์จ ์ฒ๋ฆฌ๋์ง ์์ ์์ ์ ๋ ฅ์ด๋ผ๋ ๊ฒ์ ์ธ์ํ๊ฒ ํ๋ ํต์ฌ ํ์ ์ด๋ค.
๐ฉ ์์ ๊ณ ๋ฏผ ํฌ์ธํธ: ์ด๋ฏธ Request DTO๊ฐ ์๋๋ฐ ๊ตณ์ด UserInput ํ์ ์ผ๋ก ๋๊ฒจ์ค์ผ ํ ๊น?
์ฌ์ค ์ฝ๋๋ฅผ ์์ฑํ๋ ๋น์์๋ ๋ณ์๊ฐ ์์ด ์์์ ์๋ Agent์ ๋์ผํ๊ฒ UserInput์ ๋ฃ์ด์ฃผ๋ ์์ผ๋ก ๊ตฌํ์ ํ๋๋ฐ, Shell ๋ชจ๋๋ก ๊ตฌ๋ํ๋ Agent์ธ ๊ฒฝ์ฐ์๋ ์ฌ์ฉ์์ ์ ๋ ฅ์ด ๊ณ ์ ๋ ํ ์คํธ์ด๊ณ ์ด๋ค Agent๊ฐ ํ์ํ ์ง LLM์ด ์ง์ ๊ฐ์ ํ์ฌ ํ๋จํ๋ ๊ณผ์ ์ด ํ์ํ๊ธฐ ๋๋ฌธ์ UserInput์ผ๋ก ๋ฃ๋ ๊ฒ์ด ์ ์์ด์์ง๋ง, ์น์ผ๋ก ๊ตฌํํ ์๋น์ค์ Agent๋ฅผ ๋ถ์ผ ๋๋ Input์ ๊ท๊ฒฉ์ด ๋ฑ ์ ํด์ ธ ์์ด์ ๊ตณ์ด UserInput์ ๋๊ฒจ์ค์ผ ํ๋? ํ๋ ์๋ฌธ์ด ๋ค์๋ค.
UserInput์ UserContent๋ฅผ ์์ํ๊ธฐ ๋๋ฌธ์ ํ๋ ์์ํฌ๊ฐ ๊ฐ๋๋ ์ผ์ ์ ์ฉํ ๋, ์ฌ์ฉ์๋ก๋ถํฐ ์จ ์ ๋ ฅ์ ๋ํด์๋ง ์ ํด์ฑ ๊ฒ์ฌ๋ฅผ ์ํํ๋๋ก ํธ๋ฆฌ๊ฑฐํ๋ ๊ธฐ์ค์ด ๋๋ค. ๊ทธ๋ฌ๋๊น UserInput์ผ๋ก ๋ฃ์ด์ฃผ๋ฉด ๊ฐ๋ฐ์๊ฐ ๊ฐ๊ณต์ ํตํด ๋ฃ์ ๊ฐ์ด ์๋ ์์ ์ธ๋ถ ์ฌ์ฉ์์ ์ ๋ ฅ์์ ๋ช ์ํ๊ณ ๊ฒ์ฆ์ด๋ ์ ์ ๋ฅผ ๊ฑฐ์น๋๋ก ์ ๋ํ ์ ์๋ ๊ฒ์ด๋ค.
๊ทธ๋ฌ๋ ๋ณ๋ค๋ฅธ ์ฒ๋ฆฌ๊ฐ ํ์ํ ๊ฒ์ด ์๋๋ผ๋ฉด AgentInvocation.invoke(myRequestDto)์ ๊ฐ์ ์์ผ๋ก dto๋ฅผ ์ง์ ์ ๋ ฅํด๋ ๋ฌธ์ ๊ฐ ๋๋ ๊ฒ์ ์๋๋ค. ๋ค๋ง, ์ถ์ธก์ ํด ๋ณด์๋ฉด Shell๋ชจ๋์ ๊ฒฝ์ฐ ์ผ๋ฐ์ ์ผ๋ก UserInput์ด ์ง์ ์ ์ด ๋๊ธฐ ๋๋ฌธ์ ์ฒ์๋ถํฐ UserInput์ ๋ฐ๋๋ก ์์ด์ ํธ๋ฅผ ๊ฐ๋ฐํ๋ฉด Shell๊ณผ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ชจ๋ ํธํ๋์ด ์ฌ์ฌ์ฉ์ด ๊ฐ๋ฅํ๋ค๋ ์ฅ์ ์ด ์์ ๊ฒ ๊ฐ๋ค.
| ๊ตฌ๋ถ | UserInput | requestDto ์ง์ ์ ๋ ฅ |
| ์ ๋ ฅ ํํ | ์์ ๋ก์ด ํ ์คํธ, ์ค๋ช , ๋ฉ์์ง | ํ๋๊ฐ ์ ํด์ง ํผ ๋ฐ์ดํฐ, API ์์ฒญ |
| ์์ด์ ํธ ์ญํ | ํ ์คํธ ๋ถ์ ๋ฐ ์ ๋ณด ์ถ์ถ (DICE) | ์ถ์ถ๋ ์ ๋ณด๋ฅผ ๋ฐํ์ผ๋ก ์ ๋ฌด ์ํ |
| ์ฃผ์ ์ฅ์ | OODA ๋ฃจํ์ ์ฒซ ๋จ๊ณ๋ฅผ ์ ์ฐํ๊ฒ ์์ | ๋ถํ์ํ LLM ์ถ๋ก ๋น์ฉ ์ ๊ฐ ๋ฐ ์ ํ๋ ํฅ์ |
| ๋ณด์ | UserContent ๊ธฐ๋ฐ ๊ฐ๋๋ ์ผ ์๋ | ๋ด๋ถ ๋น์ฆ๋์ค ๋ก์ง์ผ๋ก ์ง์ ์ฃผ์
|
์์ฝํ์๋ฉด ์ด๋ฏธ ๋ฐ์ดํฐ๊ฐ ๊ตฌ์กฐํ๋์ด ์๋ค๋ฉด ์ง์ ๋๊ฒจ๋ ๋์ง๋ง, ์ฌ์ฉ์๊ฐ ์ ๋ ฅํ ์์ฐ์ด๋ฅผ ๋ถ์ํ์ฌ ํน์ ๋๋ฉ์ธ ๊ฐ์ฒด๋ฅผ ์ถ์ถํด์ผ ํ๋ ์ํฉ์ด๋ผ๋ฉด UserInput์ผ๋ก ๊ฐ์ธ์ ํธ์ถํ๋ ๊ฒ์ด Embabel์ ํ์ ์์ ์ฑ๊ณผ ์๋ ์ค์ผ์คํธ๋ ์ด์ ๊ธฐ๋ฅ์ ๊ฐ์ฅ ์ ํ์ฉํ๋ ๋ฐฉ๋ฒ์ด๋ค. ๊ฐ๋ฐ์๋ ์ ๋ง ์ ๋ต์ด ์์ด์ ๐ฅฒ
๐ ์ด์ด์ด๊ฐ๋จ ์ ํ๋ฆฌ์ผ์ด์ ๋ง๋ค์ด ๋ณด๊ธฐ
์ฐ๋ฆฌ๊ฐ ๋ง๋ค ๊ฒ์ ๋ฐ๋ก๋ฐ๋ก

๋ฐ๋ช ์๋ค.
์๊ฐ์ ์ป์ ๊ฒ์ ์คํฐ์ง๋ฐฅ์ ๋์ค๋ ๋ง๋ฒ์ ์๋ผ๊ณ ๋์ด๋ค.

์ ํ๋ฆฌ์ผ์ด์ ์๊ตฌ ์ฌํญ์ ๋ค์๊ณผ ๊ฐ๋ค:
- ์ฌ์ฉ์๊ฐ ์ง์ ์ํ๋ ๋งํฌ๋, ์ฑ๊ฒฉ, ๋ต๋ณ ๊ท์น์ ์ง์ ํ ์ ์์ด์ผ ํ๋ค.
- ์ฌ์ฉ์๊ฐ ๋ง๋ฒ์ ์๋ผ๊ณ ๋์ฑ ์ฒ๋ผ ๋ฏธ๋ฆฌ ๊ณ ์ ๋ ๋ต๋ณ์ ์ถ๊ฐํ ์ ์์ด์ผ ํ๋ค.
- ๋ฏธ๋ฆฌ ์ง์ ๋ ๊ณ ์ ๋ต๋ณ๋ค ์ค ์ฌ์ฉ์์ ์ง๋ฌธ์ ๋ํ ๋ต์ด ๋ ์ ์๋ ์ต์ ์ ํ๋ณด๋ฅผ ์ฐพ๊ณ , ์ต๊ณ ์ ์ด 0.7์ ๋๊ธฐ์ง ์๋ ๊ฒฝ์ฐ์๋ ์ฌ์ฉ์๊ฐ ์ ๋ ฅํ ๋งํฌ, ์ฑ๊ฒฉ, ๋ต๋ณ ๊ท์น์ ๋ฐ๋ผ ์ง์ ๋ต๋ณ์ ์์ฑํ๋ค.
๋์ถฉ ๋ฐ๋ช ์๋ฅผ ํ๋ด๋ผ ํ๋ฅด์๋๋ฅผ ์ค๊ณํด ์ค๋ค:
@Builder
public record PersonaContext(
String name,
String description,
String tone,
String rules,
List<String> quotes,
List<StyleExampleContext> styleExamples
) { }
๊ทธ๋ฆฌ๊ณ ์๊ตฌ ์ฌํญ์ ๋ง์กฑํ๊ธฐ ์ํด ๋ค์๊ณผ ๊ฐ์ ํ๋๋ค์ ์ํํ ์ ์์ด์ผ ํ๋ค:
- ํ๋ฅด์๋ ์ ๋ณด ์กฐํ (์ ๋ ฅ: ํ๋ฅด์๋ ์๋ณ์, ์ถ๋ ฅ: ํ๋ฅด์๋ ์ ๋ณด)
- ๊ณ ์ ๋ต๋ณ ๊ธฐ๋ฐ ์๋ต ์์ฑ (์ ๋ ฅ: ํ๋ฅด์๋ ์ ๋ณด, ์ฌ์ฉ์ ์ง๋ฌธ, ์ถ๋ ฅ: ์ง๋ฌธ์ ๋ํ ์๋ต)
- ํ๋ฅด์๋ ๊ธฐ๋ฐ ์๋ต ์์ฑ (์ ๋ ฅ: ํ๋ฅด์๋ ์ ๋ณด, ์ฌ์ฉ์ ์ง๋ฌธ, ์ถ๋ ฅ: ์ง๋ฌธ์ ๋ํ ์๋ต)
์ฐ์ MagicConchAgent๋ผ๋ ํด๋์ค๋ฅผ ์ ์ํ๊ณ ์ค๋ช ์ ์ถ๊ฐํด ์ค๋ค.
@Slf4j
@RequiredArgsConstructor
@Agent(description = "Generate answer for user's question")
public class MagicConchAgent {}
ํ๋ฅด์๋ ์ ๋ณด ์กฐํ Action์ ๋ค์๊ณผ ๊ฐ์ด ์ ์ํ๋ค ๐จ
@Transactional
@Action
public PersonaContext searchContext(Long personaId) {
Persona persona = personaRepository.findById(personaId)
.orElseThrow();
return PersonaContext.fromEntity(persona);
}
๊ทธ๋ฆฌ๊ณ ๊ณ ์ ๋ต๋ณ ๊ธฐ๋ฐ ์๋ต ์์ฑ Action๊ณผ ํ๋ฅด์๋ ๊ธฐ๋ฐ ์๋ต ์์ฑ Action์ ๊ฐ๊ฐ ๊ตฌํํ๋ค.
@Action
public QuoteBasedAnswer createQuoteBasedAnswer(
PersonaContext personaContext,
String question,
Ai ai
) {
if (personaContext.quotes().isEmpty()) {
return new QuoteBasedAnswer(null, 0);
}
String quotesText = personaContext.buildQuotesText();
String prompt = QUOTE_BASED_PROMPT.formatted(question, quotesText);
QuoteScoreList result = ai
.withAutoLlm()
.createObject(prompt, QuoteScoreList.class);
QuoteScoreResult best = result.scores()
.stream()
.max((a, b) -> Double.compare(a.score(), b.score()))
.orElseThrow();
if (best.quoteIndex() <= 0 || best.quoteIndex() > personaContext.quotes().size()) {
return new QuoteBasedAnswer(null, 0);
}
String bestQuote = personaContext.quotes().get(best.quoteIndex() - 1);
return new QuoteBasedAnswer(bestQuote, best.score());
}
@Action
public Optional<PersonaBasedAnswer> createPersonBasedAnswer(
PersonaContext personaContext,
QuoteBasedAnswer quoteBasedAnswer,
String question,
Ai ai
) {
if (quoteBasedAnswer.score >= 0.7) {
return Optional.empty();
}
String examples = personaContext.buildStyleExamplesText();
String prompt = PERSONA_BASED_PROMPT.formatted(
personaContext.name(),
personaContext.description(),
personaContext.tone(),
personaContext.rules(),
examples,
question
);
String answer = ai.withAutoLlm().createObject(prompt, String.class);
return Optional.of(new PersonaBasedAnswer(answer));
}
๊ทธ๋ฌ๋ฉด Optional<PersonaBasedAnswer>์ QuoteBasedAnswer๋ฅผ ์ ๋ ฅ๋ฐ๋ ์ต์ข Action์ ์ ์ํ ์ ์๋ค.
@AchievesGoal(description = "answer user's question.")
@Action
public ConchAnswerResult conchAnswerResult(
QuoteBasedAnswer quoteBasedAnswer,
Optional<PersonaBasedAnswer> personaBasedAnswer
) {
String answer = personaBasedAnswer.map(PersonaBasedAnswer::answer)
.orElse(quoteBasedAnswer.answer());
return new ConchAnswerResult(answer);
}
์ฝ๋์์ ๋์๊ฐ ๋๋ค๊ณ ๋๊ผ๋ค๋ฉด ์์ฃผ ์ ํํ๋ค… ๐ง๐ง
๐ Embabel ๊ทธ๋ ๊ฒ ์ฐ๋ ๊ฑฐ ์๋๋ฐ
์๊ฐ์์ ๋งํ๋ฏ ์์ด์ ํฑ AI์ ํต์ฌ์ ์์ธ ์ํฉ์ด๋ ์์์น ๋ชปํ ๊ฒฐ๊ณผ์ ๋ํ ๋์ ๋ฅ๋ ฅ์ด๋ค. ํ์ฌ ์ฝ๋์์ quoteBasedAnswer.score >= 0.7 ์กฐ๊ฑด์ ์ฒดํฌํ์ฌ ๋ต๋ณ์ ์์ฑํ ์ง ๋ง์ง ๊ฒฐ์ ํ๊ณ ์์ง๋ง, ๋ง์ฝ ์์ฑ๋ ๋ต๋ณ๋ง์ ํ์ง์ด ๋ฎ์ ๋ค์ ์์ฑํด์ผ ํ๋ ์ํฉ์ด ๋ฐ์ํ๋ฉด ์ผ๋ฐ์ ์ธ ์ก์ ๊ตฌ์กฐ๋ก๋ ์ด๋ฅผ ์ฒ๋ฆฌํ๊ธฐ ๋งค์ฐ ๊น๋ค๋กญ๋ค.
https://docs.embabel.com/embabel-agent/guide/0.3.5-SNAPSHOT/#reference.states
Embabel Agent Framework User Guide
Agentic AI is the use of large language (and other multi-modal) models not just to generate text, but to act as reasoning, goal-driven agents that can plan, call tools, and adapt their actions to deliver outcomes. The JVM is a compelling platform for this
docs.embabel.com
๊ทธ๋์ ๊ฐ์ด๋ ๋ฌธ์์ ๋ช ์๋๋๋ก Embabel์์ ์ ๊ณตํ๋ ๊ธฐ๋ฅ๋ค์ ์ฌ์ฉํ๋ฉด ์์ฃผ ์ฝ๊ฒ ๋ฃจํ๋ฅผ ํํํ ์ ์๋ค.
๋์ถฉ ํ๋ฆ์ ๋จผ์ ๊ทธ๋ ค ๋ณด๋ฉด ์ด๋ฐ ๋๋์ด๋ค.

๊ณ ์ ๋ต๋ณ ๊ธฐ๋ฐ์ผ๋ก ์ ์ ํ ์๋ต์ ์ฐพ๊ณ , ์ ์ ํ ๊ฐ์ด ์๋ ๊ฒฝ์ฐ์๋ ์ ํฉ๋๊ฐ 0.7 ์ด์์ธ ๋ต๋ณ์ด ๋์ฌ ๋๊น์ง AI ์ก๋๋ฆฌ๋ฅผ ํ๋ ๊ตฌ์กฐ์ด๋ค๐
1. ๊ณตํต ์ธํฐํ์ด์ค
@State
public interface MagicConchState {
}
2. ๊ณ ์ ๋ต๋ณ ๊ธฐ๋ฐ ์๋ต ์์ฑ ๋จ๊ณ
public record SearchingQuote(
PersonaContext context,
String question
) implements MagicConchState {
@Action
public MagicConchState searchFromQuotes(Ai ai) {
if (context.quotes().isEmpty()) {
return new GeneratingAnswer(context, question, "์ฌ์ฉ ๊ฐ๋ฅํ ์ฟผํธ๊ฐ ์์ต๋๋ค.");
}
QuoteScoreList result = ai.withAutoLlm()
.createObject(
QUOTE_BASED_PROMPT.formatted(
question,
context.buildQuotesText()
),
QuoteScoreList.class
);
QuoteScoreResult best = result.scores().stream()
.max(Comparator.comparingDouble(QuoteScoreResult::score))
.orElseThrow();
if (best.score() >= 0.7) {
String quote = context.quotes().get(best.quoteIndex() - 1);
return new FinalAnswer(quote);
}
return new GeneratingAnswer(context, question,
"์ ์ ํ ์ฟผํธ๋ฅผ ์ฐพ์ง ๋ชปํ์ต๋๋ค (์ต๊ณ ์ : " + best.score() + ")");
}
record QuoteScoreResult(
int quoteIndex,
double score
) {
}
record QuoteScoreList(
List<QuoteScoreResult> scores
) {
}
}
3. ํ๋ฅด์๋ ๊ธฐ๋ฐ ์๋ต ์ง์ ์์ฑ ๋จ๊ณ
public record GeneratingAnswer(
PersonaContext context,
String question,
String reason
) implements MagicConchState {
@Action
public EvaluatingAnswer generate(Ai ai) {
String prompt = PERSONA_BASED_PROMPT.formatted(
context.name(),
context.description(),
context.tone(),
context.rules(),
context.buildStyleExamplesText(),
question
);
String generated = ai.withAutoLlm()
.withLlm(LlmOptions.withAutoLlm().withTemperature(0.8))
.createObject(prompt, String.class);
return new EvaluatingAnswer(context, question, generated);
}
}
4. ๋ต๋ณ ํ๊ฐ ๋จ๊ณ
@Slf4j
public record EvaluatingAnswer(
PersonaContext context,
String question,
String candidateAnswer
) implements MagicConchState {
@Action(clearBlackboard = true)
public MagicConchState evaluate(Ai ai) {
Double qualityScore = ai.withAutoLlm()
.createObject(
"๋ค์ ๋ต๋ณ์ด ํ๋ฅด์๋ ์ค์ ์ ์ผ๋ง๋ ๋ถํฉํ๋์ง ์ ์(0.0~1.0)๋ง ์ถ๋ ฅํด์ค: "
+ candidateAnswer,
Double.class
);
if (qualityScore >= 0.7) {
return new FinalAnswer(candidateAnswer);
}
log.info("[MagicConch] ํ์ง ๋ฏธ๋ฌ({}), ๋ค์ ์์ฑํฉ๋๋ค.", qualityScore);
return new GeneratingAnswer(context, question, "ํ์ง ํ๊ฐ ์คํจ");
}
}
โ๏ธ์ฌ๊ธฐ์ @Action์ clearBlackboard=true๋ผ๋ ์์ฑ์ ์ฌ์ฉํ๋๋ฐ, ์ด๊ฒ ์์ด์ ํธ๊ฐ ๋ฃจํ๋ฅผ ์ํํ ์ ์๊ฒ ๋ง๋๋ ํต์ฌ ์ฅ์น์ด๋ค.
(๊ทผ๋ฐ ์ง์ง blackboard๋ผ๊ณ ํ ๊ฑฐ ์ง์ง ๋ ์ ๋ ์ฐฐ๋ก์ฝฉ๋ก ๋ค์ด๋ฐ…)
1. ์ค๋ณต ํ์ ์ ์ํ ์คํ ์ฐจ๋จ ๋ฐฉ์ง ์ญํ
Emababel์ GOAP ํ๋๋๋ ๋ธ๋๋ณด๋์ ํน์ ํ์ ์ ๊ฐ์ฒด๊ฐ ์ด๋ฏธ ์กด์ฌํ๋ฉด, ํด๋น ๋ชฉํ๊ฐ ์ด๋ฏธ ๋ฌ์ฑ๋์๋ค๊ณ ํ๋จํ์ฌ ๊ทธ ๊ฐ์ฒด๋ฅผ ์์ฑํ๋ ์ก์ ์ ๋ค์ ์คํํ์ง ์์ผ๋ ค๊ณ ํ๋ค.
๋ง์ฝ clearBlackboard๋ฅผ ์ค์ ํ์ง ์๋๋ค๋ฉด, ๋ ๋ฒ์งธ ๋ฃจํ์์ ํ๋๋๋ ๋ธ๋๋ณด๋์ ์ด๋ฏธ ์ด์ ๋จ๊ณ์ GeneratingAnswer๋ EvaluatingAnswer ๊ฐ์ฒด๊ฐ ๋จ์ ์๋ ๊ฒ์ ๋ณด๊ณ , ์ด๋ฏธ ํด๋น ๋จ๊ณ๋ค์ ๊ฑฐ์ณค์ผ๋ ๋ค์ ์์ํ ํ์๊ฐ ์๋ค๊ณ ํ๋จํ์ฌ ํ๋ก์ธ์ค๊ฐ ๋ฉ์ถ๊ฑฐ๋ ์ ๋๋ก ์๋ํ์ง ์๊ฒ ๋๋ค.
clearBlackboard=true๋ฅผ ์ค์ ํ๋ฉด ์ก์ ์ ๊ฒฐ๊ณผ๋ฌผ(์ด ๊ฒฝ์ฐ ์๋ก์ด GeneratingAnswer ๋๋ FinalAnswer ๊ฐ์ฒด)๋ฅผ ์ ์ธํ ๋๋จธ์ง ๋ชจ๋ ์ค๊ฐ ๋ฐ์ดํฐ๊ฐ ๋ธ๋๋ณด๋์์ ์ ๊ฑฐ๋๋ฏ๋ก, ํ๋๋๊ฐ ์ฐ๋ฆฌ์ ์๋๋๋ก ๋ค์ ๊ณํ์ ์๋ฆฝํ๋ ์ ์ฐํ ๊ตฌ์กฐ์ด๋ค.
2. ์คํ ์ด๋ ฅ(hasRun) ๋ฆฌ์
Embabel์ ์ก์ ์ ๋ฌดํ ๋ฐ๋ณต์ ๋ง๊ธฐ ์ํด ๊ฐ ์ก์ ์ ์คํ ์ฌ๋ถ๋ฅผ hasRun์ด๋ผ๋ ์กฐ๊ฑด์ผ๋ก ์ถ์ ํ๋ค. ํ๊ฐ ๊ฒฐ๊ณผ๊ฐ ๊ธฐ์ค ๋ฏธ๋ฌ์ด์ด์ ๋ค์ ์์ฑ ๋จ๊ณ๋ก ๋์๊ฐ์ผ ํ๋ ๊ฒฝ์ฐ, ์ด์ ์ ์คํํ๋ generate ์ก์ ์ ๋ค์ ์คํํด์ผ ํ๋๋ฐ, ์ด๋ ๋ธ๋๋ณด๋๋ฅผ ๋น์์ฃผ๋ฉด hasRun ์ถ์ ์กฐ๊ฑด๋ค๋ ํจ๊ป ์ ๊ฑฐ๋์ด์ ํ๋๋๊ฐ ์ด์ ์ ์คํํ๋ ์ก์ ๋ค์ ๋ค์ ์คํ ๊ฒฝ๋ก์ ํฌํจ์ํฌ ์ ์๊ฒ ๋๋ค ๐
5. ์ต์ข ๋ต๋ณ ํ์ ๋จ๊ณ
public record FinalAnswer(
String answer
) implements MagicConchState {
@AchievesGoal(description = "์ฌ์ฉ์ ์ง๋ฌธ์ ๋ํ ์ต์ข
๋ต๋ณ ํ์ ")
@Action
public ConchAnswerResult finalizeAnswer() {
return new ConchAnswerResult(answer);
}
}
๋ง์ง๋ง์ผ๋ก ์ง์ ์ ์ด ๋ Agent๋ฅผ ์ ์ํ๋ค:
@Agent(description = "Generate answer for user's question")
@RequiredArgsConstructor
public class MagicConchAgent {
private final PersonaRepository personaRepository;
@Action
@Transactional
public SearchingQuote start(Long personaId, String question) {
Persona persona = personaRepository.findById(personaId).orElseThrow();
return new SearchingQuote(PersonaContext.fromEntity(persona), question);
}
}
๊ทผ๋ฐ ์ฌ๊ธฐ์ ์ ๊น
๋ถ๋ช AchievesGoal์ด ์๋ Agent๋ ์ด๋นจ ๋น ์ง ํธ๋์ด, ํ๊น์ท ์๋ ์นํจ, ๋ฆฌ๋ฐ์ด ์๋ ์กฐ์ฌ๋ณ๋จ, ์ฃผ๋ ์๋ ๋์ด๋ฌ๋๋ฐ…


์ถฉ๊ฒฉ AchievesGoal์ด ์๋ Agent ์ค์กด!

์ด์ ๋ ๋ค์๊ณผ ๊ฐ๋ค:

Agent์ ๋ฐํ๊ฐ์ธ SearchingQuote๋ MagicConchState๋ฅผ ๊ตฌํํ๋๋ฐ, MagicConchState๋ @State ์ด๋
ธํ
์ด์
์ด ๋ถ์ด ์๊ธฐ ๋๋ฌธ์ MagicConchState์ ๊ตฌํ์ฒด๋ค ์ค FinalAnswer๊ฐ @AchievesGoal์ ๊ฐ์ง๊ณ ์์ผ๋ฉด Embabel์ ์์์ ๊ฒฝ๋ก๋ฅผ ์ถ๋ก ํ ์ ์๋ค.
ํธ๊ธฐ์ฌ์ MagicConchState์์ @State ์ด๋ ธํ ์ด์ ์ ์ ๊ฑฐํ๊ณ ์ ํ๋ฆฌ์ผ์ด์ ์ ์คํํด ๋ดค๋๋ฐ

Embabel์ด ๋ฐ๋ก ํ๋ฅผ ๋๋ค ๐ฅ
๋ฌผ๋ก ๋ชจ๋ Action์ Agent์ ๋๋ ค๋ฃ๊ณ ํ์ ๊ธฐ๋ฐ ์ถ์ ์ผ๋ก @State ์์ด๋ ๊ตฌํ์ด ๊ฐ๋ฅํ๋ค. (์๋๋ ์ด๋ฐ์ ๋ฌธ์๋ฅผ ๋์ถฉ ์ฝ๊ณ ๋ ๋ค ๊ตฌํ๋ถํฐ ํ๋ ๋ฐ๋์ ํ์ํ ๋ฆฌํฉํ ๋ง ์ ์ฝ๋์ด๋ค. state record๋ ๊ฑฐ๋ ๋ฟ… ๋๋ ๋ชจ๋ ์ฑ ์์ MagicConchAgent์๊ฒ ์์ํ๋ค)
@Slf4j
@RequiredArgsConstructor
@Agent(description = "Generate answer for user's question")
public class MagicConchAgent {
private final PersonaRepository personaRepository;
@Transactional
@Action
public SearchingQuote initContext(Long personaId, String question) {
Persona persona = personaRepository.findById(personaId).orElseThrow();
return new SearchingQuote(PersonaContext.fromEntity(persona), question);
}
@Action
public MagicConchState searchFromQuotes(SearchingQuote search, Ai ai) {
PersonaContext ctx = search.context();
if (ctx.quotes().isEmpty()) {
return new GeneratingAnswer(ctx, search.question(), "์ฌ์ฉ ๊ฐ๋ฅํ ์ฟผํธ๊ฐ ์์ต๋๋ค.");
}
QuoteScoreList result = ai.withAutoLlm()
.createObject(QUOTE_BASED_PROMPT.formatted(search.question(), ctx.buildQuotesText()), QuoteScoreList.class);
QuoteScoreResult best = result.scores().stream()
.max(Comparator.comparingDouble(QuoteScoreResult::score))
.orElseThrow();
if (best.score() >= 0.7) {
String quote = ctx.quotes().get(best.quoteIndex() - 1);
return new FinalAnswer(quote); // ๋ฐ๋ก ๋ชฉํ ํ์
๋ฐํ ๊ฐ๋ฅ
}
return new GeneratingAnswer(ctx, search.question(), "์ ์ ํ ์ฟผํธ๋ฅผ ์ฐพ์ง ๋ชปํ์ต๋๋ค (์ต๊ณ ์ : " + best.score() + ")");
}
@Action
public EvaluatingAnswer generateByPersona(GeneratingAnswer generating, Ai ai) {
PersonaContext ctx = generating.context();
String prompt = PERSONA_BASED_PROMPT.formatted(
ctx.name(), ctx.description(), ctx.tone(), ctx.rules(), ctx.buildStyleExamplesText(), generating.question()
);
String generated = ai.withAutoLlm()
.withLlm(LlmOptions.withAutoLlm().withTemperature(0.8)) // ์์ฑ ์ ์ฐฝ์์ฑ ๋ถ์ฌ
.createObject(prompt, String.class);
return new EvaluatingAnswer(ctx, generating.question(), generated);
}
@Action(clearBlackboard = true) // ๋ฃจํ๋ฅผ ์ํด ์ด์ ์ํ๋ค์ ๋น์
public MagicConchState evaluateGeneratedAnswer(EvaluatingAnswer evaluation, Ai ai) {
Double qualityScore = ai.withAutoLlm().createObject(
"๋ค์ ๋ต๋ณ์ด ํ๋ฅด์๋ ์ค์ ์ ์ผ๋ง๋ ๋ถํฉํ๋์ง ์ ์(0.0~1.0)๋ง ์ถ๋ ฅํด์ค: " + evaluation.candidateAnswer(),
Double.class
);
if (qualityScore >= 0.7) {
return new FinalAnswer(evaluation.candidateAnswer());
}
log.info("[MagicConch] ํ์ง ๋ฏธ๋ฌ({}), ๋ค์ ์์ฑํฉ๋๋ค.", qualityScore);
return new GeneratingAnswer(evaluation.context(), evaluation.question(), "์ด์ ์์ฑ ๋ต๋ณ์ ํ์ง์ด ๋ฎ์");
}
@AchievesGoal(description = "์ฌ์ฉ์ ์ง๋ฌธ์ ๋ํ ์ต์ข
๋ต๋ณ ํ์ ")
@Action
public ConchAnswerResult finalize(FinalAnswer result) {
return new ConchAnswerResult(result.answer);
}
// โ๏ธโ๏ธโ๏ธ @State ์ด๋
ธํ
์ด์
์ ๊ฑฐ โ๏ธโ๏ธโ๏ธ
public interface MagicConchState {
}
}
๋ฌผ๋ก ์ด๊ฒ ๋ณด๊ธฐ ์ข์ ์ฝ๋๋ ์๋๊ณ ์ ์ง๋ณด์๋ฅผ ํ๊ธฐ์ ์ข์ ์ฝ๋๋ ๋๋์ฑ ์๋๊ธด ํ์ง๋ง, ์ด์จ๋ ์ด๋ ๊ฒ ๊ฐ๋ก ๊ฐ์ด ์ง๊ณ ์คํ์์ผ๋ ๋๋ํ Embabel์ด ์ ๋ ฅ ํ์ ๊ณผ ์ถ๋ ฅ ํ์ ๋งคํ์ผ๋ก ๊ธฐ๋ฅ์ด ์ ๋์๊ฐ์ @State๋ฅผ ์ ์จ์ผ ํ๋์ง? ์ ๋ํ ์๋ฌธ์ด ์์ฐ์ค๋ฝ๊ฒ ์๊ฒผ๋ค. (์ฝ๋๋ก ๋ฅ์ ์์ง๋ง ๋ ๊น์ ํ์ต์ ํ ์ ์๋ค๋ ์์ ๋ญํค๋นํค๋์ํฐ์๋๐งฝ)
1. ์ํ ์ค์ฝํ
@State๋ฅผ ์ฌ์ฉํด์ผ ํ๋ ๊ฐ์ฅ ๊ฒฐ์ ์ ์ธ ์ด์ ๋ ๊ฒฝ๋ก ๊ฒฉ๋ฆฌ์ด๋ค. @State๊ฐ ์์ผ๋ฉด ํ๋๋๋ ๋งค ๋จ๊ณ๋ง๋ค ์์ด์ ํธ์ ์ ์๋ ๋ชจ๋ ์ก์ ์ ๊ฒํ ํ์ฌ ๋ค์ ๊ฒฝ๋ก๋ฅผ ์ฐพ๋๋ค. ๋ฌธ์ ๋ ์ก์ ์ด 10๊ฐ, 20๊ฐ๋ก ๋์ด๋๋ฉด ํ๋๋๊ฐ ์๋ฑํ ์ก์ ์ ์ ํํ ๊ฐ๋ฅ์ฑ์ด ์๊ธฐ๊ณ , ํ๋จ์ ๊ฑธ๋ฆฌ๋ ๋น์ฉ ๋ฌธ์ ๋ ์๋ค. ๊ทธ๋ฌ๋ ์ก์ ์ด @State ๊ฐ์ฒด๋ฅผ ๋ฐํํ๋ฉด, ํ๋๋๋ ํด๋น ์ํ ํด๋์ค ๋ด๋ถ์ ์ ์๋ ์ก์ ๋ค๋ง ์คํ ํ๋ณด๋ก ๊ณ ๋ฅด๋ ค๊ณ ํ๊ธฐ ๋๋ฌธ์ ์ํ ์ค์ฝํ์ด ์์ฃผ ํฐ ์ฅ์ ์ด๋ผ๊ณ ๋ณผ ์ ์๋ค.
2. ์ํ ์จ๊น
@State๋ฅผ ์ฌ์ฉํ๋ฉด ํ๋ ์์ํฌ๊ฐ ์ํ ์ ์ด ์ ์ด์ ์ํ ๊ฐ์ฒด๋ค์ ์๋์ผ๋ก ์จ๊น์ฒ๋ฆฌํ๋ค. ์ด์ ์ํ๋ ๋ธ๋๋ณด๋ history์๋ ๋จ์ง๋ง, ํ๋๋์ ๋์๋ ๋ณด์ด์ง ์๊ฒ ๋์ด์ ๋ค์ ๋จ๊ณ์ ์์ฌ ๊ฒฐ์ ์ ๋ฐฉํดํ์ง ์๋๋ค. ์ฆ, ํ๋์ State ๊ตฌํ์ฒด๋ง์ด ํ๋๋์ ์์ฌ ๊ฒฐ์ ์ ๊ด์ฌํ๊ฒ ๋๋ค.
3. ์ ์ง๋ณด์์ ๊ด์ฌ์ฌ์ ๋ถ๋ฆฌ
๋ชจ๋ ๋ก์ง์ ์์ด์ ํธ์ ๋๋ ค ๋ฃ์ผ๋ฉด ์ฝ๋๊ฐ ๋น๋ํด์ง๊ณ ๊ฐ ๋จ๊ณ ๊ฐ์ ๊ฒฝ๊ณ๊ฐ ๋ชจํธํด์ง๋ค. ์ ๋ง๋ค์ด์ง EvaluatingAnswer ์ํ ํด๋์ค๋ ๋ค๋ฅธ ์์ด์ ํธ์์๋ ๊ทธ๋๋ก ๊ฐ์ ธ๋ค ์ฐ๊ฑฐ๋ ์์๋ฐ์ ๊ธฐ๋ฅ์ ํ์ฅํ๊ธฐ ๋งค์ฐ ์ฉ์ดํ ๊ตฌ์กฐ๊ฐ ๋๋ค.
โ๏ธ ๋๊ตฌ ์ฅ์ฌ์ฃผ๊ธฐ
๋๋ํด์ง์ง ๋ง, ์๋ ๋๋ํด์ ธ, ์๋ ๋๋ํด์ง์ง ๋ง, ๋ด ๋ฐฅ๊ทธ๋ฆ ๋นผ์์ง ์์ ๋งํผ๋ง ๋๋ํด์ ธ

Embabel ํ๋ ์์ํฌ์์ ๋๋ฉ์ธ ๊ฐ์ฒด๋ค์ ๋จ์ํ ๋ฐ์ดํฐ ๋ฉ์ด๋ฆฌ๊ฐ ์๋๋ค. ์ ํ์ ์ผ๋ก ๋๋ฉ์ธ์ด ํ ์ ์๋ ํ๋์ LLM์๊ฒ ์๋ ค์ฃผ์ด์ ์์ด์ ํธ๊ฐ Action์ ์ํํ๋ ๋์ ๋๋ฉ์ธ๊ณผ ์ํธ์์ฉ์ ํ ์ ์๋ค๋ ํน์ง์ด ์๋ค.
๋ค์๊ณผ ๊ฐ์ด ์ธํฐ๋ทฐ ์ ์ฒด ์ปจํ ์คํธ๋ฅผ ์ ์ฅํ๋ ๋ ์ฝ๋๊ฐ ์์ ๋, ์ธํฐ๋ทฐ์ ๋ํ ์ด๋ ํ ์์ฝ ์ ๋ณด๋ ๋ค์ ์ง๋ฌธ ์ถ์ฒ์ ๋ฐ๊ณ ์ถ์ ๊ฒฝ์ฐ ์ด๋ค ์ ๋ณด๋ฅผ ๋ฃ์ด์ค์ผ ํ ๊น?
public record InterviewContext(
CandidateAnalysisResult candidateAnalysis,
PositionAnalysisResult positionAnalysis,
List<EvaluationCategoryResult> evaluationCategories,
List<String> checklist,
List<AskedQuestionResult> history
) {
}
์ ํด์ง ๋ต์ ์๋ค. ํ์ฌ ์ธํฐ๋ทฐ์ ์งํ ์ํฉ์ด๋, ์ง์์์ ํน์ง, ๊ณต๊ณ ์ ํน์ง ๋ฑ์ ๋ฐ๋ผ ์ด๋ค ์ ๋ณด๋ AI์ ์์ฌ ๊ฒฐ์ ์ ๋์์ด ๋๊ณ ์ด๋ค ์ ๋ณด๋ ์๋ ๊ฒ์ด๋ค. ์๋ฅผ ๋ค์ด, ๋ฉด์ ์ง๋ฌธ์ ์ถ์ฒํด ๋ฌ๋ผ๊ณ ์์ฒญํ์ ๊ฒฝ์ฐ ๋ฉด์ ๊ด์ ์ง๋ฌธ์ ๋ํ ์ง์์์ ๋ง์ง๋ง ๋ต๋ณ์ด ์ฐ์ด์ ๊ผฌ๋ฆฌ ์ง๋ฌธ์ ์์ฑํ๊ธฐ ์ด๋ ค์ด ๋ต๋ณ์ผ ๊ฒฝ์ฐ (ex. ์ ๋ชจ๋ฅด๊ฒ ์ต๋๋ค.), AI๋ ์ถ๊ฐ ์ง๋ฌธ ์์ฑ์ ์ํด ์ ์ฒด ๊ณต๊ณ ์ ์ง์์์ ์์ฝ ์ ๋ณด๋ฅผ ์ถ๊ฐ๋ก ํ์๋ก ํ ๊ฒ์ด๊ณ , ๋ฐ๋๋ก ๋ง์ง๋ง ๋ฌธ๋ต์ด ๊ผฌ๋ฆฌ ์ง๋ฌธ์ ์์ฑํ๊ธฐ์ ์ ํฉํ ๊ฒฝ์ฐ์๋ ๊ตณ์ด ์ ์ฒด ๋ฉด์ ์ปจํ ์คํธ๋ฅผ ํ์ธํ ํ์๊ฐ ์๋ค. LLM์ ๋๋ํ๊ธฐ ๋๋ฌธ์, ํ๋จ ๊ณผ์ ์์ ์์ ์ด ์ด๋ค ์ ๋ณด๊ฐ ๋ ๋ถ๊ฐ์ ์ผ๋ก ํ์ํ์ง, ํน์ ํ์ฌ ์ ๋ณด๋ก๋ ์ถฉ๋ถํ ๋ง์กฑ์ค๋ฌ์ด ๋ต๋ณ์ ๋ฑ์ ์ ์๋์ง๋ฅผ ์ถ๋ก ํ ์ ์๋ค. ๋ฐ๋ผ์ ์ฐ๋ฆฌ๋ InterviewContext์ ์๋ ๋ชจ๋ ๋ด์ฉ์ ๋ชจ์กฐ๋ฆฌ LLM์๊ฒ ์ ๋ฌํ๊ณ ํ๋กฌํํธ๋ฅผ ์์ฑํ๋ ๋์ , ๋ค์๊ณผ ๊ฐ์ด InterviewContext์ ๋๊ตฌ๋ฅผ ์ถ๊ฐํ ์ ์๋ค.
public record InterviewContext(
CandidateAnalysisResult candidateAnalysis,
PositionAnalysisResult positionAnalysis,
List<EvaluationCategoryResult> evaluationCategories,
List<String> checklist,
List<AskedQuestionResult> history
) {
@LlmTool(description = "Get detailed candidate analysis")
public CandidateAnalysisResult getCandidateAnalysis() {
return candidateAnalysis;
}
@LlmTool(description = "Get detailed position or purpose analysis")
public PositionAnalysisResult getPositionAnalysis() {
return positionAnalysis;
}
@LlmTool(description = "Get the most recent interview question and answers")
public AskedQuestionResult getLastInteraction() {
if (history.isEmpty()) {
return null;
}
return history.getLast();
}
@LlmTool(description = "Get previous interaction by index. 0 is the first question.")
public AskedQuestionResult getInteraction(int index) {
if (index < 0 || index >= history.size()) {
return null;
}
return history.get(index);
}
@LlmTool(description = "Get recent interview interactions")
public List<AskedQuestionResult> getRecentHistory(int count) {
int start = Math.max(0, history.size() - count);
return history.subList(start, history.size());
}
@LlmTool(description = "Search interview history by keyword")
public List<AskedQuestionResult> searchHistory(String keyword) {
return history.stream()
.filter(q -> q.content().toLowerCase().contains(keyword.toLowerCase()))
.toList();
}
@LlmTool(description = "Get evaluation categories used to score the candidate")
public List<EvaluationCategoryResult> getEvaluationCategories() {
return evaluationCategories;
}
@LlmTool(description = "Get checklist topics that interview questions should cover")
public List<String> getChecklist() {
return checklist;
}
public String toSummary() {
return """
Candidate: %s
Position: %s
Categories: %d
Checklist topics: %d
""".formatted(
candidateAnalysis.summary(),
positionAnalysis.summary(),
evaluationCategories.size(),
checklist.size()
);
}
}
์ด๋ ๊ฒ ์ฝ๋๋ฅผ ์์ฑํด์ ๊ธฐ๋ณธ์ ์ผ๋ก๋ Summary๋ง ํ๋กฌํํธ์ ์ ๋ ฅํด ์ฃผ์ด๋, LLM์ด ํ์์ ์ํด ์์์ ๋๊ตฌ๋ฅผ ์ฃผ์ฌ์ฃผ์ฌ ์ฐพ์์ ์ฐ๊ธฐ ๋๋ฌธ์ ์ ์ฒด ์ปจํ ์คํธ๋ฅผ ์ ๋ ฅํ๋ ๊ฒฝ์ฐ๋ณด๋ค ํจ์ฌ ํจ์จ์ ์ด๋ค.
์ด LLM์ด ํ๋จ ๊ณผ์ ์์ ์ด ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ๊ฒ ๋ง๋ค๊ณ ์ถ์ผ๋ฉด ๊ทธ๋ฅ .withToolObject์ ๋ฃ์ด์ฃผ๋ฉด ๋ก์ด๋ค. ์๋ก์ฝ๋กฌ
String[] generated = ai.withAutoLlm()
.withToolObject(context)
.createObject(prompt, String[].class);

๋ฒ์ธ) MCP (Model Context Protocol)
๋๊ตฌ๋ฅผ ํธ์คํ ํ๊ณ ๊ณต์ ํ๊ธฐ ์ํ ํ์คํ๋ ํ๋กํ ์ฝ์ธ๋ฐ, ์์ฃผ ์ด์ง ๋ณด์ฌ์ฃผ์๋ฉด
@Configuration
public class NotionMcpConfig {
@Bean
public McpToolFactory mcpToolFactory(List<McpSyncClient> clients) {
return new McpToolFactory(clients);
}
@Bean(name = "notionTool")
public Tool notionTool(McpToolFactory mcpToolFactory) {
return mcpToolFactory.matryoshka(
"notion",
"Notion MCP editing tool",
callback -> true
);
}
}
NotionTool์ Bean์ผ๋ก ๋ฑ๋กํ๊ณ ๊ทธ๊ฑธ ๋๊ตฌ๋ก ๋ฑ๋กํด ์ฃผ๋ฉด
@Agent(description = "Exports evaluation result to Notion")
@RequiredArgsConstructor
public class EvaluationNotionAgent {
private final Tool notionTool;
@AchievesGoal(description = "Exported to Notion")
@Action
public String export(Long candidateId, EvaluationResult result, Ai ai) {
String categoryTable = result.categoryScores().stream()
.map(c -> "%s | %.1f | %.1f | %s".formatted(
c.content(),
c.weight(),
c.score(),
c.rationale()
))
.collect(Collectors.joining("\n"));
String evaluationData = FORMAT_STRING.formatted(
result.totalScore(),
categoryTable,
result.interviewAssessment(),
result.strengths(),
result.weaknesses(),
result.hiringRecommendation()
);
return ai.withAutoLlm()
.withTool(notionTool)
.createObject(NOTION_PROMPT.formatted(pageId, candidateId, evaluationData), String.class);
}
}
๋์ถฉ ์ด๋ฐ ๋๋์ผ๋ก ์ ํ๋ฆฌ์ผ์ด์ API ํธ์ถ ์ AI๊ฐ ์์์ ๋ ธ์ ์ ์ ๋ฐ์ดํธํด์ฃผ๋ ๊ฒ๋ ๊ฐ๋ฅํ๋ค:

๋ฐ๋ช ์_์ต์ข .png



์ฐธ๊ณ ์๋ฃ
https://docs.embabel.com/embabel-agent/guide/0.3.5-SNAPSHOT/#superior-extensibility-and-reuse
'Java' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| ๋์์ฑ ํ์ฅํ๊ธฐ ๊ป์ด๋ค (๋ถ์ - Runtime Data Area) (2) | 2024.09.22 |
|---|---|
| [Java] Enum์ ๋น๊ตํด ๋ณด์ (56) | 2024.04.15 |
| [Java] String.matches() ๋์ Pattern์ ์ฌ์ฉํ์ (12) | 2024.03.17 |