LLM in LSP 설계 제안


배경

생성형 Language model을 좀 더 생활밀착형으로 사용하기 위해서 기존 컴퓨터에서 수행하던 작업에 LLM을 적용해 보고 있다. 수행하는 작업 중 큰 부분을 문서 작업 혹은 코드 작업이 차지하게 되는데, 편집기로 주로 활용하는 NeoVim에서 여러가지 방법( 연관문서 검색, 문법 교정) 을 써서 이런저런 것들을 시도해 봤지만, 더 강한맛을 찾다가 이제 기존 LSP 구현에 LLM을 내장하는 방법에 대해 검토해보게 되었다.

요구사항

  1. 로컬 LLM일 것 - 개인 노트에 업무 관련 내용을 적을 때도 있는데, 그런 내용이 외부로 전송되는 것은 곤란하다. 일단 로컬에서 돌릴 수 있는 모델만 쓰고 싶었다.
  2. NeoVim을 실행할 때 같이 실행할 수 있었으면 좋겠고, 또 한번 모델을 로드하면 그것을 계속 재사용할 수 있는 구조였으면 좋겠다.

기존 구현 검토

가장 내가 원하던 바에 근접한 구현은 llm.nvim NeoVim 플러그인이었다. 딱 LLM 부분이 Huggingface API라는 것만 빼면 LSP를 활용하기 때문에 대략 내가 원하는 에디터 환경에 적당히 붙여서 돌릴 수 있다는 점이 마음에 들었다. 다만 프로토콜에 정의되지 않은 custom command를 쓰기 때문에 사실은 에디터별 플러그인을 추가로 개발해야 한다는 게 문제이긴 한데… 일단 LSP에 해당 명령어들이 표준으로 추가될 가능성은 현재로선 낮아보이기 때문에 (MS가 코파일럿 팔아야지 뭣하러 LSP에 추가해주겠나…) 그럭저럭 차선책이라고 판단했다.

설계 1차

따라서 대략의 설계는 llm.vim이 내부적으로 사용하는 llm-ls라는 language server 구현을 내가 대충 인터페이스만 따다가 로컬 모델을 돌리게 구현하면 에디터 플러그인은 추가로 구현하지 않아도 되겠지? 하는 식으로 진행되었다.

로컬 모델 구현 - llama.cpp

내가 그동안 지지고 볶던 LSP 구현체가 Rust로 구현되고 있었기 때문에 이제와서 LLM 때문에 언어를 바꾸기는 쉽지 않았다. 그리고 가능하면 최대한 빠른 구현체를 유지하고 싶었기 때문에 어떻게든 Rust에서 쓸 수 있는 구현을 찾아봤다.

아마 간편하게 쓸 수 있는 UI를 찾자면 대충 text-generation-webui, 혹은 Ollama 정도일 텐데, 모델을 구동하는 입장에서는 llama.cpp가 가장 낫다고 본다. 왜냐하면 gguf라는 나름 portable한 파일 형식을 주도적으로 만들고 여러 모델에서 지원하고 있기 때문에 내가 원하는 모델을 별다른 작업 없이 가장 빠르게 quantization 적용해서 돌릴 수 있다는 큰 장점이 있고, C/C++로 구현되어 있기 때문에 Rust에 적용하는 것도 일단은 가능하다.

Llama.cpp Rust binding

뭐… 쉬운 방법은 없다. 결론적으로는 ggml-sys-bleedingedge를 포크해서 쓰게 되었다.

아뿔싸, 이렇게 만들면 에디터 창마다 LLM 모델을 로드하게 되네?

결론적으로 이렇게 만들고 나서 보니 LSP 인스턴스마다 LLM 모델이 돌아가는 꼴이라서 메모리 낭비가 심해진다는 판단이 들었다. (사실 개인 노트 하나만 열고 쓴다면 여전히 쓸 수 있긴 하지만…)

설계 2차

따라서 LLM 서버를 하나만 돌리고 LSP들이 클라이언트 역할을 하는데, 만약 LLM이 돌아가고 있지 않다면, LLM을 데몬처럼 하나 띄우면 좋겠다는 생각이 들었다. 가장 근접한 구현은 bazel 빌드 서버 혹은 buck daemon이다. 이렇게 생각하니 굳이 Rust로 서버를 띄울 것에 고집할 필요 없이 llama.cpp에서 제공하는 예제 서버를 써도 되겠다는 생각이 들긴 하는데

결론

하여튼 설계 1차를 만들었다가 ‘아 맞다’ 하고 대충 망한 김에 블로그 글로 정리를 했으니, 이제 심기일전해서 설계 2차에서 생각한 부분을 구현해보면 되겠다는 결론이다.