표준화된 도구 통합 및 외부 API 활용 설계 - 100-hours-a-week/KTB4-13th-wiki GitHub Wiki
MCP는 아무 데나 쓰는 게 아니라, "AI가 대화·추론 중에 도구를 골라 부르는" 딱 그 상황을 위한 규격이다.
1. MCP라는 표준 연결 규격은 안 쓴다
-
표준으로 연결할 곳은 한 지점 존재
- V2 쇼핑 에이전트가 BE 기능 7개(장바구니 담기·빼기·조회, 주문 요약, 재고 확인, 책 정보)를 부르는 것.
- 외부 AI 모델(문장 생성, 벡터 변환)은 이미 우리 자체 창구인 "모델 게이트웨이"로 깔끔하게 정리돼 있다.
- MCP의 장점은 "도구가 계속 늘고, 남이 만든 도구를 갖다 쓰고, 여러 AI가 도구를 나눠 쓸 때" 나온다.
- 우리는 도구 7개 고정 + 상대는 우리 BE 팀 하나라 장점이 없음
2. 대신 쇼핑 에이전트 BE tool 기능 목록을 "MCP랑 똑같은 양식"으로만 적어둔다
- 기능마다 이름 / 설명 / 받는 값 형식 / 성격(읽기 전용인지, 여러 번 불러도 안전한지)을 적는다. (실제 통신은 그냥 평범한 내부 REST.)
- 나중에 진짜 MCP로 바꿔야 할 일이 생겨도, 부르는 쪽 코드는 안 고쳐도 된다. 통로만 갈아 끼우면 끝.
3. 모델 부르는 건 이미 표준화돼 있다
- 추천·검색 코드는
complete()(문장 생성)와embed()(벡터 변환) 두 함수만 부른다.- 어느 회사 모델을 쓰는지 몰라도 된다.
- 과제가 요구하는 "표준 인터페이스 + 권한 관리 + 비용·지연·안정성 제어"는 MCP가 아니라 이 게이트웨이 계층이 담당한다.
4. 이미지·영상·음성 만드는 기능이 없다
AI 서버가 자기 프로세스 밖과 주고받는 지점
| # | 상대 | 방향 | 성격 | 호출 시점 | 지연 민감도 | 과금 |
|---|---|---|---|---|---|---|
| I1 | 상용 LLM API (텍스트 생성) | AI → 외부 | 생성 | ③ 델타 + 카드 이유 short·long·match_basis(온라인, 한 호출), ⑤ 취향 추출(야간 배치), (V2) ③ 표지 VLM 폴백 | 높음(③ SSE) | 토큰 |
| I2 | 임베딩 백엔드 | AI → 자체 서빙(단일. 장애 시 다른 모델로 폴백하지 않고 강등) | 인코딩 | ①③ 쿼리·spec(온라인), ⑤ 취향 문장(야간), ② 문서(배치) | 중간 | 자체=0 |
| I3 | 비전 인코더 + OCR (V2) | AI → 자체 서빙(인코더) / 외부 API(OCR) | 인코딩·인식 | 표지 색인(배치), ③ 이미지 턴(온라인, V2, 저볼륨) | 색인 낮음 / 질의 중간 | OCR API=이미지당 |
| I4 | BE 커머스 tool (7종) | AI → BE | 읽기 5 / 쓰기 2 | ⑦ 에이전트 턴(V2)만. tool 루프 ≤3스텝 | 중간 | 없음(내부) |
| I5 | 국중도 SEOJI 서지 API | 적재 크론 → 외부 | 조회 | 카탈로그 적재·갱신(오프라인, 1 req/s) | 없음 | 무료(키·한도) |
| MCP의 강점 | 우리 상황 | 회수되나 |
|---|---|---|
| 도구를 동적으로 발견하고 능력을 협상 | tool 7종 고정. allow_tools 화이트리스트도 이미 계약에 있음 |
✗ 정적이라 이득 없음 |
| 서드파티 도구 서버를 표준 방식으로 연결 | 상대는 자체 BE 팀 하나. 결제대행·배송사 등 외부 직결 없음(주문·결제는 시스템 밖) | ✗ 서드파티 없음 |
| 한 도구 서버를 여러 AI 클라이언트가 공유 | BE tool의 소비자는 AI 서버 하나 | ✗ 단일 소비자 |
| 모델 ↔ 도구 사이 전송·인증 표준화 | AI↔BE는 같은 내부망, 방향 전용 서비스 토큰, 소유권은 BE가 user_id로 재확인 |
△ 이미 단순하게 풀림 |
| LLM SDK의 네이티브 tool-calling 연동 | 이건 MCP 없이도 JSON Schema 디스크립터만 있으면 됨 | △ 스키마만 필요 |
💡 판정: MCP 전송/서버 프로세스는 미도입.
MCP의 스키마 관례(선언적 도구 디스크립터 + 능력 어노테이션)만 차용한다.
AI↔BE tool을 아래 형식으로 정의한다.
BE는 이걸 구현하고, AI 서버의 ToolRegistry가 그대로 읽어 LLM tool-calling 목록으로 넘긴다.
{
"name": "cart.add",
"description": "장바구니에 도서를 담는다. 재고·권한은 BE가 재검증한다.",
"inputSchema": {
"type": "object",
"required": ["user_id", "book_id", "qty"],
"properties": {
"user_id": { "type": "integer" },
"book_id": { "type": "integer" },
"qty": { "type": "integer", "minimum": 1 }
}
},
"annotations": {
"readOnlyHint": false,
"idempotentHint": true,
"audience": ["agent"],
"owner": "backend-commerce"
}
}- "성격표시" 칸을 보고 재시도 정책이 자동으로 갈립니다.
설명서의 성격표시 칸에 "이 도구는 읽기만 함" / "두 번 불러도 안전함" 같은 게 적혀 있습니다. 호출 실패했을 때 우리 코드가 이걸 보고 알아서 판단
- 읽기 도구(재고 조회 등) → 실패하면 그냥 다시 부름 (여러 번 불러도 아무 일 안 생기니까)
- 쓰기 도구(장바구니 담기 등) → "이거 두 번 하지 마" 표식(멱등키)이 붙어 있을 때만 다시 부름 (안 그러면 두 번 담길 수 있으니까)
- 이 설명서를 AI 코드에 넣어두고, BE 팀과 "계약서"로 공유한다
- 에이전트가 자체 BE 밖의 도구를 써야 한다
- 배송사 추적 API 직결, 외부 예약/티켓, 결제대행.
- BE tool의 소비자가 AI 서버 외에 생긴다
- 다른 내부 서비스, 파트너.
- tool 세트가 동적·대규모가 되어 수기 디스크립터 관리가 불가능해진다
- (단계 4의 LangGraph 재평가 트리거 ⓑ와 같은 신호)
- 서드파티가 우리 추천을 도구로 호출하는 B2B 요구(우리가 MCP 서버가 되는 경우)
전환 비용: ToolInvoker의 전송 어댑터 1개 교체(REST → MCP 클라이언트). 디스크립터가 이미 MCP 형식이라 호출부·프롬프트·grounding 로직 무변경.
flowchart TB
subgraph AI["AI 서버 · FastAPI (단일 배포, 논리 모듈 분리)"]
SRCH["검색 모듈 ①<br/>파서→BM25+벡터→RRF→랭킹"]
PIPE["추천·취향 모듈 ③④⑤⑥<br/>해석·후보검색·재랭킹·이유·취향추출·프로필"]
AGT["쇼핑 에이전트 ⑦ (V2)<br/>지시어해소→의도분류→tool루프→grounding"]
subgraph GWs["표준화 게이트웨이 계층"]
MGW["모델 게이트웨이<br/>complete() · embed() · vision_encode() · ocr()<br/>어댑터·폴백체인·서킷·응답캐시·레이트리밋"]
TGW["도구 게이트웨이 (V2)<br/>ToolRegistry(MCP호환 디스크립터)<br/>ToolInvoker(재시도·타임아웃·서킷·파생멱등키·감사)<br/>GroundingEnforcer"]
end
end
subgraph SELF["자체 서빙 (같은 VPC / 사이드카)"]
E5["임베딩 e5-small (CPU, uvicorn workers)"]
CLIP["표지 이미지 인코더 (open CLIP, 배치)<br/>V2+ · 트리거 관측 시에만"]
end
subgraph EXT["외부 상용 API"]
LLM["상용 LLM API<br/>텍스트 생성 (스트리밍)"]
OCRAPI["OCR + VLM 폴백 API<br/>표지 텍스트·단서 추출 (V2)"]
end
subgraph BE["백엔드 · 커머스 코어"]
TOOLS["/internal/tools/*<br/>cart.add · cart.update · cart.view<br/>order.preview · inventory.check · book.detail"]
CATCRON["카탈로그 레코드 적재 크론<br/>(제목·저자·가격·ISBN)"]
BEDB[("BE · MySQL (커머스 원본)<br/>회원 · 카탈로그 · 장바구니 · 주문 · 결제")]
end
NL["국중도 SEOJI 서지 API<br/>BOOK_INTRODUCTION · BOOK_TB · 표지(조건부)"]
AIDB[("AI 전용 · PostgreSQL + pgvector<br/>v_books·v_user_* (복제 사본)<br/>book_embeddings · taste_profile<br/>book_cover_embedding (V2)")]
SRCH --> MGW
PIPE --> MGW
AGT --> PIPE
AGT --> TGW
PBATCH --> MGW
MGW -->|"embed()"| E5
MGW -->|"vision_encode()"| CLIP
MGW -->|"complete()"| LLM
MGW -->|"ocr()"| OCRAPI
TGW -->|"내부 REST + 서비스 토큰"| TOOLS
SRCH --> AIDB
PIPE --> AIDB
CLIP --> AIDB
PBATCH --> AIDB
CATCRON -->|"1 req/s, 체크포인트"| NL
PBATCH -->|"1 req/s, BOOK_INTRODUCTION·BOOK_TB 전문"| NL
CATCRON --> BEDB
TOOLS --> BEDB
BEDB ==>|"BE→AI 단방향 복제<br/>(신선도 예산: 명세 §1)"| AIDB
| 노드 | 역할 | 왜 호출하나 | 대체 전략 |
|---|---|---|---|
| 모델 게이트웨이 | 모든 모델 I/O의 단일 창구. provider-neutral 인터페이스 뒤로 자체/외부 차이를 어댑터가 흡수 | 파이프라인·에이전트가 프로바이더를 몰라도 되게. 교체·폴백·비용 제어를 한 곳에 | 계층 자체가 대체 전략의 구현체 |
| 도구 게이트웨이 (V2) | ⑦ 에이전트의 tool 루프 실행 — 디스크립터 로드, BE 호출, 재시도/서킷, 파생 멱등키, tool_calls[] 감사 기록 |
LLM이 낸 tool 호출을 안전하게(권한·중복·근거) 실행하려고 | BE tool 장애는 200으로 흡수(발화 안내 + 실패 기록), 서킷 오픈 시 해당 의도만 비활성 |
| 상용 LLM API | ③ spec 델타 추출 + 카드 reason_short·reason_long·match_basis(한 호출), ⑤ 야간 취향 사실 추출, (V2) ③ 표지 VLM 폴백 |
스트리밍 생성 품질을 초기 구축비 없이 확보 | LLM 장애 시 요청 spec으로 점수 상위 3권 + reason_short 규칙 생성 + reason_long: null + 본문 degraded:true(③). |
| 임베딩 e5-small (자체) | ①③ 쿼리·spec, ⑤ 취향 문장, ② 문서 벡터 | 최다 호출 경로의 왕복·과금 제거(22배). 검색·추천·취향이 같은 좌표계 공유 강제 | 다른 모델로 폴백하지 않는다(모델이 다르면 저장된 벡터와 비교할 수 없다). 장애 시 ① 키워드 전용(keyword-only) · ④ 규칙 점수만(rule-only) · ⑤ 503 후 다음 날 재처리 |
| 표지 인코더 (자체, V2) | 카탈로그 표지 벡터 색인 + 질의 표지 인코딩 | "사진으로 책 찾기"는 본질이 이미지 최근접 검색(단계 5) | 오프라인 색인이라 지연 무관. 미색인 책은 lazy 색인 |
| OCR API (V2) | 질의 표지 사진에서 제목·저자 텍스트 추출 → BM25 교차검증 | 이미지 임베딩만으로는 유사 디자인 혼동. 저볼륨(이미지 턴만)이라 상용이 저렴 | 자체 OCR(Tesseract/Paddle) 폴백, OCR 실패 시 이미지 최근접 단독 |
| BE 커머스 tool (7종, 내부) | 장바구니 담기·수정·조회, 주문 초안 요약, 재고, 도서 메타 | ⑦이 발화의 수치·상태를 그 턴 tool 결과에서만 채우려고(grounding) | 배송 문의는 보통 [주문내역] 딥링크로 안내. recommendations.candidates는 BE 아님(AI 내부 모듈, 감사만 기록) |
| BE MySQL (커머스 원본) | 회원·카탈로그·장바구니·주문·결제의 소유자. tool 쓰기·카탈로그 적재가 여기로 들어감 | AI가 커머스 트랜잭션에 관여하지 않도록 물리적으로 분리 | BE MySQL 장애는 AI 응답에 영향 없음 AI는 낡은 복제본으로 계속 200. 복제만 멈춤 |
| 국중도 SEOJI | 책소개·목차·표지(조건부) 카탈로그 소스 | 카탈로그 단일 소스(2026-09-05 확정). | 채움율 낮은 신간은 description만 짧게 채워짐. 표지 URL 미확보 시 알라딘 lazy fetch |
| AI 전용 Postgres + pgvector | 복제된 커머스 테이블(v_books·v_user_*·v_book_popularity) + AI 소유(임베딩·취향 프로필) |
별도 벡터 DB의 동기화 파이프라인(ML 아닌 일)을 안 만들려고. 벡터와 복제 커머스 테이블이 한 DB라 필터(WHERE)+벡터(ORDER BY)가 한 SQL(옵션 C) | HNSW 인덱스 장애 시 전수 스캔(E2 실측 p95 7.5ms). AI PG 전면 장애만 ①③④ 500, 복제 지연·행 부재는 낡은 값으로 200 |
| 항목 | 내용 |
|---|---|
| 용도 | ③ spec 델타 추출 · ③ 카드 reason_short(≤80자)·reason_long(2~4문장, ≤300자)· match_basis 한 호출로 함께 생성(카드 선택과 이유가 어긋나지 않게) ⑤ 야간 취향 사실 추출 (V2) ③ 표지 OCR 실패 시 제목·저자 단서 구조화 추출(VLM 폴백) |
| 후보 | Gemini 2.x/3.x flash-lite급, GPT-4.x mini급, Claude Haiku급 소형·저지연·스트리밍 클래스. 게이트웨이 어댑터로 교체 가능하게 두고 1차 선정은 한국어 품질·단가·RPD로 결정 |
| 선택 이유 | (1) ③이 SSE 스트리밍 필수: 자체 소형 LLM 서빙(vLLM/TGI + GPU)은 초기 구축·운영비가 트래픽 대비 과함 (2) 후보 10권 검색·채점은 결정론(단계 4)이고 LLM은 그중 3권 최종 선택 + 이유 문장만 → 소형 모델로 충분 (3) LLM 호출 지점이 온라인 1곳(③) + 야간 1곳(⑤), V2에서 ⑦·표지 → 교체 위험 국소적 |
| 성능(실측·단계 2) | 이유 1건 p95 1.3초, TTFB p95 1.0초 (한→해외 RTT 포함). ③ chat 1턴 SLO 4초 안에 여유(카드 3장 한 호출이라 출력 토큰↑ 여유분 확인 필요). ⑤는 야간 배치라 지연 무관 |
| 비용 구조 | 입력+출력 토큰 과금. ③ 1턴 ≈ 입력3400~700 tok(spec + 후보 10권 메타) + 출력 300~600 tok(카드 3장 × short+long+basis). 카드를 다시 열어도 LLM 0회 BE가 reason_long을 카드와 함께 보관하고 상세 페이지에서 그대로 렌더(AI측 이유 캐시·이유 엔드포인트 없음). 트레이드오프: 사용자가 안 여는 카드의 긴 이유까지 생성해 턴당 출력 토큰↑ |
| 제약 / 리스크 | ① RPD/RPM 한도 → 게이트웨이 클라이언트 레이트리밋 + 429 시 Retry-After 전파 ② 데이터 경계 이탈 — 발화·spec이 외부로 나감. 완화: AI 서버는 대화 원문 미저장·로그 마스킹, 프로바이더 학습 비활성(no-train) 플래그 + DPA 필수, 가능하면 리전 고정 ③ 크로스보더 지연 변동 → 타임아웃 30초 + 504 generation_timeout
|
| 자체 구축 vs API | 자체(GPU 상시 A10/L4 ≈ $360~720/월 + 서빙 운영) vs 상용(사용량 과금). 현재 볼륨(텍스트 소형 생성만, 캐시 적중률 높음)에서는 상용이 명백히 저렴. 손익분기 ≈ 지속 토큰 지출이 GPU 상시비를 넘길 때 → 그 지점에서 소형 LLM 자체서빙 파일럿(단계 2 계획과 동일) |
| 캐시 / 폴백 | AI측 이유 캐시 없음(BE가 reason_long 보관). LLM 장애 폴백: 요청 spec으로 점수 상위 3권 + reason_short 규칙 생성("관심 카테고리 '에세이'에서 골랐어요" 류) + reason_long: null + 본문 degraded: true. X-Degraded 헤더는 ③에 붙지 않는다(계약값 keyword-only·rule-only 둘뿐). ⑤ 야간 배치는 실패 시 다음 날 재처리 |
-
결정: multilingual-e5-small 자체 서빙 확정(
bench/).- 상용 임베딩 API는 런타임 폴백으로도 쓰지 않는다. 모델이 다르면 벡터 공간(대개 차원까지)이 달라
book_embeddings·취향 프로필·기억 벡터와 비교할 수 없다.
- 상용 임베딩 API는 런타임 폴백으로도 쓰지 않는다. 모델이 다르면 벡터 공간(대개 차원까지)이 달라
-
근거: 최다 호출 경로(모든 검색·추천 질의).
- 자체 서빙으로 p95 467ms→21ms(22배), 건당 과금 제거, 하이브리드 recall 0.90(Gemini 벡터 단독 동급). 인제스트 처리량 5.6→200+ docs/s.
-
제약: GIL+torch로 스레드 확장 안 됨
-
uvicorn --workers프로세스 복제. CPU 코어가 스케일 단위.
-
-
자체 vs API: 이미 자체가 이김.
- 자체 서빙 장애는 폴백 없이 강등(①
keyword-only· ④rule-only· ⑤ 503)으로 받는다. - 모델 회귀 시 되돌리기는 설정 변경이 아니라 모델 교체 절차를 따른다: 도서 임베딩 전량 재적재 + BE의 전 사용자 ⑥ 재호출 + 기억 벡터 재생성(ERD §7).
- 자체 서빙 장애는 폴백 없이 강등(①
V2의 "사진으로 책 찾기"는 이미지 임베딩 최근접이 아니라 이미지→텍스트 변환 + 기존 텍스트 검색이다.
최종 매칭은 항상 v_books 검색이 결정하고(카탈로그 밖 반환 불가), 모델은 검색어만 만든다.
| 구성요소 | 채택 | 이유 |
|---|---|---|
| OCR (표지 텍스트 추출) | 상용 API 우선 + 자체 폴백 | 저볼륨(이미지 턴, V2)이라 상용 단가(대략 이미지당 $0.001~0.0015)가 무의미한 수준. 대부분 표지는 OCR로 끝. 자체 Tesseract/PaddleOCR를 폴백으로 |
| VLM (OCR 실패분 폴백) | 상용 API, 제약된 프롬프트 | 글꼴·각도·가림으로 OCR이 실패한 경우만. 제목/저자/시리즈 단서를 구조화 텍스트로만 추출 "무슨 책인지" 답하게 하지 않음 (자유 응답 시 환각 → 최종 매칭은 DB 검색이 결정) |
| 표지 이미지 임베딩 인덱스 | V2에서도 보류 | 새 인프라(이미지 인코더·벡터 테이블·임계 튜닝) 값어치가 텍스트 안 읽히는 소수 케이스 대비 크지 않음. 국중도 서지에 표지 URL 존재 확인 → 트리거 관측 시 자체 open CLIP 색인으로 확장 (색인·질의 같은 인코더, 상용 멀티모달 API 락인 회피) |
| 생성형 VLM ("이 책 뭐야" 직접 답변) | 미채택 | 카탈로그 밖 환각 + 신간 학습 지연. "검색 결과만 반환"이 ③ grounding 원칙과 동일 |
-
비용·성능: 질의 = OCR(API, ~수백 ms) + (폴백 시) VLM +
v_books텍스트 검색(BM25, ms대). 이미지 턴 SLO에 여유. - 제약: OCR·VLM API 모두 데이터 경계 이탈 사용자 사진이 나감 → no-train + 짧은 보관 프로바이더 선택, Pre-signed URL만 넘기고 AI는 이미지 미저장.
- 표지 사진은 BE가 임시 보관함에 올리고 AI엔 만료되는 주소만 전달(명세 ③ V2).
| 시나리오 | 판단 | 근거 |
|---|---|---|
| 이미지 생성 API | 도입 안 함 | 서비스에 생성 기능 없음. "표지 없는 책 표지 생성"은 단계 5에서 상품정보 정확성·저작권 위반으로 기각 → 규칙 기반 타이포 플레이스홀더 |
| 영상 분석/생성 API | 도입 안 함 | 서비스에 영상 자산·기능 없음 |
| 음성 합성 / STT | 도입 안 함 | 음성 입출력 없음(단계 5 §5-C). STT→③ 텍스트 턴은 접근성 백로그 후보로만 도입 시 게이트웨이에 transcribe() 어댑터 1개 추가로 흡수 |
→ 과제의 "GPU 자원 부족 시 상용 생성 API" 긴장은 이 서비스에 거의 비해당. 유일한 상용 의존은 소형 텍스트 LLM이고, 그건 게이트웨이가 관리한다.
프로바이더 중립 인터페이스. 파이프라인·에이전트는 이것만 호출한다.
class ModelGateway:
def complete(self, req: CompletionReq) -> CompletionResp | Iterator[Delta]: ... # LLM, 스트리밍 옵션
def embed(self, texts: list[str], purpose: str) -> list[Vector]: ... # e5-small 단일. 다른 모델로 폴백하지 않음
def ocr(self, image: bytes) -> OcrResult: ... # OCR API(주) / 자체(폴백) → 실패분 VLM 폴백 (V2)
def vision_encode(self, image: bytes) -> Vector: ... # 자체 open CLIP — V2+, 표지 벡터 인덱스 트리거 시에만| 책임 | 구현 |
|---|---|
| 프로바이더 어댑터 |
GeminiAdapter · OpenAIAdapter · SelfE5Adapter · SelfClipAdapter · GcpVisionAdapter … 요청/응답 형식 차이 흡수 |
| 폴백 체인 |
[llm_primary, llm_secondary] 설정으로 순서 지정. 앞이 서킷 오픈이면 다음으로. 임베딩은 체인을 두지 않는다(다른 모델 벡터는 기존 인덱스와 섞을 수 없다) |
| 서킷 브레이커 | 프로바이더별 실패율·지연 임계 초과 시 오픈 → 폴백 or 강등. 임베딩 장애는 폴백 없이 바로 계약 헤더(X-Degraded: keyword-only/rule-only), LLM 장애는 ③ 본문 degraded: true
|
| 응답 캐시 | 임베딩은 텍스트 해시 키(선택). LLM 이유 응답은 캐시하지 않는다 긴 이유 보관은 BE 몫(명세 §1·③). TTL·스토리지 §8 |
| 레이트리밋 | 프로바이더별 토큰 버킷(RPM/RPD). 초과 시 429 매핑 + Retry-After
|
| 타임아웃·재시도 | 기본 타임아웃(생성 30s / 임베딩 5s), 재시도는 idempotent 호출만 + 지터 백오프 |
| 비용 관측 | 호출당 토큰·프로바이더·purpose 로깅 → 일일 지출 대시보드, 손익분기 추적 |
class ToolGateway:
registry: ToolRegistry # MCP 호환 디스크립터 로드 (2-B), allow_tools 필터
def invoke(self, call: ToolCall, turn: AgentTurn) -> ToolResult: ...
class ToolInvoker:
# 내부 REST: POST {BE}/internal/tools/{name}, Authorization: Bearer <서비스 토큰>
# - annotations.idempotentHint=false → 파생 멱등키 {idempotency_key}:{tool_call_index}
# - annotations.readOnlyHint=true → 재시도 자유 / false → 멱등키 있을 때만 재시도
# - 타임아웃 3s, 재시도 2회(지터 백오프), tool별 서킷 브레이커
# - 실패는 예외로 던지지 않고 ToolResult(ok=False, error=...) 로 흡수 → 발화로 안내, 200 유지
# - 모든 호출을 tool_calls[]에 감사 기록 (name/arguments/result/grounded)-
인증: 방향 전용 서비스 토큰(BE 발급).
- 사용자 위임 토큰 없음. 소유권은 BE가
arguments.user_id로 tool마다 재확인.
- 사용자 위임 토큰 없음. 소유권은 BE가
-
권한 모델:
allow_tools(턴별 화이트리스트, null이면 전부 허용, 표에 없는 이름은 400) →registry필터 →user_context.logged_in=false면 쓰기 tool(cart.add·cart.update) 원천 차단. -
grounding:
GroundingEnforcer가 발화의 모든 수치·상태(가격·재고·수량·합계·별점)를tool_calls[].result에 대조- → 뒷받침 없으면 그
tool_call을grounded: false로 기록하고 가능하면 그 문장만 축약(200). - 핵심 주장을 못 대면 422
grounding_failed(답변 통째 폐기, 클라이언트가 일반 안내로 대체).context_cards[].price는 직전 턴 스냅샷이라 쓰지 않고inventory.check/book.detail로 재검증한 값만 사용.
- → 뒷받침 없으면 그
-
되물음:
resolved_reference.confidence < 0.6이면 tool 미실행하고 되묻는 답변(200,tool_calls: []). -
왜 gRPC가 아니라 REST
- BE가 이미 REST, tool 수 적음, 감사 로그·디버깅 가독성, LLM tool-calling이 JSON Schema 네이티브. gRPC는 임베딩 자체서빙 사이드카(고QPS 인제스트)에서만 후보로 남긴다.
class CatalogSource(Protocol): # 새 소스 추가 시 이 인터페이스만 구현
def fetch(self, isbn13: str) -> RawRecord: ...
def list_recent(self, since: date) -> Iterator[str]: ...
class SeojiClient(CatalogSource):
# rate_limit=1req/s, 체크포인트(마지막 성공 ISBN), 실패 ISBN 데드레터 큐
# 에러 {RESULT:ERROR,ERR_CODE} → 재시도 or 데드레터, 크론은 계속 진행-
두 적재가 같은 SEOJI 소스를 보지만 쓰는 DB가 다르다:
- 카탈로그 레코드(제목·저자·가격·ISBN)는 BE MySQL로 들어가 복제되고,
- AI는 같은 원본에서
BOOK_INTRODUCTION·BOOK_TB전문만 따로 받아 청킹·②로 임베딩해 AI Postgresbook_passage에 색인한다.
- 병합은 ISBN-13 조인 키 3계층(
aladin-ingest메모- raw →
v_booksCOALESCE → 서빙). 새 소스는 raw 테이블 + COALESCE 우선순위만 추가.
- raw →
-
AI가 직접 돌리는 적재는 도서 임베딩(
book_embeddings) 배치뿐이다.-
v_books.description복제가 끝난 뒤 이어서 ②로 임베딩해 upsert한다(카탈로그 복제 크론과 레이트리밋 공유). -
v_books.description복제가 끝난 뒤 이어서 ②로 임베딩해 upsert한다(카탈로그 복제 크론과 레이트리밋 공유).
-
| 정책 | 모델 게이트웨이 | 도구 게이트웨이 | 적재 커넥터 |
|---|---|---|---|
| 타임아웃 | 생성 30s / 임베딩 5s / OCR 8s | 3s | 10s |
| 재시도 | idempotent만, 지터 백오프 2회 | 읽기 자유 / 쓰기 멱등키 있을 때만 2회 | 3회 후 데드레터 |
| 서킷 브레이커 | 프로바이더별 | tool별 | 소스별(크론 중단 아님) |
| 레이트리밋(아웃바운드) | 프로바이더 토큰 버킷 | BE가 넉넉(내부), 야간 배치 폭주 흡수용 안전장치 | 1 req/s 고정 |
| 에러 매핑 | 429 rate_limited+Retry-After / 503 upstream_unavailable / 504 generation_timeout
|
실패 흡수 → 200 + tool_calls 실패 기록 |
로그 + 데드레터 |
| 강등 신호 |
계약값은 X-Degraded: keyword-only(①) rule-only(④) 둘뿐. ③ LLM 장애는 본문 degraded: true + reason_long: null. rag-off는 AI 내부 관측 헤더(계약 밖, 단계 5). 복제 지연·행 부재엔 아무 헤더도 안 붙음 |
서킷 오픈 시 해당 의도만 비활성, 발화로 안내 | N/A |
| 상관관계 ID |
X-Request-Id 전파 |
턴 ID + tool_call_index
|
크론 실행 ID |
| 캐시 | 임베딩 텍스트 해시(선택). 이유는 캐시 안 함 — BE가 reason_long 보관 |
없음(쓰기·실시간 상태) | raw jsonb 원본 보관 |
| 추가 대상 | 절차 | 호출부 영향 |
|---|---|---|
| 새 모델 프로바이더 (LLM 교체, 소형 LLM 자체서빙) | 게이트웨이 어댑터 1개 + 폴백 체인 설정 | 없음 (LLM 한정. 임베딩 모델 교체는 어댑터 교체에 더해 도서 임베딩 전량 재적재·BE의 ⑥ 재호출·기억 벡터 재생성이 필요하다, ERD §7) |
| 새 모달리티 (STT 접근성, 향후 오디오북) | 게이트웨이에 transcribe() 인터페이스 + 어댑터 |
신규 파이프라인만, 기존 무변경 |
| 새 BE tool (에이전트 스코프 확장) | MCP 호환 디스크립터를 registry에 추가 + BE가 /internal/tools/{name} 구현 |
LLM이 registry에서 자동 인식. allow_tools로 점진 롤아웃 |
| 자체 BE 밖 도구 (배송사·예약 등) |
ToolInvoker 전송 어댑터를 MCP 클라이언트로 교체 |
디스크립터가 이미 MCP 형식이라 전송만 스왑 |
| 새 카탈로그 소스 (알라딘 복귀 등) |
CatalogSource 구현 + raw 테이블 + COALESCE 우선순위 |
서빙·추천 무변경(복제된 v_books만 읽음) |
| 우리가 MCP 서버가 되는 경우 (B2B 추천 도구 제공) | ①③ 로직을 MCP tools/list+tools/call로 래핑한 별도 어댑터 배포 |
코어 파이프라인 무변경, 어댑터 계층 신설 |
-
모든 외부 I/O가 2개 게이트웨이로 수렴
- 커넥션 풀링, 레이트리밋 샤딩, 프로바이더별 스케일을 한 곳에서 조정. 파이프라인 수가 늘어도 통합 표면은 안 늘어난다.
-
커머스 데이터는 복제로 들어와 AI가 BE에 매 요청 조회하지 않는다
- BE 부하·왕복 지연이 추천 트래픽에 비례해 늘지 않고, AI Postgres 하나만 읽기 스케일하면 된다. 대가는 신선도 예산 안의 지연(가격·재고 5분 등).
- tool 세트가 선언적 디스크립터라, 에이전트 능력 확장이 "디스크립터 + BE 엔드포인트" 추가로 끝난다.
- MCP 전송을 안 깔았지만 디스크립터를 MCP 형식으로 유지해 전환 옵션을 열어뒀다
- 서드파티·다중 소비자가 실제로 생기는 시점에 전송만 교체.
- 프로바이더·tool 변경이 어댑터/디스크립터로 국소화. LLM 호출 지점이 온라인 1곳(③)·야간 1곳(⑤)으로 명시적(V2에서 ⑦·표지 추가)
- → grounding·강등 로직을 그 지점에 직접 붙인다(프레임워크 추상화 뒤에 숨지 않음
- 후보 선택·채점은 결정론, LLM은 최종 3권 + 이유만).
- MCP 서버 프로세스·JSON-RPC 브리지를 안 넣어 의존성 트리가 얇다
- (FastAPI + 임베딩 SDK + LLM SDK + HTTP 클라이언트). 배포·장애원인 추적이 단순.
- 외부 호출 정책(재시도·타임아웃·서킷)이 한 표로 통일
- → 신규 통합도 같은 규칙을 따른다.
- 자체 서빙 임베딩이 최다 호출·최다 과금 경로를 제거(건당 과금 0, 22배 지연 개선).
-
긴 이유를 BE가 보관해 상세 페이지 재진입 시 LLM 0회
- 이유 캐시 테이블·만료·권한 규칙이 통째로 사라짐(명세 결정). 대가는 안 열어 볼 카드의 긴 이유까지 생성하는 턴당 출력 토큰.
- 상용 LLM은 진짜 생성이 필요한 텍스트에만, 소형 모델 클래스로. 유휴 GPU 없음.
- 표지 인식은 V2에서도 텍스트 검색(OCR+VLM 폴백)으로 출시
- 이미지 벡터 인프라를 트리거 관측 전엔 안 얹음.
- MCP 전송 오버헤드(추가 홉·프로세스)를 값이 안 나오는 곳에 안 넣음.
- 게이트웨이의 호출당 비용 로깅이 자체서빙 전환 손익분기를 상시 추적
- → 트래픽이 임계를 넘으면 데이터에 근거해 소형 LLM 자체서빙으로 전환.