만든 Agent를 남에게 넘기려면 무엇이 더 필요할까요?

내 컴퓨터에서 잘 돌던 서비스를 동료에게 넘기면 첫날 어디선가 막힙니다. 내 컴퓨터에만 있던 설치 버전, 켜는 명령을 아는 사람은 나뿐이라는 점, 고쳤을 때 나아졌는지 확인할 방법이 없다는 점, 왜 이렇게 만들었는지 아무도 모른다는 점입니다.

그래서 코드 말고 네 가지를 더 넘깁니다. 실행 환경, 사용 설명서(README), 평가 결과, 설계 결정 기록(ADR)입니다. 아래에서 하나씩 갖춰 가며 받는 사람이 어디까지 가는지 보세요.

갖춘 것

막히는 이유는 이 글의 서비스에서 실제로 겪었거나 확인한 것입니다. 받는 사람의 첫날을 단순하게 그린 그림입니다.

서버를 둘로 나누면 의존성이 부딪치지 않습니다

이 서비스에는 의존성이 서로 부딪치는 곳이 있었습니다. MCP 서버를 만든 파이썬 SDK는 2점대이고 MCP 서버에 연결하는 langchain-mcp-adapters 0.3.2는 설치할 때 SDK 1점대를 끌고 옵니다. 한 환경에 둘을 설치하면 서버 코드의 MCPServer를 불러올 수 없었습니다.

컨테이너(container)는 프로그램과 필요한 라이브러리를 한 상자에 담아 어디서나 같게 실행하는 기술입니다. 티켓 서버와 API를 상자 둘로 나누고 주소(TICKETS_URL)로 이으면 각자 필요한 버전을 씁니다. 두 구성을 눌러 보세요.

compose.yml (두 컨테이너)yaml
services:
  tickets:                          # 1 MCP 2점대 SDK로 도는 티켓 서버
    build: { context: ., dockerfile: Dockerfile.tickets }
  api:
    build: { context: ., dockerfile: Dockerfile.api }
    environment:
      TICKETS_URL: http://tickets:8000/mcp    # 2 컨테이너 이름이 주소가 된다
      ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY} # 3 키는 이미지에 굽지 않는다
    ports: ["8010:8010"]
    depends_on: [tickets]
터미널
$ python ticket_server.py --http # 티켓 MCP 서버, 포트 8000 $ TICKETS_URL=http://127.0.0.1:8000/mcp uvicorn helpdesk:app --port 8010 $ curl -N -X POST localhost:8010/chat -d '{"thread_id":"z","message":"T-1042 상태 알려 줘"}' event: tool_call event: tool_result event: answer data: {"text": "T-1042는 부품 대기 중이고 담당은 kim입니다."}

출력은 예시입니다. 이 환경은 Docker 데몬이 꺼져 있어 이미지 빌드는 해 보지 못했습니다. 컨테이너 대신 두 프로세스를 따로 띄워 같은 구성(서버 하나, API 하나, 주소로 연결)이 동작하는 것을 확인했습니다. 모델은 모의 서버입니다.

  1. 서버를 따로 둡니다. 티켓 서버는 SDK 2점대만 설치한 이미지에서 --http 옵션으로 켭니다. 다른 서비스도 같은 서버를 주소로 쓸 수 있어 재사용에 좋습니다.
  2. 컨테이너 이름이 주소입니다. 같은 compose 안에서는 서비스 이름(tickets)으로 서로를 찾습니다. 다른 컴퓨터에서 접속하는 주소 이름(Host)이 바뀌어도 서버가 거절하지 않는 것을 확인했습니다.
  3. 비밀은 환경 변수로 넣습니다. API 키를 이미지 안에 굽지 않고 실행할 때 넘깁니다. 이미지를 공유해도 키가 새지 않습니다.

서비스 자체를 평가하면 서비스의 결함이 나옵니다

앞에서 Agent 하나를 평가했다면 이번에는 만든 서비스를 바깥에서 평가합니다. 서비스 주소로 질문을 보내 스트림 이벤트를 모아 기대한 도구 경로와 답이 나왔는지 봅니다. 사례는 지식 질문, 조회, 승인을 받는 생성, 거절하는 생성, 지식 질문 하나 더, 다섯 가지입니다.

