개발 slecs

AI 상담사를 Claude API 직접 호출로 전환해 품질과 보안 강화

목차

외부 서비스를 끼고 AI 기능을 붙이는 건 초기엔 빠르다. SDK 하나 설치하고 엔드포인트 주소만 바꾸면 동작하니까. 근데 그 편의가 나중엔 발목을 잡는다. 응답 포맷이 래퍼 서비스 마음대로 바뀌거나, 시스템 프롬프트 주입에 제약이 생기거나, 중간 레이어의 레이턴시가 누적되는 문제. 이번 작업은 그 중간 레이어를 걷어내고 Claude API를 직접 호출하는 구조로 전환한 것이다.

작업 규모 자체는 크지 않았다. 하지만 "직접 호출"이라는 결정이 이후 커스터마이징 폭 전체를 바꿔놨다.

구조 전환 - 왜 직접 호출인가

기존 구조는 외부 서비스가 Claude를 한 번 감싸고 있어서, 시스템 프롬프트를 얼마나 어떻게 넣을 수 있는지가 그 서비스 스펙에 달려 있었다. FAQ를 주입하거나 역할 지시를 세밀하게 조정하려면 그쪽 문서를 뒤져야 했고, 안 되면 포기해야 했다.

Claude API의 messages 엔드포인트를 직접 쓰면 이게 다 사라진다. 시스템 프롬프트 구조, 턴 구성, 온도값, 토큰 제한 전부 코드 안에서 제어된다.

전환 후 기능 흐름은 이렇다:

사용자 질문
  → 내부 클래스 (API 호출 담당)
    → Claude API (messages endpoint)
  → 응답 파싱
  → 채팅 UI 렌더링

단순해 보이지만 내부 클래스가 생긴 게 포인트다. API 키 관리, 재시도 로직, 에러 핸들링이 전부 이 클래스 안에 묶여 있어서 UI 쪽 코드는 "질문 보내고 답 받는다"만 알면 된다. 나중에 모델을 바꾸거나 파라미터를 조정해도 UI 코드는 안 건드린다.

레이턴시도 눈에 띄게 줄었다. 중간 레이어 하나를 없앴을 뿐인데 체감 응답 속도가 달라졌다. 이건 측정해서 수치로 남겼어야 했는데 그냥 넘어간 게 아쉽다.

FAQ 주입과 상담사 페르소나 설계

이번 작업에서 생각보다 손이 많이 간 부분이 시스템 프롬프트 설계였다. 직접 호출 전환 자체는 API 키 연결하고 포맷 맞추면 끝인데, "이 AI 상담사가 뭘 알고 어떻게 말해야 하는지"를 정의하는 게 더 오래 걸렸다.

두 가지를 넣었다.

첫 번째는 역할과 이름 정의다. 처음 접속했을 때 사용자가 "이게 뭘 해주는 건데?"를 묻지 않도록, 어떤 걸 물어볼 수 있는지 안내 문구가 나온다. 이게 없으면 빈 입력창 앞에서 멈추는 사람이 생긴다. 상담사 이름도 정했는데, 이름이 있으면 "이거 AI야?"보다 "이거 뭐 물어봐도 돼?" 쪽으로 심리적 진입장벽이 낮아진다.

두 번째가 FAQ 주입이다. 자주 묻는 질문에 대해 일관된 답이 나오게 하려면 모델한테 그 정보를 줘야 한다. 구조는 DB나 파일에서 FAQ를 로드해서 시스템 프롬프트에 동적으로 붙이는 방식이다.

def build_system_prompt(faq_list: list[dict]) -> str:
    faq_block = "\n".join(
        f"Q: {item['question']}\nA: {item['answer']}"
        for item in faq_list
    )
    return f"""당신은 [서비스명] AI 상담사입니다.
아래 FAQ를 참고해 답변하되, 없는 내용은 추측하지 마세요.

[FAQ]
{faq_block}
"""

FAQ를 하드코딩하지 않고 로드 구조로 만든 건 맞는 방향이다. 운영하다 보면 FAQ가 바뀌는데, 코드 배포 없이 DB만 수정해서 반영할 수 있다.

보안 레이어 - 경고, 정지, 블랙리스트 자동 등록

AI 상담 기능은 입력이 자유롭기 때문에 악용 가능성이 일반 폼보다 크다. 이번에 세 가지 레이어를 붙였다.

레이어 내용
접근 제어 로그인 필수, 비로그인 차단
경고/정지 부적절 이용 감지 시 경고, 누적 시 30분 정지
자동 블랙리스트 공격성 요청 감지 시 DB 등록, 이후 요청 즉시 차단

경고·정지 횟수는 DB에 남긴다. 메모리에만 두면 서버 재시작 시 초기화되고, 세션 단위로만 추적하면 로그아웃 후 재접속으로 우회된다.

자동 블랙리스트가 실시간 대응에서 핵심이다. 수동으로 확인하고 차단하는 구조는 공격 감지와 차단 사이에 공백이 생긴다. 감지와 동시에 DB에 등록하면 다음 요청부터 바로 막힌다.

다만 자동 등록은 오탐 리스크가 있다. 운영하면서 주의할 것들:

  • 오탐 방지: 로드 테스트나 배치 작업이 봇으로 잡히지 않도록 화이트리스트 IP나 API 키 예외 처리를 미리 넣는다
  • TTL 설정: 영구 차단보다 만료 시간을 걸어서 오탐이 났을 때 자동 복구되게 한다. 영구 차단은 관리자 수동 해제가 필요해서 운영 부담이 크다
  • 알림 연동: 자동 차단이 발생할 때 슬랙이나 디스코드로 알림을 보내야 한다. 차단 자체는 자동화했어도, 패턴이 뭔지 모니터링은 사람이 해야 하기 때문

자동화의 함정은 "잘 돌아가고 있겠지"다. 알림 없이 자동 차단만 걸어두면 오탐이 쌓여도 모른다.

이번 전환 작업 전체를 돌아보면, 기능 추가보다 구조를 제대로 잡는 데 시간을 더 쓴 것 같다. FAQ 주입, 페르소나 정의, 보안 레이어, 이 세 가지가 다 처음부터 같이 설계됐어야 하는데 순서가 좀 엉켰다. 다음에 비슷한 걸 붙일 때는 시스템 프롬프트 설계를 API 연결보다 먼저 하는 게 맞다. 응답 품질이 결국 거기서 결정되니까.

댓글 0

첫 댓글 달아줘.