프롬프트 버전 관리: 재배포 없이 10 s 안에 변경 적용
LLM 프롬프트를 라이브러리에 저장하고 prompt_id로 호출하며, 라벨을 옮겨 배포합니다. 실측 결과 라벨 변경이 실제 호출에 반영되기까지 8~9초가 걸렸고, 프롬프트 토큰 수는 인라인 방식과 동일한 114.5개로 측정됐습니다.
추천 모델
실시간 가격| 모델 | 평가 | 가격 |
|---|---|---|
| GPT-6 Luna support-answer의 v1과 v2를 제공했습니다. 프롬프트 ID를 사용한 호출은 렌더링된 요청을 인라인으로 전송했을 때와 동일하게 114.5개의 프롬프트 토큰을 사용했습니다(20쌍의 중앙값). | 데모의 답변 프롬프트에 사용된 모델 | $0.1/100만부터 |
| DeepSeek V4.1 Flash 버전에 저장된 출력 스키마로 policy-check v1을 실행했습니다. reasoning_effort를 none으로 설정한 상태에서 출력 토큰 18개로 JSON 답변을 생성했으며, 비용은 $0.000033였습니다. | 데모의 JSON 검사에 사용된 모델 | $0.3/100만부터 |
목차
프롬프트 버전 관리는 프롬프트를 코드 밖에 번호가 붙은 불변 버전으로 저장하고, ID로 호출하며, 라벨이 가리키는 버전을 바꿔 운영 환경에 적용할 버전을 선택하는 방식입니다. 라벨은 production, staging 같은 이름이 붙은 포인터로, 한 번에 하나의 버전을 가리킵니다. Synthorai 프롬프트 라이브러리에서는 요청에 prompt_id를 지정하면 게이트웨이가 해당 버전의 메시지, 모델, 파라미터를 채웁니다. production 라벨을 옮기면 이후 호출에 적용되는 버전이 바뀝니다. 테스트에서는 트래픽이 계속 들어올 때 라벨 변경이 실제 호출에 반영되기까지 8-9초가 걸렸고, 호출이 한동안 없다가 다시 들어온 경우에는 첫 호출부터 반영됐습니다. ID로 호출할 때 사용한 프롬프트 토큰 수는 같은 요청을 코드에 직접 작성했을 때와 정확히 같았습니다.
TL;DR
- Synthorai 게이트웨이에
prompt_id로 호출한 결과, 20쌍의 프롬프트 토큰 중앙값은 114.5개로 인라인 요청과 같았습니다. - 트래픽이 계속 들어올 때는
production라벨을 옮긴 뒤 8.0-8.5 s(중앙값) 후 호출에 반영됐습니다. 15 s 동안 호출이 없었던 경우에는 즉시 반영됐습니다. - 라벨을 20번 옮기는 동안 새 버전이 처음 응답한 뒤 이전 버전이 응답한 호출은 없었습니다.
- 처리할 수 없는 호출은 모델 실행 전에 400 또는 404로 실패했으며, 과금되지 않았습니다.
이 가이드에서는 LLM 에이전트 추적하기의 고객 지원 에이전트가 사용하는 프롬프트를 라이브러리로 옮기고, 게이트웨이가 지원하는 모든 방식으로 호출합니다. 코드는 github.com/synthorai-io/use-cases의 prompt-library/에 있습니다. Python 3.10 이상, 추론용 키, prompts.write 권한이 있는 관리자 키가 필요합니다. 키는 콘솔에서 발급받습니다.
git clone https://github.com/synthorai-io/use-cases
cd use-cases
cp .env.example .env # put both keys in it
cd prompt-library
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
python setup.py # saves the prompts and their versions; safe to run again
python call.py # every way to call a prompt
python ship.py # moves production and times it
python errors.py # what a call that cannot be served answers
이 글의 테스트는 2026년 10월 19일에 실행했습니다. setup.py는 프롬프트 2개를 저장합니다. support-answer는 에이전트의 답변 단계를 담당하며, 실제로 있을 법한 변경을 하나씩 적용한 버전 4개로 구성됩니다. policy-check는 JSON으로 응답하는 하위 에이전트의 검사 단계입니다.
| 프롬프트 | 버전 | 변경 내용 | 모델 |
|---|---|---|---|
support-answer | v1 | 기준 버전: 주문 번호를 명시하고 환불 날짜는 약속하지 않음 | GPT-6 Luna |
support-answer | v2 | 시스템 텍스트를 줄이고 주문 번호 규칙을 제거 | GPT-6 Luna |
support-answer | v3 | v1의 텍스트를 다른 모델에서 사용 | DeepSeek V4.1 Flash |
support-answer | v4 | v1에 기본값 없는 {{tone}} 변수 추가 | GPT-6 Luna |
policy-check | v1 | 주제와 위험 수준을 스키마에 맞는 JSON으로 반환 | DeepSeek V4.1 Flash |

