wang2122/sprix-sage-router) 딥다이브sprix_sage.py 749줄 · 벤치마크 345줄 · 테스트 210줄 · 총 1,848줄 · 별 약 1,009개 / 포크 12개 · 2026-08-18 생성 · TrendShift Daily #12)
SAGE는 State-Aware Graph Exchange의 약자다. 이름의 "State-Aware(상태를 아는)"가 핵심이다. 보통의 에이전트 매칭은 작업이 시작되기 전에 "이 일에 누가 제일 잘 맞나"를 고른다. SAGE는 이미 45% 진행됐고, 계획 단계는 끝났고, 한 번 실패한 적 있는 상태에서 다시 판단한다.
이 차이가 왜 중요하냐면, 진행률이 높을수록 담당자를 바꾸는 비용이 커지기 때문이다. 이미 쌓인 맥락(대화 기록·중간 산출물·시행착오)을 새 에이전트에게 완전히 옮길 수 없다면, 아무리 실력이 좋은 전문가라도 넘기는 게 손해일 수 있다. SAGE는 이 손실을 handoff_loss·switch_loss라는 이름의 페널티 항으로 수식에 직접 넣는다.
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 안에서 보안까지 함께 커버하는 팀을 만들려면 후자를 넣는 편이 전체 효용이 높다고 판단한 것이다.
2026년의 멀티에이전트 도구는 대부분 연결에 집중해 있다. MCP는 도구를 붙이고, A2A는 에이전트끼리 말을 트게 하고, 오케스트레이터는 순서를 짠다. 그런데 실제로 굴려 보면 다음 질문이 남는다 — "에이전트를 더 붙이면 더 좋아지긴 하는데, 왜 비용이 세 배가 되지?"
보통의 프레임워크에서 "혼자 할지 / 넘길지"는 프롬프트나 if 문으로 흩어져 있다. SAGE는 세 모드를 같은 저울에 올린다. 담당자가 그대로 하는 것도 하나의 후보이고, 팀을 꾸리는 것도, 넘기는 것도 후보다. 전부 같은 식으로 점수를 매겨 최대값을 고른다.
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.003 | 0.389 | 0.239 | 26.4% |
| 광고 능력치 단독 | 0.558 ±0.005 | 0.435 | 0.292 | 11.9% |
| 단독 오라클(숨은 정보 사용) | 0.553 ±0.005 | 0.440 | 0.271 | 0.0% |
| 정적 SAGE(학습 없음) | 0.591 ±0.007 | 0.467 | 0.329 | 0.0% |
| 온라인 SAGE(학습 포함) | 0.634 ±0.006 | 0.487 | 0.434 | 0.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의 굵은 숫자도 별 수가 아니라 모멘텀 점수다. 쓸 물건이 아니라 읽을 물건으로 접근하는 게 맞다 — 그리고 읽을 물건으로는 밀도가 상당히 높다.
이 저장소의 기술 스택 지도는 그리기가 민망할 만큼 단순하다. 그리고 그게 의도다.
pyproject.toml에 dependencies 항목 자체가 없다. 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
| 표준 모듈 | 이 프로젝트에서 하는 일 |
|---|---|
dataclasses | frozen=True 불변 객체 8종 + replace()로 결정 객체 복사 수정 |
enum | Mode(str, Enum) — 문자열이면서 열거형이라 mode.value가 그대로 "self" |
itertools.combinations | 팀 내 모든 2인 조합 순회(시너지·중복도 계산) |
math | exp/sqrt/prod/inf — 시그모이드, 노이지-OR, 무한대 기본 예산 |
random.Random | 시드 고정 톰슨 샘플링(betavariate) — 재현 가능한 탐색 |
typing | Iterable/Mapping만 — 나머지는 3.10 내장 제네릭(tuple[str, ...]) |
로지스틱 회귀를 쓰면서 scikit-learn을 안 붙였다는 건, 모델을 갈아끼울 수 있게 남겨 뒀다는 뜻이다. ALGORITHM.md도 "이 가벼운 모델은 의도적으로 교체 가능하다 — 프로덕션에서는 GBDT·인코더 모델·베이지안 신경망으로 바꾸되 제약과 탐색 층은 그대로 쓰면 된다"고 명시한다. 학습 코드가 20줄이라 바꾸기 쉽다는 것 자체가 기능인 설계다.
| 타입 | 표현하는 것 | 핵심 필드 |
|---|---|---|
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"] — 단일 모듈 배포 |
| CI | GitHub 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 (모듈·문서 전부) |
전체 그림부터 보자. SAGE는 A2A 스택에서 검색과 실행 사이에 끼는 정책 층이다.
가장 많이 배울 만한 부분이다. 권한·예산·마감은 점수가 아니라 필터다. 아무리 예측 성공률이 높아도 권한이 없으면 순위에 들어오지도 못한다.
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에 가깝다 — 그 계산을 마치고 나서 마감을 검사하는 것이다.
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)"이라고 조심스럽게 표현한다.
두 번째 줄이 특히 영리하다. 믿을 수 없는 에이전트가 "나 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)이므로 전역 최적을 주장하지 않는다."
_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] # 이후엔 재사용 (일관된 비교)
정직하게 말하면 이 저장소에는 해부할 디렉토리가 없다. 파일 15개, docs/assets/와 .github/ 말고는 전부 루트에 평평하게 놓여 있다.
그래서 진짜 해부 대상은 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개 전부 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 | 실행 결과가 믿음을 갱신 |
"제약이 있는 최적화"를 배울 때 가장 헷갈리는 게 제약을 페널티로 넣을지, 필터로 넣을지다. SAGE는 이 둘을 명확히 갈랐다 — 어기면 안 되는 것은 필터(권한·예산·마감), 덜한 게 좋은 것은 페널티(비용·지연·위험). 실무에서 이 구분을 흐리면 "예산을 조금 넘겨도 성공률이 높으니까"라는 결정이 나오고, 그건 보통 사고로 이어진다.
비행기 무게 제한은 필터다. "이 짐이 정말 중요하니까 3kg만 넘기자"는 협상 대상이 아니다. 반면 연료 효율은 페널티다 — 나쁘면 감점이지만, 다른 장점으로 상쇄될 수 있다. 두 가지를 같은 점수판에 올리는 순간 비행기가 못 뜬다.
BetaBelief는 켤레 사전분포(conjugate prior)의 가장 깨끗한 예제다. 관측이 들어오면 α와 β에 더하기만 하면 사후분포가 나온다 — 적분도, 최적화도 없다. 여기에 불확실성까지 공짜로 딸려 온다(uncertainty 프로퍼티). 이 불확실성은 효용 함수에서 감점 항인 동시에 탐색 모드에서는 보너스 항으로 쓰인다.
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로 마이그레이션할 때 그대로 훔쳐 쓸 만한 패턴이다.
_schedule()이 하는 일은 프로젝트 관리에서 말하는 임계 경로(critical path) 계산과 같다. 위상 정렬, 자원 제약(같은 사람은 동시에 두 일 못 함), 의존 제약(선행 작업이 끝나야 시작)을 함께 다룬다. 운영체제나 컴파일러 수업에서 배운 개념이 에이전트 팀 편성에 그대로 적용되는 걸 볼 수 있는 흔치 않은 예제다.
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는 드물다. 무엇을 증명했고 무엇을 증명하지 않았는지 구분해 쓰는 것은 오픈소스든 사내 문서든 그대로 가져갈 만한 습관이다.
이 항목은 짧게 끝난다. 필요한 건 파이썬 3.10 이상뿐이다.
| 항목 | 요구사항 | 비고 |
|---|---|---|
| Python | 3.10 이상 | CI는 3.10/3.11/3.12 검증. tuple[str, ...]·float | None 문법 때문 |
| 서드파티 패키지 | 0개 | pip install 없이 git clone 후 바로 실행 |
| GPU | 불필요 | LLM을 호출하지 않는다 — 순수 계산 로직 |
| 메모리 | 수십 MB | 믿음 객체가 에이전트 수 × 요구사항 수만큼 생김 |
| 디스크 | 약 1 MB | SVG 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명을 먼저 추리는 단계가 실서비스에서는 반드시 앞에 붙어야 한다.
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).
RouterWeights의 6개 값을 각각 ±50% 흔들면서 benchmark.py를 돌리고, 품질·비용·마감위반이 어떻게 움직이는지 표로 만들어 보자.
확인할 것: 어떤 가중치가 결과를 가장 크게 흔드는가? 기본값 중 handoff=0.22가 가장 크다는 게 설계자의 선택인데, 벤치마크에서도 그 선택이 정당한가? 기본값을 의심해 보는 연습이 이 과제의 핵심이다.
OnlineSuccessModel.weights에 특징이 9개 있다. 여기에 하나를 더해 보자 — 예를 들어 팀 크기(team_size)나 요구사항 개수(n_requirements). _evaluate()의 features 딕셔너리에 값을 넣고, weights에 초기값을 주면 끝이다.
확인할 것: 벤치마크 성능이 오르는가, 아니면 과적합으로 떨어지는가? L2 계수(l2=0.001)를 키우면 어떻게 되는가? 특징을 추가한다고 항상 좋아지지는 않는다는 걸 직접 겪어 보는 게 목적이다.
ALGORITHM.md가 대놓고 초대하는 실습이다. OnlineSuccessModel과 같은 인터페이스(predict(features) / update(features, outcome))를 가진 대체 모델을 만들어 갈아끼워 보자. 후보: 작은 그래디언트 부스팅, 2층 신경망, 또는 문맥적 밴딧(LinUCB).
확인할 것: 인터페이스가 정말 그것만으로 충분한가? model_features가 RouteDecision에 repr=False로 실려 다니는 이유를 이해하면(결정 시점의 특징을 그대로 학습에 재사용하기 위함) 왜 이 교체가 쉬운지 보인다.
로드맵의 미완 항목을 직접 채워 보는 과제다. ① 실제 AgentCard JSON을 읽어 Agent로 변환하는 파서, ② decision을 받아 A2A message/send로 실행하는 실행기, ③ 실행 결과를 ExecutionOutcome으로 되먹이는 회수기.
확인할 것: AgentCard.skills는 자유 텍스트인데 SAGE는 0~1 벡터를 요구한다 — 이 정규화 문제가 실제로는 가장 어렵다. 임베딩으로 풀 것인지, 태그 사전으로 풀 것인지가 이 과제의 진짜 설계 결정이다.
| 날 | 할 일 |
|---|---|
| 1–2일 | ALGORITHM.md 완독 → 각 수식이 sprix_sage.py의 몇 번째 줄인지 표로 매핑 |
| 3–4일 | route() → _evaluate() → _coverage_and_assignment() 호출 흐름을 손으로 추적 |
| 5–7일 | 테스트 12개를 하나씩 읽고 일부러 깨뜨려 보기(조건을 뒤집어 실패를 확인) |
| 주제 | 왜 필요한가 | 자료 |
|---|---|---|
| 베타-이항 켤레 | BetaBelief의 전부 | 베이즈 통계 입문 아무거나 · 켤레 사전분포 장 |
| 다중 슬롯머신 · 톰슨 샘플링 | 탐색/활용 균형 | Sutton & Barto 2장 |
| 온라인 로지스틱 회귀 | OnlineSuccessModel | SGD + L2 정규화 기초 |
| 제출모듈러 최대화 | 빔 서치를 쓴 이유 | 탐욕 알고리즘의 보장과 그 한계 |
| DAG 리스트 스케줄링 | _schedule() | 운영체제·병렬처리 교재의 임계경로 절 |
| 주제 | 구체적으로 |
|---|---|
| A2A 사양 읽기 | AgentCard 구조 · task 생명주기 · 인증 요구사항. SAGE의 "A2A 통합" 표와 대조 |
| MCP와의 차이 | MCP는 도구 연결, A2A는 에이전트 연결 — 라우팅이 필요한 쪽은 후자 |
| 참고 논문 4편 | RouteLLM(모델 라우팅) · DyLAN(동적 팀) · GPTSwarm(그래프 최적화) · MasRouter(모드+역할+모델) |
| 날 | 할 일 |
|---|---|
| 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). 멀티에이전트를 도입할 때는 조율 실패·목표 충돌 같은 별도 위험이 따라오므로, 라우팅 최적화와 도입 여부 판단을 분리해서 생각하는 게 좋다.
| 용어 | 코드에서 | 뜻 |
|---|---|---|
| SAGE | — | State-Aware Graph Exchange. "상태를 아는 그래프 교환" |
| A2A | — | Agent2Agent 프로토콜. 에이전트 간 표준 통신 규약 |
| incumbent | incumbent_id | 지금 작업을 맡고 있는 담당 에이전트 |
| SELF / COLLABORATE / HANDOFF | Mode | 혼자 계속 / 동료 합류(소유권 유지) / 통째로 이관 |
| coverage | _coverage_and_assignment | 노이지-OR로 계산한 요구사항 충족도. 1 − ∏(1 − q) |
| bottleneck | features["bottleneck"] | 가장 약한 요구사항의 기준 충족 비율. 평균이 가리는 구멍을 잡는 지표 |
| contextual trust | _contextual_trust | 전역 신뢰(35%) + 요구사항별 신뢰(65%)의 혼합 |
| synergy / redundancy | _team_terms | 짝 궁합(베타 믿음) / 능력 벡터 코사인 유사도(중복은 감점) |
| topology | decision.topology | 역할 배정에서 유도된 팀 통신 간선 목록. 간선 수가 지연을 늘린다 |
| switch loss / handoff loss | _switch_loss | 담당자 교체 시 잃는 맥락. 진행률 × 비전달성 × 미유지비율 |
| bid fidelity | cost_fidelity, latency_fidelity | 견적이 실제와 얼마나 맞았는지. 낮으면 다음 견적을 부풀려 심사 |
| Beta belief | BetaBelief | 성공/실패 횟수(α, β)로 확률을 추정하는 켤레 사전분포 |
| Thompson sampling | _belief_value, _draw_cache | 평균 대신 분포에서 표본을 뽑아 고르는 탐색 기법 |
| noisy-OR | math.prod(1 - s) | "전원이 동시에 실패할 확률"의 여집합으로 팀 능력 계산 |
| beam search | _beam_collaboration_decisions | 상위 B개 후보만 남기며 넓혀 가는 탐색. 그리디와 전수의 중간 |
| critical path | _schedule | 병렬 실행 시 전체를 결정하는 가장 긴 의존 경로 |
| credit assignment | record_outcome | 팀 성과를 누구 공으로 돌릴지 배분하는 문제 |
| research preview | README | "아직 프로덕션 아님"의 공식 표현. SLA·검증된 성능 없음 |