페이지 목차
이 페이지에서 검색
멀티 에이전트
이 페이지에 묻기
멀티 에이전트 · 현재 차시와 목차 본문에서만 찾아 답합니다. 답을 지어내지 않고 해당하는 대목을 그대로 보여 주며, 여기에 없으면 없다고 알려 드립니다.
생성형 AI가 아니라 이 페이지 본문을 찾아 그대로 보여 주는 방식입니다. 외부로 나가는 요청이 없습니다.
MULTI · AGENT
멀티 에이전트
생성 수정
도구 실행·검색·기억·그래프 흐름·전문 에이전트의 협업을 구성하고 권한·평가·사람의 확인으로 통제합니다.
26개 문서 · 3일차 · 25 / 26
24차시 · 서비스 통합과 실행
지도 위치 : 3일차 · README 4장의 ⑥ 서비스 층(마무리) · 과정 전체 정산 | 이전: 23차시 Human-in-the-Loop | 다음: 전체 목차로 키워드 : API 서버, 스트리밍, 통합 테스트
이 차시를 다 읽으면
- API 서버를 유와 종차로 정의하고, 오피스봇의 엔드포인트(대화 전송 · 승인 처리 · 세션 조회 · 기억 삭제)를 설계할 수 있다.
- 로그인한 사용자의 신원이 도구 권한까지 이어지는 길을 설명할 수 있다.
- 스트리밍(SSE)을 축 ⑥으로 설명하고, "빨라지는 것이 아니라 먼저 보여 주는 것 "임을 말할 수 있다.
- 통합 테스트 시나리오표와 평가셋 재실행으로 서비스 전체를 검수하고, 운영에서 볼 지표를 댈 수 있다.
- 과정 전체를 여섯 층 · 여덟 축 · 중심 축의 여정 · 세 문장 으로 정산할 수 있다.
0. 이 차시의 판별 축
| # | 축 | 왼쪽 (0) | 오른쪽 (1) | 이 축으로 갈리는 것 | 큰 축 (README 5장) |
|---|---|---|---|---|---|
| 1 | 동기 ↔ 비동기 ⭐ | 다 끝난 뒤 한 번에 받기 | 만들어지는 대로 조금씩 받기 (스트리밍) | 사용자가 기다리는 동안 무엇을 보나 | ⑥ 동기 ↔ 비동기 |
| 2 | 로컬 ↔ 원격 | 우리 서버 (그래프 · 도구 · DB) | 화면(브라우저)과 외부 LLM API | 무엇이 어느 경계를 넘나 | ④ 로컬 ↔ 원격 |
| 3 | 무상태 ↔ 상태 | 요청 하나하나는 독립 | 세션 · 체크포인트 · 기억은 DB에 | 여러 요청 사이에 무엇이 이어지나 | ⑦ 무상태 ↔ 상태 |
| 4 | 증거 ↔ 주장 | 시나리오표 · 평가셋 재실행 결과 | "다 연결했습니다" | 서비스가 완성됐다고 믿을 근거 | ⑧의 파생 |
| 5 | 과정 전체 정산 | — | — | 24차시 동안 쌓은 층과 축 | README 4 · 5 · 6장 |
이 차시가 답하는 질문은 둘입니다. "지금까지 만든 그래프를 사람들이 쓰는 서비스로 어떻게 내보내나?" , 그리고 "이 과정 전체에서 무엇이 남았나?" 1~5절은 앞의 것을, 7절은 뒤의 것을 다룹니다.
1. API 서버
1.1 정의
API 서버 — [서버 프로그램]인데, [ 정해진 주소(엔드포인트)로 요청을 받아 프로그램의 기능을 실행하고 결과를 돌려준다 ]는 점에서 사람이 화면을 보며 쓰는 웹 페이지 자체나, 한 사람이 터미널에서 돌리는 스크립트와 구별된다.
FastAPI — [Python 웹 프레임워크]인데, [ 함수에 주소를 붙이는 것만으로 API를 만들고, 요청 형식을 Pydantic 스키마로 검증한다 ]는 점에서 화면까지 함께 만드는 웹 프레임워크와 구별된다. 앞에서 배운 구조화 출력(4차시)과 같은 Pydantic을 씁니다.
| 반례 — 이건 서비스가 아닙니다 | 왜 |
|---|---|
노트북 셀에서 graph.invoke(...) | 한 사람 · 한 프로세스. 인증 · 동시 사용자 · 세션 분리가 없음 |
| 화면 코드가 LLM API를 직접 호출 | API 키가 브라우저로 나감(축 ④). 권한 검사를 화면이 할 수 없음 |
1.2 엔드포인트 설계
| 엔드포인트 | 하는 일 | 앞에서 만든 것 |
|---|---|---|
POST /chat | 메시지를 받아 그래프 실행, 진행 상황과 답을 스트리밍 | 18~22차시 그래프 |
POST /approvals/{thread_id} | 승인 · 수정 · 거절을 받아 Command(resume=...)로 재개 | 23차시 |
GET /sessions/{session_id} | 대화 이력 · 승인 대기 상태 조회 (본인 것만) | 16차시 세션, 23차시 체크포인트 |
GET /me/memory, DELETE /me/memory | 내 장기 기억 열람 · 삭제 | 17차시 |
2. 인증 — 사용자 신원에서 도구 권한까지
from fastapi import FastAPI, Depends, HTTPException
api = FastAPI()
@api.post("/approvals/{thread_id}")
async def approve(thread_id: str, body: ApprovalBody, user=Depends(get_current_user)):
if not owns_thread(user.id, thread_id): # 남의 승인 대기는 건드릴 수 없음
raise HTTPException(status_code=403)
config = {"configurable": {"thread_id": thread_id}}
return await graph.ainvoke(Command(resume=body.model_dump()), config)
🔍 읽는 포인트
Depends(get_current_user)— 요청마다 회사 로그인(SSO 토큰 등)을 검증해 사번을 꺼내는 함수입니다. 구현은 회사 인증 방식에 따릅니다.- ⭐ 이
user.id가 유일한 신원 입니다. State의requester_id(21차시), 도구 함수의 "요청자 = 대상" 검사(8차시), 기억 조회(17차시), 승인자 확인(23차시)이 모두 이 값에서 출발합니다. 모델이 대화에서 말한 사번은 어디에서도 신원으로 쓰지 않습니다. owns_thread(...)— thread_id만 알면 남의 흐름을 재개할 수 있으면 안 됩니다. 소유자를 코드로 확인합니다.
3. 스트리밍 — 축 ⑥
3.1 정의
스트리밍 — [응답 전달 방식]인데, [ 결과가 다 만들어지기 전에 만들어지는 대로 조금씩 보낸다 ]는 점에서 다 끝난 뒤 한 번에 보내는 방식과 구별된다.
SSE (Server-Sent Events) — [스트리밍 규약]인데, [ 서버에서 화면 쪽으로 한 방향으로 data: ... 줄을 이어 보낸다 ]는 점에서 양쪽이 자유롭게 주고받는 웹소켓과 구별된다.
⚠️ 축 ⑥의 함정 그대로입니다. 스트리밍해도 답이 완성되는 시간은 같습니다. 달라지는 것은 사용자가 첫 글자를 보는 시간 과 "무엇을 하고 있는지" 아는 것입니다. 멀티에이전트는 단계가 많아 수십 초가 걸릴 수 있는데, 그동안 빈 화면이면 사용자는 고장으로 여깁니다.
3.2 에이전트의 중간 진행 상황 보여 주기
from fastapi.responses import StreamingResponse
import json
LABELS = {"classify": "질문 유형 확인 중", "hr": "인사 담당이 확인 중",
"policy": "규정 검색 중", "synthesize": "답변 정리 중"}
@api.post("/chat")
async def chat(req: ChatRequest, user=Depends(get_current_user)):
config = {"configurable": {"thread_id": f"{user.id}:{req.session_id}"}}
inputs = {"question": req.message, "requester_id": user.id}
async def events():
async for update in graph.astream(inputs, config, stream_mode="updates"):
for node, change in update.items():
if node == "__interrupt__": # 승인 대기 (표시 방식은 버전 확인)
yield sse({"type": "approval_needed", "thread_id": config["configurable"]["thread_id"]})
elif node in LABELS:
yield sse({"type": "progress", "text": LABELS[node]})
if isinstance(change, dict) and "answer" in change:
yield sse({"type": "answer", "text": change["answer"]})
return StreamingResponse(events(), media_type="text/event-stream")
def sse(obj) -> str:
return f"data: {json.dumps(obj, ensure_ascii=False)}\n\n"
🔍 읽는 포인트
thread_id = f"{user.id}:{...}"— 세션 열쇠에 사번을 묶어 다른 사용자의 세션과 섞이지 않게 합니다(16차시 — 세션 분리 실패는 개인정보 사고).graph.astream(..., stream_mode="updates")— 18차시의stream과 같은 것의 비동기판입니다. 노드가 끝날 때마다 바뀐 값이 옵니다. 진행 상황(progress)은 여기서 만듭니다.LABELS— 노드 이름을 사용자 말로 바꿉니다. 이 표에 없는 노드(내부 노드)는 화면에 보이지 않습니다."__interrupt__"— 23차시의 승인 대기에서 멈춘 신호입니다. 화면에 승인 창을 띄우게 합니다. 인터럽트가 스트림에 어떤 모양으로 나오는지는 버전에 따라 다르니 공식 문서를 확인 하세요.data: ...\n\n— SSE의 형식입니다. 빈 줄로 한 사건이 끝납니다.- 💡 답의 글자 단위 스트리밍이 필요하면 답변 노드 안에서
client.messages.stream(...)의text_stream을 쓰고, 그래프 스트림 방식도 그에 맞게 바꿉니다(문서 확인).
3.3 화면 연동
const res = await fetch("/chat", { method: "POST", body: JSON.stringify({ session_id, message }),
headers: { "Content-Type": "application/json" } });
const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { value, done } = await reader.read();
if (done) break;
handleEvents(decoder.decode(value)); // "data: {...}" 줄을 잘라 progress · answer · approval_needed 처리
}
🔍 읽는 포인트
fetch+getReader()— 응답을 도착하는 대로 조각씩 읽습니다. 브라우저의EventSource는 GET만 지원하므로, 메시지를 POST로 보낼 때는 이 방식을 씁니다.- 화면은 LLM API 키를 모릅니다. 화면 → 우리 서버 → 외부 LLM 순서이고, 키와 권한 검사는 서버에만 있습니다(축 ④).
4. 통합 테스트와 운영
4.1 통합 테스트 시나리오표
통합 테스트 — [소프트웨어 시험]인데, [ 화면 · 서버 · 그래프 · 도구 · 외부 API를 실제로 이어 붙인 상태에서 사용자 시나리오 단위로 확인한다 ]는 점에서 부품 하나씩 따로 보는 단독 시험(21차시)과 구별된다.
| # | 시나리오 | 입력 · 조건 | 기대 결과 | 결과 |
|---|---|---|---|---|
| 1 | 정상 | "연차 몇 개 남았어?" | 본인 잔여 + 진행 표시 | |
| 2 | 권한 위반 | 직원 A가 "B 연차 알려줘" | 거부, 도구 로그에 차단 기록 | |
| 3 | 승인 필요 | "모니터 신청해 줘" | approval_needed → 승인 → 접수번호 1건 | |
| 4 | 검색 실패 | 규정에 없는 질문 | 재검색 2회 후 "찾지 못함" 또는 사람에게 | |
| 5 | LLM 오류 | 외부 API를 일부러 429 · 5xx로 | 재시도 후 사과 메시지, 서버는 살아 있음 | |
| 6 | 동시 사용자 | 두 직원이 동시에 대화 | 세션 · 기억 · 승인이 섞이지 않음 | |
| 7 | 기억 삭제 | DELETE /me/memory 후 새 세션 | 이전 선호를 모름 |
(결과 칸은 실측 후 채우세요.) 5번은 LLM API 에러를 종류별로 다루는지 보는 시나리오입니다 — anthropic.RateLimitError(429)와 5xx(APIStatusError)는 잠시 후 재시도, BadRequestError(400)는 요청이 틀린 것이라 재시도해도 소용없습니다.
4.2 평가셋 전체 재실행
과정 중에 만든 평가셋을 서비스 주소로 다시 돌립니다. 부품을 이어 붙이며 동작이 바뀌지 않았는지 보는 회귀 확인 입니다(11차시).
| 평가셋 | 만든 차시 | 단독 점수 | 서비스 경유 점수 | 차이 |
|---|---|---|---|---|
| 의도 분류 | 4 | |||
| 도구 선택 | 7 | |||
| 검색 Recall@k | 15 | |||
| 라우팅 혼동표 | 19 | |||
| 전문 에이전트 단독 | 21 |
(빈칸 양식입니다.) 점수가 떨어졌다면 이어 붙인 경계 (인증 · 세션 · 넘겨주기)를 먼저 의심합니다.
4.3 운영 시 볼 것
| 지표 | 왜 | 이상 신호 |
|---|---|---|
| 비용 (요청당 · 사용자당 · 일별 토큰) | 멀티에이전트는 요청마다 비용 편차가 큼 | 특정 질문 유형에서 급증 → 22차시 상한 확인 |
| 지연 (첫 진행 표시까지 · 답 완성까지) | 스트리밍은 앞의 것만 줄임 | 답 완성 시간이 계속 늘어남 |
| 실패율 (에러 · escalate · 상한 도달 비율) | 조용한 실패를 찾음 | escalate 급증 → 분기 기준 · 검색 품질 점검 |
| 추적 (요청 단위 실행 기록) | 누가 무엇을 했나 (22차시) | 마스킹 누락 여부도 정기 점검 (17차시) |
5. ⭐ 과정 전체 정산
5.1 README 4장 — 여섯 층에 무엇이 남았나
| 층 | 차시 | 오피스봇에 남은 것 | 그 층에서 가장 많이 쓴 축 |
|---|---|---|---|
| ① 호출 | 1~4 | LLM 호출, 프롬프트, 의도 분류기(구조화 출력) | ⑧ 구문 ↔ 의미, 결정론 ↔ 확률론 |
| ② 행동 | 5~11 | 조회 도구, 권한 경계, 에이전트 루프, 가드레일, 프레임워크 · 추적 | ② ③, 부탁 ↔ 강제 |
| ③ 지식 | 12~15 | 규정 문서 검색, 출처가 붙은 답, 검색 평가 | ⑧, 증거 ↔ 주장 |
| ④ 기억 | 16~17 | 세션, 요약 메모리, 사용자 프로필 | ⑦, ①, ④ |
| ⑤ 협업 | 18~22 | 그래프, 분기, 작업 분해, 전문 에이전트, Supervisor | ⑤, 누가 흐름을 정하는가 |
| ⑥ 서비스 | 23~24 | 승인 게이트, API 서버, 스트리밍, 통합 테스트 | 사람이 손을 대는가, ⑥ |
위층은 아래층을 부품으로 썼습니다. 22차시의 Supervisor는 4차시의 구조화 출력으로 결정을 내고, 21차시의 전문 에이전트는 6 · 8차시의 도구와 권한 경계를 그대로 품고 있습니다. 아래층이 부실하면 위층이 그 부실을 반복해서 키운다는 README 1.2의 말이 그래서 맞습니다.
5.2 README 5장 — 여덟 큰 축이 어디서 나왔나
| 큰 축 | 처음 만난 곳 | 3일차에서 다시 쓴 곳 |
|---|---|---|
| ① 디스크 ↔ 메모리 | 세션 저장 · 벡터 DB (13 · 16차시) | 장기 기억(17), 체크포인터(18 · 23) |
| ② 추상 ↔ 구상 | 도구 선언 ↔ 함수 (5차시) | 에이전트 입출력 계약(21) |
| ③ 속성 ↔ 기능 | 도구 결과 ↔ 도구 실행 (5 · 6차시) | State ↔ Node(18) |
| ④ 로컬 ↔ 원격 | 외부 LLM API로 나가는 프롬프트 (2 · 8차시) | 기억 주입(17), 추적(22), 화면 ↔ 서버(24) |
| ⑤ 하나 ↔ 여럿 | 도구 하나 ↔ 여럿 (7차시), 청크 (12차시) | 작업 분해(20), 전문 에이전트(21), Supervisor(22) |
| ⑥ 동기 ↔ 비동기 | 병렬 도구 호출 (5 · 7차시) | 병렬 워커(20), 스트리밍(24) |
| ⑦ 무상태 ↔ 상태 | LLM API는 무상태 (2 · 9 · 16차시) | 그래프 State(18), 인터럽트 재개(23) |
| ⑧ 구문론 ↔ 의미론 | 스키마 검증 ↔ 분류 정확도 (4차시) | 계획 검수(20), 라우팅 혼동표(19) |
5.3 README 5.2 — 중심 축 "누가 흐름을 정하는가"의 여정
코드가 정함 (0) ←──────────────────────────────────→ 모델이 정함 (1)
1일차 의도 분류 후 if 분기 (4 · 7) ●
2일차 에이전트 루프 (9) ●
3일차 그래프 (18) — 큰 흐름은 코드 ● ← 칸 안 판단은 모델
규칙 라우터 / LLM 라우터 (19) ● ●
Planner-Worker (20) ●
Supervisor (22) ●
HITL (23) — 이 축 위에 "사람이 정하는 칸"을 끼워 넣음
1일차는 왼쪽, 2일차 루프는 오른쪽 끝까지 갔다가, 3일차는 둘을 섞는 법 을 배웠습니다. 큰 흐름은 그래프(코드)가 쥐고, 칸 안의 판단은 모델이 하고, 되돌릴 수 없는 칸에는 사람이 앉습니다. 같은 요청이라도 어느 칸을 어느 쪽에 둘지 고르는 것이 이 과정에서 기른 설계 판단입니다.
5.4 README 6장 — 세 문장 회수
| 문장 | 이 과정에서 지킨 자리 | 오늘 마지막으로 확인한 곳 |
|---|---|---|
| 1. 가장 단순한 것부터, 충분하면 멈춘다 | 워크플로우 먼저(1), 루프는 필요할 때(9), 나누기 전에 평가셋(21), 복합일 때만 분해(20) | 4.3 비용 지표 — 단순한 질문이 멀티에이전트를 타고 있지 않은가 |
| 2. 모델은 판단하고, 코드는 지킨다 | 스키마(4), 권한 경계(8), 상한(10 · 19 · 22), 프로필 스키마(17), 승인 게이트(23) | 2절 — 신원은 로그인 세션 하나에서만, 4.1 시나리오 2 · 3 |
| 3. 근거와 측정이 없으면 믿지 않는다 | 출처(14), 평가셋(4 · 7 · 15 · 19 · 21), 추적(11 · 22) | 4.1 시나리오표, 4.2 평가셋 재실행 |
6. 실습 — 맡기고 검수하기
지시
오피스봇 그래프(18~23차시)를 FastAPI 서버로 내보내고 간단한 채팅 화면을 붙여줘.
참고 공식 문서: FastAPI(StreamingResponse, Depends), LangGraph 스트리밍(astream, stream_mode)과 interrupt 문서,
Claude API 스트리밍 · 에러 처리 문서. 설치된 버전을 모두 출력할 것.
구조: 1.2의 엔드포인트 5개. /chat은 SSE로 progress · answer · approval_needed 사건을 보낼 것.
제약:
- 신원은 get_current_user 하나에서만(테스트용 가짜 로그인 토큰 2개). 모델 출력의 사번은 쓰지 말 것.
- thread_id에 사번을 포함하고, 승인 · 조회 엔드포인트에서 소유자 검사.
- API 키는 환경변수로만, 화면 코드에 넣지 말 것. 서버 외부 공개(포트 개방 · 배포)는 나에게 승인받은 뒤에.
테스트: 4.1 시나리오 7개 전부 + 4.2 평가셋 재실행.
보고 형식: 4.1 표와 4.2 표를 채우고, 실패 시나리오는 [시나리오 | 기대 | 실제 | 로그 원문] 표로.
추측하지 말고 실행 명령, 서버 로그 원문, 화면 캡처를 붙여.
받을 증거
- 서버 실행 명령과 로그, 시나리오 7개 결과표 , 평가셋 재실행표
- 화면 캡처: 진행 표시가 차례로 바뀌는 장면, 승인 창
- 시나리오 6(동시 사용자)의 두 세션 로그 — 서로 다른 thread_id
내가 직접 확인할 것
| 볼 것 | 이걸로 아는 것 |
|---|---|
| 화면에서 답보다 진행 표시가 먼저 뜨는가 | 스트리밍이 "먼저 보여 주기"로 동작하나 (3.1) |
| 브라우저 개발자 도구의 네트워크 탭에 API 키가 보이지 않는가 | 키가 서버에만 있나 (3.3, 축 ④) |
| 시나리오 2 · 6에서 남의 정보 · 세션이 안 보이는가 | 신원이 한 곳에서 이어지나 (2) |
| 시나리오 3에서 신청 기록이 정확히 1건 인가 | 승인 · 멱등성이 서비스 경계에서도 지켜지나 (23차시) |
| 평가셋 점수가 단독보다 떨어진 항목 | 이어 붙인 경계의 문제 (4.2) |
| 5.4의 세 문장을 오피스봇에 대해 내 말로 설명할 수 있는가 | 과정 전체를 쥐었는가 |
부록 A. 스스로 확인하기
| # | 질문 | 답이 있는 곳 |
|---|---|---|
| 1 | API 서버를 유와 종차로 정의하면? 화면이 LLM을 직접 부르면 안 되는 이유는? | 1.1 |
| 2 | 오피스봇의 엔드포인트와 각각 이어지는 앞 차시는? | 1.2 |
| 3 | 로그인 신원이 도구 권한까지 이어지는 길은? | 2 |
| 4 | 스트리밍이 "빨라지는 것"이 아닌 이유는? | 3.1 |
| 5 | 멀티에이전트에서 진행 상황을 보여 주는 방법은? | 3.2 |
| 6 | 통합 테스트 시나리오 7개는? | 4.1 |
| 7 | 평가셋을 서비스 경유로 다시 돌리는 이유는? | 4.2 |
| 8 | 여섯 층과 여덟 축을 오피스봇으로 정산하면? | 5.1, 5.2 |
| 9 | 중심 축 위에서 3일이 어떻게 움직였나? | 5.3 |
| 10 | 세 문장을 오늘 어디서 마지막으로 확인했나? | 5.4 |
부록 B. 자주 하는 실수
| 실수 | 바로잡기 |
|---|---|
| 화면 코드에서 LLM API를 직접 호출 | ❌ 키가 노출되고 권한 검사를 못 합니다. 서버를 거치세요 |
| 요청 본문의 사번을 신원으로 사용 | ❌ 로그인 검증 함수 하나에서만 신원을 꺼내세요 |
| "스트리밍하면 빨라진다" | ⚠️ 완성 시간은 같습니다. 먼저 보여 줄 뿐입니다 |
| thread_id만 알면 승인 가능 | ❌ 소유자 검사를 코드로 |
| 정상 시나리오만 시험 | ⚠️ 권한 위반 · LLM 오류 · 동시 사용자를 꼭 넣으세요 |
| 단독 평가만 하고 서비스 경유 재실행은 생략 | ❌ 이어 붙인 경계에서 점수가 떨어집니다 |
| 운영에서 비용만 봄 | ⭕ 지연 · 실패율 · 추적(마스킹 포함)을 함께 보세요 |
부록 C. NotebookLM 복습 프롬프트
이 24차시 노트만 근거로 비전공자용 6문제를 내줘.
"신원이 도구 권한까지 이어지는 길", "스트리밍과 축 ⑥", "통합 테스트 시나리오", "중심 축의 여정", "세 문장 회수"는 반드시 포함해줘.
내가 답한 뒤에 채점하고, 틀린 이유를 노트의 절 번호를 근거로 설명해줘.
LangTailor