AI 자동화 프로젝트 2: 작업은 성공했는데 답은 틀렸다
TL;DR
- 작업과 답변을 PostgreSQL에 보존하고, 웹에서 상태·재시도·Kafka 연결 관계를 확인하도록 확장했다.
- 실제 Ollama로 요청 10개를 처리했다. 모두 전달까지 완료됐지만, 답변에는 분류 오류와 원문 왜곡이 있었다.
- 처리 완료와 답변 품질을 구분하기 위해 Jev 평가 기능을 추가했다. 현재는 API 키 없이 구현·모의 검증까지 마쳤으며, 실제 Jev의 정확도는 아직 확인하지 않았다.
들어가며
첫 번째 기록에서는 Kafka와 Coral을 연결하고, 요청 하나가 메시지로 남는 흐름을 확인했다. 당시 Worker는 DEMO 응답을 반환했고, 실제 모델 추론과 기록 영속화가 다음 과제였다.
이후에도 Codex와 함께 구현을 이어 갔다. 업무 시나리오는 기존의 프롬프트 입력 흐름을 유지하면서, 결과를 보존하고 실패한 작업을 다시 처리하는 기능부터 채웠다. 실제 LLM도 연결했다. 그런데 화면에 ‘완료’가 표시되는 것만으로는 부족하다는 사실을 실행 결과에서 확인했다.
기록을 남기고, 실패한 단계부터 다시 실행하기
작업과 답변은 PostgreSQL에 저장하도록 바꿨다. 접수할 때 작업과 Kafka 전송 대기 기록을 같은 트랜잭션에 넣고, 별도 작업이 대기 기록을 읽어 발행하는 Outbox 방식을 사용한다. Kafka가 잠시 중단되어도 접수한 작업이 DB에 남는다.
상태는 QUEUED → RUNNING → DELIVERING → SUCCEEDED로 이어진다. 추론 결과는 Coral 전달 전에 저장한다. 모델 실행이 실패하면 추론을 다시 시도하고, 전달만 실패하면 저장된 답변을 재전송한다. 이미 만든 답변을 전달하기 위해 LLM까지 다시 호출할 필요는 없다.
중복 이벤트와 이전 시도의 늦은 응답도 확인한다. 다만 장애 시점에 따라 외부 모델 호출 자체가 반복될 가능성까지 없앤 것은 아니다. Coral의 스레드와 메시지는 여전히 메모리 상태이지만, 앱에서 조회하는 작업과 답변은 DB에 남도록 했다.
Kafka 연결 관계도 화면에서 보고 싶었다
웹 화면에서는 프롬프트 접수, 상태 조회, 결과 확인, 실패 재시도를 할 수 있다. 여러 요청을 한 번에 넣고 묶음의 진행률과 대기·추론·전체 시간을 보는 기능도 추가했다.
Kafka 구성은 동적 그래프로 만들었다. 토픽과 Consumer 그룹, 개별 Consumer, 실제 파티션 할당을 조회한다. 토픽이나 Consumer가 늘고 줄거나 리밸런싱이 일어나면, 10초 주기로 갱신하는 화면에 그 관계가 반영된다.
실선은 현재 파티션 할당이고, 점선은 앱 설정으로 확인한 발행·실패 처리 경로다. 개별 메시지가 이동하는 장면을 추적하는 기능은 아니다. 그래도 고정된 아키텍처 그림보다 지금 실행 중인 구성을 확인하기에는 도움이 된다.
실제 모델을 연결하니 보인 것
앱, Kafka, PostgreSQL, Coral에 이어 Ollama도 로컬 Docker 환경에서 실행했다. CPU에서 qwen2.5:1.5b를 사용하고 출력 토큰 상한을 256으로 설정했다. 예시 프롬프트 10개를 한 묶음으로 실행한 기록은 다음과 같다.
| 항목 | 측정 결과 |
|---|---|
| 전달까지 완료한 작업 | 10개 / 10개 |
| 묶음 전체 소요 시간 | 약 47.9초 |
| 작업당 평균 추론 시간 | 약 4.6초 |
한 번의 로컬 실행 기록이다. 부하 테스트나 모델 간 성능 비교 결과는 아니다. 묶음 전체 시간에는 대기와 전달도 포함된다.
답변을 읽어 보니, 완료한 작업 중에도 문제가 있었다.
| 요청 요지 | 실제 출력 | 확인한 문제 |
|---|---|---|
| “이 가방은 방수가 되나요?”를 배송·환불·상품 중 분류 | 배송 |
상품 특성 문의를 잘못 분류했다. |
| “제품이 고장 나서 사용할 수 없습니다.”의 감정 분류 | 긍정 |
부정적인 리뷰를 반대로 판단했다. |
| 회의가 오후 2시에서 3시로 바뀌었다는 안내 작성 | "오늘의 회의 시간이 오전 2시에서 3시로 변경되었습니다." |
‘오후’를 ‘오전’으로 바꿨다. |
현재 SUCCEEDED는 답변을 생성하고 저장한 뒤 Coral 전달까지 완료했다는 뜻이다. 정확성을 승인했다는 뜻은 아니다. 답변이 없어서 실패한 경우와, 답변은 있지만 내용이 잘못된 경우를 따로 다뤄야 했다.
Kafka 뒤에서 LLM을 호출하는 것만으로 충분할까
만드는 도중 이런 질문도 생겼다. 결국 Consumer에서 LLM을 호출하는 구조라면, 이 프로젝트의 메리트는 얼마나 있을까?
Kafka를 붙였다는 사실만으로 차별성이 커지지는 않는다. 단순한 대기열이 필요하다면 다른 큐나 DB 기반 작업 처리도 검토할 수 있다. Kafka의 순서 보장은 파티션 단위이고, 여러 파티션에 걸친 전체 작업의 완료 순서를 보장하지 않는다. Worker를 늘려도 모델 서버가 병목이면 처리량이 기대만큼 늘지 않을 수 있다. Kafka Consumer 공식 문서
현재까지 직접 구현한 가치는 작업을 보존하고, 어디에서 실패했는지 확인하고, 필요한 단계부터 다시 실행하는 데 있다. 여기에 답변의 적합성을 살펴보는 후속 처리를 연결해 보기로 했다.
Jev를 결과 평가 Consumer로 연결했다
검토한 모델은 TypeSafe의 Jev다. 입력 상태와 평가 질문을 보내면 정해진 타입의 답과 확률을 반환하는 API를 제공한다. 이번에는 원래 요청과 생성된 답변을 함께 보내고, 각 기준에 pass / fail / unknown을 선택하는 Choice 형식을 사용했다. TypeSafe 공식 소개
처음 아이디어에는 어떤 토픽으로 요청을 보낼지 판단하는 기능도 있었다. 이번 단계에서는 생성된 답변의 평가 기록부터 구현했다. 의미 기반 토픽 분기와 자동 재생성, 사람의 승인 흐름은 아직 적용하지 않았다.
아래는 shadow 모드를 활성화했을 때의 흐름이다. 현재 로컬은 키가 없어 OFF 상태다. Jev는 이 구성에서 외부 API로 호출하며, 실제 호출은 아직 하지 않았다.
평가 Consumer는 전달 Consumer와 다른 그룹이다. 두 그룹이 같은 결과 토픽을 각각 읽기 때문에, 평가가 전달용 메시지를 나눠 가져가지 않는다. 하나의 결과를 서로 다른 후속 작업에 연결하는 데 Kafka의 구독 구조를 활용했다. Kafka Consumer 공식 문서
평가 기준은 요청을 수행했는지, 입력의 사실과 의미를 유지했는지, 지정한 언어와 형식을 지켰는지의 세 가지다. ‘배송’이 허용된 분류값인지만 확인하면 부족하다. 방수 여부를 묻는 요청에 맞는 선택인지도 판단하도록 질문을 구성했다.
| 조건 | 종합 평가 의견 |
|---|---|
기준 이상 확신도의 fail이 하나라도 있음 |
수정 권고 (REJECT) |
모든 항목이 기준 이상 확신도의 pass |
통과 의견 (PASS) |
그 외: 낮은 확신도나 unknown |
검토 권고 (REVIEW) |
임계값 0.85는 검증 전 초기값이다. Jev의 확신도는 선택지 확률 분포에서 계산한 값이므로, 이를 정답률 85%로 해석해서는 안 된다. 실제 사례에서 사람의 판단과 비교하며 기준을 조정해야 한다. Jev 확신도 문서
외부 자료를 검색하는 기능은 붙이지 않았다. 제공한 요청과 답변으로 확인할 수 없는 사실은 unknown으로 판단하도록 명시했다. 정확한 글자 수, 산술, JSON 스키마처럼 코드로 확인할 조건은 별도 검증이 필요하다. Jev 문서에서도 숫자와 정밀한 계산 등의 한계를 설명한다. Jev 1.13 모델 한계
먼저 평가를 기록하는 모드로 시작했다
shadow 모드에서는 평가가 답변 전달을 막지 않는다. 작업은 SUCCEEDED인데 평가 의견은 REJECT일 수 있다. 웹에서도 전달 완료와 수정 권고를 구분해 표시한다. 평가 모델도 오답을 통과시키거나 정상 답변을 거절할 수 있으므로, 먼저 그 판단을 관찰하려고 한다.
평가 Consumer는 DB에 평가 작업을 저장하고, 별도 실행 작업이 API를 호출한다. 평가 API 장애가 기존 답변 전달에 영향을 주지 않도록 흐름을 나눴다. 중복 결과 이벤트는 같은 답변의 자동 평가를 반복하지 않으며, 일시적인 호출 실패는 제한된 횟수 안에서 재시도한다.
이력에는 당시 요청과 답변, 모델 버전, 평가 기준과 임계값을 남긴다. 저장된 답변을 다시 평가할 수 있어 LLM이 답변을 새로 생성할 필요는 없다. 현재처럼 키가 없으면 웹에 미설정 상태를 표시하고 평가 버튼을 비활성화한다.
확인한 것과 다음에 확인할 것
현재 테스트 기록은 48개 중 46개 통과, 2개 건너뜀이다. 건너뛴 항목은 별도 설정으로 활성화하는 실제 서비스 연동 테스트다. 이와 별도로 로컬의 Kafka·PostgreSQL·Coral과 실제 Ollama를 사용하는 smoke 검증을 통과했다.
Jev는 모의 HTTP 응답으로 요청·응답 처리, 평가 의견 조합, 중복 방지, 재시도와 복구, 웹 표시를 확인했다. 앞의 오답 사례는 실제 Ollama 실행 결과지만, Jev가 그 오류를 찾아냈다는 실험 결과는 아직 없다. 1편에서 모의 서버와 실제 Coral 응답이 달랐던 일을 겪었으므로, 이번에도 구현 검증과 실제 서비스 검증을 구분해 두었다.
다음에는 정답과 판단 근거를 붙인 한국어 사례로 실제 Jev 평가를 비교하려고 한다. 정상 답변, 의미가 틀린 답변, 형식만 틀린 답변, 정보가 부족한 사례가 모두 필요하다. 잘못 통과시킨 비율과 잘못 거절한 비율, 평가 지연과 비용을 확인한 뒤 자동화할 범위를 정할 생각이다.
첫 글에서는 요청이 끝까지 전달되는지 확인했다. 이번에는 그 답변을 보존하고, 읽어 보고, 평가 기록을 남길 수 있는 구조까지 만들었다. 다음에 확인할 것은 평가자도 내가 발견한 오류를 찾아내는지다.
이 글은 2026년 9월 29일의 로컬 구현과 검증 기록을 기준으로 작성했다. 프로젝트 저장소의 Jev 평가 추가분은 작성 시점에는 아직 push 전이다.
Comments