처음 돌린 결과는 5건 중 2건 통과였습니다. 승인 사례 둘의 도구 경로가 어긋나 있었습니다. 원인은 평가가 아닌 서비스에 있었습니다. 승인 뒤 이어 가는 스트림이 이미 보낸 tool_call 이벤트를 한 번 더 내보냈습니다. 고치기 전과 고친 뒤를 눌러 보세요.

eval_service.pypython
CASES = [{"id": "faq-vpn", "msg": "VPN이 안 돼요", "path": ["search_kb"], "answer": "kb-vpn-01"}, ...]
async def stream(client, url, body):                       # 1 SSE를 이벤트 목록으로 모은다
    ...
for cs in CASES:
    ev = await stream(c, "/chat", {"thread_id": tid, "message": cs["msg"]})
    if any(n == "approval_required" for n, _ in ev):        # 2 승인 요청이 오면 답을 보낸다
        ev += await stream(c, "/approve", {"thread_id": tid, "approve": cs["approve"]})
    path = [d["name"] for n, d in ev if n == "tool_call"]    # 3 부른 도구 순서
    answer = next((d["text"] for n, d in ev if n == "answer"), "")
    ok = path == cs["path"] and cs["answer"] in answer       # 4 경로와 답
터미널
$ python eval_service.py # 고친 뒤 | 사례 | 경로 | 답 | 승인 요청 | | faq-vpn | ✓ | ✓ | - | | ticket-get | ✓ | ✓ | - | | ticket-create-ok | ✓ | ✓ | 있음 | | ticket-create-no | ✓ | ✓ | 있음 | | faq-paper | ✗ | ✗ | - | 통과 4/5 (고치기 전 2/5)

출력은 예시입니다. 모의 모델 서버로 실제 서비스를 평가한 결과를 줄였고 마지막 사례의 실패는 모의 서버가 문서 질문에 티켓 도구를 고르게 만든 것입니다. 실제 모델의 점수가 아닙니다.

  1. 스트림을 모읍니다. 서비스를 사용자처럼 부르고 이벤트 이름과 내용을 목록으로 만듭니다. 서비스 내부를 몰라도 평가할 수 있습니다.
  2. 승인 요청에 답합니다. 승인이 필요한 사례는 요청이 오면 미리 정한 답(승인 또는 거절)을 보내 이어 갑니다.
  3. 경로와 답을 봅니다. 부른 도구의 순서가 기대와 같은지, 답에 핵심 말이 있는지를 봅니다. 평가를 돌릴 때마다 대화 번호는 새로 만들고 티켓 서버의 데이터는 처음 상태로 돌려야 결과가 흔들리지 않습니다.

설명서와 결정 기록과 평가 결과를 문서로 남깁니다

README는 처음 온 사람이 십 분 안에 서비스를 켜 보게 하는 문서입니다. 무엇을 하는 서비스인지, 준비물, 켜는 명령, 시험해 보는 방법, 알려진 한계를 적습니다. 알려진 한계는 꼭 적습니다. 메모리 저장소라 서버를 다시 켜면 대화가 사라진다는 사실이 대표적입니다.

ADR(Architecture Decision Record, 설계 의사결정 기록)은 왜 그렇게 만들었는지를 한 장씩 남기는 문서입니다. 코드를 읽어서는 알 수 없는 이유를 남겨 나중에 같은 논쟁을 되풀이하지 않게 합니다. 세 문서를 눌러 보세요.

이 시리즈는 Agent가 무엇인지에서 시작해 직접 만든 루프, LangChain과 LangGraph, 여러 Agent, MCP, 평가와 보안을 거쳐 서비스까지 왔습니다. 마지막에는 코드보다 이 네 가지를 갖춰 남에게 넘길 수 있는 상태가 남습니다. 만든 Agent가 믿을 만한지는 점수, 기록, 이유로 설명할 수 있어야 합니다.

참고 자료

출처 4개 펼치기