uel/, graphv2/, skills/라는 이름으로 직접 재구현해서 한 패키지 안에 넣어놨다는 것이다.
거기에 16종 안전 정책 엔진, "Ralph 루프"라는 독특한 자율 개발 패턴, Slack·Discord·Telegram 연동, OCR 파이프라인까지 들어있다. 결과물은 소스만 파이썬 19.4만 줄 · 525개 파일 · 37개 서브패키지의 거대한 단일 프레임워크다.
그래서 이 문서는 "Upsonic 쓰는 법"이 아니라 유명 프레임워크들의 핵심 설계를 한 저장소에서 비교하며 읽는 법에 초점을 맞춘다. 남의 코드로 LCEL과 LangGraph를 동시에 공부할 수 있는, 흔치 않은 교보재다.
(저장소: Upsonic/Upsonic · Python · MIT · v0.77.3 · 별 7,913 · 포크 738 · 생성 2024-05-26 · 최신 push 2026-06-18 · TrendShift 실시간 언급 등재)
pip install로 제공하려는, 배터리 내장형(batteries-included) 파이썬 에이전트 프레임워크.LLM 호출 래퍼(29개 프로바이더), 툴 시스템(+MCP), 멀티에이전트 팀, 그래프 오케스트레이션, 벡터DB·RAG, 메모리·스토리지 7종, 안전 정책 엔진, OCR, 메신저 연동 6종이 전부 하나의 패키지 안에 있다.
대부분의 에이전트 프레임워크는 전문점이다. LangChain은 체인, LangGraph는 상태 그래프, LlamaIndex는 RAG — 각자 잘하는 걸 하고 나머지는 조합해서 쓴다.
Upsonic은 대형마트를 지향한다. 체인도 있고, 그래프도 있고, RAG도 있고, 심지어 정육 코너(OCR)와 문구 코너(Slack 봇)까지 있다. 장점은 명확하다 — 한 번 들어가면 다른 데 갈 일이 없다. 단점도 명확하다 — 각 코너의 깊이는 전문점만 못할 수 있고, 마트 전체를 유지보수하는 건 어렵다.
Upsonic이 제공하는 진입점은 크게 둘이다. 이름이 헷갈리기 쉬우니 먼저 정리한다.
| 클래스 | 무엇인가 | 언제 쓰나 |
|---|---|---|
Agent | 전통적 에이전트. 태스크를 받아 LLM에 던지고, 툴을 호출하고, 결과를 돌려준다. 내부적으로 24단계 파이프라인을 통과한다. | "이 문장을 요약해줘", "이 API 불러서 표로 만들어줘" 같은 경계가 명확한 작업 |
AutonomousAgent | Agent를 상속하고, 파일시스템 툴킷 + 셸 툴킷을 기본 장착한 버전. 작업 폴더(workspace)를 주면 그 안에서 파일을 읽고 쓰고 명령을 실행한다. | "이 로그 폴더를 분석해서 이상 패턴을 찾아줘" 같은 스스로 돌아다녀야 하는 작업 |
# 전통적 에이전트 — 한 번 물어보고 답 받기
from upsonic import Agent, Task
agent = Agent(model="anthropic/claude-sonnet-4-5", name="Stock Analyst")
task = Task(description="현재 시장 트렌드를 분석해줘")
agent.print_do(task)
# 자율 에이전트 — 폴더를 주고 알아서 돌아다니게 하기
from upsonic import AutonomousAgent, Task
agent = AutonomousAgent(
model="anthropic/claude-sonnet-4-5",
workspace="/path/to/logs", # ← 이 폴더 밖으로는 못 나간다
)
agent.print_do(Task("서버 로그를 분석해서 이상 패턴을 찾아줘"))
README는 "Build Autonomous AI Agents in Python"이라고 하는데, pyproject.toml의 description 필드에는 아직 "Agent Framework For Fintech"가 남아 있다. 2024년 5월 시작 당시의 핀테크 지향 포지셔닝이 그대로 남은 흔적으로 보인다. PyPI에서 검색하면 이 옛 문구가 보일 수 있으니 혼동하지 말 것.
이게 Upsonic의 가장 큰 특징이자, 가장 논쟁적인 지점이다. 세 군데를 보자.
| Upsonic 모듈 | 원본 | 일치 정도 |
|---|---|---|
uel/ | LangChain LCEL | chain = prompt | model | parser 파이프 문법, RunnableSequence, RunnableParallel, RunnableBranch, RunnableLambda, StrOutputParser, @chain 데코레이터, itemgetter 헬퍼까지 동일 |
graphv2/ | LangGraph | StateGraph, add_conditional_edges, START/END, Command, Send, interrupt_before, MemorySaver라는 클래스 이름까지 동일 |
skills/ | Anthropic Agent Skills | SKILL.md + references/ + scripts/ 디렉토리 구조, frontmatter 필드(name 64자, description 1024자 제한)까지 스펙 준수. 검증기 첫 줄 docstring이 "following the Agent Skills spec"이라고 명시 |
안드로이드 폰을 사면 구글 지도, 유튜브, 지메일이 들어있다. 삼성 폰을 사면 여기에 삼성 지도, 삼성 브라우저, 삼성 페이가 나란히 더 들어있다. "이미 있는데 왜 또?"라는 반응과 "하나로 통일돼서 편한데?"라는 반응이 동시에 나오는 그 상황이, 지금 Upsonic이 놓인 자리다.
LCEL의 | 연산자가 내부에서 어떻게 동작하는지 궁금할 때, LangChain 본체는 수많은 추상화 레이어에 묻혀 있어 따라가기 어렵다. Upsonic의 uel/runnable.py는 같은 API를 훨씬 작은 코드로 재현해놓아서, 핵심 아이디어만 골라 읽기 좋다. LangGraph의 체크포인트 개념도 마찬가지다.
재구현본은 원본의 커뮤니티·생태계·버그픽스 속도를 따라가기 어렵다. LangGraph를 쓰려는데 graphv2를 대신 쓸 이유는 "Upsonic 안에서 다른 기능과 통합돼 있다"는 것뿐이다. 반대로 이미 LangChain 생태계를 쓰고 있다면 갈아탈 유인이 크지 않다.
ralph/ 모듈은 코드 주석에서 스스로를 "Ralph/Groundhog 기법"이라 부른다. Groundhog Day(사랑의 블랙홀), 즉 "매일 같은 하루가 반복되지만, 기억은 파일에 남아 조금씩 나아진다"는 그 영화의 구조를 그대로 가져온 이름이다.
LLM에게 큰 프로젝트를 맡기면 대화가 길어질수록 앞의 맥락을 잃고 헤맨다. Ralph의 해법은 과감하다 — "기억을 아예 포기하고, 대신 종이에 적는다."
매 반복마다 완전히 새로운 에이전트 인스턴스를 만든다. 그 에이전트는 아무것도 기억하지 못한 채 fix_plan.md라는 체크리스트 파일을 열어보고, 아직 - [ ]인 항목 하나만 처리하고, 체크하고 퇴근한다. 다음 반복에서 또 새 에이전트가 출근한다. 인수인계는 오직 파일로만 이뤄진다.
# ralph/phases/incremental.py — 이터레이션 성공 판정
# LLM이 "다 했어요"라고 말하는 걸 믿지 않는다.
state_after = self.state_manager.load_state()
pending_count_after = len(state_after.get_pending_items())
# 진짜 성공: 체크리스트 항목이 [ ] → [x] 로 바뀌었는가
task_completed = pending_count_after < pending_count_before
return IterationResult(
iteration=self._current_iteration,
task_picked=first_item,
success=task_completed,
backpressure_passed=task_completed,
)
여기에 백프레셔 게이트가 붙는다. ralph/backpressure/gate.py의 BackpressureGate는 사용자가 지정한 build_command / test_command / lint_command를 실제로 실행해 PASS/FAIL을 판정하고, 그 결과를 문자열로 에이전트에게 되돌려준다. "빌드와 테스트가 통과해야 다음으로 넘어간다"는 물리적 제동장치다.
safety_engine/policies/에는 PII, 금융, 의료, 법률, 피싱, 사기 탐지, 내부자 위협, 암호화폐, 사이버보안, 욕설, 성인 콘텐츠, 전화번호, 민감 사회 이슈, 기술 정보, 스킬 프롬프트 인젝션, 도구 안전성 등 16개 영역의 정책이 들어 있다. 각 정책은 정규식 규칙과 LLM 판정 규칙을 모두 갖는 공통 패턴을 따른다.
특히 눈여겨볼 것은 anonymization.py의 가역적 익명화다.
고객 주민번호가 든 문서를 외부 번역가에게 맡겨야 한다고 하자. 그냥 지우면(마스킹) 번역가가 문맥을 못 잡는다. Upsonic의 Anonymizer는 가짜 번호로 바꿔치기한다 — 숫자는 숫자로, 대문자는 대문자로, 특수문자는 그대로 유지해서 "생김새는 같지만 가짜인" 값을 만든다. 그리고 변환 맵을 들고 있다가, 번역 결과를 받으면 가짜를 다시 진짜로 되돌려놓는다. LLM은 진짜 개인정보를 한 번도 본 적이 없게 된다.
LLM 판정 쪽도 재밌다. safety_engine/llm/upsonic_llm.py의 UpsonicLLMProvider는 판정을 위해 또 다른 Upsonic Agent를 내부에서 생성한다. 즉 에이전트가 에이전트를 감시하는 구조다. 그리고 판정 결과는 Pydantic 모델로 강제되며, confidence >= 0.7일 때만 채택한다.
README는 "모든 파일·셸 작업이 workspace로 제한되고, 경로 탈출과 위험 명령이 차단된다"고 말한다. 절반만 맞다.
파일 작업은 실제로 견고하게 막혀 있다(다음 섹션 코드 참조). 하지만 셸은 다르다 — shell_toolkit.py의 차단 목록은 딱 5개 패턴(rm -rf /, rm -rf /*, 포크폭탄, mkfs, dd if=/dev/zero)뿐이고, 명령 문자열에 이 패턴이 포함되는지만 본다. cat /etc/passwd나 curl로 외부에 데이터를 보내는 건 전혀 막히지 않는다. 명령은 cwd=workspace로 실행될 뿐, 경로가 강제되지는 않는다.
README가 "다음 단계: E2B 샌드박스 프로바이더를 연결해 격리된 클라우드 실행 환경을"이라고 안내하지만, 코드상 AutonomousAgent와 E2B 사이에는 어떤 자동 연결도 없다(autonomous_agent/ 전체에 e2b 참조 0건). E2B는 tools/custom_tools/e2b.py의 완전히 별개인 opt-in 툴킷이며, 사용자가 AutonomousAgent(tools=[E2BTools(api_key=...)])처럼 직접 붙여야 한다.
graph/와 graphv2/가 나란히 있고, 최상위 memory/(SHA256 파일명 기반 단순 JSON 저장)와 storage/memory/(본격 Memory 오케스트레이터)가 별개로 존재한다. 문서를 읽을 때 어느 쪽을 가리키는지 항상 확인해야 한다. 큰 프레임워크가 빠르게 자랄 때 흔히 생기는 부채다.
이 정도 규모의 프레임워크치고 필수 의존성이 적다. pyproject.toml의 기본 dependencies는 22개뿐이고, 무거운 것들은 전부 optional extras로 빠져 있다.
| 영역 | 패키지 | 역할 |
|---|---|---|
| 데이터 검증 | pydantic ≥2.10, pydantic-core | Task/툴 스키마/구조화 출력의 뼈대. Upsonic 전체가 Pydantic 위에 서 있다. |
| 비동기 | anyio, nest-asyncio, httpx, aiohttp | async-first 설계. nest-asyncio는 주피터 노트북처럼 이미 이벤트 루프가 도는 환경 대응용. |
| LLM | openai ≥2.2, genai-prices | OpenAI SDK만 기본 포함(호환 엔드포인트가 많아서). genai-prices는 토큰 비용 계산용 — agent.cost 속성의 근거. |
| 문서 파싱 | griffe | 함수 docstring을 파싱해 툴 스키마를 자동 생성. Google/NumPy/Sphinx 형식 지원의 실체. |
| 관측 | sentry-sdk[opentelemetry], psutil | 에러 추적 + OTel 트레이싱. v0.77.0부터 Sentry는 strict opt-in으로 바뀌었다(그 전엔 기본 켜짐이었다는 뜻). |
| 출력 | rich | 터미널의 컬러 테이블·패널. print_do()가 예쁘게 나오는 이유. |
| 직렬화 | cloudpickle | 람다·클로저까지 피클링. 툴/그래프 노드를 저장하기 위함. |
pip install upsonic[ocr]처럼 대괄호로 지정해야 설치되는 추가 패키지 묶음. 기본 설치는 가볍게 유지하고, 필요한 사람만 무거운 걸 받게 하는 파이썬 패키징 관례. Upsonic은 이걸 매우 공격적으로 쓴다.이 구조가 주는 교훈: "모든 걸 지원한다"와 "설치가 무겁다"는 양립할 수 있다. 벡터DB 7종을 지원하지만, 실제로 설치되는 건 당신이 고른 하나뿐이다.
providers/ 디렉토리 파일명 기준 전체 목록이다. 코드에 실재하는 것만 적었다.
models/와 providers/가 거의 같은 이름으로 나란히 있어서 헷갈린다. 콘센트와 어댑터로 생각하면 쉽다 — providers/는 각 회사 API에 실제로 접속하는 어댑터(인증·엔드포인트·요청 형식), models/는 그 위에서 Upsonic이 공통으로 다루는 규격화된 플러그(토큰 계산, 스트리밍, 툴 호출 변환)다.
Upsonic에는 웹 UI가 없다. 대신 interfaces/가 메신저를 UI로 삼는다. Slack, Discord, Telegram, WhatsApp, Gmail, 범용 메일 6종이며, 각 폴더는 <채널>.py + schemas.py 구성으로 통일돼 있다.
InterfaceManager.serve(port=...)로 웹훅 서버를 띄우면(uvicorn) 메신저 메시지가 에이전트로 흘러든다. 동작 모드는 둘이다.
| 모드 | 동작 | 비유 |
|---|---|---|
TASK | 메시지 하나가 독립 태스크. 이전 대화를 기억하지 않는다(stateless). | 민원 창구 — 매번 새 번호표 |
CHAT | 대화 세션이 누적된다(stateful). /reset으로 히스토리 초기화. | 담당자 지정 상담 — 지난 얘기를 기억 |
| 도구 | 용도 |
|---|---|
uv | 패키지 관리·빌드·배포. v0.77.1에서 CI를 pypa 액션 → uv publish로 전환했다. |
pytest (4-tier) | unit_tests 122개 · smoke_tests 174개 · doc_examples 136개 · integration_tests 3개 파일 |
release-please | release-please-config.json — 커밋 메시지 기반 자동 버전업·CHANGELOG 생성. v0.77.0에서 도입. |
Makefile | 테스트·린트 단축 명령 |
tests/doc_examples/는 공식 문서에 적힌 코드가 실제로 돌아가는지를 CI에서 자동 검증한다. 문서가 낡아서 예제가 안 돌아가는 건 모든 프레임워크의 고질병인데, 이걸 테스트로 묶어버린 건 배울 만한 실천이다.
agent/agent.py는 5,424줄짜리 괴물 파일이다. 하지만 실제 실행 로직은 대부분 agent/pipeline/으로 빠져나가 있다. 모든 단계는 하나의 추상 클래스를 상속한다.
# agent/pipeline/step.py (구조 요약)
class Step(ABC):
async def execute(self, ctx) -> StepResult: ... # 일반 실행
async def execute_stream(self, ctx): # 스트리밍 실행
yield AgentEvent(...) # 이벤트를 흘려보냄
async def run(self, ctx): ... # 래퍼(에러·로깅)
async def run_stream(self, ctx): ...
공항 출국 절차를 생각해보자. 체크인 → 수하물 → 보안검색 → 출국심사 → 게이트. 각 단계는 독립된 창구이고, 앞 단계의 결과(탑승권, 수하물 태그)를 뒤 단계가 이어받는다. 어느 창구를 개선하고 싶으면 그 창구만 고치면 되고, 새 검사 절차를 넣고 싶으면 사이에 창구를 하나 끼워 넣으면 된다.
Upsonic의 Step이 정확히 이 구조다. 그리고 각 창구마다 "일반 줄"과 "실시간 중계 줄"이 따로 있는 게 execute / execute_stream 쌍이다.
순서 중에 특히 의미 있는 배치가 있다. 10번 UserPolicyStep이 11번 UserInputBuildStep보다 앞이다. 즉 사용자 입력을 프롬프트로 조립하기 전에 안전 검사를 먼저 통과시킨다. 반대로 AgentPolicyStep은 19번 — 모델이 답을 만든 뒤에 출력을 검사한다. 입구와 출구 양쪽에 검문소가 있는 셈이다.
Step은 "언제"를 정하고, 실제 "어떻게"는 agent/context_managers/의 매니저 객체들이 맡는다.
| 매니저 | 맡은 일 |
|---|---|
LLMManager | 모델 문자열("anthropic/claude-sonnet-4-5") → 실제 Model 인스턴스 해석 |
SystemPromptManager | 최종 시스템 프롬프트 문자열 조립(역할·스킬·문화 등을 합침) |
ContextManager | 태스크별 동적 컨텍스트(지식베이스·RAG 결과)를 프롬프트에 삽입 |
MemoryManager | 실행 전 메모리 주입, 실행 후 세션 저장 |
CallManager | 모델 호출 자체를 감싸며 타이밍·툴 호출 표시 처리 |
ReliabilityManager | reliability_layer 연동 — 출력에서 내부 태그(<Context>, <Knowledge Base> 등)를 정규식으로 제거 |
context_management_middleware | 컨텍스트 윈도우 초과 시 대화를 요약해 압축 |
LLM의 긴 컨텍스트는 비싸고 부정확해진다. Ralph는 에이전트의 기억을 매번 지우고, 진실을 파일시스템에 둔다. 그리고 완료 여부를 LLM의 자기신고가 아니라 파일 diff로 판정한다. "AI가 다 했다고 하는데 실제로는 안 했다"는 문제를 구조적으로 회피하는 방식이다.
파일 툴킷의 모든 함수(read_file, write_file, edit_file, list_dir …)는 경로를 쓰기 전에 반드시 이 메서드를 거친다.
# agent/autonomous_agent/filesystem_toolkit.py:64-87
def _validate_path(self, path: str) -> Path:
if path.startswith("/"):
resolved = Path(path).resolve()
else:
resolved = (self.workspace / path).resolve() # ← .. 등을 정규화
try:
resolved.relative_to(self.workspace) # ← 하위인지 검사
except ValueError:
raise ValueError(f"Path '{path}' is outside workspace")
return resolved
../../etc/passwd 같은 경로는 "3층 복도 끝에서 계단으로 내려가 1층 금고실"이라고 적은 주소와 같다. .resolve()는 이 길찾기를 끝까지 따라가서 최종 도착지의 절대주소를 구하는 것이고, relative_to()는 "그 주소가 우리 층 안인가?"를 묻는 것이다. 우회 경로를 아무리 꼬아도 도착지만 보면 되니 뚫기 어렵다.
반면 셸 쪽은 대조적으로 허술하다. 같은 문서 안에서 이 격차를 인식하는 게 중요하다.
# agent/autonomous_agent/shell_toolkit.py:74-102 — 차단 목록 전체
self.blocked_commands: List[str] = blocked_commands or [
"rm -rf /", "rm -rf /*", ":(){:|:&};:", "mkfs", "dd if=/dev/zero",
]
# _validate_command()는 이 5개가 문자열에 포함되는지만 확인한다.
# curl, cat /etc/passwd, ssh … 전부 통과한다.
파이썬에서 a | b는 a.__or__(b)를 호출한다. LCEL류 문법의 비밀은 이 매직 메서드 하나다.
# uel/runnable.py
class Runnable(ABC, Generic[Input, Output]):
def __or__(self, other):
other_runnable = coerce_to_runnable(other) # 함수·dict도 러너블로 변환
if isinstance(self, RunnableSequence):
# 이미 시퀀스면 스텝을 뒤에 붙인다 (중첩 방지)
return RunnableSequence(steps=self.steps + [other_runnable])
return RunnableSequence(steps=[self, other_runnable])
그래서 이렇게 쓴다.
from upsonic.uel import ChatPromptTemplate, StrOutputParser, RunnableParallel
prompt = ChatPromptTemplate.from_template("{topic}에 대해 설명해줘")
chain = prompt | model | StrOutputParser() # 순차 연결
chain.invoke({"topic": "트랜스포머"})
# 병렬 — dict 리터럴이 자동으로 RunnableParallel이 된다
both = {"요약": summarize_chain, "번역": translate_chain} | merge_chain
__or__, __add__처럼 앞뒤로 밑줄 두 개가 붙은 특수 메서드. 파이썬 연산자(|, +)를 내 클래스에서 쓸 수 있게 해준다. LCEL, pandas, pathlib(Path / "sub")의 우아한 문법이 전부 여기서 나온다.storage/memory/memory.py의 Memory 클래스가 흥미로운 건, "저장할 것"과 "다음에 불러올 것"을 독립적으로 켜고 끌 수 있다는 점이다.
| 저장 플래그 | 주입 플래그 | 무엇 |
|---|---|---|
full_session_memory | load_full_session_memory | 대화 전문 |
summary_memory | load_summary_memory | 대화 요약 |
user_analysis_memory | load_user_analysis_memory | 유저 프로필·특성 |
회의록을 전부 기록해두되(감사·복기용), 다음 회의 시작할 때는 요약본과 참석자 프로필만 나눠주는 것과 같다. 기록은 다 남기니 나중에 찾아볼 수 있고, 매 회의 프롬프트는 짧으니 토큰이 절약된다. 이 조합이 full_session_memory=True + load_full_session_memory=False + load_summary_memory=True다.
team/team.py의 Team은 세 가지 모드를 갖는다.
coordinate 모드의 핵심은 DelegationManager.get_delegation_tool()이다. 리더에게 delegate_task(member_id, description, tools, context, expected_output)라는 툴을 동적으로 만들어 쥐어준다. 리더 입장에서 위임은 그냥 함수 호출 하나다.
작은 최적화 하나가 눈에 띈다 — ResultCombiner.should_combine_results()는 len(results) > 1일 때만 병합용 LLM을 호출한다. 결과가 하나면 그대로 반환한다. 불필요한 LLM 호출 = 불필요한 비용이라는 감각이 코드에 박혀 있다.
16개 정책(PII, 금융, 의료, 법률, 피싱, 사기, 내부자 위협, 암호화폐, 사이버보안, 욕설, 성인, 전화번호, 민감 사회 이슈, 기술, 스킬 인젝션, 툴 안전성)이 전부 이 골격을 공유한다. 새 정책을 추가하려면 Rule 하나와 Action 하나만 쓰면 된다는 게 이 설계의 이득이다.
# graphv2/state_graph.py:227-235
def compile(
self, *,
checkpointer: Optional[BaseCheckpointer] = None, # MemorySaver / SqliteCheckpointer
store: Optional[BaseStore] = None, # 스레드 간 공유 저장소
cache: Optional[BaseCache] = None, # 노드 단위 결과 캐시
interrupt_before: Optional[List[str]] = None, # HITL: 실행 전 멈춤
interrupt_after: Optional[List[str]] = None,
durability: Literal["exit", "async", "sync"] = "async",
) -> "CompiledStateGraph":
LangGraph를 써본 사람이라면 시그니처가 거의 그대로임을 즉시 알아볼 것이다. Command, Send, InterruptException, GraphRecursionError까지 이름이 일치한다.
MemorySaver(메모리)와 SqliteCheckpointer(파일) 두 구현을 제공한다.from upsonic.ocr import OCR
from upsonic.ocr.layer_1.engines import EasyOCREngine
engine = EasyOCREngine(languages=["en"])
ocr = OCR(layer_1_ocr_engine=engine) # ← 엔진을 주입
text = ocr.get_text("invoice.pdf")
엔진을 생성자 인자로 주입하는 게 핵심이다. Tesseract로 시작했다가 정확도가 아쉬우면 engine= 한 줄만 바꿔 DeepSeek 비전 모델로 갈아탈 수 있다. 이게 의존성 주입(DI)의 실용적 이득이다.
저장소에 examples/가 있지만 실제 파일이 없다. 실행 가능한 예제를 찾는다면 notebooks/(2개)와 tests/doc_examples/(136개)를 보는 게 훨씬 빠르다. 후자는 CI가 통과시키는 코드라 확실히 돌아간다.
| 순서 | 파일 | 왜 |
|---|---|---|
| 1 | uel/runnable.py | 200줄 남짓. __or__ 하나로 파이프 문법이 완성되는 걸 눈으로 확인. 가장 짧고 가장 얻는 게 많다. |
| 2 | agent/pipeline/steps.py | 27개 Step의 이름만 훑어도 "에이전트 실행에 무엇이 필요한지"의 체크리스트가 된다. |
| 3 | agent/autonomous_agent/filesystem_toolkit.py | 샌드박싱 구현. _validate_path 20줄이 핵심. |
| 4 | ralph/phases/incremental.py | 자율 루프의 완료 판정 로직. 설계 철학이 가장 잘 드러난다. |
| 5 | safety_engine/policies/pii_policies.py | Rule/Action 패턴의 구체 사례 하나. |
| 6 | graphv2/state_graph.py | LangGraph를 이미 안다면 비교하며 읽기. 모른다면 여기서 처음 배워도 좋다. |
| — | agent/agent.py | 5,424줄. 처음에 열지 말 것. 위를 다 읽은 뒤 특정 기능을 추적할 때만 검색해서 들어가라. |
흥미로운 폴더가 하나 있다. documents/ai/guides/에는 feature 추가, refactor, bug-fix, testing, coding-standards, serena, memory, subagents, 새 프리빌트 에이전트 추가, commit — 총 9~10개의 AI 에이전트용 작업 가이드가 들어 있다.
그리고 documents/ai/explanation/<서브시스템>/<서브시스템>.md를 "행위 계약(behavior contract)의 권위 있는 문서"로 취급하며, 코드를 바꾸면 이 문서도 함께 갱신하도록 프로세스에 못 박아뒀다.
"코드가 문서다"는 사람에게는 통했지만, AI 에이전트에게는 코드 전체를 읽히는 게 비싸다. Upsonic은 서브시스템별 계약 문서를 따로 두고 그것만 읽히는 방식을 택했다. 자기 프로젝트에 CLAUDE.md 하나만 두는 것에서 한 걸음 더 나간 형태로, 참고할 만하다.
uel/runnable.py의 __or__/__ror__가 어떻게 a | b | c를 하나의 RunnableSequence로 평탄화하는지 보라. __ror__가 있어야 {"a": chain} | model처럼 왼쪽이 내 클래스가 아닐 때도 동작한다.
실습 아이디어 — 나만의 미니 파이프라인 클래스를 40줄로 구현해보기. coerce_to_runnable처럼 일반 함수를 자동 래핑하는 것까지 해보면 감이 온다.
resolve() → relative_to() 2단 조합은 문자열 필터링보다 훨씬 안전하다. "../"를 문자열로 걸러내는 방식은 %2e%2e%2f, 심볼릭 링크, ....// 등으로 우회당한다. 최종 경로를 실제로 계산한 뒤 검사하는 게 정답이다.
실습 아이디어 — _validate_path를 복사해 놓고, 우회를 시도하는 테스트 케이스 10개를 작성해 전부 막히는지 확인. 이어서 심볼릭 링크를 workspace 안에 만들어 밖을 가리키게 해보라(resolve()가 심링크를 따라가므로 이것도 막힌다).
Ralph의 완료 판정이 대표 사례다. 에이전트가 "완료했습니다"라고 말해도 fix_plan.md의 체크박스가 안 바뀌었으면 실패로 기록한다. 에이전트를 만들 때 가장 중요한 질문은 "어떻게 시킬까"가 아니라 "했는지 어떻게 확인할까"다.
실습 아이디어 — 내가 만든 에이전트의 모든 태스크에 기계적으로 검증 가능한 완료 조건(파일 존재, 테스트 통과, 종료 코드 0)을 붙여보기.
27개 Step + 순서 정의라는 구조는 순서를 데이터로 만든다. 실행 순서가 코드 흐름이 아니라 리스트에 담기니, 순서를 바꾸거나 단계를 끼워 넣는 게 리스트 편집이 된다.
실습 아이디어 — 내 프로젝트의 긴 함수 하나를 골라 Step 클래스 5~6개로 분해하고, 실행 순서를 리스트로 뽑아내 보기. 그 뒤 "캐시 검사 단계"를 새로 끼워 넣어보라.
단순 마스킹(***)은 안전하지만 문맥을 파괴한다. 형식을 보존하는 치환(format-preserving)은 문맥을 살리면서 원본을 감춘다. 그리고 변환 맵을 들고 있다가 되돌리는 것이 핵심이다.
실습 아이디어 — 이메일·전화번호를 형식 보존 방식으로 치환하고 되돌리는 클래스를 만들어보기. 같은 원본이 여러 번 나오면 같은 가짜 값으로 매핑되어야 한다는 점(일관성)이 함정이다.
벡터DB 7종·스토리지 5종·OCR 6종을 지원하면서 기본 설치는 22개 패키지. 게다가 upsonic[storage]가 upsonic[sqlite-storage] 등을 다시 참조하는 중첩 extras 기법도 배울 수 있다.
실습 아이디어 — 내 라이브러리에 [dev], [all] extras를 만들고, [all]이 개별 extras를 재참조하도록 구성해보기.
tests/doc_examples/ 136개 파일. 문서에 적힌 코드가 곧 테스트다. 문서가 썩는 걸 구조적으로 막는다.
실습 아이디어 — README의 코드 블록을 추출해 실행하는 pytest 픽스처 작성. pytest --doctest-glob이나 mktestdocs 같은 도구부터 살펴보라.
| 항목 | 요구사항 |
|---|---|
| Python | ≥ 3.10 (classifiers는 3.10/3.11/3.12 명시. 일부 extras는 python_version < '3.14' 조건부) |
| 설치 | uv pip install upsonic 권장 (pip도 가능) |
| GPU | 불필요 — API 기반 프레임워크다. 단, 로컬 OCR(PaddleOCR·EasyOCR)이나 vllm/ollama로 로컬 LLM을 돌리면 그쪽 요구사항을 따른다. |
| 메모리 | 프레임워크 자체는 가볍다. FAISS·Chroma로 대량 임베딩을 인메모리에 올리면 그때부터 RAM이 필요. |
| API 키 | 최소 1개 필요(ANTHROPIC_API_KEY, OPENAI_API_KEY 등). python-dotenv가 기본 의존성이므로 .env 파일로 관리 가능. |
| DB | 선택. 기본은 in-memory / JSON 파일. 세션을 영속하려면 SQLite(파일 하나) → Redis/Postgres/Mongo 순으로 확장. |
| 네트워크 | MCP stdio 서버는 로컬 프로세스를 실행한다. SSE/streamable-HTTP는 아웃바운드 연결 필요. |
셸 차단 목록이 5개뿐이라는 걸 다시 상기하자. AutonomousAgent를 외부 사용자가 프롬프트를 직접 넣을 수 있는 자리(공개 Slack 채널, 웹 폼)에 붙이면, 프롬프트 인젝션으로 임의 명령을 실행당할 수 있다. 실전 배포라면 최소한:
① blocked_commands를 직접 크게 확장하거나, ② 컨테이너/VM 안에서만 실행하거나, ③ E2BTools 같은 격리 샌드박스를 명시적으로 붙일 것.
코드 자체가 최초 1회 경고를 띄운다:
# tools/mcp.py:66-76
console.print(
"[yellow]⚠️ MCP Security: Only connect to MCP servers you trust. "
"Stdio servers run arbitrary processes on your machine; "
"do not use commands or config from untrusted sources.[/yellow]"
)
출처가 불분명한 MCP 설정을 복사해 붙이는 건 정체불명의 실행 파일을 받아 실행하는 것과 동등하다.
v0.77.0 릴리스 노트에 "Sentry를 strict opt-in으로 변경"이 명시돼 있다. 그 이전 버전을 쓴다면 에러 리포팅이 기본 활성일 수 있으니, 사내 환경이라면 버전과 설정을 확인하자.
uel/runnable.py를 열고 __or__에 print()를 하나 심는다. 그 다음 chain = prompt | model | parser를 실행해보라.
확인할 것 — __or__가 몇 번 호출되는가? 결과로 만들어진 RunnableSequence.steps에 몇 개가 들어있는가? 중첩 시퀀스가 아니라 평탄한 3개짜리 리스트가 나오는 이유를 코드에서 찾아라.
추가 — chain = model | prompt처럼 순서를 뒤집으면 어디서 터지는가? 타입 검사가 있는가, 아니면 런타임까지 가는가?
빈 폴더를 workspace로 주고 AutonomousAgent를 띄운 뒤, 아래를 각각 시켜보고 막히는 것과 통과하는 것을 표로 정리하라.
배울 것 — F가 특히 중요하다. 차단이 문자열 부분 일치로 구현돼 있으므로 공백 하나만 더 넣어도 패턴이 안 맞는다. "블랙리스트 방식 보안"의 근본적 한계를 몸으로 이해하게 된다.
다음 단계 — blocked_commands를 화이트리스트 방식(허용 명령만 실행)으로 바꾸는 패치를 직접 작성해보라.
safety_engine/policies/의 기존 정책 하나(예: pii_policies.py)를 템플릿 삼아 "사내 프로젝트 코드명 유출 방지" 정책을 만든다.
검증 — 코드명이 든 문장을 태스크로 던졌을 때 (1) LLM에 실제로 전송된 프롬프트에는 가짜 값이 들어가고 (2) 최종 응답에는 진짜 값이 복원돼 있는지 확인하라. Anonymizer.transformation_map을 출력해보면 명확해진다.
심화 — LLM 판정 버전(_LLM_Finder)도 추가하고, confidence 임계값 0.7을 0.5/0.9로 바꿔가며 오탐·미탐이 어떻게 변하는지 기록하라.
빈 저장소를 만들고 Ralph에게 목표를 준다. 예: "FastAPI로 TODO CRUD API를 만들고, pytest 테스트를 전부 통과시켜라."
진짜 배울 것 — 성공 사례보다 실패 패턴이 값지다. 에이전트가 같은 항목을 반복해서 붙들고 있거나, 체크박스만 체크하고 실제 구현은 안 하는 경우를 잡아내라. 그리고 그걸 막으려면 백프레셔에 무엇을 더 넣어야 할지 설계해보라.
이 저장소를 공부 자료로 쓰는 가장 값진 방법이다. 같은 문제를 원본과 재구현본 양쪽으로 풀어보고 차이를 문서화한다.
| 주제 | 원본 | 재구현본 | 비교 축 |
|---|---|---|---|
| 체인 | LangChain LCEL | upsonic.uel | 스트리밍 지원 범위, 배치 처리, 에러 전파, 타입 힌트 정확도 |
| 그래프 | LangGraph | upsonic.graphv2 | 체크포인트 저장 형식, 병렬 노드(Send) 동작, 재귀 제한 처리 |
| 스킬 | Anthropic Agent Skills | upsonic.skills | frontmatter 필드 커버리지, 로더 소스, 버전 충돌 해결 |
결과물 — "어느 게 더 낫다"가 아니라 "원본이 왜 그 복잡도를 감수했는지"를 설명하는 글. 재구현본이 단순한 자리는 대개 원본이 실전에서 만난 엣지 케이스를 아직 안 만난 자리다. 이걸 찾아내는 게 프레임워크 설계 감각을 기르는 가장 빠른 길이다.
| 주차 | 주제 | 읽을 것 | 만들 것 |
|---|---|---|---|
| 1주 | 파이썬 고급 문법 매직 메서드 · 제네릭 · ABC |
uel/ 전체(11파일), agent/pipeline/step.py |
40줄짜리 파이프라인 DSL. |, invoke, ainvoke 지원까지. |
| 2주 | Pydantic 심화 구조화 출력 · 검증기 |
tools/config.py(ToolConfig), safety_engine/llm/upsonic_llm.py의 응답 모델들 |
LLM에 JSON 스키마를 강제해 파싱 실패율 0으로 만드는 래퍼. model_validator로 상호배타 필드 검증까지. |
| 3주 | 비동기 파이썬 async/await · 취소 · 타임아웃 |
agent/pipeline/의 execute_stream 계열, shell_toolkit.py의 timeout 주석 |
툴 호출에 타임아웃을 걸되 프로세스 누수 없이 취소되는 실행기. (Upsonic이 timeout=None으로 뺀 이유를 직접 겪어볼 것) |
| 4주 | 에이전트 오케스트레이션 상태 그래프 · 체크포인트 · HITL |
graphv2/ 전체(8파일) → 이어서 LangGraph 공식 문서 |
승인 단계가 있는 워크플로. interrupt_before로 멈추고, 사람이 승인하면 체크포인트에서 재개. |
| 5주 | LLM 보안 프롬프트 인젝션 · 샌드박싱 · PII |
safety_engine/, autonomous_agent/*_toolkit.py, tools/mcp.py의 경고 |
레드팀 리포트 — 실습 2의 결과를 정리하고, 화이트리스트 셸 패치를 PR 형태로 작성. |
| 6주 | RAG 스택 로더 · 청킹 · 임베딩 · 벡터DB |
loaders/(16) · text_splitter/(11) · embeddings/(10) · vectordb/(13) |
같은 문서를 청킹 전략 3가지로 나눠 검색 품질 비교. 벡터DB를 Chroma→Qdrant로 교체해 추상화가 실제로 새는지 확인. |
이 표를 순서대로 다 하려 들지 말 것. 지금 필요한 한 줄을 고르는 용도다. 에이전트를 이미 만들고 있다면 4~5주차가, 파이썬 실력을 올리고 싶다면 1~3주차가 우선이다. "만들 것" 칸이 비어 있는 주는 그 주를 건너뛰어도 좋다는 뜻으로 읽어도 된다 — 읽기만 한 주는 대개 한 달 뒤 아무것도 남지 않는다.
| 용어 | 뜻 |
|---|---|
| Agent | Upsonic의 전통적 에이전트. 24단계 파이프라인을 통과해 LLM을 호출하고 툴을 쓴다. |
| AutonomousAgent | Agent + 파일시스템/셸 툴킷. workspace 폴더 안에서 자율적으로 돌아다닌다. |
| Task | 에이전트에게 시키는 일 한 건을 담는 객체(설명·툴·출력형식·첨부). |
| Step | 파이프라인의 한 단계. execute(일반)와 execute_stream(스트리밍) 쌍을 갖는다. 총 27종. |
| context manager | Step이 실제 작업을 위임하는 협력 객체(LLMManager, MemoryManager 등). 파이썬의 with 문 컨텍스트 매니저와는 다른 의미이니 주의. |
| UEL | Upsonic Expression Language. LangChain LCEL을 재구현한 파이프 체인 시스템. |
| Runnable | UEL의 기본 단위. __or__를 구현해 |로 연결된다. |
| RunnableSequence / Parallel / Branch | 각각 순차 실행 · 동시 실행(dict로 수집) · 조건 분기. |
| graphv2 / StateGraph | LangGraph 스타일 상태 그래프 엔진. 노드·엣지·조건부 엣지로 워크플로를 정의한다. |
| checkpointer | 그래프 실행 중간 상태를 저장하는 장치. MemorySaver(메모리), SqliteCheckpointer(파일). |
| Command / Send | 그래프 노드가 다음 흐름을 직접 지시하는 원시타입. Send는 동일 노드를 여러 입력으로 병렬 실행. |
| HITL | Human-in-the-loop. 사람이 중간에 확인·승인하는 지점. interrupt_before/after, 툴의 requires_confirmation으로 구현. |
| Ralph / Groundhog 루프 | 매 반복마다 새 에이전트를 만들고, 상태는 파일(fix_plan.md)에만 두는 자율 개발 패턴. |
| backpressure | 빌드·테스트·린트를 실제 실행해 통과해야 진행시키는 제동장치. |
| fix_plan.md | Ralph의 체크리스트 파일. - [ ] → - [x] 변화가 곧 진척의 증거이자 완료 판정 기준. |
| safety_engine | 16개 영역의 입출력 안전 정책 엔진. Rule(탐지) + Action(대응) 구조. |
| 가역적 익명화 | 민감정보를 형식 보존 가짜 값으로 바꿔 LLM에 보내고, 응답에서 원본으로 되돌리는 기법. |
| reliability_layer | 모델 출력에서 <Context> 같은 내부 태그를 제거해 응답을 정제하는 계층. |
| reflection | 에이전트가 자기 출력을 평가 기준으로 채점하고 개선 여부를 결정하는 자기평가 루프. |
| culture | 어투·금기 주제·도움 주제 같은 행동 가이드라인을 시스템 프롬프트에 주입하는 모듈. 주기적 재주입 가능. |
| Team (sequential/coordinate/route) | 멀티에이전트 협업 3모드. coordinate에서는 리더만 메모리를 갖는다. |
| delegate_task | coordinate 모드에서 리더에게 동적으로 쥐어주는 위임용 툴. |
| MCP | Model Context Protocol. 외부 데이터·서비스를 툴로 붙이는 표준. stdio/SSE/streamable-HTTP 3가지 전송. |
| @tool | 일반 함수를 에이전트 툴로 만드는 데코레이터. docstring을 파싱해 스키마를 자동 생성(griffe). |
| ToolConfig | 툴의 동작 설정. requires_confirmation/requires_user_input/external_execution 3개는 상호배타. |
| Agent Skills | SKILL.md + references/ + scripts/ 구조의 스킬 규격. Upsonic이 skills/에 구현. |
| optional extras | pip install upsonic[ocr]처럼 선택 설치하는 의존성 묶음. |
| path traversal | ../로 허용 범위 밖 파일에 접근하는 공격. resolve()+relative_to()로 방어. |
| OCR Layer 0 / Layer 1 | Layer 0은 문서→이미지 변환, Layer 1은 이미지→텍스트 인식(엔진 주입식). |
| usage_registry | v0.77.3에서 중앙집중화된 토큰·비용 집계 모듈. agent.cost의 근거. |
uel/과 비교graphv2/와 비교skills/의 스펙 원본src/upsonic/uel/runnable.py — 파이프 연산자의 전부src/upsonic/agent/pipeline/steps.py — 27개 Step 목록src/upsonic/agent/autonomous_agent/filesystem_toolkit.py — 샌드박싱src/upsonic/ralph/phases/incremental.py — 자율 루프 판정 로직src/upsonic/safety_engine/anonymization.py — 가역 익명화documents/ai/guides/ — AI 에이전트용 작업 가이드 9종tests/doc_examples/ — 실제로 돌아가는 예제 136개