🧬 연구 허브

Skill

llm-provider-switch 바로 실행

온프렘 Ollama와 클라우드(OpenAI·Azure)를 설정만 바꿔 오가고, 하나가 죽으면 자동으로 넘긴다.

언제 쓰나

병원 프로젝트는 거의 항상 이 왕복을 한다: 온프렘 Ollama에서 개발 → Azure/OpenAI로 성능 검증 → 다시 온프렘 배포. 매번 새로 짜면 프로젝트마다 provider 코드가 갈라진다. 실제로 CRC-TNM-Agents 7벌의 llm_manager.py가 서로 다르게 갈라져 있다.

정본 코드

~/Documents/IMOK/CRC-TNM-Agents_crc/src/llm/llm_providers/
  base_llm_provider.py   311줄  추상 인터페이스 + 예외 체계 + 재시도
  llm_manager.py         572줄  provider 선택·폴백·health check
  ollama_provider.py     783줄  다중서버 로드밸런싱 + thinking 대응
  openai_provider.py     464줄
  azure_provider.py      505줄

_crc가 이 계열에서 가장 최근에 손댄 버전(2026-07-14)이고 ollama_http_client.py를 유일하게 가지고 있다. 새 프로젝트는 이 디렉토리를 복사해서 시작한다. src.setting.config에 의존하므로 import 경로만 고치면 된다.

바로 쓰는 코드

# 추론 모델 응답에서 JSON 건지기 (4단 폴백)
echo '<think>{"category":"T3"}</think> 설명' | python3 scripts/thinking_json.py

scripts/thinking_json.pyollama_provider.py 의 thinking 처리 4종을 provider 의존 없이 뺀 것. extract_thinking · remove_thinking_tags · extract_json(json.loads 검증 포함) · answer_to_json. parse(content, value_pattern) 이 본문 → thinking → 값 변환 순으로 물러나며 시도하고, 어느 단계에서 나왔는지 함께 돌려준다.

구조

3계층으로 나눈다. 이 경계를 지켜야 provider 추가가 한 파일 추가로 끝난다.

LLMManager          어느 provider를 쓸지 결정. 폴백·health check·통계
  └ BaseLLMProvider  추상 인터페이스 (ABC)
      ├ OllamaProvider     온프렘
      ├ OpenAIProvider     클라우드
      └ AzureOpenAIProvider 병원 계약 클라우드

BaseLLMProvider가 강제하는 5개 추상 메서드 — 새 provider를 붙일 때 이것만 구현하면 된다:

메서드역할
initialize()클라이언트 생성, 모델 확인
generate_response(LLMRequest) -> LLMResponse단발 호출
generate_stream(LLMRequest)스트리밍
validate_connection() -> boolhealth check용
get_available_models() -> List[str]모델 목록

LLMRequest / LLMResponse 데이터클래스로 provider별 응답 형식을 흡수한다. 호출부는 provider를 절대 몰라야 한다 — 이게 깨지면 전환 계층의 의미가 없어진다.

폴백 동작

primary provider 시도
  → 실패하면 fallback_providers 순회
    → 성공한 provider로 응답 + 어느 provider가 응답했는지 메타데이터 기록
  → 전부 실패하면 LLMManagerError

메타데이터 기록(_record_llm_metadata)이 중요하다. 어느 응답이 폴백으로 나온 것인지 남기지 않으면 나중에 결과 품질 차이를 설명할 수 없다. 의료 데이터에서 "이 케이스는 Ollama가 아니라 Azure가 답했다"는 재현성의 핵심 정보다.

health check는 5분 간격(_health_check_interval = 300)으로 lazy 실행 — 매 호출마다 하면 느리고, 안 하면 죽은 서버로 계속 보낸다.

함정

1. thinking 모델이 JSON을 thinking 안에 넣는다

Qwen3 등 reasoning 모델은 <think>...</think> 안에 정답 JSON을 넣고 본문에는 산문을 쓴다. 그대로 파싱하면 전부 실패한다. ollama_provider.py의 4단 대응을 그대로 가져다 쓴다:

_extract_thinking()         <think> 내용 추출
_remove_thinking_tags()     본문에서 태그 제거
_extract_json_from_thinking()  thinking 안의 ```json 블록 또는 { } 추출 + json.loads 유효성 검증
_convert_thinking_to_json()    "Final Answer: T2b", **T2b**, \boxed{T2b} 같은 텍스트 응답을 JSON으로 변환

추출한 JSON은 반드시 json.loads로 검증하고, 실패하면 다음 패턴으로 넘어간다. 정규식 매치만 믿으면 깨진 JSON이 통과한다.

2. Azure content filter가 의료 텍스트를 막는다

병리·수술 기록에 나오는 표현이 Azure content filter에 걸린다. should_use_azure_safe_mode()로 우회 모드를 켠다 — Azure provider이면서 content_filter_enabled 환경변수가 True일 때만 활성화. 환경변수로 끄고 켤 수 있어야 한다. 코드에 하드코딩하면 다른 병원 계약에서 못 쓴다.

3. Ollama 서버 여러 대

_GlobalServerIndexManager 싱글턴이 round_robin으로 서버를 배분한다. 프로세스 전역이라 워커가 여러 개여도 한쪽 서버로 쏠리지 않는다. 서버별 healthy/unhealthy 마킹(_mark_server_unhealthy)으로 죽은 서버를 건너뛴다.

첫 호출 전에 _warmup_models()로 모델을 올려둔다. 안 하면 첫 케이스만 수십 초 걸려서 벤치마크가 왜곡된다.

4. 예외를 provider별로 뭉개지 마라

base_llm_provider.py가 정의하는 예외 체계를 유지한다 — LLMConnectionError(폴백 대상), LLMAuthenticationError(폴백해도 소용없음), LLMRateLimitError(재시도 대상), LLMTimeoutError, LLMModelNotFoundError. 전부 Exception으로 잡으면 인증 오류에도 폴백을 시도하며 시간을 낭비한다.

연관 skill

tnm-staging-extract, pathology-llm-extract, medical-code-extract, cancer-type-classify — 모두 이 계층 위에서 동작한다.