IT/AI

[Kanana-o] 직접 만든 멀티모달 채용검색 서비스 오픈해보기 (#CareerLens)

snapcoder 2026. 7. 5. 17:34
728x90
반응형
SMALL

CareerLens - AI 기반 채용검색 서비스

https://careerlens.kro.kr/

 

CareerLens - AI 기반 채용검색 서비스

당신의 커리어, 렌즈로 보다

careerlens.kro.kr

 

 

 

 

 

 

Kanana-O로 멀티모달 채팅 서비스를 만들었는가

사이드 프로젝트에 AI 채팅 기능을 붙이면서 고민한 부분이 있었습니다. 텍스트만 주고받는 챗봇은 이미 흔하고, 음성과 이미지까지 함께 처리할 수 있으면 사용자 입장에서 훨씬 자연스러운 대화가 가능해집니다. 마침 Kanana-O API 앰배서더로 활동할 기회가 생겨, 직접 개발 중인 서비스에 Kanana-O 멀티모달을 연동해 보기로 했습니다.

이 글은 단순 튜토리얼이 아닙니다. 실제로 개발하면서 맞닥뜨린 API 제약, 설계 고민, 그리고 직접 테스트한 결과를 기록한 글입니다. Kanana-O에 대한 공식 문서는 카카오 HuggingFace 페이지(https://huggingface.co/kakaocorp)에서 확인하실 수 있습니다.

 

kakaocorp (AXZ Corp.)

Org profile for AXZ Corp. on Hugging Face, the AI community building the future.

huggingface.co

 





어떤 서비스인가

채용 관련해서 데이터를 주기적으로 수집·적재하고,

사용자가 검색하거나 AI와 대화하면서 원하는 정보를 제공하는 서비스입니다.

 

기존에는 키워드 검색만 제공했는데, AI 채팅 기능을 추가하면서 "음성으로 말하면 알아서 찾아줬으면", "이 이미지 보고 관련된 것 추천해줘"처럼 더 자연스러운 인터페이스가 필요하다고 판단했습니다.

 

그래서 Kanana-O의 멀티모달 기능을 핵심으로 삼아 채팅 컴포넌트를 설계했습니다.

계층 기술스택
Backend Java 17 + Spring Boot 3, WebSocket
Frontend Next.js 14 + TypeScript, Web Audio API
AI API Kanana-O (텍스트·음성·이미지 멀티모달) + 기타 무료 LLM 모델들
검색 Elasticsearch 8.18 (kNN + BM25 하이브리드)

 



7가지 입력 조합 설계

Kanana-O는 텍스트, 음성(오디오), 이미지를 모두 처리할 수 있는 멀티모달 모델입니다.

이 세 가지를 조합하면 이론상 7가지 입력 시나리오가 만들어집니다.

 

처음에는 "텍스트만 지원하고 나머지는 나중에"라고 생각했는데, API 구조를 살펴보니 처음부터 전체 시나리오를 설계해두는 편이 낫겠다고 판단했습니다. 나중에 하나씩 추가하면 메시지 구조가 계속 바뀌어서 히스토리 관리가 복잡해지기 때문입니다.

시나리오 입력 API 호출 방식
1 텍스트만 streamChat
2 이미지만 streamChat (이미지 캡셔닝 후 텍스트로)
3 텍스트 + 이미지 streamChat (이미지 포함)
4 음성만 streamChatAudio (오디오 직접 전송)
5 텍스트 + 음성 streamChatAudio
6 음성 + 이미지 streamChatAudio (이미지 캡셔닝 병렬)
7 텍스트 + 음성 + 이미지 streamChatAudio

 

음성이 포함된 시나리오(4~7)는 오디오 데이터를 Kanana-O API에 직접 전송합니다.

STT 결과 텍스트는 화면에 표시하는 용도로만 쓰고, API 파라미터로는 사용하지 않습니다.

Kanana-O가 오디오를 직접 이해하는 능력을 활용하는 방향으로 설계했습니다.



 

 

 

 

이미지 처리 흐름과 캡셔닝 타이밍의 설계

이미지가 포함된 요청에서 캡셔닝(=Kanana-o API 호출하여 이미지 키워드 추출하는 프로세스)을 언제 실행할지는 생각보다 중요한 설계 포인트입니다.

캡셔닝은 Kanana-O API를 한 번 더 호출하는 동기 작업이라, 무조건 먼저 실행하면 불필요한 지연이 생깁니다.

 

텍스트가 없을 때는 캡셔닝이 의도 판단의 유일한 단서이므로 먼저 실행해야 하지만,

텍스트가 있는 경우엔 텍스트만으로 의도를 먼저 판단할 수 있습니다.

 

이를 반영해 아래 세 가지 경우로 분기합니다.

케이스 캡셔닝 시점 이유
이미지만 (텍스트 없음) 의도 감지 전 캡션이 없으면 의도 판단 불가
텍스트 + 이미지, 의도 YES 의도 감지 후 추천 확정 후 이미지 키워드로 ES 검색 보완
텍스트 + 이미지, 의도 NO 스킵 추천 불필요 → 캡셔닝 API 호출 자체를 생략

 

if (!hasText && hasImage) {
    // 텍스트 없음: 캡셔닝 먼저 → 캡션으로 의도 판단
    imageCaption = kananaService.extractKeywords(imageDataUrl, null, ...);
}

// 텍스트(또는 캡션)로 의도 감지
SmartKeywordExtractor.JobIntentResult intentResult =
    smartKeywordExtractor.detectJobIntent(intentInput, conversationContext);

if (intentResult.isJobRequest()) {
    if (hasText && hasImage) {
        // 추천 의도 확정 후에만 캡셔닝 실행 — 이미지 키워드로 ES 검색 보완
        imageCaption = kananaService.extractKeywords(imageDataUrl, lastUserText, ...);
    }
    // ES 검색: 텍스트 키워드 우선, 없으면 이미지 캡션 키워드로 대체
    List<String> searchKeywords = !intentResult.keywords().isEmpty()
        ? intentResult.keywords()
        : Arrays.asList(imageCaption.split(",\\s*"));
    recommendedJobs = esService.searchWithFallback(searchKeywords, 10, searchMethod);
}
// 의도 NO + 텍스트+이미지 → 캡셔닝 스킵, Kanana-O 이미지 직접 전달

 

이 구조 덕분에 "이 사진 봐줘" 같은 단순 대화에선 캡셔닝에 대한 Kanana-o API 호출이 발생하지 않고,

추천 요청일 때만 이미지 분석 비용이 소모됩니다.



 

 

 

data URL prefix 400 에러

이미지 연동을 막 시작했을 때 계속 400 Bad Request가 떨어졌습니다.

프론트에서 이미지를 data:image/jpeg;base64,... 형태(data URL)로 전달했는데,

Kanana-O API가 이 형식을 지원하지 않는것 같습니다.

# data URL 형식 전송 시
→ 400 "Message N image item N contains invalid base64 image data"

# raw base64만 전송 시
→ 200 OK (정상 응답)

 

curl로 직접 두 형식을 비교 테스트해서 원인을 확인했습니다.

OpenAI API는 data URL을 그대로 받아주지만, Kanana-O는 순수 base64 문자열만 허용하는 것 같습니다.

 

 

이후 서버에서 data:image/...;base64, prefix를 제거하는 전처리를 추가했습니다.

// Kanana-o API는 raw base64만 허용 — data URL prefix 제거
String finalImageUrl = imageDataUrl.startsWith("data:image/")
    ? imageDataUrl.substring(imageDataUrl.indexOf(",") + 1)
    : imageDataUrl;

 

Kanana-O를 연동하는 분이라면 이미지 전송 시 prefix 제거를 처음부터 챙기시면 좋을 것 같습니다.



 

 

멀티턴 대화에서 음성·이미지를 어떻게 처리하는가

멀티턴 대화를 구현하면서 중요한 제약을 하나 발견했습니다. 공식 문서와 실제 테스트 결과를 종합하면, 음성과 이미지는 현재 턴(마지막 메시지)에만 포함 가능합니다. (이전 턴의 음성·이미지를 그대로 base64로 넘기는 방식은 지원되지 않습니다.)

# 멀티턴 메시지 배열 구조
messages = [
    {"role": "user", "content": "백엔드 개발자야"},           # Turn 1 (텍스트화)
    {"role": "assistant", "content": "반가워요! ..."},         # Turn 2 (텍스트만)
    {"role": "user", "content": "공고 추천해줘"},              # Turn 3 (텍스트화)
    {
        "role": "user",
        "content": [                                          # Turn 4 — 현재 턴만 멀티모달
            {"type": "input_audio", "input_audio": {"data": "...", "format": "wav"}},
            {"type": "text", "text": "이미지도 같이 볼게"}
        ]
    }
]

 

이건 OpenAI GPT-4o도 동일한 패턴입니다. 과거 대화의 핵심은 "의도와 결과"이지 원본 파일이 아니라는 설계 철학입니다. 음성 입력이 들어오면 STT 텍스트를 히스토리에 저장하고, 이미지는 추출한 키워드/설명으로 변환해서 다음 턴 컨텍스트로 활용하는 방식으로 구현했습니다.



 

 

 

 

스트리밍 응답 특성에 대한 OpenAI와 다른 점

Kanana-O와 OpenAI 모두 SSE(Server-Sent Events)로 스트리밍을 제공하지만, delta 값의 의미가 다릅니다.

  OpenAI (GPT-4o 등) Kanana-O
delta 값 증분(diff) — 이번 토큰만 누적 전체 텍스트
클라이언트 처리 기존 버퍼에 append 매번 덮어씀(set)
최종 응답 전체 delta 합산 마지막 delta = 최종 응답

 

OpenAI 방식에 익숙하면 Kanana-O에서 처음에는 응답이 계속 반복되는 것처럼 보여서 당황할 수 있습니다. 실제로 그런 경험을 했습니다. 서버와 프론트 양쪽 모두 "append가 아닌 set" 방식으로 처리해야 합니다.

// Kanana-O: delta마다 누적값 → 마지막 값이 최종 응답이므로 항상 덮어씀
lastText.set(chunk);  // append 아님
sendWsJson(wsSession, Map.of("type", "token", "data", chunk));

// OpenAI라면 이렇게 썼을 코드 (append 방식)
// buffer.append(chunk);
// sendWsJson(wsSession, Map.of("type", "token", "data", buffer.toString()));

 

프론트엔드에서도 수신한 token 값을 기존 텍스트에 붙이는 게 아니라 그대로 교체하면 됩니다. 어느 쪽이 낫다기보다는 API마다 다른 규약이니 문서에서 먼저 확인하시면 좋을 것 같습니다.



 

 

 

 

API 키 Fallback 설계

Kanana-O API를 쓰면서 쿼터(429) 초과가 종종 발생했습니다. 사이드 API 키를 확보할 수 있다면, 순서대로 fallback하는 구조를 적용하면 좋을 것 같습니다.

// 1차 키 쿼터 초과 시 → 2차 키로 재시도
if (isQuotaExceeded(response.statusCode()) && shouldFallback) {
    response = httpClient.sendAsync(buildRequest(json, apiKey, 60), ...);
}
// 2차 키도 초과 시 → 3차 키로 재시도
if (isQuotaExceeded(response.statusCode()) && shouldFallback) {
    response = httpClient.sendAsync(buildRequest(json, doubleApiKey, 60), ...);
}

 

서비스 UI에는 API 키를 직접 입력할 수 있는 기능도 구현해두었습니다. 설정 아이콘을 누르면 아래처럼 키 변경 모달이 열립니다.

 

본인 키를 입력하면 해당 키로 요청이 전송되므로, 사용해보고 싶은 분들께서는 누구나 쓰실 수 있습니다.

https://careerlens.kro.kr/ 에 접속하셔서 직접 체험해보세요!

 

음성·이미지·텍스트 모두 지원하는 멀티모달 채팅과

3가지 페르소나(서울 표준어, 부산 사투리, 기본 AI)를 자유롭게 시험해보시면 좋을 것 같습니다.

 

CareerLens - AI 기반 채용검색 서비스

당신의 커리어, 렌즈로 보다

careerlens.kro.kr

 



 

 

3가지 페르소나, 방언 지원까지

채팅에 페르소나 개념을 도입해서 3가지 모드를 지원합니다.

페르소나 특징 파라미터
서울 표준어 표준 경어체 상담 응답 Seoul
부산 사투리 부산 방언으로 친화적 상담 Busan
기본 AI 모드 일반 AI 응답 (기능 테스트용) Test

 

페르소나는 시스템 프롬프트 구성 시 dialect 파라미터로 분기됩니다. 이미지 캡셔닝 단계에서도 동일한 dialect를 넘겨서, 캡션 추출 프롬프트도 페르소나에 맞게 조정됩니다.





실제 테스트 결과

멀티턴 대화 테스트

"나 모빌리티 보안 백엔드 개발자야 안녕"이라고 자신을 소개한 뒤 -> "채용공고 추천해줘"라고 요청하는 흐름을 테스트했습니다.

 

첫 번째 메시지에서 Kanana-O가 직군(모빌리티 보안 백엔드)을 파악하고 자연스러운 관심사 질문으로 대화를 이어갑니다.

이후 "채용공고 추천해줘" 요청에서는 이전 대화 컨텍스트를 반영해서 보안 관련 직무 공고 목록을 반환했습니다.

멀티턴이 제대로 동작하는 핵심은 히스토리를 텍스트로 변환해서 메시지 배열에 쌓는 구조입니다.

음성으로 대화하더라도 히스토리는 항상 텍스트로 관리되므로, 다음 턴에서 "아까 말한 것" 같은 참조가 자연스럽게 작동합니다.



 

 

 

 

이미지 기반 추천 테스트

이미지를 첨부하고 "이 사진과 관련된 공고 추천해줘"라고 입력하는 시나리오를 테스트했습니다.

 

이미지를 받으면 Kanana-O가 관련 키워드를 추출합니다.

그 키워드로 Elasticsearch 검색을 수행한 뒤 결과 공고 목록을 직접 반환합니다.

실제로 테스트 결과 이미지 분석부터 공고 목록 응답까지 수초 내에 처리됩니다.



 

 

 

음성 입력 + TTS 응답

음성으로 질문하면 Kanana-O가 텍스트 + 오디오(TTS)를 동시에 스트리밍으로 반환합니다.

프론트에서는 텍스트 버블과 재생 가능한 오디오 플레이어를 함께 표시합니다.

// 텍스트와 오디오를 함께 응답하도록 설정
body.put("modalities", List.of("text", "audio"));
body.put("audio", Map.of("voice", selectedVoice));  // 사용자별 TTS 목소리 선택 가능

 

부산 사투리 페르소나로 음성 대화를 하면 방언 억양이 반영된 TTS가 나오는 게 흥미로웠습니다. 텍스트 프롬프트만으로 방언 스타일을 제어할 수 있다는 점이 Kanana-O의 강점 중 하나입니다.

 

[관련 테스트 결과 블로깅]

https://snapcode.tistory.com/232

 

[Kanana-o] 멀티모달 정말 잘 될까? 7가지 입력 시나리오 테스트 결과 총정리

멀티모달 검증지난 3개월 간 Kanana-O API의 멀티모달 입력 기능을 체계적으로 검증했습니다.텍스트만 입력하는 것부터 음성, 이미지를 함께 조합하는 방식까지 총 7가지 시나리오를 16개의 구체적

snapcode.tistory.com

 





멀티모달 연동을 마치며

Kanana-O를 서비스에 직접 붙이면서 단순히 API를 호출하는 것 이상의 설계가 필요하다는 걸 실감했습니다.

  • 이미지는 raw base64로 — data URL prefix는 반드시 제거
  • 멀티턴은 텍스트로 — 음성·이미지 히스토리는 텍스트 변환 후 보관
  • 스트리밍은 누적값 — 마지막 delta가 최종 응답

단순한 챗봇이 아니라, 음성으로 말하고 이미지를 보내면 맥락에 맞는 결과를 돌려주는 인터페이스를 만들 수 있었습니다.

Kanana-O 앰배서더 활동 덕분에 실제 서비스에서 멀티모달 AI를 다양하게 실험해볼 수 있었고, 개발하면서 발견한 제약사항들이 나중에 누군가에게는 도움이 돼고, Kanana의 성능 발전에 조금이나마 기여됐으면 하는 바램입니다.

 

728x90
반응형
LIST