RAG 기반 맞춤형 요금제 추천 시스템 구축기
전 문서에 추천 방식이 개인화되기 위해서 왜 캐릭터 기반 추천에서 LLM을 추가한, RAG 방식의 추천 아키텍쳐로 바뀌었는지 설명드렸습니다. 통신사 고객 맞춤형 상품 추천을 위해 캐릭터 기반 추천에서 LLM + RAG 방식으로 아키텍쳐를 전환한 과정과 최종 아키텍쳐를 정리하였습니다. 기술 선택과 trade off, 그리고 최종 구조를 한눈에 볼 수 있도록 정리하였습니다. Spring boot 기반 메인 서비스와 Fast API 기반 AI 추천 서비스, PostgreSQL (pgVector) 벡터 검색, Open AI LLM을 결합한 아키텍쳐로 개인화된 상품 추천하는 구조입니다.
📚 Part 0. 기술 스택과 용어
이 문서와 추천 시스템을 이해하는 데 필요한 기술 스택과 핵심 용어를 정리합니다.
0.1 기술 스택
| 구분 | 기술 | 역할 |
|---|---|---|
| 클라이언트 | 웹/앱 | 사용자가 추천을 요청하는 진입점 |
| 메인 서비스 | Java, Spring Boot | API Gateway, 캐시 조회, FastAPI 호출, Kafka Consumer, 최종 응답 |
| AI 추천 서비스 | Python, FastAPI | 고객 컨텍스트 조회, 임베딩, 벡터 검색, LLM 호출, Kafka 발행 |
| DB | PostgreSQL + PgVector | 유저 컨텍스트·추천 캐시·상품 마스터·상품 임베딩 벡터 저장 |
| 메시지 큐 | Apache Kafka | FastAPI → Spring 간 비동기 추천 결과 전달 |
| 외부 AI | OpenAI API | Embedding(text-embedding-3-small), Text Generation(GPT 등) |
0.2 문서를 이해하기 위한 용어 정리
| 분류 | 용어 (Term) | 정의 및 역할 (Definition & Role) | 비고 / 핵심 기술 |
|---|---|---|---|
| AI 아키텍처 | RAG | 검색(Retrieval)으로 후보 상품을 뽑고, LLM이 추천 사유를 생성(Generation)하는 방식입니다. | Retrieval + Generation |
| AI 아키텍처 | JIT RAG | 요청 시점에 실시간으로 고객 컨텍스트를 구성하여 검색과 생성을 한 번에 처리합니다. | Real-time Context |
| 데이터 처리 | 임베딩 (Embedding) | 텍스트를 숫자 벡터로 변환하여 의미적 유사도를 계산할 수 있게 합니다. | OpenAI text-embedding-3-small |
| 데이터 처리 | PgVector | PostgreSQL 내에서 벡터 저장 및 유사도 검색(<=>, <#>) 연산자를 수행하는 확장 기능입니다. |
VECTOR(1536) 타입 사용 |
| 추천 전략 | Segment (세그먼트) | 고객을 이탈 위험(CHURN_RISK), 상향 판매(UPSELL), 일반(NORMAL)으로 분류하여 전략을 차별화합니다. | 전략적 라우팅 기준 |
| 추천 전략 | Persona (페르소나) | 유저의 성향(예: SPACE_SHERLOCK)에 따라 마케팅 문구의 톤앤매너와 선호 태그를 결정합니다. |
캐릭터 기반 개인화 |
| 데이터베이스 | member_llm_context |
추천에 필요한 고객 정보를 한 행으로 압축하여, 조회 속도를 극대화한 추천 전용 컨텍스트 테이블입니다. | 데이터 마트 성격 |
| 데이터베이스 | persona_recommendation |
최종 추천 결과를 저장하는 테이블로, 7일간의 추천 데이터를 캐시(Cache)로 활용합니다. | 7일 캐시 유지 |
| 데이터베이스 | Product 테이블 | 단순 마스터를 넘어 벡터 검색 대상, LLM 컨텍스트 제공, 태그 가중치 부여 등 다중 역할을 수행합니다. | embedding_vector필드 포함 |
| 비동기 통신 | CompletableFuture | Spring에서 FastAPI 호출 결과를 기다리는 동안 HTTP 연결을 유지하기 위해 사용하는 비동기 객체입니다. | 비동기 응답 처리 (Java) |
| 비동기 통신 | Kafka | FastAPI의 생성 결과와 Spring의 DB 적재 및 응답 로직을 분리하여 시스템 안정성을 높이는 메시지 큐입니다. | recommendation-topic |
Part 1. 배경: 캐릭터 기반 → LLM RAG로의 전환
전 문서에서 다뤘지만, 개인화가 되려면 누구에게, 어떤 상황에서, 어떤 문장으로 제안할지가 중요합니다. 기존에 캐릭터 기반 추천만으로 고객의 최근 상담, 사용량, 이탈 위험, 최근 본 상품이 반영된 맥락 있는 추천이 어렵다고 판단하였고, RAG 방식을 도입하였습니다.
- Retrieval: 고객 상태와 상황을 텍스트로 요약하고 임베딩하여, 상품 벡터와의 유사도로 검색을 하여 추천 후보를 생성했습니다.
- Generation: 후보 상품 + 고객 context를 LLM에게 넘겨서, 세그먼트 / 고객 캐릭터에 맞는 추천이유와, 마케팅 문장을 생성하기 위해 LLM을 도입하였습니다.
최종적으로, Spring Boot 메인 서비스 + FastAPI AI 추천 서비스 + PostgreSQL(PgVector) + OpenAI를 결합한 아키텍처로, 개인화된 상품 추천을 제공하는 구조가 되었습니다.
Part 2. 최종 아키텍쳐가 나오기까지의 고민
각 결정의 이유와 Trade off를 중심으로 정리하였습니다.
고민 1. 왜 Python FastAPI 서버를 썼는가? (Java LangChain4J 대안)
다음과 같은 고민이 되었습니다. AI 추천 로직을 메인 서비스(Spring Boot) 안에 LangChain4j로 넣을지, 별도 Python FastAPI 서비스로 뺄지.
핵심 비교
| 기준 | Spring Boot + LangChain4j | Python FastAPI + LangChain |
|---|---|---|
| LangChain 생태계 완성도 | Java 포팅, 기능 지연 있음 | 원조, 기능 즉시 사용 |
| 커뮤니티 / 레퍼런스 | 매우 적음 | 압도적 다수 (예제 대부분 Python) |
| PgVector 연동 | pgvector-spring 존재하나 미성숙 | Python 클라이언트 성숙 |
| 비동기 성능 | Webflux 필요 (복잡도 증가) | async/await 기본 |
| LLM 신기능 반영 속도 | Java 포팅 대기로 느림 | 즉시 반영 |
| 팀 개발 속도 | Spring 팀이 API + AI 모두 관리 | AI 로직만 독립 관리 ⭐️ |
| 인프라 복잡도 | 단일 서버 (단순) | 서버 2대 운영 (복잡) |
| 독립 스케일 아웃 | API 서버 전체 스케일 | LLM 서버만 선택적 스케일 ⭐️ |
| 장애 격리 | LLM 오류 시 API 서버 전체 영향 | LLM 서버 다운 시 API 서버는 무관 |
| 배포 독립성 | LLM 로직 변경 시 전체 재배포 | LLM 서버만 재배포 |
파이썬에서는, LangChain Expression Language 기반의 체이닝과, LangSmith와 RAGAS등 품질 평가와 디버깅 도구가 있었고, Java에서도 기존서비스와 통합을 한다면, 설정이 단순하고, 컴파일 타임 타입의 안정성이라는 장점이 있었습니다.
하지만 Python 서버를 따로 두기로 한 이유는 다음과 같습니다.
- PgVector + SQL 하이브리드 검색 구현 난이도 Python에서는 SQLAlchemy + pgvector로 가중치를 주거나, 벡터 정렬을 한 쿼리로 표현하기 쉽습니다. LangChain4J의 EmbeddingStore에서는 SQL 분기처리와, fallback을 넣는것은 번거롭고, JDBC와 벡터 연산자를 직접 조합해야할 가능성이 있습니다.
- 세그먼트별 전략 라우팅과 fallback 유연성 저희는 고객을 이탈위험군, upselling 대상, 그 이외의 대상으로 고객을 나눴습니다. sql 분기나 커스텀 파이프라인은 LangChain Python이 흐름제어하기에 적합하다고 판단되었습니다.
- 임베딩 모델 교체 용이성 현재는 Open AI text embedding 3 small 모델을 사용하고 있지만, 향후 다른 모델으로 바꿀때, python은 설정만 바꾸면 되기에 향후 확장성을 고려하였습니다.
- 테스트 전략 Python에서 RAG의 성능 품질을 판단하는, RAGAS, LLM-as-a-Judge가 파이썬 생태계에 맞춰있습니다. 향후 품질 테스트를 하였을때 용이하다 판단하였습니다.
결론적으로
추천 서버의 독립성과 구현 난이도, 유지보수, 확장성을 기반으로 Python FastAPI를 도입하기로 결정하였습니다.
고민 2. 상품 임베딩 & 임베딩 모델 선택
Retrieval 단계에서 유사도 검색을 하기 위해서는 상품을 임베딩 벡터로 표현을 해야했습니다. 다음과 같은 과정으로 임베딩을 진행하였습니다.
상품 임베딩 (초기 1회성 구축)
- Postgres에서 상품 정보를 읽어와, LLM이 이해하기 쉬운 구조화된 텍스트로 변환을 하고, 타겟 사용자를 명시하는 것이 핵심이었습니다.
- 사전에 텍스트 추출 기준으로 상품마다 태그를 걸어두고, 상품 정보를 조합해
embedding_text를 만든 후 OpenAI text-embedding-3-small로 1536차원 벡터를 생성해product.embedding_vector에 update를 하였습니다.
왜 Open AI 임베딩 모델을 썼는가?
우선, 검색 품질과 단기 프로젝트 특성상 llm을 파인튜닝하는 것은 오버엔지니어링이라고 판단하였고, open ai의 범용 모델을 사용하여, 비용과 지연이 적은 모델을 도입하고자 하였고, 차원이 1536인 text-embedding-3-small 모델을 사용해, PgVector 차원과 맞춰 관리하기 용이 하였습니다.
고민 3. FastAPI가 호출될때 어디서 데이터 가져올까?
기존 상품과 고객의 정보가 PostgreSQL에 여러 테이블에 혼재되어 있었고, 로그 데이터와 최근 본 상품 상담 데이터 테이블이 혼재되어 있어 추천 api를 불러올때, 여러 테이블을 조인하는 시간이 많아질 것 같다는 판단을 하였고, 응답 지연으로 이어져 서비스에 영향을 줄 것 같다고 판단하였습니다.
기존 캐릭터 분류 배치 작업때 member_llm_context 테이블에 llm context에 사용될 데이터들을 고객 한명당 한 행으로 넣어, 응답 속도 개선과 연산 부담을 배치로 이전하는 결정을 하였습니다. 또한, 추천된 결과를 persona_recommendation 테이블에 저장을 하여서 매번 llm을 호출하는 비용을 절감하는 선택을 하였습니다.
왜 따로 persona_recommendation 테이블에 저장을 했을까?
- 7일 이내 동일 고객이 재요청시 LLM 재호출 없이 즉시 응답을 내려줄 수 있도록 하였습니다. (비용 절감) → 따로 DB에 저장을 하여, 바로 응답을 내릴 수 있습니다.
- Spring이 캐시조회와 캐시 미스시 FastAPI 호출 후 결과 수신 및 저장을 담당하고, Fast API는 생성만 담당하는 책임 원칙 분리를 지켰습니다.
고민 4. 왜 Kafka를 쓸까? CompletableFuture를 왜 썼는가?
이미 로그 파이프라인과 키워드 상담 배치에도 카프카 MSK를 쓰고 있는 상태였습니다. 그리고 다음과 같은 요구사항이 있었습니다.
Spring
persona_recommendation조회 → 7일 이내이면 즉시 응답- 없으면 FastAPI 호출 후, 반드시 그 요청에 대해 생성된 결과를 프론트에 돌려줘야 함
FastAPI
member_llm_context조회 → 프롬프트 조립 → OpenAI 호출 → 결과를 어딘가에 전달해야함- Spring이 그 결과를 DB에 적재하고 같은 HTTP 연결로 프론트에 응답해야합니다.
동기 방식의 한계
- FastAPI가 LLM까지 실행한 후 HTTP 응답 결과를 주면, LLM 지연동안 HTTP 연결이 계속 유지해야하고, 이것은 타임아웃, 리소스 부담까지 이어집니다.
이러한 한계로 선택한 방식은, Spring은 FastAPI에 요청하자마자, 202 Accepted를 받고, FastAPI는 llm 메시지의 결과를 kafka recommendation-topic으로 발행하고, Spring의 Kafka Consumer가 메시지를 받으면, Spring에서 member_id로 보관해둔, CompletableFuture를 찾아 complete 메소드를 호출하고, 최종 응답이 나가 연결이 종료되는 과정입니다.
구체적인 과정
- Spring은 DB에서 일주일 내로 추천된 응답을 받지 못하면,
CompletableFuture를 만들고,member_id를 키로 메모리에 보관합니다. (ConcurrentHashMap) → “이 member_id에 대한 결과가 나오면 이 Future에 채워 넣고, 대기 중인 HTTP에 응답한다” - Spring이 FastAPI에
POST /api/v1/recommendations { memberId }를 보냅니다. - FastAPI는 요청을 받자마자 202 Accepted만 반환합니다. → Spring–FastAPI HTTP는 여기서 끝나고, 실제 LLM 작업은 FastAPI 백그라운드에서 수행합니다.
- FastAPI는 LLM 결과를 Kafka recommendation-topic으로 발행합니다. → FastAPI는 DB에 쓰지 않고, “결과를 토픽에 넣는 것”만 책임집니다.
- Spring의 Kafka Consumer가 메시지를 받으면
persona_recommendation에 INSERT/UPDATE- 메시지의
member_id로 보관해 둔 CompletableFuture를 찾아complete(결과)호출
- 그 Future를 기다리던 프론트로 향한 HTTP 연결에 최종 응답이 나가며 연결이 종료됩니다
카프카를 쓴 이유
- FastAPI와 Spring이 프로세스/서버가 분리되어 있어, “FastAPI가 끝났을 때 Spring에게 알려 주는” 수단이 필요합니다.
- Http polling 이나, 긴 연결 대신 이벤트 기반으로 하기 위해 메시지 큐를 사용하였습니다.
- 이미 다른 서비스에서 카프카를 사용하고 있었기 때문에 도입하기 쉬웠습니다.
Part 3. 최종 아키텍쳐 정리
3.1 전체 구조

전체 시스템은 Client → 메인 서비스(Spring Boot) → AI 추천 서비스(FastAPI) → DB 및 외부 AI API 순으로 구성됩니다. 우선 다음은 전반적 AI를 도입한 추천 시스템의 아키텍쳐 입니다.
- 클라이언트: 추천 API 요청 (GET, Access Token 등)
- Spring Boot: API Gateway, 캐시(
persona_recommendation7일) 조회, 캐시 미스 시 FastAPI 호출, Kafka Consumer로 결과 수신·DB 적재·CompletableFuture 완료, 프론트에 최종 응답 - FastAPI:
member_llm_context조회, 고객 컨텍스트 임베딩, PgVector 유사도 검색, 세그먼트/페르소나 맞춤 프롬프트로 LLM 호출, 결과를 Kafka로 발행 - PostgreSQL
- PgVector 확장으로 **상품 임베딩 벡터(1536차원)** 저장·검색
- 일반 테이블: member_llm_context(유저 컨텍스트), persona_recommendation(추천 캐시)
- OpenAI: Embedding, Text Generation
3.2 단계별 흐름 (요청부터 응답까지)
1. 추천 요청 (Client → Spring Boot)
클라이언트가 추천 API를 호출하면 Spring Boot가 진입점이 됩니다. API Gateway 역할을 하며, 추천 캐시를 관리합니다.
2. 캐시 확인 (HIT 시 즉시 반환)
persona_recommendation에서 해당 회원의 7일 이내 데이터를 조회합니다.
유효한 캐시가 있으면 LLM을 호출하지 않고 캐시된 데이터를 그대로 응답하고 종료합니다.
3. 캐시 미스 시 FastAPI 호출
캐시가 없거나 만료되었으면, CompletableFuture를 생성해 member_id로 보관한 뒤,
FastAPI로 POST /api/v1/recommendations (member_id 포함)를 보냅니다.
프론트와의 HTTP 연결은 이 Future가 완료될 때까지 유지됩니다.
4. FastAPI: 유저 컨텍스트 로드
member_llm_context에서 해당 회원의 데이터 사용량, 페르소나, 최근 본 상품, 최근 상담 이력 등 LLM에 필요한 컨텍스트를 조회합니다.
5. 유저 컨텍스트 임베딩
조회한 고객 컨텍스트(또는 retrieval_query_text)를 OpenAI Embedding API로 1536차원 벡터로 변환합니다.
(RAG의 “query text 생성 + 임베딩” 단계)
6. 벡터 유사도 검색
고객 쿼리 벡터와 PgVector에 저장된 상품 임베딩을 비교해, Top-K 후보 상품을 검색합니다. (Retrieval 단계. 필요 시 태그·세그먼트 가중치 적용)
7. 세그먼트/페르소나 맞춤 LLM 호출
고객 세그먼트·페르소나에 맞는 시스템 프롬프트와, 고객 정보 + 후보 상품 목록을 담은 유저 프롬프트를 OpenAI Text Generation API에 보내, 최종 추천 상품 3개와 이유·마케팅 문장을 JSON으로 받습니다.
8. Kafka 발행
FastAPI는 생성된 결과를 recommendation-topic으로 발행합니다. (Spring과의 HTTP는 이미 202로 끝난 상태)
9. Spring Kafka Consumer: DB 적재 & 응답 완료
Spring이 메시지를 consume하면:
persona_recommendation에 INSERT/UPDATE- 메시지의 member_id에 해당하는 CompletableFuture에 결과를 넣어 complete
→ 대기 중이던 HTTP 연결로 최종 응답이 나가며 연결이 종료됩니다.
3.3 핵심 테이블 역할 요약
| 테이블 | 역할 |
|---|---|
| product | 상품 마스터 + embedding_text + embedding_vector(1536). Retrieval 대상, LLM 컨텍스트, 태그 가중치, 최종 응답 원본. |
| member_llm_context | 고객 상태를 한 행으로 압축. 배치에서 미리 채움. FastAPI가 여기서만 읽어 쿼리/프롬프트 구성. |
| persona_recommendation | 최종 추천 결과 저장. retrieved_candidates, recommended_products, 마케팅 문장 등. 7일 이내는 캐시로 사용. |
Part 4. 세그먼트·페르소나·Retrieval·Generation 요약
4.1 1차 룰 기반 Segment 판정
- CHURN_RISK: 이탈 위험 높음 (
churn_tier=HIGH_RISK, penalty_click, 미납, 약정 만료, 해지/위약금 관련 상담 등) - UPSELL: 이탈 위험은 낮으나 상향/교차 판매 가능
Segment에 따라 Retrieval 필터 (고가 제외, 중복 제외, 프리미엄 강조)와 LLM 시스템 프롬프트가 달라진다.
4.2 Persona 분류와 추천 방향
- SPACE_SHERLOCK: 최저가·제휴카드·효율 → 저가형, 할인, 절약 톤
- SPACE_GRAVITY: 가족결합, 인터넷+TV, 재약정
- SPACE_OCTOPUS: 멀티디바이스, 쉐어링, 테더링, 워치/태블릿
- SPACE_SURFER: OTT, 콘텐츠, 미디어
- SPACE_GUARDIAN: 보안, 안정성, 안심
- SPACE_EXPLORER: 자동 혜택, 리필 쿠폰, 장기 고객
각 페르소나별 선호 태그와 마케팅 문장 스타일이 정의되어 있고, persona_style_prompt로 LLM에 전달됩니다.
4.3 Retrieval 단계
- 대상:
product의embedding_text,embedding_vector,tags - 쿼리:
member_llm_context기반으로 retrieval_query_text 생성 → 동일 임베딩 모델로 벡터화 - 점수:
final_score = vector_similarity_score + recent_viewed_tag_bonus + persona_tag_bonus + segment_tag_bonus + product_type_bonus - duplicate_penalty - 필터: CHURN_RISK는 고가·중복 제외·부담 완화 우선, UPSELL은 같은/상위 가격·결합 강조, NORMAL은 관심 태그+인기 혼합 등 (SPEC 참고)
4.4 Generation 단계
- Retrieval에서 뽑은 **Top-K(약 7개)** 후보를 LLM에 넘깁니다.
- Segment가 추천 제약(무엇을 추천할지, 무엇을 피할지)을 결정하고, Persona가 마케팅 문구와 reason 스타일을 결정합니다.
- System Prompt: segment 공통 프롬프트(CHURN_RISK/UPSELL/NORMAL) + persona 스타일 프롬프트
- User Prompt: 고객 context(프로필, 구독, 사용량, 이탈, 최근 행동, 페르소나 지표, 상담 이력 포함) + 후보 상품 목록(
format_products) + JSON 출력 규칙
최종 recommended_products JSON 구조와 format_products 포맷은 초안 및 LLM_RAG_RECOMMENDATION_SPEC.md의 “User Prompt”, “상품 목록 주입 포맷”을 따릅니다.
부록 A. member_llm_context 컬럼 매핑 (상세)
LLM 호출 직전 고객 상태를 한 행으로 압축한 테이블이다. 배치에서 아래 규칙으로 채운다.
| 컬럼명 | 타입 | 소스 테이블/컬럼 | 생성 규칙 / 비고 |
|---|---|---|---|
| member_id | BIGINT PK | member.member_id | 회원 PK |
| membership | VARCHAR(20) | member.membership | GOLD / VIP / VVVIP 등 |
| age_group | VARCHAR(10) | member.birth_date | 현재 나이 기준 10대/20대/30대 등 |
| join_months | INTEGER | member.join_date | 현재일 - 가입일 개월 수 |
| children_count | INTEGER | member.children_count | 자녀 수 |
| family_group_num | INTEGER | member.family_group_id 기반 집계 | 가족 결합 그룹 인원 수 |
| family_role | VARCHAR(30) | member.family_role | 대표 / 구성원 등 |
| persona_code | VARCHAR | index_persona_snapshot.persona_code | 최종 캐릭터 식별값 (예: SPACE_SHERLOCK) |
| segment | VARCHAR(20) | 계산값 | CHURN_RISK / UPSELL / NORMAL |
| current_subscriptions | JSONB | subscription + product | 현재 구독 중 상품 목록 스냅샷 |
| current_product_types | JSONB | subscription + product | 예: {“MOBILE_PLAN”: true, “INTERNET”: true} |
| current_data_usage_ratio | INTEGER | usage_monthly | (현재 사용량 / 제공량) * 100 |
| data_usage_pattern | VARCHAR(10) | usage_monthly | OVER / FIT / UNDER, 무제한은 NULL |
| churn_score | INTEGER | 별도 churn 모델 | 이탈 점수 퍼센트 |
| churn_tier | VARCHAR(20) | churn_score 기반 | HIGH / MEDIUM / LOW |
| recent_counseling | TEXT | support_case.title | 최근 최대 3개 상담 제목 묶음 |
| product_type_clicks | JSONB | user_event_features_7d.product_type_clicks | 프로덕트 타입별 클릭 수, 예: {“INTERNET”:3} |
| recent_viewed_tags_top_3 | JSONB | user_event_features_7d.product_type_top_tags | 최근 관심 태그 상위 3개 |
| contract_expiry_within_3m | BOOLEAN | subscription.contract_end_date | 약정 만료일이 3개월 이내면 true |
| updated_at | TIMESTAMPTZ | 시스템 생성 | 최신 갱신 시각 |
retrieval_query_text는 컬럼이 아니라, 호출 시점에 build_retrieval_query_text(ctx)로 생성한 뒤 임베딩해 query_vector로 사용한다.
부록 B. persona_recommendation 역할 & 최종 응답 JSON 구조
persona_recommendation은 최종 추천 결과를 저장하는 테이블이다.
- retrieved_candidates: Retrieval 단계에서 뽑힌 상품 스냅샷
- recommended_products: 최종 선택된 3개 상품 스냅샷 + LLM reason (아래 JSON과 동일 구조)
프론트엔드에 내려줄 최종 recommendation_products JSON 구조 예시:
{
"marketing_message": "고객 맞춤 마케팅 한 문장",
"cached_llm_recommendation": "전체 추천 요약 2~3문장",
"recommended_products": [
{
"product_id": 101,
"product_name": "상품명",
"product_price": 65000,
"sale_price": 59000,
"product_type": "MOBILE_PLAN",
"reason": "현재 데이터 사용량이 제공량을 초과하고 최근 OTT 태그 상품을 자주 보셔서, 콘텐츠 혜택과 데이터 여유를 함께 얻을 수 있는 상품입니다."
}
]
}
부록 C. Retrieval 점수 공식 & 상품 목록 주입 포맷
참고