ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 이코에코(Eco²) Agent: LLM 모델 선택 기능 E2E 검증 완료
    이코에코(Eco²)/Agent 2026. 1. 19. 20:46

    모델: GPT 5.2 / geimi-3.0-flash-preview, 이미지 생성: gemini-2.5-image-pro 고정

    API 요청 시 model 파라미터를 통한 LLM Provider 동적 선택 기능 검증 및 수정

    작성일: 2026-01-19
    브랜치: develop (via fix/model-verify-* worktree)
    관련 PR:

    • [#445] fix(chat_worker): provider auto-inference from model parameter
    • [#447] fix(chat_worker): async API fix for GeminiLLMClient
    • [#449] fix(chat_worker): restore await + add traceback logging
    • [#450] fix(chat_worker): add fallback for LLMs without get_langchain_llm

    목차

    1. 개요
    2. API 요청 스펙
    3. 백엔드 Provider 판단 로직
    4. 내부 생성 로직 분기
    5. 발견된 오류 및 수정
    6. E2E 테스트 결과
    7. 결론

    1. 개요

    Chat Agent API에서 클라이언트가 model 파라미터를 통해 사용할 LLM을 지정할 수 있는 기능을 구현하고 검증했습니다.

    1.1 요구사항

    요구사항 설명
    기본 동작 model 미지정 시 환경변수 기반 기본 모델 사용
    OpenAI 선택 model=gpt-5.2 등 OpenAI 모델 지정
    Google 선택 model=gemini-3-pro-preview 등 Gemini 모델 지정
    Provider 자동 추론 모델명에서 provider(openai/google) 자동 판단

    1.2 아키텍처 개요

    ┌─────────────────────────────────────────────────────────────────────────┐
    │                           Client Request                                 │
    │                   POST /api/v1/chat/{chat_id}/messages                  │
    │                   {"message": "...", "model": "gemini-3-pro-preview"}   │
    └────────────────────────────────┬────────────────────────────────────────┘
                                     │
                                     ▼
    ┌─────────────────────────────────────────────────────────────────────────┐
    │                            Chat API                                      │
    │                     (chat/presentation/http)                            │
    │                                                                          │
    │   • 요청 검증                                                            │
    │   • Worker에 작업 제출 (model 파라미터 포함)                             │
    └────────────────────────────────┬────────────────────────────────────────┘
                                     │ RabbitMQ
                                     ▼
    ┌─────────────────────────────────────────────────────────────────────────┐
    │                          Chat Worker                                     │
    │                                                                          │
    │  ┌─────────────────────────────────────────────────────────────────┐   │
    │  │                    dependencies.py                               │   │
    │  │                                                                   │   │
    │  │   1. model 파라미터 확인                                         │   │
    │  │   2. model명에서 provider 자동 추론                              │   │
    │  │   3. provider에 따라 LLM 클라이언트 생성                         │   │
    │  │      - openai → LangChainLLMAdapter (LangChain 래퍼)            │   │
    │  │      - google → GeminiLLMClient (네이티브 SDK)                  │   │
    │  └─────────────────────────────────────────────────────────────────┘   │
    │                                 │                                        │
    │                                 ▼                                        │
    │  ┌─────────────────────────────────────────────────────────────────┐   │
    │  │                      LangGraph Pipeline                          │   │
    │  │                                                                   │   │
    │  │   intent_node → router → subagents → aggregator → answer_node   │   │
    │  └─────────────────────────────────────────────────────────────────┘   │
    └─────────────────────────────────────────────────────────────────────────┘

    2. API 요청 스펙

    2.1 메시지 전송 엔드포인트

    POST /api/v1/chat/{chat_id}/messages

    2.2 요청 본문 (SendMessageRequest)

    {
      "message": "안녕하세요",
      "model": "gemini-3-pro-preview",  // 선택 (기본값: 환경변수)
      "image_url": null,                 // 선택
      "user_location": null              // 선택
    }

    2.3 모델별 요청 예시

    시나리오 1: 기본 모델 사용 (model 미지정)

    curl -X POST "https://api.dev.growbin.app/api/v1/chat/{chat_id}/messages" \
      -H "Authorization: Bearer $TOKEN" \
      -d '{"message": "hello"}'
    • model: 미지정 → 환경변수 LLM_MODEL 사용 (기본: gpt-5.2)
    • provider: 환경변수 LLM_PROVIDER 사용 (기본: openai)

    시나리오 2: OpenAI 모델 지정

    curl -X POST "https://api.dev.growbin.app/api/v1/chat/{chat_id}/messages" \
      -H "Authorization: Bearer $TOKEN" \
      -d '{"message": "hello", "model": "gpt-5.2"}'
    • model: gpt-5.2
    • provider: 모델명에서 자동 추론 → openai

    시나리오 3: Google Gemini 모델 지정

    curl -X POST "https://api.dev.growbin.app/api/v1/chat/{chat_id}/messages" \
      -H "Authorization: Bearer $TOKEN" \
      -d '{"message": "hello", "model": "gemini-3-pro-preview"}'
    • model: gemini-3-pro-preview
    • provider: 모델명에서 자동 추론 → google

    3. 백엔드 Provider 판단 로직

    3.1 Model Registry

    파일: apps/chat_worker/domain/models/provider.py

    class LLMProvider(str, Enum):
        OPENAI = "openai"
        GOOGLE = "google"
    
    MODEL_REGISTRY: dict[str, LLMProvider] = {
        # OpenAI Models
        "gpt-4o": LLMProvider.OPENAI,
        "gpt-4o-mini": LLMProvider.OPENAI,
        "gpt-5.2": LLMProvider.OPENAI,
        "o3-mini": LLMProvider.OPENAI,
    
        # Google Gemini Models
        "gemini-2.0-flash": LLMProvider.GOOGLE,
        "gemini-3-flash-preview": LLMProvider.GOOGLE,
        "gemini-3-pro-preview": LLMProvider.GOOGLE,
    }

    3.2 Provider 자동 추론 함수

    def infer_provider_from_model(model: str) -> LLMProvider | None:
        """모델명에서 provider를 자동 추론합니다."""
        # 1. Registry에서 정확히 매칭
        if model in MODEL_REGISTRY:
            return MODEL_REGISTRY[model]
    
        # 2. 접두사 기반 추론 (fallback)
        model_lower = model.lower()
        if model_lower.startswith(("gpt-", "o1-", "o3-")):
            return LLMProvider.OPENAI
        if model_lower.startswith("gemini"):
            return LLMProvider.GOOGLE
    
        return None  # 추론 불가

    3.3 Dependencies에서 Provider 결정

    파일: apps/chat_worker/setup/dependencies.py

    def get_process_chat_command(
        model: str | None = None,
        provider: str | None = None,
    ) -> ProcessChatCommand:
        """ProcessChatCommand 인스턴스 생성."""
    
        # 1. model이 지정되었으면 provider 자동 추론
        if model and not provider:
            inferred = infer_provider_from_model(model)
            if inferred:
                provider = inferred.value
                logger.info(
                    "Provider auto-inferred from model",
                    extra={"model": model, "provider": provider},
                )
    
        # 2. 기본값 적용
        effective_provider = provider or os.getenv("LLM_PROVIDER", "openai")
        effective_model = model or os.getenv("LLM_MODEL", "gpt-5.2")
    
        # 3. Provider에 따라 LLM 클라이언트 생성
        if effective_provider == "google":
            llm = GeminiLLMClient(model=effective_model)
        else:
            llm = LangChainLLMAdapter(model=effective_model)
    
        return ProcessChatCommand(llm=llm, ...)

    3.4 Provider 결정 흐름도

    ┌─────────────────────────────────────────────────────────────────┐
    │                    model 파라미터 확인                           │
    └────────────────────────────────┬────────────────────────────────┘
                                     │
                        ┌────────────┴────────────┐
                        │                         │
                  model 있음                model 없음
                        │                         │
                        ▼                         ▼
            ┌───────────────────┐       ┌─────────────────┐
            │ MODEL_REGISTRY    │       │ 환경변수 사용    │
            │ 에서 provider 조회 │       │ LLM_PROVIDER    │
            └─────────┬─────────┘       │ LLM_MODEL       │
                      │                 └────────┬────────┘
             ┌────────┴────────┐                 │
             │                 │                 │
        Registry 히트     Registry 미스          │
             │                 │                 │
             ▼                 ▼                 │
       provider 확정    접두사 기반 추론         │
       (openai/google)  (gpt-*, gemini*)        │
             │                 │                 │
             └────────┬────────┘                 │
                      │                          │
                      ▼                          │
            ┌─────────────────────┐              │
            │ LLM 클라이언트 생성  │◄─────────────┘
            │                     │
            │ openai → LangChain  │
            │ google → Gemini SDK │
            └─────────────────────┘

    4. 내부 생성 로직 분기

    4.1 LLM 클라이언트 타입

    Provider 클라이언트 특징
    openai LangChainLLMAdapter LangChain ChatOpenAI 래퍼, get_langchain_llm() 메서드 제공
    google GeminiLLMClient Google genai SDK 직접 사용, 네이티브 async API

    4.2 Answer Node의 생성 로직 분기

    파일: apps/chat_worker/infrastructure/orchestration/langgraph/nodes/answer_node.py

    async def answer_node(state: dict[str, Any]) -> dict[str, Any]:
        # ... 컨텍스트 준비 ...
    
        # LLM 호출 분기
        if hasattr(llm, "get_langchain_llm"):
            # ═══════════════════════════════════════════════════════
            # LangChain 방식 (OpenAI)
            # ═══════════════════════════════════════════════════════
            langchain_llm = llm.get_langchain_llm()
    
            # LangChain 메시지 구성
            langchain_messages = []
            if prepared.system_prompt:
                langchain_messages.append(SystemMessage(content=prepared.system_prompt))
            langchain_messages.append(HumanMessage(content=prepared.prompt))
    
            # LangChain astream() 사용
            async for chunk in langchain_llm.astream(langchain_messages):
                content = chunk.content
                if content:
                    answer_parts.append(content)
                    await event_publisher.notify_token_v2(...)
        else:
            # ═══════════════════════════════════════════════════════
            # 네이티브 LLM 방식 (Gemini 등)
            # ═══════════════════════════════════════════════════════
            async for chunk in llm.generate_stream(
                prompt=prepared.prompt,
                system_prompt=prepared.system_prompt,
            ):
                if chunk:
                    answer_parts.append(chunk)
                    await event_publisher.notify_token_v2(...)

    4.3 LangChain vs 네이티브 비교

    ┌─────────────────────────────────────────────────────────────────────────┐
    │                        LangChain 방식 (OpenAI)                          │
    ├─────────────────────────────────────────────────────────────────────────┤
    │                                                                          │
    │   LangChainLLMAdapter                                                   │
    │         │                                                                │
    │         ▼                                                                │
    │   get_langchain_llm() → ChatOpenAI 인스턴스                             │
    │         │                                                                │
    │         ▼                                                                │
    │   SystemMessage + HumanMessage 구성                                     │
    │         │                                                                │
    │         ▼                                                                │
    │   langchain_llm.astream(messages)                                       │
    │         │                                                                │
    │         ▼                                                                │
    │   AIMessageChunk → chunk.content 추출                                   │
    │                                                                          │
    └─────────────────────────────────────────────────────────────────────────┘
    
    ┌─────────────────────────────────────────────────────────────────────────┐
    │                       네이티브 방식 (Gemini)                             │
    ├─────────────────────────────────────────────────────────────────────────┤
    │                                                                          │
    │   GeminiLLMClient                                                       │
    │         │                                                                │
    │         ▼                                                                │
    │   generate_stream(prompt, system_prompt)                                │
    │         │                                                                │
    │         ▼                                                                │
    │   client.aio.models.generate_content_stream(...)                        │
    │         │                                                                │
    │         ▼                                                                │
    │   async for chunk in response → chunk.text 추출                         │
    │                                                                          │
    └─────────────────────────────────────────────────────────────────────────┘

    5. 발견된 오류 및 수정

    5.1 Issue 1: Provider 자동 추론 누락

    문제

    model 파라미터가 전달되어도 provider가 자동 추론되지 않아, Gemini 모델이 OpenAI API로 전송됨.

    # 에러 로그
    HTTP Request: POST https://api.openai.com/v1/chat/completions
    → 404 Not Found (model=gemini-3-pro-preview)

    원인

    process_task.py에서 get_process_chat_command(model=model)을 호출할 때 provider 추론 로직이 없었음.

    수정 (PR #445)

    # dependencies.py
    def get_process_chat_command(model: str | None = None, provider: str | None = None):
        # model이 지정되었으면 provider 자동 추론
        if model and not provider:
            inferred = infer_provider_from_model(model)
            if inferred:
                provider = inferred.value

    5.2 Issue 2: Gemini Model Registry 오류

    문제

    MODEL_REGISTRY에 잘못된 모델명이 등록되어 있어 provider 추론 실패.

    # 수정 전 (잘못됨)
    "gemini-3.0-preview": LLMProvider.GOOGLE,
    
    # 실제 모델명
    "gemini-3-pro-preview"  # .0이 아니라 -pro

    수정 (PR #445)

    MODEL_REGISTRY = {
        # ...
        "gemini-3-flash-preview": LLMProvider.GOOGLE,
        "gemini-3-pro-preview": LLMProvider.GOOGLE,  # 수정됨
    }

    5.3 Issue 3: GeminiLLMClient Async API 오류

    문제

    generate_stream에서 await 사용 여부로 인한 혼란.

    # 초기 구현 (동기 API 사용 시도)
    response = self._client.models.generate_content_stream(...)  # 동기 API
    
    # 수정 시도 1 (await 제거)
    response = self._client.aio.models.generate_content_stream(...)  # TypeError
    
    # 수정 시도 2 (await 복원)
    response = await self._client.aio.models.generate_content_stream(...)  # 정상

    원인

    Google genai SDK의 aio.models.generate_content_stream()은 코루틴을 반환하므로 await이 필요.

    수정 (PR #447, #449)

    async def generate_stream(self, prompt: str, ...) -> AsyncIterator[str]:
        # ...
        response = await self._client.aio.models.generate_content_stream(
            model=self._model,
            contents=full_prompt,
        )
        async for chunk in response:
            if chunk.text:
                yield chunk.text

    5.4 Issue 4: get_langchain_llm 메서드 없음 (Critical)

    문제

    answer_node.py에서 모든 LLM 클라이언트에 get_langchain_llm() 메서드가 있다고 가정.

    AttributeError: 'GeminiLLMClient' object has no attribute 'get_langchain_llm'

    원인

    • LangChainLLMAdapter: get_langchain_llm() 메서드 있음 ✅
    • GeminiLLMClient: get_langchain_llm() 메서드 없음 ❌
    # 수정 전 (문제 코드)
    langchain_llm = llm.get_langchain_llm()  # GeminiLLMClient에서 실패

    수정 (PR #450)

    # 수정 후
    if hasattr(llm, "get_langchain_llm"):
        # LangChain 방식 (OpenAI)
        langchain_llm = llm.get_langchain_llm()
        async for chunk in langchain_llm.astream(langchain_messages):
            # ...
    else:
        # 네이티브 LLM 방식 (Gemini 등)
        async for chunk in llm.generate_stream(prompt, system_prompt):
            # ...

    5.5 오류 발생 타임라인

    ┌─────────────────────────────────────────────────────────────────────────┐
    │                           오류 발생 타임라인                             │
    ├─────────────────────────────────────────────────────────────────────────┤
    │                                                                          │
    │  1차 테스트 (시나리오 3)                                                │
    │  ├─ 요청: model=gemini-3-pro-preview                                    │
    │  ├─ 결과: 404 Not Found                                                 │
    │  └─ 원인: Provider 자동 추론 누락 → OpenAI API로 전송됨                 │
    │      └─ 수정: PR #445 (provider auto-inference)                         │
    │                                                                          │
    │  2차 테스트                                                              │
    │  ├─ 요청: model=gemini-3-pro-preview                                    │
    │  ├─ 결과: TypeError (async API 오류)                                    │
    │  └─ 원인: generate_content_stream에서 await 누락                        │
    │      └─ 수정: PR #447, #449 (async API 수정)                            │
    │                                                                          │
    │  3차 테스트                                                              │
    │  ├─ 요청: model=gemini-3-pro-preview                                    │
    │  ├─ 결과: AttributeError ('get_langchain_llm')                          │
    │  └─ 원인: answer_node가 LangChain 전용 메서드 호출                      │
    │      └─ 수정: PR #450 (hasattr 분기 추가)                               │
    │                                                                          │
    │  4차 테스트 (최종)                                                       │
    │  ├─ 요청: model=gemini-3-pro-preview                                    │
    │  ├─ 결과: ✅ 성공                                                        │
    │  └─ 로그: "Provider auto-inferred: gemini-3-pro-preview → google"       │
    │           "GeminiLLMClient initialized"                                  │
    │           "Answer generated"                                             │
    │                                                                          │
    └─────────────────────────────────────────────────────────────────────────┘

    6. E2E 테스트 결과

    6.1 테스트 환경

    항목
    클러스터 EKS (dev 환경)
    API 도메인 api.dev.growbin.app
    테스트 일시 2026-01-19 20:39 KST

    6.2 테스트 시나리오 및 결과

    시나리오 요청 기대 Provider 기대 LLM Client 결과
    1 model 미지정 openai (환경변수) LangChainLLMAdapter ✅ 성공
    2 model=gpt-5.2 openai (자동 추론) LangChainLLMAdapter ✅ 성공
    3 model=gemini-3-pro-preview google (자동 추론) GeminiLLMClient ✅ 성공

    6.3 시나리오 3 상세 로그

    [INFO] Chat task received
    [INFO] Provider auto-inferred from model: gemini-3-pro-preview → google
    [INFO] GeminiLLMClient initialized
    [INFO] ProcessChatCommand started
    [INFO] AFC is enabled with max remote calls: 10.
    [INFO] Single intent classification completed
    [INFO] Intent node completed
    [INFO] Dynamic router completed
    [INFO] Aggregator: contexts collected
    [INFO] token_stream_finalized
    [INFO] Answer generated
    [INFO] ProcessChatCommand completed

    7. 결론

    7.1 구현 완료 항목

    항목 상태
    Model 파라미터를 통한 LLM 선택 ✅ 완료
    모델명에서 Provider 자동 추론 ✅ 완료
    OpenAI (LangChain) 경로 지원 ✅ 완료
    Google Gemini (네이티브) 경로 지원 ✅ 완료
    E2E 테스트 통과 ✅ 완료

    7.2 아키텍처 개선 포인트

    1. 확장성: 새로운 LLM Provider 추가 시 MODEL_REGISTRY와 클라이언트 구현만 추가
    2. 호환성: hasattr 체크로 LangChain 래퍼와 네이티브 클라이언트 모두 지원
    3. 추적성: 로깅으로 어떤 Provider/Model이 선택되었는지 명확히 확인 가능

     

    댓글

ABOUT ME

🎓 부산대학교 정보컴퓨터공학과 학사: 2017.03 - 2023.08
☁️ Rakuten Symphony Jr. Cloud Engineer: 2024.12.09 - 2025.08.31
🏆 2025 AI 새싹톤 우수상 수상: 2025.10.30 - 2025.12.02
🌏 이코에코(Eco²) 백엔드/인프라 고도화 중: 2025.12 - Present

Designed by Mango