build-from-scratch · Upstage Solar

도서관 검색 AI를
Solar로 처음부터 만들기

서울 100여 개 도서관을 한 번에 검색하는 AI 에이전트를, 빈 폴더에서 한 걸음씩 다시 만듭니다.

이번 버전의 핵심 변화: OpenAI GPT-4o-mini 대신 Upstage Solar를 두뇌로 씁니다. 아래로 죽 내리면 전 과정이 한 페이지에 있습니다.

이게 뭔가요?

BookToss는 2025 IA×AI 해커톤에서 만든 프로젝트입니다. 서울의 각 구청 도서관 사이트는 제각각이고 API도 없어서, 책 한 권을 찾으려면 사이트마다 들어가 팝업을 닫고 검색을 반복해야 합니다. BookToss는 이 과정을 AI 에이전트가 브라우저를 직접 조작해 한 번에 처리합니다.

이 페이지는 그 프로젝트를 완전히 처음부터 다시 만드는 과정을 누구나 따라 할 수 있도록 단계별로 기록한 튜토리얼입니다. 각 단계마다 무엇을 했는지뿐 아니라 그렇게 했는지를 함께 적었습니다.

이 문서를 보는 세 사람

따라 하는 개발자 — 명령어를 그대로 복사해 같은 결과를 재현할 수 있습니다.
저장소를 구경하는 사람 — 프로젝트가 어떻게 자랐는지 스크롤 한 번으로 봅니다.
Claude (AI 페어) — 다음 단계를 이어갈 때 이 문서를 맥락으로 참고합니다.

왜 Solar로 바꾸나요?

원본은 OpenAI GPT-4o-mini로 브라우저를 조작했습니다. UpstageSolar는 OpenAI 호환 엔드포인트를 제공하는 한국형 LLM이라, 인터페이스를 거의 바꾸지 않고 두뇌만 교체할 수 있습니다. 이 교체가 얼마나 쉬운지/어디서 막히는지를 직접 겪어보는 것이 이 재구축의 학습 목표 중 하나입니다.

핵심 사실

Solar는 https://api.upstage.ai/v1에서 OpenAI 호환 Chat Completions를 제공합니다. 모델은 solar-pro2(31B sLLM, 빠른 chat 모드 / 추론 모드 선택 가능). browser-use의 ChatOpenAI 래퍼에 base_urlapi_key만 Solar로 바꿔주면 됩니다 — 별도 어댑터가 필요 없습니다.

전체 그림 (목표 구조)

레이어기술역할
LLMUpstage Solar (solar-pro2)브라우저를 어떻게 조작할지 판단
에이전트browser-use각 도서관 사이트 자동 검색
파이프라인LangGraphresolve_catalog → search_book → parse_html
Streamlit웹 UI · 지도

아래 로드맵을 차례로 따라가면 이 구조가 한 조각씩 완성됩니다.

로드맵 — v0.0.1 → v0.1.0

"한 번에 다 만들기"는 막히면 어디서 틀어졌는지 알 수 없습니다. 그래서 각 버전이 끝나면 눈으로 확인할 수 있는 결과물이 남도록 10개의 작은 단계로 쪼갰습니다. v0.1.0의 목표는 거창하지 않습니다 — 도서관 딱 한 곳에서 책 한 권을 검색해 결과를 화면에 띄우는 것. 그 한 줄기 흐름(end-to-end)이 통하면 나머지는 도서관을 늘리는 일입니다.

버전이 버전이 끝나면…핵심 기술상태
v0.0.1Streamlit "Hello, BookToss" 화면이 브라우저에 뜬다Streamlit✅ 완료
v0.0.2터미널에서 Solar API에 첫 호출이 성공한다Upstage Solar · .env⏳ 다음
v0.0.3Streamlit 입력창에 쓰면 Solar의 답이 화면에 나온다Streamlit + Solar예정
v0.0.4코드를 src/ 구조로 정리한다 (llm·nodes·graph 분리)패키지 레이아웃예정
v0.0.5Playwright로 브라우저를 열고 어떤 페이지의 제목을 읽어온다Playwright예정
v0.0.6설정에서 도서관 한 곳의 검색 URL을 찾아낸다 (resolve_catalog)YAML · LangGraph 노드예정
v0.0.7그 도서관 페이지에서 검색창을 찾아 책 제목을 입력·제출한다Playwright예정
v0.0.8Solar가 페이지를 보고 "어떻게 검색할지"를 판단한다Solar + 브라우저 제어예정
v0.0.9검색 결과 HTML에서 책 정보(제목·저자·대출가능)를 뽑아낸다BeautifulSoup (parse_html)예정
v0.1.0🎉 도서관 1곳 end-to-end: 검색어 입력 → 결과를 화면에 표시전체 통합🎯 목표
이 로드맵을 읽는 법

