모델로 가는 하나의 문, 8부. 게이트웨이 위의 Orchestration
retry와 reroute를 하는 orchestration framework는 게이트웨이와 경쟁한다. 8부는 framework를 얇게 유지하고 tools를 같은 identity 아래에 둔다.

애플리케이션이 tools를 호출하기 시작하는 순간 모델의 출력은 읽을 텍스트이기를 멈추고 실행할 명령이 되는데, 이 시리즈가 지금까지 쌓아 온 모든 보장은 token에 관한 것이었다. 프레임워크는 그 루프를 쓰기 쉽게 만들어 주면서, 같은 동작으로 스로틀링을 숨기고, 게이트웨이의 라우팅을 우회해 다른 경로를 타고, 계량된 요청 하나를 아무도 계획하지 않은 열한 개로 바꿔 놓을 수 있다. 7부는 애플리케이션에 retrieval을 주었다. 이번 편은 그 위에 올라가는 것에 관한 이야기이고, 구체적으로는 플랫폼이 이미 하고 있기 때문에 프레임워크가 하지 말아야 할 일이 무엇인지에 관한 이야기다.
프레임워크를 게이트웨이로 향하게 하고, 거기서 멈춰라
메커니즘은 사소하고, 그래서 규율이 명시적이어야 한다. orchestration framework는 OpenAI Chat Completions 형식을 말하고, 게이트웨이는 정확히 그것을 노출하므로, 통합은 base URL 하나와 자격 증명 하나다:
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
base_url="https://apim-genai-prod.azure-api.net/llm/v1",
api_key=gateway_token, # Entra ID token, not a model key
model="chat-default", # gateway alias, never a deployment name
max_retries=0, # the gateway owns retry and failover
timeout=60,
)
max_retries=0이 중요한 줄이고, 사람들이 가장 먼저 지우는 줄이기도 하다. 프레임워크는 클라이언트 측 retry, 지수 백오프, 그리고 종종 프로바이더 fallback을 기본으로 싣고 나온다. 모델 프로바이더와 직접 이야기하는 애플리케이션에는 모두 합리적인 기본값이지만, 라우팅과 로드 밸런싱과 circuit breaking을 이미 하는 게이트웨이 앞에서는 모두 적극적으로 해롭다.
클라이언트 측 retry가 3부의 circuit breaker에 무슨 짓을 하는지 생각해 보라. breaker가 트립되어 503을 반환하는 것은 정확히 호출자가 실패 중인 백엔드를 두드리기를 멈추게 하기 위해서다. 503을 백오프와 함께 다섯 번 retry하도록 설정된 프레임워크는 그 신호를 다섯 배의 부하로 바꾸고, 그 부하는 백엔드가 가장 감당하지 못하는 순간에 정확히 도착한다. 429도 마찬가지다. token 한도는 수요를 조형하려고 존재하는데, 그것을 뚫고 retry하는 클라이언트는 조형되지 않는다. retry는 한 곳에 속하고, 그곳은 이미 존재한다.
정직한 반대급부 하나. 연결 수준 실패에 대한 클라이언트에서의 retry 한 번은 괜찮다. 규칙은 게이트웨이 자신의 응답을 retry하는 것에 관한 것이지, 네트워크 불안정에 관한 것이 아니다.
프레임워크가 여전히 해야 할 일
프로바이더 추상화, 라우팅, retry, failover를 직무 기술서에서 제거하고 나면, 프레임워크에는 직접 만들기가 정말로 지루하면서 플랫폼 관심사가 전혀 없는 부분들이 남는다. 타입이 있는 입력을 받는 프롬프트 템플릿, 함수 시그니처에서 tool 스키마를 생성하는 일, 출력을 구조화된 타입으로 파싱하는 일, 그리고 tool call 하나를 후속 메시지로 바꾸는 루프 장부 정리다. 이것은 실제로 상당한 양의 일이고, 의존성을 받아들일 합리적인 이유다.
프레임워크가 되어서는 안 되는 것은 모델 선택이 사는 곳이다. 위 코드의 alias는 게이트웨이 alias이고, 2부의 모델 카탈로그가 해석한다. 프레임워크 자신의 모델 레지스트리, 자신의 fallback 체인, 자신의 프로바이더 목록, 이 모든 것은 두 번째 카탈로그이고, 모델이 은퇴하는 날 첫 번째 카탈로그와 어긋나게 된다.
tools는 두 번째 표면이고, 첫 번째 문을 공유할 수 있다
agent의 tools는 모델과 같은 대접을 받을 자격이 있다. 하나의 통제된 진입점, 테넌트별 접근, 감사 추적이다. API Management는 이미 관리하는 REST API를 원격 MCP server로 노출할 수 있고, 선택한 오퍼레이션을 MCP 클라이언트가 호출하는 tools로 게시하며, Developer, Basic, Standard, Premium 티어와 그 v2 변형에서 사용할 수 있다. 그 MCP server를 product에 연결하면 tool 접근이 모델 접근과 같은 product와 subscription으로 관리되는데, 이것이 요점의 전부다. 하나의 identity, 하나의 quota 이야기, 취소할 곳 하나다.
오늘 이것이 어디까지 가는지는 두 가지 제약이 결정한다. API Management는 관리형 REST API에서 노출한 서버에 대해 MCP server tools는 지원하지만 MCP resources와 prompts는 지원하지 않는다. 그리고 MCP server 기능은 workspace에서 지원되지 않는데, 이는 5부가 workspace를 가리켰던 바로 그 격리된 팀들에게 중요하다. workspace 게이트웨이와 MCP tool 게시가 둘 다 필요한 팀은 하나를 골라야 하고, 이는 아키텍처를 그리는 동안 배우는 편이 낫다.
프롬프트 경로의 content safety
agent loop는 입력 표면을 넓힌다. 검색된 문서, tool 출력, 사용자 텍스트가 모두 프롬프트 하나에 담긴다. llm-content-safety 정책은 모델이 보기 전에 콘텐츠를 Azure AI Content Safety로 라우팅한다.
<llm-content-safety backend-id="content-safety-backend" shield-prompt="true">
<categories output-type="EightSeverityLevels">
<category name="Hate" threshold="4" />
<category name="Violence" threshold="4" />
</categories>
</llm-content-safety>
속성 세 개는 복사하기보다 주의 깊게 읽어야 한다. threshold는 한계처럼이 아니라 허용치처럼 동작한다. threshold="4"이면 필터는 심각도 0부터 3까지를 허용하고 4부터 7까지를 차단하므로, 숫자를 올리는 것은 허용치를 올리고 덜 차단한다. 6부의 cache score threshold와 같은 반전이고, 사람들을 같은 방식으로 걸려 넘어지게 한다.
shield-prompt="true"는 적대적 사용자 공격에 대한 검사를 켜고, 기본값은 false다. 검색된 문서를 컨텍스트에 이어 붙이는 agent에게 그 기본값은 틀린 값이다. 현실적인 위협은 문서를 타고 도착하는 프롬프트 인젝션이지, 채팅창에 공격을 타이핑하는 사용자가 아니다.
enforce-on-completions 역시 기본값이 false이고, 정책이 inbound에 있을 때 검사를 모델의 응답까지 확장한다. 그리고 Content Safety의 10,000자 한도가 기본값인 window-size는 응답에 대해서만 설정할 수 있다. 요청에는 언제나 기본 window가 쓰인다. 따라서 아주 긴 검색된 컨텍스트는 당신이 제어하지 못하는 그 windowing으로 평가되며, 이는 필터가 알아서 확장된다고 믿을 것이 아니라 retrieval 결과를 제한된 크기로 유지해야 한다는 논거다.
루프가 5부의 모든 숫자에 하는 일
tool call 네 번을 유발하는 사용자 질문 하나는 요청 하나가 아니라 모델 호출 다섯 번이고, 매번 자라나는 대화 기록을 다시 보낸다. 플랫폼에 미치는 결과는 셋이다:
- 분당 quota는 더 이상 사용자 볼륨에 관한 것이 아니다. 사용자 볼륨 곱하기 평균 루프 깊이가 되고, 루프 깊이는 애플리케이션 프롬프트의 속성이며, 플랫폼 리뷰 없이 바뀐다.
- 폭주하는 루프는 부하와 구별되지 않는다. 게이트웨이가 보는 것은 quota 안의 정상적인 요청들이다. 애플리케이션이 자신의 반복 횟수를 스스로 제한해야 하고, 플랫폼은 이를 가정할 것이 아니라 요구 사항으로 공표해야 한다.
- chargeback에는 요청당이 아니라 사용자 상호작용당 correlation ID가 필요하다. 아니면 팀별 숫자는 사실이면서 쓸모없다. 그 ID는 로그 레코드에 속하고, 5부가 높은 카디널리티 데이터를 거기에 둔 것이 정확히 이 이유 때문이다.
게이트웨이의 요청당 token 상한은 유용한 안전장치이지 대체물이 아니다. 거대한 호출 하나의 피해를 제한할 뿐, 작은 호출 이백 번을 만드는 루프에는 아무것도 하지 못한다.
지켜봐야 할 실패 모드
- circuit breaker를 무력화하는 프레임워크 retry. 기본값은 켜짐이다. 의도적으로 끄고 이유를 문서화하라. 아니면 플랫폼의 보호 메커니즘이 부하 증폭기가 된다.
- 프레임워크 안의 두 번째 모델 카탈로그. 모델 은퇴일에 게이트웨이의 카탈로그와 어긋날 것이고, 애플리케이션은 아무도 있는 줄 몰랐던 배포 이름에 고정되어 있을 것이다.
- 한계로 읽힌 content safety threshold. 높을수록 더 허용적이다. 오탐을 줄이려고 튜닝하던 팀이 조였다고 믿으면서 실질적으로 필터를 꺼 버릴 수 있다.
- 기본값으로 남은
shield-prompt. 꺼짐이다. 검색된 문서를 섭취하는 agent에서 그것은 열린 채로 남은 인젝션 경로다. - 제한 없는 루프. "여섯 번 반복하면 멈춰라"를 표현하는 게이트웨이 정책은 없다. 애플리케이션만이 할 수 있으므로, 이는 통제가 아니라 계약이다.
- MCP와 workspace의 조합. 오늘은 함께 지원되지 않는다. 팀에게 둘 다 약속한 뒤에 발견하면 아키텍처 롤백이다.
9부가 물려받는 것
orchestration을 갖춘 애플리케이션, 같은 게이트웨이를 통해 게시된 tools, 그리고 요청 경로에 들어간 content safety다. 이 모든 것이 정책 XML, alias 매핑, product 정의로 설정되어 있고, 지금은 다른 모든 변경과 같은 방식으로 프로덕션에 도달하는데, 바로 그 부분이 아직 검토되지 않았다. 다음 편은 Azure DevOps, 그리고 모델 버전 변경을 리뷰 코멘트가 아니라 평가 뒤에 게이트로 세운다는 것이 무엇을 뜻하는지다.
다음으로 읽을 글
- 9부, 평가 게이트와 모델 은퇴, 이 모든 설정이 프로덕션에 도달하는 곳이고, 만료된 승인이 성공처럼 보인다는 사실이 드러나는 곳이다.
- Bedrock Agents vs 직접 만든 루프, 같은 직접 구축 대 도입 질문을 한 단계 위에서, 편의가 아니라 통제를 근거로 다투는 글이다.
- 당신의 문서를 통한 프롬프트 인젝션, retrieval이 컨텍스트를 채우는 바로 그곳에서
shield-prompt가 왜 가장 중요한지 다루는 글이다.
이것을 대규모로 운영할 때의 인프라와 플랫폼 쪽 이야기는 ercan.cloud에, 허브는 ercanermis.com에 있다.
참고 자료
Ercan의 다른 글
같은 저자, 다른 영역의 사이트 두 개.