Skip to content

Split View: 추론 강도는 모델 선택이 아니라 요청 단위 배포 파라미터입니다

✨ Learn with Quiz
|

추론 강도는 모델 선택이 아니라 요청 단위 배포 파라미터입니다

같은 모델인데 점수가 세 개 실려 있습니다

새 모델이 나와서 벤치마크 결과를 확인하러 갑니다. 그런데 점수가 하나가 아닙니다. 세 개입니다. 어느 숫자를 슬랙에 올려야 할까요.

이 상황은 이제 예외가 아니라 기본값에 가깝습니다. 추론 강도를 조절할 수 있는 모델이 늘면서, 모델 하나가 점 하나가 아니라 곡선 하나로 발표되기 시작했습니다. 그리고 이 변화는 발표 형식의 변화가 아니라 배포 설계의 변화입니다.

예전에는 모델을 고르는 것이 결정의 끝이었습니다. 어떤 모델을 쓸지 정하고 나면 성능과 비용이 함께 정해졌습니다. 지금은 모델을 고른 뒤에도 결정이 하나 남아 있고, 그 결정이 성능과 비용을 양쪽으로 크게 흔듭니다. 이걸 설정 파일에 상수로 박아 두면 그 흔들림을 전부 포기하는 셈입니다.

ARC 결과 페이지가 실제로 알려 주는 것

ARC Prize의 DeepSeek V4 Flash 0731 결과 페이지를 보겠습니다. 2026년 7월 31일에 공개된 모델이고, 추론 강도 세 단계가 각각 측정되어 있습니다.

추론 강도ARC-AGI-1ARC-AGI-2
Max89.0%61.4%
High87.0%56.0%
Low84.0%46.0%

비용은 최대 강도 기준으로만 표기되어 있습니다. ARC-AGI-1 준비공개 평가에서 과제당 0.02달러, ARC-AGI-2에서 과제당 0.04달러입니다.

강도가 사 주는 것은 과제 난이도에 따라 세 배 차이 납니다

이 표에서 바로 계산되는 것이 하나 있습니다. 강도를 최저에서 최대로 올렸을 때의 이득입니다.

ARC-AGI-1에서는 84.0에서 89.0으로 5.0퍼센트포인트가 오릅니다. ARC-AGI-2에서는 46.0에서 61.4로 15.4퍼센트포인트가 오릅니다. 같은 다이얼을 같은 만큼 돌렸는데 어려운 쪽에서 세 배 넘게 벌었습니다.

방향을 뒤집어 읽으면 더 실용적입니다. 쉬운 과제에서 최대 강도를 쓰는 것은 대체로 낭비입니다. ARC-AGI-1에서 최저 강도는 최대 강도의 94퍼센트 수준을 이미 내고 있습니다. 반대로 어려운 과제에서 최저 강도로 돌리는 것은 성능을 크게 버리는 선택입니다.

그래서 "이 모델을 어떤 강도로 쓸 것인가"라는 질문은 애초에 잘못된 형태입니다. 강도는 모델의 속성이 아니라 과제의 속성에 맞춰 정할 값입니다.

요청 단위로 정한다는 것의 의미

실무에서 이 결론은 설정 파일 한 줄이 아니라 라우팅 로직이 됩니다. 들어온 요청마다 난이도를 추정하고 강도를 고릅니다.

난이도 추정이 완벽할 필요는 없습니다. 대부분의 서비스에서는 아주 거친 신호로도 충분합니다. 입력 길이, 요구된 단계 수, 과거 같은 유형에서의 실패율, 사용자가 명시한 긴급도 같은 것들입니다.

다만 이 추정을 한 번에 맞히려 들면 곧 한계에 부딪힙니다. 난이도를 예측하는 분류기를 붙이는 순간 그 분류기 자체가 유지 대상이 되고, 분포가 바뀔 때마다 다시 학습해야 합니다. 더 튼튼한 방법은 추정하지 않고 틀렸을 때 올리는 것입니다. 예측 대신 관측을 쓰는 쪽이 거의 언제나 더 오래 갑니다.

사다리는 검증자가 있을 때만 성립합니다

"""단계 상향 사다리: 싸게 먼저 시도하고, 틀린 게 확인되면 올린다."""
from typing import Callable, Sequence

LADDER = ("low", "high", "max")


def solve_with_escalation(
    task,
    run: Callable[[object, str], object],       # (task, effort) -> answer
    verify: Callable[[object, object], bool],   # (task, answer) -> 통과 여부
    ladder: Sequence[str] = LADDER,
):
    """검증자가 통과시킬 때까지 강도를 올린다. 사용한 강도를 함께 반환한다."""
    attempts = []
    for effort in ladder:
        answer = run(task, effort)
        ok = verify(task, answer)
        attempts.append((effort, ok))
        if ok:
            return answer, effort, attempts
    return answer, ladder[-1], attempts   # 끝까지 실패하면 최종 시도를 돌려준다