위로 갈수록 "기반 다지기"(UI·API·구조), 아래로 갈수록 "실제 도서관 다루기"입니다. 각 줄은 독립적으로 동작하는 작은 마일스톤이라, 하나씩 PR로 머지하며 진행합니다. 진행하면서 이 표의 상태가 ✅로 바뀌고, 해당 버전의 상세 섹션이 이 아래에 채워집니다.

STEP 0 ✓

깨끗한 출발점 만들기

"처음부터 다시"라면, 정말로 빈 폴더에서 시작해야 합니다. 단, 옛 코드는 버리지 않고 참고용으로 보존합니다.

목표

왜 새 브랜치인가, 왜 orphan인가

같은 저장소 안에서 새 시도를 하려면 새 브랜치를 파는 게 자연스럽습니다. 그래야 안정된 상태를 건드리지 않고 자유롭게 실험할 수 있으니까요.

왜 일반 브랜치가 아니라 orphan?

일반 git checkout -b는 기존 커밋 이력을 그대로 물려받습니다. --orphan이력이 0인 브랜치를 만듭니다. 우리가 만들 첫 커밋이 이 브랜치의 진짜 첫 줄이 되죠. "from scratch"라는 의도와 가장 잘 맞습니다.

1. orphan 브랜치 생성

git checkout --orphan v2-solar
git reset           # 옛 파일들을 스테이지에서 내려놓음 (작업트리는 그대로)

2. 옛 코드를 docs/v1/ 로 이동

mkdir -p docs/v1
git mv 00_src README.md app.py requirements.txt docs/v1/
mv "BookToss_시연영상2.mp4" *.hwp *.pdf docs/v1/   # 추적 안 되던 자료들도 함께
왜 지우지 않고 옮기나

재구축의 두 핵심 자산 — 서울 도서관 100여 곳의 포털 URL을 담은 catalog_index.yaml, 그리고 구청 사이트별 HTML 파싱 규칙 — 은 처음부터 다시 만들 필요가 없습니다. 바퀴를 재발명하지 않고 "참고해서 골라 옮기는" 것이 현명한 재시작입니다.

3. 큰 바이너리는 git에서 제외

