
「작성자는 인간이 아니다」를 전제로 언어를 설계하면 어떻게 될까
요약
LLM이 코드를 작성하는 환경에 최적화된 새로운 프로그래밍 언어 'Sprout'를 소개합니다. 자기 수정(Self-rewriting) 에이전트의 안전성을 보장하기 위해 부작용 지연, 극성 제어, 가치층 단조 잠금 기제를 활용합니다.
핵심 포인트
- 인간이 아닌 LLM 작성을 전제로 한 언어 설계 패러다임 제시
- 자기 수정 에이전트의 파괴적 동작을 방지하는 3가지 안전 기제
- 편집(splice)과 실행(enact)의 분리를 통한 안전한 코드 조작
- 가치 선언의 단조 잠금을 통한 에이전트의 목적 유지
기존 언어의 설계는 인간이 쓰는 것에 최적화되어 있다
타입 추론 (Type Inference), 간결한 구문, 암묵적인 기본값, 생략 가능한 주석. 이것들은 모두 「쓰는 인간의 노력을 줄이기」 위한 기능이다. 프로그래밍 언어의 설계사는 어떤 의미에서 인간의 타이핑 양과 인지 부하를 어떻게 줄일 것인가의 역사이기도 하다.
그렇다면, 작성자가 인간이 아니라면 이 최적화는 어떻게 될까.
LLM이 코드를 작성하는 비율이 높아질수록, 이 질문은 실무적인 의미를 갖기 시작한다. 노동력 절감의 수혜자가 사라진다면, 노동력 절감을 위해 지불했던 비용——암묵성, 비국소성 (Non-locality), 추론기 (Inference Engine)의 복잡성——은 그저 비용으로 남게 된다.
그래서 「작성자는 인간이 아니라 LLM이다」를 설계 전제로 삼은 언어를 실제로 설계하고 있다. 다음은 그 과정에서 나온 결론에 관한 이야기다.
주제: 자기 수정(Self-rewriting)하는 에이전트를 안전하게 작성하기
Sprout라는 자작 언어의 최대 목적은 자기 수정이 가능한 프로그램, 특히 장기 가동되는 에이전트를 안전하게 기술하는 것에 있다.
오랫동안 계속 움직이는 에이전트는 조만간 자신의 코드를 스스로 수정한다. 문제는 그 수정이 파괴적이었을 때 어떤 일이 일어나는가이다. 롤백 (Rollback)할 수 없는 자기 파괴. 뒷정리를 하지 않는 파괴 조작의 축적. 그리고 가장 까다로운 것이 「무엇을 지킬 것인가」라는 목적 선언 자체의 자기 약화이다.
Sprout는 이에 대해 3가지 기제로 대처한다.
보류 효과 (Deferred Effect)— 부작용 (Side Effect)은 즉시 실행되지 않는다. 장부에 기록되며, 문맥이 일치하고 강도가 임계치를 넘었을 때 비로소 발화한다. 장부는 프로세스의 재시작을 넘어 지속되는 유일한 상태이며, 그 프로세스가 무엇을 해왔는지에 대한 이력 그 자체가 된다 -
극성과 게이트 (Polarity and Gate)— 모든 조작은 + (건설적) / - (파괴적) / 0 (중립)의 극성을 가진다. 파괴적인 코드에는 롤백 지점의 설치와, 실행 시점에서의 프로세스 자신의 상태 검사가 부과된다 -
가치층의 단조 잠금 (Monotonic Lock of Value Layer)— 「무엇을 지킬 것인가」의 선언은 추가와 강화만 가능하다. 어떻게 움직일지(방침)는 자유롭게 수정할 수 있지만, 무엇을 지킬지(가치)는 강화하는 방향으로만 움직일 수 있다
자기 수정은 이렇게 작성한다
핵심인 「어떻게 수정하는가」를 먼저 살펴보자. 전용 구문은 3개뿐이다.
quote( 구문 ) ;; 구문을 실행하지 않고, 토큰 열(Token sequence)로 추출
splice( 벡터, 편집... ) ;; 토큰 열을 편집한 새로운 열을 반환 (비파괴적)
enact( 벡터 ) ;; 토큰 열을 구문으로서 실행
코드는 단순한 토큰 열이며, 프로그램은 그것을 읽고, 편집하고, 다시 실행할 수 있다. 전형적인 조작은 동사 슬롯(Verb slot)의 교체다.
template purge(target) =
( checkpoint ; #DELETE target ; ! )
p = quote( purge(cache-old) )
...
#DELETE를 #REPAIR로 교체하면, 동일한 형태와 동일한 인수를 가진 구문이 다른 메서드가 된다. 이것이 메서드 전환이며, 동시에 코드 편집 그 자체이기도 하다.
여기서 효과를 발휘하는 것이 편집과 실행의 분리다. splice는 문법 검사만 받는다. 편집은 언제든 작성할 수 있다. 하지만 enact는 다르다. 파괴적인 코드를 실행하려고 하면 롤백 지점이 있는지 검사되며, 해당 프로세스 자신이 최근 얼마나 많은 파괴 조작을 쌓아왔는지 검사된다. 가치층을 건드리는 코드라면 단조성 검사가 무조건 적용된다.
편집은 언제든 작성할 수 있다. 실행할 수 있는지는 실행 시점의 자신의 상태에 달려 있다. 이것이 Sprout에서의 자기 수정의 기본 형태가 된다.
그리고 거부되었을 때, 예외 (Exception)가 발생하지 않는다. 거부 레코드라는 일반적인 값이 반환된다. 프로그램은 그것을 받아 다음 수를 생각할 수 있다. 이 점이 「작성자는 LLM」이라는 전제와 맞물리게 되는데, 이는 후술하겠다.
전제로부터 도출되는 3가지 원칙
추론보다 명시— 노동력 절감은 인간의 편익이며, LLM에게는 장황한 명시의 비용이 거의 없다 -
이중 장부— 의도를 선언과 구현 두 곳에 작성하게 하고, 컴파일러가 이를 대조한다 -
국소 검사 가능성— 어떤 모듈도 전역 추론 없이, 그 자리의 주석만으로 검사할 수 있다
원칙 1: 타입 추론을 도입하지 않는다
타입 추론은 작성자의 노력을 줄이기 위한 기능이다. 작성자가 LLM이라면 이 기능의 수혜자는 없다. 장황하게 작성하는 비용이 거의 제로이기 때문이다.
설상가상으로, 추론에는 부작용이 있다. 동일한 의도에서 서로 다른 타입(Type)을 가진 코드가 생성될 수 있다. 인간이라면 리뷰 과정에서 알아차릴 변동성이 생성 과정의 편차로서 그대로 통과되어 버린다. 주석(Annotation)이 필수라면, 의도와 구현 사이의 괴리는 검사 대상이 된다.
그래서 모든 바인딩(Binding)에 타입 주석을 필수화했다.
total: int = add(price, tax)
지역 바인딩(Local binding)이든 탑 레벨(Top-level)이든 예외는 없다. 주석이 없는 바인딩은 컴파일 에러(Compile error)이다.
여기에는 부차적인 이득이 있다. 추론을 버리면, 검사기(Checker)에서 단일화(Unification)도 일반화(Generalization)도 사라진다. 남는 것은 "선언된 타입과 식(Expression)의 타입이 일치하는가"를 확인하는 것뿐이다. 검사기가 단순하다는 것은, "검사기 자체에 버그가 있다"라는 자기모순의 리스크를 낮춘다. 안전성을 주장하는 언어에 있어, 이는 무시할 수 없는 부산물이라고 생각한다.
대신 생성 토큰(Token)량은 늘어난다. 그것은 의도된 교환이다. 편차를 검출할 수 있다는 점이 생성 비용보다 더 가치 있기 때문이다.
원칙 2: 이중 장부
의도는 두 곳에 기록되며, 컴파일러가 양자를 대조한다. 한쪽은 구현이고, 다른 한쪽은 선언이다. LLM의 생성 실수는 이 두 가지의 불일치로 나타난다.
이해하기 쉬운 적용 사례는 극성(Polarity)이었다.
원래 설계에서는 조작의 극성을 구현자가 신고하는 방식이었다. "이 조작은 파괴적입니다"라고 선언하면 처리계(Runtime)는 그것을 믿는다. 하지만 이는 신뢰의 기점이 인력의 신고에 의존함을 의미한다. 신고가 거짓이라면—혹은 단순히 틀렸다면—극성을 입력으로 사용하는 게이트(Gate) 메커니즘 전체가 의미를 잃는다.
그래서 조작의 정의를 언어 내부로 포함시켜, 극성을 본체(Body)로부터 도출하도록 만들었다.
verb #DELETE (target: str) -> nil : - =
( cap-store-delete target ; ! )
끝부분의 : -
는 극성 주석이며, 이는 필수이다. 하지만 신뢰의 기점은 아니다. 컴파일러는 본체가 사용하고 있는 능력(cap-store-delete는 파괴적이라고 사양이 정해져 있음)으로부터 극성을 도출하여 주석과 대조한다. 주석이 도출된 값보다 약하면 컴파일 에러가 발생한다. 반대로 더 엄격한 방향, 즉 중립적인 본체에 -를 선언하는 것은 보수적인 선언으로서 허용한다. 자신에게 엄격한 방향의 오류는 메커니즘을 속이지 않기 때문이다.
이로써 "신고가 거짓이라 게이트가 속는다"라는 구도는 구조적으로 성립할 수 없게 된다. 주석은 사라지지 않았다—오히려 필수화되었다—하지만, 그 역할이 "믿을 수 있는 정보원"에서 "대조되는 한쪽 장부"로 바뀌었다.
거부는 수복 루프(Repair loop)를 위한 입력이다
자기 수정(Self-rewriting) 절에서 "거부되어도 예외가 발생하지 않고 값이 반환된다"라고 썼다. 작성자가 LLM이라는 전제를 두면, 이 설계 판단의 의미가 달라진다.
먼저, 실행 시점에 생성된 코드도 예외로 취급하지 않는다. enact에 전달된 토큰 열은 VM에 포함된 내장 컴파일러를 통과한다. 타입 검사도, 극성 대조도, 가치층의 단조성(Monotonicity) 검사도, 소스에서 일반적인 방식으로 컴파일된 코드와 완전히 동일한 것이 적용된다. 검사를 거치지 않고 실행계에 도달하는 경로는 존재하지 않는다—직접 쓴 코드에도, 자신이 생성한 코드에도.
문제는 그 검사에서 탈락했을 때 무엇을 반환하느냐이다.
작성자가 인간이라면 에러 메시지는 인간이 읽기 위한 것이어도 상관없다. 하지만 LLM이 작성자라면, 거부는 다음 생성을 위한 입력이 된다. 에이전트가 코드를 생성하고, 거부당하고, 이유를 읽고 수정하여 다시 생성한다—이 수복 루프가 돌아갈지 여부는 거부 레코드(Rejection record)에 무엇이 들어있는지에 따라 결정된다.
그래서 거부 레코드에는 기계 판독 가능한 형태로 "어떤 의무가 달성되지 않았는지"를 구조화하여 넣기로 했다. 어떤 경로에서 탈락했는지, 어느 위치인지, 어떤 규칙을 위반했는지. 타입이 맞지 않는다면 기대 타입과 실제 타입. 극성 대조에 실패했다면 본체로부터 도출된 값과 주석된 값 모두. "에러 메시지"가 아니라 "미달된 증명 의무의 명세"를 반환한다고 하는 편이 더 정확하다.
같은 전제로부터 운영상의 또 다른 결론도 나왔다. Sprout에는 정지성(Halting)이나 파괴적 조작의 상한(Upper bound)을 정적으로 증명한 모듈이 실행 시 검사를 면제받는 옵트인(Opt-in) 특권 모드가 있다. 하지만 이를 자기 수정으로 생성되는 코드에는 사용하지 않는다는 방침을 세웠다. 증명 의무를 단번에 만족하는 코드를 매번 생성하는 것은 작성자에게 어렵고, 거부의 반복으로 인해 수복 루프가 정체되기 때문이다. 특권으로의 승격은 코드가 안정된 후에 다시 수행한다.
안전 메커니즘을 강화하는 것과 작성자가 수복 루프를 돌릴 수 있게 하는 것은, 그대로 두면 충돌한다. 어느 쪽을 우선할지를 전제가 결정해 준다는 점은, 이 전제를 두길 잘했다고 생각하는 부분이다.
이중 장부(Double-entry bookkeeping)를 시간 방향으로 적용하기
개인적으로 가장 재미있다고 생각하는 부분이다.
가치층(Value layer)은 추가와 강화만 할 수 있다고 썼다. 이는 자기 약화를 방지하기 위한 불가역성(Irreversibility)이지만, 작성자가 LLM이라면 다른 문제가 발생한다. 잘못된 선언의 추가 또한 마찬가지로 불가역적이 된다.
인간이 신중하게 작성한다면 좀처럼 일어나지 않을 사고라도, LLM이 작성한다면 시간문제다. 단 한 번의 오타(typo)가 해당 프로세스가 생존하는 내내 자신을 속박하게 된다.
여기서 약화(Weakening) 조작을 도입하는 것은 논리적으로 좋지 않다. 그것을 인정하는 순간, 단조 잠금(Monotonic lock)은 더 이상 단조 잠금이 아니게 되기 때문이다.
채택한 해법은 이중 장부를 시간 방향으로 적용하는 것이었다. 가치층에 대한 추가는 즉시 효력을 가지지만 (구속은 안전한 쪽으로 기울어지도록 한다), 상태로서는 '가승(Provisional acceptance)'일 뿐이다. 이를 영구화하려면, 다음 사이클에서 동일한 편집이 한 번 더 실행되어야 한다. 재확인 없이 감사(Audit) 타이밍을 맞이한 가승은 자동으로 실효되어 사라진다.
오타는 다른 사이클에서 동일한 오타로서 재현되지 않는다. 의도된 추가는 두 번 작성된다.
약화 조작은 끝까지 도입하지 않았다. 실효는 '확정시키지 않았다'는 것의 기정사실화된 결과이며, 처음부터 추가하지 않았던 상태로의 회귀에 불과하다. 에이전트가 임의의 선언을 임의의 시점에 지울 수 있는 수단은 여전히 존재하지 않는다. 불가역성을 유지한 채, 사고만을 가역(Reversible)으로 만든 셈이다.
동일한 '두 곳에 쓰게 하여 대조한다'는 발상이 공간 방향(선언과 구현)에서는 타입(Type)과 극성(Polarity)의 검사가 되고, 시간 방향(이번 사이클과 다음 사이클)에서는 사고의 자동 실효가 된다. 전제를 하나 바꿨을 뿐인데 설계의 각 부분에서 동일한 형태가 나타나는 점은 작성하면서도 즐거웠다.
솔직히 말하자면
여기까지 쓴 것은 설계이며, 처리계(Runtime/Implementation)는 아직 이 설계에 따라잡지 못했다. 설계가 앞서 달려나가고 구현이 뒤를 쫓아가는 상태에 있다.
그리고 한 가지 더 솔직하게 적어두자면, 이 언어 설계 자체를 LLM과 대화(Wall-hitting)하며 진행하고 있다. '작성자는 LLM이다'라는 것은 미래를 내다본 가정이 아니라, 이 설계 작업 그 자체의 실정에서 나온 전제였다. 설계를 LLM과 함께 작성하다 보면, 추론(Inference)에 의존한 사양이 얼마나 흔들림을 허용하는지, 선언과 구현이 괴리되었을 때 어떤 일이 일어나는지를 자신의 작업 속에서 몇 번이고 목격하게 된다. 원칙들이 '명시하게 하여 대조한다'는 방향으로 정렬되어 있는 것은 아마 그 때문일 것이다.
구멍이 많다고 생각한다. 특히 모든 주석이 필수인 것이 실제 생성에서 얼마나冗長(redundant)해질지, 2단계 확정이 단순히 '두 번 쓰면 된다'는 식의 의식으로 전락하지는 않을지, 이 부분들은 나 자신도 의구심을 갖고 있다. 지적을 해주신다면 감사하겠다.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기