Spring AI 학습 1주차

조현희·2026년 2월 26일

기본 구조

ChatModel

  • LLM과 실제로 통신하는 ai 엔진. 모델에 요청 보내고 응답을 받음
  • 모델별로 응답방식이 다르다는 문제가 있음

ChatClient

  • chatModel을 감싸는 래퍼. 스프링 스타일로 쓰기 좋게 만든 것
  • 이걸 사용하는게 좋다고 함
@Configuration
@Slf4j
@RequiredArgsConstructor
public class AiConfig {
    @Bean
    OpenAiChatModel openAiChatModel(@Value("${spring.ai.openai.api-key}") String apiKey) {
        // API + Option > ChatModel
        OpenAiApi api = OpenAiApi.builder().apiKey(apiKey).build();
        OpenAiChatOptions options = OpenAiChatOptions.builder()
        .model("gpt-4o-mini")
        .temperature(0.8)
        .build();
        return OpenAiChatModel.builder().openAiApi(api).defaultOptions(options).build();
    }

    @Bean
    ChatClient openAiChatClient(OpenAiChatModel chatModel) {
        // ChatModel > ChatClient
        return ChatClient.builder(chatModel).build();
    }


    // OllamaChatModel 빈을 생성하는 템플릿 메서드
    private OllamaChatModel getOllamaChatModel(String baseUrl, String model) {
        // 접속 대상 서버 정보
        OllamaApi api = OllamaApi.builder().baseUrl(baseUrl).build();
        OllamaOptions options = OllamaOptions.builder().model(model).temperature(0.7).build();
        return OllamaChatModel.builder().ollamaApi(api).defaultOptions(options).build();
    }

    @Bean
    ChatClient ollamaGemma3nChatClient(@Value("${spring.ai.ollama.base-url}") String baseUrl){
        return ChatClient.builder(getOllamaChatModel(baseUrl, "gemma3n:e4b")).build();
    }

    @Bean
    ChatClient ollamaDeepSeekR1ChatClient(@Value("${spring.ai.ollama.base-url}") String baseUrl) {
        return ChatClient.builder(getOllamaChatModel(baseUrl, "qwen3:8b")).build();
    }
}

기본 설정

  • application.properties를 통해 1개 모델일 때 간단히 사용할 수 있고 보통은 defaultXXX 메서드를 통해 설정
  • defaultSystem(String text), defaultSystem(Consumer systemSpecConsumer), ..기본 시스템 메시지를 정의하는 메서드로 Consumer를 사용하여 람다를 통해 기본 매개변수를 설정할 수 있다.
  • defaultUser(String text), defaultUser(Consumer userSpecConsumer),... 기본 사용자 메시지를 정의하는 메서드로 Consumer를 사용하여 람다를 통해 기본 매개변수를 설정할 수 있다.
  • defaultOptions(ChatOptions chatOptions)ChatOptions에 선언된 portable 가능한 옵션 또는 특정 모델에 특화된 옵션을 전달할 수 있다.
  • defaultAdvisors(Advisor...advisor)요청/응답에 해당하는 메시지를 수정할 수 있는 advisor들을 등록할 수 있다. 예를 들어 로깅을 위해 SimpleLoggerAdvisor를 사용할 수 있다.
  • defaultTools(FunctionCallback callback)AI 모델이 사용할 수 있는 특정 기능을 정의하는 것으로 AI 모델의 정확도를 올리기 위한 Tool을 지정하기 위한 설정이다.
@Value("${quietjun.ai.system-prompt}")
private String systemPrompt;
@Value("${spring.ai.ollama.base-url}")
String baseUrl;

