들어가며
LLM이 내 애플리케이션의 코드를 실행하게 하려면?
ChatGPT나 Gemini, Claude와 같은 LLM은 학습된 데이터 내에서만 답변을 생성할 수 있어 외부 시스템을 제어하는 작업에 약합니다. 이러한 한계를 극복하게 해주는 기능이 Tool Calling이라고 부르는 기능입니다.
Tool Calling이란 사용자가 자신의 애플리케이션 내에 있는 함수의 이름, 설명, 매개변수 구조를 JSON 형태로 사전에 정의하여 모델에게 제공하는 방식으로 동작합니다. LLM은 프롬프트를 분석한 뒤 상황에 맞는 Tool이 있는 경우 이를 호출해 달라는 요청(Tool Call)을 반환합니다. 그러면 사용자의 애플리케이션에서 이 요청대로 실제 함수를 실행하고, 그 결과를 모델에게 다시 전달하는 방식으로 동작합니다.
이러한 방식을 통해 LLM은 보다 더 상황에 맞는 문맥을 기반으로 더 나은 품질의 답변을 해주거나, 외부 시스템을 제어할 수 있게 됩니다.
이번 포스트에선 관리자 페이지의 AI 채팅패널을 통해, 사용자가 원하는 기간의 방문자 수 추이를 라인차트로 그리는 기능을 구현해보도록 하겠습니다.
방법 1: AI에게 코드를 작성해달라고 요청하기
당장 떠오르는 쉬운 방법은, AI에게 코드를 작성해달라고 요청한 다음, 이를 eval 함수로 실행하는 것입니다. 다만 이 방법은 여러 문제점이 있습니다.
- AI가 아주 조금만 실수해도 코드 실행이 안됨
- eval 자체의 위험성
먼저, AI가 작성한 코드가 완벽하다는 보장이 없습니다. 대부분의 경우 잘 실행되겠지만, 가끔가다가 따옴표를 이상하게 붙인다거나 하면 기능이 오동작하게 됩니다. 그렇다고 예외처리를 하자니, 어떤 예외가 있을지도 모르고, 발생 가능한 예외도 너무 다양하니 어렵습니다. 그리고 eval을 사용해야 한다는 것도 큰 문제입니다. eval은 사용자 브라우저에서 외부 코드를 실행하는 함수인 만큼, 만약 AI가 이상한 코드를 작성해서 준다면 예상치 못한 문제를 만날 수 있게 됩니다. 따라서 이 방법은 그다지 선택하고 싶은 방법이 아니네요.
방법 2: AI의 Tool Calling
ChatGPT, Gemini, Claude와 같은 AI 서비스는 AI를 통해 함수를 호출하는 기능을 제공합니다. 흔히 이를 Tool Calling이라고 부르는데, 좀 더 자세히 설명하면,
사전에 정의된 함수를 AI에게 알려주면, AI가 상황에 맞는 함수를 어떤 매개변수로 호출할지를 알려주는 기능이라고 할 수 있습니다.
Tool Calling을 사용하면, AI가 전체 코드를 작성하고 이를 실행하는 방법이 아니기에, 실수할 확률도 현저하게 줄어들며, 의도하지 않은 코드가 실행될 가능성도 차단할 수 있습니다.
또한 여러 Tool Calling을 조합한다면, AI가 서비스에서 복잡한 기능을 보다 더 안전하게 실행할 수 있게 됩니다.
백문이 불여일타라고, 직접 실습하면서 사용 방법을 알아보겠습니다.
Tool Calling 실습, Next.js에서 Tool Calling을 구현하여 채팅메시지로 차트 생성 및 수정하기
애플리케이션 구성
프로젝트 초기화
먼저 Next.js 프로젝트를 설치하고 간단하게 세팅하겠습니다. 아래의 명령어를 실행해주세요.
1pnpm create next-app@16.2.1 ai-tool-exam --yes
2실행하고 나면, 현재 경로 밑에 ai-tool-exam이라는 폴더가 생성되고, 그 안에 next.js 앱이 생성됩니다.
필요한 차트 라이브러리를 추가로 설치하겠습니다.
1pnpm install chart.js react-chartjs-2
2실습에서 사용할 차트 컴포넌트 만들기
실습에서 사용할 차트 컴포넌트입니다. 아래와 같은 상황을 가정하겠습니다.
- API를 통해 지정한 기간의 날짜별 방문자 수 정보를 구할 수 있는 상황.
- 관리자는 AI 채팅을 통해 원하는 기간의 방문자 수 정보를 라인 차트로 보고 싶은 상황.
app 폴더 밑에 line-chart.tsx라는 파일을 생성하고, 아래와 같이 작성합니다.
1'use client';
2
3import {
4 Chart as ChartJS,
5 CategoryScale,
6 LinearScale,
7 PointElement,
8 LineElement,
9 Title,
10 Tooltip,
11 Legend,
12 ChartData,
13 ChartOptions,
14} from 'chart.js';
15
16// React용 Line 차트 컴포넌트를 불러옵니다.
17import { Line } from 'react-chartjs-2';
18
19// ChartJS에 사용할 요소들을 등록
20ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
21
22export interface LineChartProps {
23 options: ChartOptions<'line'>;
24 data: ChartData<'line'>;
25}
26
27export function LineChart({ options, data }: LineChartProps) {
28 // 설정한 옵션과 데이터를 Line 컴포넌트에 전달하여 렌더링
29 return <Line options={options} data={data} />;
30}
31그 다음, app/use-query-data.ts 파일을 생성하고 아래와 같이 작성합니다.
1import { ChartData } from 'chart.js';
2
3export interface QueryDataType {
4 label: string;
5 date: Date;
6 value: number;
7}
8
9export interface QueryDataOptions {
10 startDate: Date;
11 endDate: Date;
12}
13
14export interface ToChartDataOptions {
15 data: QueryDataType[];
16 label: string;
17 borderColor: string;
18 backgroundColor: string;
19}
20
21// 일년치 더미데이터 생성
22const dummyData = Array(365)
23 .fill(null)
24 .map((_, i) => {
25 const date = new Date(2025, 0, i + 1);
26 const year = date.getFullYear().toString().substring(2, 4);
27 const month = date.getMonth() + 1;
28 const day = date.getDate();
29 const label = `${year}/${month}/${day}`;
30
31 return {
32 label,
33 date,
34 value: Math.floor(Math.random() * 201) + 100,
35 };
36 });
37
38export function useQueryData() {
39 /**
40 * 데이터를 쿼리하는 함수를 Mocking하는 함수.
41 * startDate, endDate 사이의 데이터를 반환함.
42 *
43 * @param startDate 범위의 시작
44 * @param endDate 범위의 끝
45 * @returns 날짜 범위 내의 데이터
46 */
47 const queryData = ({ startDate, endDate }: QueryDataOptions): QueryDataType[] => {
48 const start = startDate.getTime();
49 const end = endDate.getTime();
50
51 // 매개변수로 받은 날짜 범위 안의 데이터
52 return dummyData.filter((v) => {
53 const time = v.date.getTime();
54
55 return start <= time && time <= end;
56 });
57 };
58
59 /**
60 * QueryData에서 반환한 데이터를 라인차트를 생성하는데 필요한 데이터로 변환하는 함수
61 * @param 맵핑 옵션
62 * @returns ChartData
63 */
64 const toChartData = ({ data, ...options }: ToChartDataOptions): ChartData<'line'> => {
65 return {
66 labels: data.map((v) => v.label),
67 datasets: [
68 {
69 data: data.map((v) => v.value),
70 ...options,
71 },
72 ],
73 };
74 };
75
76 return {
77 queryData,
78 toChartData,
79 };
80}
81그리고 app/page.tsx를 아래와 같이 수정합니다.
1'use client';
2
3import { useState } from 'react';
4import { LineChart, LineChartProps } from './line-chart';
5import { QueryDataOptions, ToChartDataOptions, useQueryData } from './use-query-data';
6
7export default function Home() {
8 const [options, setOptions] = useState<LineChartProps['options']>({});
9 const [data, setData] = useState<LineChartProps['data']>({
10 datasets: [{ label: 'Unknown', data: [] }],
11 });
12 const { queryData, toChartData } = useQueryData();
13
14 /**
15 * LineChart를 생성하는 함수. AI도 해당 함수를 사용해서 차트를 생성할 수 있어야 함.
16 *
17 * @param queryOption 쿼리 옵션. 이걸로 데이터의 범위를 제어함.
18 * @param toChartDataOptions 차트를 그리는데 필요한 포인트, 색상등의 정보.
19 * @param chartOptions 차트 옵션. 제목, 범례등의 옵션을 제어함.
20 */
21 const createLineChart = (
22 queryOption: QueryDataOptions,
23 toChartDataOptions: Omit<ToChartDataOptions, 'data'>,
24 chartOptions: LineChartProps['options'] = {},
25 ) => {
26 const data = queryData(queryOption);
27 const chartData = toChartData({
28 data,
29 ...toChartDataOptions,
30 });
31
32 setData(chartData);
33 setOptions(chartOptions);
34 };
35
36 const onClick = () => {
37 createLineChart(
38 { startDate: new Date(2025, 0, 1), endDate: new Date(2025, 5, 7) },
39 {
40 label: 'Testing',
41 borderColor: 'rgb(53, 162, 235)',
42 backgroundColor: 'rgba(53, 162, 235, 0.5)',
43 },
44 {},
45 );
46 };
47
48 return (
49 <div className="mx-auto flex gap-5 w-full h-screen p-10">
50 <div className="w-[60%] border border-gray-200 shadow-md rounded-md p-5">
51 <LineChart options={options} data={data} />
52 </div>
53
54 <div className="border border-gray-200 shadow-md rounded-md p-5 flex-1">
55 <button
56 className="border border-gray-200 px-5 py-2 rounded-md cursor-pointer hover:bg-gray-50 active:bg-gray-100 transition-all duration-200"
57 onClick={onClick}
58 >
59 Test
60 </button>
61 </div>
62 </div>
63 );
64}
65지금은 Test 버튼을 누르면 차트를 그리도록 구현되어 있습니다. 이제 AI를 사용해서 사용자가 말한 범위 내의 데이터로 차트를 그리도록 구현해보겠습니다.
아래의 명령어를 실행해서 next.js 앱을 실행하고, localhost:3000으로 접속 후, 차트 화면이 잘 나오는지 확인합니다.
1pnpm dev
2AI 호출 코드 작성하기
환경변수 설정
프로젝트 루트 경로에 .env 파일을 생성하고 아래와 같이 작성합니다. Gemini API Key를 발급받는 방법은 Gemini API 가이드를 참고해주세요.
1GEMINI_API_KEY="GEMINI API키를 여기에 입력"
2Google GenAI SDK 설치
아래 명령어를 실행하여 GenAI SDK를 설치합니다. 해당 라이브러리는 Gemini API를 호출할 수 있도록 도와주는 라이브러리입니다.
1pnpm install @google/genai
2채팅 요청을 처리하는 서버액션 구현하기
앞서 설치한 genai 라이브러리를 통해 채팅 요청을 처리하는 서버액션을 구현하겠습니다. 서버액션에 대해 잘 모르시는 분들은 서버액션에 대한 포스트를 참고해주세요.
먼저 프로젝트 루트 경로에 server/request-ai.ts 파일을 생성하고 아래와 같이 작성해주세요.
1'use server';
2
3import { GoogleGenAI, FunctionDeclaration, Type, FunctionCall } from '@google/genai';
4import { QueryDataOptions, ToChartDataOptions } from '../app/use-query-data';
5
6const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY! });
7
8// AI가 호출할 수 있는 createLineChart 툴 선언
9const createLineChartDeclaration: FunctionDeclaration = {
10 name: 'createLineChart',
11 description: '지정한 날짜 범위와 스타일 옵션으로 라인 차트를 생성합니다.',
12 parameters: {
13 type: Type.OBJECT,
14 properties: {
15 startDate: {
16 type: Type.STRING,
17 description: '데이터 조회 시작일 (ISO 8601 형식, 예: 2025-01-01)',
18 },
19 endDate: {
20 type: Type.STRING,
21 description: '데이터 조회 종료일 (ISO 8601 형식, 예: 2025-03-31)',
22 },
23 label: {
24 type: Type.STRING,
25 description: '차트 데이터셋 레이블',
26 },
27 borderColor: {
28 type: Type.STRING,
29 description: '선 색상 (CSS 색상값, 예: rgb(75, 192, 192))',
30 },
31 backgroundColor: {
32 type: Type.STRING,
33 description: '배경 색상 (CSS 색상값, 예: rgba(75, 192, 192, 0.2))',
34 },
35 title: {
36 type: Type.STRING,
37 description: '차트 제목 (선택 사항)',
38 },
39 },
40 required: ['startDate', 'endDate', 'label', 'borderColor', 'backgroundColor'],
41 },
42};
43
44export interface CreateLineChartArgs {
45 queryOption: QueryDataOptions;
46 toChartDataOptions: Omit<ToChartDataOptions, 'data'>;
47 chartOptions: { plugins?: { title?: { display: boolean; text: string } } };
48}
49
50export interface AiResponse {
51 text: string;
52 functionCall?: {
53 name: 'createLineChart';
54 args: CreateLineChartArgs;
55 };
56}
57
58/**
59 * 사용자의 메시지를 받고 LLM을 호출하여 답변을 반환하는 서버액션.
60 */
61export async function requestAi(userMessage: string): Promise<AiResponse> {
62 const response = await ai.models.generateContent({
63 model: 'gemini-2.5-flash-lite',
64 contents: userMessage,
65 config: {
66 systemInstruction:
67 '당신은 데이터 시각화 도우미입니다. 사용자가 차트 생성을 요청하면 createLineChart 툴을 호출하세요. ' +
68 '라인 차트 외 다른 종류의 차트(바 차트, 파이 차트 등)는 지원하지 않으므로, ' +
69 '다른 차트를 요청받으면 현재는 라인 차트만 지원한다고 안내하세요. ' +
70 'n월의 차트를 그려달라고 요청하면 해당 월의 시작일과 마지막 일자를 사용할 수 있도록 유연하게 대응하세요. ' +
71 '사용자가 색상을 따로 지정하지 않으면, 적당한 색상을 알아서 선정한다. ' +
72 '데이터는 2025년도만 있다고 가정한다. ' +
73 '위의 지시사항을 준수하여 사용자가 명시하지 않은 param을 채워넣는다.',
74 // 아래와 같이 LLM에게 사용가능한 Tool 정보를 제공한다.
75 tools: [{ functionDeclarations: [createLineChartDeclaration] }],
76 },
77 });
78
79 const functionCall = response.functionCalls?.[0] as FunctionCall | undefined;
80
81 if (functionCall?.name === 'createLineChart') {
82 const raw = functionCall.args as Record<string, string>;
83 return {
84 text: '라인 차트를 생성했습니다.',
85 functionCall: {
86 name: 'createLineChart',
87 args: {
88 queryOption: {
89 startDate: new Date(raw.startDate),
90 endDate: new Date(raw.endDate),
91 },
92 toChartDataOptions: {
93 label: raw.label,
94 borderColor: raw.borderColor,
95 backgroundColor: raw.backgroundColor,
96 },
97 chartOptions: raw.title ? { plugins: { title: { display: true, text: raw.title } } } : {},
98 },
99 },
100 };
101 }
102
103 return { text: response.text ?? '응답을 생성하지 못했습니다.' };
104}
105코드가 좀 긴데 핵심 코드는 generateContent와 createLineChartDeclaration입니다. 먼저 generateContent부터 보겠습니다.
1const response = await ai.models.generateContent({
2 model: 'gemini-2.5-flash-lite',
3 contents: userMessage,
4 config: {
5 systemInstruction:
6 '당신은 데이터 시각화 도우미입니다. 사용자가 차트 생성을 요청하면 createLineChart 툴을 호출하세요. ' +
7 '라인 차트 외 다른 종류의 차트(바 차트, 파이 차트 등)는 지원하지 않으므로, ' +
8 '다른 차트를 요청받으면 현재는 라인 차트만 지원한다고 안내하세요. ' +
9 'n월의 차트를 그려달라고 요청하면 해당 월의 시작일과 마지막 일자를 사용할 수 있도록 유연하게 대응하세요. ' +
10 '사용자가 색상을 따로 지정하지 않으면, 적당한 색상을 알아서 선정한다. ' +
11 '데이터는 2025년도만 있다고 가정한다. ' +
12 '위의 지시사항을 준수하여 사용자가 명시하지 않은 param을 채워넣는다.',
13 // 아래와 같이 LLM에게 사용가능한 Tool 정보를 제공한다.
14 tools: [{ functionDeclarations: [createLineChartDeclaration] }],
15 },
16});
17- 모델은 Gemini 2.5 Flash Lite를 사용합니다. 해당 포스트를 작성하는 시점에 이 모델이 제일 저렴해서 골랐습니다.
- config를 보면 systemInstruction과 tools가 있습니다.
- systemInstruction은 시스템 프롬프트입니다. LLM은 해당 프롬프트에서 정한 규칙을 최우선으로 따릅니다. 주로 AI가 사용자의 요청을 어떻게 처리할지를 이곳에서 작성합니다.
- tools는 LLM이 실행할 수 있는 Tool 정보입니다. LLM은 적절한 상황에 Tool Calling을 사용하게 됩니다.
LLM의 응답에서 response.functionCalls를 통해, 지금 Tool Calling을 호출해야 하는 상황인지를 우리가 만든 애플리케이션에서 판단할 수 있습니다. 호출해야 하는 경우, LLM은 functionCall의 name으로 어떤 툴을 실행해야 하고 매개변수는 뭔지 args에 담아서 응답하니, 이를 활용하면 됩니다.
그럼 Tool Calling을 어떻게 선언했는지 보겠습니다.
1import { GoogleGenAI, FunctionDeclaration, Type, FunctionCall } from '@google/genai';
2
3const createLineChartDeclaration: FunctionDeclaration = {
4 name: 'createLineChart',
5 description: '지정한 날짜 범위와 스타일 옵션으로 라인 차트를 생성합니다.',
6 parameters: {
7 type: Type.OBJECT,
8 properties: {
9 startDate: {
10 type: Type.STRING,
11 description: '데이터 조회 시작일 (ISO 8601 형식, 예: 2025-01-01)',
12 },
13 endDate: {
14 type: Type.STRING,
15 description: '데이터 조회 종료일 (ISO 8601 형식, 예: 2025-03-31)',
16 },
17 // ... 생략
18 },
19 required: ['startDate', 'endDate', 'label', 'borderColor', 'backgroundColor'],
20 },
21};
22- genai는 Tool Calling의 타입을 선언하는데 사용할 수 있도록, FunctionDeclaration, Type 등의 타입과 Enum을 제공합니다.
- 해당 타입을 통해 툴에 대한 설명과 매개변수 정보를 설정할 수 있습니다.
- 이를 간결하고 정확하게 적어주어야 LLM이 실수하지 않고 해당 함수의 파라미터를 작성해주니, 꼼꼼하게 적어주어야 합니다.
- 또한 필수 파라미터는 required 배열에 명시해주어, LLM이 빠뜨리지 않고 args를 작성할 수 있도록 해야 합니다.
- 이걸 언제 다 작성하지... 라고 걱정하실 수 있는데, Tool에서 호출할 함수를 Copilot이나 Claude Code한테 알려주고 Tool 선언을 만들어달라고 하면 잘 만들어주니, 이를 활용하면 좋습니다.
Tool Calling 실행하기
이어서, Tool Calling을 실행하는 코드를 작성하겠습니다. "앞서 한거가 Tool Calling을 실행한게 아니냐?" 라고 하실 수 있는데 아직입니다. 앞서 했던 코드에선 LLM에게 이러이러한 도구가 있다고 알려주었고(createLineChartDeclaration), LLM이 어떤 도구를 사용하겠다(response.functionCalls) 라고 응답한 것 까지 다루었습니다.
이제 response.functionCalls를 가지고 실제 함수를 호출하는 코드를 작성하려고 합니다. 그 전에, AI와의 대화를 할 수 있도록 채팅패널을 구현해보겠습니다.
app/chat-panel.tsx파일을 생성하고 아래와 같이 작성해줍니다.
1'use client';
2
3import { useState } from 'react';
4
5export type ChatMessage = {
6 id: string;
7 role: 'user' | 'assistant';
8 content: string;
9 createdAt: number;
10};
11
12type ChatPanelProps = {
13 messages: ChatMessage[];
14 onSend: (content: string) => void;
15};
16
17export function ChatPanel({ messages, onSend }: ChatPanelProps) {
18 const [input, setInput] = useState('');
19
20 const handleSubmit = (e: React.FormEvent) => {
21 e.preventDefault();
22 const trimmed = input.trim();
23 if (!trimmed) return;
24 onSend(trimmed);
25 setInput('');
26 };
27
28 return (
29 <div className="flex flex-col h-full">
30 <div className="flex-1 overflow-y-auto flex flex-col gap-2 mb-4">
31 {messages.length === 0 && (
32 <p className="text-gray-400 text-sm text-center mt-4">메시지가 없습니다.</p>
33 )}
34 {messages.map((msg) => (
35 <div
36 key={msg.id}
37 className={`max-w-[80%] px-3 py-2 rounded-lg text-sm ${
38 msg.role === 'user'
39 ? 'self-end bg-blue-500 text-white'
40 : 'self-start bg-gray-100 text-gray-800'
41 }`}
42 >
43 {msg.content}
44 </div>
45 ))}
46 </div>
47
48 <form onSubmit={handleSubmit} className="flex gap-2">
49 <input
50 className="flex-1 border border-gray-200 rounded-md px-3 py-2 text-sm outline-none focus:border-blue-400"
51 placeholder="메시지를 입력하세요..."
52 value={input}
53 onChange={(e) => setInput(e.target.value)}
54 />
55 <button
56 type="submit"
57 className="border border-gray-200 px-4 py-2 rounded-md text-sm cursor-pointer hover:bg-gray-50 active:bg-gray-100 transition-all duration-200"
58 >
59 전송
60 </button>
61 </form>
62 </div>
63 );
64}
65페이지에서 채팅패널 적용 & requestAi 서버액션 호출하기
그리고 app/page.tsx를 아래와 같이 수정해줍니다.
1'use client';
2
3import { useState } from 'react';
4import { LineChart, LineChartProps } from './line-chart';
5import { QueryDataOptions, ToChartDataOptions, useQueryData } from './use-query-data';
6import { ChatMessage, ChatPanel } from './chat-panel';
7import { requestAi } from '../server/request-ai';
8
9function newMessage(role: ChatMessage['role'], content: string): ChatMessage {
10 return { id: crypto.randomUUID(), role, content, createdAt: Date.now() };
11}
12
13export default function Home() {
14 const [options, setOptions] = useState<LineChartProps['options']>({});
15 const [data, setData] = useState<LineChartProps['data']>({
16 datasets: [{ label: 'Unknown', data: [] }],
17 });
18 const { queryData, toChartData } = useQueryData();
19 const [messages, setMessages] = useState<ChatMessage[]>([]);
20
21 // 채팅 메시지 전송을 처리하는 핸들러
22 const handleSend = async (content: string) => {
23 setMessages((prev) => [...prev, newMessage('user', content)]);
24 const result = await requestAi(content);
25
26 // functionCall이 있다면, 해당하는 함수를 찾아서 호출한다.
27 if (result.functionCall?.name === 'createLineChart') {
28 const { queryOption, toChartDataOptions, chartOptions } = result.functionCall.args;
29 createLineChart(queryOption, toChartDataOptions, chartOptions);
30 }
31
32 const assistantMessage = newMessage('assistant', result.text);
33 setMessages((prev) => [...prev, assistantMessage]);
34 };
35
36 /**
37 * LineChart를 생성하는 함수. AI도 해당 함수를 사용해서 차트를 생성할 수 있어야 함.
38 *
39 * @param queryOption 쿼리 옵션. 이걸로 데이터의 범위를 제어함.
40 * @param toChartDataOptions 차트를 그리는데 필요한 포인트, 색상등의 정보.
41 * @param chartOptions 차트 옵션. 제목, 범례등의 옵션을 제어함.
42 */
43 const createLineChart = (
44 queryOption: QueryDataOptions,
45 toChartDataOptions: Omit<ToChartDataOptions, 'data'>,
46 chartOptions: LineChartProps['options'] = {},
47 ) => {
48 const data = queryData(queryOption);
49 const chartData = toChartData({
50 data,
51 ...toChartDataOptions,
52 });
53
54 setData(chartData);
55 setOptions(chartOptions);
56 };
57
58 return (
59 <div className="mx-auto flex gap-5 w-full h-screen p-10">
60 <div className="w-[60%] border border-gray-200 shadow-md rounded-md p-5">
61 <LineChart options={options} data={data} />
62 </div>
63
64 <div className="border border-gray-200 shadow-md rounded-md p-5 flex-1">
65 <ChatPanel messages={messages} onSend={handleSend} />
66 </div>
67 </div>
68 );
69}
70ChatPanel 컴포넌트를 사용해서 사용자에게 채팅 메시지를 보여주도록 구현해보았습니다. 코드가 좀 길었는데, 주요 코드를 따로 보도록 하겠습니다.
1// 채팅 메시지 전송을 처리하는 핸들러
2const handleSend = async (content: string) => {
3 setMessages((prev) => [...prev, newMessage('user', content)]);
4
5 const result = await requestAi(content);
6
7 // functionCall이 있다면, 해당하는 함수를 찾아서 호출한다.
8 if (result.functionCall?.name === 'createLineChart') {
9 const { queryOption, toChartDataOptions, chartOptions } = result.functionCall.args;
10 createLineChart(queryOption, toChartDataOptions, chartOptions);
11 }
12
13 const assistantMessage = newMessage('assistant', result.text);
14 setMessages((prev) => [...prev, assistantMessage]);
15};
16앞서 만든 서버액션인 requestAi를 호출하는 것을 볼 수 있는데요, 이 때 결과인 result에서 functionCall에 따라 해당하는 함수를 호출하는 것을 볼 수 있습니다. 함수를 호출하는데 까지 성공했다면, 아래와 같이 사용자 채팅메시지를 통해 차트를 생성하는 기능까지 완성입니다.
"사용자 채팅메시지를 기반으로 차트 생성"
Gemini API는 사용량이 워낙 많아서 간헐적으로 실패할 수 있습니다. 또, 저희는 Free Tier라서 사용량 초과 문제도 발생할 수 있습니다. 그러니 오류가 발생하는 경우, VS Code의 터미널에서 어떤 오류 메시지가 발생하는지 확인해주셔야 합니다. 만약 사용량 초과인 경우, 다른 모델로 변경하시거나 아니면 그냥 기존 Google AI Studio로 가셔서 기존 프로젝트를 지운 뒤 새로 만드셔도 됩니다.(이러면 할당량 초기화 되거든요)
중간 정리
여기까지 해서 LLM이 텍스트 생성 외에도 외부 시스템에 개입해서 함수를 호출하는 방법에 대해 알아보았습니다. 또한 실습을 통해 직접 Next.js에서 Gemini로 이를 구현해보았구요.
핵심은 아래와 같습니다.
- AI에게 사용 가능한 Tool 정보를 제공한다(Gemini의 경우 tools의 functionDeclarations)
- 시스템 프롬프트를 통해 어떤 상황에서 어떤 툴을 사용할지를 명시한다.
- AI의 응답에서 실행해야 하는 Tool 정보를 추출하고, 이를 토대로 필요한 함수를 호출한다.(response.functionCalls)
차트를 수정하는 Tool Calling 추가하기
이번엔 차트를 수정하는 Tool Calling도 구현해보겠습니다. 다만 수정 기능을 구현하기 위해선 히스토리 기능을 구현해야 합니다. LLM은 기본적으로 상태를 유지하지 않는 stateless 모델입니다. 따라서 LLM에게 차트를 수정해줘 라고 요청해도, LLM 입장에선 생성한 차트가 없으니 정상적으로 작업을 처리할 수 없게 됩니다.
이를 위해, 이전 대화 내용을 첨부해서 요청할 수 있도록 기능을 개선해야 합니다.
차트 수정을 위한 함수 작성하기(page.tsx)
app/page.tsx를 수정합니다.
import에 UpdateChartOptionsArgs를 추가합니다.
1import { requestAi, UpdateChartOptionsArgs } from '../server/request-ai';
2handleSend를 아래와 같이 수정하여, AI 응답결과에 따라 updateChartOptions 함수를 호출할 수 있도록 합니다.
또한, requestAi 함수를 호출할 때, 이전 메시지 기록도 인자로 전달하여 LLM이 문맥을 이해하고 답변할 수 있도록 합니다.
1const handleSend = async (content: string) => {
2 setMessages((prev) => [...prev, newMessage('user', content)]);
3
4 const result = await requestAi(content, messages);
5
6 if (result.functionCall?.name === 'createLineChart') {
7 const { queryOption, toChartDataOptions, chartOptions } = result.functionCall.args;
8 createLineChart(queryOption, toChartDataOptions, chartOptions);
9 } else if (result.functionCall?.name === 'updateChartOptions') {
10 updateChartOptions(result.functionCall.args);
11 }
12
13 const assistantMessage = newMessage('assistant', result.text);
14 setMessages((prev) => [...prev, assistantMessage]);
15};
16마지막으로 updateChartOptions를 구현합니다.
1const updateChartOptions = (args: UpdateChartOptionsArgs) => {
2 if (args.borderColor !== undefined || args.backgroundColor !== undefined) {
3 setData((prev) => ({
4 ...prev,
5 datasets: prev.datasets.map((ds) => ({
6 ...ds,
7 ...(args.borderColor !== undefined && { borderColor: args.borderColor }),
8 ...(args.backgroundColor !== undefined && { backgroundColor: args.backgroundColor }),
9 })),
10 }));
11 }
12
13 setOptions((prev) => ({
14 ...prev,
15 plugins: {
16 ...prev?.plugins,
17 ...(args.title !== undefined && {
18 title: { display: args.title !== '', text: args.title },
19 }),
20 ...(args.showLegend !== undefined && {
21 legend: { display: args.showLegend },
22 }),
23 },
24 scales: {
25 ...prev?.scales,
26 x: {
27 ...prev?.scales?.x,
28 grid: {
29 ...prev?.scales?.x?.grid,
30 ...(args.showGrid !== undefined && { display: args.showGrid }),
31 ...(args.gridColor !== undefined && { color: args.gridColor }),
32 },
33 },
34 y: {
35 ...prev?.scales?.y,
36 grid: {
37 ...prev?.scales?.y?.grid,
38 ...(args.showGrid !== undefined && { display: args.showGrid }),
39 ...(args.gridColor !== undefined && { color: args.gridColor }),
40 },
41 },
42 },
43 }));
44};
45차트 수정 Tool에 대한 Tool Calling 선언 작성하기(request-ai)
새로 추가한 updateChartOptions에 대한 선언을 작성해주어야 합니다. server/request-ai.ts를 연 다음... updateChartOptions에 대한 Tool 선언인 updateChartOptionsDeclaration을 추가합니다.
1const updateChartOptionsDeclaration: FunctionDeclaration = {
2 name: 'updateChartOptions',
3 description:
4 '현재 표시된 차트의 시각적 옵션(색상, 그리드, 제목, 범례 등)을 수정합니다. 데이터나 날짜 범위는 변경하지 않습니다.',
5 parameters: {
6 type: Type.OBJECT,
7 properties: {
8 borderColor: {
9 type: Type.STRING,
10 description: '데이터셋 선 색상 (CSS 색상값, 예: rgb(255, 99, 132))',
11 },
12 backgroundColor: {
13 type: Type.STRING,
14 description: '데이터셋 배경 색상 (CSS 색상값, 예: rgba(255, 99, 132, 0.2))',
15 },
16 showGrid: {
17 type: Type.BOOLEAN,
18 description: 'x/y 축 그리드 표시 여부',
19 },
20 gridColor: {
21 type: Type.STRING,
22 description: '그리드 선 색상 (CSS 색상값, 예: rgba(0, 0, 0, 0.1))',
23 },
24 title: {
25 type: Type.STRING,
26 description: '차트 제목. 빈 문자열("")이면 제목을 숨깁니다.',
27 },
28 showLegend: {
29 type: Type.BOOLEAN,
30 description: '범례 표시 여부',
31 },
32 },
33 },
34};
35그 다음 서버액션의 리턴타입을 아래와 같이 수정합니다.
1export interface UpdateChartOptionsArgs {
2 borderColor?: string;
3 backgroundColor?: string;
4 showGrid?: boolean;
5 gridColor?: string;
6 title?: string;
7 showLegend?: boolean;
8}
9
10export interface AiResponse {
11 text: string;
12 functionCall?:
13 | { name: 'createLineChart'; args: CreateLineChartArgs }
14 | { name: 'updateChartOptions'; args: UpdateChartOptionsArgs };
15}
16그리고 히스토리 기능 구현을 위해 아래와 같이 타입을 추가합니다.
1type HistoryItem = { role: 'user' | 'assistant'; content: string };
2requestAi 함수는 아래와 같이 수정합니다.
1export async function requestAi(
2 userMessage: string,
3 history: HistoryItem[] = [],
4): Promise<AiResponse> {
5 // 아래와 같이 히스토리 메시지를 추가하여 LLM이 문맥을 이해할 수 있도록 개선
6 const contents = [
7 ...history.map((msg) => ({
8 role: msg.role === 'assistant' ? 'model' : 'user',
9 parts: [{ text: msg.content }],
10 })),
11 { role: 'user', parts: [{ text: userMessage }] },
12 ];
13
14 const response = await ai.models.generateContent({
15 model: 'gemini-2.5-flash-lite',
16 contents,
17 config: {
18 // ...생략
19 tools: [
20 { functionDeclarations: [createLineChartDeclaration, updateChartOptionsDeclaration] },
21 ],
22 },
23 });
24
25 const functionCall = response.functionCalls?.[0] as FunctionCall | undefined;
26
27 // ... 생략
28
29 // updateChartOptions에 대한 처리 추가
30 if (functionCall?.name === 'updateChartOptions') {
31 const raw = functionCall.args as Record<string, string | boolean>;
32 return {
33 text: '차트 옵션을 수정했습니다.',
34 functionCall: {
35 name: 'updateChartOptions',
36 args: {
37 ...(raw.borderColor !== undefined && { borderColor: raw.borderColor as string }),
38 ...(raw.backgroundColor !== undefined && {
39 backgroundColor: raw.backgroundColor as string,
40 }),
41 ...(raw.showGrid !== undefined && { showGrid: raw.showGrid as boolean }),
42 ...(raw.gridColor !== undefined && { gridColor: raw.gridColor as string }),
43 ...(raw.title !== undefined && { title: raw.title as string }),
44 ...(raw.showLegend !== undefined && { showLegend: raw.showLegend as boolean }),
45 },
46 },
47 };
48 }
49
50 return { text: response.text ?? '응답을 생성하지 못했습니다.' };
51}
52이제 채팅패널에서 차트의 색을 바꾸거나 그리드를 없애달라고 요청하면 마법같이 반영되는 것을 확인할 수 있습니다.
"채팅메시지를 통해 차트를 수정하는 화면"
마치며
이번 포스트에선 LLM을 통해 외부 시스템을 제어하는 방법인 Tool Calling에 대해 알아보았습니다. 구현하기 어려운 기능이 아닐까 싶었지만 생각보다 간단했습니다. 어떤 Tool인지 설명과 파라미터에 대한 명세를 적어주고, 응답 결과를 통해 Tool Calling을 호출해야 하는지 확인하고 호출하는 방식이었는데요, 이를 통해 사용자를 보다 더 편하게 하는 애플리케이션을 개발할 수 있을거라고 생각합니다.
실습에서는 Gemini만 사용했지만, ChatGPT나 Claude API도 Tool Calling 기능을 제공합니다. 다만 함수 선언 방식이나 옵션등이 다소 차이가 날 수 있으니, 이 점만 유념해서 개발하면 됩니다.
이번 실습의 코드는 ai-tool-exam 저장소에서 확인하실 수 있습니다.
긴 글 읽어주셔서 감사합니다!