Post

03 Spring AI 사용

03 Spring AI 사용

1. Note

  • 메시지를 보내는 방식
    • 실제 웹 또는 AP에서 문자로 보내는 방식은 동일
    • 백엔드에서 하는것은 이것을 다듬어서 어떻게 최소한의 토큰으로 프롬프트를 전달하는가가 중요
    • 내용이 길어질수록 토큰이 비싸지고, 비용이 많이 발생함.
  • 프롬프트 엔지니어링에서 내용을 메소드로 분류를 하는 느낌

2. Spring 설정

1. build.gralde

1
2
3
4
5
6
7
8
9
10
11
12
13
  # 버전은 지속적으로 바뀌는 상황이라 확인은 필요.
  # dependencyManagement에 bom등록과 의존성 필요 
  dependencyManagement {
      imports {
          mavenBom "org.springframework.ai:spring-ai-bom:1.0.0-M6"
      }
  }
  
  dependencies {
      // Google AI Gemini 스타터 추가
      // spring AI 1.1 이상에서는 OpenAI뿐만 아니라 Gemini, Claude 등 다른 LLM도 동일한 API 구조로 연결 가능
      implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter'
  }

2. application.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 spring:
  ai:
    openai:
      # 1. API 키: Google AI Studio에서 발급받은 키를 입력합니다.
      # ${GOOGLE_AI_GEMINI_API_KEY}
      # 키값은 AP에서 
      api-key: GOOGLE_AI_GEMINI_API_KEY 
      chat:
        # 2. Base URL: Google Gemini가 제공하는 OpenAI 호환 API 주소입니다.
        base-url: "https://generativelanguage.googleapis.com/v1beta/openai/"
        options:
          # 모델명: 현재 가장 최신/경량 모델인 gemini-2.0-flash-lite 등을 지정합니다.
          model: "gemini-2.5-flash-lite"
          # 온전성(Temperature): 0.0은 가장 보수적이고 사실적인 답변을 생성합니다.
          # (분석, 요약, 데이터 추출에 최적화된 설정)
          temperature: 0.7
          # 최대 토큰 수: AI가 생성할 답변의 최대 길이를 제한합니다.
          max-tokens: 4096
        # 4. 엔드포인트 경로: 대화형 API를 호출하기 위한 표준 경로입니다.
        completions-path: "/chat/completions"

3. 기본 메시지 형태

1
2
3
4
5
6
7
  @GetMapping("/type1")
  public String generateMarketing() {  
    return chatClient.prompt()   // 프롬프트 생성 시작
        .user("메시지")          // 사용자 입력 설정
        .call()                  // 실제 LLM 호출
        .content();              // 결과 텍스트 추출
  } 

3. 메시지 구분

1. System / User / Assistant

구분SystemUserAssistant
역할AI의 행동/규칙 정의사용자 요청AI의 응답을 기록
의미“어떻게 답할지” 결정“무엇을 물어보는지”“이전에 어떻게 답했는지”
우선순위가장 높음중간낮음
사용 목적톤, 스타일, 정책 설정질문, 명령대화 흐름 유지
사용 시점보통 최초 1회 또는 고정매 요청마다히스토리 포함 시
영향 범위전체 응답에 지속 영향해당 요청다음 응답에 간접 영향
없을 경우기본 모델 성향 사용요청 자체가 없음단일 응답 구조

2. 메소드 구분

1. 편의성

1
2
3
  chatClient.prompt()
    .system("너는 백엔드 전문가야")
    .user("JPA 설명해줘")

2. 편의성메소드의 실제 모습

1
2
3
4
5
  chatClient.prompt()
    .messages(
        new SystemMessage("너는 백엔드 전문가야"),
        new UserMessage("JPA 설명해줘")
    )

4. AI 메시지 전송 관련 설정(System / User)

1. message 템플릿

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  @GetMapping("/marketing")
  public String generateMarketing(
      @RequestParam(value = "productName") String productName,
      @RequestParam(value = "features") String features) {

    // 메시지 템플릿을 주게되면
    String template = """
        제품명 {productName}의 마케팅 문구를 작성하세요.
        주요 특징: {features}
        조건: 감성적이고 100자 이내로 작성할 것.
        """;

    return chatClient.prompt()
        .user(u -> u.text(template) // 프롬프트에 규칙 또는 정해진 메시지 형태를 줄 수 있음.
            .param("productName", productName)
            .param("features", features))
        .call()
        .content();
  }

2. 역할 제공

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
   @GetMapping("/translate")
  public String translate(
      @RequestParam(value = "text") String text,
      @RequestParam(value = "targetLanguage", defaultValue = "영어") String targetLanguage) {

    return chatClient.prompt()
        // 1. AI의 페르소나 설정 (System Message)
        .system("당신은 전문 번역가입니다. 주어진 텍스트를 문맥에 맞게 자연스럽게 번역해주세요.")

        // 2. 동적 파라미터 주입 (Prompt Template)
        .user(u -> u.text("다음 텍스트를 {lang}로 번역해주세요: {text}")
            .param("lang", targetLanguage)
            .param("text", text))
        .call()
        .content();
  } 

