OCaml 함수로 구현하는 언어 통합형 LLMs
요약
본 글은 OCaml 언어와 자체 포함형 추론 엔진 Dwarfstar를 활용하여 LLM을 일반 라이브러리로 임베딩하는 방법을 소개합니다. 이를 통해 외부 서비스 의존성 없이 애플리케이션 내부에 에이전트 기능을 구현할 수 있으며, 특히 로컬 환경에서 강력한 트래픽 분류 에이전트를 구축하는 사례를 제시했습니다.
핵심 포인트
- OCaml과 Dwarfstar로 LLM을 라이브러리로 임베딩 가능
- 외부 의존성 없이 애플리케이션 내부에 에이전트 기능 구현
- LLM의 추론 엔진을 OCaml 함수에 직접 바인딩하여 사용
- 오픈 웨이트 모델을 로컬에서 실행하는 방법 제시
토요일 새벽 1시에 OCaml 런타임에서 동시성 버그를 찾으며 작업하다가 Fable에 문제가 생겼습니다. 이 주제와 관련하여 탁월한 논의들이 있었고, 저도 직접 참여해서 오픈 웨이트 모델 사용에 진지하게 임해보고자 했습니다. DeepSeek의 모델들은 첫 출시 이후로 점점 더 능력이 향상되고 있으며, v4 Flash는 제 Mac에서도 실행할 수 있을 만큼 작습니다 (물론, 노트북과 데스크톱 각각 128GB/512GB RAM을 가진 매우 고사양 Mac 기준입니다).
문제는 제가 사용하던 에이전트형 CLI(Command Line Interface)를 쉽게 대체할 수 있느냐 하는 것입니다. 시스템이 어떻게 작동하는지 배우는 가장 좋은 방법은 유니커널(unikernel) 스타일로 구축해 보는 것이고, 그래서 저는 LLM을 일반적인 OCaml 라이브러리로 노출시키는 것을 목표로 했습니다. 이렇게 하면 비대해진 CLI를 통한 라우팅을 피할 수 있고, 연결하는 애플리케이션이 자신의 특정 요구에 따라 에이전트형 루프(agentic loop)를 구동하게 할 수 있습니다.
이것을 실용적으로 만드는 것은 Antirez의 Dwarfstar라는 자체 포함형 네이티브 추론 엔진입니다. 이 엔진은 Apple Metal과 포터블(portable)한 C를 지원합니다. 저는 이것을 OCaml 5 및 Eio에 직접 바인딩하여 ocaml-deepseek를 만들었고, 이제 제 노트북에서 단순 함수 호출만으로 애플리케이션 내부에 LLM을 가져올 수 있게 되었습니다.

