본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 17. 11:06

Sentience가 단일 제공자에 종속되지 않고 60개 이상의 AI 도구를 하나의 로컬 데스크톱 앱에 담아내는 방법

요약

다양한 AI 모델 제공자(OpenAI, Anthropic, Groq, Ollama) 간의 도구 스키마 차이를 극복하고 60개 이상의 도구를 통합 관리하는 데스크톱 앱 'Sentience' 개발 사례를 소개합니다. 어댑터 패턴을 활용해 코드 복잡성을 최소화하며 멀티 모델 환경을 구축하는 방법을 다룹니다.

핵심 포인트

  • OpenAI 표준과 Anthropic API 간의 도구 스키마 차이 해결
  • 얇은 어댑터(Thin Adapter)를 통한 제공자 레이어 최적화
  • 60개 이상의 도구 함수를 단일 인터페이스로 통합 관리
  • PySide6 기반의 로컬 데스크톱 AI 어시스턴트 구현

저는 노트북 덮개를 닫아버릴 수 있는 데스크톱 앱에서 Cursor의 UX와 Zo Computer의 도구 폭을 모두 갖추기를 원했습니다. 그래서 저는 Sentience를 만들었습니다. 이는 완전히 사용자의 기기에서 실행되며, 자체 브라우저, 자체 이메일 클라이언트, 자체 음성 컨트롤러를 갖추고 모델에 60개 이상의 도구 함수 (tool functions)를 노출하는 PySide6 기반 데스크톱 AI 어시스턴트입니다.

어려운 점은 도구 자체가 아니었습니다. 어려운 점은 Groq, OpenAI, Anthropic, 그리고 로컬 Ollama 인스턴스 전체에서 60개 이상의 도구 스키마 (tool schemas)가 동일하게 작동하도록 만드는 것이었습니다. 제공자별 도구 디스패처 (provider-specific tool dispatcher)를 작성하지 않고도, 사용자가 오늘 어떤 모델을 사용하고 있는지 고민하게 만들지 않으면서 말이죠.

이 부분은 제가 실제로 자랑스럽게 생각하는 코드베이스의 일부이며, 아무도 튜토리얼로 공개하지 않는 부분입니다.

제약 사항 (The constraint)

OpenAI의 /v1/chat/completions 형식은 이제 사실상의 표준 (de facto standard)입니다. Groq도 이를 구현합니다. Ollama도 구현합니다. Localai도 구현합니다. 따라서 제가 지원하고자 했던 네 가지 제공자 중 세 곳은 OpenAI 도구 스키마 (tool schema)를 기준으로 삼는다면 하나의 HTTP 호출로 "그냥 작동"합니다.

Anthropic은 그렇지 않습니다. Messages API는 다음과 같은 방식을 사용합니다:

  • 배열 내의 시스템 메시지 대신 별도의 system 필드 사용
  • Authorization: Bearer 대신 x-api-key 사용
  • 필수 헤더로 anthropic-version: 2023-06-01 사용
  • 응답 측에서 다른 tool_use / tool_result 콘텐츠 블록 형식 사용

여기서 질문은 이것입니다: 두 개의 도구 디스패처와 두 개의 실행 경로를 작성할 것인가, 아니면 하나의 통합된 도구 목록과 하나의 디스패처를 유지할 수 있게 해주는 얇은 어댑터 (thin adapter)를 작성할 것인가?

저는 어댑터를 선택했습니다. 전체 제공자 레이어 (provider layer)는 60줄에 불과합니다.

제공자 레지스트리 (The provider registry)

