콘텐츠로 이동

라우터 관리 웹 검색

Continuum Router는 자체 호스팅 LLM 백엔드(vLLM, Ollama, llama.cpp, MLxcel, LM Studio, Generic OpenAI 호환, Continuum Router 원격)로 라우팅되는 채팅 완료 요청에 OpenAI 스타일의 web_search 도구를 투명하게 제공할 수 있습니다. 그래서 사용자는 모든 클라이언트에 검색 통합을 직접 구성하지 않고도 자체 호스팅 모델에서 완전한 에이전트 워크플로를 실행할 수 있습니다.

상용 프로바이더(OpenAI, Azure OpenAI, Gemini, Anthropic)는 이미 네이티브 웹 검색 도구를 제공하므로 라우터는 이들을 완전히 변경하지 않고 통과시킵니다. gpt-4o 또는 claude-sonnet-4.6에 대한 요청은 이전과 동일하게 라우터를 통해 흐릅니다.

동작 방식

각 채팅 완료에 대해 라우터는:

  1. 모델을 처리할 백엔드를 결정합니다.
  2. 백엔드가 자체 호스팅이고 web_search 기능이 활성화되어 있으면, 아웃바운드 tools[] 배열에 web_search 함수 도구를 주입합니다.
  3. 요청을 디스패치합니다. 모델이 web_search에 대한 tool_calls 항목으로 응답하면 라우터는:
    • {"query": "..."} 인수를 파싱하고,
    • 구성된 검색 프로바이더를 호출하며,
    • JSON 인코딩된 결과가 포함된 tool 역할 메시지를 추가하고,
    • 보강된 대화로 백엔드를 다시 호출합니다.
  4. max_tool_iterations 횟수(기본값 5)까지 반복한 후 모델이 생성한 최종 응답을 반환합니다.

프로바이더가 실행 중간에 실패하면 라우터는 요청을 실패시키는 대신 모델에 구조화된 {"error": "..."} 도구 결과를 반환합니다. 모델은 그런 다음 사과하거나, 재시도하거나, 검색 없이 답변할 수 있습니다.

Anthropic Messages API 지원

클라이언트가 /anthropic/v1/messages 엔드포인트로 라우터와 통신할 때, 라우터는 두 가지 형태의 web_search 도구를 지원합니다:

커스텀 web_search 도구

클라이언트가 JSON input_schema를 가진 Anthropic 커스텀 도구 형식의 web_search를 보내면, 라우터는 일반 자체 호스팅 요청과 동일하게 처리합니다: OpenAI 포맷으로 변환 → 제한된 도구 실행 루프 실행 → 응답을 Anthropic 포맷으로 다시 변환. Claude Code의 메인 대화가 이 경로를 사용합니다.

Anthropic 서버 도구 web_search_20250305

Anthropic 네이티브 웹 검색은 {"type": "web_search_20250305", "name": "web_search"} 형태로 식별됩니다. Claude Code의 내장 WebSearch 기능은 이 형태와 함께 tool_choice: {type: "tool", name: "web_search"}를 사용해 모델이 도구를 반드시 호출하도록 강제합니다. 이 형태에 일반 도구 실행 루프를 적용하면 강제된 tool_choice 때문에 모델이 매 라운드마다 도구 호출을 다시 내보내 절대 종료되지 않으므로, 라우터는 전용 경로를 사용합니다:

  1. 한 번의 백엔드 왕복으로 모델의 tool call에서 query를 추출합니다 (백엔드가 tool call을 내보내지 않으면 Claude Code 특유의 "Perform a web search for the query: " 접두어를 사용자 프롬프트에서 제거하는 폴백 사용).
  2. 구성된 검색 프로바이더를 한 번만 호출합니다 (루프 없음).
  3. 응답은 Anthropic 네이티브 포맷으로 조립됩니다:

    • 쿼리를 담은 server_tool_use 블록,
    • 순위가 매겨진 결과를 담은 web_search_tool_result 블록 (각 encrypted_content 필드에는 base64 인코딩된 스니펫이 저장됨).
  4. 스트리밍 클라이언트는 api.anthropic.com과 직접 통신할 때 Claude Code의 파서가 소비하는 것과 동일한 SSE 시퀀스(message_start, 두 블록 각각에 대한 content_block_start/delta/stop, message_delta, message_stop)를 수신합니다.

