RuoYi(Spring Boot & Vue.js) 프레임워크 환경에서 Aliyun의 대규모 언어 모델인 통의천문(Tongyi Qianwen)을 통합하는 방법을 설명합니다. 단순히 API를 호출하는 것을 넘어, 대화 내용을 데이터베이스에 저장하고 SSE(Server-Sent Events) 방식을 통해 실시간 스트리밍 답변을 프런트엔드에 전달하는 완성도 높은 챗봇 시스템 구축 과정을 다룹니다.
1. 데이터베이스 스키마 설계
먼저 대화 이력을 관리하기 위한 테이블을 생성합니다. 사용자 메시지와 AI 응답을 구분하고, 첨부 파일 경로 및 생성 시간을 저장합니다.
CREATE TABLE `ai_chat_log` (
`chat_id` varchar(32) NOT NULL COMMENT 'PK',
`user_id` varchar(30) DEFAULT NULL COMMENT '사용자 ID',
`attachment_url` varchar(500) DEFAULT NULL COMMENT '파일 경로',
`is_bot_reply` char(1) DEFAULT '0' COMMENT 'AI 응답 여부 (0:사용자, 1:AI)',
`message_content` text COMMENT '대화 내용',
`delete_status` char(1) DEFAULT '0' COMMENT '삭제 여부 (0:존재, 2:삭제)',
`creator_name` varchar(64) DEFAULT '' COMMENT '생성자',
`created_at` datetime DEFAULT NULL COMMENT '생성 시간',
`updated_at` datetime DEFAULT NULL COMMENT '수정 시간',
`notes` varchar(500) DEFAULT NULL COMMENT '비고',
PRIMARY KEY (`chat_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI 채팅 이력 테이블';
2. 백엔드 설정 및 의존성 주입
통의천문 SDK와 유틸리티 도구를 위해 pom.xml에 다음 의존성을 추가합니다.
<!-- 통의천문 SDK -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dashscope-sdk-java</artifactId>
<version>2.18.3</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Hutool 유틸리티 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.25</version>
</dependency>
application.yml 파일에 Aliyun에서 발급받은 API 정보를 설정합니다.
ai:
tongyi:
app-id: YOUR_APP_ID
api-key: YOUR_API_KEY
3. AI 통신 및 데이터 처리 로직
AI 모델과의 인터페이스를 담당할 유틸리티 클래스를 작성합니다. 일반 호출과 스트리밍 호출(SSE)을 모두 지원하도록 구성합니다.
@Component
public class AiModelProvider {
@Value("${ai.tongyi.app-id}")
private String appId;
@Value("${ai.tongyi.api-key}")
private String apiKey;
public Flowable<ApplicationResult> streamResponse(String prompt) throws NoApiKeyException, InputRequiredException {
ApplicationParam param = ApplicationParam.builder()
.apiKey(apiKey)
.appId(appId)
.prompt(prompt)
.incrementalOutput(true)
.build();
Application application = new Application();
return application.streamCall(param);
}
}
Service 계층에서는 AI 응답 데이터를 가공하고 DB에 저장하는 역할을 수행합니다. reactor-core의 Flux를 활용하여 스트림 데이터를 프런트엔드로 반환합니다.
@Service
public class ChatLogServiceImpl extends ServiceImpl<ChatLogMapper, ChatLog> implements ChatLogService {
@Autowired
private AiModelProvider aiProvider;
@Override
public Flux<String> processStreamingChat(ChatLog userMsg) throws Exception {
// 1. 사용자 메시지 선저장
this.save(userMsg);
// 2. AI 스트리밍 호출
Flowable<ApplicationResult> responseStream = aiProvider.streamResponse(userMsg.getMessageContent());
return Flux.from(responseStream)
.map(result -> result.getOutput().getText())
.publishOn(Schedulers.boundedElastic());
}
}
4. 컨트롤러 구현
SSE 방식을 사용하기 위해 MediaType.TEXT_EVENT_STREAM_VALUE를 반환 타입으로 지정합니다.
@RestController
@RequestMapping("/api/ai/chat")
public class AiChatController extends BaseController {
@Autowired
private ChatLogService chatLogService;
@PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamChat(@RequestBody ChatLog chatLog) {
try {
chatLog.setUserId(getUsername());
chatLog.setIsBotReply("0");
return chatLogService.processStreamingChat(chatLog);
} catch (Exception e) {
return Flux.just("Error: AI 응답 생성 중 오류가 발생했습니다.");
}
}
}
5. 프런트엔드 스트리밍 처리
Vue.js 환경에서 브라우저의 fetch API를 사용하여 스트림 데이터를 실시간으로 읽어와 UI를 갱신합니다.
async function fetchAiResponse() {
const response = await fetch('/dev-api/api/ai/chat/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + getToken()
},
body: JSON.stringify({ messageContent: userInput.value })
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
// 정규식을 활용하여 텍스트 정제 및 마크다운 스타일 변환
processTextChunk(chunk);
}
}
function processTextChunk(text) {
// 줄바꿈 및 강조 처리 예시
let formatted = text.replace(/\n/g, "<br>")
.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
currentChatDisplay.value += formatted;
scrollToBottom();
}
6. 주요 구현 포인트
- 정규식 데이터 정제: 스트리밍으로 들어오는 원시 텍스트 데이터를 사용자에게 보기 좋게 표시하기 위해 프런트엔드에서 실시간으로 HTML 태그(br, strong, li 등)로 변환하는 로직이 중요합니다.
- 동기화 문제 해결: AI의 답변이 완료된 후 최종 결과물을 DB에 저장할 때, 프런트엔드에서 수집된 전체 텍스트를 백엔드로 재전송하여 저장하거나 백엔드 Flux 파이프라인 마지막에서 저장 로직을 수행합니다.
- UI/UX 편의성: 대화창이 길어질 경우 자동으로 하단 스크롤이 내려가도록
nextTick과scrollTop을 제어합니다. 또한, 전체 화면 전환 및 파일 업로드 UI를 제공하여 확장성을 확보합니다.