-
이코에코(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(viafix/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. 개요
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}/messages2.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.pyclass 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.pydef 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 클라이언트 특징 openaiLangChainLLMAdapterLangChain ChatOpenAI래퍼,get_langchain_llm()메서드 제공googleGeminiLLMClientGoogle genai SDK 직접 사용, 네이티브 async API 4.2 Answer Node의 생성 로직 분기
파일:
apps/chat_worker/infrastructure/orchestration/langgraph/nodes/answer_node.pyasync 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.2openai (자동 추론) LangChainLLMAdapter ✅ 성공 3 model=gemini-3-pro-previewgoogle (자동 추론) 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 아키텍처 개선 포인트
- 확장성: 새로운 LLM Provider 추가 시
MODEL_REGISTRY와 클라이언트 구현만 추가 - 호환성:
hasattr체크로 LangChain 래퍼와 네이티브 클라이언트 모두 지원 - 추적성: 로깅으로 어떤 Provider/Model이 선택되었는지 명확히 확인 가능
'이코에코(Eco²) > Agent' 카테고리의 다른 글
이코에코(Eco²) Agent: Optimistic Update (FE) & Eventual Consistency (BE) 통합 트러블슈팅 (0) 2026.01.21 이코에코(Eco²) Agent: Image Generation E2E 검증 완료 (1) 2026.01.20 이코에코(Eco²) Agent: Token Streaming E2E 검증 완료 (0) 2026.01.19 이코에코(Eco²) Agent: Multi-Intent 분류 E2E 검증 완료 (0) 2026.01.19 이코에코(Eco²) Agent: Token Streaming 트러블슈팅 (0) 2026.01.19