production은 v1을, staging은 v2를 가리킵니다. v2와 v3는 후속 가이드인 배포 전 버전 테스트와 운영 트래픽 일부에 새 버전 적용하기에서 다룹니다. 콘솔에서는 각 버전을 직전 버전과 비교할 수 있습니다.

프롬프트를 ID로 호출하려면?
/v1/chat/completions, /v1/responses, /v1/messages에 일반 요청을 보내면서 본문에 prompt_id와 prompt_variables를 추가하면 됩니다. 변수는 버전의 텍스트에 들어 있는 {{name}} 형태의 자리표시자이며, prompt_variables에 이번 호출에서 사용할 값을 지정합니다. SDK는 모델과 메시지를 필수로 요구하므로 아무 모델 이름과 빈 사용자 메시지를 보냅니다. 게이트웨이는 해당 버전의 모델을 사용하고 빈 메시지는 제거합니다. OpenAI Python SDK에서는 추가 필드를 extra_body에 넣습니다.
import os
from openai import OpenAI
client = OpenAI(base_url="https://synthorai.io/v1", api_key=os.environ["SYNTHORAI_API_KEY"])
raw = client.chat.completions.with_raw_response.create(
model="any", # the version's model is used
messages=[{"role": "user", "content": ""}], # required by the SDK; dropped
extra_body={
"prompt_id": "support-answer",
"prompt_variables": {
"customer_message": "Where is order A-50977? It has been a week.",
"order_status": "in transit, shipped on 2026-10-01",
},
},
)
print(raw.headers["x-synthorai-prompt-version"]) # which version answered
print(raw.parse().choices[0].message.content)
product는 버전에 기본값(“가정용 전자제품”)이 지정되어 있어 호출에 넣지 않았습니다. 어떤 버전이 응답했는지는 X-Synthorai-Prompt-Version 응답 헤더로 확인합니다. 라벨로 요청한 호출에서는 이 헤더가 실제 사용된 버전을 알 수 있는 유일한 방법입니다.
버전을 지정하지 않으면 production이 가리키는 버전을 사용합니다. 다른 버전을 선택하는 필드는 2개이며, 한 호출에서는 둘 중 하나만 보낼 수 있습니다.
| 필드 | 예시 | 데모에서 사용된 버전 |
|---|---|---|
| 없음 | v1 (production) | |
prompt_label | "staging" | v2 |
prompt_version | 1 | v1 |
같은 ID 호출은 엔드포인트 3개에서 모두 동작했습니다. Chat Completions와 Responses는 OpenAI SDK로, Messages는 같은 extra_body를 사용해 Anthropic SDK로 호출했습니다. 모두 GPT-6 Luna의 v1이 처리했습니다. Responses에서는 버전의 메시지가 input에 들어갑니다. 엔드포인트별 필드 매핑은 프롬프트 라이브러리 가이드에 나와 있습니다.
ID로 호출하면 코드에 프롬프트를 직접 넣을 때보다 비용이 더 드나요?
ID로 호출해도 게이트웨이는 공급자에게 일반 요청을 보내므로, 코드에 직접 작성한 요청과 비용이 같습니다. 요청 20쌍으로 확인했습니다. 각 쌍에서는 프롬프트를 렌더링해 인라인으로 보내고, 같은 값을 ID로도 보냈습니다. 호출 순서는 번갈아 바꿨으며, 같은 쌍에는 동일한 솔트를 사용했습니다. 솔트는 메시지에 붙이는 짧은 무작위 태그로, 서로 다른 쌍이 같은 요청이 되지 않도록 넣었습니다.
| 프롬프트 토큰 중앙값 | 클라이언트 지연 시간 중앙값 | |
|---|---|---|
prompt_id로 호출 | 114.5 | 4.1 s |
| 인라인 호출 | 114.5 | 4.6 s |
토큰 수는 동일합니다. ID 호출이 오히려 더 빨랐는데, 추가 오버헤드로는 이 결과를 설명할 수 없습니다. 따라서 이 차이는 GPT-6 Luna의 호출 간 변동으로 보고, ID 호출에 따른 측정 가능한 지연은 없었다고 판단했습니다. 지연 시간 차이에 대한 통계 검정은 하지 않았습니다.
모델을 호출하지 않고 실제로 어떤 요청이 전송될지 확인하려면 렌더링하면 됩니다. POST /v1/prompts/{id}/render는 같은 변수를 받아 완성된 요청과 선택된 버전을 반환합니다.
curl https://synthorai.io/v1/prompts/support-answer/render \
-H "Authorization: Bearer $SYNTHORAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"format": "chat", "prompt_variables": {"customer_message": "Where is order A-50977?", "order_status": "in transit"}}'
렌더링은 과금되지 않으며 로그에도 남지 않습니다. 데모에서 렌더링한 뒤 해당 세션 ID로 로그를 조회했지만 요청은 없었습니다. 다만 키의 요청 속도 제한에는 포함됩니다.
라벨을 옮기면 호출에 반영되기까지 얼마나 걸리나요?
10초 미만입니다. 테스트에서는 호출이 계속 들어올 때 8-9 s가 걸렸고, 한동안 호출이 없다가 다시 들어온 경우에는 즉시 반영됐습니다. 버전 배포는 관리자 키로 라벨이 가리킬 버전 번호를 지정하는 호출 한 번이면 됩니다.
import os
import httpx
admin = httpx.Client(base_url="https://synthorai.io/api/v1",
headers={"Authorization": f"Bearer {os.environ['SYNTHORAI_PROMPTS_ADMIN_KEY']}"})
admin.put("/prompts/support-answer/labels/production", json={"version": 3}).raise_for_status()
롤백도 이전 버전 번호로 같은 호출을 보내면 됩니다. 애플리케이션 코드는 계속 같은 ID를 호출하며, 재배포는 필요 없습니다. 라벨은 관리자나 워크스페이스 소유자만 옮길 수 있고, 모든 이동은 워크스페이스 감사 로그에 기록됩니다.
ship.py로 시간을 측정했습니다. 0.5 s마다 호출을 보내면서 API로 production을 옮겼습니다. 헤더에 새 버전이 나타날 때까지, 그리고 그 이후 10 s 동안 호출을 계속했습니다. v1에서 v3로 5번, 반대로 5번 수행한 뒤, 각 이동 전에 15 s 동안 호출을 멈추는 조건으로 10번을 반복했습니다.
| 조건 | 이전 버전이 응답한 마지막 호출의 전송 시점 | 새 버전이 응답한 첫 호출의 전송 시점 |
|---|---|---|
| 0.5 s마다 호출, v1에서 v3로 이동 | 이동 후 중앙값 8.02 s (7.53-8.53) | 중앙값 8.52 s (8.03-9.03) |
| 0.5 s마다 호출, v3에서 v1로 이동 | 중앙값 7.52 s (7.52-8.52) | 중앙값 8.02 s (8.02-9.02) |
| 15 s 동안 호출 중단 후, 양방향 | 없음 | 10번 모두 0.0 s |
시간은 라벨 이동 API 호출이 반환된 시점부터 측정했습니다. 문서에는 라벨 이동이 호출에 반영되기까지 “약 10초”가 걸린다고 나와 있습니다. 두 조건의 결과는 게이트웨이가 마지막으로 읽은 라벨 정보를 몇 초 동안 보관한다고 보면 설명됩니다. 트래픽이 계속 들어오면 보관한 정보가 만료될 때 변경을 확인하고, 호출이 멈췄다가 다시 들어오면 첫 호출에서 라벨을 새로 읽는 방식입니다. 20번의 이동 모두 새 버전이 처음 응답한 뒤 이전 버전이 응답한 호출은 없었습니다. 변경이 전파되는 동안 두 버전 사이를 오가지는 않았습니다.
처리할 수 없는 호출은 어떻게 되나요?
모델을 호출하기 전에 400 또는 404로 실패합니다. 오류 응답 형식은 호출한 엔드포인트를 따르며 과금은 없습니다. errors.py에서 이런 호출 5개를 보냈습니다.
| 호출 | 상태 | 코드 | 메시지 |
|---|---|---|---|
tone 없이 v4 호출 | 400 | prompt_variable_missing | ”prompt_variables: tone 누락(전달된 값과 기본값 모두 없음)“ |
| 버전과 라벨을 함께 지정 | 400 | prompt_invalid | ”prompt_version과 prompt_label 중 하나만 전송해야 함” |
| 존재하지 않는 ID | 404 | prompt_not_found | ”prompt_id: 이 워크스페이스에 “support-answer-missing” 프롬프트가 없음” |
| 버전 99 | 404 | prompt_not_found | ""support-answer” 프롬프트에 버전 99가 없음” |
| 아무 버전도 가리키지 않는 라벨 | 404 | prompt_not_found | ""support-answer” 프롬프트의 “development” 라벨이 아직 어떤 버전도 가리키지 않음” |
5개 모두 과금 기록이 없습니다. 첫 번째 행은 기존 호출을 깨뜨리는 변경입니다. v4에는 기본값 없는 변수가 추가되므로 tone을 보내지 않는 호출은 모두 실패합니다. 콘솔은 배포 전에 이를 감지합니다. production을 v4로 옮기려고 하면 현재 호출에 어떤 영향이 있는지 확인하는 대화상자가 열리고 기본값을 입력받습니다. 입력한 기본값은 다음 버전으로 저장됩니다.

