LLM API로 앱에 AI 채팅 넣기 (Groq + Flutter 실전)

S_Soo100·5일 전

ai

목록 보기
7/8
post-thumbnail

앱 안에 AI 채팅을 넣었습니다 — 내부 데이터 우선, LLM은 보조

앱에 이미 쌓아둔 데이터를 먼저 쓰고, 거기서 답이 안 나올 때만 LLM을 부르는 하이브리드 채팅을 Flutter로 만든 과정입니다.
지금은 혼자 보니까 무료 API(Groq)만 갖고 돌아갑니다.


정적 데이터만 갖고 부족한데 어쩌지?

개인적으로 키우는 도마뱀과 동물들을 위해 반려동물 건강 가이드 앱을 만들고 있습니다. 앱 안에 동물 별 건강 정보를 JSON으로 넣어뒀는데, 이런 질문에는 답을 줄 수 없었습니다.

  • "골든리트리버 관절 영양제 뭐가 좋아?"
  • "강아지가 구토를 하는데 언제 병원 가야 해?"
  • "포메라니안 슬개골 탈구 예방법은?"

정적 위키는 "ㅁㅁ견종의 적정 체중: 5~8kg"이라고만 알려줍니다. 상황에 따른 구체적 질문에는 무력합니다.

그렇다고 LLM에게 모든 걸 맡기면? 앱 안에 검증해둔 데이터가 있는데 그걸 무시하고 LLM의 일반 지식에만 의존하게 됩니다. 할루시네이션 위험은 덤이고요.

원하는 구조는 이겁니다:

사용자 질문
  → 앱 내부 데이터에서 관련 정보 찾기
  → 있으면: 그 데이터를 LLM에게 컨텍스트로 넘겨서 답변 생성
  → 없으면: LLM 일반 지식으로 답변 + "일반 지식 기반" 표시

LLM을 "만능 답변기"로 쓰는 게 아니라, 내 데이터를 읽고 설명해주는 도우미로 쓰는 겁니다.


전체 구조

┌──────────────────────────────────────────┐
│            사용자 질문 입력                  │
└───────────────┬──────────────────────────┘
                │
                ▼
┌──────────────────────────────────────────┐
│  1. 질문 분석 — 무슨 주제에 대한 질문인가?        │
│     키워드 매칭으로 품종(breed) + 카테고리 감지   │
└───────────────┬──────────────────────────┘
                │
                ▼
┌──────────────────────────────────────────┐
│  2. 내부 데이터 검색                         │
│     감지된 품종 + 카테고리로 JSON 데이터 조회     │
│     → 관련 스니펫 추출                       │
└───────────────┬──────────────────────────┘
                │
        ┌───────┴───────┐
        ▼               ▼
   데이터 있음        데이터 없음
        │               │
        ▼               ▼
┌─────────────┐  ┌─────────────┐
│ 시스템 프롬프트 │  │ 시스템 프롬프트 │
│ + 내부 데이터  │  │ (데이터 없이)  │
│ + 질문       │  │ + 질문       │
└──────┬──────┘  └──────┬──────┘
       │                │
       └───────┬────────┘
               ▼
┌──────────────────────────────────────────┐
│  3. LLM API 호출 (Groq, 무료)              │
└───────────────┬──────────────────────────┘
                │
                ▼
┌──────────────────────────────────────────┐
│  4. 앱 코드가 출처/경고를 강제 첨부              │
│     데이터 있었음 → "📎 출처: PetMD..."       │
│     데이터 없었음 → "⚠️ 일반 지식 기반"         │
└──────────────────────────────────────────┘

핵심은 LLM을 항상 부르되, 내부 데이터가 있으면 컨텍스트로 넘긴다는 점입니다. 내부 데이터만으로 답변을 완성하는 게 아니라, LLM이 내부 데이터를 "참고"해서 자연어 답변을 만들도록 하는 구조입니다.


1단계: 무료 LLM 프로바이더 세팅

Groq를 선택한 이유

프로바이더무료 티어OpenAI 호환한국어
Groq1,000 요청/일, 카드 불필요양호
DeepSeek크레딧제양호
Gemini15 RPM❌ (자체 SDK)중간
OpenAI유료네이티브우수

Groq는 가입만 하면 하루 1,000번 무료로 쓸 수 있고, OpenAI 호환 API라서 나중에 프로바이더를 바꾸고 싶으면 URL과 모델명만 교체하면 됩니다.

Flutter에서 API 호출

// llm_api_repository.dart
Future<String> sendChat(List<Map<String, String>> messages) async {
  final response = await http.post(
    Uri.parse('https://api.groq.com/openai/v1/chat/completions'),
    headers: {
      'Authorization': 'Bearer $apiKey',
      'Content-Type': 'application/json',
    },
    body: jsonEncode({
      'model': 'llama-3.3-70b-versatile',
      'messages': messages,
      'max_tokens': 1024,
      'temperature': 0.3,  // 사실 기반 답변 → 낮게
    }),
  );
  
  final data = jsonDecode(response.body);
  return data['choices'][0]['message']['content'];
}