PROVIDERS = {
    "groq": {
        "name": "Groq",
...

무료 티어 (Free tier)는 주석이 아닌 일급 객체 필드 (first-class field)입니다. 설정 대화 상자에서는 Groq와 Ollama에 대해 "free" 배지를 표시하며, README는 신규 사용자를 위해 이 두 가지를 앞세웁니다.

두 가지 메서드를 가진 chat()

클래스 전체에는 하나의 진입점 (entry point)과 두 개의 프라이빗 메서드 (private methods)가 있습니다. 진입점은 self.provider를 기반으로 경로를 선택합니다. 그게 전부입니다.

class AIClient:
    def __init__(self, provider: str, model: str, api_key: str = ""):
        self.provider = provider
...

Anthropic 브랜치는 OpenAI 브랜치와 정확히 세 가지 차이점만 있습니다: 헤더 이름 (header names), 시스템 메시지 위치 (system-message location), 그리고 경로 (path)입니다. 그 외의 모든 것 — tools 리스트, messages 배열, max_tokens 필드 — 은 동일합니다. 따라서 제가 Groq를 위해 등록했던 60개의 동일한 도구 스키마 (tool schemas)가 단 하나의 함수 정의도 다시 작성하지 않고도 Claude에서 작동합니다.

이는 사용자에게 "Claude Sonnet으로 전환"이라고 적힌 드롭다운 메뉴를 제공할 수 있으며, 사용자가 _다음에 입력하는 메시지_는 정확히 동일한 도구 인터페이스 (tool surface)를 가진 Anthropic의 API로 전송된다는 것을 의미합니다. 모델은 네 가지 제공자 중 어느 곳에서나 read_file, list_directory, browser_navigate, send_email, oauth_github_login을 호출할 수 있습니다. 디스패처 (dispatcher)는 상관하지 않습니다.

통합 도구 리스트 (The unified tool list)

도구들은 각자의 모듈에 존재하며, 단일 * 스프레드 (spread) 연산자를 통해 집계됩니다:

from browser.automation import BROWSER_TOOLS, PLAYWRIGHT_AVAILABLE
from email_agent.client import EMAIL_TOOLS, init_email, execute_email_tool
from oauth_manager.manager import OAUTH_TOOLS, get_oauth_manager, execute_oauth_tool
...

각 서브모듈 (submodule)은 스키마 (schema) (BROWSER_TOOLS, EMAIL_TOOLS 등)와 실행기 (executor) (execute_browser_tool, execute_email_tool)를 모두 내보냅니다 (export). 스키마는 OpenAI 함수 호출 (function-calling) 딕셔너리 (dict)입니다. 실행기는 모델이 도구를 호출할 때 디스패처가 실제로 호출하는 Python 함수입니다.

디스패처 (The dispatcher)

def execute_tool(name: str, args: dict, workspace: str) -> dict:
    try:
        if name == "read_file":
...

제가 강조하고 싶은 두 가지 패턴은 다음과 같습니다:

  1. 모든 실행기(executor)는 {"success": bool, ...}를 반환합니다. 어떤 도구에서 오류가 발생하더라도 모델은 일관된 응답 형태를 받게 됩니다. 이를 통해 모델은 재시도할지, 상위 단계로 에스컬레이션(escalate)할지, 아니면 단순히 사용자에게 "그 파일을 읽을 수 없습니다"라고 말할지를 결정할 수 있습니다. 이것이 바로 60개의 도구 중 하나가 대화 도중에 실패하더라도 시스템을 실제로 사용 가능하게 만드는 핵심입니다.

  2. 모든 파일 경로는 워크스페이스(workspace)를 기준으로 해결(resolved)됩니다. 모델은 사용자가 허가하지 않은 절대 경로(absolute path)를 지정할 수 없습니다. Path(workspace) / path는 아주 짧은 코드 한 줄이지만, 이 한 줄 덕분에 "낯선 사람의 노트북에서 이 앱을 실행하더라도 ~ 탈출 공격(escape exploits)을 걱정할 필요가 없음"을 의미하게 됩니다.

이것이 사용자에게 주는 가치

실제적인 핵심 기능(killer feature)은 바로 **설정 드롭다운(settings dropdown)**입니다. 사용자는 다음과 같은 작업을 수행할 수 있습니다:

  • 테스트를 위해 Groq의 무료 티어(llama-3.3-70b-versatile)로 시작
  • 더 나은 추론(reasoning)이 필요한 작업이 생기면 Claude Sonnet으로 전환 — 동일한 60개의 도구, 동일한 채팅 기록 유지
  • 와이파이가 없는 비행기 안에서는 로컬 Ollama로 전환 — 동일한 60개의 도구, 동일한 채팅 기록 유지
  • 어떤 모델이 대화하고 있든 관계없이 동일한 세션을 통해 OAuth 흐름과 브라우저 자동화(browser automation)를 라우팅

도구 디스패처(tool dispatcher)는 self.provider에 어떤 제공자(provider)가 있는지 알 필요도 없고 신경 쓰지도 않습니다. 도구 목록은 변하지 않습니다. 채팅 기록에 "이것은 Anthropic 스레드임"과 같은 특별한 분기(branch)가 생기지도 않습니다. 800줄짜리 main.py에는 단 하나의 AIClient와 하나의 execute_tool이 있으며, 나머지는 PySide6 위젯과 메시지 라우팅 접착제(glue)로 구성되어 있습니다.

내가 다르게 했을 부분

두 가지가 있습니다:

  1. Anthropic의 도구 결과(tool result) 형식은 여전히 응답 측면에서 다릅니다. Claude가 도구를 호출할 때, 응답은 tool_use 블록을 사용하며 다음 턴에 tool_result 블록을 다시 보내야 합니다. 저는 이를 디스패처(dispatcher)가 아닌 메시지 라우팅(message-routing) 레이어에서 처리합니다. 만약 처음부터 다시 시작한다면, 도구 결과 변환 작업을 _chat_anthropic 메서드
    내부로 이동시켜서, 디스패처가 네 가지 제공자 모두가 동일한 형태를 반환하는 것처럼 처리할 수 있게 만들었을 것입니다.

  2. 스트리밍(Streaming)은 절반만 구현되었습니다. Groq와 OpenAI는 동일하게 스트리밍하지만, Anthropic은 다르게 스트리밍합니다 (데이터 전용 SSE가 아닌 이벤트 타입 방식). 현재 빌드는 전체 응답을 버퍼링한 후 한 번에 보여줍니다. 4k 토큰 답변에는 괜찮지만, 64k 추론 과정(reasoning trace)에는 적합하지 않습니다. 해결 방법은 채팅 디스패처와 구조가 같습니다. 제공자 제품군당 하나의 스트리밍 메서드를 만들고, UI를 위해 하나의 정규화된 청크 반복자(normalized chunk iterator)를 사용하는 것입니다.

스택 (The stack)

  • GUI: PySide6 (Qt for Python) — 네이티브 위젯 사용, Electron 미사용
  • Browser: stealth 패치가 적용된 Playwright
  • Email: 표준 라이브러리(stdlib)의 imaplib + smtplib
  • Voice: SpeechRecognition + pyttsx3
  • AI: 3/4개 제공자를 위한 OpenAI 호환 클라이언트, Anthropic을 위한 네이티브 어댑터
  • Skills: 매니페스트(manifest)와 실행기(executor)를 갖춘 도메인별 Python 모듈
  • Storage: 채팅 기록을 위한 SQLite, 설정을 위한 JSON
  • BYOK: Bring Your Own Key — Groq 무료 티어, OpenAI, Anthropic, 또는 완전히 로컬인 Ollama

실행해보기 (Try it)

git clone https://github.com/AmSach/sentience
cd sentience
pip install -r requirements.txt
...

100% 오프라인 사용 시:

ollama pull llama3.2
OLLAMA_HOST=http://localhost:11434 python src/main.py

MIT 라이선스입니다. PR(Pull Request)을 환영합니다 — 특히 스트리밍 레이어, 새로운 제공자, 또는 다른 도구 모듈(캘린더? GitHub? Linear?)에 관한 것이라면 더욱 좋습니다.

GitHub: https://github.com/AmSach/sentience

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0