라이브러리를 일시적으로 읽을 수 없으면 게이트웨이는 마지막으로 읽은 사본을 최대 24시간 동안 사용하고, 응답에 X-Synthorai-Prompt-Stale: 1을 추가합니다. 최근 사본이 없는 프롬프트는 503 prompt_unavailable을 반환하므로 재시도해야 합니다. 데모에서는 두 상황 모두 발생하지 않았습니다.
버전에는 텍스트 외에 무엇을 저장할 수 있나요?
버전에는 시스템 텍스트, 메시지, 모델, 파라미터 5개, 출력 스키마, 함수 도구를 저장할 수 있습니다. 파라미터는 호출자가 보낸 값이 우선합니다. 버전에 저장된 temperature, max_tokens, top_p, response_format, reasoning_effort는 요청에 같은 필드가 없을 때만 적용됩니다.
policy-check는 JSON 스키마를 버전에 저장하므로, 호출자가 형식을 지정하지 않아도 해당 스키마에 맞는 JSON을 받습니다. API로 저장할 때는 params에 형식을 지정하고 schema에 스키마를 넣습니다.
version = {
"model": "deepseek-v4.1-flash",
"system": "You review messages to an online electronics shop's support desk. ...",
"messages": [{"role": "user", "content": "{{customer_message}}"}],
"params": {"max_tokens": 300, "response_format": "json_schema"},
"schema": {"name": "policy_check", "strict": True, "schema": {
"type": "object",
"properties": {"topics": {"type": "array", "items": {"type": "string"}},
"risk": {"type": "string", "enum": ["low", "medium", "high"]}},
"required": ["topics", "risk"], "additionalProperties": False}},
}
admin.post("/prompts/policy-check/versions", json={"body": version, "note": "v1: JSON check"}).raise_for_status()
response_format: "json_schema" 없이 스키마만 저장하면 그 이유를 알리는 400 오류로 거부됩니다. DeepSeek V4.1 Flash에서 policy-check를 한 번 호출했을 때 {"topics": ["shipping", "refunds"], "risk": "medium"}을 반환했습니다. 입력 151토큰, 출력 18토큰을 사용했고 비용은 $0.000033였습니다. 데모의 버전들은 reasoning_effort 저장 기능이 생기기 전에 만들어져서, 이 호출에서는 호출자가 reasoning_effort: "none"을 보냈습니다. 이제는 버전의 params에 저장할 수 있습니다. reasoning_effort: "none"을 지정한 테스트 버전을 저장하고 렌더링한 결과, Chat Completions 요청에는 reasoning_effort가, Responses 요청에는 reasoning.effort가 포함됐습니다. Messages에서는 Claude 모델의 사고 설정 형식이 다르므로 버전에 저장된 값은 제외됩니다. 이 경우에는 호출자가 직접 설정을 보내야 합니다.
도구 호출과 멀티턴 대화는 라이브러리 프롬프트를 쓰지 않을 때와 동일하게 동작합니다. 버전에 저장된 메시지가 먼저 나오고, 호출자가 보낸 대화가 그 뒤에 붙습니다. 데모의 도구 호출 루프에서 1번째 라운드에 모델은 lookup_order({"order_id": "A-50977"})를 요청했습니다. 2번째 라운드에서는 모델의 도구 호출과 도구 실행 결과를 일반 메시지로 담아 같은 ID로 보냈고, v1이 그 결과를 바탕으로 답했습니다.
어떤 버전이 응답했는지는 어디에서 확인하나요?
모든 호출에는 프롬프트와 버전이 함께 기록되므로, 다음 3가지 방법으로 확인할 수 있습니다.
- 응답:
X-Synthorai-Prompt-Version헤더를 확인합니다. 데모는 결과의 각 호출 옆에 이 값을 기록합니다. - 로그:
prompt = support-answer와prompt_version = 3으로 필터링하면 해당 버전의 호출이 나옵니다. 요청 상세에도 프롬프트와 버전이 표시됩니다.GET /api/v1/requests에서도 같은prompt_id,prompt_version필터를 사용할 수 있습니다. - 트레이스: 워터폴의 각 모델 호출 행에 “prompt vN” 배지가 붙고, 호출 패널에 프롬프트와 버전이 표시됩니다. LLM 에이전트 추적하기의 추적 헤더는 ID 호출에서도 그대로 사용할 수 있습니다.

