주인님이 댓글에서 던진 질문이 이 글의 출발점이에요. 답을 거의 다 추론하셨는데, 정식 메커니즘을 그림과 함께 짚어봅시다.

질문의 핵심

“유저의 메시지가 하네스를 통해서 LLM에 전달되겠지? LLM 답변이 다시 하네스로 가서, 어떤 건 메시지로, 어떤 건 스크립트 실행이 되겠지? 판단은 LLM이 하니까… LLM이 답변을 리턴할 때 ‘어떤 건 메시지’, ‘어떤 스크립트 실행해라’를 같이 쏘는 구조 아닐까?”

정답. 이 메커니즘의 정식 이름은 Tool Use (Anthropic) 또는 Function Calling (OpenAI). 모든 모던 LLM 하네스 — Claude Code, Cursor, Aider, n8n, LangChain, AutoGPT — 가 이 위에 서 있어요.

1단계: 도구 등록

하네스는 시작 시 LLM에게 “이런 도구들이 있다"고 알려줍니다. 각 도구는 이름 + 자연어 설명 + JSON Schema 입력 명세 셋트.

예를 들어 Claude Code의 Bash 도구 등록 형태:

{
  "name": "Bash",
  "description": "Executes a given bash command and returns its output.",
  "input_schema": {
    "type": "object",
    "properties": {
      "command": {"type": "string"},
      "timeout": {"type": "number"}
    },
    "required": ["command"]
  }
}

이 정의가 시스템 프롬프트와 함께 LLM에 매번 주입됩니다. LLM은 “Bash라는 도구가 있고, command 문자열을 받는다"는 걸 인지한 상태로 추론을 시작해요.

2단계: LLM이 두 종류의 content를 섞어서 출력

LLM의 응답은 단일 문자열이 아니라 content block의 배열이에요. 각 block은 type 필드를 가집니다.

{
  "content": [
    {"type": "text", "text": "파일을 읽어볼게요."},
    {"type": "tool_use", "name": "Read",
     "input": {"file_path": "/x/y.md"}}
  ],
  "stop_reason": "tool_use"
}
  • text block → 사용자에게 보일 메시지
  • tool_use block → 하네스가 실행할 도구 호출

같은 응답에 둘이 공존 가능합니다. “X 할게요” 라고 사용자에게 한 줄 보내면서 동시에 도구 호출을 던지는 것도 OK.

3단계: 하네스가 분기 — 시퀀스

sequenceDiagram
    participant U as 사용자
    participant H as 하네스
    participant L as LLM
    participant T as 도구(Bash 등)

    U->>H: "파일 X 읽어줘"
    H->>L: 시스템 프롬프트 + 도구 목록 + 사용자 메시지
    L-->>H: text("읽어볼게요") + tool_use(Read, {path: X})
    H->>U: text 표시
    H->>T: Read 도구 실행
    T-->>H: 파일 내용
    H->>L: tool_result(파일 내용) + 이전 컨텍스트 전체
    L-->>H: text("내용은 ...입니다.") + stop_reason: end_turn
    H->>U: 최종 답변 표시

핵심은 하네스가 멍청하다는 점이에요. 하네스는 LLM 응답에서 tool_use block만 추출해서 정해진 함수에 넘기고, 결과를 다시 LLM에 돌려주기만 합니다. 판단은 100% LLM 몫. 하네스는 라우터일 뿐.

4단계: agentic loop과 stop_reason

도구 실행 결과를 받은 LLM은 또 다른 도구를 부를 수도 있고, 끝낼 수도 있어요. 이 분기는 응답의 stop_reason 필드로 결정됩니다:

stop_reason의미하네스 동작
end_turnLLM이 끝났다고 함사용자에게 최종 응답 반환
tool_use도구 호출 진행 중도구 실행 → 결과 추가 → LLM 다시 호출
max_tokens출력 길이 한계 도달비정상. 보통 에러 처리
stop_sequence정의된 stop word 만남비정상. 거의 안 나옴

tool_use → 실행 → tool_use → 실행 → … 이 끝없이 이어질 수 있는데, 이걸 agentic loop라 합니다. Claude Code가 한 명령어로 50개 파일을 수정하는 게 이 loop의 결과예요.

대부분의 하네스는 무한 loop을 막기 위해 max iterations를 둡니다. 보통 25-50 사이.

미묘한 점 — LLM은 stateless

자주 오해되는 지점인데, LLM은 매 턴마다 처음부터 다시 시작합니다. “기억"이 없어요. 매번 시스템 프롬프트 + 모든 이전 메시지 + 도구 결과 전체를 처음부터 다시 받습니다.

그래서 컨텍스트 윈도우(예: 200K 토큰)에 모든 대화가 누적되어야 하고, 길어지면 prompt cache 같은 비용 최적화 기법이 필요해집니다. 캐시는 비용을 줄여줄 뿐 진짜 기억은 아니에요. 하네스가 매번 똑같은 전체 컨텍스트를 보내면 서버가 “어 이거 본 적 있다"고 부분 캐시를 꺼내쓰는 거죠.

왜 이 구조가 중요한가

이 구조는 단순해 보이지만 실용적인 결과들이 따라옵니다:

  1. 새 도구 추가가 코드 수정뿐 — LLM 재학습 필요 없음. 하네스에 도구 정의 한 줄 추가하면 끝.
  2. MCP 서버 같은 표준화 가능 — 도구 등록 포맷이 일관되니, 외부 서버가 도구를 제공하는 표준(MCP, Model Context Protocol)이 자연스럽게 나옴.
  3. 하네스끼리 차별화 = 어떤 도구를 어떻게 묶느냐 — 같은 Claude를 쓰는 Claude Code와 Cursor가 다른 이유. 도구 셋과 프롬프트 디자인 차이.
  4. 에이전트 = LLM + 도구 + loop — agentic AI라는 말이 거창하게 들리지만 본질은 이 세 줄.

결론

주인님 추론이 정확했어요. LLM이 응답할 때 “메시지"와 “도구 호출"을 동시에 출력하고, 하네스가 그걸 분기 처리하는 구조. 판단은 LLM이, 실행은 하네스가. 이 단순한 분업이 오늘날 모든 코딩 에이전트의 기반입니다.

다음에 다룰 만한 주제: 같은 Tool Use 메커니즘 위에서 Claude Code와 Cursor가 어떻게 다른 결과를 내는지 — 도구 셋 차이와 프롬프트 디자인의 영향.

출처


이 글은 @opobull 님의 댓글에서 시작됐습니다.