# .gitignore
.env
__pycache__/
docs/v1/*.mp4
docs/v1/*.hwp
docs/v1/*.pdf
docs/v1/00_src/data/raw/

4. 출발점 커밋

git add -A
git commit -m "chore: start v2-solar from scratch; archive v1 under docs/v1"

결과

repo 루트
├── README.md          ← v2 의도 + 진행표
├── .gitignore         ← .env, 캐시, 큰 미디어 제외
└── docs/
    ├── index.html     ← 지금 보고 있는 이 페이지
    └── v1/            ← 기존 해커톤 프로젝트 통째로 보존
        └── 00_src/configs/catalog_index.yaml  ← 나중에 재활용
짚고 갈 점

같은 저장소 안에 두 개의 독립된 역사가 공존하다가, 재구축 결과가 무르익으면 main으로 합쳐집니다. 옛 코드는 docs/v1/에 박제되어 사라지지 않습니다.

v0.0.1 · 완료 ✓

Streamlit "Hello, BookToss"

첫 결과물은 거창하지 않습니다 — 브라우저에 화면 하나를 띄우는 것. 이후 모든 단계의 결과를 볼 자리를 먼저 만듭니다.

왜 UI를 가장 먼저?

Solar 응답도, 도서관 검색 결과도, 결국 어딘가에 보여줘야 합니다. 화면을 먼저 세워두면 다음 단계부터는 print 대신 실제 화면으로 결과를 확인할 수 있습니다. Streamlit은 파이썬 스크립트 하나로 웹 UI를 그려주는 도구라, 가장 빠르게 "보이는 것"을 만들 수 있습니다.

1. Streamlit 설치

# requirements.txt
streamlit

# 설치
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt

2. app.py — 화면의 뼈대

페이지 설정과 제목까지는 정해진 틀입니다. 그 아래 첫 화면 본문은 직접 채워봅니다 (아래 "직접 해보기").

# app.py
import streamlit as st

st.set_page_config(page_title="BookToss", page_icon="📚", layout="centered")
st.title("📚 BookToss")
st.caption("서울 도서관을 한 번에 검색하는 AI 에이전트 · v0.0.1")

# 👉 여기에 첫 화면 본문을 직접 채웁니다 (TODO(human))

3. 실행

streamlit run app.py
# 브라우저가 자동으로 http://localhost:8501 을 엽니다
Streamlit 실행 모델 (알아두면 좋은 점)

streamlit run은 스크립트를 위→아래로 통째로 다시 실행하며 화면을 그립니다. 위젯과 상호작용할 때마다 전체가 재실행돼요. 그래서 "상태"는 보통 st.session_state로 따로 관리합니다 — 지금은 몰라도 되고, v0.0.3에서 만납니다.

첫 화면에 뭘 보여줄까 — 이렇게 채웠습니다

"무엇이 보일지"를 정하는 자리입니다. 이번엔 한 줄 소개 + 책 제목 입력칸 + 검색 버튼을 뒀습니다. 버튼 로직은 아직 진짜 검색이 아니라 "다음 버전에서 연결된다"는 안내만 합니다 — 이 입력칸과 버튼이 v0.1.0의 실제 검색창으로 자랍니다.

st.write("서울 곳곳의 도서관을 **한 번에** 검색하는 AI 에이전트입니다. 찾는 책을 입력해 보세요.")

book = st.text_input("찾는 책 제목", placeholder="예: 코스모스")
if st.button("검색", type="primary"):
    if book:
        st.info(f"'{book}' 검색은 다음 버전에서 연결됩니다. (지금은 화면만!)")
    else:
        st.warning("책 제목을 입력해 주세요.")
막혔던 지점

없었습니다 — streamlit run app.py 한 번에 화면이 떴습니다. Streamlit이 "파이썬만으로 가장 빠르게 보이는 것"을 만들어주는 이유죠. (앞으로 막히는 단계가 나오면 이 칸에 솔직히 기록합니다.)

v0.0.1 완료 — 화면이 뜬다. 다음은 v0.0.2 · Solar API 연결.

v0.0.2 · 진행 중

Solar API 연결

두뇌를 붙입니다. 터미널에서 Solar에 한 마디 건네고 답이 오는지 확인 — 이게 되면 이후 모든 LLM 단계의 토대가 섭니다.

핵심: Solar는 OpenAI 호환

Solar 전용 SDK는 필요 없습니다. 표준 openai 파이썬 SDK에서 base_urlapi_key만 Solar로 바꾸면 됩니다. 덕분에 LangChain·browser-use 같은 OpenAI 생태계 도구를 그대로 재사용할 수 있어요.

항목
base_urlhttps://api.upstage.ai/v1
모델solar-pro2
API 키 (env)UPSTAGE_API_KEYconsole.upstage.ai에서 발급

1. 의존성 + 키 보관

# requirements.txt 에 추가
openai
python-dotenv

pip install -r requirements.txt

키는 코드에 박지 않고 .env에 둡니다. .env는 git에 올리지 않고 (.gitignore에 포함), 대신 빈 틀 .env.example을 커밋해 둡니다.

# .env  (git에 올리지 않음)
UPSTAGE_API_KEY=up_여기에_본인_키

2. solar.py — 클라이언트와 호출

load_dotenv().env의 값을 환경변수로 불러오고, 그 키로 OpenAI 클라이언트를 Solar에 연결합니다. 실제 호출 한 줄은 직접 채웁니다.

from openai import OpenAI
from dotenv import load_dotenv
load_dotenv()

client = OpenAI(api_key=KEY, base_url="https://api.upstage.ai/v1")

# 👉 chat 호출은 직접 (TODO(human))
resp = client.chat.completions.create(model="solar-pro2", messages=[...])

3. 실행

python solar.py
# Solar: 안녕하세요, 저는 ...  ← 이런 응답이 뜨면 성공
막혔던 지점

(구현 후 채워집니다 — 키 오타, base_url 누락, 모델명 등 자주 막히는 자리.)

⏳ 진행 중. 응답이 오면 v0.0.2 완료. 다음은 v0.0.3 · Streamlit 입력 → Solar 응답 표시.