전체 글로 돌아가기

[해결] 사진 분석 AI 결과를 100점 만점 점수로 변환하는 구조

Gemini API가 반환하는 무작위 텍스트 대신 responseJsonSchema로 관찰값을 구조화하고 Lambda 가중치 알고리즘으로 신뢰성 있는 점수를 생성한 방법입니다.

fruit.nxnl.appGoodFruit
GeminiAPIJSONSchemaAWSLambda

[해결] 사진 분석 AI 결과를 100점 만점 점수로 변환하는 구조

한 줄 요약: Gemini API가 반환하는 무작위 텍스트 대신 responseJsonSchema로 6가지 외관 관찰값과 세부 점수를 구조화하고, 서버(AWS Lambda)에서 가중치 계산 알고리즘을 통해 100점 만점 신뢰성 있는 총점과 별점으로 변환하도록 구현했습니다.

1. 어떤 문제가 발생했나

LLM(대형 언어 모델) 기반 AI 비전 모델을 서비스에 적용할 때 가장 큰 문제는 응답의 일관성과 객관적인 수치화였습니다.

AI에게 "이 사과 사진의 점수를 100점 만점으로 매겨줘"라고 자유 형태(Free-form text)로 요청하면 다음과 같은 심각한 문제가 발생했습니다.

  • **점수 왜곡 및 일관성 부재**: 똑같은 사진을 올려도 매번 75점, 90점, 60점으로 점수가 무작위로 널뛰기를 합니다.
  • **형식 파싱 실패**: JSON 형식을 요청해도 텍스트 설명이나 마크다운 태그, 불필요한 서문이 섞여 들어와 서버 파싱 오류가 다발했습니다.
  • **과일이 아닌 사진 처리 미흡**: 과자가 찍힌 사진에도 사과라고 우기며 80점을 부여하는 등 예외 처리가 불가능했습니다.

2. 기존에는 어떻게 구현했나

초기 시도에서는 프롬프트에 "반드시 JSON 형식으로 score: 85, reason: '...' 형태로 답변하세요"라는 문구를 넣고 생성된 문장 전체를 Regex로 파싱하려 했습니다.

// 기존 시도 (문장 생성 방식 - 실패 패턴)
const prompt = "이 과일 사진을 보고 100점 만점 점수와 이유를 JSON으로 줘. 예: {\"score\": 80}";
// 결과: "네, 분석 결과입니다: ```json {"score": 80} ```" -> JSON.parse 오류 발생

3. 원인은 무엇이었나

  • **LLM의 생성적 특성**: AI는 본질적으로 다음에 올 단어를 확률적으로 예측하므로, 엄격한 데이터 타입 검증이나 수학적 가중치 산술 합산을 AI 내부 문장 생성 단계에 온전히 위임하는 것은 불안정합니다.
  • **역할의 혼선**: AI에게 '사진 객관적 관찰'과 '수학적 점수 계산' 두 가지 역할을 모두 맡겨버렸기 때문입니다.

4. 실패했던 방법

  • **프롬프트 강조 강화**: "절대 딴소리 하지 마" 문구를 추가했으나 Gemini 모델 업데이트 시 파싱 실패율이 여전히 5~10% 발생.
  • **클라이언트 직투과**: 프런트엔드에서 Gemini API를 직접 호출하고 점수를 받도록 구현했으나 API Key 유출 위험 및 점수 검증 로직 노출 문제 발생.

5. 최종 해결 방법

AI에게는 '사진 속 시각적 사실 관찰 및 항목별 0~100 평가'만 맡기고, '최종 점수 계산 및 유효성 검증'은 AWS Lambda 서버에서 엄격하게 처리하도록 역할을 분리했습니다.

  • **Structured Outputs (Strict JSON Schema Enforcement)**: Gemini API 호출 시 `responseJsonSchema` 옵션을 지정하여 반환 타입을 코드로 강제합니다.
  • **6가지 핵심 외관 관찰 항목 규격화**: `color`(색상), `shape`(모양), `surface`(표면/윤기), `damage`(상처), `stem`(꼭지), `ripeness`(숙도) 항목별로 0~100점 점수와 reason을 추출합니다.
  • **가중치 기반 점수 산출 (Weighted Scoring Algorithm)**: 과일 선택에 결정적인 항목에 가중치(Color 20%, Surface 24%, Damage 24% 등)를 적용하여 서버에서 총점을 조합합니다.

