尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗대화만으로 계좌조회·상품비교·이체까지 처리하는 AI 은행원
매치뱅크는 은행 웹사이트에 "AI 은행원" 을 심어, 사용자가 대화만으로 실제 은행 업무를 처리할 수 있게 만든 데모 서비스입니다. 질문에 답만 하는 챗봇이 아니라, AI가 계좌 조회·상품 비교·이체 신청을 직접 수행하고 그 결과를 사용자가 확인·승인하는 구조로 설계했습니다.
왜 만들었나 — 세 가지 문제
시중의 은행 챗봇 대부분은 자주 묻는 질문에 답하는 안내판에 머물러 있습니다. 계좌를 보려면, 이체를 하려면 결국 사람이 메뉴를 눌러가며 처리해야 합니다. 둘째로 예금·대출 상품을 비교하려면 은행마다 앱을 켜고 금리를 하나씩 확인해야 하는데, 전세자금대출이나 신용대출처럼 은행별 조건 차이가 큰 상품일수록 번거로움이 커집니다. 셋째로 대부분의 AI 챗봇은 만들고 나면 운영할 방법이 없습니다. 답변이 이상해도 원인을 볼 수 없고, 프롬프트를 바꾸려면 코드를 다시 배포해야 합니다.
이 세 가지를 함께 풀려고 시작했습니다. 그래서 이름도 "AI 챗봇"이 아니라 "AI 은행원" 입니다.
누구를 위한 것인가
로그인 없이 바로 둘러보실 수 있습니다. AI 은행원에게 금리를 물어보고 상품을 비교하는 것, 상품안내·공지사항·FAQ는 전부 그냥 쓰시면 됩니다.
로그인은 본인 정보가 걸린 곳에서만 요구합니다 — 내 계좌·이체(AI 은행원을 통한 이체 포함)·마이페이지. 그 자리에서 안내가 뜨고, 로그인하면 하던 자리로 돌아갑니다(AI 은행원은 하던 대화까지 그대로 이어집니다).
| 홈 | AI 은행원 |
|---|---|
| 상품안내 (금융감독원 실시간 데이터) | 백오피스 대시보드 |
|---|---|
LangGraph의 ReAct 에이전트로 구현했습니다(agent.py). 사용자의 요청을 이해하고 → 필요한 도구를 스스로 골라 호출하고 → 결과를 확인한 뒤 → 다음 행동을 정하는 과정을 목표를 달성할 때까지 반복합니다. "질문 하나에 답변 하나"로 끝나는 일반 챗봇과의 핵심 차이입니다.
에이전트가 가진 도구 9개
| 도메인 | 도구 | 하는 일 |
|---|---|---|
| 계좌 | get_accounts | 내 모든 계좌와 잔액 조회 |
get_transactions | 계좌번호로 거래내역 조회 | |
| 이체 | lookup_recipient | 받는 계좌의 예금주·은행·수수료 확인 |
propose_transfer | 이체를 '제안'만 함 (실행하지 않음) | |
| 상품 | search_products | 금융감독원 실시간 상품 조회·비교 |
| 고객지원 | get_faqs / get_notices / get_documents | FAQ·공지·서식 검색 |
create_inquiry | 1:1 문의 접수 |
AI 은행원은 로그인 없이 바로 쓸 수 있습니다. 금리 비교·상품 추천·공지·FAQ는 개인정보를 다루지 않으니 로그인을 요구할 이유가 없다고 봤습니다. 위 도구 9개 중 상품·고객지원 4개는 애초에 토큰을 보내지 않아 로그인 여부와 무관하게 동작합니다.
본인 확인이 필요한 나머지는 대화 중에 그 요청이 나왔을 때만 로그인을 안내합니다. 화면을 막지 않고, 로그인하면 하던 대화에서 그대로 이어서 처리합니다.
비로그인: "예금 금리 비교해줘" → 금융감독원 실시간 데이터로 바로 답변
비로그인: "김철수한테 10만원 보내줘" → "이체는 로그인이 필요합니다" + [로그인하기]
→ 로그인 → 같은 대화로 복귀 → 이어서 처리
권한 판정은 프런트가 아니라 백엔드가 합니다. 개인정보를 반환하는 엔드포인트는 예외 없이 인증을 걸고 쿼리도 WHERE user_id = ?로 묶여 있어서, 에이전트가 토큰 없이 계좌·이체 도구를 호출하면 401이 돌아옵니다. 화면 게이트는 사용자 경험을 위한 것이지 보안 경계가 아닙니다.
이 프로젝트에서 가장 신경 쓴 부분입니다. LLM에게는 이체를 실행할 수단 자체를 주지 않습니다.
사용자: "김철수한테 5만원 보내줘"
↓
LLM : propose_transfer 호출 → 예금주·수수료 확인한 '제안'만 반환 (실행 수단 없음)
↓
UI : 확인 카드 렌더 (예금주 · 금액 · 보내는 분 계좌 선택 · 신규 수취계좌 경고 · 이체 시점)
↓
사람 : "이체 확인" → 모달에서 체크박스 확인 + 이체 비밀번호 6자리 입력
↓
그제야 execute_transfer → POST /api/transfer 실행
execute_transfer는 LLM이 호출할 수 있는 도구 목록에 아예 없습니다. UI가 사용자 승인 후 직접 부르는 일반 함수입니다(agent.py). 모델이 잘못 판단하거나 프롬프트 주입을 당하더라도 돈이 움직이지 않는 구조입니다.
최종 확인(체크박스 + 이체 비밀번호)을 모달로 분리한 것도 같은 의도입니다. 카드 안에 폼 필드로 섞여 있으면 PIN이 입력란 하나로 묻히지만, 모달로 빼면 차단성 인증 이벤트로 읽혀 "돈이 움직이기 전에 사람이 한 번 막는다"는 설계가 화면에 드러납니다. 실패 시 모달은 닫히지 않고 그 안에서 사유를 보여줘 바로 재시도할 수 있습니다.
flowchart TB
User(["👤 사용자"])
subgraph L1["① 채널"]
direction LR
Site["웹사이트"]
Chat["AI 은행원"]
end
subgraph L2["② 은행 에이전트"]
Orch["🧠 오케스트레이터<br/>LangGraph ReAct · 도구호출"]
A1["💰 계좌"]
A2["💸 이체"]
A3["📈 상품"]
A4["💬 고객지원"]
Orch --> A1 & A2 & A3 & A4
end
subgraph L3["③ 백엔드 API :8000"]
API["REST API · 인증(JWT)"]
end
subgraph L4["④ 데이터 · 외부"]
direction LR
DB[("SQLite / Turso")]
Kafka{{"Kafka"}}
Worker["이체 워커"]
ES[("Elasticsearch")]
OpenAI["OpenAI"]
FSS["금융감독원 API"]
end
User --> Site
User --> Chat
Site --> API
Chat --> Orch
A1 --> API
A2 --> API
A3 --> API
A4 --> API
Orch --> OpenAI
API --> DB
API --> ES
API --> FSS
API --> Kafka
Kafka --> Worker
Worker --> DB
하나의 프로그램이 아니라 여러 프로세스가 협력하는 구조입니다.
| 프로세스 | 포트 | 역할 | 필수 |
|---|---|---|---|
| FastAPI 백엔드 | 8000 | 정적 사이트 서빙 + 은행 REST API (엔드포인트 88개) | 필수 |
| Streamlit 챗봇 | 8501 | AI 은행원 UI, 사이트에 iframe으로 임베드 | AI 은행원 사용 시 |
| 이체 워커 | — | Kafka 소비 → 잔액 차감·거래내역 기록 | Kafka 사용 시 |
| Kafka | 9092 | 이체 이벤트 큐 | 선택 |
| Elasticsearch | 9200 | RAG 색인 (BM25 + kNN 하이브리드) | 선택 |
| Redis / ChromaDB | 6379 / — | 시맨틱 캐시 L1 / L2 | 선택 |
| Phoenix | 6006 | OpenTelemetry 트레이스 수집 | 선택 |
외부 서비스는 전부 없어도 앱이 죽지 않습니다. 모두 lazy import + try/except로 감싸 연결이 안 되면 해당 기능만 비활성화됩니다.
POST /api/transfer는 검증만 하고 pending 상태로 기록한 뒤 Kafka에 발행하고 즉시 응답합니다. 실제 잔액 차감과 거래내역 기록은 transfer_consumer.py가 소비해서 처리합니다. 요청이 몰려도 순서대로 안정적으로 처리하기 위한 구조입니다.
예금·적금과 대출 3종은 optionList 스키마가 완전히 다릅니다. 예적금은 intr_rate/intr_rate2를 쓰지만 주택담보·전세자금대출은 lend_rate_min/lend_rate_max, 신용대출은 crdt_grad_1~13(신용점수 구간별 금리)를 씁니다. fss_fetcher.py가 카테고리별로 파싱을 분기합니다.
신용대출에는 함정이 하나 더 있습니다. optionList에 "대출금리" 외에 "기준금리"·"가산금리"·"가감조정금리" 가 섞여 들어옵니다. 필터링하지 않으면 가감조정금리 같은 작은 값이 최저금리로 잘못 집계됩니다. crdt_lend_rate_type_nm == "대출금리"인 행만 사용합니다.
(fin_co_no, fin_prdt_cd) 복합키로 옵션 그룹핑fin_prdt_cd(상품코드)는 은행마다 독립적으로 매기기 때문에, 서로 다른 은행이 우연히 같은 코드를 쓰는 경우가 실제로 있습니다. 상품코드만으로 매칭하면 다른 은행의 금리 옵션이 섞여 들어옵니다. 반드시 은행코드까지 포함한 복합키로 묶어야 합니다.
/api/products는 예금·적금을 최고금리 내림차순으로, 대출을 최저금리 오름차순으로 정렬합니다. 대출은 금리가 낮을수록 유리하기 때문입니다. 에이전트의 search_products 응답에도 이 점을 note로 명시해 LLM이 "가장 좋은 상품"을 반대로 고르지 않게 합니다.
Redis(L1, 정확 일치) + ChromaDB(L2, 유사도 검색)로 이미 답한 적 있는 질문의 답변을 재사용합니다(cache.py). 히트 여부는 Phoenix 스팬 속성 cache.hit으로 기록합니다.
시스템 프롬프트와 모델을 백오피스에서 바로 바꿀 수 있고, 변경 이력이 남아 diff를 보고 이전 버전으로 롤백할 수 있습니다. 저장하지 않고 현재 프롬프트와 편집 중인 프롬프트로 같은 질문을 동시에 실행해 답변과 지연시간을 비교하는 A/B 기능도 있습니다(POST /api/admin/prompt-ab-test). 사용자의 좋아요/싫어요 피드백은 사유별로 집계됩니다.
현재 정책은 수수료 0원입니다. 국내에서는 이미 면제가 시장 표준이라, 신규 서비스가 건당 수수료를 받으면 재방문할 이유가 약해집니다. 다만 수수료 로직 자체는 지우지 않았습니다 — 잔액 검사(amount + fee), 별도 이체수수료 거래내역, 백오피스 이체정책 편집이 모두 그대로 있고 정책값만 0입니다. 백오피스에서 수수료를 올리면 이체 화면 안내가 "면제"에서 실제 금액으로 자동 전환되고, 강조 색도 빠집니다.
화면 문구를 하드코딩하지 않은 이유가 여기 있습니다. 한도·수수료는 백오피스에서 바뀔 수 있어서, 이체 화면 안내와 "이체 후 잔액" 미리보기가 모두 GET /api/me/limits를 읽습니다(예전에는 타행 수수료 500원이 마크업에 박혀 있어 정책과 어긋났습니다).
네이티브 <select>의 옵션 목록은 OS가 그리는 메뉴라 CSS로 크기·폰트·위치를 제어할 수 없습니다(macOS에서 목록이 화면 가운데에 아주 크게 떴습니다). <option> 안에서는 flexbox·정렬도 동작하지 않아 "은행 · 계좌번호 · 잔액(오른쪽 정렬)" 같은 행 구성이 불가능하고, 드롭다운 화살표는 background-image라 오른쪽 테두리에만 붙습니다.
그래서 목록 UI만 직접 그리는 재사용 컴포넌트(.cs)를 만들었습니다. 네이티브 <select>는 지우지 않고 숨겨서 값 저장용으로 남겼습니다 — 이체 폼 여러 곳이 .value를 읽고 쓰기 때문에 그 인터페이스를 유지하고, 선택 시 change 이벤트를 발생시켜 기존 핸들러를 그대로 재사용합니다.
로컬은 표준 sqlite3, 배포는 Turso(libSQL)를 씁니다. 그런데 libsql은 row_factory와 executescript를 구현하지 않아 그대로는 코드가 갈라집니다. sqlite3.Row 호환 래퍼를 만들어 흡수했고(backend/db.py), 덕분에 애플리케이션 코드는 양쪽에서 동일합니다. 스키마 마이그레이션도 ALTER TABLE ADD COLUMN 멱등 루프로 처리해 기존 DB가 그대로 따라옵니다.
사용자
| 화면 | 내용 |
|---|---|
| 홈 | 3D 히어로, 빠른 액션, 공지·이벤트·특별상품과 연동되는 배너 캐러셀 |
| AI 은행원 | 대화로 계좌조회·상품추천·이체, 답변 피드백(좋아요/싫어요 + 사유 선택), 파일 첨부 |
| 내 계좌 | 잔액·거래내역 조회, 3단계(입력→확인→완료) 이체 · 이체 수수료 면제 · 정기점검(23:50~00:10) 중에는 예약 이체로 전환 |
| 상품안내 | 5종 상품 실시간 비교, 카테고리별 인기 TOP5, 특별상품, 은행 공식 사이트 바로가기 |
| 마이페이지 | 내 정보, 보안(이체 비밀번호·이상행동 기록), 계좌 관리, 즐겨찾기, 예약이체, 문의내역, 거래명세서 |
| 고객센터 | 공지사항, FAQ, 이벤트(응모·추첨), 1:1 문의, 서식·약관 자료실 |
로그인이 필요한 곳 — AI 은행원 상담·상품안내·공지·FAQ는 로그인 없이 쓸 수 있습니다. 본인 정보가 걸린 곳에서만 로그인을 요구하고, 막히는 범위에 따라 안내 모양을 다르게 했습니다 — 화면 전체가 막히면 화면 안 안내문(내 계좌·마이페이지), 화면 안의 탭·버튼 하나만 막히면 팝업(1:1 문의·이벤트 응모). 뒤에 계속 볼 화면이 남아 있을 때만 팝업이 의미가 있다고 봤습니다. 어느 쪽이든 로그인하면 막혔던 자리로 돌아갑니다.
백오피스 (관리자 전용)
대시보드 · 회원관리 · 이체 모니터링 · 이용통계 · 성능관리(인프라 연동 현황, LLM/RAG 사용 지표) · 프롬프트 관리(편집·버전 이력·롤백·A/B 비교·피드백 집계) · 금융상품 관리 · FAQ·공지사항·이벤트 관리 · 배너 관리
상세 기획은 docs/기획서.md에 있습니다.
| 영역 | 사용 기술 |
|---|---|
| AI 에이전트 | LangGraph (ReAct), LangChain, OpenAI (gpt-4o-mini / gpt-4o / gpt-4.1-mini) |
| 백엔드 | FastAPI, Uvicorn, PyJWT, bcrypt |
| 데이터 | SQLite (로컬) / Turso libSQL (배포) — 18개 테이블 |
| 메시징 | Kafka (KRaft) |
| 검색·캐시 | Elasticsearch (BM25 + kNN 하이브리드 RAG), Redis, ChromaDB |
| 관측 | Arize Phoenix (OpenTelemetry) |
| 프런트엔드 | Vanilla JS SPA, Three.js, IBM Plex Sans KR |
| 챗봇 UI | Streamlit |
| 외부 연동 | 금융감독원 금융상품 통합 비교공시 API |
Python 3.12 전용입니다.
python3.12 -m venv .venv # 시스템 기본 python3 가 3.12 가 아니면 전체 경로로 지정
.venv/bin/pip install -r requirements.txt
# 데모 데이터 시드 — bank.db 는 .gitignore 대상이라 이 단계가 필수입니다
.venv/bin/python seed_bank.py # 사용자·계좌·거래·이체 (demo/demo1234, admin/admin1234)
.venv/bin/python seed_support.py # 공지·FAQ·서식·이벤트·배너
.venv/bin/python seed_usage.py # 이용 통계 이벤트
.venv/bin/uvicorn backend.app:app --port 8000
→ http://localhost:8000 접속. 백엔드가 정적 사이트까지 서빙하므로 별도 웹서버가 필요 없습니다.
cp .streamlit/secrets.toml.example .streamlit/secrets.toml # OPENAI_API_KEY 입력
.venv/bin/streamlit run app.py # 별도 터미널
상품안내를 로컬에서 보려면 금융감독원 API 키를 발급받아 config.json의 fss_api_key에 넣거나 FSS_API_KEY 환경변수로 지정합니다.
./start_infra.sh # Kafka + Elasticsearch + Phoenix 일괄 기동
.venv/bin/python transfer_consumer.py # 이체 워커 (Kafka 사용 시)
.venv/bin/python ingest_fss.py # FSS 상품 → Elasticsearch 색인
없어도 앱은 정상 동작하며 해당 기능만 비활성화됩니다.
| 변수 | 용도 |
|---|---|
OPENAI_API_KEY / FSS_API_KEY | config.json에 값이 없을 때의 폴백 (배포 환경용) |
BACKEND_URL | 챗봇이 호출할 백엔드 주소 (기본 http://localhost:8000) |
CHAT_BASE_URL | 사이트가 임베드할 챗봇 주소 (기본 http://localhost:8501) |
TURSO_DATABASE_URL / TURSO_AUTH_TOKEN | 설정 시 SQLite 대신 Turso 사용 |
KAFKA_DISABLED | Kafka 없이 동기 처리 |
DEMO_READONLY / DEMO_PUBLIC | 공개 데모 보호 (백오피스 쓰기 차단 / 챗봇 남용 상한) |
JWT_SECRET | JWT 서명 키. 배포 환경에서는 반드시 설정 — 미설정 시 저장소에 있는 폴백 상수를 쓰게 되고, 공개 저장소이므로 누구나 role:"admin" 토큰을 위조할 수 있다 |
ADMIN_PASSWORD | 관리자 계정 비밀번호. 라이브에서는 공개하지 않는 값으로 설정한다. ⚠️ 이미 존재하는 사용자의 비밀번호는 시드가 덮어쓰지 않으므로, 최초 1회는 마이페이지>보안에서 직접 변경해야 한다 |
.venv/bin/pytest # 14개 (RAG · 시맨틱 캐시 · FSS 파서)
.venv/bin/python batch_test.py # 배치 품질 테스트 → batch_test_results.json
.venv/bin/python build_stats.py # 결과 집계 → site/data/stats.json (백오피스 성능관리에서 조회)
batch_test.py는 10개 카테고리 999문항을 동시 실행하면서 Phoenix로 트레이스를 남기고, 응답 성공률·지연과 함께 정답 채점(수학은 정답 문자열 exact 비교, 나머지는 LLM-as-judge)까지 수행합니다.
최근 실행 결과 (gpt-4o-mini, 999문항 / 171초):
| 항목 | 값 |
|---|---|
| 응답 성공률 | 100% (999/999) |
| 정확도 | 96.3% (950/987 채점) |
| 평균 지연 | 1,038 ms |
| 중앙값 / p95 | 863 ms / 1,881 ms |
카테고리별 정확도 — 수학·과학·영어·파이썬코딩 100%, 경제 99.0%, 세계지리 97.0%, 한국역사 94.9%, 상식 93.9%, 문화예술 93.0%, 논리 84.5%. 논리 추론이 뚜렷하게 약한 구간으로 드러났습니다.
문항이 은행 업무가 아니라 일반 지식인 이유는, 이 배치가 도메인 정확도 평가가 아니라 응답 파이프라인의 안정성과 지연 특성을 재기 위한 것이기 때문입니다. 은행 업무 답변의 품질은 사용자 피드백(좋아요/싫어요 + 사유) 집계로 따로 봅니다. 채점되지 않은 12건은 판정 모델이 O/X 외의 응답을 낸 경우입니다.
실사용자가 없는 데모 프로젝트이므로 "가입자 수" 같은 지표는 존재하지 않습니다. 대신 답변 품질을 스스로 측정하고 개선할 수 있는 구조를 만든 것 자체를 성과로 정의했습니다 — 배치 평가, 프롬프트 A/B 실측, 사용자 피드백 집계, 트레이스 기반 캐시 히트율 측정이 그 구조입니다.
backend/ FastAPI REST API + 데이터 계층
app.py 엔드포인트 88개 (인증·계좌·이체·상품·고객지원·백오피스)
db.py SQLite/libSQL 겸용 어댑터, 18개 테이블, 도메인 로직
auth.py JWT 발급·검증, 관리자 권한 가드
kafka_io.py 이체 이벤트 발행 (+ Kafka 없을 때 동기 폴백)
infra_metrics.py 인프라 연동 상태 수집
agent.py LangGraph ReAct 에이전트 + 도구 9개
llm.py LLM 제공자 추상화 (스트리밍)
rag.py Elasticsearch 하이브리드 검색
cache.py 2계층 시맨틱 캐시
fss_fetcher.py 금융감독원 API 파싱 (카테고리별 스키마 분기)
app.py Streamlit 챗봇 UI (이체 확인 카드·피드백·대화 저장)
transfer_consumer.py Kafka 소비 → 이체 실제 처리
site/ 정적 SPA (index.html + js/main.js + css/style.css)
tests/ pytest 14개
docs/ 기획서 · 디자인 토큰 · 스크린샷
| 문서 | 내용 |
|---|---|
docs/기획서.md | 문제 정의 · 타겟 · 차별점 · 정보구조 · 성공지표 |
docs/tokens.md | 디자인 토큰 (컬러·타이포·스페이싱·모션) |
architecture.mmd | 시스템 구성도 (Mermaid) |
CLAUDE.md | 아키텍처 설명 및 개발 규칙 |
session-log.md | 개발 과정 상세 기록 |
backlog.md | 남은 작업 체크리스트 |
JWT_SECRET 환경변수로 분리했습니다(미설정 시 로컬 개발용 폴백).?token=으로 JWT를 넘깁니다. 주소에 실린 토큰은 브라우저 기록·referrer·서버 로그에 남을 수 있어, 실서비스라면 바꿔야 합니다(챗봇을 별도 호스트에 두면 그 호스트에도 토큰이 닿습니다). 로그인 없이 쓸 수 있게 되면서 평소에는 URL에 토큰이 실리지 않게 됐지만(이체·계좌를 쓸 때만 로그인), 구조 자체는 그대로입니다. 반대 방향(챗봇 → 사이트)은 이미 postMessage로 처리하고 있습니다 — 챗봇은 사이트와 다른 출처(:8501)의 iframe이라 링크로 상위 창을 직접 움직이지 않고 사이트가 메시지를 받아 이동하도록 했습니다.데이터 출처: 금융감독원 금융상품 통합 비교공시