메뉴
HN
Hacker News • 22일 전

TERMy – LLM 없이 동작하는 빠른 터미널 어시스턴트 공개

IMP
5/10
핵심 요약

PJON 개발자가 AI 비용 인상에 대응해 자택 하드웨어(GTX 1050 Ti)로 소형 transformer를 직접 학습해 보았지만 결과가 만족스럽지 않자, LLM 없이 결정론적(deterministic) 방식으로 동작하는 터미널 어시스턴트 'TERMy'를 개발했습니다. 간단한 자연어 명령을 빠르고 저렴하게 처리하는 것이 목표로, 단순 반복 작업에 거대 모델이 불필요하다는 점을 보여준다는 점에서 주목할 만합니다.

번역된 본문

NPC-Forge와 TERMy 개발

저는 연구개발을 사랑합니다. PJON(Padded Jittering Operative Network) 때문에 저를 알고 계실 수도 있습니다. PJON은 제가 2010년부터 개발하기 시작한 네트워크 프로토콜로, Pius Sieber의 연구 덕분에 최근 취리히 연방공과대학(ETH Zurich)에 의해 실리콘(하드웨어)으로 구현되었습니다.

7월 초부터 2개월간 AI 가격 인상과 보조금으로 지원되던 토큰 남용 시대의 종언이라는 기묘한 시기에 개인 프로젝트에 집중할 기회가 있었습니다. 간단한 자연어 요청을 처리할 수 있는 터미널 어시스턴트를 처음부터 직접 개발할 수 있는지 궁금했습니다. 저는 기억력이 나빠서 "가상 환경 활성화" 같은 자잘한 작업을 Copilot에게 매달 적지 않은 비용을 지불하며 요청하는 데 익숙해져 있었습니다. 그러다 생각했습니다. 내 워크플로를 더 효율적으로 만들 방법이 없을까? 이런 작업에 정말 수조 개의 파라미터가 필요한가?

집에서 하는 Transformers

집에서 생성 모델을 구현하고 2010년대 초에 Kerbal Space Program을 하던 컴퓨터(RAM 16GB, NVIDIA GTX 1050 Ti 4GB VRAM, i7-4790K 4.0GHz 8코어 CPU로 "업그레이드"된)로 처음부터 학습하는 것의 실현 가능성에 대한 개방형 연구를 시작했습니다. 저는 실험에서 미니멀리즘을 추구하는 편이라, 제약된 환경을 스스로 부과하여 우아하고 효율적인 해결책을 향해 작업하도록 강제했습니다.

먼저 Python으로 처음부터 직접 구현한 transformer 학습·평가 프레임워크를 개발했습니다. NanoGPT와 매우 유사한 1~2억 파라미터 규모로 시작해 flash attention과 예상되는 최적화들을 모두 추가했고, Mamba 같은 새로운 아키텍처도 시도했습니다. 결과는 대체로 만족스럽지 못했고, 소름 끼치거나 완전히 무섭기까지 했습니다. 예를 들어:

프롬프트를 입력하세요 (빈 값이면 빈 시작): 외계인이란 무엇인가? 생성 중... 토크나이저 사용: /ollm/checkpoints/gpt/tinyostrich/tokenizer.model === 생성된 텍스트 === "그는 이 세계의 일원이 아니다. 그는 더 이상 아무것도 믿을 수 없다. 저 동물들은 모두 배설물처럼 보이지만, 모든 입이 나쁜 것은 아니다. 그들은 닮았고, 모두 정말 끔찍하다. 그들은 각자 자신의 삶을 가지며, 각각은 그들이 아니라는 신호다. 그들은 괜찮다고 말한다. 그들은 그저 만들 수 있는 조금 다른 것들일 뿐이다. 그들은 그들을 본 적이 없다. 안 보는 게 나았다. 그중 일부는 권리를 가진다. 일부는 떠나야 한다. 하지만 우리는 얼마나 많은지에 대한 미래를 바라보고 있지 않다. 마지막 둘은 모두 우주에 있다."

(excrements라는 단어 대신, 여기에 게시하고 싶지 않은 4글자 욕설이 사용되었습니다)

