-
[project]성능평가3FISA 2026. 6. 8. 10:38
AI 추천 시스템 개발 및 성능 평가
1. 시스템 개요
MoneyLog는 청년을 위한 개인 금융 관리 애플리케이션으로, 사용자의 소비 패턴을 분석해 맞춤형 카드, 보험, 청년 정책을 추천하는 AI 추천 시스템을 핵심 기능으로 합니다.
추천 파이프라인은 LangGraph를 기반으로 profile → embed → vector_search → rerank → filter → graph_expand → conflict → llm → save 순서로 구성되며, pgvector, Neo4j AuraDB, AWS Bedrock Titan, Cross-Encoder 리랭커가 각 단계에서 역할을 담당합니다.
2. 추천 파이프라인 단계별 개발
Stage 1: 벡터 검색 기반 추천
사용자의 월간 소비 패턴을 텍스트로 변환하고 AWS Bedrock Titan Embed V2로 임베딩한 뒤, pgvector에 저장된 상품 임베딩들과 코사인 유사도를 계산해 가장 가까운 상품을 추천하는 구조입니다.
이 단계에서 LLM Judge 기준 55.1점이 나왔습니다. 나이나 소득 조건이 맞지 않는 정책도 텍스트가 비슷하면 상위에 올라오고, 동시에 신청할 수 없는 정책들이 함께 추천되어도 걸러낼 방법이 없다는 한계가 있었습니다.
Stage 2: Neo4j 그래프 DB 도입
정책 간 충돌 관계와 카테고리 연결을 처리하기 위해 Neo4j AuraDB를 도입했습니다.
동일 주관기관, 동일 카테고리의 정책들은 중복 신청이 불가능하다는 규칙을 CONFLICTS_WITH 엣지로 표현하고, conflict 노드에서 이를 감지해 LLM 프롬프트에 명시적으로 전달했습니다.
또한 상품 간 IN_CATEGORY, TAGGED_WITH, SIMILAR_TO 관계를 그래프로 구성해 벡터 검색이 놓친 의미적 연관 상품을 Graph Retrieval로 추가 탐색하는 방식을 적용했습니다.
이 단계에서 LLM Judge 점수는 50.8점으로 오히려 하락했습니다. 분석 결과, 현재 데이터 규모에서 그래프 확장이 pgvector가 이미 잘 찾은 후보를 덜 적합한 상품으로 교체하는 부작용이 있었습니다. 다만 conflict 감지 기능은 정상 동작했고, 이는 다음 단계에서 확인되었습니다.
Stage 3: Cross-Encoder 리랭커 추가
Neo4j로 인한 후보 교체의 부작용을 보완하고 벡터 검색 후보의 품질을 높이기 위해 Cross-Encoder 기반 리랭커를 추가했습니다.
Bi-Encoder 방식의 벡터 유사도보다 쿼리와 문서를 함께 인코딩하는 Cross-Encoder가 더 정교한 관련성 판단이 가능합니다. Bedrock Cohere Rerank API가 ap-northeast-2 리전에서 지원되지 않아 로컬 모델로 전환했고, 한국어 특화 모델인 bongsoo/klue-cross-encoder-v1을 채택해 30개 후보를 7개로 압축한 뒤 LLM에 전달하는 구조로 변경했습니다.
리랭커 추가 후 LLM Judge 60.2점으로 Stage 2 대비 9.4점 향상되었습니다. 특히 Faithfulness가 0.54에서 0.75로 39% 향상되었는데, 리랭커가 유저 소비 맥락과 더 맞는 상품을 앞에 배치하면서 LLM이 추천 사유를 작성할 때 소비 키워드를 더 자연스럽게 언급하게 된 결과입니다.
Stage 4: 소득 조건 필터 추가
LLM Judge 평가에서 소득 조건이 맞지 않는 정책이 추천되는 문제가 반복적으로 지적되었습니다. filter_node에 소득 조건 텍스트 파싱 로직을 추가해 "중위소득 150% 이하", "연소득 5천만원 이하" 같은 다양한 표현을 2025년 1인가구 기준 중위소득(월 239만원)을 기준으로 파싱하고 검증하도록 개선했습니다. 최종 LLM Judge 66.6점으로 Stage 1 대비 11.5점(+21%) 향상되었습니다.
3. Ablation Study 최종 결과
스테이지 LLM 적합성 사유 조건 다양성 Faith ILD Stage 1: 벡터만 55.1 21.0 17.9 7.5 8.0 0.593 0.558 Stage 2: +Neo4j 50.8 18.8 14.2 8.0 9.2 0.538 0.469 Stage 3: +Reranker 60.2 24.7 17.0 12.2 8.0 0.748 0.609 Stage 4: Full Pipeline 66.6 28.2 19.1 12.4 8.4 0.752 0.602 Stage 1 → Stage 4 개선: +11.5점 (+21%) Faithfulness: +15.8%p 조건 충족 점수: +4.9점
4. Neo4j Graph RAG 실효성 검증
Ablation Study에서 Neo4j 단독 적용 시 점수가 하락했지만, 두 가지 실질적 기여가 확인되었습니다.
conflict 감지
추천 시스템에서 벡터 유사도만으로는 감지할 수 없는 정책 간 중복 신청 불가 규칙을 Neo4j의 CONFLICTS_WITH 엣지로 표현했습니다. 실제 DB에 저장된 conflict 관계는 다음과 같습니다.
청년수당(서울시) ↔ 국민취업지원제도 청년 구직활동 지원금(경기도) ↔ 청년수당(서울시) 청년도약계좌 ↔ 청년내일저축계좌 청년내일채움공제 ↔ 청년도약계좌Ablation Study Stage 3, 4에서 총 7건의 conflict가 감지되었으며, 다음 두 케이스가 실제 추천 사유에 명시되었습니다.
케이스 1: 취업준비_자기계발_중심 유저 (25세, 월 소득 150만원)
해당 유저에게 청년 교통비 지원(서울시)이 추천되었고, 이와 동시에 신청할 수 없는 청년수당(서울시)과의 conflict가 감지되었습니다. LLM이 생성한 실제 추천 사유는 다음과 같습니다.
"교통비 지출도 상당한 편이네요. 서울시의 청년 교통비 지원 정책을 통해 매월 교통비를 일부 환급받으실 수 있어요. 단, 청년수당(POL_REAL_025)과는 중복 신청이 안 돼요. 둘 중 하나만 선택하세요!"
케이스 2: 저소득_주거_중심 유저 (30세, 월 소득 220만원)
해당 유저에게 청년 교통비 지원(서울시)이 추천되었고, 청년 구직활동 지원금(경기도)과의 conflict가 감지되었습니다. 실제 추천 사유는 다음과 같습니다.
"서울시 청년 교통비 지원 정책은 중위소득 150% 이하 청년을 대상으로 교통비를 일부 환급해주는 제도입니다. 유저님의 교통비 지출이 80,000원인 점을 고려했을 때 도움이 될 것 같아요. 단, 청년 구직활동 지원금(경기도)과는 중복 신청이 안 돼요. 둘 중 하나만 선택하세요!"
두 케이스 모두 LLM이 Neo4j에서 가져온 conflict 트리플을 컨텍스트로 받아 추천 사유에 중복 신청 불가 경고를 자동으로 생성했습니다. 벡터 유사도만으로는 두 정책이 텍스트상 유사해 보여도 중복 신청 불가라는 사실을 감지할 수 없습니다. 그래프 구조로 도메인 규칙을 인코딩함으로써 사용자가 잘못된 정책 조합을 신청해 불이익을 받는 상황을 방지할 수 있었습니다.
리랭커와의 시너지
Neo4j 단독(Stage 2)보다 Neo4j+리랭커(Stage 3)가 9.4점 높게 나왔습니다. 이는 그래프 컨텍스트가 LLM에 전달될 때 리랭커가 유저 맥락에 맞는 상품을 앞에 배치해야 그래프 트리플이 의미 있는 컨텍스트로 작동한다는 것을 보여줍니다. Neo4j와 리랭커는 독립적으로 사용할 때보다 함께 사용할 때 시너지가 발생하는 구조입니다.
5. 임베딩 품질 개선
초기 코사인 유사도 수치가 0.19 수준으로 낮게 나온 원인을 분석했습니다.
가중 평균 임베딩 도입: 단일 서술형 텍스트를 하나로 임베딩하던 방식에서, 카테고리별로 혜택 키워드 텍스트를 따로 만들어 각각 임베딩한 뒤 지출 비중을 가중치로 평균 내는 방식으로 수정했습니다. 식비가 45%라면 "외식할인 음식점할인 배달할인" 벡터에 0.45 가중치를, 교통이 27%라면 "대중교통할인 교통비지원 버스지하철" 벡터에 0.27 가중치를 주어 최종 벡터를 산출합니다. 이 수정으로 코사인 유사도가 0.19에서 0.31로 약 60% 향상되었습니다.
임베딩 차원 256으로 축소: Bedrock Titan Embed V2 기본 설정인 1024차원에서는 고차원 공간에서 모든 벡터 간 거리가 비슷해지는 차원의 저주가 발생합니다. AWS 공식 문서에 따르면 1024차원 대비 256차원에서 97% 이상의 검색 정확도를 유지합니다. RDS product_embedding 테이블 컬럼을 vector(1024)에서 vector(256)으로 변경하고 전체 상품 임베딩을 재생성한 결과 코사인 유사도가 추가 향상되었습니다.
6. 성능 평가 방법론
평가 방식: 실제 API 호출 기반
테스트 유저 10명의 소비 데이터를 RDS에 직접 INSERT하고 실제 API(POST /internal/recommend/generate/{user_id})를 호출하는 방식으로 평가했습니다. 파이프라인 전체(pgvector, Neo4j, Bedrock Claude, 리랭커)가 실제로 동작한 결과를 평가합니다.
테스트 유저 10명은 식비/교통 중심, 쇼핑/카페 중심, 저소득/주거 중심, 취업준비/자기계발 중심, 고소득/투자 중심, 창업/IT 중심, 문화/여가 중심, 건강/의료 중심, 해외여행/글로벌 중심, 자동차/주유 중심으로 다양한 소비 패턴을 대표합니다.
정량 지표 세트
Cosine Similarity: 유저 가중 평균 임베딩과 추천 상품 임베딩의 코사인 유사도입니다. 임베딩 구조에 종속적이라 절대값보다 개선 전후 상대 비교에 의미가 있습니다.
Overlap Coefficient: 유저 소비 카테고리 키워드와 상품 혜택 키워드의 매칭률입니다. 처음에는 Jaccard 유사도를 사용했는데, 유저 키워드 집합이 상품 키워드 집합보다 훨씬 커서 분모가 커지는 구조적 문제가 있었습니다. 분모를 합집합이 아닌 더 작은 집합으로 나누는 Overlap Coefficient로 전환해 상품 혜택 키워드 중 유저와 관련된 비율을 더 직관적으로 측정했습니다. 동의어 정규화("버스"→"대중교통")와 금융 도메인 불용어 제거("할인", "지원")도 적용했습니다.
ILD (Intra-List Diversity): 추천 상품들의 임베딩 벡터 간 코사인 거리 평균입니다. 타입별로 따로 계산해 카드끼리, 보험끼리, 정책끼리 얼마나 다양한지 측정합니다.
Personalization: 10명 유저 간 추천 겹침 비율로 개인화 수준을 측정합니다. 모든 유저 쌍의 Jaccard 유사도 평균을 1에서 뺀 값입니다.
Faithfulness: LLM이 생성한 추천 사유에 유저의 실제 소비 카테고리 키워드가 포함된 비율입니다.
LLM-as-Judge (100점 만점): Anthropic Claude API로 추천 결과를 4가지 기준으로 평가했습니다. 혜택 적합성(40점), 추천 사유 품질(30점), 나이/소득 조건 충족(20점), 추천 다양성(10점)으로 구성됩니다.
Hit@K, NDCG@K: 나이/소득/카테고리 조건을 모두 만족하는 상품을 정답으로 자동 라벨링하는 룰 기반 ground truth를 구성해 측정했습니다. 카드 Hit@K 1.0, NDCG@K 0.82, 보험 Hit@K 0.9, NDCG@K 0.9를 달성했습니다.
최종 지표
Cosine Similarity 0.35 임베딩 벡터 공간 유사도 Overlap Coefficient 0.26 유저 소비 키워드 vs 상품 혜택 키워드 매칭률 ILD 0.60 추천 목록 내 상품 다양성 Personalization 0.96 유저 간 추천 차별화 Faithfulness 0.75 추천 사유의 소비 맥락 반영률 Hit@K (카드/보험) 1.0/0.9 룰 기반 ground truth 매칭 NDCG@K (카드/보험) 0.82/0.9 순위 정확도 LLM Judge 66.6/100 맥락 적합성 종합 평가모든 오프라인 지표는 실제 유저의 클릭률, 북마크율, 신청 전환율 같은 온라인 지표를 대체할 수 없으며, 각 지표의 한계를 인식한 상태에서 다각도로 측정해 단일 지표에 의존하지 않는 평가 체계를 구축한 것이 핵심입니다. 실서비스 오픈 후 온라인 지표로 검증하는 것이 최종 목표입니다.
728x90'FISA' 카테고리의 다른 글
최종 플젝 회고…는 아니고 주저리 (1) 2026.06.19 [PROJECT] ai 추천시스템 성능평가1 (0) 2026.06.05 [Project]RDS설정 중 생긴 일 (0) 2026.05.30 [project] AWS EKS IAC with Terraform (0) 2026.05.28 [Project]성능 평가 스토리 (0) 2026.05.27