에이전트 애플리케이션이 작을 때는 화면에서 모델을 직접 호출하고 데이터 접근 코드까지 같은 프로세스에 넣는 편이 빠르다. 문제는 데모가 업무 시스템으로 자라기 시작할 때 드러난다. UI 변경이 실행 로직에 영향을 주고, 인증 문맥이 여러 함수 인자로 흩어지며, 모델이 어떤 권한으로 데이터를 읽었는지 설명하기 어려워진다.
이 프로젝트는 기술 이름보다 책임을 기준으로 경계를 나눈다. Streamlit은 사용자 경험, FastAPI는 공개 계약과 실행 수명주기, 에이전트 런타임은 계획과 도구 선택, MCP 서비스는 업무 규칙과 데이터 소유권을 담당한다.
프런트엔드는 백엔드 패키지를 가져오지 않는다
apps/frontend/app.py는 FastAPI와 HTTP로만 대화한다. 데이터베이스 모델이나 에이전트 런타임을 import하지 않는다. 이 원칙은 단순한 코드 정리가 아니다. 프런트엔드가 우연히 서비스 계층을 건너뛰어 데이터를 읽거나 쓰는 경로를 막는다.
프런트엔드가 알아야 하는 것은 제한적이다.
- 대화 요청을 어느 API에 보낼지
- SSE 이벤트를 어떻게 화면 상태로 바꿀지
- 승인이 필요할 때 어떤 인자를 보여줄지
- 승인 또는 거절을 어느 엔드포인트로 보낼지
그 밖의 인증 해석, 도구 구성, 체크포인트 재개는 API 뒤에 남는다. 나중에 Streamlit을 다른 웹 UI로 교체해도 업무 경계는 바뀌지 않는다.
API는 요청의 수명주기를 소유한다
FastAPI 계층은 스레드 생성과 조회, 메시지 저장, SSE 스트리밍, 승인과 거절을 관리한다. 요청마다 사용자와 tenant 문맥을 만들고 이 값을 MCP 헤더로 전달한다. 에이전트에게 단순 문자열 프롬프트로 tenant를 알려주는 데서 끝내지 않는다.
API가 공개 계약을 소유하면 UI와 런타임 사이의 변화 속도를 분리할 수 있다. 에이전트 라이브러리의 내부 이벤트가 바뀌어도 브라우저에 보내는 이벤트 종류는 progress, citation, approval_required, final, error처럼 안정적인 형태로 유지할 수 있다.
에이전트는 능력별 도구만 받는다
런타임은 MCP에서 발견한 도구를 이름별로 나눈 뒤 전문 에이전트에 allowlist로 전달한다. 계정 분석가는 고객 기록 도구만, 정책 조사자는 지식 검색 도구만 받는다. 쓰기 도구인 create_followup_task는 supervisor에 남고 interrupt 대상이 된다.
account_tools = [get_customer, list_open_cases, get_case]
policy_tools = [search_policy_documents, get_policy_document]이렇게 하면 전문 에이전트의 프롬프트가 잘못되더라도 애초에 보유하지 않은 도구를 호출할 수 없다. 역할 설명은 모델의 행동을 유도하고, 도구 목록은 가능한 행동의 상한을 정한다.
서비스 분리는 배포 개수와 같지 않다
경계가 있다고 해서 모든 모듈을 별도 서버로 만들어야 하는 것은 아니다. 이 레퍼런스에서는 Records와 Knowledge를 MCP 서비스로 분리하지만, 공통 계약과 설정은 패키지로 공유한다. 분리의 목적은 네트워크 호출을 늘리는 것이 아니라 권한과 데이터 소유권을 명확히 하는 것이다.
좋은 경계에는 다음 특징이 있다.
- 우회 경로 없이 한 책임을 수행한다.
- 입력과 출력이 타입으로 정의된다.
- 인증 문맥이 경계를 통과할 때 사라지지 않는다.
- 실패했을 때 어느 계층이 복구하거나 중단할지 분명하다.
- UI를 교체해도 업무 규칙이 유지된다.
초기 프로젝트라면 먼저 한 프로세스 안에서 모듈 경계를 만들고, 보안이나 확장 요구가 실제로 생긴 지점만 서비스로 분리해도 된다. 중요한 것은 서비스 수가 아니라 모델에서 데이터까지 이어지는 권한 경로를 한 장에 그릴 수 있는가이다.