모든 테스트에서 모델은 같은 문장을 반복하는 루프에 빠지기 쉬웠고, QA로 학습시켜도 특히 기술적인 질문에는 일관되게 답하는 경우가 드물었습니다. Project Gutenberg의 로열티 프리 책 모음, 많은 오픈소스 소프트웨어, Huggingface의 다양한 데이터셋으로 모델을 학습시켰습니다. 곧 이 접근이 실현 불가능하다는 것을 깨달았습니다. 제대로 학습하려면 최소 한 달간 멈추지 않고 돌려야 했습니다. 모델이 살아있고 마법처럼 보여서 놀랐지만, 동시에 엄청나게 낭비적이고 사실상 쓸모없다는 점에 부끄러웠습니다.

로컬 모델

ollama와 오픈 웨이트 모델로 방향을 전환해 howto를 개발했습니다. 프리프롬프트를 사용해 모델이 터미널 명령어로만 답하도록 강제하는 또 하나의 터미널 하네스입니다. 결과는 응답까지 걸리는 시간 때문에 대체로 만족스럽지 않았습니다. ornith:9b, mistral:7b, cogito:14b 같은 모델이 가끔은 역할을 해내지만, 특히 VRAM이 4GB뿐이라면 일반적인 용도로는 충분히 빠르고 신뢰할 만하지 않았습니다.

결정론적으로 가기

그러다 블록체인 열풍을 떠올렸습니다. 모두가 어딘가에 블록체인을 끼워 넣어 다음 대박이라고 팔려던 시절 말입니다. 저는 이전 하이프 사이클에서 그 사람들이 그랬던 것처럼 시간과 돈을 낭비하고 싶지 않아서, 터미널 어시스턴트를 처음부터 직접 만들기 시작했습니다.