프롬프트 페이지에서는 7일, 30일, 90일 기준으로 버전별 호출 수, 오류 수, 비용, 토큰 수, 지연 시간을 집계합니다. 점진 배포 가이드에서는 이 화면으로 실제 트래픽에서 두 버전을 비교합니다.
이 글에서 다루지 않은 내용
- 점진 배포와 테스트. 운영 트래픽 일부를 후보 버전으로 보내는 방법은 프롬프트 변경 A/B 테스트에서, 고정된 입력 집합으로 버전을 테스트하는 방법은 배포 전 프롬프트 테스트에서 다룹니다.
- 부분 템플릿. 텍스트 블록을 한 번 저장해 두고 여러 프롬프트에서
{{>id}}로 포함할 수 있습니다. 저장된 각 버전은 포함한 부분 템플릿의 버전도 고정합니다. - 변경 웹훅. 프롬프트에 버전이 추가되거나 라벨이 이동하면 서명된 POST를 보냅니다. 프롬프트를 git이나 채팅 채널과 동기화하는 데 사용할 수 있습니다. 데모의
webhook.py는 서명을 검증하지만, 실제 웹훅 수신은 확인하지 못했습니다. - 생성 요청의 멱등성.
Idempotency-Key헤더를 사용하면 안전하게 재시도할 수 있습니다. API는 24시간 동안 같은 키의 요청에 최초 응답을 반환하므로 프롬프트가 중복 생성되지 않습니다. 프롬프트를 삭제한 뒤에도 이 규칙이 유지되므로, 삭제한 프롬프트를 다시 만들려면 새 키를 보내야 합니다.
FAQ
프롬프트 버전 관리란 무엇인가요?
프롬프트를 저장 후 바뀌지 않는 번호별 버전으로 관리하고, 코드 변경 대신 라벨로 운영 환경에서 사용할 버전을 선택하는 방식입니다. 롤백하려면 라벨이 이전 버전을 가리키도록 바꾸면 됩니다.
프롬프트를 ID로 호출하면 지연 시간이나 토큰이 늘어나나요?
20쌍의 테스트에서는 둘 다 늘어나지 않았습니다. 프롬프트 토큰 중앙값은 두 방식 모두 114.5개였고, ID 호출이 더 느리지도 않았습니다. 게이트웨이는 사용할 버전을 결정한 뒤 공급자에게 일반 요청을 보냅니다.
프롬프트 변경이 운영 환경에 반영되기까지 얼마나 걸리나요?
약 10초 이내입니다. 테스트에서 호출이 계속 들어올 때는 라벨을 옮긴 뒤 8.0-8.5 s(중앙값)에 전송한 호출부터 새 버전이 응답했습니다. 트래픽이 15 s 동안 멈췄던 경우에는 즉시 반영됐습니다.
OpenAI나 Anthropic SDK를 계속 사용할 수 있나요?
두 SDK 모두 수정 없이 사용할 수 있습니다. 프롬프트 필드는 extra_body로 보내고, 필수 인자를 채우기 위해 아무 모델 이름과 빈 사용자 메시지를 넣으면 됩니다.
함께 읽기
- 프롬프트 라이브러리 가이드: 이 글에 나온 모든 필드, 오류, 응답 헤더.
- 콘솔의 프롬프트 관리: 편집기, 버전 비교, 라벨 이동 대화상자.
- 프롬프트 변경 A/B 테스트: 10% 점진 배포와 버전별 비교.
- 배포 전 프롬프트 테스트: 테스트 세트, 검사 항목, 한 번의 실행으로 알 수 있는 것과 알 수 없는 것.
- LLM 에이전트 추적하기: 데모에서 계속 전송하는 세션 및 트레이스 헤더.