Reasoning effort 실험 계획
LLM 모델의 Reasoning 강도 설정을 바꿔보며 어떤 차이가 나는지 실험을 해보고 싶어. 내가 쓰는 모델은 “Reasoning effort: High"를 넣어주면 설정을 바꿀 수있어. medium, Low도 가능해. endpoint url이 주어졌을 때 실험 계획을 세우고 스텝 바이 스텝으로 알려줘.
좋아요! “Reasoning effort: High/Medium/Low”를 바꿔가며 어떤 차이가 실제로 나는지 확인하려면, 변수·지표·실험 설계를 딱 정해 놓고 돌리는 게 핵심입니다. 아래는 엔드포인트 URL만 있으면 그대로 따라 할 수 있는 스텝-바이-스텝 실험 계획 이에요.
1) 목적과 가설 정하기
-
목적: Reasoning effort 수준에 따라 정확도/추론 성공률 , 지연 시간(latency) , 토큰/비용 이 어떻게 달라지는지 정량 비교.
-
가설(예시):
-
High > Medium > Low 순으로 정확도↑, 반대로 지연 시간·비용은 High가 가장 큼.
-
난도 높은 문제(멀티스텝, 함정 포함)는 High의 이점이 더 큼.
-
2) 비교할 조건 정의
-
독립변수:
reasoning_effort ∈ {low, medium, high} -
통제변수: 동일 모델/버전, temperature(예: 0.2), max_tokens, 시스템 프롬프트, seed(가능하면 고정), API 엔드포인트.
-
반복수: 각 프롬프트×effort별 n=3~5회 반복(랜덤성 평균화).
-
평가 세트: 3개 버킷 권장
-
(A) 정확/오답이 명확한 문제: 예) 수학/논리(정답 존재) 50~100문항
-
(B) 장문 요약/분류/정보추출: 기준 답안 또는 스코어러 있음
-
(C) 코딩/퍼즐/툴-이용 시나리오: 테스트케이스로 채점 가능
-
-
샘플 크기: 각 버킷당 최소 50개(가능하면 100개). 적을수록 반복수를 늘려 불확실성 보완.
3) 지표 설계
핵심 지표
-
정확도/스코어 : 정답률, F1/ROUGE/BLEU, 테스트케이스 통과율 등 태스크별 자동 채점.
-
지연 시간 : end-to-end(요청~응답 수신) ms.
-
비용/토큰 : input/output/reasoning 토큰(가능 시) 및 총 비용.
-
실패율 : 타임아웃/에러율.
보조 지표
-
스텝 수 추정 : 응답 길이(토큰)로 간접 추정.
-
일관성 : 반복 응답 간 편차(분산).
4) 실험 디자인
-
페어드 테스트(권장): 같은 프롬프트를 Low/Med/High 모두에 돌려 쌍대 비교.
-
무작위 순서화: (프롬프트 i)×(effort j) 실행 순서를 무작위로 섞음(순서 효과 제거).
-
라틴 스퀘어(선택): 순서 편향이 걱정되면 적용.
-
리트라이 규칙: 타임아웃/429는 지수 백오프 로 2~3회 재시도, 실패는 로그에 표기.
5) 데이터 포맷(로그 스키마)
각 호출마다 아래를 저장하세요(JSON line 추천).
{
"prompt_id": "A_023",
"bucket": "math",
"effort": "high|medium|low",
"trial": 1,
"request_ts": "...",
"latency_ms": 842,
"input_tokens": 312,
"output_tokens": 521,
"reasoning_tokens": 409, // 제공되면
"cost_usd": 0.0123, // 단가표로 계산
"response_text": "...",
"tool_calls": [], // 있으면
"error": null
}
6) 실행 방법(엔드포인트 예시)
아래에서 ENDPOINT_URL과 API_KEY만 바꿔 쓰면 됩니다.
Effort는 요청 바디 에 reasoning_effort 필드로 넣는다고 가정했습니다. (예: "high"|"medium"|"low")
(A) cURL 단건 테스트
curl -X POST "$ENDPOINT_URL" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "your-model-id",
"reasoning_effort": "high",
"temperature": 0.2,
"max_tokens": 1024,
"seed": 42,
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "문제: 12개의 사과를 3명에게 공평하게 나누면 한 사람당 몇 개인가?"}
]
}'
(B) 파이썬 배치 스크립트(재현 가능, 로그 저장)
import os, time, json, random, uuid
import requests
from datetime import datetime
ENDPOINT_URL = os.getenv("ENDPOINT_URL")
API_KEY = os.getenv("API_KEY")
MODEL = "your-model-id"
HEADERS = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"
}
EFFORTS = ["low", "medium", "high"]
TEMPERATURE = 0.2
SEED = 42
MAX_TOKENS = 1024
REPEATS = 3 # 각 프롬프트×effort 반복 수
# 프롬프트 목록 예시
dataset = [
{"prompt_id": "M_001", "bucket": "math", "user": "세 수의 평균이 10이고..."},
{"prompt_id": "S_014", "bucket": "summary", "user": "아래 글을 1문장으로 요약..."},
# ... (CSV/JSON에서 로드 권장)
]
def call_model(user_text, effort):
payload = {
"model": MODEL,
"reasoning_effort": effort,
"temperature": TEMPERATURE,
"max_tokens": MAX_TOKENS,
"seed": SEED,
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": user_text}
]
}
t0 = time.time()
r = requests.post(ENDPOINT_URL, headers=HEADERS, json=payload, timeout=120)
dt = int((time.time() - t0) * 1000)
if r.status_code != 200:
return dt, None, {"status": r.status_code, "body": r.text}
data = r.json()
# API 사양에 맞게 파싱하세요
out_text = data["choices"][0]["message"]["content"]
usage = data.get("usage", {})
return dt, {"text": out_text, "usage": usage}, None
random.seed(2025)
rows = []
pairs = []
for item in dataset:
for eff in EFFORTS:
for trial in range(REPEATS):
pairs.append((item, eff, trial))
random.shuffle(pairs)
for item, eff, trial in pairs:
latency_ms, result, err = call_model(item["user"], eff)
row = {
"id": str(uuid.uuid4()),
"ts": datetime.utcnow().isoformat(),
"prompt_id": item["prompt_id"],
"bucket": item["bucket"],
"effort": eff,
"trial": trial,
"latency_ms": latency_ms,
"input_tokens": (result or {}).get("usage", {}).get("prompt_tokens"),
"output_tokens": (result or {}).get("usage", {}).get("completion_tokens"),
"reasoning_tokens": (result or {}).get("usage", {}).get("reasoning_tokens"),
"cost_usd": None, # 단가표로 후처리 계산
"response_text": (result or {}).get("text"),
"error": err
}
rows.append(row)
print(item["prompt_id"], eff, trial, "latency:", latency_ms, "ms", "ok" if not err else "ERR")
with open("runs.jsonl", "w", encoding="utf-8") as f:
for r in rows:
f.write(json.dumps(r, ensure_ascii=False) + "\n")
print("Saved runs.jsonl")
7) 자동 채점/품질 평가
-
정답형 문제: 정답 키와 딱 매칭(문자 정규화 후).
-
요약/분류: 정답 레이블/참조 요약과 비교(F1/ROUGE 등).
-
코딩: 테스트케이스 실행 통과율.
-
주관형 품질(선택): 블라인드 휴먼 평가 가이드 예시
-
관련성(0–5), 사실성(0–5), 구조/명료성(0–5), 과잉추론/환각(감점).
-
평가자는 effort 값을 모른 채로 평가(이중맹검).
-
8) 분석 계획(스텝별)
-
전처리: 프롬프트·effort·trial로 그룹핑, trial 평균(또는 중위) 계산.
-
기본 통계: effort별 평균 정확도/지연/비용 표·박스플롯.
-
쌍대 비교: 같은 프롬프트 기준으로
-
정량지표(정답률 등): 윌콕슨 부호순위 검정 또는 짝지은 t-검정
-
effort 3수준 동시 비교: 반복측정 ANOVA 또는 프리드먼 검정
-
-
효과크기 산출: Cliff’s delta / Cohen’s d.
-
신뢰구간: 부트스트랩(프롬프트 단위 리샘플) 95% CI.
-
난이도 교호작용: 버킷(A/B/C)별로 분리해서 동일 분석.
-
비용-효용 곡선: x축=비용(또는 토큰), y축=정확도(또는 승률)로 프런티어 시각화.
9) 결론 도출 체크리스트
-
High가 얼마나 더 정확한가? (절대/상대 개선 %)
-
그 개선이 통계적으로 유의 한가? (p값/CI)
-
추가 지연/비용 대비 가치 가 있는가? (업무 케이스별)
-
어떤 태스크 에서 High의 이득이 특히 큰가?
-
운영 권장치(예: “기본 Medium, 실패/난해 시 High 재시도”).
10) 리스크/주의 사항
-
일관된 시드·온도 로 공정성 확보.
-
프롬프트 누수 방지: 정답/해설을 시스템/컨텍스트에 포함하지 않기.
-
체인오브소트(내적추론) 노출 은 평가에 포함하되, 외부 배포/저장 정책 준수.
-
레이트 리밋 과 타임아웃 대비(백오프, 큐잉).
-
데이터 보호: 민감 데이터 비익명 전송 금지.
11) 산출물(템플릿)
-
runs.jsonl원시 로그 -
metrics.csv(프롬프트×effort 집계표) -
report.md(요약: 설정, 데이터, 결과, 결론, 한계)
12) 빠른 파일럿(30분 컷)
-
버킷 A에서 30문항만 추려서 Low/Med/High 각 2회씩.
-
정확도·지연·토큰만 우선 비교.
-
효과 있으면 전체 배치 확장.
필요하면 채점 스크립트/리포트 노트북(그래프 포함)도 만들어 드릴게요.
엔드포인트 사양이 약간 다르면(예: reasoning_effort 키 이름, usage 필드 명) 알려주시면 위 코드/스키마를 맞춰 드리겠습니다.
데이터가 없어 이런 걸 하는 라이브러리 없나?
LLM의 “Reasoning effort” 설정을 직접 비교해 보고 싶은데, 직접 데이터가 없어 번거로우시죠? 다행히, 이미 다양한 LLM 평가 라이브러리 가 있어 실험 기반 마련에 훨씬 수월하게 접근할 수 있어요.
주요 오픈소스 평가 라이브러리 소개
1. OpenAI Evals
- OpenAI에서 제공하는 공식 평가 프레임워크 로, 기본 벤치마크부터 커스텀 평가까지 지원합니다.GitHub
2. EleutherAI’s lm-evaluation-harness
- 수십여 개의 학술 벤치마크를 한 곳에서 실행 가능하고, 자체 API뿐 아니라 Hugging Face 모델, GPT‑NeoX, Megatron‑DeepSpeed 등 다양한 모델을 지원합니다.GitHub
3. DeepEval (Confident AI)
4. LightEval (Hugging Face)
- 다양한 벤치마크와 메트릭을 하나의 파이프라인으로 통합한 평가 툴킷으로, Transformers, Accelerate, Hugging Face Hub와 유기적으로 연동됩니다.Cohorte
5. Evidently (LLM-as-a-Judge)
- LLM을 평가자로 사용하는 “LLM‑as‑a‑Judge” 방식에 특화된 오픈소스 라이브러리로, 상세한 평가 기준과 대시보드를 제공하며 최근까지도 업데이트되고 있어요.Evidently AI+3
6. Verdict
- “LLM-as-a-Judge” 접근을 강화한 오픈소스 라이브러리로, 검증, 토론(debate), 집계(aggregation) 같은 모듈 기반 평가 파이프라인을 제공해 평가 신뢰도를 높입니다.arXiv
7. LLM Reasoners + AutoRace
- 체인 오브 추론(chain-of‑thought)처럼 단계별 reasoning을 자동으로 평가하는 프레임워크이며, reasoning 알고리즘을 모듈화한 라이브러리도 포함되어 있습니다.arXiv
8. OpenR
- 오픈소스 프레임워크로, 추론능력을 향상시키기 위한 강화학습(RL), test-time compute, 과정 감독(process supervision) 등을 통합한 플랫폼입니다.arXiv
추천 실험 활용 방안
| 목표 | 추천 라이브러리 | 활용법 |
|---|---|---|
| 기본적인 정확도·지연·토큰 비교 | lm-evaluation-harness , OpenAI Evals , LightEval | 벤치마크 태스크로 실험 자동화. |
| 출력의 질적 평가 (환각, 요약 퀄리티 등) | DeepEval , Evidently | Pytest 기반 테스트 또는 LLM-judge 활용. |
| Reasoning 과정 분석 | LLM Reasoners + AutoRace | reasoning 단계별 성능 정량화. |
| 고급 신뢰도 높은 평가 | Verdict | 평가자의 검증 및 합의 절차 포함. |
| 전체적인 reasoning pipeline 강화 | OpenR | test-time compute + RL 통합 실험 설계. |
요약
물리적인 데이터셋 없이도, 위와 같은 오픈소스 評價 라이브러리 들을 활용하면 “Reasoning effort” 설정에 따른 비교 실험 설계가 훨씬 간편해져요.
— 기본 비교 에는 lm-evaluation-harness 나 OpenAI Evals 추천
— 정성적 평가 엔 DeepEval, Evidently
— 특화된 reasoning 평가 엔 LLM Reasoners, Verdict
— 훈련/추론 중심 통합 실험 엔 OpenR
필요하시면 해당 라이브러리 중 하나를 골라 설치부터 실험 코드 예제 까지 도와 드릴게요!
할게 너무 많네 내가 실험하려는 모델은 gpt oss야 이미 한 사람 없을까? 우리 default 모델이 어떤 reasoning level이 default인지 간단히 실험해보고 싶어
맞아요—이미 해본 사람들이 있고, 문서에도 기본값은 보통 “medium” 으로 명시돼요. 예컨대 Databricks의 reasoning 모델 가이드에 reasoning_effort 값으로 "low", "medium"(default) , "high"가 나온다고 되어 있어요. 또 gpt-oss 소개/모델 카드에서도 세 레벨을 지원한다고 합니다. Databricks DocumentationOpenAI+1
다만 “우리 인프라/엔드포인트”의 실제 기본값 은 게이트웨이나 프록시에서 덮어쓸 수도 있으니, 초미니 검증 실험 으로 확정하는 게 제일 깔끔해요. 아래 그대로 돌려보면 5분 컷.
초간단 검증 플랜 (step-by-step)
-
테스트 프롬프트 고정
- 짧고 정답이 있는 문제 1–2개(예: 간단 산수, 상식 문제).
-
네 가지 호출 수행
-
A:
reasoning_effort미지정 (실제 기본값 체크) -
B:
"medium" -
C:
"low" -
D:
"high"
-
-
로그로 비교할 항목
-
latency_ms,usage.reasoning_tokens(지원 시),completion_tokens, 응답 길이/정확도. -
A와 B가 거의 동일하면 기본값=medium 으로 판단.
-
C < B < D 순으로
reasoning_tokens와 지연이 증가하면 파라미터 정상 동작.
-
cURL 예시 (엔드포인트만 바꾸면 됨)
A) 기본값 확인 (미지정)
curl -s -X POST "$ENDPOINT_URL" \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d '{
"model":"gpt-oss-20b",
"temperature":0.2,
"messages":[
{"role":"system","content":"You are a helpful assistant."},
{"role":"user","content":"12개의 사과를 3명에게 공평하게 나누면?"}
]
}'
B) Medium
-d '{
"model":"gpt-oss-20b",
"reasoning_effort":"medium",
"temperature":0.2,
"messages":[{"role":"system","content":"You are a helpful assistant."},
{"role":"user","content":"12개의 사과를 3명에게 공평하게 나누면?"}]
}'
C) Low / D) High는 "reasoning_effort" 값만 각각 "low", "high"로 바꾸면 됩니다.
파이썬 스니펫 (결과를 표로 비교)
import os, time, requests
URL=os.getenv("ENDPOINT_URL"); KEY=os.getenv("API_KEY")
headers={"Authorization":f"Bearer {KEY}","Content-Type":"application/json"}
payload_base={
"model":"gpt-oss-20b",
"temperature":0.2,
"messages":[
{"role":"system","content":"You are a helpful assistant."},
{"role":"user","content":"12개의 사과를 3명에게 공평하게 나누면?"}
]
}
def run(p):
t0=time.time()
r=requests.post(URL,headers=headers,json=p,timeout=120)
dt=round((time.time()-t0)*1000)
j=r.json()
u=j.get("usage",{})
return {
"effort":p.get("reasoning_effort","<unspecified>"),
"latency_ms":dt,
"reasoning_tokens":u.get("reasoning_tokens"),
"in_tokens":u.get("prompt_tokens"),
"out_tokens":u.get("completion_tokens"),
"text":j["choices"][0]["message"]["content"][:80]
}
rows=[]
for effort in [None,"medium","low","high"]:
p=dict(payload_base)
if effort is not None: p["reasoning_effort"]=effort
rows.append(run(p))
for r in rows:
print(r)
기대되는 판별 기준
-
effort=미지정과medium의 지연/토큰 수가 거의 동일 ⇒ 실 기본값은 medium. -
low는 더 짧고 토큰 적음,high는 더 길고 토큰 많음 ⇒ 레벨 스위치 정상.
“이미 한 사람 없을까?”에 대한 짧은 정리
- gpt-oss 출시 소개/모델 카드/플랫폼 문서에서 세 레벨(저·중·고) 지원과 기본값 medium 을 확인할 수 있습니다. 다만 배포 환경에 따라 기본값이 바뀔 수 있으니 위 미니 테스트로 우리 환경의 실제 기본값 을 확정하는 걸 권장합니다. OpenAI+1Databricks Documentation
원하시면 위 파이썬 스니펫을 CSV 저장/간단 그래프 까지 확장해 드릴게요.
너무 질문이 간단한거 아니야? 그건 문제가 안돼?
질문이 간단해도 기본값 확인 에는 아무 문제가 없어요.
여기서 우리가 보고 싶은 건 추론 정확도 가 아니라 reasoning_effort 설정에 따른 토큰·지연 시간 변화 이기 때문이에요.
왜 간단한 질문이 괜찮은지
-
목표가 “default 값이 무엇인지 확인” → 정답 여부보다 응답 패턴 이 핵심
reasoning_effort를 바꿨을 때 reasoning tokens 수나 latency가 달라지는지 확인하면 충분.
-
복잡한 질문은 오히려 변수 증가
- 난이도 높은 질문은 랜덤성·창의성 요소가 커서 비교 잡음(noise) 증가.
-
단순 질문도 reasoning 단계 수에 영향을 줄 수 있음
- High일 경우, 불필요하게도 더 장황하게 chain-of-thought을 생성할 수 있음 → 토큰/시간 차이 명확히 관찰 가능.
다만 이런 점 주의
-
너무 trivial해서 토큰·시간 차이가 거의 없는 경우 가 있을 수 있음.
→ 그럴 땐 난이도를 살짝 올린 짧은 논리 퍼즐/두세 단계 산수 문제 추천. -
같은 문제를 Low/Medium/High 모두 돌려야 비교 가능.
-
결과 해석 시 절대값 보다 상대 비교 를 보아야 함.
원하시면 제가 짧지만 reasoning 차이를 잘 드러내는 질문 세트 를 만들어 드릴 수 있어요.
예를 들어 5~10초 내로 풀 수 있지만, reasoning effort가 높으면 불필요하게 더 풀어쓰는 문제들요.
너무 trivial 하잖아. 유치원생 레벨
맞아요 😂 “12개의 사과 나누기”는 너무 유치원 급이라 High·Medium·Low 차이가 거의 안 날 수 있어요.
기본값 확인이 목적이라도, 짧지만 추론 단계를 2~4단계 거치게 만드는 질문 을 쓰면 차이가 훨씬 잘 드러납니다.
Reasoning 차이를 잘 드러내는 간단·짧은 예시
- 2단계 산수 퍼즐
3개의 상자에 각각 4, 7, 9개의 사탕이 있다. 상자 2에서 5개를 꺼내 상자 1에 넣고, 상자 3에서 2개를 꺼내 상자 2에 넣으면, 각 상자에는 몇 개씩 남는가?
- 간단 논리 문제
철수, 영희, 민수는 달리기 시합을 했다. 철수는 영희보다 빠르고, 영희는 민수보다 느리다. 누가 1등인가?
- 날짜 계산
오늘이 2025년 8월 11일이라면, 45일 후는 무슨 요일인가?
- 짧은 조건 추론
사과는 500g, 배는 700g이다. 사과 3개와 배 2개의 총 무게는 몇 kg인가?
- 패턴 완성
수열 2, 4, 8, 16, ? 다음 숫자는?
이유
-
이런 문제들은 High reasoning effort 일 때 불필요하게 세세한 Chain-of-Thought을 생성하므로 토큰·지연 차이가 확연.
-
Medium은 필요한 계산만, Low는 더 요약된 추론만 제공하는 경향.
-
정답이 명확해서 비교 시 혼동 없음.
원하시면 제가 이 문제들로 미니 default 값 검증 스크립트 를 만들어 드릴게요.
그러면 바로 한 번에 Low/Medium/High & 미지정 네 가지 호출을 비교할 수 있습니다.
그렇게 할까요?
응 스크립트 만들어줘
완료! 바로 돌릴 수 있는 미니 기본값 검증 스크립트 를 만들어 놨어요.
Download the script (gpt_oss_effort_smoke_test.py)
어떻게 쓰나요? (step-by-step)
- 환경변수 설정
export ENDPOINT_URL="https://<your-endpoint>"
export API_KEY="sk-..."
- 실행
python gpt_oss_effort_smoke_test.py --model gpt-oss-20b --repeats 2
-
각 프롬프트에 대해 effort 미지정 / low / medium / high 4가지로 호출합니다.
-
runs.jsonl(원시 로그)과summary.csv(요약 평균치)를 저장하고, 콘솔에 노력수준별 평균 latency / tokens 를 표로 출력합니다. -
usage.reasoning_tokens가 API에 있으면 같이 집계합니다.
해석 팁
-
콘솔 표에서
<unspecified>와medium이 거의 동일하면, 여러분 환경의 기본값=medium 일 가능성이 높습니다. -
보통 토큰/지연은 low < medium < high 순으로 증가합니다.
원하면 CSV → 그래프(plot) 버전도 바로 만들어 드릴게요.
python
# Create a ready-to-run Python script for mini default verification of reasoning_effort.
script = r'''#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
gpt_oss_effort_smoke_test.py
Mini smoke test to infer the default reasoning_effort for your GPT-OSS endpoint
and verify Low/Medium/High switches affect latency and tokens.
USAGE
-----
1) Set environment variables:
export ENDPOINT_URL="https://<your-endpoint>"
export API_KEY="sk-..."
2) (Optional) Choose model id via CLI: --model gpt-oss-20b
3) Run:
python gpt_oss_effort_smoke_test.py --repeats 2
This will:
- Send a compact set of short-but-not-trivial prompts.
- Call the endpoint with four variants per prompt:
A) effort unspecified (to detect actual DEFAULT)
B) effort = "medium"
C) effort = "low"
D) effort = "high"
- Log per-call latency, tokens, and short text preview.
- Save results to runs.jsonl and summary.csv.
- Print a quick table showing means by effort (latency / tokens).
Notes
-----
- The script assumes an OpenAI-compatible chat completions API that returns:
{ choices: [{ message: { content: str }}],
usage: { prompt_tokens, completion_tokens, reasoning_tokens? } }
- If your API names differ, adjust parse_usage().
- The script retries transient HTTP errors with backoff.
"""
import argparse
import json
import os
import random
import sys
import time
from datetime import datetime
from typing import Any, Dict, Optional
import requests
# ---------- Configurable defaults ----------
DEFAULT_MODEL = "gpt-oss-20b"
DEFAULT_TEMPERATURE = 0.2
DEFAULT_MAX_TOKENS = 512
DEFAULT_SEED = 42
DEFAULT_TIMEOUT = 60
DEFAULT_REPEATS = 2
PROMPTS = [
{
"id": "math_boxes",
"text": (
"3개의 상자에 각각 4, 7, 9개의 사탕이 있다. "
"상자 2에서 5개를 꺼내 상자 1에 넣고, 상자 3에서 2개를 꺼내 상자 2에 넣으면, "
"각 상자에는 몇 개씩 남는가? 최종 결과만 한국어로 말해줘."
),
},
{
"id": "logic_race",
"text": (
"철수, 영희, 민수는 달리기 시합을 했다. 철수는 영희보다 빠르고, "
"영희는 민수보다 느리다. 누가 1등인가? 한 단어로만 답해."
),
},
{
"id": "date_calc",
"text": (
"오늘이 2025년 8월 11일이라면, 45일 후는 무슨 요일인가?"
" 한국 시간대를 기준으로, 요일만 한국어로 답해."
),
},
{
"id": "weight_sum",
"text": (
"사과는 500g, 배는 700g이다. 사과 3개와 배 2개의 총 무게는 몇 kg인가? "
"소수점 둘째 자리까지 kg 단위로만 답해."
),
},
{
"id": "sequence",
"text": "수열 2, 4, 8, 16, ? 다음 숫자는? 숫자만."
},
]
EFFORT_LEVELS = [None, "medium", "low", "high"] # None = unspecified (to detect actual default)
# ---------- Helpers ----------
def env(key: str, default: Optional[str] = None) -> str:
val = os.getenv(key, default)
if val is None:
print(f"[ERROR] Missing environment variable: {key}", file=sys.stderr)
sys.exit(1)
return val
def parse_usage(obj: Dict[str, Any]) -> Dict[str, Optional[int]]:
usage = obj.get("usage", {}) or {}
return {
"prompt_tokens": usage.get("prompt_tokens"),
"completion_tokens": usage.get("completion_tokens"),
"reasoning_tokens": usage.get("reasoning_tokens"), # optional
"total_tokens": usage.get("total_tokens"),
}
def short(s: Optional[str], n: int = 80) -> str:
if not s:
return ""
s = s.replace("\n", " ").strip()
return s[:n] + ("…" if len(s) > n else "")
def post_with_retries(url: str, headers: Dict[str, str], payload: Dict[str, Any], timeout: int) -> requests.Response:
backoffs = [0.5, 1.0, 2.0]
for i, delay in enumerate([0.0] + backoffs):
if delay:
time.sleep(delay)
try:
resp = requests.post(url, headers=headers, json=payload, timeout=timeout)
if resp.status_code in (429, 500, 502, 503, 504):
# transient; let the loop retry
last = resp
continue
return resp
except requests.RequestException as e:
last = e # type: ignore[assignment]
# On failure after retries, raise or return last response if available
if isinstance(last, requests.Response):
return last
raise RuntimeError(f"HTTP error after retries: {last}")
def call_once(endpoint: str, api_key: str, model: str, user_text: str,
effort: Optional[str], temperature: float, max_tokens: int,
seed: Optional[int], timeout: int) -> Dict[str, Any]:
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": user_text},
]
body: Dict[str, Any] = {
"model": model,
"temperature": temperature,
"max_tokens": max_tokens,
"messages": messages,
}
if seed is not None:
body["seed"] = seed
if effort is not None:
body["reasoning_effort"] = effort
t0 = time.time()
resp = post_with_retries(endpoint, headers, body, timeout=timeout)
elapsed_ms = int(round((time.time() - t0) * 1000))
result: Dict[str, Any] = {
"status": resp.status_code,
"latency_ms": elapsed_ms,
"response_text": None,
"usage": None,
"raw": None,
"error": None,
}
try:
data = resp.json()
except Exception as e:
result["error"] = f"Non-JSON response: {resp.text[:200]} ({e})"
return result
result["raw"] = data
if resp.status_code != 200:
result["error"] = data
return result
try:
text = data["choices"][0]["message"]["content"]
except Exception:
text = None
result["response_text"] = text
result["usage"] = parse_usage(data)
return result
def mean(xs):
vals = [x for x in xs if isinstance(x, (int, float))]
return sum(vals)/len(vals) if vals else None
def safe_int(x):
return int(x) if isinstance(x, (int, float)) else None
# ---------- Main ----------
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--model", default=DEFAULT_MODEL)
parser.add_argument("--repeats", type=int, default=DEFAULT_REPEATS,
help="Number of repeats per (prompt, effort) pair")
parser.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT)
parser.add_argument("--temperature", type=float, default=DEFAULT_TEMPERATURE)
parser.add_argument("--max_tokens", type=int, default=DEFAULT_MAX_TOKENS)
parser.add_argument("--no_seed", action="store_true", help="Do not send seed")
args = parser.parse_args()
endpoint = env("ENDPOINT_URL")
api_key = env("API_KEY")
model = args.model
seed = None if args.no_seed else DEFAULT_SEED
random.seed(2025)
pairs = []
for p in PROMPTS:
for eff in EFFORT_LEVELS:
for trial in range(args.repeats):
pairs.append((p, eff, trial))
random.shuffle(pairs)
runs_path = "runs.jsonl"
summary_rows = []
n_ok = 0
n_err = 0
with open(runs_path, "w", encoding="utf-8") as f:
for p, eff, trial in pairs:
res = call_once(
endpoint=endpoint, api_key=api_key, model=model,
user_text=p["text"], effort=eff, temperature=args.temperature,
max_tokens=args.max_tokens, seed=seed, timeout=args.timeout
)
row = {
"ts": datetime.utcnow().isoformat(),
"prompt_id": p["id"],
"effort": eff if eff is not None else "<unspecified>",
"trial": trial,
"status": res["status"],
"latency_ms": res["latency_ms"],
"prompt_tokens": (res["usage"] or {}).get("prompt_tokens") if res["usage"] else None,
"completion_tokens": (res["usage"] or {}).get("completion_tokens") if res["usage"] else None,
"reasoning_tokens": (res["usage"] or {}).get("reasoning_tokens") if res["usage"] else None,
"total_tokens": (res["usage"] or {}).get("total_tokens") if res["usage"] else None,
"text_preview": short(res["response_text"], 120),
"error": res["error"],
}
f.write(json.dumps(row, ensure_ascii=False) + "\n")
if res["error"]:
n_err += 1
else:
n_ok += 1
summary_rows.append(row)
# Aggregate means by effort
by_effort = {}
for r in summary_rows:
e = r["effort"]
by_effort.setdefault(e, {"latency": [], "comp": [], "reason": [], "total": []})
if r["latency_ms"] is not None:
by_effort[e]["latency"].append(r["latency_ms"])
if r["completion_tokens"] is not None:
by_effort[e]["comp"].append(r["completion_tokens"])
if r["reasoning_tokens"] is not None:
by_effort[e]["reason"].append(r["reasoning_tokens"])
if r["total_tokens"] is not None:
by_effort[e]["total"].append(r["total_tokens"])
# Print quick table
print("\n=== Quick Means by Effort ===")
print("Effort | Latency(ms) | OutTokens | ReasoningTokens | TotalTokens")
print("-----------------+-------------+-----------+------------------+------------")
def fmt(x):
return f"{x:.1f}" if isinstance(x, (int, float)) else "-"
for e in ["<unspecified>", "low", "medium", "high"]:
mlat = mean(by_effort.get(e, {}).get("latency", []))
mout = mean(by_effort.get(e, {}).get("comp", []))
mrea = mean(by_effort.get(e, {}).get("reason", []))
mtot = mean(by_effort.get(e, {}).get("total", []))
print(f"{e:16} | {fmt(mlat):>11} | {fmt(mout):>9} | {fmt(mrea):>16} | {fmt(mtot):>10}")
# Save a compact CSV summary
csv_path = "summary.csv"
try:
import csv
with open(csv_path, "w", newline="", encoding="utf-8") as cf:
writer = csv.writer(cf)
writer.writerow(["effort","mean_latency_ms","mean_completion_tokens","mean_reasoning_tokens","mean_total_tokens"])
for e in ["<unspecified>", "low", "medium", "high"]:
mlat = mean(by_effort.get(e, {}).get("latency", []))
mout = mean(by_effort.get(e, {}).get("comp", []))
mrea = mean(by_effort.get(e, {}).get("reason", []))
mtot = mean(by_effort.get(e, {}).get("total", []))
writer.writerow([e, f"{mlat:.1f}" if mlat else "", f"{mout:.1f}" if mout else "", f"{mrea:.1f}" if mrea else "", f"{mtot:.1f}" if mtot else ""])
print(f"\nSaved: {csv_path}")
except Exception as e:
print(f"[WARN] CSV write failed: {e}")
print(f"\nSaved raw runs to: {runs_path}")
print(f"OK: {n_ok}, ERR: {n_err}")
print("\nInterpretation tip:")
print("- If '<unspecified>' and 'medium' are nearly identical across latency/tokens, your gateway default is likely 'medium'.")
print("- Expect tokens/latency trend: low < medium < high (if reasoning tokens are exposed, same order).")
if __name__ == "__main__":
main()
'''
with open('/mnt/data/gpt_oss_effort_smoke_test.py', 'w', encoding='utf-8') as f:
f.write(script)
print("Script written to /mnt/data/gpt_oss_effort_smoke_test.py")
네, 이 코드 그대로 /mnt/data/gpt_oss_effort_smoke_test.py 에 저장돼 있어서 바로 다운로드 후 실행하시면 돼요.
📥 gpt_oss_effort_smoke_test.py 받기
실행 방법 요약
- 환경변수 설정
export ENDPOINT_URL="https://<your-endpoint>"
export API_KEY="sk-..."
- 스크립트 실행
python gpt_oss_effort_smoke_test.py --model gpt-oss-20b --repeats 2
- 결과 확인
-
콘솔 표: effort별 평균 latency / tokens
-
파일:
-
runs.jsonl: 모든 호출 기록 -
summary.csv: 평균치 요약
-
팁
-
<unspecified>와medium이 비슷하면 default가 medium일 가능성 높음 -
low < medium < high순으로 reasoning_tokens와 latency가 증가하는지 확인
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
gpt_oss_effort_smoke_test.py
Mini smoke test to infer the default reasoning_effort for your GPT-OSS endpoint
and verify Low/Medium/High switches affect latency and tokens.
USAGE
-----
1) Set environment variables:
export ENDPOINT_URL="https://<your-endpoint>"
export API_KEY="sk-..."
2) (Optional) Choose model id via CLI: --model gpt-oss-20b
3) Run:
python gpt_oss_effort_smoke_test.py --repeats 2
This will:
- Send a compact set of short-but-not-trivial prompts.
- Call the endpoint with four variants per prompt:
A) effort unspecified (to detect actual DEFAULT)
B) effort = "medium"
C) effort = "low"
D) effort = "high"
- Log per-call latency, tokens, and short text preview.
- Save results to runs.jsonl and summary.csv.
- Print a quick table showing means by effort (latency / tokens).
Notes
-----
- The script assumes an OpenAI-compatible chat completions API that returns:
{ choices: [{ message: { content: str }}],
usage: { prompt_tokens, completion_tokens, reasoning_tokens? } }
- If your API names differ, adjust parse_usage().
- The script retries transient HTTP errors with backoff.
"""
import argparse
import json
import os
import random
import sys
import time
from datetime import datetime
from typing import Any, Dict, Optional
import requests
# ---------- Configurable defaults ----------
DEFAULT_MODEL = "gpt-oss-20b"
DEFAULT_TEMPERATURE = 0.2
DEFAULT_MAX_TOKENS = 512
DEFAULT_SEED = 42
DEFAULT_TIMEOUT = 60
DEFAULT_REPEATS = 2
PROMPTS = [
{
"id": "math_boxes",
"text": (
"3개의 상자에 각각 4, 7, 9개의 사탕이 있다. "
"상자 2에서 5개를 꺼내 상자 1에 넣고, 상자 3에서 2개를 꺼내 상자 2에 넣으면, "
"각 상자에는 몇 개씩 남는가? 최종 결과만 한국어로 말해줘."
),
},
{
"id": "logic_race",
"text": (
"철수, 영희, 민수는 달리기 시합을 했다. 철수는 영희보다 빠르고, "
"영희는 민수보다 느리다. 누가 1등인가? 한 단어로만 답해."
),
},
{
"id": "date_calc",
"text": (
"오늘이 2025년 8월 11일이라면, 45일 후는 무슨 요일인가?"
" 한국 시간대를 기준으로, 요일만 한국어로 답해."
),
},
{
"id": "weight_sum",
"text": (
"사과는 500g, 배는 700g이다. 사과 3개와 배 2개의 총 무게는 몇 kg인가? "
"소수점 둘째 자리까지 kg 단위로만 답해."
),
},
{
"id": "sequence",
"text": "수열 2, 4, 8, 16, ? 다음 숫자는? 숫자만."
},
]
EFFORT_LEVELS = [None, "medium", "low", "high"] # None = unspecified (to detect actual default)
# ---------- Helpers ----------
def env(key: str, default: Optional[str] = None) -> str:
val = os.getenv(key, default)
if val is None:
print(f"[ERROR] Missing environment variable: {key}", file=sys.stderr)
sys.exit(1)
return val
def parse_usage(obj: Dict[str, Any]) -> Dict[str, Optional[int]]:
usage = obj.get("usage", {}) or {}
return {
"prompt_tokens": usage.get("prompt_tokens"),
"completion_tokens": usage.get("completion_tokens"),
"reasoning_tokens": usage.get("reasoning_tokens"), # optional
"total_tokens": usage.get("total_tokens"),
}
def short(s: Optional[str], n: int = 80) -> str:
if not s:
return ""
s = s.replace("\n", " ").strip()
return s[:n] + ("…" if len(s) > n else "")
def post_with_retries(url: str, headers: Dict[str, str], payload: Dict[str, Any], timeout: int) -> requests.Response:
backoffs = [0.5, 1.0, 2.0]
for i, delay in enumerate([0.0] + backoffs):
if delay:
time.sleep(delay)
try:
resp = requests.post(url, headers=headers, json=payload, timeout=timeout)
if resp.status_code in (429, 500, 502, 503, 504):
# transient; let the loop retry
last = resp
continue
return resp
except requests.RequestException as e:
last = e # type: ignore[assignment]
# On failure after retries, raise or return last response if available
if isinstance(last, requests.Response):
return last
raise RuntimeError(f"HTTP error after retries: {last}")
def call_once(endpoint: str, api_key: str, model: str, user_text: str,
effort: Optional[str], temperature: float, max_tokens: int,
seed: Optional[int], timeout: int) -> Dict[str, Any]:
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": user_text},
]
body: Dict[str, Any] = {
"model": model,
"temperature": temperature,
"max_tokens": max_tokens,
"messages": messages,
}
if seed is not None:
body["seed"] = seed
if effort is not None:
body["reasoning_effort"] = effort
t0 = time.time()
resp = post_with_retries(endpoint, headers, body, timeout=timeout)
elapsed_ms = int(round((time.time() - t0) * 1000))
result: Dict[str, Any] = {
"status": resp.status_code,
"latency_ms": elapsed_ms,
"response_text": None,
"usage": None,
"raw": None,
"error": None,
}
try:
data = resp.json()
except Exception as e:
result["error"] = f"Non-JSON response: {resp.text[:200]} ({e})"
return result
result["raw"] = data
if resp.status_code != 200:
result["error"] = data
return result
try:
text = data["choices"][0]["message"]["content"]
except Exception:
text = None
result["response_text"] = text
result["usage"] = parse_usage(data)
return result
def mean(xs):
vals = [x for x in xs if isinstance(x, (int, float))]
return sum(vals)/len(vals) if vals else None
def safe_int(x):
return int(x) if isinstance(x, (int, float)) else None
# ---------- Main ----------
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--model", default=DEFAULT_MODEL)
parser.add_argument("--repeats", type=int, default=DEFAULT_REPEATS,
help="Number of repeats per (prompt, effort) pair")
parser.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT)
parser.add_argument("--temperature", type=float, default=DEFAULT_TEMPERATURE)
parser.add_argument("--max_tokens", type=int, default=DEFAULT_MAX_TOKENS)
parser.add_argument("--no_seed", action="store_true", help="Do not send seed")
args = parser.parse_args()
endpoint = env("ENDPOINT_URL")
api_key = env("API_KEY")
model = args.model
seed = None if args.no_seed else DEFAULT_SEED
random.seed(2025)
pairs = []
for p in PROMPTS:
for eff in EFFORT_LEVELS:
for trial in range(args.repeats):
pairs.append((p, eff, trial))
random.shuffle(pairs)
runs_path = "runs.jsonl"
summary_rows = []
n_ok = 0
n_err = 0
with open(runs_path, "w", encoding="utf-8") as f:
for p, eff, trial in pairs:
res = call_once(
endpoint=endpoint, api_key=api_key, model=model,
user_text=p["text"], effort=eff, temperature=args.temperature,
max_tokens=args.max_tokens, seed=seed, timeout=args.timeout
)
row = {
"ts": datetime.utcnow().isoformat(),
"prompt_id": p["id"],
"effort": eff if eff is not None else "<unspecified>",
"trial": trial,
"status": res["status"],
"latency_ms": res["latency_ms"],
"prompt_tokens": (res["usage"] or {}).get("prompt_tokens") if res["usage"] else None,
"completion_tokens": (res["usage"] or {}).get("completion_tokens") if res["usage"] else None,
"reasoning_tokens": (res["usage"] or {}).get("reasoning_tokens") if res["usage"] else None,
"total_tokens": (res["usage"] or {}).get("total_tokens") if res["usage"] else None,
"text_preview": short(res["response_text"], 120),
"error": res["error"],
}
f.write(json.dumps(row, ensure_ascii=False) + "\n")
if res["error"]:
n_err += 1
else:
n_ok += 1
summary_rows.append(row)
# Aggregate means by effort
by_effort = {}
for r in summary_rows:
e = r["effort"]
by_effort.setdefault(e, {"latency": [], "comp": [], "reason": [], "total": []})
if r["latency_ms"] is not None:
by_effort[e]["latency"].append(r["latency_ms"])
if r["completion_tokens"] is not None:
by_effort[e]["comp"].append(r["completion_tokens"])
if r["reasoning_tokens"] is not None:
by_effort[e]["reason"].append(r["reasoning_tokens"])
if r["total_tokens"] is not None:
by_effort[e]["total"].append(r["total_tokens"])
# Print quick table
print("\n=== Quick Means by Effort ===")
print("Effort | Latency(ms) | OutTokens | ReasoningTokens | TotalTokens")
print("-----------------+-------------+-----------+------------------+------------")
def fmt(x):
return f"{x:.1f}" if isinstance(x, (int, float)) else "-"
for e in ["<unspecified>", "low", "medium", "high"]:
mlat = mean(by_effort.get(e, {}).get("latency", []))
mout = mean(by_effort.get(e, {}).get("comp", []))
mrea = mean(by_effort.get(e, {}).get("reason", []))
mtot = mean(by_effort.get(e, {}).get("total", []))
print(f"{e:16} | {fmt(mlat):>11} | {fmt(mout):>9} | {fmt(mrea):>16} | {fmt(mtot):>10}")
# Save a compact CSV summary
csv_path = "summary.csv"
try:
import csv
with open(csv_path, "w", newline="", encoding="utf-8") as cf:
writer = csv.writer(cf)
writer.writerow(["effort","mean_latency_ms","mean_completion_tokens","mean_reasoning_tokens","mean_total_tokens"])
for e in ["<unspecified>", "low", "medium", "high"]:
mlat = mean(by_effort.get(e, {}).get("latency", []))
mout = mean(by_effort.get(e, {}).get("comp", []))
mrea = mean(by_effort.get(e, {}).get("reason", []))
mtot = mean(by_effort.get(e, {}).get("total", []))
writer.writerow([e, f"{mlat:.1f}" if mlat else "", f"{mout:.1f}" if mout else "", f"{mrea:.1f}" if mrea else "", f"{mtot:.1f}" if mtot else ""])
print(f"\nSaved: {csv_path}")
except Exception as e:
print(f"[WARN] CSV write failed: {e}")
print(f"\nSaved raw runs to: {runs_path}")
print(f"OK: {n_ok}, ERR: {n_err}")
print("\nInterpretation tip:")
print("- If '<unspecified>' and 'medium' are nearly identical across latency/tokens, your gateway default is likely 'medium'.")
print("- Expect tokens/latency trend: low < medium < high (if reasoning tokens are exposed, same order).")
if __name__ == "__main__":
main()
이 스크립트는 그대로 실행해도 gpt-oss 기본 reasoning_effort 값을 빠르게 확인할 수 있게 잘 짜여 있어요.
요약
-
프롬프트 : 너무 trivial하지 않고 reasoning 차이를 드러낼 수 있는 5문항.
-
비교 모드 :
-
<unspecified>(미지정 → default 값 확인) -
low,medium,high
-
-
측정 지표 :
-
latency(ms)
-
prompt/completion/reasoning/total tokens
-
-
출력 :
-
콘솔에 평균 표
-
runs.jsonl(원시 로그) -
summary.csv(평균 요약)
-
실행 방법
export ENDPOINT_URL="https://<your-endpoint>"
export API_KEY="sk-..."
python gpt_oss_effort_smoke_test.py --model gpt-oss-20b --repeats 2
결과 해석
-
<unspecified>와medium이 거의 동일 → default = medium 가능성 큼 -
reasoning_tokens / latency 순서:
low < medium < high이면 정상 동작
원하면 여기에 CSV 결과를 시각화하는 플롯 코드 를 붙여서, 한눈에 비교할 수 있게 만들어 드릴까요?