def expected_cost(p_pass_by_effort: dict, unit_cost: dict, ladder=LADDER) -> float:
    """사다리의 기대 비용. 낮은 단계 통과율이 높을수록 평균이 내려간다."""
    total, reach = 0.0, 1.0
    for effort in ladder:
        total += reach * unit_cost[effort]
        reach *= 1 - p_pass_by_effort[effort]
    return total


p = {"low": 0.80, "high": 0.60, "max": 0.50}     # 각 단계에 도달했을 때의 통과율
cost = {"low": 0.004, "high": 0.010, "max": 0.020}
print(round(expected_cost(p, cost), 5))          # 최대 강도만 쓰는 경우와 비교해 볼 것

이 구조의 전제는 verify입니다. 검증이 불가능하면 사다리는 성립하지 않고, 남는 선택지는 처음부터 높은 강도로 돌리는 것뿐입니다. 그래서 강도 최적화를 하고 싶다면 먼저 물어야 할 질문은 "강도를 어떻게 고를까"가 아니라 틀렸다는 걸 싸게 알아낼 방법이 있는가입니다.

한 가지 더 주의할 점은 지연 시간입니다. 사다리는 평균 비용을 낮추지만 최악의 경우 지연을 늘립니다. 세 단계를 다 밟은 요청은 한 번에 최대 강도로 돌린 요청보다 늦게 끝납니다. 그래서 사용자가 기다리는 동기 경로에서는 사다리를 두 단계로 줄이거나, 낮은 단계를 먼저 스트리밍으로 보여 주고 상향 결과로 교체하는 식의 설계가 필요합니다. 배치 작업이라면 이 고민은 하지 않아도 됩니다.

검증자를 만들 수 없을 때의 대체 신호

완전한 검증자가 없어도 부분적인 신호는 대개 만들 수 있습니다.

코드 생성이라면 컴파일과 테스트 실행이 그대로 검증자입니다. 구조화된 출력이라면 스키마 검증과 참조 무결성 검사가 상당 부분을 잡습니다. 검색 기반 답변이라면 인용된 문장이 원문에 실제로 존재하는지를 문자열 대조로 확인할 수 있습니다.

이런 것조차 없는 자유 서술 과제라면 신호를 결과가 아니라 과정에서 찾습니다. 같은 입력을 낮은 강도로 두 번 돌려 답이 서로 크게 다르면, 그 요청은 이 모델에게 어려운 요청일 가능성이 높습니다. 두 번 돌리는 비용이 한 단계 올리는 비용보다 싼 구간에서는 이 방법이 실용적입니다.

여기서 답이 다르다는 판정을 다시 모델에게 맡기면 비용 이점이 사라집니다. 문장 임베딩의 코사인 거리나, 답에서 추출한 핵심 숫자와 고유명사의 집합 비교처럼 값싼 대조로 충분한 경우가 많습니다. 검증자는 정확할 필요가 없고 싸면서 한쪽으로만 틀리면 됩니다. 놓치는 실패가 있어도 되지만, 맞은 답을 틀렸다고 판정하는 일은 드물어야 사다리가 낭비 없이 돕니다.

이 페이지에서 확인할 수 없는 것

정직하게 짚어 둘 부분이 있습니다. 앞의 표에는 강도별 점수가 세 줄 있지만, 비용은 최대 강도 기준 한 줄만 있습니다. 낮은 강도의 과제당 비용은 이 페이지에서 확인되지 않습니다.

그래서 비용당 한계 정확도를 이 데이터만으로 완전히 계산할 수는 없습니다. 위 코드의 단가는 구조를 보여 주기 위한 예시일 뿐이고, 실제 값은 각자 자기 트래픽에서 재야 합니다. 그리고 이건 이 페이지의 결함이라기보다 지금 업계 전반의 발표 관행입니다. 강도별 점수는 늘어났는데 강도별 비용은 아직 같이 나오지 않습니다.

측정은 어렵지 않습니다. 자기 평가셋 100건을 세 강도로 각각 돌리고, 정확도와 실제 청구 토큰을 함께 기록하면 곡선이 나옵니다. 이 실험은 반나절이면 끝나고, 이후 모든 강도 결정의 근거가 됩니다.