4. Playful Mode (재미용 닮은 과일 비교): 과일이 아닌 대상(not_fruit)이 감지되면 가장 닮은 지원 과일과의 시각적 유사도 점수로 유쾌하게 전환합니다.

6. 수정 전후 코드

`infra/analyze-api/index.mjs` (백엔드 Lambda 적용 코드)

// 1. 6가지 관찰 항목별 가중치 정의
const CHECK_WEIGHT = {
  color: 0.20,
  shape: 0.12,
  surface: 0.24,
  damage: 0.24,
  stem: 0.08,
  ripeness: 0.12,
};

// 2. Gemini API Strict JSON Schema 정의
const responseSchema = {
  type: "object",
  properties: {
    status: {
      type: "string",
      enum: ["ok", "not_fruit", "multiple_fruits", "low_quality", "unsupported"],
    },
    fruitName: { type: "string" },
    matchedFruitName: { type: "string" },
    detectedSubject: { type: "string" },
    confidence: { type: "number", minimum: 0, maximum: 1 },
    checks: {
      type: "object",
      properties: {
        color: checkSchema,
        shape: checkSchema,
        surface: checkSchema,
        damage: checkSchema,
        stem: checkSchema,
        ripeness: checkSchema,
      },
      required: ["color", "shape", "surface", "damage", "stem", "ripeness"],
    },
  },
  required: ["status", "fruitName", "matchedFruitName", "detectedSubject", "confidence", "checks"],
};

// 3. 서버 단 가중치 점수 합산 로직
function calculateScore(checks, weights = CHECK_WEIGHT) {
  let weightedScore = 0;
  let totalWeight = 0;
  for (const [key, weight] of Object.entries(weights)) {
    const check = checks[key];
    if (check && check.status !== "unknown" && check.status !== "not_applicable") {
      weightedScore += check.score * weight;
      totalWeight += weight;
    }
  }
  if (totalWeight === 0) return 0;
  return Math.round(weightedScore / totalWeight);
}

7. 실제 테스트 결과

  • **JSON 파싱 오류율**: 8.4% -> **0.0%** (Gemini `responseJsonSchema` 적용 후 파싱 에러 완전 제거)
  • **점수 신뢰성**: 동일한 사과 사진 10회 연속 검측 시 총점 오차 범위 **±2점 이내**로 대폭 향상.
  • **비과일 사진 핸들링**: 강아지 사진 업로드 시 `not_fruit` 감지 후 "가장 닮은 과일: 복숭아 (유사도 72점)"로 안정적 전환 성공.

8. 다른 환경에서 주의할 점

  • `responseJsonSchema` 적용 시 모델 버전 호환성을 확인해야 합니다. (Gemini 2.5 Flash / 3.1 Flash 계열 지원)
  • API Key는 절대로 소스코드나 Lambda 환경변수에 직접 평문으로 노출하지 말고 **AWS Secrets Manager**에서 동적으로 인출해야 보안 유지가 가능합니다.

9. 정리

AI를 활용할 때 AI에게 모든 결정권(마지막 결론 및 수학 산술)을 넘기지 않고, AI는 관찰자로서 구조화된 데이터만 뽑아내게 하고 최종 산출은 백엔드 비즈니스 로직에서 제어하는 구조가 훨씬 안정적이고 검증 가능한 서비스를 만듭니다.

10. 관련 글

  • [사진 한 장으로 과일 맛을 판정하는 서비스를 만든 이유](file:///Users/uram/dev/goodfruit/posts/01-why-built-goodfruit.md)
  • [회원가입 없이 결과를 다시 볼 수 있게 만든 방법](file:///Users/uram/dev/goodfruit/posts/03-zero-auth-sharing-structure.md)
  • [GA4 커스텀 이벤트로 분석 완료율 측정하기](file:///Users/uram/dev/goodfruit/posts/07-ga4-custom-events-completion-rate.md)

실제 적용 서비스

GoodFruit의 신뢰성 높은 과일 맛 분석 알고리즘을 지금 직접 체험해보세요.

👉 [GoodFruit에서 내 과일 점수 확인하기](https://fruit.nxnl.app/?utm_source=tistory&utm_medium=content&utm_campaign=goodfruit&utm_content=02_ai_observation_to_score)

내부 원문 복사