상용 네이티브 Anthropic 경로(forward_to_anthropic_native)는 수정되지 않습니다. Anthropic 백엔드가 구성되어 있으면 서버 도구가 변경 없이 그대로 프록시되어 Anthropic이 직접 실행합니다.

설정

config.yaml에 최상위 web_search 섹션을 추가합니다:

web_search:
  enabled: true
  provider: serper              # serper | exa | brave
  api_key: "${SERPER_API_KEY}"  # 환경 변수 치환 지원
  timeout_ms: 5000              # 요청당 타임아웃 (100 - 60 000 ms)
  max_results: 5                # 1 - 20
  max_tool_iterations: 5        # 도구 라운드의 하드 캡 (1 - 20)
  inject_policy: auto           # auto | always | never
  tool_name: web_search         # 모델에 알려지는 함수 이름
  result_char_cap: 4000         # 결과당 스니펫 잘라내기 캡 (바이트)
  streaming_enabled: false      # 스트리밍 중 도구 실행 (고급)
  loop_wall_clock_ms: 60000     # 루프의 총 벽시계 예산
  max_total_result_bytes: 32768 # 도구 결과 콘텐츠의 누적 바이트 캡

주입 정책

inject_policy는 라우터가 아웃바운드 요청에 도구 정의를 추가하는 시점을 결정합니다:

  • auto (기본값): 클라이언트가 enable_web_search: true를 보내거나 클라이언트가 이미 web_search 도구를 이름으로 참조하는 경우 주입합니다.
  • always: 모든 자체 호스팅 요청에 주입합니다 (활성화된 경우).
  • never: 절대 주입하지 않습니다 (전체 기능을 비활성화하지 않고도 주입을 비활성화할 수 있어 A/B 테스트에 유용).

백엔드별 재정의

web_search:
  enabled: true
  # ... 전역 설정 ...
  per_backend:
    sensitive-vllm:
      enabled: false           # 이 백엔드에 대해서만 비활성화
    ollama-external:
      inject_policy: always
    # canary-vllm:
    #   enabled: true          # 전역 enabled=false 상태에서도 이 백엔드만
    #                          # 개별적으로 켤 수 있습니다

핫 리로드

API 키를 포함한 모든 web_search 필드는 라우터의 구성이 다시 로드될 때 다시 읽힙니다. 라우터를 재시작하지 않고도 API 키를 교체하거나 기능을 켜고 끌 수 있습니다.

지원되는 프로바이더

프로바이더 상태 엔드포인트
Serper 구현됨 https://google.serper.dev/search
Brave 구현됨 https://api.search.brave.com/res/v1/web/search
Exa 구현됨 https://api.exa.ai/search

세 프로바이더 모두 동일한 SearchProvider 트레이트를 공유하며, web_search 구성 섹션에서 provider: 한 줄만 바꿔 프로바이더를 전환할 수 있습니다.

보안

  • API 키는 로드 시 ${ENV} 참조에서 확장되며 로그나 오류 응답에 절대 기록되지 않습니다.
  • WebSearchConfig, SerperProvider, BraveProvider, ExaProviderDebug 출력은 api_key 필드를 수정합니다.
  • 검색 결과의 제목과 스니펫은 먼저 HTML/제어 문자를 제거하도록 정리된 뒤, 모델 컨텍스트에 다시 주입되기 전에 result_char_cap으로 잘립니다.

관찰 가능성

metrics 기능이 컴파일될 때 해당 기능이 내보내는 Prometheus 메트릭:

  • web_search_calls_total{provider, outcome} — 실행된 도구 호출당 하나씩.
  • web_search_call_duration_seconds{provider, outcome} — 검색 실행 지연 시간 히스토그램.
  • web_search_iteration_cap_total{component="loop"}max_tool_iterations 캡에 도달할 때마다 증가.
  • web_search_injections_total{backend_type} — 라우터가 도구를 성공적으로 주입한 요청당 하나씩.

안전 경계