private ChatClient getOllamaChatClient(String name) {
    // baseUrl 설정을 통해 Ollama 서비스와 연결
    OllamaApi api = OllamaApi.builder().baseUrl(baseUrl).build();
    // OllamaApi와 연결된 모델 생성
    ChatModel model = OllamaChatModel.builder().ollamaApi(api).build();
    // 모델을 통해서 생성되는 ChatClient에 기본 속성 적용
    return ChatClient.builder(model)
            defaultSystem(c -> c.text(systemPrompt)
                        .param("language", "korean").param("character", "chill"))
            .defaultOptions(OllamaOptions.builder().model(name).temperature(0.1).build())
            .defaultAdvisors(new SimpleLoggerAdvisor()).build();
}

@Bean
ChatClient ollamaGemma3ChatClient() {
    return getOllamaChatClient("gemma3:4b-it-qat");
}

프롬프트

  • 모델에 보내는 질문
  • 길어지면 토큰 초과 문제
  • 메시지: 프롬프트를 구성하는 단위
메시지 타입설명
SystemMessage (System Role)대화를 시작하기 전에 AI에게 지침을 제공하는 역할. AI의 행동, 응답 스타일, 규칙 등을 설정함. 모델이 입력을 해석하고 응답하는 방식에 대한 기준을 정의함.
예) “친절하게 단계별로 질문에 답변하고 표 형식으로 결과를 반환해줘.”
UserMessage (User Role)사용자가 AI에게 전달하는 질문 또는 요청. 모델이 작업을 수행하거나 응답을 생성하기 위한 핵심 입력 정보.
예) “오늘 날씨 어때?”, “한강 작가의 도서 목록을 알려줘.”
AssistantMessage (Assistant Role)사용자의 입력에 대한 AI의 응답. 이 응답은 이후 대화의 맥락(context)으로 다시 활용될 수 있음. Tool 호출 요청 정보도 포함될 수 있음.
ToolResponseMessage (Tool Role)Tool 호출 결과로 생성되는 메시지. 외부 API나 함수 실행 결과를 포함하며, 이후 모델이 응답을 생성하는 데 추가 컨텍스트로 사용됨.
  • 코드
# application.properties
# system prompt
quietjun.ai.system-prompt=You are an artificial intelligence known as an omniscient scholar.
When you speak, use {language} and answer with a {character} personality.



public class Prompt implements ModelRequest<List<Message>> {
    private final List<Message> messages;
    private ChatOptions chatOptions;
}

call()과 stream()

  • call(): 한번에 전체 응답 받기. 응답이 길면 느리다
  • stream(): 실시간으로 받기(토큰단위). 중간에 끊기면 처리 힘듬

chatOptions

  • temperature, maxTokens 등 설정
  • temperature 높으면 창의적이지만 헛소리
  • maxTokens 작으면 답변 끊김

프롬프트 엔지니어링

zero-shot

  • 예시 없이 바로 질문하는 것. 모호하면 엉뚱한 결과를 줌

few-shot

  • 약간의 예시를 주는 것. 토큰을 많이 먹음

role-prompting

  • 역할을 부여하는 것. 역할이 과도하면 왜곡됨

Chain-of-Thought (CoT)

  • 생각 과정을 단계별로 설명하라고 하는 것
  • 응답 길어지므로 토큰 증가

PromptTemplate

  • 동적으로 값 넣는 템플릿. 보안 위험
    ex) {name}의 나이는 {age}

구조화된 응답 처리하기

StructuredOutputConverter

  • ai 응답을 특정 형식으로 변환. 모델이 형식을 어기면 파싱 실패

BeanOutputConverter

  • 응답을 자바 객체로 바로 매핑. 필드 누락이나 타입 불일치시 예외발생

BeanOutputConverter

  • 이 형식으로 출력하라고 모델에게 명시적으로 알리기. 모델이 무시할 수 있음

전체 흐름

  • prompt 입력 -> chatClient, chatModel 호출 -> call 또는 stream -> StructuredOutputConverter 로 객체 변환

참고 https://github.com/camp-robbie/spring-ai-study

https://docs.spring.io/spring-ai/reference/2.0/api/chatclient.html 까지 읽었다!

profile
도전

0개의 댓글