들어가며
지난 몇 달 동안 제가 참여한 프로젝트에 LLM 에이전트를 적용했습니다. 사용자가 자연어로 요청하면 에이전트가 필요한 조회 도구를 선택하고, 결과를 확인한 뒤 답을 만들거나 추가 판단이 필요할 때 다시 질문하는 구조였습니다. 이 글은 그 과정에서 모델을 감싸 도구 호출과 대화 흐름을 제어한 agent harness를 만들고, 여러 모델을 교체해 보며 얻은 경험을 정리한 글입니다.
처음에는 harness를 ‘어떤 모델을 연결해도 안정적으로 동작하게 만드는 범용 제어 계층’이라고 생각했습니다. 그래서 로컬 모델이 스키마를 벗어나거나 도구 호출을 틀릴 때마다 파서, 재시도, 추론 규칙을 추가했습니다. 당시에는 실패를 한 건씩 막아냈으므로 합리적인 대응처럼 보였습니다.
하지만 같은 harness에 모델만 바꾸어 반복 실행하자 결과가 달라졌습니다. 로컬 오픈 모델들은 전체 과업을 끝내기 전에 형식 오류, 잘못된 인자, 빈 응답, 과도한 지연이 계속 발생했습니다. 반면 gpt-5.6은 같은 도구와 같은 요청에서 가장 안정적으로 끝까지 수행했습니다. 차이는 근소하지 않았고, 실제 사용 가능 여부가 갈릴 정도로 압도적이었습니다.
|
보상적 harness는 범용 안전망이 아니라 관찰한 모델의 실패 분포에 맞춘 코드였습니다. 모델이 좋아지면 그 코드는 필요 없어지고, 오히려 이전 모델에 맞춘 보상이 새 모델의 단순한 실행 경로를 방해할 수 있습니다. |
|---|
그렇다고 harness 전체가 사라지는 것은 아닙니다. 모델의 부족함을 메우는 보상적(compensatory) harness와, 승인·권한·예산·감사처럼 시스템이 책임져야 하는 harness를 구분해야 합니다. 이 글에서는 제가 실제로 만든 보상 코드를 되짚고, 왜 좋은 모델을 선택하는 일이 코드 바깥의 비용 문제가 아니라 아키텍처 결정인지 설명합니다.
1. 로컬 모델을 선택한 배경과 harness의 시작
프로젝트 초기에 로컬 모델을 우선 검토한 이유는 명확했습니다. 업무 데이터가 외부로 나가지 않는 구성이 필요했고, 이미 사용할 수 있는 사내 추론 서버가 있었으며, 호출 비용도 줄일 수 있었습니다. 모델과 provider를 설정으로 교체할 수 있게 만들면 애플리케이션 코드는 그대로 유지할 수 있다고 판단했습니다.
외부에 공개할 수 없는 프로젝트 식별자를 제외하고 단순화하면 설정은 다음과 같은 형태였습니다.
provider와 모델을 교체하기 위한 설정
agent:
provider: ${LLM_PROVIDER:local}
endpoint: ${LLM_ENDPOINT}
model: ${LLM_MODEL}
capabilities:
tool-calling: true
structured-output: false
설정만 보면 모델 교체는 문자열 하나를 바꾸는 일처럼 보입니다. 실제로는 그렇지 않았습니다. 첫 번째 로컬 모델은 스키마에 선언한 필드 이름을 다른 단어로 바꾸었고, 두 번째 모델은 필요한 조회를 마치기 전에 답을 만들거나 응답이 오래 지연되었습니다. 다른 모델은 도구 인자를 틀리거나 내용과 도구 호출이 모두 없는 빈 턴을 반환했습니다.
실패가 날 때마다 저는 모델이 다시 성공할 수 있도록 코드를 덧붙였습니다. 필드 이름의 동의어를 받아 주고, 잘못된 유형을 선택지 개수로 추정하고, 빈 응답을 재시도하고, 한 모델의 호출 습관에 맞춰 스텝 수를 늘렸습니다. 개별 오류는 줄었지만 harness는 점점 모델별 지식을 품게 되었습니다.
이 과정에서 중요한 함정이 있었습니다. 각 수정은 테스트를 통과했고 실제 오류를 해결했습니다. 따라서 코드만 보면 모두 필요한 방어 로직처럼 보였습니다. 그러나 그 필요성은 제품 계약이 아니라 당시 사용한 모델의 행동에서 나왔습니다. 모델을 바꾸면 근거도 함께 사라질 수 있는 코드였습니다.
2. 실제로 추가했던 보상적 harness
제가 추가한 보상 로직은 형태가 서로 달라도 같은 문장으로 설명할 수 있습니다. ‘모델이 X처럼 실패했으므로 코드가 Y로 고친다’는 구조입니다. 대표 사례를 외부에 공개해도 되는 일반화된 코드로 정리했습니다.
2.1 스키마 필드 이름을 동의어로 흡수했습니다
사용자에게 추가 질문을 보내는 도구는 questionId, question, inputType 같은 필드를 요구했습니다. 그러나 일부 모델은 questionId 대신 id나 key를, inputType 대신 type이나 kind를 보냈습니다. 그대로 파싱하면 질문 전체가 누락되므로 여러 이름을 순서대로 읽는 함수를 만들었습니다.
필드 이름 변형을 받아 주는 파서
private String firstText(Map<String, Object> values, String... names) {
for (String name : names) {
String value = text(values.get(name));
if (!value.isBlank()) {
return value;
}
}
return "";
}
String questionId = firstText(values, "questionId", "id", "key");
문제는 동의어 목록이 명세에서 나온 것이 아니라는 점입니다. 실행 로그에서 본 단어를 하나씩 추가한 결과였습니다. 같은 모델도 실행마다 다른 단어를 만들었고, 모델을 바꾸면 또 다른 변형이 나왔습니다. 파서는 넓어졌지만 계약은 더 모호해졌습니다.
2.2 잘못된 입력 유형을 코드가 추론했습니다
필드 이름을 찾은 뒤에도 값이 문제였습니다. Text, Radio, Select처럼 닫힌 값만 허용했지만 모델은 multiple_choice, dropdown, checkbox 같은 표현을 만들었습니다. 질문을 버리지 않기 위해 문자열을 정규화하고, 그래도 알 수 없으면 선택지 개수로 화면 유형을 결정했습니다.
모델 출력값을 정규화하고 추정하는 로직
String normalized = raw.toLowerCase(Locale.ROOT).replaceAll("[^a-z]", "");
QuestionType declared = switch (normalized) {
case "text", "freetext", "string" -> QuestionType.Text;
case "radio", "singlechoice", "choice" -> QuestionType.Radio;
case "select", "dropdown", "list" -> QuestionType.Select;
case "multiselect", "multiplechoice", "checkbox" -> QuestionType.MultiSelect;
default -> null;
};
return declared != null ? declared : inferFrom(options);
이 로직은 화면을 그릴 수 있게 해 주었지만, 모델이 만든 모호함을 애플리케이션이 임의로 해석한다는 위험이 생겼습니다. 잘못된 추정이 정상 입력처럼 저장될 수 있고, 새 모델이 정확한 값을 보내도 오래된 fallback이 계속 남습니다. 단기 복구에는 유용했지만 장기 계약으로 삼기에는 좋지 않았습니다.
2.3 구조화 출력 대신 프롬프트에 계약을 반복했습니다
일부 로컬 실행 환경에서는 구조화 출력 옵션을 켰을 때 생성이 정상 종료되었다고 표시되면서도 본문이 비어 오는 현상이 있었습니다. 결국 네이티브 형식 강제를 끄고, 프롬프트 안에 JSON 스키마와 ‘JSON만 출력’이라는 문장을 다시 적은 뒤 파서가 결과를 검증하도록 만들었습니다.
형식 강제를 프롬프트와 파서로 우회한 예
request.disableNativeSchema();
request.addInstruction("Respond with JSON only.");
request.addInstruction(renderSchema(responseSchema));
Response parsed = parser.parse(modelResponse);
validator.validate(parsed);
이 방식은 당장 동작했지만 계약을 세 군데에 복제했습니다. 도구 스키마, 프롬프트 문장, 파서 방어 규칙이 같은 내용을 서로 다른 방식으로 가지고 있었습니다. 어느 하나를 바꾸면 나머지도 함께 바꿔야 했고, 모델이 좋아져도 이 중복은 자동으로 사라지지 않았습니다.
2.4 빈 턴과 반복 오류를 재시도로 덮었습니다
도구 조회를 몇 차례 성공한 뒤 content와 tool calls가 모두 비어 있는 응답이 돌아오는 경우가 있었습니다. 마지막 한 턴 때문에 이미 얻은 관찰 결과를 전부 잃지 않도록 빈 응답을 재시도하고, 남은 정보로 응답을 마무리하는 경로를 추가했습니다.
빈 응답 복구와 실행 예산
private static final int MAX_EMPTY_RESPONSE_RETRIES = 2;
if (response.hasNoContent() && response.hasNoToolCalls()) {
retryOrFinishWithCollectedEvidence();
}
RunBudget budget = RunBudget.of(maxSteps, maxDuration, maxTokens);
재시도 자체는 필요한 안전장치일 수 있습니다. 그러나 몇 회를 재시도할지, 한 실행에 몇 스텝을 줄지는 당시 모델의 실패 빈도와 호출 습관에 맞춰 정했습니다. 한 모델에는 모자라고 다른 모델에는 과한 숫자가 되었으며, 같은 상수가 모델별로 전혀 다른 비용을 의미했습니다.
2.5 프롬프트도 특정 모델의 실패 기록이 되었습니다
코드뿐 아니라 시스템 프롬프트에도 ‘식별자를 그대로 복사하라’, ‘자리 표시자를 만들지 말라’, ‘도구 결과를 확인하기 전에 결론을 내리지 말라’는 문장이 늘어났습니다. 대부분 한 번 발생한 실제 실패에서 시작한 문장이었습니다.
프롬프트가 길어질수록 어떤 지시가 제품 정책이고 어떤 지시가 특정 모델 보상인지 구분하기 어려워졌습니다. 새로운 모델이 이미 잘하는 행동까지 반복해서 지시하면서 중요한 정책의 우선순위가 흐려졌습니다. 이 역시 harness가 모델에 fit된 흔적이었습니다.
3. 같은 harness에 모델만 바꾸어 본 결과
3.1 비교 방법
가정을 확인하기 위해 동일한 시스템 프롬프트, 동일한 도구 스키마, 동일한 조회 데이터, 동일한 실행 예산을 유지한 채 모델만 바꾸었습니다. 실제 업무에서 사용한 것과 같은 복합 요청을 여러 차례 반복했습니다. 요청은 필요한 정보를 여러 도구로 확인하고, 사용자의 결정이 필요한 항목을 질문하는 과업이었습니다.
전체 과업이 중단 없이 끝까지 완료되는지 확인했습니다.
도구 이름과 인자가 스키마에 맞는지 확인했습니다.
응답 형식 오류, 빈 턴, 재시도가 반복되는지 확인했습니다.
같은 요청을 다시 실행했을 때 결과의 편차가 큰지 확인했습니다.
harness가 개입하지 않아도 정상 결과가 유지되는지 확인했습니다.
처음 작성한 초안에서는 일부 하위 지표를 숫자로 정리했습니다. 그러나 다시 보니 그 값은 실제 체감과 전체 성공 여부를 제대로 나타내지 못했습니다. 예를 들어 질문 JSON 한 번이 형식에 맞았더라도 앞선 도구 호출이 실패해 전체 과업이 끝나지 않았다면 성공이라고 보기 어렵습니다. 이번 문서에서는 기억에 맞추어 수치를 새로 만들지 않고, 반복 실행에서 실제로 재현된 결과만 상태로 정리했습니다.
3.2 실제 관찰 결과
표 1. 동일한 harness에서 모델만 교체해 반복 실행한 관찰 결과
|
관찰 항목 |
gpt-oss:120b |
gemma4:31b |
qwen3.6:35b |
gpt-5.6 |
|---|---|---|---|---|
|
전체 과업 완료 |
도중 오류가 반복되어 안정적 완료가 어려웠습니다. |
도구 호출 오류와 빈 응답이 반복되었습니다. |
응답 지연과 형식 오류로 실패가 반복되었습니다. |
반복 실행에서 가장 일관되게 끝까지 완료했습니다. |
|
도구 호출 |
스키마에 맞는 호출을 이어서 수행했습니다. |
필요한 호출을 누락하는 경우가 많았습니다. |
불완전한 종료가 있었습니다. |
스키마에 맞는 호출을 이어서 수행했습니다. |
|
응답 형식 |
형식 검증이 종종 필요했습니다. |
빈 응답 복구가 필요했습니다. |
재시도와 형식 검증이 필요했습니다. |
재시도 1~2회로 응답이 안정적이었습니다. |
|
실행 편차 |
결과와 실행 흐름의 편차가 적은 편이였습니다. |
응답 형식의 편차가 있었습니다. |
응답 형식의 편차가 있었습니다. |
결과와 실행 흐름의 편차가 가장 작았습니다. |
|
종합 판단 |
현재 조건에서 운영 적용이 어려웠습니다. |
현재 조건에서 운영 적용이 어려웠습니다. |
현재 조건에서 운영 적용이 어려웠습니다. |
유일하게 실제 적용 기준을 안정적으로 충족했습니다. |
당시 회차별 정량 로그를 모든 모델에 같은 형식으로 보존하지 못했으므로 정확하지 않은 성공률이나 평균 시간을 새로 만들지 않았습니다. 표에는 반복해서 확인된 실패 형태와 실제 적용 판단만 기록했습니다.
결과는 gpt-5.6이 압도적이었습니다. 다른 모델은 한 문제를 보완하면 다른 지점에서 다시 오류가 발생했고, 실행할 때마다 실패 형태가 달라졌습니다. 반면 gpt-5.6은 동일한 harness에서 도구 호출부터 최종 질문까지 흐름을 가장 안정적으로 완주했습니다. 제가 작성한 보상 코드의 양보다 모델 자체의 도구 사용 능력과 계약 준수 능력이 결과를 더 크게 좌우했습니다.
특히 중요한 차이는 ‘한두 번 형식에 맞는 답을 만들었는가’가 아니라 ‘전체 작업을 반복해서 끝낼 수 있는가’였습니다. 로컬 모델에서도 부분 성공은 있었습니다. 그러나 사용자에게 제공할 수 있는 기능은 전체 흐름이 완료되어야 합니다. 이 기준으로 보니 모델 간 차이가 훨씬 선명해졌습니다.
harness는 약한 모델을 일정 수준까지 끌어올렸지만 실패 분포를 없애지는 못했습니다. 동의어를 추가하면 새로운 동의어가 나왔고, 재시도를 늘리면 시간과 비용이 늘었으며, 스텝 예산을 높이면 잘못된 호출도 더 오래 반복했습니다. 보상 로직의 효과는 있었지만 한계도 분명했습니다.
3.3 harness가 모델에 fit된다는 의미
보상적 harness의 입력은 제품 요구사항이 아니라 모델의 관찰된 실패입니다. 관찰은 특정 모델, 특정 버전, 특정 프롬프트, 특정 추론 서버에서 나옵니다. 따라서 그 관찰을 코드로 옮긴 보상 로직도 같은 조건에 묶입니다.
|
모델이 X처럼 실패했습니다. → harness가 Y로 보정합니다. → harness는 X라는 실패를 보인 모델에 fit됩니다. |
|---|
결정적인 코드로 비결정적인 출력을 감싸면 안정성이 생기는 것처럼 보입니다. 그러나 모델이 바뀌면 출력 분포도 바뀝니다. 예전 모델의 흔한 오류가 새 모델에서는 발생하지 않을 수 있고, 새 모델의 호출 방식은 예전 보상 로직이 가정한 순서와 다를 수 있습니다. 결국 범용 계층이라고 생각한 코드가 특정 모델의 행동을 근사한 경험적 모델이 됩니다.
이 관점에서 모델 교체는 단순한 provider 변경이 아닙니다. harness와 모델의 결합을 다시 평가하는 작업입니다. 기존 보상 코드를 그대로 두고 새 모델만 연결하면, 필요 없는 fallback이 정확한 값을 바꾸거나 불필요한 재시도가 지연을 만들 수 있습니다. 모델 업그레이드 뒤에는 코드 추가보다 삭제가 먼저 검토되어야 합니다.
4. gpt-5.6으로 바꾸자 사라진 것
gpt-5.6으로 교체한 뒤 가장 크게 달라진 점은 이전 harness를 더 잘 통과했다는 사실만이 아니었습니다. 보상이 필요한 상황 자체가 크게 줄었습니다. 정확한 필드 이름과 인자 형태를 유지하고, 필요한 도구를 선택해 전체 과업을 마치는 비율이 높아지자 공용 루프에 있던 복구 코드의 존재 이유가 약해졌습니다.
표 2. 모델 교체 전후 보상적 harness의 변화
|
보상 항목 |
오류가 반복되던 모델 |
gpt-5.6 적용 후 |
|---|---|---|
|
필드 이름 동의어 |
여러 별칭을 순서대로 탐색했습니다. |
정해진 스키마 이름을 그대로 사용해 대부분 불필요했습니다. |
|
입력 유형 추론 |
모르는 값을 문자열 규칙과 선택지 개수로 추정했습니다. |
허용된 유형을 사용해 추정 fallback을 제거할 수 있었습니다. |
|
프롬프트의 스키마 반복 |
계약을 긴 문장으로 다시 설명했습니다. |
도구와 응답 스키마 자체에 계약을 집중할 수 있었습니다. |
|
빈 응답 복구 |
재시도와 관찰 보존 경로가 자주 개입했습니다. |
정상 경로가 안정되어 예외적인 장애 처리로 축소할 수 있었습니다. |
|
모델별 스텝 보정 |
호출 습관에 맞춰 상수를 늘렸습니다. |
불필요한 호출이 줄어 더 단순한 예산 정책을 적용할 수 있었습니다. |
여기서 ‘필요 없어졌다’는 말은 코드를 즉시 전부 삭제했다는 뜻이 아닙니다. 실제 삭제 전에는 새 모델로 회귀 테스트를 돌리고, 보상 로직을 하나씩 끈 상태에서 결과가 유지되는지 확인해야 합니다. 중요한 변화는 새 보상을 더 추가하는 방향이 아니라, 기존 보상을 제거할 수 있는 방향으로 검증의 초점이 바뀌었다는 점입니다.
좋은 모델은 단순히 정답률을 높이지 않습니다. 파서 분기, 재시도 횟수, 모델별 설정, 실패 로그 분석, 회귀 테스트 조합을 함께 줄입니다. 호출 단가만 보면 로컬 모델이 저렴할 수 있지만, 실패를 조사하고 보상 코드를 유지하는 개발 비용까지 포함하면 결론이 달라집니다. 실제 프로젝트에서는 gpt-5.6을 선택하는 편이 전체 비용과 위험을 낮췄습니다.
또 하나 배운 점은 모델 평가를 단일 답변 품질로 끝내면 안 된다는 것입니다. 에이전트에서는 도구 선택, 인자 정확성, 여러 턴의 상태 유지, 실패 후 회복, 최종 응답까지가 하나의 성능입니다. gpt-5.6의 우위는 문장을 더 잘 쓴다는 수준이 아니라 전체 실행 그래프를 완주한다는 데 있었습니다.
5. 그래도 남겨야 하는 harness
좋은 모델이 보상적 harness를 줄여 주더라도 시스템의 책임까지 대신하지는 않습니다. 모델이 아무리 정확해도 권한이 없는 데이터를 읽어서는 안 되고, 되돌리기 어려운 변경은 승인 없이 실행해서는 안 되며, 사용한 근거와 실행 결과는 추적할 수 있어야 합니다.
5.1 보상과 책임을 구분하는 질문
코드를 분류할 때 저는 다음 질문을 사용했습니다. ‘모델이 계약을 완벽하게 지켜도 이 코드가 필요한가?’ 답이 '아니요’라면 보상적 harness이고, ‘예’라면 시스템 책임에 가까웠습니다.
표 3. 보상적 harness와 책임지는 harness의 경계
|
구분 |
보상적 harness |
책임지는 harness |
|---|---|---|
|
발생 근거 |
특정 모델에서 관찰한 오류입니다. |
제품 정책, 보안, 운영 책임입니다. |
|
모델 의존성 |
높습니다. 모델과 버전에 따라 달라집니다. |
낮습니다. provider가 바뀌어도 유지됩니다. |
|
대표 사례 |
동의어 파싱, 값 추론, 빈 턴 재시도, 모델별 상수입니다. |
권한 확인, 승인 게이트, 예산 상한, 감사 기록입니다. |
|
좋은 모델 적용 후 |
회귀 검증 뒤 삭제하거나 adapter로 축소합니다. |
그대로 유지하고 테스트를 강화합니다. |
|
실패 처리 |
가능하면 조용히 고치지 않고 명시적으로 드러냅니다. |
정책 위반 시 실행을 중단하고 이유를 기록합니다. |
5.2 사용자 승인과 변경 경계는 남깁니다
읽기 전용 조회와 실제 변경은 같은 도구 호출처럼 취급하지 않았습니다. 에이전트가 정보를 확인하고 계획을 설명하는 단계까지는 자동화하되, 데이터가 바뀌거나 외부에 결과가 게시되는 지점에서는 사용자의 명시적인 승인을 받도록 했습니다. 이 경계는 모델 성능과 무관합니다.
내부 이름을 제거해 일반화한 승인 흐름
사용자 요청
-> 읽기 전용 조회
-> 변경 계획과 영향 제시
-> 사용자 승인
-> 실제 변경 실행
-> 결과와 근거 기록
질문이나 승인 요청을 모델이 다시 작성하게 하지 않고, 시스템이 정해진 구조로 사용자에게 전달하는 것도 남겼습니다. 이것은 모델이 필드 이름을 틀릴까 봐 우회하는 보상만이 아니라, 사용자 결정을 생성 텍스트와 분리한다는 제품 원칙이기 때문입니다.
5.3 예산, 권한, 감사 기록은 남깁니다
한 실행이 사용할 수 있는 스텝, 시간, 토큰에 상한을 두는 구조는 필요합니다. 다만 숫자 자체는 모델의 속도와 호출 방식에 따라 달라질 수 있으므로 설정으로 분리해야 합니다. 상한이라는 정책은 책임 harness이고, 특정 모델에 맞춘 값은 보상적 설정입니다.
도구 허용 목록과 권한 확인도 모델에게 맡기지 않습니다. 조회 권한이 없는 상황과 조회 결과가 비어 있는 상황은 사용자에게 전혀 다른 의미입니다. 모델이 그 차이를 잘 설명하더라도, 호출 가능 여부를 결정하는 주체는 시스템이어야 합니다.
마지막으로 어떤 요청에서 어떤 도구가 호출되었고, 어떤 결과를 근거로 응답했는지 기록해야 합니다. 운영 중 문제가 생겼을 때 모델의 최종 문장만 남아 있으면 원인을 재구성할 수 없습니다. 감사 기록은 모델 품질이 높을수록 덜 중요해지는 기능이 아니라, 실제 사용 범위가 넓어질수록 더 중요해지는 기능입니다.
6. 모델 선택과 harness를 함께 설계하는 방법
이번 경험 이후에는 약한 모델을 먼저 고르고 harness로 보완하는 순서를 바꾸었습니다. 먼저 최소한의 계약과 안전장치만 둔 얇은 실행기로 후보 모델을 평가하고, 대표 시나리오를 끝까지 수행할 수 있는 모델을 고릅니다. 그 다음에도 반복되는 오류만 격리된 adapter에서 보상합니다.
6.1 전체 과업 성공을 먼저 측정합니다
JSON 한 번의 파싱 성공률이나 도구 한 번의 호출 성공률만 보면 실제 사용성을 과대평가하기 쉽습니다. 사용자가 원하는 결과까지 도달했는지, 같은 요청을 반복했을 때 결과가 유지되는지, 실패했을 때 원인이 명확한지를 함께 봐야 합니다.
대표 업무 시나리오를 짧은 단위가 아니라 시작부터 종료까지 실행합니다.
모델별로 같은 프롬프트, 도구, 데이터, 예산을 사용합니다.
최종 성공 여부와 함께 도구 인자 오류, 빈 턴, 재시도, 소요 시간을 기록합니다.
harness를 켠 결과와 보상 로직을 끈 결과를 나란히 비교합니다.
모델을 바꾼 뒤에는 새 보상을 추가하기 전에 기존 보상을 삭제할 수 있는지 확인합니다.
6.2 보상 코드는 adapter 안에 가둡니다
모델별 보상 로직이 공용 실행 루프에 들어가면 코드가 어떤 모델을 위해 존재하는지 알기 어렵습니다. 보상은 provider 또는 모델 adapter 안쪽에 두고, 공용 루프는 명확한 계약이 지켜진다고 가정하는 편이 좋습니다. 계약을 지키지 못하면 조용히 추론하기보다 진단 가능한 오류로 실패시키는 것이 장기적으로 안전했습니다.
불가피하게 보상을 추가할 때는 모델명, 버전, 재현 조건, 제거 조건을 주석과 테스트에 남겨야 합니다. 그러면 다음 모델로 교체할 때 삭제 후보를 바로 찾을 수 있습니다. 보상 코드에는 기능 설명뿐 아니라 유효기간을 판단할 근거가 필요합니다.
6.3 모델 비용에 유지보수 비용을 포함합니다
모델 선택 표에는 토큰 가격과 추론 서버 비용만 들어가기 쉽습니다. 그러나 실제 프로젝트에서는 실패 분석 시간, 보상 코드 구현과 테스트, 재시도에 따른 지연, 운영 장애 가능성도 비용입니다. 로컬 모델의 호출 비용이 낮더라도 오류를 계속 수습해야 한다면 전체 비용은 더 커질 수 있습니다.
저의 경우 gpt-5.6은 모델 비용보다 개발 흐름을 단순하게 만든 효과가 더 컸습니다. 실패가 재현될 때마다 새로운 분기를 추가하던 작업이 줄었고, 핵심 정책과 도구 계약에 집중할 수 있었습니다. 성능 좋은 모델을 선택하는 것이 단순한 품질 향상이 아니라 코드와 운영 복잡도를 줄이는 선택이 되었습니다.
7. 적용 후 정리한 실무 체크리스트
현재는 모델을 도입하거나 교체할 때 다음 순서로 확인합니다.
후보 모델이 대표 시나리오를 처음부터 끝까지 안정적으로 수행하는지 확인합니다.
실패를 출력 형식, 도구 선택, 인자 정확성, 상태 유지, 지연으로 나누어 기록합니다.
모델 자체의 문제를 공용 비즈니스 로직에서 보상하지 않습니다.
보상 로직은 모델별 adapter와 설정에 격리하고 제거 조건을 함께 기록합니다.
승인, 권한, 실행 예산, 감사 기록은 모델과 무관한 정책으로 유지합니다.
모델 업그레이드 시 기능 추가보다 불필요한 harness 삭제를 먼저 검토합니다.
정량 로그가 없으면 기억에 맞춘 숫자를 만들지 않고 재현 가능한 현상만 공유합니다.
이 체크리스트의 목적은 harness를 없애는 것이 아닙니다. 모델이 책임질 일과 시스템이 책임질 일을 구분하는 것입니다. 모델의 결함을 코드가 무한히 흡수하도록 두면 harness는 계속 두꺼워지고, 어떤 모델에서도 최적이 아닌 중간층이 됩니다.
마치며
처음에는 좋은 harness가 모델 차이를 지워 줄 수 있다고 생각했습니다. 실제로는 harness의 상당 부분이 한 모델의 결함을 코드로 옮겨 적은 것이었습니다. 그 코드는 당시에는 문제를 해결했지만, 다른 모델에서도 계속 필요한 범용 규칙은 아니었습니다.
같은 harness를 여러 모델에 연결해 보니 gpt-5.6의 성능이 압도적이었고, 다른 모델에서는 오류가 계속 발생했습니다. 이 경험으로 모델의 성능이 단순한 부품 사양이 아니라 harness의 크기와 형태를 결정한다는 사실을 체감했습니다. 약한 모델을 보상하는 코드가 쌓일수록 시스템은 그 모델에 더 강하게 fit됩니다.
|
좋은 모델은 harness를 전부 없애지는 않습니다. 모델 결함을 메우던 보상적 harness를 줄이고, 승인·권한·예산·감사처럼 시스템이 끝까지 책임져야 할 harness만 남게 합니다. |
|---|
따라서 모델을 고를 때는 호출 비용만 비교해서는 안 됩니다. 전체 과업 성공률, 오류의 반복성, 보상 코드의 양, 운영 위험, 개발자가 수습하는 시간까지 함께 봐야 합니다. 프로젝트에서 gpt-5.6을 선택한 이유도 같은 기준에서 설명할 수 있습니다. 성능 좋은 모델을 사용하는 것이 결과를 개선했을 뿐 아니라, 시스템을 더 단순하고 설명 가능하게 했습니다.
앞으로 모델이 다시 바뀌더라도 같은 질문을 먼저 던질 생각입니다. 지금 추가하려는 이 코드는 시스템의 책임입니까, 아니면 현재 모델의 부족함을 메우는 임시 보상입니까? 이 구분을 남겨 두는 것이 다음 모델로 옮길 때 지울 것과 지켜야 할 것을 가장 빠르게 알려 주는 기준이 되었습니다.
IAN