[U+Pick #1] Function Calling 기반 결합 할인 챗봇 구축
✅ 오늘의 목표
- GPT-4.1-mini Function Calling으로 가족결합 할인 계산 기능 구현
- 프론트 React + 백엔드 Express 기반 챗봇 서비스 구축
- 결합 할인 금액 계산 로직 모듈화 및 Function Schema 설계
🧩 문제 상황 / 배경
- LG U+ 요금제에 대한 상담/추천 기능을 챗봇 형태로 제공하고 싶었음
- 가족결합 할인 계산은 단순 텍스트 답변으로 제공하기에는 정확도가 낮고 복잡한 규칙을 포함
- GPT Function Calling 기능을 사용하면 실제 계산 로직과 GPT의 자연어 처리를 유연하게 조합할 수 있다고 판단
- React 기반의 심플한 챗 UI로 사용자 경험을 테스트하고자 함
🛠️ 해결 과정 / 코드 정리
🔸 프로젝트 구조
1
2
3
4
5
6
7
8
9
10
11
12
backend/
├── server.js
├── openai
├── client.js
├── controllers
├── gptChat.js
├── functions
├── gptFunctions.js
├── calculateFamilyBundle.js
frontend/
├── components
├── ChatBox.jsx
🔸 가족결합 할인 계산 로직 모듈화
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// backend/functions/calculateFamilyBundle.js
export function calculateFamilyBundle(prices) {
const phoneCount = prices.length;
const discounts = prices.map((price) => {
if (phoneCount >= 4) {
if (price >= 88000) return 8800;
if (price >= 69000) return 6600;
return 4400;
}
if (phoneCount === 3) {
if (price >= 88000) return 6600;
if (price >= 69000) return 5500;
return 3300;
}
if (phoneCount === 2) {
if (price >= 88000) return 4400;
if (price >= 69000) return 3300;
return 2200;
}
return 0;
});
const total = discounts.reduce((sum, d) => sum + d, 0);
return { phoneCount, discounts, totalDiscount: total };
}
✅ 가격별로 할인율을 계산하는 함수를 별도로 분리
✅ 할인 총액, 인원수를 함께 리턴
🔸 Function Calling Schema 설계 - backend/functions/gptFunctions.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export const calculateFamilyBundleDiscountSchema = {
name: "calculateFamilyBundleDiscount",
description: "가족 결합 할인 계산",
parameters: {
type: "object",
properties: {
planNames: {
type: "array",
items: { type: "string" },
description: "결합할 요금제 이름 리스트",
},
planPrices: {
type: "array",
items: { type: "number" },
description: "결합할 요금제 가격 리스트",
},
},
},
};
✅ GPT가 어떤 매개변수를 보내야 하는지 명확히 정의 (planNames, planPrices)
✅ ChatGPT가 상황에 따라 자동으로 함수 호출을 시도할 수 있도록 설계
🔸 GPT Chat 컨트롤러 구성 및 Function Calling 처리 흐름
이 서비스의 가장 핵심 부분은 gptChatHandler
- 사용자가 입력한 메시지를 GPT-4.1-mini로 전달
- 상황에 따라 Function Calling을 유도
- 실제 할인 계산 로직과 연계
- 최종 결과를 자연어 답변으로 변환 후 응답
✅ 코드 흐름
1
사용자 입력 → 키워드 룰 검사 → GPT 호출 → Function 호출 → 할인 계산 → 최종 답변
✅ systemPrompt 역할
1
2
3
4
5
const systemPrompt = `
당신은 친절하고 전문적인 LG U+ 통신 요금제 추천 상담사입니다.
...
- 고객이 요금제명 또는 요금제 가격을 입력한 경우, 반드시 calculateFamilyBundleDiscount 함수를 호출하세요.
`;
→ GPT가 어떤 어조로, 어떤 기능을 써야 하는지 명확하게 유도하는 역할
→ Function Calling을 자동으로 유도하기 위해 “반드시 함수를 호출하세요” 문구 사용
✅ 키워드 기반 선처리 (Rule-based fallback)
1
2
3
4
5
6
7
8
9
10
11
const keywordRules = [
{
pattern: /지인\s?결합/,
response:
"지인 결합 할인에 대한 정보는 저희 서비스에서 확인할 수 있어요\n`U+Pick url`",
},
{
pattern: /어떤\s?결합.*(할인|가능|받을|있나|알려)/,
response: "지인 결합과 가족 결합 중 어떤 결합으로 알려드릴까요?",
},
];
→ GPT 호출 전에 패턴 매칭으로 즉시 응답이 가능한 경우 처리 → 챗봇 UX의 반응속도와 응답 일관성 확보 → 아주 단순한 FAQ 성 패턴들은 빠르게 대응 (API 비용 절감 효과도 있음)
? 정규식 코드 의미
✅ Function 호출 흐름
1
2
3
4
5
6
const response = await openai.chat.completions.create({
model: "gpt-4.1-mini",
messages: systemMessages,
functions,
function_call: "auto",
});
→ GPT가 상황을 보고 알아서 calculateFamilyBundleDiscount
를 호출할지 말지 선택하도록 구성 → 자동으로 사용자 입력과 함수 호출 흐름을 자연스럽게 연결
✅ 함수 호출 여부 판단
1
2
3
4
5
const funcCall = choice.message.function_call;
if (!funcCall) {
return res.send(choice.message.content);
}
→ GPT가 함수 호출이 필요 없다고 판단한 경우, 바로 자연어 응답을 리턴 → 반면, 호출한 경우에는 arguments 를 파싱해서 할인 계산 단계로 넘어감
✅ 요금제명 or 가격 매핑 처리
1
2
3
4
5
6
7
8
9
10
if (args.planNames && Array.isArray(args.planNames)) {
prices = args.planNames
.map(findClosestPlanPrice) // testPlans 에서 price 가져옴
.filter((p) => p !== null);
}
else if (args.planPrices && Array.isArray(args.planPrices)) {
prices = args.planPrices
.map((p) => parseInt(p, 10))
.filter((p) => !isNaN(p));
}
→ GPT가 planNames
로 요금제명을 주는 경우도 있고, planPrices
로 숫자를 주는 경우도 있음 → 두 가지 모두 대응할 수 있도록 분기 처리
✅ findClosestPlanPrice()
함수 설계 의도
1
2
3
4
5
6
7
8
9
function findClosestPlanPrice(planName) {
const exactMatch = testPlans.find((p) => planName.includes(p.name));
...
const aliasMatch = testPlans.find((p) =>
p.alias?.some((alias) =>
lowerInput.includes(alias.toLowerCase().replace(/\s/g, ""))
)
);
}
→ 사용자가 입력한 요금제명이 정확하지 않거나 띄어쓰기 오류가 있어도 → 유사한 alias 매칭으로 최대한 가격을 찾아낼 수 있도록 설계 → 실제 상담 시 사용자가 입력하는 텍스트가 깨끗하지 않은 경우가 많기 때문에 robust한 매칭 로직 필요
✅ 할인 계산 후 최종 응답 흐름
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const result = calculateFamilyBundle(prices);
const finalResponse = await openai.chat.completions.create({
model: "gpt-4.1-mini",
messages: [
...messages,
{
role: "function",
name: "calculateFamilyBundleDiscount",
content: JSON.stringify(result),
},
],
});
res.send(finalResponse.choices[0].message.content);
→ 할인 계산 후, function 응답을 GPT에 다시 넘겨서 자연어로 표현하게 구성 → GPT가 최종적으로 사용자가 읽기 쉬운 형태로 응답을 완성 → ex) “3회선 기준 총 17,600원의 할인이 적용됩니다!”
✅ 전체 설계 포인트
포인트 | 설명 |
---|---|
Function Calling | 정확한 계산이 필요한 부분과 GPT의 자유로운 응답을 적절히 분리 |
Robust 매칭 | 사용자의 입력 변형에 대응하는 유사도 매칭 로직 |
선처리 룰 | 패턴 매칭으로 간단한 질문은 빠르게 대응 |
Modular 구조 | 할인 로직/스키마/API 요청부가 명확히 모듈화됨 |
대화 기록 유지 | 이전 messages 전체 history를 유지하며 컨텍스트 반영 |
✅ 개인적인 느낀 점
gpt-4.1-mini
모델이라도 Function Calling이 잘 동작해서 규칙 기반 계산이 매우 안정적으로 연결 가능- 기존 룰 기반 챗봇보다 훨씬 자연스러운 UX 구현 가능
- Function arguments 매핑에서 GPT가 가끔 불완전한 arguments를 주는 경우가 있어서, fallback이나 validation 로직을 좀 더 강화하면 좋겠다는 개선 포인트도 발견
🔸 프론트엔드 챗 UI 구성 - frontend/components/ChatBox.jsx
1
2
3
4
5
6
7
8
const [messages, setMessages] = useState([]);
const res = await fetch("http://localhost:5001/api/gpt/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ messages: newMessages }),
});
const data = await res.text();
setMessages([...newMessages, { role: "assistant", content: data }]);
✅ React에서 간단한 chat history 구성
✅ 사용자가 메시지 입력 시 서버에 기존 messages 배열과 함께 전달
✅ 서버 응답을 받아 chat 창에 append
✅ loading 표시 추가
🖼️ 작업 결과물
✅ 결과 및 느낀 점
- GPT Function Calling을 활용하면 규칙 기반 계산과 자연어 응답을 효과적으로 결합할 수 있다는 점을 경험
- 기존 단순 텍스트 프롬프트 기반보다는 신뢰도 높은 할인 계산이 가능
- React 간단한 챗 UI를 통해 빠르게 프로토타이핑 가능
- GPT-4.1-mini의 응답 속도가 생각보다 빨라 실시간 상담형 UX에도 적용 가능성을 확인