5분 AI.
TrendShift 딥다이브 · 2026-08-24 · Daily #12

Sprix SAGE Router (wang2122/sprix-sage-router) 딥다이브
— "혼자 갈까, 팀을 부를까, 넘길까"를 수식 하나로 판정하는 에이전트 라우터

sprix-sage-router는 여러 AI 에이전트가 서로 일을 주고받는 A2A 네트워크에서 "이미 일을 시작한 뒤"에 생기는 질문 하나에만 집중하는 라이브러리다 — 지금 이 작업, 담당 에이전트가 계속 혼자 할까(SELF), 부족한 부분만 채울 동료를 부를까(COLLABORATE), 아니면 통째로 전문가에게 넘길까(HANDOFF)? 에이전트 검색(discovery)이 "누가 있는가"를 답한다면, SAGE는 "이미 절반쯤 진행된 지금, 누가 무엇을 맡아야 하는가"를 답한다. 세 선택지를 각각 따로 만든 규칙(heuristic)으로 판단하지 않고 하나의 효용 함수에 넣어 정면 비교하는 게 이 프로젝트의 축이다.
(저장소: wang2122/sprix-sage-router · MIT · v0.2.0 · Python ≥3.10 · 런타임 의존성 0개 · 핵심 모듈 sprix_sage.py 749줄 · 벤치마크 345줄 · 테스트 210줄 · 총 1,848줄 · 별 약 1,009개 / 포크 12개 · 2026-08-18 생성 · TrendShift Daily #12)
목차
  1. 프로젝트 한 줄 요약
  2. 왜 지금 주목받는가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석
  5. 디렉토리 구조 해부
  6. 학습 포인트
  7. 하드웨어 · 시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한 줄 요약

"진행 중인 작업을 SELF · COLLABORATE · HANDOFF 세 갈래로 비교해, 권한·예산·마감을 어긴 후보는 아예 제외하고 남은 것 중 최대 효용을 고르는 749줄짜리 라우터"

SAGEState-Aware Graph Exchange의 약자다. 이름의 "State-Aware(상태를 아는)"가 핵심이다. 보통의 에이전트 매칭은 작업이 시작되기 전에 "이 일에 누가 제일 잘 맞나"를 고른다. SAGE는 이미 45% 진행됐고, 계획 단계는 끝났고, 한 번 실패한 적 있는 상태에서 다시 판단한다.

이 차이가 왜 중요하냐면, 진행률이 높을수록 담당자를 바꾸는 비용이 커지기 때문이다. 이미 쌓인 맥락(대화 기록·중간 산출물·시행착오)을 새 에이전트에게 완전히 옮길 수 없다면, 아무리 실력이 좋은 전문가라도 넘기는 게 손해일 수 있다. SAGE는 이 손실을 handoff_loss·switch_loss라는 이름의 페널티 항으로 수식에 직접 넣는다.

용어
A2A (Agent2Agent 프로토콜)
서로 다른 회사·프레임워크가 만든 AI 에이전트들이 표준 형식으로 대화하게 하는 규약. 에이전트가 자기 능력을 적어 놓은 명함(AgentCard), 작업 상태, 산출물, 인증, 전송 방식을 정의한다. 다만 "누구에게 맡길지"는 정하지 않는다 — A2A는 전화망이고, SAGE는 그 위에서 "누구한테 전화할지" 정하는 쪽이다.
한눈에 이해하는 비유

"이사 절반쯤 하다가, 이삿짐센터를 바꿀지 결정하는 문제"

혼자 이사를 하다가 짐의 45%를 옮긴 시점이다. 세 가지 선택지가 있다. ① 그냥 끝까지 혼자 한다. ② 무거운 피아노만 전문 업체를 불러 같이 한다. ③ 남은 전부를 이삿짐센터에 통째로 넘긴다.

③이 매력적으로 보여도, 지금까지 "어느 상자에 뭐가 들었는지" 나만 아는 정보가 쌓여 있다. 그걸 설명하는 데 드는 시간과 오해가 바로 맥락 전달 손실이다. 게다가 예산과 이사 마감 시각이라는 절대 어길 수 없는 선이 있다 — 아무리 좋은 업체라도 예산을 넘기면 후보에서 빠진다. SAGE가 하는 계산이 정확히 이것이다.

실제 코드는 놀랄 만큼 짧다. 에이전트 목록과 작업 요구사항을 넘기면 결정 객체 하나가 돌아온다.

from sprix_sage import Agent, ExecutionOutcome, Requirement, SAGERouter, Task

agents = [
    Agent("planner", {"planning": 0.92, "coding": 0.55}, cost=0.08, latency_ms=900),
    Agent("coder",   {"planning": 0.35, "coding": 0.96}, cost=0.12, latency_ms=1200),
]

task = Task(
    "build-feature",
    requirements=(
        Requirement("planning", 0.4),
        Requirement("coding", 0.6, depends_on=("planning",)),   # 의존 관계 = DAG
    ),
    value=1.0, budget=0.30, deadline_ms=4000,
    progress=0.35,        # ← 이미 35% 진행된 상태에서 판단
)

router = SAGERouter(agents, incumbent_id="planner")
decision = router.route(task)
print(decision.mode, decision.assignments, decision.topology)

저장소의 demo.py를 실제로 돌려 보면 다음이 나온다(직접 실행해 확인한 출력이다).

mode       : collaborate
agents     : incumbent-planner, security-reviewer
utility    : 0.616
p(success) : 0.814
cost       : 0.176
latency    : 1122 ms
assignments: {'planning': 'incumbent-planner', 'coding': 'security-reviewer',
              'security': 'security-reviewer'}
topology   : (('incumbent-planner', 'security-reviewer'),)
reason     : COLLABORATE via [incumbent-planner, security-reviewer]: ...
             estimated success=0.814, coverage=0.756; ...

주목할 부분은 reason 필드다. 이 라이브러리는 결정만 뱉지 않고 왜 그렇게 골랐는지를 사람이 읽을 문장으로 함께 돌려준다. 그리고 coding 요구사항이 code-specialist가 아니라 security-reviewer에게 갔다는 점도 흥미롭다 — 코딩 실력만 보면 전자가 0.97로 압도적이지만, 예산 0.30 안에서 보안까지 함께 커버하는 팀을 만들려면 후자를 넣는 편이 전체 효용이 높다고 판단한 것이다.

2왜 지금 주목받는가

"멀티에이전트가 '되긴 되는데 왜 이렇게 비싸지?' 단계에 들어선 시점 · 논문 아이디어를 749줄로 압축 · 자기 벤치마크를 자기가 못 믿게 설계"

2026년의 멀티에이전트 도구는 대부분 연결에 집중해 있다. MCP는 도구를 붙이고, A2A는 에이전트끼리 말을 트게 하고, 오케스트레이터는 순서를 짠다. 그런데 실제로 굴려 보면 다음 질문이 남는다 — "에이전트를 더 붙이면 더 좋아지긴 하는데, 왜 비용이 세 배가 되지?"

① 남들이 규칙으로 때우던 자리에 하나의 목적함수를 놓았다

보통의 프레임워크에서 "혼자 할지 / 넘길지"는 프롬프트나 if 문으로 흩어져 있다. SAGE는 세 모드를 같은 저울에 올린다. 담당자가 그대로 하는 것도 하나의 후보이고, 팀을 꾸리는 것도, 넘기는 것도 후보다. 전부 같은 식으로 점수를 매겨 최대값을 고른다.

U = V·p̂ − λc·비용 − λl·지연 − λr·위험 − λh·인수인계손실 − λo·조율비용 − λu·불확실성 + β·탐색보너스 기본 가중치: 비용 0.18 · 지연 0.10 · 위험 0.12 · 인수인계 0.22 · 조율 0.08 · 불확실성 0.05 (RouterWeights)

가중치를 보면 설계자의 관점이 드러난다. 인수인계 손실(0.22)이 비용(0.18)보다 무겁다. "담당자를 바꾸는 건 돈보다 비싸다"는 판단이 숫자에 박혀 있는 셈이다.

② "잘하는 사람"이 아니라 "빈칸을 채우는 사람"을 뽑는다

팀을 만들 때 흔한 실수가 개별 점수 높은 에이전트만 모으는 것이다. 코딩 0.97짜리 셋을 모아도 보안 요구사항은 여전히 빈칸이다. SAGE는 중복도(redundancy)를 코사인 유사도로 계산해 감점하고, 요구사항별 커버리지를 따로 본다. 게다가 bottleneck(가장 약한 요구사항의 충족률)을 별도 특징으로 두어, 평균은 좋은데 한 항목이 치명적으로 비어 있는 팀을 걸러낸다.

③ 신뢰도를 "한 개의 별점"으로 두지 않는다

대부분의 에이전트 마켓 구상은 평판을 숫자 하나로 관리한다. SAGE는 전역 신뢰도요구사항별 신뢰도를 따로 학습하고 35:65로 섞는다. 코딩을 열 번 잘했다고 보안까지 잘한다고 보지 않는다는 뜻이다. 테스트 파일에도 test_contextual_reliability_does_not_bleed_across_skills라는 이름으로 이 성질이 못 박혀 있다.

④ 자기 벤치마크를 자기가 채점하지 않는다

이게 이 저장소에서 가장 정직한 부분이다. benchmark.py는 SAGE의 예측 모델과 일부러 다르게 만든 외부 시뮬레이터를 쓴다. 숨은 실제 능력치는 광고된 능력치와 다르고, 품질 함수는 비선형이고, 실제 비용·지연은 견적과 어긋난다. README는 이전 버전이 "SAGE의 확률로 SAGE의 정답을 만드는 순환 평가"였다고 스스로 밝힌다.

직접 돌려 본 결과(python3 benchmark.py, 2,500개 작업 × 5개 시드, 약 2.8초)는 README 표와 정확히 일치했다.

전략품질공통 효용예산 사용률마감 위반
담당자 고정0.507 ±0.0030.3890.23926.4%
광고 능력치 단독0.558 ±0.0050.4350.29211.9%
단독 오라클(숨은 정보 사용)0.553 ±0.0050.4400.2710.0%
정적 SAGE(학습 없음)0.591 ±0.0070.4670.3290.0%
온라인 SAGE(학습 포함)0.634 ±0.0060.4870.4340.2%

여기서 읽어야 할 것은 "SAGE가 이겼다"가 아니라 마감 위반이 26.4% → 0%로 떨어진 부분이다. 품질 차이(0.507→0.634)는 시뮬레이터 설계에 크게 좌우되지만, 마감·예산을 구조적으로 못 어기게 만든 건 점수가 아니라 설계의 결과다. 반대로 온라인 SAGE는 예산을 0.329→0.434로 더 쓴다 — 저자는 이 트레이드오프를 숨기지 않고 표에 남겨 뒀다.

비교 대상들과의 위치

도구맡는 일SAGE와의 차이
A2A 프로토콜에이전트 명함·메시지·작업 전송"누가 있는가"까지. SAGE는 그 위에서 "누가 맡을까"를 정하는 정책 층
RouteLLM질문마다 싼 모델/비싼 모델 선택모델 1개 고르기. SAGE는 팀·역할·통신 구조까지 고름
GPTSwarm / AFlow에이전트 그래프·워크플로 자체를 최적화사전 설계 중심. SAGE는 실행 중간 재판단이 주무대
LangGraph · CrewAI워크플로를 코드로 짜서 실행사람이 라우팅을 직접 씀. SAGE는 그 결정을 학습형 함수로 대체
sprix-sage-router진행 중 SELF/COLLABORATE/HANDOFF 판정 + DAG 역할 배정실행은 하지 않음 — 결정만 돌려준다
먼저 알고 볼 것
이건 프로덕션 라이브러리가 아니라 "연구 프리뷰"다

README가 스스로 early-stage research preview라고 못 박는다. 실제 A2A 어댑터(검색·실행·스트리밍·취소)는 로드맵에서 아직 미체크 상태고, 라우터는 결정만 돌려줄 뿐 작업을 전송하지 않는다. 벤치마크도 전부 합성 데이터다.

지표도 감안해서 보자. 별 약 1,009개인데 포크는 12개, 생성 4일 만의 수치다. 이 비율은 "많이 쓰이는 라이브러리"의 모양이 아니라 화제성 위주로 별이 붙은 초기 저장소의 모양에 가깝다. TrendShift의 굵은 숫자도 별 수가 아니라 모멘텀 점수다. 쓸 물건이 아니라 읽을 물건으로 접근하는 게 맞다 — 그리고 읽을 물건으로는 밀도가 상당히 높다.

3기술 스택 전체 지도

"의존성 0개의 순수 표준 라이브러리 · dataclass로 만든 타입 안전 · 파이썬 3.10/3.11/3.12 CI"

이 저장소의 기술 스택 지도는 그리기가 민망할 만큼 단순하다. 그리고 그게 의도다.

런타임 — 서드파티 의존성 0개

pyproject.tomldependencies 항목 자체가 없다. NumPy도, scikit-learn도, PyTorch도 안 쓴다. 모듈 상단의 import는 이게 전부다.

from dataclasses import dataclass, field, replace
from enum import Enum
from itertools import combinations
import math
import random
from typing import Iterable, Mapping
표준 모듈이 프로젝트에서 하는 일
dataclassesfrozen=True 불변 객체 8종 + replace()로 결정 객체 복사 수정
enumMode(str, Enum) — 문자열이면서 열거형이라 mode.value가 그대로 "self"
itertools.combinations팀 내 모든 2인 조합 순회(시너지·중복도 계산)
mathexp/sqrt/prod/inf — 시그모이드, 노이지-OR, 무한대 기본 예산
random.Random시드 고정 톰슨 샘플링(betavariate) — 재현 가능한 탐색
typingIterable/Mapping만 — 나머지는 3.10 내장 제네릭(tuple[str, ...])
배울 점
"의존성이 없다"가 곧 설계 문서다

로지스틱 회귀를 쓰면서 scikit-learn을 안 붙였다는 건, 모델을 갈아끼울 수 있게 남겨 뒀다는 뜻이다. ALGORITHM.md도 "이 가벼운 모델은 의도적으로 교체 가능하다 — 프로덕션에서는 GBDT·인코더 모델·베이지안 신경망으로 바꾸되 제약과 탐색 층은 그대로 쓰면 된다"고 명시한다. 학습 코드가 20줄이라 바꾸기 쉽다는 것 자체가 기능인 설계다.

모델링 층 — 8개의 불변 dataclass

타입표현하는 것핵심 필드
Requirement작업의 요구 능력 1개name, weight, minimum(최소 기준), depends_on
Task작업 전체 + 제약requirements(DAG), budget, deadline_ms, progress, handoff_friction
ExecutionState지금 진행 상태(라이브)active_agents, completed_requirements, failed_agents, failure_count
Agent후보 에이전트skills(능력 벡터), cost, latency_ms, permissions, load
Bid에이전트가 부른 견적quoted_cost, promised_latency_ms, confidence
RouteDecision라우터의 답mode, agents, assignments, topology, explanation
ExecutionOutcome실행 후 관측 증거success, agent_scores, requirement_scores, actual_cost
RouterWeights효용 함수의 λ 값들cost, latency, risk, handoff, coordination

전부 frozen=True이고 __post_init__에서 값 범위를 검증한다. 예를 들어 Requirement는 이름이 비었거나 가중치가 0 이하면 즉시 ValueError를 던진다. Task는 한술 더 떠 요구사항 의존 그래프에 순환이 있으면 생성 자체가 실패한다.

학습 층 — 두 종류의 "믿음"

학습 컴포넌트방식무엇을 배우나
BetaBelief베타 분포 켤레 사전분포(α=2, β=2 시작)전역 신뢰도 · 요구사항별 신뢰도 · 짝 시너지 · 견적 정확도
OnlineSuccessModel온라인 로지스틱 회귀 + L2 정규화9개 특징 → 성공 확률

BetaBelief.update()는 딱 두 줄이다 — alpha += weight × 점수, beta += weight × (1 − 점수). 베이즈 갱신을 이보다 짧게 쓰기 어렵다. uncertainty는 베타 분포의 표준편차 공식을 그대로 쓴다.

개발·배포 인프라

영역구성
패키징setuptools>=68, py-modules = ["sprix_sage"] — 단일 모듈 배포
CIGitHub Actions, Python 3.10 / 3.11 / 3.12 매트릭스, fail-fast: false
CI 단계py_compile(문법 검사) → python -m unittest -v(12개 테스트)
테스트표준 unittest만. pytest 없음. 실행 0.003초
문서README(224줄) · ALGORITHM.md(137줄) · CONTRIBUTING · SECURITY · CODE_OF_CONDUCT · CITATION.cff
도식docs/assets/에 SVG 3장(라우팅 시스템·정책 지도·벤치마크 도트플롯)
라이선스MIT (모듈·문서 전부)

4아키텍처 심화 분석

"거르고 → 채점하고 → 스케줄 짜고 → 고르고 → 배운다: route() 한 번에 벌어지는 5단계"

전체 그림부터 보자. SAGE는 A2A 스택에서 검색과 실행 사이에 끼는 정책 층이다.

┌──────────────────────────────────────────────────────────────┐ │ A2A 검색 계층 (SAGE 밖) │ │ AgentCard 목록 · 보안 요건 · 입출력 모드 · 공급자 견적 │ └───────────────────────────┬──────────────────────────────────┘ │ Agent[] · Bid[] ▼ ┌──────────────────────────────────────────────────────────────┐ │ SAGERouter.route(task, bids, state) ◀── 이 저장소 │ │ │ │ ① 자격 필터 _eligible() ← 절대선. 점수로 못 뒤집음 │ │ · 권한 부분집합 · 가용성 > 0 · 실패 이력 제외 │ │ · 위험보정 비용 ≤ 예산 · 위험보정 지연 ≤ 마감 │ │ │ │ │ ▼ │ │ ② 세 갈래 후보 생성 │ │ SELF ──────── (담당자 1명) │ │ COLLABORATE ─ 빔 서치(폭 8, 최대 협력자 2) │ │ HANDOFF ───── 자격 통과한 모든 동료 각각 1명 │ │ │ │ │ ▼ │ │ ③ 후보마다 _evaluate() │ │ 커버리지·역할배정 ▸ 시너지/중복 ▸ DAG 스케줄 ▸ 비용 │ │ │ │ │ ▼ 9개 특징 │ │ OnlineSuccessModel.predict() ──▶ p̂ │ │ │ │ │ ▼ │ │ U = V·p̂ − 비용 − 지연 − 위험 − 인수인계 − 조율 − 불확실성 │ │ │ │ │ ▼ │ │ ④ _team_feasible() 2차 검사 ← 팀 합산 비용·임계경로 지연 │ │ │ │ │ ▼ │ │ max(효용) → RouteDecision(모드·팀·역할·통신구조·이유) │ └───────────────────────────┬──────────────────────────────────┘ │ 실행은 호출자 몫 (A2A message/send) ▼ ┌──────────────────────────────────────────────────────────────┐ │ ⑤ router.record_outcome(decision, ExecutionOutcome(...)) │ │ 증거 등급별 가중 갱신 → 신뢰도·시너지·견적정확도·성공모델 │ └──────────────────────────────────────────────────────────────┘

설계 패턴 ① 하드 제약과 소프트 점수를 절대 섞지 않는다

가장 많이 배울 만한 부분이다. 권한·예산·마감은 점수가 아니라 필터다. 아무리 예측 성공률이 높아도 권한이 없으면 순위에 들어오지도 못한다.

def _eligible(self, agent: Agent, task: Task, bid: Bid) -> bool:
    return (
        agent.availability > 0
        and task.required_permissions.issubset(agent.permissions)   # 권한: 부분집합
        and self._risk_adjusted_cost(agent.agent_id, bid) <= task.budget
        and self._risk_adjusted_latency(agent.agent_id, bid) <= task.deadline_ms
    )

그런데 여기에 함정이 하나 있다. 개별 에이전트가 예산을 통과해도 둘을 합치면 넘을 수 있다. 그래서 팀이 만들어진 뒤 한 번 더 검사한다.

@staticmethod
def _team_feasible(decision: RouteDecision, task: Task) -> bool:
    return decision.cost <= task.budget and decision.latency_ms <= task.deadline_ms

이 2차 검사는 DAG 스케줄링으로 계산한 임계 경로 지연을 쓴다. 각자 1,200ms짜리 두 명을 붙였다고 지연이 2,400ms가 되는 게 아니라, 의존 관계가 없으면 병렬로 도니까 1,200ms에 가깝다 — 그 계산을 마치고 나서 마감을 검사하는 것이다.

설계 패턴 ② 커버리지는 노이지-OR로 — "여럿이면 놓칠 확률이 준다"

Cr(S) = 1 − ∏a∈S (1 − qa,r) "팀 전원이 동시에 실패할 확률"의 여집합 — 파이썬으로는 1.0 - math.prod(1.0 - score for ...)

코딩 능력 0.9인 사람과 0.7인 사람이 함께 있으면 커버리지는 1 − (0.1 × 0.3) = 0.97이다. 단순 평균(0.8)이나 최대값(0.9)과 다르다. 다만 이 식은 두 사람의 실패가 독립이라고 가정한다 — 실제로는 같은 이유로 함께 틀릴 수 있으므로, SAGE는 그 오차를 짝 시너지(synergy)와 중복도(redundancy) 특징으로 보정한다. ALGORITHM.md도 이것들을 "주장이 아니라 특징(features rather than claims)"이라고 조심스럽게 표현한다.

설계 패턴 ③ 신뢰도를 두 겹으로 쪼갠다

ta,r = 0.35 · E[θa] + 0.65 · E[θa,r]
b̃ = t · b + (1 − t) · 0.5
qa,r = sa,r · (0.65 + 0.35·t) · (0.70 + 0.30·b̃) s = 광고된 능력치, b = 견적서에 적힌 자신감, t = 문맥 신뢰도

두 번째 줄이 특히 영리하다. 믿을 수 없는 에이전트가 "나 99% 자신 있어요"라고 해도, 그 자신감이 0.5(모름) 쪽으로 끌려간다. 신뢰(t)가 낮으면 자기 신고 값의 영향력이 자동으로 줄어드는 구조다. 세 번째 줄의 0.65 + 0.35t·0.70 + 0.30b̃도 마찬가지 — 계수 하한이 0이 아니라 0.65/0.70이라, 실적이 하나도 없는 신규 에이전트도 완전히 배제되지는 않는다(콜드 스타트 대응).

설계 패턴 ④ 팀 탐색은 빔 서치 — 욕심쟁이보다 넓게, 전수보다 싸게

가능한 팀 조합을 전부 세면 조합 폭발이다. 반대로 매번 제일 좋은 한 명씩만 붙이는 그리디는 "혼자는 별로지만 셋이 모이면 최고인 조합"을 못 찾는다. SAGE는 그 사이를 택했다.

frontier = [(incumbent,)]                     # 담당자 1명에서 출발
for _ in range(max_collaborators):            # 기본 2회
    expanded = {}
    for team in frontier:                     # 살아남은 팀 후보들
        for agent_id in pool:                 # 모든 동료를 하나씩 붙여 봄
            quoted = sum(비용 for 팀원 in 후보팀)
            if quoted > task.budget: continue # 예산 초과면 확장 자체를 포기
            expanded[frozenset(후보팀)] = self._evaluate(COLLABORATE, ...)
    ranked = sorted(expanded.values(), key=효용, reverse=True)
    frontier = [item.agents for item in ranked[:self.beam_width]]   # 상위 8개만 유지

복잡도는 동료 n명, 빔 폭 B, 협력자 상한 k, 요구사항 |R|에 대해 대략 O(B·k·n·(|R| + k²))다. ALGORITHM.md는 이게 근사라는 점을 분명히 한다 — "전체 목적함수가 열등모듈러(non-submodular)이므로 전역 최적을 주장하지 않는다."

용어
열등모듈러성(submodularity)과 그 부재
"이미 큰 팀에 한 명 더 넣었을 때의 이득이, 작은 팀에 넣었을 때보다 작다"는 성질. 이게 성립하면 그리디 알고리즘이 이론적 성능 보장을 받는다. SAGE는 인수인계 손실·조율 비용·짝 시너지 때문에 이 성질이 깨진다고 인정하고, 대신 빔 서치로 여러 갈래를 동시에 살려 둔다.

설계 패턴 ⑤ 요구사항 DAG를 실제로 스케줄링한다

_schedule()은 운영체제 수업에 나오는 리스트 스케줄링을 그대로 한다. 위상 정렬로 순서를 정하고, 각 작업의 시작 시각을 "선행 작업이 끝난 시각"과 "그 에이전트가 비는 시각" 중 늦은 쪽으로 잡는다.

for item in self._topological_requirements(task, state):
    agent_id = assignments[item.name]
    dependency_ready = max((finish[name] for name in item.depends_on), default=0.0)
    start = max(dependency_ready, agent_ready[agent_id])       # 자원 제약 + 의존 제약
    duration = 위험보정지연(agent_id) * item.weight / total_weight
    finish[item.name] = start + duration
    agent_ready[agent_id] = finish[item.name]                  # 같은 사람 일은 직렬화
    for dependency in item.depends_on:
        if assignments.get(dependency) != agent_id:
            topology.add((assignments[dependency], agent_id))  # 다른 사람이면 통신선

마지막 두 줄이 재미있다. 의존 관계가 있는 두 요구사항이 서로 다른 에이전트에게 배정되면, 그 순간 통신 간선이 하나 생긴다. 즉 팀의 통신 구조가 사람이 그린 게 아니라 역할 배정에서 자동으로 유도된다. 그리고 간선 수만큼 지연에 조율 오버헤드가 곱해진다(latency *= 1 + coordination_overhead × 간선수) — "회의가 많아지면 느려진다"를 수식으로 옮긴 셈이다.

설계 패턴 ⑥ 증거의 등급을 나눠 학습한다

실행이 끝나면 record_outcome()으로 결과를 먹인다. 여기서 SAGE는 얼마나 확실한 증거인가에 따라 갱신 가중치를 다르게 준다.

증거 종류신용 값가중치
에이전트별 명시 점수그 점수 그대로1.00
그 사람이 맡은 요구사항 점수들의 평균평균값0.85
팀 전체 성공 여부만 있을 때전체 값0.35

팀 전체 결과 하나만 있을 때 모든 팀원에게 똑같이 만점을 주는 것이 흔한 실수다. SAGE는 그걸 "약한 증거"로 낮춰 잡는다. 짝 시너지 갱신은 한 발 더 나간다.

individual_mean = (개인점수_왼쪽 + 개인점수_오른쪽) / 2
pair_credit = clip(0.5 + overall - individual_mean)   # 개인 실력으로 설명되지 않는 '잔차'
pair_weight = 0.70

둘 다 원래 잘하는 사람이면 팀 성과가 좋아도 시너지 점수는 0.5(중립) 근처에 머문다. 개인 실력으로 설명되지 않는 초과분만 궁합으로 인정하는 방식이다. 다만 ALGORITHM.md는 이것도 "아직 인과적 공로 배분은 아니다"라고 선을 긋는다.

설계 패턴 ⑦ 견적을 믿되 검증한다

에이전트가 "0.1달러, 1초"라고 견적을 냈는데 실제로 0.2달러가 나왔다면? SAGE는 그 에이전트의 견적 신뢰도를 깎고, 다음부터 그 사람의 견적을 부풀려서 자격 심사한다.

def _risk_adjusted_cost(self, agent_id, bid):
    return bid.quoted_cost * (1.0 + 0.20 * (1.0 - self.cost_fidelity[agent_id].mean))

def _risk_adjusted_latency(self, agent_id, bid):
    agent = self.agents[agent_id]
    quote = bid.promised_latency_ms * (1.0 + 0.20 * (1.0 - self.latency_fidelity[agent_id].mean))
    return quote * (1.0 + 0.50 * agent.load) / max(agent.availability, 0.10)   # 부하·가용성 반영

지연 쪽에는 현재 부하까지 곱해진다. 부하 100%인 에이전트는 견적의 1.5배로 계산되어 마감 심사를 받는다. 현실의 큐잉 지연을 아주 단순하게 근사한 것이다.

설계 패턴 ⑧ 톰슨 샘플링을 "한 번만" 뽑는다

exploration=True로 켜면 신뢰도의 평균 대신 베타 분포에서 표본을 뽑아 쓴다(탐색). 그런데 여기 미묘한 버그 지점이 있다 — 후보를 비교할 때마다 새로 뽑으면, 같은 에이전트가 후보 A에서는 운 좋게 0.9, 후보 B에서는 0.3이 되어 비교 자체가 무의미해진다. SAGE는 라우팅 이벤트 하나마다 캐시를 비우고, 같은 믿음에 대해서는 한 번 뽑은 값을 재사용한다.

def _belief_value(self, key, belief):
    if not self.exploration:
        return belief.mean
    if key not in self._draw_cache:            # 이 라우팅 이벤트에서 처음이면
        self._draw_cache[key] = belief.draw(self.rng)   # 한 번만 뽑고
    return self._draw_cache[key]               # 이후엔 재사용 (일관된 비교)

5디렉토리 구조 해부

"디렉토리가 사실상 없다 — 대신 749줄 한 파일이 어떻게 층을 나눴는지를 본다"

정직하게 말하면 이 저장소에는 해부할 디렉토리가 없다. 파일 15개, docs/assets/.github/ 말고는 전부 루트에 평평하게 놓여 있다.

sprix-sage-router/ ├── sprix_sage.py 749줄 ★ 라이브러리 전부가 여기 한 파일 ├── benchmark.py 345줄 외부 시뮬레이터 + 5개 전략 비교 ├── test_sprix_sage.py 210줄 unittest 12개 ├── demo.py 71줄 읽기 좋은 end-to-end 예제 ├── README.md 224줄 설계 개요 · 벤치마크 표 · 로드맵 ├── ALGORITHM.md 137줄 ★ 수식과 한계를 적은 진짜 설계 문서 ├── pyproject.toml 33줄 setuptools · py-modules 단일 모듈 ├── CITATION.cff 학술 인용 메타데이터 ├── CONTRIBUTING.md · SECURITY.md · CODE_OF_CONDUCT.md · LICENSE(MIT) ├── docs/assets/ SVG 3장 (시스템도 · 정책지도 · 벤치마크) └── .github/ ├── workflows/tests.yml 3.10/3.11/3.12 매트릭스 CI ├── ISSUE_TEMPLATE/ (버그·기능·config) ├── PULL_REQUEST_TEMPLATE.md └── CODEOWNERS

그래서 진짜 해부 대상은 sprix_sage.py 안의 층 구조다. 749줄이 뚜렷한 5개 층으로 나뉜다.

줄 범위내용
1–27유틸모듈 독스트링, _clip(), 오버플로 안전 _sigmoid()
30–167도메인 모델Mode · Requirement · Task(DAG 검증 포함) · ExecutionState · Agent · Bid
169–231학습 컴포넌트BetaBelief(베이즈 갱신) · OnlineSuccessModel(로지스틱)
233–280설정·출력 타입RouterWeights · RouteDecision · ExecutionOutcome
282–749라우터 본체SAGERouter — 공개 메서드 2개(route, record_outcome) + 내부 메서드 16개
읽는 순서 추천
한 파일이라 오히려 순서가 중요하다

① ALGORITHM.md 먼저(수식 → 코드 순서로 봐야 이해가 빠르다) → ② demo.py(입출력 감 잡기) → ③ sprix_sage.py의 30–280줄(자료구조만) → route() 하나(315–354줄, 40줄뿐이다) → _evaluate()(620–692줄, 효용 계산의 심장) → ⑥ test_sprix_sage.py(12개 테스트 이름이 곧 명세다).

공개 API는 딱 두 개다. route()record_outcome(). 나머지 16개 메서드는 전부 _로 시작하는 내부 함수다. 클래스 하나에 메서드 18개면 큰 편이지만, 공개 표면이 2개로 좁다는 점이 이 설계를 구해 준다 — 밖에서 볼 때는 "결정 요청 / 결과 보고" 두 동작밖에 없다.

테스트 12개가 곧 사양서다

테스트 이름만 읽어도 이 라이브러리가 무엇을 보장하려는지 다 나온다. 직접 실행해 보면 12개 전부 0.003초에 통과한다.

테스트못 박는 성질
permissions_are_a_hard_filter권한 없으면 점수와 무관하게 탈락
self_for_easy_task_with_expensive_peer쉬운 일에 비싼 동료를 부르지 않음
handoff_to_clear_specialist격차가 확실하면 넘긴다
collaboration_for_complementary_skills능력이 상보적이면 팀을 만든다
contextual_reliability_does_not_bleed_across_skills코딩 실적이 보안 신뢰로 번지지 않음
team_level_deadline_is_enforced_after_dag_scheduling팀 구성 후 임계경로로 마감 재검사
failed_incumbent_triggers_replan_to_peer담당자가 실패하면 재계획
partial_credit_updates_agents_differently팀원마다 다른 공로 배분
requirement_dependencies_must_form_a_dag순환 의존은 생성 시점에 거부
outcome_rejects_unselected_agent_evidence고르지도 않은 에이전트 점수는 거부
dag_route_assigns_roles_and_builds_topology역할 배정 + 통신 구조 생성
outcomes_update_reliability_and_pair_synergy실행 결과가 믿음을 갱신

6학습 포인트

"의사결정 이론 · 베이즈 온라인 학습 · 스케줄링 · 그리고 '한계를 문서에 쓰는 법'"

① 제약 최적화를 코드로 옮기는 법

"제약이 있는 최적화"를 배울 때 가장 헷갈리는 게 제약을 페널티로 넣을지, 필터로 넣을지다. SAGE는 이 둘을 명확히 갈랐다 — 어기면 안 되는 것은 필터(권한·예산·마감), 덜한 게 좋은 것은 페널티(비용·지연·위험). 실무에서 이 구분을 흐리면 "예산을 조금 넘겨도 성공률이 높으니까"라는 결정이 나오고, 그건 보통 사고로 이어진다.

비유

비행기 무게 제한은 필터다. "이 짐이 정말 중요하니까 3kg만 넘기자"는 협상 대상이 아니다. 반면 연료 효율은 페널티다 — 나쁘면 감점이지만, 다른 장점으로 상쇄될 수 있다. 두 가지를 같은 점수판에 올리는 순간 비행기가 못 뜬다.

② 베이즈 온라인 학습을 25줄로 구현하기

BetaBelief는 켤레 사전분포(conjugate prior)의 가장 깨끗한 예제다. 관측이 들어오면 α와 β에 더하기만 하면 사후분포가 나온다 — 적분도, 최적화도 없다. 여기에 불확실성까지 공짜로 딸려 온다(uncertainty 프로퍼티). 이 불확실성은 효용 함수에서 감점 항인 동시에 탐색 모드에서는 보너스 항으로 쓰인다.

용어
톰슨 샘플링(Thompson Sampling)
"평균이 제일 높은 것"을 고르는 대신, 각 후보의 확률분포에서 주사위를 굴려 그 값이 제일 높은 것을 고르는 방법. 데이터가 적어 분포가 넓은 후보는 가끔 높은 값이 뽑혀 기회를 얻는다 — 탐색(exploration)과 활용(exploitation)을 자연스럽게 섞는 고전 기법이다. SAGE는 exploration=True일 때만 켠다.

③ "손으로 쓴 공식"을 "학습되는 모델"로 승격시키는 법

이 저장소에서 가장 실용적인 교훈일 수 있다. v0.1은 성공률을 손으로 쓴 공식으로 계산했다. v0.2는 그걸 로지스틱 회귀로 바꿨는데, 기존 공식의 계수를 초기 가중치로 넣었다.

bias: float = -1.15
weights = {
    "coverage": 2.35, "bottleneck": 1.35, "trust": 0.80, "synergy": 0.35,
    "redundancy": -0.45, "coordination_loss": -0.55,
    "handoff_loss": -0.70, "switch_loss": -0.55, "load": -0.35,
}

사람이 만든 휴리스틱이 그대로 모델의 사전 지식이 된다. 데이터가 0개일 때는 예전 공식처럼 동작하고, 관측이 쌓일수록 데이터 쪽으로 서서히 옮겨 간다. 학습률도 0.08 / sqrt(1 + updates/50)로 감쇠시켜, 초반엔 빨리 배우고 나중엔 흔들리지 않게 했다. 규칙 기반 시스템을 ML로 마이그레이션할 때 그대로 훔쳐 쓸 만한 패턴이다.

④ DAG 스케줄링과 임계 경로

_schedule()이 하는 일은 프로젝트 관리에서 말하는 임계 경로(critical path) 계산과 같다. 위상 정렬, 자원 제약(같은 사람은 동시에 두 일 못 함), 의존 제약(선행 작업이 끝나야 시작)을 함께 다룬다. 운영체제나 컴파일러 수업에서 배운 개념이 에이전트 팀 편성에 그대로 적용되는 걸 볼 수 있는 흔치 않은 예제다.

⑤ 파이썬 dataclass로 "잘못된 상태를 만들 수 없게" 하기

frozen=True + __post_init__ 검증 조합은 그 자체로 좋은 파이썬 교재다. 특히 Task._validate_dag()DFS 색칠 기법으로 순환을 잡는다visiting(회색)과 visited(검정) 두 집합으로 재귀 중 다시 만난 노드를 순환으로 판정하는 교과서적 구현이다.

def visit(name):
    if name in visiting:                 # 아직 탐색 중인 노드를 다시 만남 = 순환
        raise ValueError("requirement dependencies must form a DAG")
    if name in visited: return           # 이미 끝난 가지는 재방문 안 함
    visiting.add(name)
    for dependency in graph[name]:
        visit(dependency)
    visiting.remove(name); visited.add(name)

⑥ 한계를 문서에 쓰는 기술

기술 외적이지만 중요한 학습 포인트다. ALGORITHM.md 10절은 자기 벤치마크에 대해 이렇게 쓴다 — "이 합성 수치는 학습과 제약을 시험하지만, 실세계 우월성의 증거는 아니다. 출판 가능한 평가에는 실제 실행에 대한 신뢰구간, 강한 학습형 베이스라인, 마켓플레이스 트레이스 재생, 보정 분석, 적대적 조건이 필요하다."

자기 프로젝트의 약점을 이렇게 구체적으로 적는 README는 드물다. 무엇을 증명했고 무엇을 증명하지 않았는지 구분해 쓰는 것은 오픈소스든 사내 문서든 그대로 가져갈 만한 습관이다.

7하드웨어 · 시스템 요구사항

"GPU 불필요 · 설치할 것 없음 · 노트북에서 2.8초에 2,500개 작업 시뮬레이션"

이 항목은 짧게 끝난다. 필요한 건 파이썬 3.10 이상뿐이다.

항목요구사항비고
Python3.10 이상CI는 3.10/3.11/3.12 검증. tuple[str, ...]·float | None 문법 때문
서드파티 패키지0개pip install 없이 git clone 후 바로 실행
GPU불필요LLM을 호출하지 않는다 — 순수 계산 로직
메모리수십 MB믿음 객체가 에이전트 수 × 요구사항 수만큼 생김
디스크약 1 MBSVG 3장 포함 저장소 전체
실행 시간테스트 0.003초 / 벤치마크 약 2.8초2,500개 작업 × 5개 시드, 일반 노트북 기준 실측
네트워크불필요실제 A2A 통신은 아직 구현 안 됨(로드맵)
OS무관표준 라이브러리만 씀
git clone --depth 1 https://github.com/wang2122/sprix-sage-router.git
cd sprix-sage-router
python demo.py            # 결정 하나 출력
python -m unittest -v     # 테스트 12개
python benchmark.py       # 2,500개 작업 비교 (약 2.8초)
규모를 가늠할 때

실사용에서는 에이전트가 수백~수천 개일 수 있다. 그런데 빔 서치 복잡도가 n(후보 수)에 선형이라, 후보가 1,000개면 매 라우팅마다 수만 번의 _evaluate()가 돈다. 그래서 ALGORITHM.md의 복잡도 표기에 "후보 검색(candidate retrieval)은 제외"라는 단서가 붙어 있고, 로드맵에도 "학습된 작업 텍스트 임베딩과 후보 검색"이 미완 항목으로 올라 있다. 1,000명 중 상위 20명을 먼저 추리는 단계가 실서비스에서는 반드시 앞에 붙어야 한다.

8직접 해볼 수 있는 실습 과제

"돌려보기 → 가중치 흔들어보기 → 특징 추가하기 → 모델 교체하기 → 진짜 A2A 붙이기"
난이도 ★☆☆☆☆ — 30분

1. 진행률만 바꿔서 결정이 뒤집히는 지점 찾기

demo.py에서 progress=0.45를 0.0부터 0.95까지 0.05씩 바꿔 가며 decision.mode를 찍어 보자. 진행률이 낮을 때는 HANDOFF가 이기다가 어느 지점부터 SELF·COLLABORATE로 넘어갈 것이다.

확인할 것: 뒤집히는 지점이 handoff_friction·context_transferability를 바꾸면 어떻게 이동하는가? 이 세 값의 관계가 _switch_loss() 한 줄에 다 들어 있다 — progress × handoff_friction × (1 − 0.65 × transferability).

난이도 ★★☆☆☆ — 1~2시간

2. 가중치 민감도 표 만들기

RouterWeights의 6개 값을 각각 ±50% 흔들면서 benchmark.py를 돌리고, 품질·비용·마감위반이 어떻게 움직이는지 표로 만들어 보자.

확인할 것: 어떤 가중치가 결과를 가장 크게 흔드는가? 기본값 중 handoff=0.22가 가장 크다는 게 설계자의 선택인데, 벤치마크에서도 그 선택이 정당한가? 기본값을 의심해 보는 연습이 이 과제의 핵심이다.

난이도 ★★★☆☆ — 하루

3. 성공 모델에 새 특징 추가하기

OnlineSuccessModel.weights에 특징이 9개 있다. 여기에 하나를 더해 보자 — 예를 들어 팀 크기(team_size)나 요구사항 개수(n_requirements). _evaluate()features 딕셔너리에 값을 넣고, weights에 초기값을 주면 끝이다.

확인할 것: 벤치마크 성능이 오르는가, 아니면 과적합으로 떨어지는가? L2 계수(l2=0.001)를 키우면 어떻게 되는가? 특징을 추가한다고 항상 좋아지지는 않는다는 걸 직접 겪어 보는 게 목적이다.

난이도 ★★★★☆ — 2~3일

4. 로지스틱 모델을 다른 것으로 교체하기

ALGORITHM.md가 대놓고 초대하는 실습이다. OnlineSuccessModel과 같은 인터페이스(predict(features) / update(features, outcome))를 가진 대체 모델을 만들어 갈아끼워 보자. 후보: 작은 그래디언트 부스팅, 2층 신경망, 또는 문맥적 밴딧(LinUCB).

확인할 것: 인터페이스가 정말 그것만으로 충분한가? model_featuresRouteDecisionrepr=False로 실려 다니는 이유를 이해하면(결정 시점의 특징을 그대로 학습에 재사용하기 위함) 왜 이 교체가 쉬운지 보인다.

난이도 ★★★★★ — 1~2주

5. 실제 A2A 어댑터를 붙여 결정을 실행까지 연결하기

로드맵의 미완 항목을 직접 채워 보는 과제다. ① 실제 AgentCard JSON을 읽어 Agent로 변환하는 파서, ② decision을 받아 A2A message/send로 실행하는 실행기, ③ 실행 결과를 ExecutionOutcome으로 되먹이는 회수기.

확인할 것: AgentCard.skills는 자유 텍스트인데 SAGE는 0~1 벡터를 요구한다 — 이 정규화 문제가 실제로는 가장 어렵다. 임베딩으로 풀 것인지, 태그 사전으로 풀 것인지가 이 과제의 진짜 설계 결정이다.

9관련 기술 심화 학습 로드맵

"4주짜리 코스 — 코드 읽기에서 시작해 자기 라우터를 만드는 데까지"

1주차 — 코드와 수식을 나란히 놓고 읽기

할 일
1–2일ALGORITHM.md 완독 → 각 수식이 sprix_sage.py의 몇 번째 줄인지 표로 매핑
3–4일route()_evaluate()_coverage_and_assignment() 호출 흐름을 손으로 추적
5–7일테스트 12개를 하나씩 읽고 일부러 깨뜨려 보기(조건을 뒤집어 실패를 확인)

2주차 — 배경 이론 채우기

주제왜 필요한가자료
베타-이항 켤레BetaBelief의 전부베이즈 통계 입문 아무거나 · 켤레 사전분포 장
다중 슬롯머신 · 톰슨 샘플링탐색/활용 균형Sutton & Barto 2장
온라인 로지스틱 회귀OnlineSuccessModelSGD + L2 정규화 기초
제출모듈러 최대화빔 서치를 쓴 이유탐욕 알고리즘의 보장과 그 한계
DAG 리스트 스케줄링_schedule()운영체제·병렬처리 교재의 임계경로 절

3주차 — 프로토콜과 생태계

주제구체적으로
A2A 사양 읽기AgentCard 구조 · task 생명주기 · 인증 요구사항. SAGE의 "A2A 통합" 표와 대조
MCP와의 차이MCP는 도구 연결, A2A는 에이전트 연결 — 라우팅이 필요한 쪽은 후자
참고 논문 4편RouteLLM(모델 라우팅) · DyLAN(동적 팀) · GPTSwarm(그래프 최적화) · MasRouter(모드+역할+모델)

4주차 — 만들어 보기

할 일
1–3일내 도메인에 맞는 Agent·Requirement 정의 (예: 문서 검토 파이프라인)
4–5일실제 LLM 에이전트 2~3개를 붙여 결정 → 실행 → record_outcome 루프 완성
6–7일20~50회 실행 후 router.reliability·skill_reliability를 덤프해 믿음이 실제로 갈라졌는지 확인
함께 볼 것
에이전트를 늘리면 정말 좋아지는가는 별도의 질문이다

SAGE는 "팀을 어떻게 짤까"를 최적화하지만, "팀을 짜는 게 나은가"는 답하지 않는다. 벤치마크에서도 온라인 SAGE가 예산을 81% 더 썼다(0.239 → 0.434). 멀티에이전트를 도입할 때는 조율 실패·목표 충돌 같은 별도 위험이 따라오므로, 라우팅 최적화와 도입 여부 판단을 분리해서 생각하는 게 좋다.

10핵심 키워드 사전

"코드를 읽다 막히면 여기부터"
용어코드에서
SAGEState-Aware Graph Exchange. "상태를 아는 그래프 교환"
A2AAgent2Agent 프로토콜. 에이전트 간 표준 통신 규약
incumbentincumbent_id지금 작업을 맡고 있는 담당 에이전트
SELF / COLLABORATE / HANDOFFMode혼자 계속 / 동료 합류(소유권 유지) / 통째로 이관
coverage_coverage_and_assignment노이지-OR로 계산한 요구사항 충족도. 1 − ∏(1 − q)
bottleneckfeatures["bottleneck"]가장 약한 요구사항의 기준 충족 비율. 평균이 가리는 구멍을 잡는 지표
contextual trust_contextual_trust전역 신뢰(35%) + 요구사항별 신뢰(65%)의 혼합
synergy / redundancy_team_terms짝 궁합(베타 믿음) / 능력 벡터 코사인 유사도(중복은 감점)
topologydecision.topology역할 배정에서 유도된 팀 통신 간선 목록. 간선 수가 지연을 늘린다
switch loss / handoff loss_switch_loss담당자 교체 시 잃는 맥락. 진행률 × 비전달성 × 미유지비율
bid fidelitycost_fidelity, latency_fidelity견적이 실제와 얼마나 맞았는지. 낮으면 다음 견적을 부풀려 심사
Beta beliefBetaBelief성공/실패 횟수(α, β)로 확률을 추정하는 켤레 사전분포
Thompson sampling_belief_value, _draw_cache평균 대신 분포에서 표본을 뽑아 고르는 탐색 기법
noisy-ORmath.prod(1 - s)"전원이 동시에 실패할 확률"의 여집합으로 팀 능력 계산
beam search_beam_collaboration_decisions상위 B개 후보만 남기며 넓혀 가는 탐색. 그리디와 전수의 중간
critical path_schedule병렬 실행 시 전체를 결정하는 가장 긴 의존 경로
credit assignmentrecord_outcome팀 성과를 누구 공으로 돌릴지 배분하는 문제
research previewREADME"아직 프로덕션 아님"의 공식 표현. SLA·검증된 성능 없음

11참고 링크

"저장소 · 프로토콜 · 이 설계가 기대고 있는 논문들"