5. Assistant (이전 히스토리를 담기)

1. 대화 방식

  • AI내부에 기존의 대화를 기억하는 방법은 없고, 직접 대화를 넣어줘야만 기억을 할 수 있는 패턴이 됨
  • 대화의 흐름
    1
    2
    3
    
    1회차 요청: `[System]` + `[User 1]` → 응답: `[Assistant 1]`
    2회차 요청: `[System]` + `[User 1]` + `[Assistant 1]` + `[User 2]` → 응답: `[Assistant 2]`
    3회차 요청: `[System]` + `[User 1]` + `[Assistant 1]` + `[User 2]` + `[Assistant 2]` + `[User 3]` → 응답: `[Assistant 3]`
    

2. 소스패턴

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  public class ChatService {
  
      private final ChatModel chatModel;
  
      public void startConversation() {
          // 1. AI의 정체성 설정 (System)
          Message systemMessage = new SystemMessage("당신은 요리 전문가입니다.");
  
          // 2. 사용자의 첫 번째 질문 (User)
          Message userMessage1 = new UserMessage("김치찌개 맛있게 만드는 비법이 뭐야?");
  
          // 3. AI의 이전 응답 (Assistant) - 이 내용이 있어야 AI가 과거 답변을 기억함
          Message assistantMessage = new AssistantMessage("김치찌개의 비법은 충분히 볶은 김치와 쌀뜨물입니다.");
  
          // 4. 사용자의 두 번째 질문 (User) - 맥락이 필요한 질문
          Message userMessage2 = new UserMessage("그럼 된장찌개는?");
  
          // 5. 전체 메시지를 리스트로 묶어서 전송
          // AI는 assistantMessage를 보고 "아, 김치찌개 비법을 알려줬으니 이번엔 된장찌개 비법을 묻는구나"라고 이해합니다.
          List<Message> history = List.of(
              systemMessage, 
              userMessage1, 
              assistantMessage, 
              userMessage2
          );
  
          chatModel.call(new Prompt(history));
      }
  }

6. Entity 파싱

1. Response 파싱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 @Getter
 @NoArgsConstructor // JSON 역직렬화를 위해 기본 생성자 필수
 @FieldDefaults(level = AccessLevel.PRIVATE)
 public class ProductAnalysisResponse {

    @JsonProperty("sentiment")
    String sentiment;

    @JsonProperty("score")
    int score;

    @JsonProperty("summary")
    String summary;
 }

2. Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
   @GetMapping("/analyze")
   public ProductAnalysisResponse analyzeReview(@RequestParam(value = "review") String review) {

    String promptText = """
        다음 제품 리뷰를 분석해주세요:
        
        리뷰 내용: {review}
        
        요구사항:
        1. sentiment는 positive, neutral, negative 중 하나로 응답하세요.
        2. score는 1점에서 10점 사이의 정수로 응답하세요.
        3. summary는 분석 내용을 한 문장으로 요약하세요.
        """;

    return chatClient.prompt()
        .user(u -> u.text(promptText).param("review", review))
        .call()
        .entity(ProductAnalysisResponse.class); // 객체로 자동 변환
  } 

3. 응답

1
2
3
4
5
 {
  "sentiment": "negative",
  "score": 1,
  "summary": "제품에서 벌레가 나와 섭취하지 못했다는 부정적인 리뷰입니다."
 }

7. 옵션방식

1. 옵션 패턴

1
2
3
4
5
6
7
8
9
10
11
  @GetMapping("/story")
  public String generateStory(@RequestParam(value = "topic") String topic) {
    return chatClient.prompt()
        .user("다음 주제로 창의적인 이야기를 작성해주세요: " + topic)
        .options(ChatOptions.builder()
            .temperature(0.9)  // 1.0에 가까울수록 창의적(랜덤성 증가)
            .maxTokens(500)    // 답변의 최대 길이 제한
            .build())
        .call()
        .content();
  }

2. 활용 가능한 메소드(주요)

파라미터정의영향 대상값 범위효과언제 사용
temperature출력 랜덤성(창의성) 조절단어 선택 확률0.0 ~ 1.0+낮으면 일관성 ↑ / 높으면 다양성 ↑창의 vs 정확 조절
topP확률 상위 비율 제한단어 후보군0.0 ~ 1.0낮으면 안전 / 높으면 다양자연스러운 생성
maxTokens최대 출력 길이 제한응답 길이정수길면 잘림 / 짧으면 빠름비용/응답 길이 제어
frequencyPenalty반복 단어 억제동일 단어0.0 ~ 2.0반복 감소리스트/중복 방지
presencePenalty새로운 단어 유도단어 다양성0.0 ~ 2.0새로운 내용 증가창의적 생성
This post is licensed under CC BY 4.0 by the author.