OpenAI SDK 없이 http 패키지 하나로 충분합니다. temperature: 0.3은 창의적 답변보다 정확한 답변이 중요한 도메인 특화 앱에 맞는 설정입니다.


2단계: 질문 분석 — 키워드 매칭

LLM에게 "이 질문이 무슨 주제야?"라고 물어볼 수도 있지만, 그러면 API를 두 번 부르게 됩니다. 대신 키워드 매칭으로 로컬에서 처리합니다.

// context_builder.dart

// 카테고리 키워드
static const _categoryKeywords = {
  'joints':      ['관절', '슬개골', '고관절', '디스크', '연골'],
  'skin':        ['피부', '알러지', '탈모', '가려움', '아토피'],
  'diet':        ['사료', '간식', '영양제', '칼슘', '급여량'],
  'vaccination': ['백신', '접종', '예방', '항체', '부스터'],
  // ...
};

Set<String> detectCategories(String question) {
  final matched = <String>{};
  for (final entry in _categoryKeywords.entries) {
    for (final keyword in entry.value) {
      if (question.contains(keyword)) {
        matched.add(entry.key);
        break;
      }
    }
  }
  return matched;
}

"골든리트리버 관절 영양제 뭐가 좋아?" → joints + diet 카테고리 감지. 이 결과로 내부 데이터에서 관절·영양 관련 정보만 꺼냅니다.

후속 질문 대응

"관절 영양제 뭐가 좋아?" 다음에 "그러면 겨울에는?"이라고 물으면 키워드가 없습니다. 이때는 이전 대화 4개를 스캔해서 카테고리를 계승합니다.

if (categories.isEmpty) {
  final history = chatRepo.getRecentMessages(conversationId, limit: 4);
  for (final msg in history) {
    categories = detectCategories(msg.content);
    if (categories.isNotEmpty) break;
  }
}

형태소 분석 없는 단순 키워드 매칭이지만, 도메인이 좁으면 이 정도로 충분합니다.


3단계: 컨텍스트 빌딩 — 핵심

여기가 이 구조의 가치를 결정하는 부분입니다. 같은 LLM이라도 컨텍스트 품질에 따라 답변이 극적으로 달라집니다.

BuildContextResult buildContext(String question, String? breedId) {
  // 1. 품종 + 카테고리 감지
  final breed = detectBreed(question) ?? breedId;
  final categories = detectCategories(question);
  
  // 2. 내부 데이터에서 관련 스니펫 추출
  String? healthSnippet;
  List<String> sources = [];
  
  if (breed != null && categories.isNotEmpty) {
    final healthInfo = healthInfoRepo.getHealthInfo(breed);
    healthSnippet = extractSnippet(healthInfo, categories);
    sources = filterSources(categories);  // 카테고리 관련 출처만
  }
  
  // 3. 시스템 프롬프트 조립
  final systemPrompt = '''
반려동물 건강 전문 AI. 주요 견종별 건강 정보 제공.
${healthSnippet != null ? '\n[앱 데이터]\n$healthSnippet' : ''}

규칙:
- [앱 데이터]가 있으면 참고하되, "앱 데이터에 따르면" 같은 메타 언급 없이 바로 답변.
- [앱 데이터]가 없어도 일반 지식으로 성실히 답변.
- 한국어, 간결체. 출처표기 금지 — 출처는 앱이 자동으로 붙입니다.
''';

  return BuildContextResult(
    messages: [
      {'role': 'system', 'content': systemPrompt},
      ...recentHistory,  // 최근 6개 메시지
      {'role': 'user', 'content': question},
    ],
    hasHealthData: healthSnippet != null,
    sources: sources,
  );
}

포인트 세 가지:

① 내부 데이터를 시스템 프롬프트에 [앱 데이터]로 삽입합니다. LLM은 이걸 "자기가 아는 것"처럼 자연스럽게 답변에 녹여냅니다.

hasHealthData 플래그를 반환합니다. 이 값으로 나중에 "출처 있음/없음"을 앱 코드가 결정합니다. LLM에게 판단을 맡기지 않습니다.

③ 출처도 카테고리별로 필터링합니다. 관절 질문에 백신 관련 출처를 보여주면 신뢰가 떨어집니다.


4단계: 출처와 신뢰도 — LLM에게 맡기지 않는 것들

이 구조에서 가장 중요한 설계 결정입니다.

처음에는 프롬프트에 "답변 끝에 출처를 달아줘"라고 시켰습니다. 결과:

시도프롬프트결과
v1"답변 기반이 앱 데이터인지 구분 표시해줘""앱 데이터에 따르면..." 장황한 서문
v3"출처 규칙 5개항 상세 지시"규칙을 일관되게 안 따름
v4"출처표기 금지 — 출처는 앱이 자동으로 붙입니다"해결

LLM은 답변 생성에만 집중시키고, 메타데이터는 앱 코드가 처리합니다:

// chat_providers.dart — AI 응답 수신 후
String finalContent = aiResponse;

