아키텍처 및 기술 스택
본 가이드는 Spring Boot 환경에서 알리바바 클라우드의 대규모 언어 모델(LLM) 서비스인 DashScope(백련) 에이전트를 연동하는 방법을 다룹니다. 동기식 요청과 Server-Sent Events(SSE)를 활용한 비동기 스트리밍 응답 두 가지 방식을 구현합니다.
- 프레임워크: Spring Boot 3.x, Spring WebFlux
- 언어: Java 17
- AI 라이브러리: Spring AI Alibaba (DashScope Starter)
- 로깅: Log4j2
프로젝트 초기 설정
Spring Initializr를 사용하여 기본 프로젝트를 생성한 후, pom.xml에 필요한 의존성을 추가합니다. 기본 로깅 프레임워크인 Logback을 제외하고 Log4j2를 사용하기 위해 exclusion을 설정합니다.
<dependencies>
<!-- Spring Boot Core -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<!-- Spring AI Alibaba DashScope -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
<version>1.0.0.2</version>
</dependency>
<!-- Utilities -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
환경 변수 및 설정
알리바바 클라우드 콘솔에서 발급받은 API Key와 대시보드에서 생성한 애플리케이션 ID를 application.yml에 매핑합니다. 보안을 위해 실제 운영 환경에서는 환경 변수를 활용하는 것이 좋습니다.
spring:
application:
name: dashscope-integration-service
ai:
dashscope:
api-key: ${DASHSCOPE_API_KEY:your-api-key-here}
agent:
app-id: ${DASHSCOPE_APP_ID:your-app-id-here}
동기식 에이전트 호출 구현
동기식 인터페이스는 사용자의 입력을 받아 DashScope 에이전트에 전달하고, 완전한 응답이 생성될 때까지 대기한 후 결과를 반환합니다. 응답 객체에서 텍스트 콘텐츠뿐만 아니라 참조된 문서(docReferences)와 추론 과정(thoughts)과 같은 메타데이터도 추출하여 로깅합니다.
package com.example.ai.controller;
import com.alibaba.cloud.ai.dashscope.agent.DashScopeAgent;
import com.alibaba.cloud.ai.dashscope.agent.DashScopeAgentOptions;
import com.alibaba.cloud.ai.dashscope.api.DashScopeAgentApi;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/chat")
public class SynchronousChatController {
private static final Logger log = LoggerFactory.getLogger(SynchronousChatController.class);
private final DashScopeAgent dashScopeAgent;
@Value("${spring.ai.dashscope.agent.app-id}")
private String applicationId;
public SynchronousChatController(DashScopeAgentApi agentApi) {
this.dashScopeAgent = new DashScopeAgent(agentApi);
}
@GetMapping("/sync")
public String executeSynchronousCall(
@RequestParam(value = "query", defaultValue = "안녕하세요!") String query) {
DashScopeAgentOptions options = DashScopeAgentOptions.builder()
.withAppId(applicationId)
.build();
ChatResponse chatResponse = dashScopeAgent.call(new Prompt(query, options));
if (chatResponse == null || chatResponse.getResult() == null) {
return "응답을 생성할 수 없습니다.";
}
AssistantMessage assistantMessage = chatResponse.getResult().getOutput();
String finalText = assistantMessage.getText();
// 메타데이터 파싱 및 로깅
Object metadataOutput = assistantMessage.getMetadata().get("output");
if (metadataOutput instanceof DashScopeAgentApi.DashScopeAgentResponse.DashScopeAgentResponseOutput output) {
log.debug("추론 과정: {}", output.thoughts());
log.debug("참조 문서: {}", output.docReferences());
}
log.info("동기식 응답 완료: {}", finalText);
return finalText;
}
}
반응형 스트리밍 에이전트 호출 구현
스트리밍 방식은 Spring WebFlux의 Flux를 사용하여 Server-Sent Events(SSE) 형태로 데이터를 실시간으로 전송합니다. incrementalOutput 옵션을 활성화하여 중복되는 텍스트 없이 새로 생성된 토큰만 순차적으로 클라이언트에 푸시합니다.
package com.example.ai.controller;
import com.alibaba.cloud.ai.dashscope.agent.DashScopeAgent;
import com.alibaba.cloud.ai.dashscope.agent.DashScopeAgentOptions;
import com.alibaba.cloud.ai.dashscope.api.DashScopeAgentApi;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
@RestController
@RequestMapping("/api/v1/chat")
public class StreamingChatController {
private static final Logger log = LoggerFactory.getLogger(StreamingChatController.class);
private final DashScopeAgent dashScopeAgent;
@Value("${spring.ai.dashscope.agent.app-id}")
private String applicationId;
public StreamingChatController(DashScopeAgentApi agentApi) {
DashScopeAgentOptions defaultOptions = DashScopeAgentOptions.builder()
.withSessionId("global-session-id")
.withIncrementalOutput(true)
.withHasThoughts(true)
.build();
this.dashScopeAgent = new DashScopeAgent(agentApi, defaultOptions);
}
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> executeStreamingCall(
@RequestParam(value = "query", defaultValue = "안녕하세요!") String query) {
DashScopeAgentOptions requestOptions = DashScopeAgentOptions.builder()
.withAppId(applicationId)
.build();
return dashScopeAgent.stream(new Prompt(query, requestOptions))
.map(chatResponse -> {
if (chatResponse == null || chatResponse.getResult() == null) {
return "";
}
AssistantMessage message = chatResponse.getResult().getOutput();
String chunk = message.getText();
log.trace("스트리밍 청크 수신: {}", chunk);
return chunk != null ? chunk : "";
})
.filter(chunk -> !chunk.isEmpty());
}
}
애플리케이션 진입점
Spring Boot 애플리케이션을 실행하기 위한 메인 클래스입니다.
package com.example.ai;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class AiIntegrationApplication {
private static final Logger log = LoggerFactory.getLogger(AiIntegrationApplication.class);
public static void main(String[] args) {
SpringApplication.run(AiIntegrationApplication.class, args);
log.info("AI 통합 서비스가 성공적으로 시작되었습니다.");
}
}