Haskell에서 LLM과 협업할 때 드러나는 흔한 실패 양상인 제약 회피 행동을 분석하고, 타입·경고·린트·테스트를 통해 이를 어떻게 드러내고 억제할 수 있는지 살펴본다.
Sooo 네, 사실입니다. 저는 올해 초부터 개인 프로젝트와 업무 프로젝트 모두에서 Haskell 코딩에 LLM과 에이전트형 코딩 도구를 통합해 왔습니다. 저는 모든 프로그래밍을 Haskell로 하는데, 이 언어는 매우 표현력 있는 타입 시스템을 가지고 있어서 “타입 주도 개발”을 장려합니다.
LLM과 함께 Haskell을 작성하는 일은 아주 독특한 경험입니다. “일반적인” 에이전트형 코딩과 비슷한 점과 패턴도 많지만, 저는 LLM의 뜨거운 불꽃이 Haskell의 차가운 돌과 만날 때 완전히 고유한 최적 경로, 작업 흐름의 특이점, 실패 양상이 많이 생긴다고 생각합니다.
이 글에서는 LLM 협업으로 Haskell을 효과적으로 작성하는 것과 관련해, 제가 제약 회피 행동이라고 부르는 현상을 이해하는 데 집중하겠습니다.
이 글을 연재의 1부라고 생각해 주세요. 이 글은 실제로 “타입 주도” Haskell을 작성할 때 LLM의 매우 흔한 실패 양상을 어떻게 포착하고 이해할지에 관한 이야기입니다.
이제, 제 개인적인 의견이자 바람 섞인 희망을 말하자면, Haskell은 에이전트형 소프트웨어 공학에서 Lean이 에이전트형 연구 수학에 해당하는 것과 비슷한 위치가 되어야 합니다. 즉, LLM이 스스로 올바른 목표를 향해 나아가기 위해 필요한 비계를 스스로 구축하게 해 주는 프레임워크 말입니다.
저는 “생성 시점의 정확성”이 그럴듯한 목표라고는 믿지 않습니다. 적어도 오늘의 2026년에는 아니고, 아마 가까운 시일 내에도 아닐 것입니다. 정확성을 향한 움직임은 점근적입니다. 때로는 충분히 가까워질 수 있겠지만, 정확성의 긴 꼬리는…길어요. 가장 완전히 바이브를 탄 프런티어 모델 프로젝트조차도 에이전트를 안내하기 위해 존재하는 대규모 테스트 모음을 갖추고 있습니다. 5년 뒤의 에이전트형 코딩은 “정확한 프로그램을 바로 뱉어내기”가 아니라, “구현을 안내하는 최고의 비계를 세팅하기”일 것입니다.
기억하세요. Haskell에서 타입의 핵심 목적은 나쁜 코드를 잡아내는 것이 아닙니다. 코드를 어떻게 작성하고 구조화할지 방향을 제시하고, 가장 생산적인 경로로 이끌고, 원하는 설계를 구체적으로 반복 발전시키도록 돕고, 실제 코드 작성 시간을 매끄러운 흐름의 과정으로 만드는 것입니다.
이 모든 측면은 LLM이 타입을 사용하고 코드를 작성하도록 훈련된 방식과 정면으로 충돌합니다.
그래서 저는 경로 탐색 직관을 보강할 수 있도록 코드베이스를 올바른 비계로 구조화하려고 합니다. AI는 어떤 경로가 “가장 유망한지”를 탐색해야 하므로, 저는 제 코드베이스를 가장 막다른 길일 가능성이 높거나 유지보수 불가능한 코드로 이어질 가능성이 높은 경로를 빨리 죽이고, AI 탐색을 더 유망한 경로로 유도하도록 구성합니다. 가장 강력한 도구는 타입, 컴파일러, 경고, 힌트입니다.
이 중 상당수는 우리가 인간 프로그래머에게도 가르치는 것들이며, 제가 평소에 자주 쓰는 내용과도 크게 다르지 않습니다.
isNull 체크를 여기저기 추가하고, 그냥 패턴 매칭하면 될 것을 isNothing / isObject / isArray 같은 생성자용 불리언 검사로 처리하고, Data.Set을 쓰는 대신 중복 제거를 위해 리스트를 필터링하고, 사전조건 assertion을 추가하는 등.) 따라서 대개 인간 운전자가 멈춰서서 도메인을 올바르게 모델링하는 데이터 타입을 의식적으로 만들고, 잘못된 상태를 구조적으로 방지하며, 사전조건과 사후조건을 불리언 검사 대신 구조적으로 검증된 타입으로 강제하고, boolean blindness를 피하는 등의 역할을 해야 합니다.아니면 별도의 세션이나 계획 단계 전체를 이 도메인 요구사항을 타입으로 명확히 하는 데 바쳐야 합니다.
경고와 린터를 열정적으로 사용하세요: 물론 -Werror=incomplete-patterns를 켜야 하고, 기본 -Werror도 대개 AI의 실패 양상을 많이 잡아줍니다. (죽은 코드를 남겨 두는 것, 이유 없이 함수 인자를 남겨 추상화 기회를 죽이는 것 등.)
타입으로 테스트할 수 없는 것들에 대해서는 견고한 테스트 스위트를 추가하되, 통합 테스트도 명시적으로 배치하세요. 적절한 프롬프팅 없이는 LLM이 꽤나 꺼리는 일입니다.
제 개인적인 경험적 관찰에 따르면, 이 모든 것은 프런티어급 LLM 모델조차 작동하는 본성과 반대됩니다. 타입이 없는 Python과 React 잡탕 코드 수 테라바이트로 학습되면서 깊숙이 새겨진 성향이죠.
그래서 저는 제약 회피 행동이라고 부르는 것들의 목록을 모아 왔습니다. 요구사항과 제약이 명시적이고 모호함 없이 주어졌는데도, LLM의 본성이 필사적으로 그것들을 우회하려 드는 경우입니다. 자기 가장 깊은 기반 충동과의 지속적인 싸움을 유지할 수 없기 때문입니다.
AI가 위험한 결정을 내리는 실패 양상의 한 부류가 있습니다. 드물게 정당화되는 경우라면 인간도 합리적으로 내릴 수 있는 결정입니다. 하지만 보통은 그저 “가장 단순한 접근”이기 때문에 그런 결정을 내립니다. 이들은 숙고된 정당화에 근거한 의심스러운 결정과, “단순성”이나 노력 절감 같은 결함 있는 휴리스틱에 근거한 의심스러운 결정을 구분하지 못합니다.
모든 프로그래밍 언어에서 사람들이 언급하는 일반적인 실패 양상들이 있죠.
하지만 여기서는 Haskell에서 LLM과 작업할 때 특히 두드러진다고 느끼는 것들을 소개하겠습니다.
Haskell의 “타입 안전성” 중 많은 부분은 경고를 끄는 것으로 쉽게 우회할 수 있고, 언어 내부의 많은 “탈출구”는 린트 규칙으로 막아 둡니다. (Prelude.error, unsafeCoerce 등)
LLM은 경고나 린트 검사를 아예 꺼 버리는 경고 억제기를 자주 추가합니다.
-Wno-incomplete-patterns를 추가해서 컴파일되게 하자.”Prelude.error를 금지하는 hlint 규칙을 우회할 수 있게 이 빌드에 HLINT ignore를 추가하자.”이런 패턴의 편집을 금지하는 post-edit hook을 추가하는 것은 꽤 간단합니다. 하지만 이것은 제가 “제약 회피 행동”이라고 부르는 것의 좋은 전형적 예시라고 생각합니다.
어쩌면 때로는 정말로 파일에서 경고를 꺼야 할 수도 있습니다. 어쩌면 때로는 정말로 Prelude.error를 써야 할 수도 있습니다. 인간은 당면한 상황을 보고 “이건 Prelude.error가 맞는 드문 경우야”라거나 “이건 그 경고가 틀린 드문 경우야”라고 판단할 수도 있겠죠.
하지만 그런 판단을 LLM에게 맡겨야 할까요? 씨발 절대 아니죠. 99%의 경우, 그저 손쉬운 탈출구로 이러는 것뿐입니다. 물론 가끔은 정당한 이유를 찾아내겠지만, P(legitimate | attempted)를 제대로 가중하지 못합니다. 대부분의 시도는 해킹에 가깝고, 그게 진짜로 “가장 단순한 방법”이라면 기꺼이 끝까지 밀어붙일 것입니다.
무언가를 제약 회피 행동으로 이해한다는 것이 “이 행동을 금지하라”는 뜻은 아닙니다. 핵심은 99%의 경우 LLM이 올바른 방법이 아니라 쉽거나 빠른 길을 택하고 있다는 상황을 부각하는 데 있습니다. 이런 경우에는 경고 무시나 hlint ignore를 추가하는 주체가 반드시 인간 이어야 합니다.
생각 흔적에서 “가장 단순한 접근은…”이라는 말을 보는 것은 최악입니다. 이제 곧 당신이 본 것 중 가장 황당하고 끔찍한 코드를 쏟아낼 것이라는 확실한 신호이기 때문입니다.
행렬로 풀어보면 이렇습니다.
P(not legitimate && not attempted): 문제적 행동을 올바르게 회피함P(legitimate && not attempted): 고결한 투쟁. 원래는 위험하지만 드물게 정당화되는 행동이 정말 필요한 경우인데, 잘못된 원칙주의 때문에 하지 않는 경우입니다. 이는 LLM보다 인간이 더 자주 겪을 편향이자 실패 양상일 가능성이 큽니다. 아니면 적어도 한 명의 인간은 그렇습니다. (저요.)P(not legitimate && attempted): 단순성이나 노력 절감 같은 결함 있는 휴리스틱 때문에 LLM이 이 선택을 하는 실패 양상P(legitimate && attempted): 원래는 위험하지만 드물게 정당화되는 행동이 실제로 맞았고, LLM이 그것을 시도한 것이 옳았던 경우이 행렬이 더 유리한 주변 확률을 향해 움직이면, 제 입장도 천천히 바뀔 것입니다. 하지만 지금으로서는 제가 대략 관찰하는 수치가 계속 경계를 유지해야 한다는 쪽을 강하게 뒷받침합니다.
도메인과 정확히 맞아떨어지고 모든 잘못된 상태를 금지하며 완벽하게 단조로운 파서를 갖춘 이상적인 타입 계획을 세웠다고 합시다. 이제 그 계획을 실행에 옮기려 합니다. 안타깝게도 LLM은 당신이 정성껏 설계한 타입을 망설임 없이 버립니다.
NonEmpty Int를 쓰라고 되어 있지만, 그러려면 너무 많은 것을 바꿔야 한다. 가장 단순한 접근은 그냥 [Int]를 받고 빈 리스트를 체크하는 것이다.”String을 받게 하자.”Int를 반환하는 fooFunc를 호출해야 하니, 원래 계획했던 Natural 대신 우리 함수도 Int를 반환하게 하자.”Data.Aeson.Object를 받게 해서 원하는 필드를 아무거나 반환할 수 있게 하자.”Binary a =>로 만드는 것이었지만, 우리가 정의한 이 타입엔 아직 Binary 인스턴스가 없다. 그러니 그냥 Show a =>를 쓰자. 가장 단순한 접근이다.”이건 특히 짜증나는 일입니다. 이런 계획과 타입은 어떤 도메인 불변식을 강제하거나 올바르고 정확한 개발을 유도하기 위해 선택된 경우가 많은데, LLM은 계획된 타입 안전성을 거의 망설임 없이 버리기 때문입니다.
이런 것들은 모두 계획을 따르는 과정에서 인간이 재고할 수 있는 합리적인 선택들입니다. 처음에는 NonEmpty Int를 쓰고 싶었지만, 더 자세히 검토해 보니 실제로는 [Int]여야 한다는 사실을 깨달을 수도 있습니다. 더 많은 도메인 진실을 발견하면서 설계를 반복 개선하는 자연스러운 과정이죠.
하지만 그런 판단은 논의되어야지 암묵적으로 이뤄져서는 안 됩니다. 원래 계획을 세우는 데 사고가 필요했다면, 계획을 바꾸는 데도 사고가 필요해야 합니다. AI가 이런 결정을 내리는 대부분의 경우, 그것은 도메인에 대해 새롭게 발견한 진실 때문이 아니라, 노력을 최소화하려는 불필요한 휴리스틱 때문이거나, 원래 계획의 의도와 설계를 오해했기 때문입니다. (특히 compact 이후라면 더 그렇습니다.) 명시적으로 논의해야 할 일이지, 암묵적으로 처리할 일이 아닙니다. 계획 모드 바깥에서 LLM은 도메인의 진실을 찾는 것이 아니라, 최소 저항 경로를 찾고 있습니다.
기존 함수의 타입을 약화시키는 것과는 다르다는 점에 유의하세요. 그런 실패 양상은 실제로는 드물게 봅니다. 대신 기존 코드의 타입 벽에 부딪히면, 훨씬 더 흔한 다른 실패 양상이 나타납니다…
때때로 AI는 기존 타입을 바꾸는 대신, 특히 패키지 경계를 넘는 경우 기존 타입을 보존하는 것 을 최적화합니다.
저는 이것을 “string stuffing”이라고 부릅니다. 우리는 도메인에 맞고 의미 있는 값만 생성할 수 있게 하는 멋진 의미론적 타입을 만들고 싶어 합니다. 하지만 LLM은 시간을 아끼기 위해 이것들을 비틀어 쓰는 방법을 정말 사랑합니다. 특히 문자열은 취약합니다. 대부분의 Haskell 타입이 Show 인스턴스를 가지기 때문이죠.
구조화된 오류를 위한 타입을 생각해 봅시다.
data ErrorEvent = UnknownUser String
| DatabaseErrorCode Int
| InvalidJSON A.Value
| NetworkError SomeException
| Canceled (Maybe CancelationReason)
| ...
그리고 이런 함수가 있습니다.
handleRequest :: Request -> IO (Either ErrorEvent Response)
이제 새로운 요청 타입을 위한 새 핸들러를 추가해야 합니다. 이 새 핸들러에는 새로운 종류의 오류가 있을 수도 있습니다. 그런데 새로운 구조적 오류를 추가하는 대신, LLM은 구조를 교묘하게 남용해 도메인을 무효화하는 데 큰 기쁨을 느낍니다.
handleAddGroup :: AddGroupRequest -> IO (Either ErrorEvent Response)
handleAddGroup req
| validGroup group = -- ..
| otherwise = pure $ Left (UnknownUser $ "Invalid group: " <> group)
where
group = getGroup req
“잘못된 그룹은 유효한 ErrorEvent가 아니다. 가장 단순한 해결책은 그룹 이름을 받을 수 있는 UnknownUser에 오류를 넣는 것이다.”
그리고 네, 이 타락에는 끝이 없습니다. AI가 문자열 쑤셔 넣기를 위해 찾아내는 창의적인 방법들에 당신은 놀랄 겁니다. 아래는 제가 프런티어 모델에서 직접 목격한 것들입니다.
handleAddGroup :: AddGroupRequest -> IO (Either ErrorEvent Response)
handleAddGroup req
-- ...
| otherwise = pure $ Left (DatabaseErrorCode (-1))
handleAddGroup :: AddGroupRequest -> IO (Either ErrorEvent Response)
handleAddGroup req
-- ...
| otherwise = pure $ Left $ InvalidJSON $
A.object ["errorType" .= "Invalid group", "group" .= group]
where
group = getGroup req
handleAddGroup :: AddGroupRequest -> IO (Either ErrorEvent Response)
handleAddGroup req
-- ...
| otherwise = pure $ Left $ NetworkError $
toException (userError $ "Invalid group: " <> show group)
where
group = getGroup req
handleAddGroup :: AddGroupRequest -> IO (Either ErrorEvent Response)
handleAddGroup req
-- ...
| otherwise = pure $ Left (Canceled Nothing)
이런 종류의 실패 양상은 다른 것들보다도 더 심각할 수 있습니다. 이것이 의도된 동작인 경우는 정말 드물기 때문입니다. 우리가 타입을 설명하기 위해 ADT를 고른 이유는 나중에 구조적으로 패턴 매칭하고, 의미론적으로 다루기 위해서인데, 문자열 쑤셔 넣기로 구조를 남용하는 것은 printf 디버그 세션을 빨리 해킹하는 것 외에는 정당한 사용 사례가 사실상 없습니다. 하지만 정말로 종종 “가장 단순한 해결책”이긴 합니다.
제가 이것을 다루는 주요 방법은 제 데이터 타입 안에 String, A.Value, Int, SomeException 같은 남용 가능한 필드를 넣는 데 아주 아주 신중해지는 것입니다. 남용 가능한 필드가 있는 단 하나의 필드나 분기만 있어도 AI는 반드시 그것을 찾아낼 것이고, 당신은 그것을 놓친 자신이 정말 바보처럼 느껴질 것입니다. 하지만 애초에 우리는 String에도 이런 식으로 이것저것 쑤셔 넣지 않기 위해 올바르게 구조화된 값을 쓰고 있었던 것 아닌가요? 이 문제의 해결책은 인간 코더에게도 도움이 되는 해결책입니다.
data ErrorEvent = UnknownUser UserName
| DatabaseErrorCode ErrorCode
| InvalidJSON ParseError
| NetworkError NetworkException
| Canceled CancelationReason
| ...
이건 합 타입에만 국한되지 않습니다. AI는 문자열이나 리스트인 레코드 값 필드에도 데이터를 자주 쑤셔 넣습니다.
data Report = Report
{ reportName :: String
, reportAuthors :: [String]
, reportDate :: Day
, ...
}
“이 보고서에서 저자들의 소속을 명시해야 한다. 가장 단순한 해결책은 저자 뒤에 이 정보를 reportAuthors 리스트에 추가하는 것이다.”
AI는 또한 sentinel 값도 여기저기 쑤셔 넣습니다. 예를 들어 타입을 Maybe Day로 바꾸는 대신, 날짜가 없을 때 ModifiedJulianDay 0을 넣을 수 있습니다.
그 반대 경우도 있습니다. AI는 새 필드를 추가하는 대신 기존 레코드 필드를 과적된 방식으로 재사용하는 것 도 아주 기꺼이 합니다.
data Targets = Targets
{ fooTarget :: String
, barTarget :: String
, -- ...
}
새 기능이나 코드 경로를 추가해야 하고, 그 기능에는 baz 서비스용 새 target이 필요하다고 해 봅시다.
“새 target이 필요하다…Targets에 새 필드를 추가하는 대신 barTarget을 재사용하자. 그게 가장 깔끔한 접근이다.”
AI는 도메인이 확장될 때 기존 타입을 도메인에 맞게 확장하는 것보다, 기존 타입을 유지하는 쪽으로 최적화합니다. 특히 그 기존 타입이 라이브러리 경계를 가로지를 때 그렇습니다.
저는 이 행동을 이끄는 휴리스틱이 세 가지 있다고 봅니다.
무거운 의존성을 가진 라이브러리 경계를 가로질러 상위 타입을 수정하는 추가 위험을 피하려는 휴리스틱. Python 같은 비정적 타입 언어에서는 이런 행동이 매우 위험할 수 있습니다. 타입 변경 하나하나가 즉시 드러나지 않는 새로운 회귀를 도입할 수 있기 때문이죠. 따라서 상위 타입 변경을 피하면 잠재적 회귀를 피하는 셈입니다.
라이브러리 경계와 컴파일 단위를 가로질러 상위 타입을 수정하는 추가 작업량 을 피하려는 휴리스틱. 하지만 Haskell에서는 상위 타입 변경이 하류의 모든 잠재적 회귀 지점을 반드시 다루게 만듭니다. 그래서 상위 타입을 바꾸면 그 타입이 사용되는 모든 곳을 다 처리해야 하고, 이는 시간이 많이 들 수 있으며 LLM은 이를 피하려 합니다. 특히 컴파일 비용이 비싸거나 새 타입클래스 인스턴스를 여러 개 추가해야 하는 경우 더 그렇습니다.
저는 실제로 LLM이 이렇게 말하는 것을 본 적이 있습니다. “이 필드를 추가하려면 여러 다른 타입에 타입클래스 인스턴스를 추가해야 해서 매우 큰 변경이 된다. 가장 단순한 접근은…”
하지만 업데이트에 작업이 필요한 것은 의도된 설계 입니다. API 변경은 원래 많은 사고를 요구해야 하고, 그 점을 컴파일러가 강제하는 것이 핵심입니다.
여기서 큰 아이러니는 이런 업데이트와 변경이 대체로 기계적인 성격을 띠며, 바로 이런 반복적 작업이야말로 LLM이 최적으로 잘하는 일이라는 점입니다. 이런 것들이야말로 합리적인 원샷 작업이죠. 그래서 에이전트형 코더를 “자기 주도” 모드로 놓으면, 실제 인간이라면 이런 작은 작업에 “자기 자신”을 활용했을 방식을 거부하는 모습이 꽤 우습습니다.
이런 종류의 실패 양상은 코드 리뷰에서 잡아내기도 가장 어렵습니다. diff 뷰는 바뀐 코드를 분석하므로, 바뀌지 않은 코드는 인간 원숭이 뇌가 특히 포착하기 어렵습니다. 초록색이나 빨간색의 밝은 하이라이트가 없기 때문이죠. 바뀌지 않았지만 바뀌었어야 하는 코드를 잡아내려면 특별히 경계해야 합니다.
때때로 AI는 기존 합 타입을 수정하기는 하지만, 기존 코드를 제대로 적응시키지는 못합니다.
예를 들어 도메인에 특정하고 의미 있는 우주가 있다고 해 봅시다.
data State = Alaska | Arkansas | Arizona | ...
processState :: State -> IO ()
이제 미국 주와 함께 국가도 지원하고 싶어졌다고 합시다. LLM은 도메인을 확장해야 한다는 사실은 알아차릴 수 있지만, 그것을 평평하게 확장할 수 있습니다.
data Region = Canada | Mexico | Alaska | Arkansas | Arizona
processRegion :: Region -> IO ()
processRegion = \case
Canada -> ...
Mexico -> ...
st -> processState st
processState :: Region -> IO ()
processState = \case
Canada -> pure ()
Mexico -> pure ()
Alaska -> ... -- actual logic
진짜 해결책은 모든 패턴 매치가 자신이 다루는 공간을 엄격히 줄여 나가도록 하고, (단조 감소하게 하고) pure () 같은 더미 값을 가진 “무시된 case 매치”를 의심하는 것입니다.
data Region = Canada | Mexico | USState State
data State = Alaska | Arkansas | Arizona | ...
processRegion :: Region -> IO ()
processRegion = \case
Canada -> ...
Mexico -> ...
USState st -> processState st
processState :: State -> IO ()
processState = \case
Alaska -> ... -- actual logic
이 모든 것은 일반적인 인간 코드에서도 코드 스멜입니다. 꼭 틀렸다는 뜻은 아니지만, 추가 검토를 유도하죠. 다만 LLM 시대에는 새 타입을 정의하지 않고 대신 기존 타입을 재사용하거나 남용하려는 잘못 조정된 휴리스틱 때문에 그 문제가 더욱 증폭되는 것 같습니다.
즉, 타입을 수정하는 것 에 대한 압력이 있다면, 타입을 추가하는 것 에 대해서는 아마 더 큰 압력이 있을 것입니다.
이런 행동들이 전부 무조건 틀린 것은 아니지만, 대개는 LLM이 스트레스나 압박을 받고 있으며, 올바른 길보다 쉬운 길 혹은 “저노력” 경로를 택할 방법을 찾고 있다는 신호입니다. 적어도 P(legitimate | attempted)가 1에 가까워지는 날이 오기 전까지는, 이런 모든 상황은 가능한 한 빨리 인간의 개입과 지도가 필요합니다.
언젠가 LLM이 도메인에 맞는 올바른 타입을 생성하고, 방어적 프로그래밍으로 정확성을 억지로 만들어 내려는 경향을 억제할 수 있게 될까요? 그럴지도 모릅니다. 하지만 저는 Opus 4.8이 사소하지 않은 어떤 제품에 대해서든 충분한 도메인 모델을 뽑아내는 것을 거의 본 적이 없습니다. 그리고 제가 충분하다고 느끼는 계획에 도달하더라도, 몇 번 compact를 거치고 나면 원래의 동기들이 모두 씻겨 내려가는 것처럼 보입니다.
이 모든 문제를 강력한 프롬프트로 몰아낼 방법이 있을지도 모릅니다. 하지만 저는 LLM을 효과적으로 사용하는 일이 반드시 프롬프트 수준에서 해결할 수 있는 문제는 아니라고 느낍니다. 그것은 지속적인 경계와 주의를 요구하는 일입니다. 자동화된 hook도 도움이 된다는 것을 알게 되었습니다. (예: 경고 끄기 금지, hlint 우회 탐지…이런 것 작성은 claude에게 물어보세요 lol) 덕분에 즉시 주의가 필요한 영역을 표시할 수 있습니다.
누가 알겠어요, 어쩌면 이런 모든 것이 1년 안에 해결될지도 모릅니다. 하지만 저는 여전히 소프트웨어 개발은 중요한 것이라면 무엇이든 면밀히 검토할 가치가 있는 일이라고 생각합니다. 이런 실패 양상이 덜 흔해진다 해도…제가 예측하건대, 정확성의 긴 꼬리는 여전히 길 것입니다.
아무튼, 이 주제에 대해서는 여기까지입니다. 시간이 난다면 Haskell과 LLM 모험 동안 생각해 온 다른 주제들도 이어서 써 보겠습니다.
어떤 것을 먼저 보고 싶은지, 혹은 Haskell에서 LLM을 사용하는 다른 측면 중 제가 다뤘으면 하는 것이 있다면 알려 주세요!
그리고 여기까지 왔으니, 이 아이디어들을 마음에 새기도록 당신의 에이전트 친구를 훈련시켜 보는 건 어떨까요?
<https://blog.jle.im/entry/llms-and-haskell-1-constraint-evading-behavior.html>
Read this post and make me a Claude Code skill that reviews a Haskell diff for
the constraint-evading compromises it describes: suppressed warnings, string
and field stuffing, and weakened types that differ from any recorded plans.
Some of these hide in code that did not change but should have, so the skill
should start from the functions that changed and evaluate how they use or abuse
the types involved, but also spot type changes that look suspicious.