Ollama Custom CLI 명령어 시스템 심층 분석
Ollama Custom의 CLI 시스템은 Cobra 프레임워크를 기반으로 구현된 강력하고 직관적인 명령행 인터페이스입니다. 이 포스트에서는 cmd/cmd.go의 1,455라인에 걸친 복잡한 CLI 구현을 상세히 분석해보겠습니다.
1. CLI 아키텍처 개요
1.1 Cobra 프레임워크 활용
func NewCLI() *cobra.Command {
log.SetFlags(log.LstdFlags | log.Lshortfile)
cobra.EnableCommandSorting = false
// 루트 명령어 정의
rootCmd := &cobra.Command{
Use: "ollama",
Short: "Large language model runner",
CompletionOptions: cobra.CompletionOptions{
DisableDefaultCmd: true,
},
Run: DefaultHandler,
}
// 서브 명령어 등록
rootCmd.AddCommand(
serveCmd, createCmd, showCmd, runCmd, stopCmd,
pullCmd, pushCmd, listCmd, psCmd, copyCmd, deleteCmd,
)
return rootCmd
}
설계 특징:
- 계층적 구조: 루트 명령어 아래 기능별 서브 명령어
- 자동 완성 비활성화:
DisableDefaultCmd: true로 커스텀 제어 - 명령어 정렬 제어: 사용자 정의 순서로 명령어 표시
1.2 핵심 명령어 구조
ollama
├── serve # 서버 실행
├── create # 모델 생성
├── show # 모델 정보 표시
├── run # 모델 실행 및 대화
├── stop # 모델 중지
├── pull # 모델 다운로드
├── push # 모델 업로드
├── list # 모델 목록
├── ps # 실행 중인 모델 목록
├── copy # 모델 복사
└── delete # 모델 삭제
2. 주요 핸들러 함수 분석
2.1 CreateHandler - 모델 생성
func CreateHandler(cmd *cobra.Command, args []string) error {
p := progress.NewProgress(os.Stderr)
defer p.Stop()
var reader io.Reader
filename, err := getModelfileName(cmd)
if os.IsNotExist(err) {
if filename == "" {
reader = strings.NewReader("FROM .\n") // 기본 Modelfile
} else {
return errModelfileNotFound
}
}
// Modelfile 파싱
modelfile, err := parser.ParseFile(reader)
if err != nil {
return err
}
// 비동기 blob 생성
var g errgroup.Group
g.SetLimit(max(runtime.GOMAXPROCS(0)-1, 1))
files := syncmap.NewSyncMap[string, string]()
for f, digest := range req.Files {
g.Go(func() error {
if _, err := createBlob(cmd, client, f, digest, p); err != nil {
return err
}
files.Store(filepath.Base(f), digest)
return nil
})
}
return g.Wait()
}
핵심 기능:
- Modelfile 처리: 사용자 정의 모델 설정 파일 파싱
- 병렬 처리:
errgroup을 활용한 동시 파일 업로드 - 진행률 표시: 실시간 업로드 진행률 모니터링
- 에러 복구: 각 단계별 상세한 에러 처리
2.2 RunHandler - 대화형 모델 실행
func RunHandler(cmd *cobra.Command, args []string) error {
interactive := true
opts := runOptions{
Model: args[0],
WordWrap: os.Getenv("TERM") == "xterm-256color",
Options: map[string]any{},
}
// 명령행 플래그 처리
format, err := cmd.Flags().GetString("format")
if err != nil {
return err
}
opts.Format = format
// 비대화형 모드 처리
if msg, _ := cmd.Flags().GetString("message"); msg != "" {
interactive = false
opts.Prompt = msg
}
if interactive {
return generateInteractive(cmd, opts)
}
return generate(cmd, opts)
}
상호작용 모드:
- 대화형 모드: 실시간 사용자 입력 처리
- 배치 모드: 단일 프롬프트 실행 후 종료
- 터미널 감지: 환경에 따른 워드 랩핑 최적화
- 플래그 통합: 다양한 실행 옵션 지원
2.3 진행률 추적 시스템
func createBlob(cmd *cobra.Command, client *api.Client, path string, digest string, p *progress.Progress) (string, error) {
// 파일 정보 획득
fileInfo, err := bin.Stat()
if err != nil {
return "", err
}
fileSize := fileInfo.Size()
var pw progressWriter
status := fmt.Sprintf("copying file %s 0%%", digest)
spinner := progress.NewSpinner(status)
p.Add(status, spinner)
// 비동기 진행률 업데이트
done := make(chan struct{})
go func() {
ticker := time.NewTicker(60 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ticker.C:
percentage := int(100 * pw.n.Load() / fileSize)
spinner.SetMessage(fmt.Sprintf("copying file %s %d%%", digest, percentage))
case <-done:
spinner.SetMessage(fmt.Sprintf("copying file %s 100%%", digest))
return
}
}
}()
return client.CreateBlob(cmd.Context(), digest, io.TeeReader(bin, &pw))
}
진행률 시스템:
- 실시간 업데이트: 60ms 간격으로 진행률 갱신
- 원자적 카운터:
atomic.Int64로 동시성 안전성 보장 - TeeReader 활용: 데이터 전송과 동시에 진행률 추적
- 채널 동기화: Goroutine 간 안전한 통신
3. 환경 설정 및 구성 관리
3.1 환경 변수 문서화
func appendEnvDocs(cmd *cobra.Command, envs []envconfig.EnvVar) {
if len(envs) == 0 {
return
}
envUsage := `
Environment Variables:
`
for _, e := range envs {
envUsage += fmt.Sprintf(" %-24s %s\n", e.Name, e.Description)
}
cmd.SetUsageTemplate(cmd.UsageTemplate() + envUsage)
}
3.2 명령어별 환경 변수 매핑
// 서버 명령어에 특화된 환경 변수들
case serveCmd:
appendEnvDocs(cmd, []envconfig.EnvVar{
envVars["OLLAMA_DEBUG"], // 디버그 모드
envVars["OLLAMA_HOST"], // 서버 호스트
envVars["OLLAMA_KEEP_ALIVE"], // 모델 유지 시간
envVars["OLLAMA_MAX_LOADED_MODELS"], // 최대 로드 모델 수
envVars["OLLAMA_MAX_QUEUE"], // 최대 큐 크기
envVars["OLLAMA_MODELS"], // 모델 저장 경로
envVars["OLLAMA_NUM_PARALLEL"], // 병렬 처리 수
envVars["OLLAMA_FLASH_ATTENTION"], // Flash Attention 사용
envVars["OLLAMA_KV_CACHE_TYPE"], // KV 캐시 타입
envVars["OLLAMA_GPU_OVERHEAD"], // GPU 오버헤드
})
구성 관리 특징:
- 명령어별 특화: 각 명령어에 필요한 환경 변수만 표시
- 자동 문서화: 도움말에 환경 변수 설명 자동 추가
- 타입 안전성:
envconfig.EnvVar구조체로 타입 보장
4. 에러 처리 및 복구 전략
4.1 계층화된 에러 처리
func StopHandler(cmd *cobra.Command, args []string) error {
opts := &runOptions{
Model: args[0],
KeepAlive: &api.Duration{Duration: 0},
}
if err := loadOrUnloadModel(cmd, opts); err != nil {
if strings.Contains(err.Error(), "not found") {
return fmt.Errorf("couldn't find model \"%s\" to stop", args[0])
}
return err
}
return nil
}
에러 처리 패턴:
- 에러 분류: 에러 메시지 내용에 따른 적절한 대응
- 사용자 친화적 메시지: 기술적 에러를 이해하기 쉬운 메시지로 변환
- 문맥 보존: 원본 에러 정보 유지하면서 추가 정보 제공
4.2 버전 호환성 체크
func versionHandler(cmd *cobra.Command, _ []string) {
client, err := api.ClientFromEnvironment()
if err != nil {
return
}
serverVersion, err := client.Version(cmd.Context())
if err != nil {
fmt.Println("Warning: could not connect to a running Ollama instance")
}
if serverVersion != "" {
fmt.Printf("ollama version is %s\n", serverVersion)
}
// 클라이언트-서버 버전 불일치 경고
if serverVersion != version.Version {
fmt.Printf("Warning: client version is %s\n", version.Version)
}
}
5. 성능 최적화 기법
5.1 동시성 제어
// CPU 코어 수에 기반한 goroutine 제한
var g errgroup.Group
g.SetLimit(max(runtime.GOMAXPROCS(0)-1, 1))
// 동시성 안전 맵 활용
files := syncmap.NewSyncMap[string, string]()
adapters := syncmap.NewSyncMap[string, string]()
for f, digest := range req.Files {
g.Go(func() error {
if _, err := createBlob(cmd, client, f, digest, p); err != nil {
return err
}
files.Store(filepath.Base(f), digest)
return nil
})
}
최적화 전략:
- 적응형 동시성: 시스템 리소스에 따른 동적 제한
- 메모리 안전성: 타입 안전 동시성 맵 사용
- 리소스 관리: 고루틴 누수 방지를 위한 제한 설정
5.2 스트리밍 처리
type progressWriter struct {
n atomic.Int64
}
func (w *progressWriter) Write(p []byte) (n int, err error) {
w.n.Add(int64(len(p)))
return len(p), nil
}
// TeeReader를 활용한 스트리밍 업로드
err := client.CreateBlob(cmd.Context(), digest, io.TeeReader(bin, &pw))
스트리밍 장점:
- 메모리 효율성: 대용량 파일을 청크 단위로 처리
- 진행률 추적: 실시간 업로드 진행률 모니터링
- 원자적 업데이트: Lock-free 카운터로 성능 향상
6. 사용자 경험 최적화
6.1 대화형 인터페이스
func generateInteractive(cmd *cobra.Command, opts runOptions) error {
// 터미널 설정
if opts.WordWrap {
scanner := bufio.NewScanner(os.Stdin)
for {
fmt.Print(">>> ")
if !scanner.Scan() {
break
}
prompt := scanner.Text()
if prompt == "/bye" {
break
}
// 실시간 응답 스트리밍
err := streamResponse(cmd, opts, prompt)
if err != nil {
fmt.Printf("Error: %v\n", err)
}
}
}
return nil
}
UX 개선사항:
- 직관적 프롬프트:
>>>표시로 입력 대기 상태 명확화 - 종료 명령어:
/bye로 자연스러운 대화 종료 - 실시간 피드백: 스트리밍 응답으로 즉각적인 반응
6.2 진행률 시각화
func (resp api.ProgressResponse) error {
if resp.Digest != "" {
bar, ok := bars[resp.Digest]
if !ok {
msg := resp.Status
if msg == "" {
msg = fmt.Sprintf("pulling %s...", resp.Digest[7:19])
}
bar = progress.NewBar(msg, resp.Total, resp.Completed)
bars[resp.Digest] = bar
p.Add(resp.Digest, bar)
}
bar.Set(resp.Completed)
}
return nil
}
7. 확장성 및 유지보수성
7.1 명령어 추가 패턴
// 새로운 명령어 등록 패턴
newCmd := &cobra.Command{
Use: "newcommand",
Short: "Description of new command",
Args: cobra.ExactArgs(1),
RunE: NewCommandHandler,
}
// 플래그 추가
newCmd.Flags().StringP("option", "o", "", "Option description")
// 루트에 추가
rootCmd.AddCommand(newCmd)
7.2 에러 타입 확장
var (
errModelfileNotFound = errors.New("specified Modelfile wasn't found")
errInvalidFormat = errors.New("invalid format specified")
errConnectionFailed = errors.New("failed to connect to server")
)
8. 결론
Ollama Custom의 CLI 시스템은 현대적인 Go 개발 패턴을 충실히 따르는 설계를 보여줍니다:
8.1 주요 장점
- 모듈화: 각 명령어가 독립적인 핸들러로 분리
- 동시성: 효율적인 비동기 처리와 진행률 추적
- 확장성: 새로운 명령어 추가가 용이한 구조
- 사용자 중심: 직관적인 인터페이스와 상세한 피드백
8.2 성능 특징
- 리소스 효율성: CPU 코어 수에 적응하는 동시성 제어
- 메모리 최적화: 스트리밍 처리로 대용량 파일 처리
- 응답성: 실시간 진행률 표시로 사용자 경험 향상
다음 포스트에서는 서버 아키텍처와 HTTP API 라우팅 시스템을 분석해보겠습니다.