if (contextResult.hasHealthData) {
  final sourceText = contextResult.sources
      .map((s) => '- $s')
      .join('\n');
  finalContent += '\n\n📎 출처:\n$sourceText';
} else {
  finalContent += '\n\n⚠️ 일반 지식 기반 답변입니다';
}

이렇게 하면 출처 표시 일관성이 100%가 됩니다. LLM 재량에 맡겼을 때는 40% 정도였습니다.

사용자가 보는 화면:

  • 내부 데이터 기반 답변 → 답변 끝에 📎 출처: PetMD, AKC Canine Health
  • 일반 지식 답변 → 답변 끝에 ⚠️ 일반 지식 기반 답변입니다

사용자는 이 답변이 검증된 데이터에 근거한 건지, LLM의 일반 지식인지 바로 알 수 있습니다.


5단계: 대화에서 지식 쌓기

여기까지면 "잘 만든 챗봇"입니다. 한 단계 더 간 부분은 대화에서 재사용 가능한 지식을 자동으로 추출·축적하는 시스템입니다.

Day 1: "강아지 피부에 빨간 반점이 생겼어" → AI 답변 → 지식으로 저장 (confidence: 0.5)
Day 3: "알러지인지 어떻게 구분해?"       → 기존 지식을 컨텍스트에 추가 → 더 정확한 답변
Day 10: 비슷한 피부 질문               → 캐시 히트! API 호출 없이 즉시 응답

반복 질문에 대한 답변이 점점 좋아지고, 일정 수준 이상이면 API를 부르지 않아 한도도 아낍니다.

오답이 영원히 반복되면?

이 시스템의 가장 큰 위험입니다. 틀린 답변이 캐시되면 계속 틀린 답을 줍니다. 차단 장치 세 가지:

  1. confidence 최대 0.9 — 자동으로 1.0에 도달 불가 (사람 검증 없이는)
  2. "부정확" 버튼 — 사용자가 누르면 confidence -0.3
  3. 자동 삭제 — confidence ≤ 0.1이면 엔트리 삭제 (신고 3회면 거의 확실히 삭제)

토큰 예산 — 무료 티어에서 살아남기

Groq 무료 티어는 하루 1,000 요청, 500K 토큰입니다. 한 번 요청에 얼마나 쓰는지 관리해야 합니다.

세그먼트~토큰
시스템 프롬프트200
내부 데이터 스니펫400~800
대화 히스토리 (최근 6개)800~1,200
사용자 질문50~100
응답 예산1,024
합계~3,000~4,000

Groq의 Llama 3.3 70B는 128K 컨텍스트이므로 매우 여유 있습니다. 내부 데이터 스니펫을 카테고리별로 필터링하는 이유가 여기에도 있습니다 — 전체 데이터를 때려넣으면 토큰 낭비입니다.


이 구조를 다른 앱에 쓰려면

도메인에 종속된 부분과 아닌 부분을 분리하면 재사용할 수 있습니다.

그대로 쓸 수 있는 것:

  • LLM API 호출 모듈 (OpenAI 호환이면 URL만 교체)
  • 지식 축적/캐시 시스템 (confidence + 피드백)
  • 출처 강제 첨부 패턴 (hasHealthData 분기)
  • 대화 저장/한도 관리

도메인별로 바꿔야 하는 것:

  • 카테고리 키워드 목록
  • 엔티티 키워드 (여기서는 견종)
  • 시스템 프롬프트
  • 내부 데이터 구조와 스니펫 빌더
  • 출처 URL 매핑

인터페이스 하나로 정리하면:

abstract class ChatConfig {
  String get systemPrompt;
  Map<String, List<String>> get categoryKeywords;
  Map<String, List<String>> get entityKeywords;
  Future<({String snippet, List<String> sources})> buildDataSnippet(
    String entityId, Set<String> categories);
}

이 인터페이스만 구현하면 요리 레시피 앱이든, 건강 관리 앱이든, 법률 상담 앱이든 같은 구조로 돌아갑니다.


결과물 미리보기

  • 조금 더 다듬어야 하겠지만, 컨텍스트도 잘 이어지고 원하는대로 잘 동작하고 있습니다.
  • 예시 질문은 레오파드 게코로 했습니다. 귀엽거든요.


정리

  1. LLM을 만능으로 쓰지 않습니다. 내부 데이터가 있으면 컨텍스트로 넘기고, LLM은 그걸 자연어로 풀어주는 역할만 합니다.
  2. 메타데이터는 앱 코드가 결정합니다. 출처 표기, 신뢰도 표시를 LLM에게 맡기면 일관성이 깨집니다. 코드로 강제하면 100%입니다.
  3. 무료 API로 충분합니다. Groq 무료 티어 + 키워드 기반 컨텍스트 빌딩 + 지식 캐시로, 비용 없이 쓸 만한 AI 채팅을 앱에 넣을 수 있습니다.
    (나중에 유저가 많아지면 갈아끼워요!)
profile
Ai agent 설계를 잘 하고싶은 개발자

0개의 댓글