비스트리밍 도구 실행 루프는 네 가지 독립적인 가드를 적용합니다:

  • max_tool_iterations는 백엔드 왕복 횟수를 제한합니다 (하드 상한, 기본값 5).
  • loop_wall_clock_ms는 루프에서 소비되는 총 벽시계 시간을 제한합니다 (기본값 60초). 초과되면 모델의 마지막 최종 응답이 변경 없이 반환되며 클라이언트에게 오류가 노출되지 않습니다.
  • max_total_result_bytes는 모든 반복에 걸쳐 대화에 추가된 도구 결과 content의 결합 바이트 길이를 제한합니다 (기본값 32 KiB). 초과되면 추가 도구 결과 페이로드가 짧은 "tool-result budget exhausted" 오류로 대체되어 모델은 닫힌 도구 호출 사이클을 볼 수 있고 최종 응답을 생성할 수 있습니다.
  • 고아 도구 호출(함수 이름이 구성된 web_search 도구 이름이 아닌 도구 호출)은 구조화된 도구 역할 오류로 응답되어 백엔드가 다음 라운드를 빈 tool_call_id로 인해 거부하지 않습니다. 라우터는 서비스할 방법을 모르는 도구 호출을 절대 실행하지 않습니다.

모델 별칭 (Claude Code 호환성)

Claude Code 같은 클라이언트는 내부 기능에 특정 모델 이름을 하드코딩합니다. 예를 들어 WebFetch의 콘텐츠 요약기와 WebSearch가 쿼리 추출에 사용하는 중간 호출에는 claude-haiku-4-5-20251001이 사용됩니다. 자체 호스팅 모델만 서비스하는 라우터에서는 이러한 모든 내부 호출이 그대로는 ModelNotFound로 실패합니다.

최상위 model_aliases 섹션은 백엔드 선택 전에 수신 model 필드를 재작성하며, cc-switch가 대중화한 ANTHROPIC_DEFAULT_{HAIKU,SONNET,OPUS}_MODEL 관례를 따릅니다:

model_aliases:
  haiku: GLM-5_1          # "haiku"를 포함하는 모든 수신 이름
  sonnet: GLM-5_1         # "sonnet"을 포함하는 모든 수신 이름
  opus: GLM-5_1           # "opus"를 포함하는 모든 수신 이름
  reasoning: GLM-5_1      # thinking.type이 enabled | adaptive일 때 사용
  default: GLM-5_1        # 폴백; 비활성화하려면 설정하지 않음
  exact:                  # 전체 이름 고정 매핑, 우선 적용
    claude-haiku-4-5-20251001: GLM-5_1

매칭 순서는 exactreasoning (thinking이 활성화된 경우) → haikuopussonnetdefault입니다. 매칭은 수신 이름에 대한 대소문자 구분 없는 부분 문자열 검사입니다. 모든 재작성은 info 레벨로 로깅되어 운영자가 리다이렉션을 관찰할 수 있습니다.

별칭은 /anthropic/v1/messages에서만 적용됩니다; OpenAI /v1/chat/completions 엔드포인트는 영향을 받지 않습니다 (해당 호출자는 이미 모델 이름을 직접 지정). 네이티브 Anthropic 포워딩은 타입이 지정된 요청을 라운드트립하므로, Anthropic 백엔드로 라우팅하면서 별칭이 적용되지 않기를 원한다면 model_aliases를 설정하지 마십시오.

주의 사항

  • 비스트리밍 도구 실행 루프와 Anthropic web_search_20250305 서버 도구 에뮬레이션 경로 (스트리밍 SSE 시퀀스 포함)는 완전히 기능합니다. 일반 커스텀 도구 루프의 스트리밍 도구 실행은 여전히 기본적으로 streaming_enabled: false로 제한되고, 일반 루프에 도달하는 스트리밍 요청 (즉, 커스텀 web_search 도구도 Anthropic 서버 도구도 아닌 경우)은 현재 라우터 측 실행 없이 기본 SSE 스트림으로 전달됩니다.
  • 라우터는 모델이 올바른 형식의 {"query": "..."} 인수를 내보낼 것이라고 신뢰합니다. 잘못된 형식의 인수는 HTTP 오류 대신 모델에 대한 도구 역할 오류 메시지를 생성합니다.
  • 주입된 도구는 의도적으로 OpenAI 함수 호출 형식(tools[].function.parameters)으로 형성되어 OpenAI 스타일 도구 사용으로 훈련된 자체 호스팅 모델이 예측 가능하게 동작합니다.
  • 에뮬레이트된 web_search_tool_result 블록의 encrypted_content는 스니펫의 base64이며 암호학적으로 서명된 blob이 아닙니다. Anthropic은 해당 필드를 후속 턴에서 검증하는 서명된 참조로 사용하지만, 이 흐름에서는 라우터가 서버 역할이므로 Claude Code가 라운드트립할 수 있는 불투명한 base64 페이로드로 충분합니다.