원문 보기
원문 보기 (영어)
NPC-Forge and TERMy development I love research and development, you may have heard of me because of PJON (Padded Jittering Operative Network) . It is a network protocol I started developing in 2010, which was recently implemented in silicon by the ETH Zurich university thanks to the research of Pius Sieber . I had a chance to focus for 2 months on my personal projects since early July, during the strange times of AI price hikes and the end of subsidized tokenmaxing. I was curious to see if I could develop from scratch a terminal assistant capable of handling simple natural language requests. I have a bad memory and got used to ask to copilot "activate the virtual environment" or similar trivial operations spending a non negligible sum every month. I started thinking, maybe I can do something to make my workflow more efficient? Do I really need trillions of parameters to accomplish those tasks? Transformers at home I started an open-ended research on the feasibility of implementing a generative model at home and training it from scratch on the computer I used to play Kerbal Space Program in the early 2010s "upgraded" with 16GB of RAM, NVIDIA GTX 1050 Ti (4GB VRAM) and a i7-4790K (4.0GHz 8 cores) CPU. In my experiments I tend to look for minimalism, so I imposed myself a constrained environment to be forced to work towards an elegant and efficient solution. I first developed a framework to train and evaluate transformers, which I implemented from scratch in Python. I have started with something very similar to NanoGPT with 100-200M parameters, then I added flash attention, and all the expected optimizations, I even tried novel architectures like Mamba. The results were generally unsatisfactory, creepy if not outright scary, like the following: Enter your prompt (or leave blank for empty start): What is an alien? Generating... Using tokenizer: /ollm/checkpoints/gpt/tinyostrich/tokenizer.model === Generated Text === He's not a member of the world. He can't believe anything anymore. All of those animals are looking like excrements, but every mouth is not a bad one. They look alike, all of which are really terrible. They get their own life, and each is a sign that they're not. They're all right, they say. They're just some different things they can make. They've never seen them. They were better not. Some of them've got their rights. Some of them have to go. But we're not looking for the future on how many. The last two of them are all in the universe. (the word excrements was used instead of an expletive composed of 4 letters that I am not willing to publish in here) All my tests produced models that were prone to enter in loops repeating the same sentence, and even if trained on QA they were rarely able to consistently answer questions, specially if technical. I trained models on a collection of royalty free books from the Project Gutemberg , a lot of open-source software, and various datasets available on Huggingface . I quickly understood that this approach was not feasible; a proper run would have required at least a month of training non-stop. I was amazed by how my models looked alive and magical, but I was also ashamed because they were incredibly wasteful and effectively useless. Local models I pivoted to ollama and open-weight models and developed howto , yet another terminal harness that uses a pre-prompt to force the model to answer only with terminal commands. Results were generally unsatisfactory because of the time required to get a response. Models like ornith:9b , mistral:7b or cogito:14b can get the job done sometimes, but they are not fast and reliable enough for general use, specially if you have only 4GB of VRAM. Going deterministic Then I remembered about the blockchain craze, when everyone wanted to fit a blockchain somewhere and sell it as the next big thing. I didn't want to waste my time and money like all those people did in the previous hype cycle, so I started building a terminal assistant from scratch with a new set of constraints: No embeddings No machine-learning No LLMs Dataset format The first things I needed was a set of conventions to rely on, so I drafted the NDF 0.0 (NPC-Forge Dataset Format which specifies the dataset format of NPC-Forge. The following object contains category, input sentences, textual response, thinking traces, permission gating and tool calls to be executed in a format compatible with VS code. { "category" : " linux_files " , "input" : [ " list files " , " list files and directories " ], "tools" : [ { "name" : " run_in_terminal " , "arguments" : { "command" : " ls -lah " , "explanation" : " Lists the files in the current directory. " , "goal" : " Display current directory contents " , "mode" : " sync " } } ], "message" : " Done " , "thinking" : [ " That is quite simple! " , " This is boring... " ], "permission" : " yolo " } I am really in love with this, it is a self-contained atom of knowledge that can be easily edited and shared. It is very simple to expand the capabilities of conversational agents if you adhere to this convention; let's say I want my terminal assistant to learn about docker commands, I can just write down a list of objects in dataset_docker.json , drop the file in the dataset directory, and the NPC will instantly learn them as Neo learnt Jujitsu in The Matrix. The next problem to solve was, how to handle questions like "create file test.txt"? I needed to parse the "variable" in there and understand the true meaning of the request, so I came up with this: { "intent" : " file_creation " , "category" : " linux_files " , "type" : " template " , "structure" : [ [ { "tag" : " <||vocab_create||> " , "type" : " vocab " , "required" : true }, { "tag" : " <||vocab_file||> " , "type" : " vocab " , "required" : false }, { "tag" : " <||file||> " , "type" : " filename " , "required" : true } ] ], "message" : " <||completion||> " , "tools" : [ { "name" : " run_in_terminal " , "arguments" : { "command" : " echo '' > '<||file||>' && termy_set_context 'active_file' '<||file||>' " , "explanation" : " Writes <||string||> in file <||file||>. " , "goal" : " Directory Allocation " , "mode" : " sync " } } ], "permission" : " ask " , "thinking" : [ " Ok, I am asked to create the file <||file||>. " ] }, Each tag like <||vocab_create||> represents a concept, in this case the action of creation, which is represented by multiple sinonyms: { "<||vocab_create||>" : [ " create " , " make " , " generate " , " craft " , " forge " ] } One or more tags can be expected at the same position and each tag can be required or optional. The "variables" or named entities are extracted according to their type and a related regular expression: { "<||filename||>" : " [ \\ w \\ -]+ \\ .[a-zA-Z0-9]{2,4} " , } I must thank my great friend Kevin to help me thinking this out. Meditations on safety Looking at the permission key I concluded that, enforcing the use of "permission": "ask" for all potentially destructive commands, the tool became inherently safe to use; obviously potential for human error remained, such as a bug in the implementation or in the dataset, but risks were strongly mitigated. The implementation I wrote FlintParser and FlintNPC classes to make use of the data format described above, implement a NLU (Natural Language Understanding) pipeline, and all the required features for the terminal assistant to work in around 1000 lines of code. I wrote those classes in identical, cross-compliant implementations for both Python, for local OS environments, and JavaScript, running client-side inside any browser tab or Node.js instance. The most difficult part was to determine what to do and in which order. I have worked a lot on a compiler for my own programming language BIPLAN and while developing that I had the honour to learn that the first thing you need to do when translating code is to remove noise and then work your way out trying the least expensive paths first. So that's the pipeline I implemented: Strip explet