# AI 서비스 개발 가이드 ## 📋 **목차** 1. [AI 제안 기능 개발](#1-ai-제안-기능) 2. [용어 사전 기능 개발](#2-용어-사전-기능) 3. [관련 회의록 추천 기능 개발](#3-관련-회의록-추천-기능) 4. [백엔드 API 검증](#4-백엔드-api-검증) 5. [프롬프트 엔지니어링 가이드](#5-프롬프트-엔지니어링) --- ## 1. AI 제안 기능 ### 📌 **기능 개요** - **목적**: STT로 5초마다 수신되는 회의 텍스트를 분석하여 실시간 제안사항 제공 - **입력**: Redis에 축적된 최근 5분간의 회의 텍스트 - **출력**: - 논의사항 (Discussions): 회의와 관련있고 추가 논의가 필요한 주제 - 결정사항 (Decisions): 명확한 의사결정 패턴이 감지된 내용 ### ✅ **구현 완료 사항** 1. **SSE 스트리밍 엔드포인트** - 파일: `ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/SuggestionController.java:111-131` - 엔드포인트: `GET /api/suggestions/meetings/{meetingId}/stream` - 프로토콜: Server-Sent Events (SSE) 2. **실시간 텍스트 처리** - 파일: `ai/src/main/java/com/unicorn/hgzero/ai/biz/service/SuggestionService.java:120-140` - Event Hub에서 TranscriptSegmentReady 이벤트 수신 - Redis에 최근 5분 텍스트 슬라이딩 윈도우 저장 3. **Claude API 통합** - 파일: `ai/src/main/java/com/unicorn/hgzero/ai/infra/client/ClaudeApiClient.java` - 모델: claude-3-5-sonnet-20241022 - 비동기 분석 및 JSON 파싱 구현 ### 🔧 **개선 필요 사항** #### 1.1 프롬프트 엔지니어링 개선 **현재 문제점**: - 회의와 관련 없는 일상 대화도 분석될 가능성 - 회의 목적/안건 정보가 활용되지 않음 **개선 방법**: ```java // ClaudeApiClient.java의 analyzeSuggestions 메서드 개선 public Mono analyzeSuggestions( String transcriptText, String meetingPurpose, // 추가 List agendaItems // 추가 ) { String systemPrompt = """ 당신은 비즈니스 회의록 작성 전문 AI 어시스턴트입니다. **역할**: 실시간 회의 텍스트를 분석하여 업무 관련 핵심 내용만 추출 **분석 기준**: 1. 논의사항 (discussions): - 회의 안건과 관련된 미결 주제 - 추가 검토가 필요한 업무 항목 - "~에 대해 논의 필요", "~검토해야 함" 등의 패턴 - **제외**: 잡담, 농담, 인사말, 회의와 무관한 대화 2. 결정사항 (decisions): - 명확한 의사결정 표현 ("~하기로 함", "~로 결정", "~로 합의") - 구체적인 Action Item - 책임자와 일정이 언급된 항목 **필터링 규칙**: - 회의 목적/안건과 무관한 내용 제외 - 단순 질의응답이나 확인 대화 제외 - 업무 맥락이 명확한 내용만 추출 **응답 형식**: 반드시 JSON만 반환 { "discussions": [ { "topic": "구체적인 논의 주제 (회의 안건과 직접 연관)", "reason": "회의 안건과의 연관성 설명", "priority": "HIGH|MEDIUM|LOW", "relatedAgenda": "관련 안건" } ], "decisions": [ { "content": "결정된 내용", "confidence": 0.9, "extractedFrom": "원문 인용", "actionOwner": "담당자 (언급된 경우)", "deadline": "일정 (언급된 경우)" } ] } """; String userPrompt = String.format(""" **회의 정보**: - 목적: %s - 안건: %s **회의 내용 (최근 5분)**: %s 위 회의 목적과 안건에 맞춰 **회의와 관련된 내용만** 분석해주세요. """, meetingPurpose, String.join(", ", agendaItems), transcriptText ); // 나머지 코드 동일 } ``` #### 1.2 회의 컨텍스트 조회 기능 추가 **구현 위치**: `SuggestionService.java` ```java @RequiredArgsConstructor public class SuggestionService implements SuggestionUseCase { private final MeetingGateway meetingGateway; // 추가 필요 private void analyzeAndEmitSuggestions(String meetingId) { // 1. 회의 정보 조회 MeetingInfo meetingInfo = meetingGateway.getMeetingInfo(meetingId); String meetingPurpose = meetingInfo.getPurpose(); List agendaItems = meetingInfo.getAgendaItems(); // 2. Redis에서 최근 5분 텍스트 조회 String key = "meeting:" + meetingId + ":transcript"; Set recentTexts = redisTemplate.opsForZSet().reverseRange(key, 0, -1); if (recentTexts == null || recentTexts.isEmpty()) { return; } String accumulatedText = recentTexts.stream() .map(entry -> entry.split(":", 2)[1]) .collect(Collectors.joining("\n")); // 3. Claude API 분석 (회의 컨텍스트 포함) claudeApiClient.analyzeSuggestions( accumulatedText, meetingPurpose, // 추가 agendaItems // 추가 ) .subscribe( suggestions -> { Sinks.Many sink = meetingSinks.get(meetingId); if (sink != null) { sink.tryEmitNext(suggestions); log.info("AI 제안사항 발행 완료 - meetingId: {}, 논의사항: {}, 결정사항: {}", meetingId, suggestions.getDiscussionTopics().size(), suggestions.getDecisions().size()); } }, error -> log.error("Claude API 분석 실패 - meetingId: {}", meetingId, error) ); } } ``` **필요한 Gateway 인터페이스**: ```java package com.unicorn.hgzero.ai.biz.gateway; public interface MeetingGateway { MeetingInfo getMeetingInfo(String meetingId); } @Data @Builder public class MeetingInfo { private String meetingId; private String purpose; private List agendaItems; } ``` --- ## 2. 용어 사전 기능 ### 📌 **기능 개요** - **목적**: 회의 중 언급된 전문 용어를 맥락에 맞게 설명 - **입력**: 용어, 회의 컨텍스트 - **출력**: - 기본 정의 - 회의 맥락에 맞는 설명 - 이전 회의에서의 사용 예시 (있는 경우) ### ✅ **구현 완료 사항** 1. **용어 감지 API** - 파일: `ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/TermController.java:35-82` - 엔드포인트: `POST /api/terms/detect` 2. **용어 설명 서비스 (Mock)** - 파일: `ai/src/main/java/com/unicorn/hgzero/ai/biz/service/TermExplanationService.java:24-53` ### 🔧 **개선 필요 사항** #### 2.1 RAG 기반 용어 설명 구현 **구현 방법**: ```java @Service @RequiredArgsConstructor public class TermExplanationService implements TermExplanationUseCase { private final SearchGateway searchGateway; private final ClaudeApiClient claudeApiClient; // 추가 @Override public TermExplanationResult explainTerm( String term, String meetingId, String context ) { log.info("용어 설명 생성 - term: {}, meetingId: {}", term, meetingId); // 1. RAG 검색: 이전 회의록에서 해당 용어 사용 사례 검색 List pastUsages = searchGateway.searchTermUsages( term, meetingId ); // 2. Claude API로 맥락 기반 설명 생성 String explanation = generateContextualExplanation( term, context, pastUsages ); return TermExplanationResult.builder() .term(term) .definition(explanation) .context(context) .pastUsages(pastUsages.stream() .map(TermUsageExample::getDescription) .collect(Collectors.toList())) .build(); } private String generateContextualExplanation( String term, String context, List pastUsages ) { String prompt = String.format(""" 다음 용어를 회의 맥락에 맞게 설명해주세요: **용어**: %s **현재 회의 맥락**: %s **이전 회의에서의 사용 사례**: %s **설명 형식**: 1. 기본 정의 (1-2문장) 2. 현재 회의 맥락에서의 의미 (1-2문장) 3. 이전 논의 참고사항 (있는 경우) 비즈니스 맥락에 맞는 실용적인 설명을 제공하세요. """, term, context, formatPastUsages(pastUsages) ); // Claude API 호출 (동기 방식) return claudeApiClient.generateExplanation(prompt) .block(); // 또는 비동기 처리 } private String formatPastUsages(List pastUsages) { if (pastUsages.isEmpty()) { return "이전 회의에서 언급된 적 없음"; } return pastUsages.stream() .map(usage -> String.format( "- [%s] %s: %s", usage.getMeetingDate(), usage.getMeetingTitle(), usage.getDescription() )) .collect(Collectors.joining("\n")); } } ``` #### 2.2 Azure AI Search 통합 (RAG) **SearchGateway 구현**: ```java package com.unicorn.hgzero.ai.infra.search; @Service @RequiredArgsConstructor public class AzureAiSearchGateway implements SearchGateway { @Value("${external.ai-search.endpoint}") private String endpoint; @Value("${external.ai-search.api-key}") private String apiKey; @Value("${external.ai-search.index-name}") private String indexName; private final WebClient webClient; @Override public List searchTermUsages(String term, String meetingId) { // 1. 벡터 검색 쿼리 생성 Map searchRequest = Map.of( "search", term, "filter", String.format("meetingId ne '%s'", meetingId), // 현재 회의 제외 "top", 5, "select", "meetingId,meetingTitle,meetingDate,content,score" ); // 2. Azure AI Search API 호출 String response = webClient.post() .uri(endpoint + "/indexes/" + indexName + "/docs/search?api-version=2023-11-01") .header("api-key", apiKey) .header("Content-Type", "application/json") .bodyValue(searchRequest) .retrieve() .bodyToMono(String.class) .block(); // 3. 응답 파싱 return parseSearchResults(response); } private List parseSearchResults(String response) { // JSON 파싱 로직 // Azure AI Search 응답 형식에 맞춰 파싱 return List.of(); // 구현 필요 } } ``` --- ## 3. 관련 회의록 추천 기능 ### 📌 **기능 개요** - **목적**: 현재 회의와 관련된 과거 회의록 추천 - **입력**: 회의 목적, 안건, 진행 중인 회의 내용 - **출력**: 관련도 점수와 함께 관련 회의록 목록 ### ✅ **구현 완료 사항** 1. **관련 회의록 조회 API** - 파일: `ai/src/main/java/com/unicorn/hgzero/ai/infra/controller/RelationController.java:31-63` - 엔드포인트: `GET /api/transcripts/{meetingId}/related` 2. **벡터 검색 서비스 (Mock)** - 파일: `ai/src/main/java/com/unicorn/hgzero/ai/biz/service/RelatedTranscriptSearchService.java:27-47` ### 🔧 **개선 필요 사항** #### 3.1 Azure AI Search 벡터 검색 구현 **구현 방법**: ```java @Service @RequiredArgsConstructor public class RelatedTranscriptSearchService implements RelatedTranscriptSearchUseCase { private final SearchGateway searchGateway; private final MeetingGateway meetingGateway; private final ClaudeApiClient claudeApiClient; @Override public List findRelatedTranscripts( String meetingId, String transcriptId, int limit ) { log.info("관련 회의록 검색 - meetingId: {}, limit: {}", meetingId, limit); // 1. 현재 회의 정보 조회 MeetingInfo currentMeeting = meetingGateway.getMeetingInfo(meetingId); String searchQuery = buildSearchQuery(currentMeeting); // 2. Azure AI Search로 벡터 유사도 검색 List searchResults = searchGateway.searchRelatedMeetings( searchQuery, meetingId, limit ); // 3. 검색 결과를 RelatedMinutes로 변환 return searchResults.stream() .map(this::toRelatedMinutes) .collect(Collectors.toList()); } private String buildSearchQuery(MeetingInfo meeting) { // 회의 목적 + 안건을 검색 쿼리로 변환 return String.format("%s %s", meeting.getPurpose(), String.join(" ", meeting.getAgendaItems()) ); } private RelatedMinutes toRelatedMinutes(SearchResult result) { return RelatedMinutes.builder() .transcriptId(result.getMeetingId()) .title(result.getTitle()) .date(result.getDate()) .participants(result.getParticipants()) .relevanceScore(result.getScore() * 100) // 0-1 -> 0-100 .commonKeywords(extractCommonKeywords(result)) .summary(result.getSummary()) .link("/transcripts/" + result.getMeetingId()) .build(); } } ``` #### 3.2 임베딩 기반 검색 구현 **Azure OpenAI Embedding 활용**: ```java @Service public class EmbeddingService { @Value("${azure.openai.endpoint}") private String endpoint; @Value("${azure.openai.api-key}") private String apiKey; @Value("${azure.openai.embedding-deployment}") private String embeddingDeployment; private final WebClient webClient; /** * 텍스트를 벡터로 변환 */ public float[] generateEmbedding(String text) { Map request = Map.of( "input", text ); String response = webClient.post() .uri(endpoint + "/openai/deployments/" + embeddingDeployment + "/embeddings?api-version=2024-02-15-preview") .header("api-key", apiKey) .header("Content-Type", "application/json") .bodyValue(request) .retrieve() .bodyToMono(String.class) .block(); // 응답에서 embedding 벡터 추출 return parseEmbedding(response); } private float[] parseEmbedding(String response) { // JSON 파싱하여 float[] 반환 return new float[0]; // 구현 필요 } } ``` **Azure AI Search 벡터 검색**: ```java @Override public List searchRelatedMeetings( String query, String excludeMeetingId, int limit ) { // 1. 쿼리 텍스트를 벡터로 변환 float[] queryVector = embeddingService.generateEmbedding(query); // 2. 벡터 검색 쿼리 Map searchRequest = Map.of( "vector", Map.of( "value", queryVector, "fields", "contentVector", "k", limit ), "filter", String.format("meetingId ne '%s'", excludeMeetingId), "select", "meetingId,title,date,participants,summary,score" ); // 3. Azure AI Search API 호출 String response = webClient.post() .uri(endpoint + "/indexes/" + indexName + "/docs/search?api-version=2023-11-01") .header("api-key", apiKey) .bodyValue(searchRequest) .retrieve() .bodyToMono(String.class) .block(); return parseSearchResults(response); } ``` --- ## 4. 백엔드 API 검증 ### 4.1 SSE 스트리밍 테스트 **테스트 방법**: ```bash # 1. SSE 엔드포인트 연결 curl -N -H "Accept: text/event-stream" \ "http://localhost:8083/api/suggestions/meetings/test-meeting-001/stream" # 예상 출력: # event: ai-suggestion # data: {"discussionTopics":[...],"decisions":[...]} ``` **프론트엔드 연동 예시** (JavaScript): ```javascript // 05-회의진행.html에 추가 const meetingId = "test-meeting-001"; const eventSource = new EventSource( `http://localhost:8083/api/suggestions/meetings/${meetingId}/stream` ); eventSource.addEventListener('ai-suggestion', (event) => { const suggestions = JSON.parse(event.data); // 논의사항 표시 suggestions.discussionTopics.forEach(topic => { addDiscussionCard(topic); }); // 결정사항 표시 suggestions.decisions.forEach(decision => { addDecisionCard(decision); }); }); eventSource.onerror = (error) => { console.error('SSE 연결 오류:', error); eventSource.close(); }; function addDiscussionCard(topic) { const card = document.createElement('div'); card.className = 'ai-suggestion-card'; card.innerHTML = `
${new Date().toLocaleTimeString()} ${topic.priority}
[논의사항] ${topic.topic}
${topic.reason}
`; document.getElementById('aiSuggestionList').prepend(card); } ``` ### 4.2 용어 설명 API 테스트 ```bash # POST /api/terms/detect curl -X POST http://localhost:8083/api/terms/detect \ -H "Content-Type: application/json" \ -d '{ "meetingId": "test-meeting-001", "text": "오늘 회의에서는 MSA 아키텍처와 API Gateway 설계에 대해 논의하겠습니다.", "organizationId": "org-001" }' # 예상 응답: { "success": true, "data": { "detectedTerms": [ { "term": "MSA", "confidence": 0.95, "category": "아키텍처", "definition": "Microservices Architecture의 약자...", "context": "회의 맥락에 맞는 설명..." }, { "term": "API Gateway", "confidence": 0.92, "category": "아키텍처" } ], "totalCount": 2 } } ``` ### 4.3 관련 회의록 API 테스트 ```bash # GET /api/transcripts/{meetingId}/related curl "http://localhost:8083/api/transcripts/test-meeting-001/related?transcriptId=transcript-001&limit=5" # 예상 응답: { "success": true, "data": { "relatedTranscripts": [ { "transcriptId": "meeting-002", "title": "MSA 아키텍처 설계 회의", "date": "2025-01-15", "participants": ["김철수", "이영희"], "relevanceScore": 85.5, "commonKeywords": ["MSA", "API Gateway", "Spring Boot"], "link": "/transcripts/meeting-002" } ], "totalCount": 5 } } ``` --- ## 5. 프롬프트 엔지니어링 가이드 ### 5.1 AI 제안사항 프롬프트 **핵심 원칙**: 1. **명확한 역할 정의**: AI의 역할을 "회의록 작성 전문가"로 명시 2. **구체적인 기준**: 무엇을 추출하고 무엇을 제외할지 명확히 명시 3. **컨텍스트 제공**: 회의 목적과 안건을 프롬프트에 포함 4. **구조화된 출력**: JSON 형식으로 파싱 가능한 응답 요청 **프롬프트 템플릿**: ``` 당신은 비즈니스 회의록 작성 전문 AI 어시스턴트입니다. **역할**: 실시간 회의 텍스트를 분석하여 업무 관련 핵심 내용만 추출 **분석 기준**: 1. 논의사항 (discussions): - 회의 안건과 관련된 미결 주제 - 추가 검토가 필요한 업무 항목 - "~에 대해 논의 필요", "~검토해야 함" 등의 패턴 - **제외**: 잡담, 농담, 인사말, 회의와 무관한 대화 2. 결정사항 (decisions): - 명확한 의사결정 표현 ("~하기로 함", "~로 결정", "~로 합의") - 구체적인 Action Item - 책임자와 일정이 언급된 항목 **필터링 규칙**: - 회의 목적/안건과 무관한 내용 제외 - 단순 질의응답이나 확인 대화 제외 - 업무 맥락이 명확한 내용만 추출 **응답 형식**: 반드시 JSON만 반환 { "discussions": [ { "topic": "구체적인 논의 주제", "reason": "회의 안건과의 연관성", "priority": "HIGH|MEDIUM|LOW", "relatedAgenda": "관련 안건" } ], "decisions": [ { "content": "결정 내용", "confidence": 0.9, "extractedFrom": "원문 인용", "actionOwner": "담당자 (있는 경우)", "deadline": "일정 (있는 경우)" } ] } --- **회의 정보**: - 목적: {meeting_purpose} - 안건: {agenda_items} **회의 내용 (최근 5분)**: {transcript_text} 위 회의 목적과 안건에 맞춰 **회의와 관련된 내용만** 분석해주세요. ``` ### 5.2 용어 설명 프롬프트 ``` 다음 용어를 회의 맥락에 맞게 설명해주세요: **용어**: {term} **현재 회의 맥락**: {context} **이전 회의에서의 사용 사례**: {past_usages} **설명 형식**: 1. 기본 정의 (1-2문장, 비전문가도 이해 가능하도록) 2. 현재 회의 맥락에서의 의미 (1-2문장, 이번 회의에서 이 용어가 어떤 의미로 쓰이는지) 3. 이전 논의 참고사항 (있는 경우, 과거 회의에서 관련 결정사항) 비즈니스 맥락에 맞는 실용적인 설명을 제공하세요. ``` ### 5.3 관련 회의록 검색 쿼리 생성 프롬프트 ``` 현재 회의와 관련된 과거 회의록을 찾기 위한 검색 쿼리를 생성하세요. **현재 회의 정보**: - 목적: {meeting_purpose} - 안건: {agenda_items} - 진행 중인 주요 논의: {current_discussions} **검색 쿼리 생성 규칙**: 1. 회의 목적과 안건에서 핵심 키워드 추출 2. 동의어와 관련 용어 포함 3. 너무 일반적인 단어는 제외 (예: "회의", "논의") 4. 5-10개의 키워드로 구성 **출력 형식**: 키워드를 공백으로 구분한 문자열 예시: "MSA 마이크로서비스 API게이트웨이 분산시스템 아키텍처설계" ``` --- ## 6. 환경 설정 ### 6.1 application.yml 확인 사항 ```yaml # Claude API 설정 external: ai: claude: api-key: ${CLAUDE_API_KEY} # 환경변수 설정 필요 base-url: https://api.anthropic.com model: claude-3-5-sonnet-20241022 max-tokens: 2000 temperature: 0.3 # Azure AI Search 설정 ai-search: endpoint: ${AZURE_AI_SEARCH_ENDPOINT} api-key: ${AZURE_AI_SEARCH_API_KEY} index-name: meeting-transcripts # Event Hub 설정 eventhub: connection-string: ${AZURE_EVENTHUB_CONNECTION_STRING} namespace: hgzero-eventhub-ns eventhub-name: hgzero-eventhub-name consumer-group: transcript: ai-transcript-group ``` ### 6.2 환경 변수 설정 ```bash # Claude API export CLAUDE_API_KEY="sk-ant-..." # Azure AI Search export AZURE_AI_SEARCH_ENDPOINT="https://your-search-service.search.windows.net" export AZURE_AI_SEARCH_API_KEY="your-api-key" # Azure Event Hub export AZURE_EVENTHUB_CONNECTION_STRING="Endpoint=sb://..." ``` --- ## 7. 테스트 시나리오 ### 7.1 전체 통합 테스트 시나리오 1. **회의 시작** - 회의 생성 API 호출 - SSE 스트림 연결 2. **STT 텍스트 수신** - Event Hub로 TranscriptSegmentReady 이벤트 발행 - Redis에 텍스트 축적 확인 3. **AI 제안사항 생성** - 5분간 텍스트 축적 - Claude API 자동 호출 - SSE로 제안사항 수신 확인 4. **용어 설명 요청** - 감지된 용어로 설명 API 호출 - 맥락에 맞는 설명 확인 5. **관련 회의록 조회** - 관련 회의록 API 호출 - 유사도 점수 확인 --- ## 8. 다음 단계 1. ✅ **MeetingGateway 구현**: Meeting 서비스와 통신하여 회의 정보 조회 2. ✅ **SearchGateway Azure AI Search 통합**: 벡터 검색 구현 3. ✅ **ClaudeApiClient 프롬프트 개선**: 회의 컨텍스트 활용 4. ✅ **프론트엔드 SSE 연동**: 05-회의진행.html에 SSE 클라이언트 추가 5. ✅ **통합 테스트**: 전체 플로우 동작 확인