아이디어에서 터미널까지: 나의 첫 LLM 기반 CLI 도구 제작기 (그리고 다르게 시도했을 점들)
요약
개발자의 워크플로우를 방해하지 않기 위해 터미널 내에서 직접 작동하는 LLM 기반 CLI 도구를 제작한 경험담입니다. 단순한 API 호출 방식에서 시작하여 컨텍스트 유지의 중요성과 실제 개발 환경에서의 한계를 깨달아가는 과정을 다룹니다.
핵심 포인트
- 컨텍스트 스위칭을 줄이기 위한 터미널 기반 LLM 도구의 필요성
- 단순 API 호출 방식의 초기 구현과 그 한계점
- 프로젝트 컨텍스트(파일, 에러, 로그) 이해의 중요성
- LLM을 실제 개발 워크플로우에 통합할 때의 고려사항
저는 터미널을 사랑합니다. 명령어를 입력하고 무언가 실행되는 것을 지켜보는 것에는 어떤 만족감이 있습니다. 하지만 최근 들어 AI 프로젝트를 진행할 때 발생하는 과도한 컨텍스트 스위칭 (Context-switching) 때문에 짜증이 나기 시작했습니다. 코드에 대해 궁금한 점이 생기거나 파일의 빠른 요약이 필요할 때, 결국 브라우저를 열고, 어떤 채팅 인터페이스로 이동하여, 내용을 붙여넣고, 기다리는 과정을 반복하게 되었습니다.
그래서 저는 간단한 주말 프로젝트라고 생각했던 것을 만들기로 결심했습니다. 바로 제 터미널 안에 상주하는 LLM (Large Language Model) 기반의 CLI (Command Line Interface) 도구입니다. 브라우저도, 복사-붙여넣기도 필요 없이, 그저 질문을 던지기만 하면 됩니다.
스포일러: 생각만큼 간단하지 않았습니다. 하지만 올해 제가 만든 것 중 가장 교육적인 작업 중 하나였습니다.
제가 어떻게 만들었는지, 실제로 무엇이 작동했는지, 그리고 오늘 다시 시작한다면 무엇을 다르게 할 것인지 공유하겠습니다.
왜 LLM을 위한 CLI 도구인가?
코드로 들어가기 전에, 이것이 왜 의미가 있었는지 설명하겠습니다.
저는 하루의 대부분을 터미널에서 보냅니다. 테스트를 실행하고, 로그를 확인하고, 컨테이너를 관리하고, 설정을 만지는 일들 말이죠. 그런 흐름(Flow) 속에 있을 때 브라우저를 여는 것은 집중력을 깨뜨립니다. 저는 다음과 같은 기능을 원했습니다:
- 제가 이미 있는 곳, 즉 터미널에서 작동할 것.
- 제 프로젝트의 컨텍스트 (파일, 에러, 로그)를 이해할 것.
- 주의를 분산시키는 기계가 되지 않으면서 빠르고 집중된 답변을 제공할 것.
이미 훌륭한 LLM 도구들이 시중에 나와 있지만, 저는 가볍고, 커스터마이징이 가능하며, 진행하면서 확장할 수 있는 오픈 소스(Open source) 형태를 원했습니다. 게다가 이를 직접 만드는 과정은 실제 워크플로우에서 LLM을 어떻게 책임감 있게 사용할 것인지 진지하게 고민하게 만들었습니다.
첫 번째 버전: "그냥 API를 호출하자"
저의 초기 접근 방식은 부끄러울 정도로 단순했습니다:
- 새로운 Python 프로젝트를 생성한다.
- argparse를 사용하여 명령줄 인자 (Command-line arguments)를 파싱한다.
- 사용자의 프롬프트 (Prompt)와 함께 LLM API를 호출한다.
- 응답을 출력한다.
대략적인 모습은 다음과 같았습니다:
# ask.py (v0.1, 매우 순진한 버전)
import argparse
import os
from openai import OpenAI
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
def main():
parser = argparse.ArgumentParser()
parser.add_argument("prompt", nargs="+", help="LLM에게 보낼 질문")
args = parser.parse_args()
prompt = " ".join(args.prompt)
response = client.chat.completions.create(
...
if name == "main":
main()
그 후 다음과 같이 실행할 수 있었습니다:
$ ask "Summarize this error log" < error.log
작동했습니다. 어느 정도는요.
질문을 던지고 답변을 얻을 수 있었고, 처음 몇 번은 마법처럼 느껴졌습니다. 하지만 아주 빠르게 문제에 직면했습니다.
현실 점검: 무엇이 잘못되었나
1. 컨텍스트(Context) 부재, 대부분 소음(Noise)
이 도구는 제가 어떤 프로젝트를 하고 있는지, 어떤 파일이 중요한지, 혹은 실제로 무엇을 하려고 하는지 전혀 알지 못했습니다. 모든 프롬프트는 제로 상태에서 시작되었습니다. 저는 다음과 같은 프롬프트를 직접 작성해야 했습니다:
"나는 FastAPI와 PostgreSQL을 사용하는 Python 프로젝트를 하고 있어. 여기 내 에러 로그가 있어. 무슨 일이 일어나고 있는지 설명하고 해결책을 제안해줘."
매번 이렇게 반복적인 컨텍스트를 타이핑하는 것은 매우 번거로운 일이었습니다.
2. 토큰 팽창(Token Bloat)과 예상치 못한 비용
파일 전체를 자동으로 전송하여 컨텍스트 문제를 "해결"하려고 시도했을 때, 토큰 사용량이 폭발했습니다. 명령 한 번에 몇 센트가 드는 것은 그리 커 보이지 않지만, 하루에 50번을 실행하면 이야기가 달라집니다.
추적 장치도, 제한도 없었기에 청구서를 확인하기 전까지는 제가 얼마나 쓰고 있는지 알 수 없었습니다.
3. 스트리밍(Streaming) vs 블로킹(Blocking) UX
첫 번째 버전은 전체 응답이 준비될 때까지 블로킹(Blocking)되었습니다. 답변이 길어지면 터미널은 그냥... 멈춰 있었습니다. 피드백도, 진행 상황도, 아무것도 없었습니다. 제가 익숙했던 스트리밍(Streaming) 채팅 인터페이스와 비교하면 느리고 투박하게 느껴졌습니다.
4. 과거 대화에 대한 기억 부재
모든 `ask` 호출은 상태가 없는(Stateless) 방식이었습니다. 만약 후속 질문을 하고 싶다면—"좋아, 그럼 SQLAlchemy 2.0을 사용하면 어떻게 달라질까?"—모든 것을 다시 설명해야 했습니다.
버전 2: 실제로 유용하게 만들기
초기의 흥분이 가라앉은 후, 저는 몇 가지 핵심적인 개선 사항을 담아 도구를 다시 구축했습니다.
**프로젝트 컨텍스트 추가 (Adding Project Context)**
저는 다음과 같은 항목을 정의할 수 있는 간단한 설정 파일(`ask.config.json`)을 추가했습니다:
- 프로젝트 유형 (python, node 등)
- 고려해야 할 주요 디렉토리
- 기본 시스템 프롬프트 (Default system prompts)
그 다음, 주변 파일들을 읽어 컨텍스트 (Context)로 주입하는 작은 헬퍼(helper) 함수를 작성했습니다:
```python
# context.py (단순화 버전)
from pathlib import Path
def gather_context(max_files=5):
cwd = Path.cwd()
files = list(cwd.glob("*.py"))[:max_files]
context_parts = []
for file in files:
content = file.read_text(errors="ignore")
context_parts.append(f"File: {file.name}\n{content}")
return "\n".join(context_parts)
메인 커맨드에서는 다음과 같이 사용합니다:
context = gather_context()
user_prompt = " ".join(args.prompt)
full_prompt = f"Here is some context from my project:\n\n{context}\n\nUser question: {user_prompt}"
이제 저는 다음과 같이 실행하기만 하면 됩니다:
$ ask "Why is this migration failing?"
그러면 도구가 현재 디렉토리에서 관련 있는 Python 파일들을 자동으로 포함합니다.
스트리밍 응답 (Streaming Responses)
사용자 경험 (UX)을 더 반응성 있게 만들기 위해, 스트리밍 (Streaming) 방식으로 전환했습니다:
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "You are a concise, practical assistant for a software engineer."},
{"role": "user", "content": full_prompt},
],
temperature=0.3,
stream=True,
)
for chunk in response:
content = chunk.choices[0].delta.content
if content:
print(content, end="", flush=True)
print() # 최종 줄바꿈
갑자기 터미널이 살아있는 것처럼 느껴졌습니다. 답변이 실시간으로 형성되는 것을 볼 수 있었고, 이는 긴 응답을 기다리는 고통을 훨씬 줄여주었습니다.
기본적인 비용 제어 (Basic Cost Control)
대략적인 토큰 추정기 (token estimator)와 --max-tokens 플래그를 추가했습니다:
parser.add_argument("--max-tokens", type=int, default=500)
그리고 호출당 대략적인 사용량을 로컬 파일에 기록했습니다. 완벽하지는 않지만, 아무 생각 없이 10k 토큰 분량의 프롬프트를 사방에 보내는 것을 방지하기에는 충분했습니다.
*_만약 제가 지금까지 배운 모든 것을 바탕으로 이 도구를 다시 만든다면, 다음과 같은 점들을 바꿀 것입니다:
- 먼저 오픈 소스 모델로 시작하기 저의 첫 번째 버전은 상용 API (commercial API)를 가정하고 만들었습니다. 하지만 CLI 도구, 특히 민감한 코드를 다룰 수 있는 도구라면, 오픈 소스 LLM (Ollama, LM Studio 또는 자체 호스팅 옵션을 통해)으로 시작했을 것입니다.
장점:
데이터와 개인정보 보호에 대한 더 나은 제어권.
비용을 추산하기 더 쉬움 (또는 로컬에서 무료로 실행).
저의 오픈 소스 마인드셋과 더 잘 부합함.
아마도 첫날부터 여러 백엔드 (backends)를 지원할 수 있도록 도구의 구조를 설계했을 것입니다:
# 프로바이더 추상화 (provider abstraction, 의사 코드)
class LLMProvider:
def generate(self, prompt: str, **kwargs) -> str:
...
class OpenAIProvider(LLMProvider):
...
class OllamaProvider(LLMProvider):
...
그런 다음 사용자가 설정 (config)이나 환경 변수 (env vars)를 통해 선택할 수 있게 했을 것입니다.
- "작고 집중된 프롬프트"를 위한 설계 "만약을 대비해 모든 것을" 보내려고 시도하는 대신, 특정 작업에 맞춰 명령어를 설계했을 것입니다:
ask error < error.log
ask summarize README.md
ask explain tests/test_auth.py::test_login_fail
각 명령어는 맞춤형 프롬프트 템플릿 (prompt template)과 명확한 범위를 갖게 되어, 토큰 사용량을 낮게 유지하고 답변의 집중도를 높였을 것입니다.
-
안전성과 경계(Boundaries)를 명시적으로 설정하기
처음부터 가드레일(guardrails)을 구축했을 것입니다:
위험한 제안(예: "그냥 이 테이블을 삭제하세요")을 지양하도록 하는 명확한 시스템 프롬프트 (system prompt).
명령어가 파일을 수정하거나 코드를 실행할 수 있을 때의 경고.
UI에서 "저는 AI 어시스턴트이며, 귀하의 판단을 대체할 수 없습니다"라는 명시적인 분위기 조성.
AI 연구 및 집필 분야에서 활동하고 있는 만큼, 저는 책임감 있는 AI (responsible AI)를 매우 중요하게 생각합니다. 터미널에서 실행되는 CLI 도구라면 이를 모델링해야 합니다. -
대화 기록(Conversation History) 추가하기 (단순하더라도)
최소한의 세션 기록을 추가했을 것입니다:
마지막 N개의 턴(turn)을 로컬 SQLite DB나 JSON 파일에 저장.
ask --thread명령어를 통해 대화를 이어갈 수 있도록 허용.
새로운 프롬프트(prompt)를 입력하기 전에 짧은 컨텍스트(context) 요약 표시.
이렇게 하면 본격적인 채팅 앱을 만들지 않고도 후속 질문을 자연스럽게 이어갈 수 있습니다. -
더 일찍 오픈 소스(Open Source)로 공개하기
도구가 "완성"되었다고 느껴질 때까지 공유할 생각을 하지 않았습니다. 그것은 실수였습니다.
GitHub에 초기 버전을 출시했다면 다음과 같은 이점이 있었을 것입니다:
UX와 기능에 대한 피드백 확보.
CLI + AI에 관심 있는 기여자(contributor) 유치.
더 나은 문서(docs)를 작성하고 설계에 대해 더 명확하게 생각하도록 강제함.
비슷한 것을 만들고 있다면: v0.1을 빠르게 출시하고, 공개적으로 반복(iterate)하세요.
핵심 요약 (Key Takeaways)
LLM 기반 CLI 도구를 만드는 것은 프롬프트 (prompt), 컨텍스트 (context), 토큰 (token), 그리고 UX에 대해 구체적인 프로젝트를 통해 배울 수 있는 환상적인 방법입니다.
첫 번째 버전은 미숙할 것입니다; 그것은 괜찮습니다. 학습은 그것을 매일 사용하고 고충(pain points)을 느끼는 과정에서 옵니다.
개발자 도구의 경우, 스트리밍 (streaming), 컨텍스트 인식 (context awareness), 그리고 비용 제어 (cost control)는 선택 사항이 아니라 경험의 핵심입니다.
오픈 소스 모델 (open-source models)과 명확한 안전 경계 (safety boundaries)로 시작하는 것이 우리 중 많은 이들이 실제로 일하는 방식과 더 잘 맞습니다.
"완벽함"을 기다리는 것보다 일찍 출시하고 공개적으로 반복하는 것이 더 낫습니다.
여러분은 어떠신가요?
CLI 형태든 아니든, LLM 기반의 도구를 만들어 본 적이 있으신가요?
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기