한 가지 더 유의할 것은 벤치마크의 난이도 분포와 우리 트래픽의 난이도 분포가 다르다는 점입니다. ARC 과제는 의도적으로 어렵게 설계된 문제 모음이고, 실제 서비스 트래픽은 대체로 쉬운 요청이 압도적으로 많은 긴 꼬리 분포입니다. 그래서 위 표에서 얻을 교훈은 "우리도 최대 강도를 써야 한다"가 아니라, 난이도 구간마다 다이얼의 값어치가 다르다는 사실 자체입니다.

배포 파라미터로 다루려면 남겨야 할 것들

마지막은 기록입니다. 강도를 요청 단위로 정하기 시작하면, 강도를 로그에 남기지 않는 순간 모든 지표가 해석 불가능해집니다.

최소한 이 네 가지를 응답과 함께 저장합니다. 실제 사용된 강도, 사다리에서 시도한 횟수, 각 시도의 검증 결과, 그리고 총 소비 토큰입니다. 여기에 요청을 어느 난이도 구간으로 분류했는지까지 남기면 나중에 라우팅 규칙을 데이터로 조정할 수 있습니다. 이게 있으면 나중에 "지난주 대비 정확도가 올랐는데 비용도 올랐다"는 상황에서 원인을 즉시 분리할 수 있습니다. 없으면 모델 탓인지 라우팅 탓인지 트래픽 구성 변화 탓인지 영원히 알 수 없습니다.

처음의 질문으로 돌아가면, 슬랙에 올려야 할 숫자는 셋 중 하나가 아닙니다. 세 개를 다 올리고 우리 과제가 어느 쪽에 가까운지를 같이 적는 것이 맞습니다.

참고 자료

Reasoning Effort Is Not a Model Choice but a Per-Request Deployment Parameter

Same model, three scores on the page

A new model comes out and you go to check the benchmark results. But there is not one score. There are three. Which number should you post in Slack?

This situation is now closer to the default than to the exception. As models with adjustable reasoning effort proliferate, a single model has started to be published as a curve rather than a point. And this change is not a change in publication format but a change in deployment design.

It used to be that choosing the model was the end of the decision. Once you decided which model to use, performance and cost were fixed together. Now one decision remains even after choosing the model, and that decision swings performance and cost hard in both directions. Hard-coding it as a constant in a config file means giving up that whole swing.

What the ARC results page actually tells you

Let us look at the ARC Prize results page for DeepSeek V4 Flash 0731. The model was released on 31 July 2026, and three levels of reasoning effort are each measured.

Reasoning effortARC-AGI-1ARC-AGI-2
Max89.0%61.4%
High87.0%56.0%
Low84.0%46.0%

Cost is listed only on a max-effort basis: $0.02 per task on the ARC-AGI-1 semi-private evaluation, and $0.04 per task on ARC-AGI-2.

What effort buys differs threefold with task difficulty

There is one thing computed directly off this table: the gain from raising effort from lowest to maximum.

On ARC-AGI-1 it goes from 84.0 to 89.0, a rise of 5.0 percentage points. On ARC-AGI-2 it goes from 46.0 to 61.4, a rise of 15.4 percentage points. The same dial turned the same amount earned more than three times as much on the harder side.

Reading it the other way round is more useful. Using maximum effort on easy tasks is mostly waste. On ARC-AGI-1 the lowest effort already delivers about 94 percent of what maximum effort does. Conversely, running the hard tasks at lowest effort is a choice that throws away a lot of performance.

So the question "what effort should we use this model at?" is malformed from the start. Effort is not a property of the model but a value to be set to match the properties of the task.

What deciding per request means

In practice this conclusion becomes routing logic, not a line in a config file. For each incoming request you estimate difficulty and choose an effort level.

The difficulty estimate does not have to be perfect. For most services even very coarse signals are enough: input length, the number of steps required, the failure rate on past requests of the same type, the urgency the user stated.

That said, trying to get this estimate right in one shot hits a wall soon. The moment you attach a classifier that predicts difficulty, that classifier becomes something to maintain, and it has to be retrained whenever the distribution shifts. The sturdier method is not to estimate but to escalate when you were wrong. Using observation instead of prediction almost always lasts longer.

The ladder only holds when there is a verifier

"""Escalation ladder: try cheap first, and raise the level once it is confirmed wrong."""
from typing import Callable, Sequence

LADDER = ("low", "high", "max")


def solve_with_escalation(
    task,
    run: Callable[[object, str], object],       # (task, effort) -> answer
    verify: Callable[[object, object], bool],   # (task, answer) -> pass or not
    ladder: Sequence[str] = LADDER,
):
    """Raise effort until the verifier passes it. Also return the effort that was used."""
    attempts = []
    for effort in ladder:
        answer = run(task, effort)
        ok = verify(task, answer)
        attempts.append((effort, ok))
        if ok:
            return answer, effort, attempts
    return answer, ladder[-1], attempts   # if it fails all the way, return the final attempt


