LLM에게 쓰기 쉬운 구문이란 무엇인가: `end`와 `end func foo`를 1,000번 이상 비교해봤습니다
요약
본 기사는 LLM에게 학습 데이터에 존재하지 않는 미지의 언어 문법을 프롬프트 내에서 처음 설명하고 코드를 생성하게 하는 실험 결과를 다룹니다. 그 결과, 블록을 단순히 `end`로 닫는 것보다 `end func foo`처럼 '무엇을 닫았는지'를 명시하는 구문이 중첩된 깊은 프로그램 구조를 훨씬 정확하게 생성함을 입증했습니다.
핵심 포인트
- 미지의 언어 문법 학습 능력 측정: 프롬프트 내 설명만으로 코드를 생성하는 능력을 테스트함.
- `end func foo` 방식의 우월성: 단순 `end`보다 중첩된 깊은 프로그램 구조를 압도적으로 정확하게 처리함.
- 실용적 가치: 독자 DSL이나 새로운 API 등 학습 데이터에 없는 것을 LLM에게 작성시키기 위함.
- 구문 설계 자체의 효과 측정 가능: 기존 언어 경험의 영향을 배제하고 순수 문법 설계만 비교할 수 있음.
서론
안녕하세요! 주식회사 NextBeat에서 테크놀로지 에반젤리스트라는 직함으로 일하고 있는 미즈시마입니다.
이번에는 'LLM에게 쓰기 쉬운 구문이란 무엇인가'를 작은 실험을 통해 확인해 본 이야기입니다.
대상으로는 기존의 프로그래밍 언어가 아니라, 프롬프트 안에서 처음 문법을 설명받는 미지의 언어입니다. 학습 데이터에 대량으로 포함된 언어라면 '그 언어를 알고 있기 때문에 쓸 수 있는 것인지' 아니면 '구문 설계가 좋기 때문에 쓸 수 있는 것인지'를 구분할 수 없습니다. 그래서 스코프만 가진 작은 언어를 직접 만들고, 문법 설명과 8가지 예시만을 주어서, 같은 내용의 프로그램을 닫는 방식이 다른 구문으로 작성하게 했습니다. 즉 여기서 측정하는 것은 처음 보는 문법을 그 자리에서 읽고 올바르게 생성할 수 있는지입니다.
결론부터 말씀드리자면, 블록을 end로 닫는 것보다 end func foo처럼 '무엇을 닫았는지'를 쓰게 하는 것이 LLM이 중첩(nest)된 깊은 프로그램을 압도적으로 올바르게 생성하게 했습니다. 깊이 8의 중첩에서
end
만으로는 24번 중 0번밖에 올바른 프로그램을 작성하지 못했지만, end func foo 형식으로는 24번 중 24번 정답을 맞혔습니다. 반면, '의존 관계의 거리가 짧은 구문일수록 생성하기 쉬 않을까'라는 저의 초기 가설은 순순히 지지받지는 못했습니다. 이 글에서는 무엇을 측정했는지, 어디까지 말할 수 있고 어디부터는 말할 수 없는지를 포함하여 작성하겠습니다.
공개 리포트(전 데이터 포함): https://kmizu.github.io/syntax-locality-experiments/
리포지토리: https://github.com/kmizu/syntax-locality-experiments
전제: 미지의 언어를 LLM에게 작성하게 한다는 것은 무엇을 의미하는가
본론에 들어가기 전에, 이번 주제인 '미지의 언어 생성'이 어떤 설정인지 자세히 설명해 드리겠습니다. LLM의 기본은 알고 있어도 이 설정에는 익숙하지 않은 분들이 많을 것 같아서요.
일반적인 '코드 생성'과 무엇이 다른가
ChatGPT나 Claude에게 'Python으로 이런 함수를 써줘'라고 요청하면, 대체로 올바른 코드가 돌아옵니다. 이것은 Python의 코드를 학습 데이터로 대량으로 읽었기 때문입니다. 문법도 관용구도 라이브러리도 이미 '알고 있는' 상태인 거죠.
이번에 하고 싶은 것은 그것과는 다릅니다. 모델이 한 번도 본 적 없는 언어의 문법을 프롬프트 안에서 처음 설명하고, 그 자리에서 이해해서 올바른 프로그램을 작성하게 하는 설정입니다. 기계 학습 용어로 말하면 in-context learning(문맥 내 학습)이며, 학습 데이터에 의존할 수 없기 때문에 '문법 설명을 읽고 그것에 따라 구조를 조립하는' 능력 자체가 시험대에 오르는 것입니다.
굳이 이런 것을 하는 이유는 두 가지가 있습니다.
하나는 실용적인 이유입니다. 사내의 독자 DSL(Domain Specific Language), 설정 언어, 새로운 프로그래밍 언어, 아직 세상에 나오지 않은 라이브러리의 API 등 학습 데이터에 포함되어 있지 않은 것을 LLM에게 작성하게 하고 싶은 상황은 실제로 있습니다. 그럴 때 '설명을 읽으면 제대로 쓸 수 있는지', '어떤 설계가 쓰기 쉬운지'는 알아두고 싶습니다.
다른 하나는 실험적으로 구분할 수 있기 때문입니다. 기존 언어로 '}와 end foo 중 어느 것이 쓰기 쉬운지'를 비교해도, Ruby나 Scala 코드를 읽은 경험의 차이가 섞여서 구문 설계 자체의 효과를 뽑아낼 수 없습니다. 미지의 언어라면 문법 설명과 예시만이 단서가 되므로, **같은 내용을 닫는 방식만 바꿔서 작성하게 했을 때의 차이는 '구문 설계의 차이'**로서 읽을 수 있습니다.
모델에게 제공하는 것과 돌려받는 것
실제 프롬프트는 '작업 설명', '문법(SYNTAX)', '예시(EXAMPLES)', '문제(TEST)'의 4부 구성입니다. 문법 부분은 실험에서 사용한 것을 그대로 인용하면 다음과 같습니다 (이름 지정 닫기의 조건).
SYNTAX
The kind keywords are unit for unit, func for func, and area for area. Each creates a local scope.
Open a block with KIND NAME on one line.
...
한국어로 번역하면 다음과 같습니다 (실험에서는 영어 버전만 제공했습니다).
구문(SYNTAX)
종류를 나타내는 키워드는 unit, func, area의 세 가지입니다. 이들은 모두 로컬 스코프를 만듭니다.
블록은 '종류 이름'을 한 줄 써서 열립니다.
...
이후에 동일한 형식의 '문제와 정답' 쌍이 8개 이어집니다 (few-shot 예시입니다. 예시 내용과 순서는 모든 조건에서 고정되어 있습니다). 마지막에 문제가 나옵니다.
생성 태스크의 문제는 구문에 의존하지 않는 AST 표입니다. 설명을 위해 작은 예로 따라가 보겠습니다. 먼저, 모델이 작성하게 하고 싶은 프로그램은 다음과 같습니다 (보기 좋게 들여쓰기 했지만, 들여쓰기에 의미는 없습니다).
let x 1
func foo
let x 2
...
(실제 실험에서는 블록 이름이 n_vjoxafca와 같은 무작위의 8자리, 변수는 x_a, 값은 4자리 정수 형태라 이름만으로는 구조를 추측할 수 없습니다. 여기서는 가독성을 위해 foo나 bar와 같은 짧은 이름과 작은 값을 사용했습니다.)
이 프로그램의 구조를 구문을 전혀 사용하지 않고 표로 만든 것이 모델에게 전달되는 '문제'입니다.
AST table:
node parent order tag kind name variable value payload
d_root - 0 ROOT - - - - -
...
각 행은 AST의 노드이며, parent가 부모, order는 부모 안에서의 순서입니다. 행의 배열은 일부러 섞여 있습니다 (부모는 자식보다 먼저 나온다는 제약만 지키고 있습니다). node 열의 ID는 표 내에서의 참조용일 뿐, 프로그램에는 나타나지 않습니다. 모델은 이 표를 읽어 트리 구조를 복원하고, 문법에 따라 작성해야 합니다. 이름 지정 닫기(D) 구문이라면, 기대하는 출력은 다음과 같습니다.
let x 1
func foo
let x 2
...
처음에 보여준 프로그램에서 들여쓰기를 제거한 것만 같지만, 모델 입장에서는 '표 → 트리 → 문자열'의 변환을 처음 보는 문법으로 수행하는 것입니다. 실제 실험에서는 이 표가 깊이 4에 15행 정도, 깊이 16에 nop를 128개씩 채운 조건에서는 수백 행이 됩니다.
채점은 엄격합니다. 반환된 전체 텍스트를 전용 파서로 분석하여, 구문 오류가 없고, AST(블록의 종류・이름・중첩・문의 순서・변수와 값)가 완전히 일치했을 때만 정답으로 합니다. '거의 맞음'은 오답입니다. 설명문이나 코드 펜스가 섞여도 오답입니다.
같은 AST 표를, 닫는 방식이 다른 구문으로 작성하게 했을 때, 정답률이 어떻게 변하는지. 이것이 이번 측정 내용입니다.
'의존성 거리'란 무엇인가
또 하나, 이 기사의 주제인 '의존성 거리'도 구체적으로 설명하겠습니다.
LLM은 텍스트를 왼쪽에서 오른쪽으로, 1 토큰씩 생성합니다. 위의 기대 출력을 작성하는 모델의 입장이 되어 보세요. 마지막 end func foo를 쓰는 시점에, 모델은 'func foo를 5줄 전에 열었고, 그 안에서 area bar를 열고 닫았으며, 지금 딱 바깥쪽 것이 하나 남아있다'는 것을 파악하고 있어야 합니다. 이것이 깊이 8이나 16이 되면, 닫는 줄을 쓸 시점에 참조해야 할 여는 줄은 수십 줄에서 수백 줄 전이 됩니다. 닫는 줄을 작성한다는 판단이 훨씬 이전의 여는 줄에 의존하는 것입니다. 이것이 장거리 의존성입니다.
여기서 'Transformer라면 먼 토큰도 참조할 수 있지 않을까'라고 생각한 분은 맞습니다. 예전 RNN과 달리, Transformer의 self-attention은 컨텍스트 내의 어떤 토큰에도 직접 접근할 수 있습니다. 거리가 멀어서 읽을 수 없다는 것은 원리적으로 없습니다. 그래서 문제는 '멀리 볼 수 있느냐'가 아니라, **'무엇을 봐야 할지가 지금 작성하는 위치에서 결정되느냐'**입니다.
닫는 줄이 end만 있는 구문이고, 다음에 end를 써야 할지 판단하려면, '지금까지 열린 블록의 수 - 닫힌 블록의 수'를 알아야 합니다. 이것은 특정 토큰을 보면 알 수 있는 정보가 아니라, 접두사 전체에 걸친 집계입니다. attention은 어디서든 볼 수 있지만, '봐야 할 한 곳'이 없기 때문에, 중첩이 깊어질수록 계산 실수가 일어나기 쉬울 것이라고 예상할 수 있습니다. 실제로 end를 하나 빠뜨려도 문법적으로는 아무것도 막아주지 못합니다.
반면에 end func foo
이러한 구문에서는 이야기가 달라집니다. 닫아야 할 블록의 이름을 쓰려면 '아직 닫지 않은 열린 줄 중 가장 안쪽 것'을 찾으면 되고, 이것은 특정 한 줄에 대한 참조입니다. 게다가 다음에 닫아야 하는 것은 방금 찾은 열린 줄 바로 바깥쪽에 있습니다. 즉, 각 단계에서 의존하는 상대는 '바로 직전에 자신이 작성한 닫는 줄'과 '그에 대응하는 열린 줄의 주변'만으로 충분하며, 프리픽스 전체를 집계할 필요가 없습니다.
이것이 제 가설의 내용입니다. end foo 타입의 구문이라면, 장거리 집계가 아니라 단거리 참조의 연쇄만으로 프로그램을 닫을 수 있다. 그러니 생성하기 쉬울 것이다라는 것입니다. 이 생각을 더 나아가면, '바로 직전 줄만 보면 다음 줄을 쓸 수 있는' 구문(들여쓰기만으로 구조가 결정되는 Python식 구문이나, 닫는 줄에 깊이의 숫자를 쓰는 구문)은 훨씬 더 생성하기 쉬울 것이라는 예상도 나옵니다.
이 가설이 맞는지 실제로 측정해 보겠습니다.
동기: Scala 3의 end는 LLM에게도 친절한가
애초에 이 의문을 갖게 된 계기는 Scala 3입니다. 저는 박사 과정에서 구문 분석(PEG)을 했던 경험이 있어, 프로그래밍 언어의 문법에는 지금도 관심이 있습니다. Scala 3에는 들여쓰기 구문으로 닫는 것을 잊지 않도록 하는 end 마커가 있습니다.
def foo(x: Int): Int =
if x > 0 then
x
...
end foo
의 foo는 중복입니다. 인간에게는 '긴 블록의 끝을 눈으로 추적할 수 있게 하기 위한' 기능이지만, 문득 '이것은 LLM에게도 효과가 있지 않을까'라고 생각했습니다. 앞 절에서 쓴 것처럼, {나 end를 올바르게 쓰려면 열린 줄과의 대응 관계를 계속 유지해야 하고, end foo는 그 대응 관계를 닫는 쪽에서도 작성하게 합니다. 만약 이것이 효과적이라면, 'LLM에게 쓰기 쉬운 구문'의 설계 지침 중 하나가 보이는 것입니다.
이전 기사에서 LLM에게 미지의 구문(Macro PEG)을 다루게 하는 실험을 한 적이 있지만, 이번에는 '구문의 설계 그 자체'가 생성 정확도에 어떻게 영향을 미치는지, 가능한 한 통제된 조건에서 측정해 보기로 했습니다.
실험 설계
미니 언어
앞 절의 예시에서 본 것처럼, 언어는 스코프만 가진 작은 것입니다. 문은 3가지 종류입니다.
let x_a 1234: 변수 바인딩 (변수명은x_a~x_f, 값은 4자리 정수)nop p_xxxxxxxx: 아무것도 하지 않는 문. 블록 내부를 채워 열린 줄과 닫는 줄의 거리를 늘리기 위해 사용합니다.unit n_xxxxxxxx/func …/area …: 블록. 3가지 종류의 '유형'이 있지만, 의미는 모두 같은 '지역 스코프 만들기'입니다.
'유형'을 3개 준비한 것은, 닫는 줄에 end func처럼 유형만 쓰도록 조건을 만들기 위해서입니다.
같은 프로그램(같은 AST, 같은 이름, 같은 값)을, 닫는 방식만 바꾼 5가지 구문으로 작성합니다.
| 기호 | 스타일 | 닫는 방식 | 닫는 줄이 가진 정보 |
|---|---|---|---|
| A | braces | { … } | 없음 (가장 일반적인 형태) |
| B | generic_end | end | 없음 |
| C | typed_end | end func | 유형만 |
| D | named_end | end func foo | 유형과 이름 |
| E | padded_end | end junk z_aaaaaaaa | 없음 (D와 같은 글자 수) |
E는 'D가 유리한 것이 단지 닫는 줄이 길기 때문만은 아닐까'라는 의혹을 해소하기 위한 대조 조건입니다. 닫는 줄의 길이는 D와 맞추었지만, junk z_aaaaaaaa는 모든 블록에서 공통이며 어떤 정보도 가지고 있지 않습니다.
func foo
let x 1
area bar
...
위가 D이고, 같은 것을 B로 쓰면 닫는 줄이 둘 다 end가 됩니다. 참고로 이 5가지 조건에서 들여쓰기는 가독성을 위해 붙인 것일 뿐, 문법적 의미는 없습니다 (프롬프트에서도 그렇게 명시하고 있습니다). 들여쓰기에 의미를 부여하는 조건은 후반에 별도로 등장합니다.
어휘를 무의미어로 대체하는 조건
그래서 Ruby나 Lua 등 셀 수 없이 많은 언어들이 닫는 구문(closing statement)을 가지고 있기 때문입니다.
이에 따라 키워드를 daxu / mepo / sovi / kep / rud / bim 같은 무의미한 단어로 대체한 nonce 조건도 준비했습니다 (8가지 어휘 세트를 고정하여 재사용합니다). 모든 결과는 일반어휘와 nonce 어휘를 동일한 가중치로 합산했습니다. 이로써 '기존 언어에 비슷해서 작성할 수 있다'는 효과는 상당히 약화되지만, 구문의 형태 자체의 유사성까지 없앨 수는 없으므로, 그 부분은 한계점으로 나중에 언급하겠습니다.
태스크
- T1 독해: 프로그램 중간에
probe를 넣어 '여기서 변수x_a의 값은?'이라고 질문합니다. 블록을 닫자마자 질문하는 조건(after_close)이 핵심이며, 닫는 줄을 올바르게 읽지 못하면 바깥쪽 값으로 돌아갈 수 없습니다. - T2 독해: '현재 열려 있는 블록의 이름을 바깥쪽부터 순서대로 말해보세요.'
- T3 생성: 앞 절에서 본 AST 표를 제공하고, 완전한 프로그램을 작성하게 합니다.
독해 태스크는 '닫는 레이블이 있으면 읽기 쉬운가'를 측정하며, 생성 태스크는 '닫는 레이블이 있으면 쓰기 쉬운가'를 측정합니다. T3가 이번의 주역입니다. 모델은 닫는 줄을 스스로 작성해야 하므로, 구문 설계의 차이가 그대로 드러납니다.
난이도의 축
프로그램은 무작위로 생성하지만, 난이도는 두 가지 축으로 제어합니다.
- 깊이: 블록 중첩 깊이 (2 / 4 / 8, 후반 실험에서는 16까지)
- 블록의 길이: 각 블록에 채워 넣는
nop문장의 수 (0 / 8 / 32, 후반에는 128까지).nop은 의미가 없기 때문에, 블록 구조를 바꾸지 않고 열린 줄과 닫는 줄 사이의 간격만 늘릴 수 있습니다.
깊이는 '동시에 열려 있는 블록의 수'에 대응하고, 블록 길이는 '열린 줄부터 닫는 줄까지의 행 수'에 대응합니다. 이 두 가지를 개별적으로 조절할 수 있다는 점이 나중에 '거리가 영향을 주는지, 깊이가 영향을 주는지'를 분리하여 분석할 때 유용합니다.
채점 및 운영
- 정답은 재귀적인 AST 인터프리터와 명시적 스택을 사용하는 이벤트 기반의 인터프리터 두 가지를 독립적으로 구현하여 일치 여부를 확인했습니다. 이는 채점기 버그로 인해 결과가 왜곡되는 것을 방지하기 위함입니다.
- 동일한 구조의 프로그램을 5 구문 × 2 어휘(일반 키워드와 nonce 어휘)로 작성하므로, 구문별 비교는 항상 '같은 프로그램을 다른 구문으로 작성한 것'끼리의 대조 비교입니다. 통계적으로 독립적인 샘플 단위는 구문이 아니라 원래의 프로그램 구조입니다.
- 부정확한 재시도, best-of-N, 조건별 프롬프트 조정은 금지합니다. 실패는 그대로 계산합니다. '개선했다', '차이가 없었다', '악화되었다' 중 어느 것도 동등한 결과로 취급하는 방침입니다.
구현은 Scala 3(표준 라이브러리 및 JDK만 사용)으로 했으며, 모델은 gpt-5.6-terra이고, 추론 노력(reasoning effort)은 low입니다. 모델 종류 1개, effort 설정 1개의 결과이므로 예비 실험 결과로 읽어주시기 바랍니다.
end는 전멸했고, end func foo는 만점이라는 결과입니다.
결과 1: 깊이 8에서 첫 번째 파일럿(1,800회 시도)의 T3 생성 결과를 중첩 깊이별로 본 것입니다. 각 셀은 24개 생성(12가지 프로그램 구조 × 2 어휘〈일반 키워드와 nonce 어휘〉)입니다.
| 스타일 | 깊이 2 | 깊이 4 | 깊이 8 |
|---|---|---|---|
| A braces | 24 | 24 | 10 |
B end | 24 | 22 | 0 |
C end func | 24 | 24 | 12 |
D end func foo | 23 | 24 | 24 |
| E 같은 길이의 무의미한 닫기 | 24 | 24 | 1 |
깊이 8에서 B와 E는 거의 전멸했고, D는 만점입니다. 실패 내용 대부분은 '블록을 완전히 닫지 못한 채 프로그램이 끝남'(unclosed_scope)이었습니다. 닫는 줄만 길게 늘인 E가 효과가 없으므로, 효과가 있는 것은 길이가 아니라 '어떤 블록을 닫았는지'라는 정보라고 할 수 있습니다.
반면, 독해 태스크(T1 after_close)는 D가 72개 중 66개, B가 72개 중 60개로 차이가 작고 신뢰 구간도 0을 넘지 않았습니다. 읽을 때는 닫는 레이블이 있어도 없어도 큰 차이가 없지만, 쓸 때는 결정적으로 다르다는 것이 이 단계의 소견입니다.
なお、読解タスクの本実験(5,120試行、事前に設計を固定したもの)は 1,412 試行まで進んだところで API クレジットが尽きて中断しており、信頼区間を伴う結論は出せていません。
결과 2: 효과적인 것은 '거리'인가 '깊이'인가
여기부터가 이번 본론입니다. 저의 가설은 '의존 관계의 거리가 짧은 구문일수록 생성하기 쉽다'였습니다. 하지만, 위의 결과는 '깊이'로 나누고 있습니다. 거리와 깊이는 별개입니다.
다행히 설계 부분에서 설명한 '블록 길이'(각 블록에 채워 넣은 nop 문 수, 0 / 8 / 32)가 있으므로, 깊이를 고정한 상태에서 열린 줄부터 닫힌 줄까지의 줄 수로서의 거리만 바꾼 효과를 볼 수 있습니다. 같은 깊이 속에서 블록 길이를 바꿔도 정답 수는 전혀 움직이지 않았습니다 (깊이 8: B 0/0/0, D 8/8/8, A 3/3/4, 각 8 생성).
즉, 첫 번째 파일럿 범위에서는 거리는 효과를 미치지 못하고, 동시에 열려 있는 블록의 개수가 효과를 미치고 있다는 것입니다.
그래서, 의존을 '정말로 짧게' 하는 구문을 두 가지 더 추가하여 실험했습니다 (P3a: 288 시행, 깊이 4/8/16, 블록 길이 0/32/128).
- G depth_end:
end 3처럼 '닫는 블록의 깊이 숫자'를 쓰는 경우. 바로 앞 줄의 깊이만 보면 쓰기 때문에 의존 거리는 1줄 - - F indent: Python 방식. 닫는 줄 없이 들여쓰기(indent)만으로 구조를 결정하는 경우. 이것 역시 바로 앞 줄만 보면 쓸 수 있습니다.
| 스타일 | 깊이4 | 깊이8 | 깊이16 | 합계 /72 | 주요 실패 |
|---|---|---|---|---|
B end | 13 | 1 | 1 | 15 |
닫기 누락 53 |
D end func foo | 22 | 21 | 19 | 62 |
구문은 맞지만 AST가 다름 9 |
G end 3 | 22 | 18 | 14 | 54 |
깊이 세기 실수 13 |
| F 인덴트 | 21 | 17 | 22 | 60 |
구문은 맞지만 AST가 다름 9 |
(각 셀 24 생성. 깊이 16의 숫자는 블록 길이 0/32/128에서 D 8/6/5, G 5/5/4, F 8/6/8입니다.)
G와 F 모두 B를 크게 상회했습니다. '국소적인 단서'는 확실히 효과가 있습니다. 다만 내용은 두 가지로 다릅니다.
G는 D에 미치지 못하고, 깊이 16에서는 end 3의 숫자를 틀리는 실패가 나옵니다. '바로 앞 줄만 보면 쓸 수 있는' 구문이라도, 깊이의 절대값을 계속 세는 것은 어려운 것 같습니다.
반면, F(인덴트)는 60 대 62로, D와 거의 차이가 없습니다. 깊이 16・블록 길이 128에서는 F가 더 높습니다. 이것을 순수하게 읽으면 '들여쓰기만으로 닫는 줄 없이도 이름 붙은 닫기와 비슷할 정도로 쓸 수 있다'가 되지만, 여기에는 의문이 있습니다. 모델은 사전 학습에서 Python, Markdown, YAML 등 들여쓰기를 구조 정보로 사용하는 텍스트를 대량으로 읽고 있습니다. 키워드를 nonce 어휘로 대체해도, '들여쓰기가 중첩을 나타낸다'는 읽기 자체는 지울 수 없습니다. 즉 F의 성적은 '의존이 짧아서 쓸 수 있다'인지 '들여쓰기 구조 처리를 학습했기 때문에 쓸 수 있다'인지, 이 실험에서는 구분할 수 없습니다. G가 D에 미치지 못한다는 것을 고려하면, 후자의 기여가 상당히 크지 않을까 의심합니다.
또 하나, 이번에는 블록 길이를 128까지 늘렸기 때문에, 깊이 8 이상에서 D/G/F가 서서히 떨어지는 거리 효과를 처음으로 볼 수 있었습니다. 거리는 '효과가 없다'가 아니라, **'깊이에 비하면 효과가 작다'**는 것이 정확한 표현입니다.
결과 3: import는 완전 한정자로 더 정확하게 쓸 수 있다
'닫는 라벨(closing label)'과 같은 구조의 질문을 다른 구문 요소에서도 시험해 보았습니다. 닫는 줄은 '훨씬 전에 열린 블록'에 대한 참조였지만, 프로그래밍 언어에는他にも '먼 정의에 대한 참조'가 있습니다. 대표적인 것이 import입니다.
그래서 미니 언어에 import 문을 추가했습니다. 의미는 '이미 닫힌 블록(모듈로 간주합니다) 안에서 바인딩된 변수를 현재 스코프로 가져오는 것'입니다. 변수 해결 순서는, 자신의 스코프의 let → 자신의 스코프의 import (나중 것이 우선) → 외부 스코프,와 정해져 있으며, 채점용 인터프리터도 이 의미론으로 2개 독립적으로 재구현하고 있습니다.
쓰는 방법을 두 가지로 준비했습니다.
- fq:
import n_root.n_mid.n_leaf
(루트로부터의 완전 경로) -
short: import n_leaf
(이름은 고유하기 때문에 이것만으로 의미가 결정됩니다)
의미는 완전히 같지만, 작성 방식만 다릅니다. 자바(Java)의 완전 한정 이름과 import를 사용한 후의 단순 이름 간의 관계를 상상해 보시면 대부분 이해할 수 있습니다. 생성 시 AST 표에는 import 대상 모듈명만 적혀 있기 때문에, 모델은 대상 모듈의 조상 체인(어떤 블록 안에 어떤 블록이 있는지)을 스스로 재구성해야 합니다. "더 많이 작성하게 하는 것이 어려워 보일 것" 같았지만, 결과는 정반대였습니다 (각 72회 시도, 두 조건 모두 동일한 프로그램 구조).
| 태스크 | fq | short | 둘 다 정답 | fq만 정답 | short만 정답 |
|---|---|---|---|---|---|
| T3 생성 | 55 | 50 | 41 | 14 | 9 |
| T1 독해 (닫은 직후) | 60 | 55 | 46 | 14 | 9 |
| T1 독해 (닫기 전) | 72 | 72 | 72 | 0 | 0 |
독해에서도 생성에서도 fq가 약간 더 높습니다. 차이는 작고, 같은 프로그램에서 결과가 나뉜 것도 14 대 9이므로 "경향"에 그치지만, 방향은 닫기 레이블의 결과와 동일했습니다.
결과 4: 들여쓰기와 이름 지정 닫기를 중첩해도, 이름 지정 닫기 단독을 넘지 못한다
"LLM은 Python을 대량으로 학습했으니, 들여쓰기 + end func foo가 가장 강력하지 않을까?"라고 생각하여 이것도 시도해 보았습니다 (P3c: 288회 시도, P3a와 동일한 프로그램 구조). 들여쓰기와 닫는 줄 모두를 검사하는 까다로운 조건입니다.
| 스타일 | 합계 /72 | 깊이 16・블록 길이 128 |
|---|---|
D end func foo (들여쓰기 무의미) | 67 | 7/8 |
F 들여쓰기만 | 64 | 7/8 |
H 들여쓰기 + end func foo | 64 | 7/8 |
I 들여쓰기 + end | 60 | 3/8 |
중첩해도 점수가 오르지 않았습니다. 여기서도 F(들여쓰기만)가 D와 3점 차이로, 같은 조건을 재측정했을 때의 변동 범위입니다. 들여쓰기만으로 이름 지정 닫기와 비슷한 수준으로 작성할 수 있다는 것 자체는 재현되었지만, 위에서 언급한 "들여쓰기 구조는 사전 학습되었다"라는 의문도 그대로 남아 있습니다. 흥미로운 점은 실패 내용인데, H와 I에서는 들여쓰기 위반도 닫는 줄 위반도 0이었습니다. 남은 실패는 모두 "구문은 맞지만 AST가 다름", 즉 어느 것을 어디에 넣을지 구조적인 오류였습니다. 구문의 단서는 이미 충분히 효과적이었고, 남아있는 오류는 단서를 추가해도 줄어들지 않는 종류의 것이었던 것 같습니다.
또 하나 중요한 부산물이 있습니다. D는 P3a와 P3c에서 동일한 72회 생성을 두 번 측정했고, 결과는 62와 67이었습니다 (8개의 프로그램이 오답→정답, 3개가 정답→오답). 1회의 측정만으로 72개 중 5 정도의 차이는 같은 조건을 재측정해도 나올 수 있다는 것입니다. 위의 표에서 D/F/H의 차이는 이 범위 안에 들어갑니다.
해석: "짧은 의존성"이 아니라 "각 줄에서 대조할 수 있는 중복성"
첫 번째 가설은 "의존 거리(dependency distance)가 짧을수록 생성하기 쉽다"였습니다. 결과를 종합해 보면,
- 순수한
end나}는 깊어지면 무너진다 (깊이 8에서 전멸) - 거리를 줄인 구문은 크게 개선된다. 다만end 3은end func foo에 도달하지 못했고, 들여쓰기는 비슷한 수준이었다. 들여쓰기의 좋은 성적은 사전 학습을 통해 들여쓰기 구조를 배우고 있다는 의심이 강하여 "의존이 짧기 때문"이라고 단정하기는 어렵다 - 이름 지정 닫기나 완전 한정 import처럼, 의미적으로 중복성이 있지만, 그 줄만으로 "무엇을 닫았는지/무엇을 참조했는지"를 대조할 수 있는 정보를 작성하게 할 때 가장 좋다 - 단서를 쌓아도 남는 오류는 구조 자체의 오류이며, 구문으로는 줄어들지 않는다
라는 형태가 됩니다. LLM에게 쓰기 쉬운 것은 "의존이 짧은 구문"이라기보다는, **"작성한 줄을 그 자리에서 정답과 대조할 수 있는 구문"**이라고 하는 것이 이번 결과에는 더 적합합니다. Scala 3의 end foo도 자바(Java)의 완전 한정 이름도, 인간에게는 중복성이라고 생각했던 것들이 LLM의 생성 정확도에도 영향을 미치고 있을 가능성이 있습니다.
다만, 이것은 어디까지나 이번 조건에서의 이야기입니다.
- 모델은 1종류, 추론 노력(reasoning effort)은 low로만 측정했습니다.
- 각 조건은 단 한 번씩만 측정했습니다. 위에서 보셨듯이, 같은 조건을 다시 측정하는 것만으로도 72개 중 5개 정도는 작동하므로, 그 범위의 차이는 읽을 수 없습니다. 사전에 설계를 고정하여 재실험은 앞으로 진행할 예정입니다.
- 설명문이 있는 인공 언어로, few-shot 예시 8개를 고정한 설정입니다. 실제 언어와는 다릅니다.
- nonce 어휘를 사용하여 키워드의 영향은 약화했지만, 기존 구문의 영향까지 배제하기는 어렵습니다.
- 들여쓰기 조건의 경우, Python / Markdown / YAML 등에서 이미 학습된 들여쓰기 구조의 영향을 분리할 수 없습니다.
- 정확도의 차이를 '어텐션 메커니즘(attention mechanism)의 부하'와 같은 내부 메커니즘으로 번역하기는 어렵습니다.
예비 실험에서는 이러한 결과가 나왔다는 정도로 읽어주시면 감사하겠습니다.
맺음말
구문 설계에 대한 논의는 오랫동안 '인간에게 읽기 쉬운가'를 축으로 해왔습니다. LLM이 코드를 작성하는 비율이 높아진다면, 'LLM에게 쓰기 쉬운가'도 설계 기준 중 하나가 될 수 있을 것입니다. 이번 작은 실험에서 얻은 답은 '장황하더라도 그 자리에서 대조할 수 있는 정보를 작성하게 하는 것'이었습니다.
실험의 모든 데이터(요청, 응답, 채점, 실패 사례)와 재현 절차는 공개했습니다. 리포트는 저장된 데이터만을 기반으로 생성되었으며, 그래프의 숫자는 모두 개별 시도까지 추적할 수 있습니다.
- 공개 리포트: https://kmizu.github.io/syntax-locality-experiments/
- 리포지토리: https://github.com/kmizu/syntax-locality-experiments
여러분도 자신이 좋아하는 언어의 구문을 LLM에게 작성하게 하고, '어디서 깨지는지'를 관찰해 보는 것은 어떨까요?
그럼, 또 뵙겠습니다!
토론
AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기