자연어로 시스템을 조작하는 기능을 붙이면 처음에는 모델 성능이 가장 중요한 변수로 보이기 쉽다.
그런데 모델을 바꿔 보면 다른 문제가 드러난다. 같은 요청인데 어떤 tool을 고르는지, parameter를 어떻게 채우는지가 조금씩 달라진다. 프롬프트를 다듬어 맞춰 놓으면 다음 모델에서 또 어긋난다.
모델 쪽에서 해결하려 하면 끝이 없다. 모델이 바뀔 때마다 다시 해야 하기 때문이다.
모델에게 넘긴 판단이 몇 개인지 센다
흔한 구조는 이렇다.
사용자 자연어
↓
LLM
↓
tool 선택 → parameter 생성 → 실행이 그림에서 LLM은 최소 네 가지를 판단한다. 무엇을 하려는 요청인지, 어느 tool이 거기 맞는지, parameter에 어떤 값을 넣을지, 그리고 그 값이 말이 되는지.
모델이 판단하는 항목 수만큼 모델 의존성이 생긴다. 넷 중 셋을 코드로 옮기면 모델을 바꿔도 흔들리는 폭이 그만큼 줄어든다.
해석과 실행 사이에 contract를 둔다
LLM의 일을 “자연어를 구조화된 요청으로 옮기는 것”까지로 자른다. 그 뒤는 결정적 코드가 맡는다.
{
"intent": "<무엇을 하려는가>",
"target": "<대상 종류>",
"scope": "<범위>",
"radius_m": 500,
"output": "map"
}intent 값은 실제 함수 이름이 아니라 미리 정해 둔 목록의 한 항목이어야 한다. 모델이 이름을 지어낼 수 있으면 contract가 아니라 제안일 뿐이다.
이 JSON이 경계다. 위쪽은 모델마다 다를 수 있고, 아래쪽은 같아야 한다.
검증은 모델 밖에서 한다
구조가 맞다고 값이 맞는 것은 아니다. 실행 전에 결정적 검증을 둔다.
- 숫자 범위가 유효한가 (반경이 음수이거나 0인가)
- 참조한 대상이 실제로 존재하는가
- 이 조합의 연산이 허용되는가
- 단위와 좌표 기준이 서로 맞는가
- 결과 크기가 감당 가능한가
모델에게 “검증도 해 줘”라고 부탁하는 것과 다르다. 여기서 걸리면 실행이 안 된다. 이 단계가 있으면 모델을 교체해도 실행 규칙은 그대로다.
저수준 API를 그대로 노출하지 않는다
“500m 안의 약국 찾기”는 내부적으로 여러 단계다.
주소 해석 → 좌표 변환 → 반경 계산 → 대상 검색 → 필터 → 결과 정리LLM이 이 조합을 매번 직접 만들면 모델마다 실행 계획이 달라진다. 순서를 바꾸거나 한 단계를 빼먹는 것도 모델의 자유가 된다.
조합 전체를 하나의 업무 목적 API 뒤로 감추면 그 책임이 application으로 넘어온다. 모델은 “이 목적을 원한다”까지만 말한다.
상단 API를 어디서 자를지는 이 기준으로 정한다.
- 호출 하나가 하나의 업무 목적에 대응하는가
- 저수준 연산의 조합 순서를 호출자가 몰라도 되는가
- 실패했을 때 어느 단계에서 실패했는지 구분되는가
세 번째가 자주 빠진다. 여러 단계를 감싸 놓고 실패를 하나로 뭉뚱그리면 디버깅이 불가능해진다.
도메인 의미론은 시스템이 소유한다
모델이 아니라 결정적 컴포넌트가 책임져야 하는 것들이 있다. 공간 데이터를 다룬다면 이런 것들이다.
| 항목 | 모델에게 맡기면 생기는 일 |
|---|---|
| 같은 이름의 행정구역이 여럿 | 모델마다 다른 쪽을 고른다 |
| 좌표계 | 변환을 건너뛰어도 숫자는 그럴듯하다 |
| geometry 유효성 | 깨진 도형이 그대로 연산에 들어간다 |
| 포함·교차 같은 술어의 정의 | 경계선 위의 점 처리가 달라진다 |
공간 데이터만의 문제가 아니다. 회계라면 회계연도 경계와 반올림 규칙, 인사라면 재직 기간 산정 기준이 같은 자리에 온다. 도메인에 “정답이 정해져 있는데 겉보기로는 여러 답이 그럴듯한” 지점이 있으면 거기가 그 자리다.
원칙은 한 줄이다.
LLM은 언어를 해석하고, 도메인 의미론은 시스템이 소유한다.
사람이 끼어들 자리를 남긴다
자연어 해석은 완전히 결정적이지 않다. 구조화된 요청을 실행 전에 사용자에게 보여 주고 고칠 수 있게 하면, 해석 오류를 실행 후가 아니라 실행 전에 잡는다.
자연어 → 구조화된 요청 → 사람이 확인·수정 → 실행이 화면이 있으면 모델을 바꿀 때 회귀를 확인하기도 쉽다. 같은 문장을 넣고 만들어진 요청을 비교하면 된다 — 최종 결과를 비교하는 것보다 원인을 좁히기 좋다.
최종 구조와 그 값
Model A / B / C
↓
Intent Contract ← 여기까지가 모델의 일
↓
Slot Validation
↓
업무 목적 API
↓
Deterministic Core
↓
데이터 / 결과모델은 교체 가능하고, 업무 계약과 실행 규칙은 고정된다. 모델을 바꾸는 작업이 “전부 다시 검증”에서 “contract 생성만 다시 검증”으로 줄어든다.
이 방법이 통하지 않는 경우
- intent 목록을 미리 못 정하는 경우. 사용자가 무엇을 요청할지 예측이 안 되는 탐색적 도구라면 contract가 계속 깨진다. 그럴 땐 목록을 고정하는 대신 실패를 안전하게 만드는 쪽이 낫다
- 단계가 하나뿐인 작업. 감쌀 조합이 없으면 상단 API는 이름만 하나 더 늘리는 일이다
- 검증 비용이 실행 비용보다 큰 경우. “대상이 존재하는가”를 확인하는 질의가 본 작업만큼 무거우면 순서를 바꾸거나 실패를 허용하는 편이 싸다
- 이 글은 결과의 안정성에 대한 것이지 정확도에 대한 것이 아니다. contract를 고정해도 모델이 문장을 잘못 읽으면 잘못된 요청이 안정적으로 만들어진다. 그건 위의 확인 화면으로 잡는 문제다