def expected_cost(p_pass_by_effort: dict, unit_cost: dict, ladder=LADDER) -> float:
    """Expected cost of the ladder. The higher the pass rate at low levels, the lower the average."""
    total, reach = 0.0, 1.0
    for effort in ladder:
        total += reach * unit_cost[effort]
        reach *= 1 - p_pass_by_effort[effort]
    return total


p = {"low": 0.80, "high": 0.60, "max": 0.50}     # pass rate given that this level was reached
cost = {"low": 0.004, "high": 0.010, "max": 0.020}
print(round(expected_cost(p, cost), 5))          # compare against using max effort only

The premise of this structure is verify. If verification is impossible the ladder does not hold, and the only remaining option is to run at high effort from the start. So if you want to optimize effort, the first question to ask is not "how do we choose the effort level" but is there a cheap way to find out that we were wrong.

One more thing to watch is latency. The ladder lowers average cost but raises worst-case latency. A request that walked all three levels finishes later than a request run at maximum effort in one go. So on a synchronous path where a user is waiting, you need a design that cuts the ladder to two levels, or streams the low level first and replaces it with the escalated result. For batch work you do not need to worry about this.

Substitute signals when you cannot build a verifier

Even without a complete verifier, partial signals can usually be built.

For code generation, compilation and test execution are the verifier as-is. For structured output, schema validation and referential integrity checks catch a good portion. For retrieval-grounded answers, you can confirm by string comparison whether a quoted sentence actually exists in the source.

For free-form tasks where even these are unavailable, look for the signal in the process rather than the result. Run the same input twice at low effort, and if the answers differ substantially, that request is likely a hard one for this model. In the range where running twice is cheaper than stepping up one level, this method is practical.

If you hand the judgement of "the answers differ" back to a model, the cost advantage disappears. Cheap comparisons are often enough — cosine distance between sentence embeddings, or set comparison over the key numbers and proper nouns extracted from the answers. A verifier does not need to be accurate; it needs to be cheap and wrong in only one direction. It is fine for it to miss failures, but it should rarely judge a correct answer to be wrong, or the ladder does not run without waste.

What this page cannot tell you

There is a part worth flagging honestly. The table above has three rows of per-effort scores, but cost has only one row, on a max-effort basis. The per-task cost at lower effort levels is not confirmable from this page.

So marginal accuracy per cost cannot be fully computed from this data alone. The unit prices in the code above are only an example to show the structure, and the real values have to be measured by each team on its own traffic. And this is less a defect of this page than the current publication convention across the industry. Per-effort scores have multiplied while per-effort costs still do not come out alongside them.

Measuring is not hard. Run 100 items from your own evaluation set at each of the three efforts, record accuracy together with actually billed tokens, and the curve appears. This experiment takes half a day and becomes the basis for every effort decision afterwards.

One more thing to keep in mind is that the difficulty distribution of a benchmark differs from the difficulty distribution of your traffic. ARC tasks are a collection of problems deliberately designed to be hard, whereas real service traffic is generally a long-tail distribution overwhelmingly dominated by easy requests. So the lesson to take from the table above is not "we should use maximum effort too" but the fact itself that the dial is worth different amounts in different difficulty bands.

What you have to record to treat it as a deployment parameter

The last piece is logging. Once you start setting effort per request, the moment you fail to log the effort, every metric becomes uninterpretable.

Store at minimum these four things alongside the response: the effort actually used, the number of attempts on the ladder, the verification result of each attempt, and the total tokens consumed. Add which difficulty band the request was classified into and you can later tune the routing rules with data. With this you can immediately separate the cause in a situation like "accuracy went up versus last week but so did cost." Without it, you will never know whether it was the model, the routing, or a shift in traffic composition.

Returning to the opening question: the number to post in Slack is not one of the three. Posting all three and noting which one our tasks resemble is the right answer.

References

  • DeepSeek V4 Flash 0731 — ARC Prize results page — the three rows of per-effort scores, the per-task cost on a max-effort basis, and the model release date are what is on this page. The 5.0 and 15.4 percentage point figures in the body are values obtained by subtraction from that table.
  • ARC Prize — has the distinction between semi-private and public evaluation and an explanation of the evaluation method.
  • The pass rates and unit prices in the ladder code are example numbers for explaining the structure, not values taken from the ARC page.
  • Related post on this blog: The claim of 100x cheaper is true only when the task was narrowed