예를 들어, 저는 이 웹사이트 자체를 구동하는 OCaml 웹 서버에 Deepseek 추론을 직접 임베드하여 의심스러운 봇 활동을 찾을 수 있습니다. 그리고 이것은 오픈 웨이트이며 로컬에서 실행되기 때문에 외부 서비스에 대한 의존성이 전혀 없습니다!
(* OCaml 내 프로세스에서 작동하는 트래픽 분류 에이전트. 이 에이전트는 두 개의 OCaml 함수 도구를 받고, 스스로 어떻게 조합할지 알아냅니다. *)
let agent = ...
로그 리더와 데이터베이스 쿼리는 모델이 호출하도록 허용된 단지 두 개의 OCaml 함수일 뿐이며, 각각 필요한 내용에 맞춰 범위가 지정되고(Eio를 사용해) 격리되어 있습니다. 모델이 언제 그리고 어떻게 이들을 조합할지 결정합니다.
1 Humpty라는 OCaml 에이전트 사용해보기
저는 패키지를 opam에 제출했으므로 opam install deepseek 또는 opam pin add deepseek https://tangled.org/anil.recoil.org/ocaml-deepseek.git를 사용할 수 있습니다.
작동할 것입니다.
이 패키지에는 Apple Silicon용 humpty-metal과 어디서든(느리게) 실행 가능한 휴대용 humpty-cpu라는 두 가지 변형을 가진 바이너리도 포함되어 있습니다.
OCaml에서 에이전트를 구축하는 방법을 설명하기 위해 사용할 네 가지 하위 명령어가 있습니다:
먼저 사용 가능한 모델 목록을 확인하는 list,
다음으로 하나를 다운로드하는 download,
그 다음 상태 비저장(stateless) 방식으로 사용하는 chat,
그리고 마지막으로 그것을 agent로 래핑하는 것입니다.
1.1 적절한 Deepseek 모델 선택하기
시작하기 전에 먼저 오픈 모델 가중치를 다운로드해야 합니다.
humpty list
은 사용 가능한 가중치 카탈로그를 출력합니다:
$ humpty-metal list
Models (download dir: /Users/avsm/.local/share/ds4)
TARGET ALIASES DESCRIPTION
...
가지고 있는 RAM 용량에 따라 하나를 선택하세요. 저는 노트북(RAM 128GB)에서는 q2q4를, Mac Studio(RAM 512GB)에서는 매우 강력한 pro-q2를 사용합니다.
모델을 여러 장치에 분산하여 실행하기 위한 스플릿 파일도 있지만, 여기서는 건너뛰겠습니다.
1.2 Deepseek 모델 가중치 가져오기
선택했다면, humpty download q4 (또는 pro-q2, 또는 원하는 것)가 Hugging Face CLI를 호출하여 GGUF 파일을 가져옵니다.
Huggingface CLI가 설치되어 있거나 uvx가 경로에 있어야 합니다.
이 과정이 진행되는 동안 차 한 잔 마시면서 기가바이트 단위의 LLM 가중치가 다운로드되기를 기다리세요. 그런 다음 OCaml에서 에이전트를 구축하는 것을 시작하겠습니다!
2 처음부터 에이전트 구축하기
먼저
Metal 커널을 위한 캐시 디렉터리를 지정하여
적용하는 경우:
let engine = Deepseek.V4.create ~cache ~model ~domain_mgr ~sw () in
V4.generate engine "Explain monads in one sentence." ~on_token:print_string;
- : unit
...
Deepseek.V4.create는 GGUF 모델 파일을 열고, 처음 실행될 때 cache에 내장된 Metal 셰이더를 생성합니다. 응답을 생성하는 것은 공급된 프롬프트를 인코딩하고, 프리필(prefill)을 실행한 다음, 시퀀스 종료 마커가 나올 때까지 on_token 콜백으로 토큰을 하나씩 샘플링하는 단일 V4.generate 호출입니다. 모든 추론은 별도의 OCaml 도메인에 있는 Metal 라이브러리에서 수행되므로, 메인 애플리케이션의 다른 Eio 섬(fibres)을 계속 사용할 수 있습니다.
humpty chat를 사용하여 이 단일 요청/응답을 시도해 볼 수 있는데, 이는 실행 간 메모리를 유지하지 않으며 응답을 표시하는 것 이상의 어떤 동작도 수행할 수 없습니다.
$ humpty-metal chat 'Explain algebraic effects in OCaml 5 in one sentence'
Algebraic effects in OCaml 5 allow functions to suspend execution and invoke
user-defined handlers for operations (like state, exceptions, or generators)
...
따라서 "대화(conversation)"는 역할 태그가 지정된 메시지 목록(예: system, user, assistant, tool)이며, 이를 라이브러리에서 LLM을 위한 프롬프트 문자열로 연결하는 것입니다.
3 상태 비저장형 LLM이 외부 세계에 효과를 요청하는 방법
앞서 언급한 단일 단계의 텍스트-투-텍스트 함수는 합의된 "형태(shape)"로 텍스트를 방출하므로, 우리는 그 출력을 기반으로 다음에 무엇을 할지 파악할 수 있습니다. DeepSeek은 DSML이라는 작은 마크업 언어를 이해하도록 모델을 훈련시켰으며, 이는 다음과 유사합니다:
<|DSML|tool_calls>
<|DSML|invoke name="edit">
<|DSML|parameter name="path" string="true">/tmp/x.c</|DSML|parameter>
...
DSML은 에이전트의 텍스트 응답에 간간이 삽입되는 유사-XML 언어입니다. 저 막대기들은 실제로는 전각 수직선 | (U+FF5C)이며, ASCII 문자 |가 아닙니다.
. DeepSeek은 희귀한 코드포인트를 DSML의 제어 토큰에 할당하여, 일반 텍스트나 코드로 모델이 출력할 수 없게 합니다.
저희는 OCaml로 DSML 구현을 갖추고 있으며, 이는 XML을 OCaml 레코드 타입으로 파싱합니다:
type thinking_mode = Chat | Thinking
type reasoning_effort = High | Max
type task = Action | Query | Authority | Domain | Title | Read_url
...
원시 텍스트 응답을 파싱하면 사용자에게 보이는 가시적 콘텐츠, 모델의 숨겨진 추론 콘텐츠, 그리고 그 과정에서 발생한 모든 도구 호출(tool calls)로 분리됩니다. tool_call은 단순히 이름 문자열과 JSON 형식의 인자들로 구성되며, 결과를 다시 연결하기 위한 선택적 식별자가 포함될 수 있습니다.
나머지 세 가지 타입은 모델이 무엇을 반환하는가보다는 어떻게 응답할지에 대한 조절 장치(knobs)입니다:
thinking_mode:Chat은 직접 답변하고,Thinking은 먼저<think>블록 내에서 추론합니다.reasoning_effort: 이 추론을 최대화하여 느린 추론 시간의 대가를 치릅니다.task: DeepSeek의 내부 파이프라인에 이 턴이Action,Query등 어떤 것인지 알려주는 간편한 지침(hint)입니다.
3.1 OCaml에서 사용자 정의 도구 함수 만들기
이제 모델이 알고 있는 특정 도구를 정의해야 합니다. 우리는 양방향 코덱(codec)을 정의하여 모델의 JSON/DSML 인자를 타입화된 OCaml 값으로 디코드하고, 그 값을 다시 렌더링합니다. 여기 간단한 터치 도구 예시가 있습니다:
let touch =
let open Dsml.Codec in
Invoke.map "touch" (fun path -> path)
...
우리는 우리가 수행하는 효과(이 경우, 단순히 빈 파일을 작성하는 것)를 JSON 메타데이터로 감싸서 모델이 프롬프트를 처리하면서 언제 그리고 어떻게 도구를 호출해야 하는지 알도록 합니다. 대부분의 정책 언어와 달리, 우리는 LLM을 다루기 때문에 이를 일반 텍스트로 설명하며, 모델이 대화에서 언제 이 도구에 대한 설명을 적용해야 할지 결정합니다.
여기에는 아직 상태(state)가 없지만, 이제 DSML 라이브러리를 사용하여 모든 메시지를 추적함으로써 전체 프롬프트 문자열을 구축할 수 있습니다.
val encode_messages :
?context:message list ->
?drop_thinking:bool ->
...
4 상태(state)를 추가하여 에이전트형 OCaml 라이브러리 만들기
이제 여기에 상태를 추가할 준비가 되었습니다! '에이전트(agent)'는 LLM을 감싸서 세 가지 작업을 수행하는 것일 뿐입니다:
encode_messages를 위해 지금까지의 대화를 기억하고- 지속적인 세션(session)을 통해 엔진의 KV-cache를 따뜻하게 유지하며
- LLM으로부터 도착하는 도구 호출(tool callbacks)을 실행합니다.
이 루프는 간단한 OCaml 이벤트 데이터 타입으로 표현됩니다:
type event =
| Reasoning of string (* 모델의 <think> 텍스트 *)
| Content of string (* 응답 청크 *)
...
LLM이 각 턴에 응답할 때, 일반 텍스트는 해당 턴이 Done임을 의미합니다.
도구 호출은 이름으로 조회된 다음 실행되고, 그 결과가 tool 메시지로 대화에 다시 통합됩니다. 에이전트 함수가 하는 모든 것은 모델이 순수 텍스트로 답변할 때까지 이 작업을 고정점(fixed point)까지 실행하는 것뿐입니다.
4.1 OS 샌드박싱 및 Eio 기능을 사용하여 사용자 정의 도구 작성하기
여기서 유니커널 스타일의 마법이 나타납니다. 도구가 우리가 직접 정의하는 것이기 때문에, OCaml 자체를 활용할 수 있습니다! 특히, 저는 제가 원하는 애플리케이션에 맞춰진, 샌드박싱하기 어려운 일반적인 셸 스크립트가 아닌 더 나은 보안과 더 세분화된 도구 호출을 원합니다.
Eio는 가능한 한 주변 권한(ambient authority)을 제거하는 객체-능력성원칙(object-capability discipline)을 따르는 OCaml 5의 효과(effects) 위에 구축된 라이브러리입니다. 우리의 Toolbox 모듈에서, 우리는 몇 가지 예시 Eio 도구를 정의합니다:
val read : dir: _ Eio.Path.t -> Tool.t
val write : dir: _ Eio.Path.t -> Tool.t
val dns : net: _ Eio.Net.t -> Tool.t
...
여기서 각 OCaml 시그니처가 해당 도구에 대한 능력을 입력받는다는 점에 주목하세요:
read와 write는 오직 ~dir로 전달된 디렉터리에만 접근할 수 있으며, 그 위로는 아무것도 할 수 없습니다. 왜냐하면 Eio가 호출을 샌드박싱하기 위해 *openat(2)*를 사용하기 때문입니다. dns는 네트워크 능력을 가지고 있기 때문에 네트워킹에 접근하며, bash는 프로세스 관리자를 가지고 있기 때문에 프로세스를 생성합니다.
이러한 기능들을 애플리케이션에서 사용할 때, 필요한 정확한 격리 환경(sandboxing)을 선택하거나, 애플리케이션별 로직을 가진 자체 도구 함수를 작성할 수 있습니다. 이것이 바로 humpty agent가 하는 일인데, --dir로 전달하는 작업 공간 디렉토리 내로 파일 및 셸 도구를 제한합니다.
(* 파일 시스템 도구를 [작업 공간]으로 격리하여 도구가 그곳에만 접근하도록 함 *)
Eio.Path.with_open_dir Eio.Path.(fs / workspace) @@ fun ws ->
let agent =
...
fs, net, 그리고 proc 값들은 모두 Eio의 표준 환경에서 가져오며, 이제 프로그래머가 이들을 어떻게 분배할지 결정해야 합니다. 읽기 전용 에이전트(read-only agent)를 원한다면, 단순히 write와 bash만 제외하면 됩니다.
도구는 코덱과 핸들러에 대한 Tool.v 함수일 뿐이므로, 호출자는 애플리케이션의 비즈니스 로직에 따라 자신만의 도구를 추가할 수 있습니다. 예를 들어, 이 웹 서버에는 연결 풀 크기를 쿼리하는 도구가 있고, 메모리 로그를 검색하는 도구 등이 있습니다.
4.2 결정론적 동작 및 재현성 관리
로컬 모델을 실행하는 장점 중 하나는 추론의 결정론적 동작에 대해 더 나은 제어력을 갖게 된다는 것입니다. 가중치와 추론 코드가 동일하다는 명백한 요인 외에도, 우리는 일반적으로 병렬 처리 때문에 GPU 기반 추론에서 다른 결과를 얻습니다.
하지만 속도 저하를 감수할 수 있다면, 여기의 CPU 백엔드는 동일한 시드(seed)를 저장하고 전달하는 것을 지원합니다:
$ humpty-cpu chat 'tell me a joke about camels in one sentence' -v
humpty-cpu: [INFO] model: DeepSeek V4 Flash (vocab 129280)
humpty-cpu: [INFO] seed: 1690400691090126 (random; pass --seed N to reproduce)
...
하지만 결정론적 시드가 Metal 백엔드에서도 작동한다는 것을 발견하고 놀랐습니다!
$ humpty-metal chat 'tell me a joke about camels in one sentence' -v
humpty-metal: [INFO] model: DeepSeek V4 Flash (vocab 129280)
humpty-metal: [INFO] seed: 2042575750328474 (random; pass --seed N to reproduce)
...
5 LLM을 라이브러리로 사용하는 것의 함의
지금까지 우리는 LLM 모델이 주로 순수 함수(pure function)로 노출될 수 있다는 것을 보았고, 도구(tool)는 그 타입이 무엇을 건드릴지 말해주는 함수이며, 에이전트(agent)는 단순히 이들을 반복하는 루프라는 것을 알았습니다.
결정적으로, 애플리케이션이 원하지 않는 한 직렬화 프로토콜(serialization protocols), REST API, MCP 인증 등 그 위에 구축된 많은 다른 계층들이 필요 없습니다. 유니커널 스타일 라이브러리의 큰 장점 중 하나는 필요한 기능들을 컴파일 타임에 훨씬 더 쉽게 특수화할 수 있다는 것입니다.
5.1 Dikjstra 모나드(monads) 또는 정제(refinements)를 사용하여 안전한 요새 구축하기
AI 자동 생성 콘텐츠
본 콘텐츠는 Lobste.rs ML의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기