ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • bind_tools — LLM이 도구를 인식하는 방법
    IT 2026. 6. 26. 21:00
    bind_tools — LLM이 도구를 인식하는 방법

    LLM은 기본적으로 텍스트 생성기다. 입력을 받아 다음에 올 확률이 높은 토큰을 순서대로 출력하는 것이 전부다. "계산기를 호출하라"거나 "데이터베이스를 조회하라"는 기능은 처음부터 없다. 그렇다면 LangGraph에서 에이전트가 도구를 사용할 수 있는 이유는 무엇일까? 답은 bind_tools에 있다.

    LLM은 도구를 어떻게 알게 되는가

    정확히 말하면, LLM이 도구를 "알게" 되는 것이 아니다. bind_tools(tools)는 도구 목록의 스키마—이름, 설명, 파라미터—를 LLM 호출 시 프롬프트에 자동으로 끼워 넣는다. LLM 입장에서는 그냥 더 긴 프롬프트를 받는 것이다.

    차이는 응답 형태에 있다. 스키마를 받은 LLM은 일반 텍스트 대신 JSON 구조의 tool_calls 필드를 포함한 응답을 내놓을 수 있다. "이 도구를 이 인자로 호출하겠다"는 의도를 구조화된 형태로 표현하는 것이다.

    diagram

    위 흐름에서 핵심은 invoke 호출 시 프롬프트에 도구 스키마가 자동 삽입되는 단계다. bind_tools가 하는 일은 모델 객체에 도구 목록을 바인딩해두는 것이고, 실제 스키마 주입은 invoke가 호출되는 시점에 일어난다. LLM 자체가 변하는 게 아니라, LLM에 보내는 요청이 바뀐다. 놓치기 쉬운 함정은 bind_tools 후에도 모든 응답이 tool_calls를 포함하지는 않는다는 것이다. LLM이 판단하기에 도구 호출이 불필요하면 일반 텍스트로 응답한다.

    bind_tools 전후 응답 비교

    같은 질문이라도 bind_tools 적용 여부에 따라 LLM의 응답 구조가 달라진다.

    diagram

    도구 없이 호출하면 LLM이 직접 계산해 텍스트로 답한다. 계산이 간단하면 맞을 수도 있지만, 복잡한 연산이나 실시간 데이터가 필요한 경우 LLM이 틀린 답을 자신 있게 내놓는 환각이 발생할 수 있다.

    diagram

    bind_tools 이후에는 LLM이 직접 답하는 대신 "multiply 도구를 a=3, b=7로 호출하겠다"는 의도를 반환한다. content는 비어 있고, tool_calls 필드에 호출 계획이 담긴다. 이 응답 자체는 실행이 아니다—실행은 ToolNode가 담당한다. 가장 흔한 오해는 "AIMessage가 왔으니 계산이 완료됐다"고 착각하는 것인데, tool_calls가 있으면 아직 절반만 처리된 상태다.

    ToolNode와 tool_calls의 연결

    diagram

    ToolNode는 AIMessage의 tool_calls 필드를 읽어, 해당 이름의 함수를 찾아 args로 실제 호출한다. 결과는 ToolMessage로 감싸져 messages 리스트에 추가된다. 이후 agent 노드가 다시 호출될 때 LLM은 도구 실행 결과를 포함한 전체 대화 맥락을 보고 최종 텍스트 답변을 생성한다. tools_condition이 라우팅 분기를 맡아, tool_calls 유무로 "도구 실행" 또는 "종료" 경로를 선택한다. 놓치기 쉬운 함정은 ToolNode가 tools 리스트를 알아야 한다는 점이다—ToolNode(tools)로 초기화할 때 bind_tools에 넘긴 것과 동일한 리스트를 사용해야 한다.

    코드로 보는 전체 구조

    llm_with_tools = model.bind_tools(tools)  # 도구 스키마를 LLM 호출에 자동 주입하기 위한 설정
    
    def agent_node(state: AgentState):
        return {"messages": [llm_with_tools.invoke(state["messages"])]}
    
    # 직접 확인 — tool_calls 유무로 "도구 호출 의도 vs 일반 응답" 구분
    result = llm_with_tools.invoke([HumanMessage(content="3 곱하기 7은?")])
    print(result.tool_calls)
    # [{"name": "multiply", "args": {"a": 3, "b": 7}, "id": "..."}]
    

    agent_node의 구조는 단순하다: state에서 messages를 꺼내 llm_with_tools에 전달하고, message로 응답한다. 노드 자체는 tool_calls 유무를 신경 쓰지 않는다—분기 판단은 tools_condition이 담당하기 때문이다. 이 역할 분리가 LangGraph 에이전트를 이해하는 핵심이다. 한 가지 주의점: result.tool_calls는 리스트이므로, 하나의 응답에 여러 도구 호출이 동시에 담길 수 있다. ToolNode는 이 경우 병렬로 실행한다.

    bind_tools가 없으면 어떻게 되는가

    bind_tools 없이 일반 model을 그래프에 사용하면, LLM이 tool_calls를 생성하지 않으므로 tools_condition은 항상 END로 라우팅한다. 도구가 존재하더라도 실행되지 않는다. 에러가 발생하는 것이 아니라 그냥 LLM이 직접 텍스트로 답하기 때문에, 디버깅이 어려울 수 있다. 그래프가 "돌아가는 것처럼 보이지만 도구를 전혀 쓰지 않는" 상황이 된다.

    결국 bind_tools는 LLM과 도구 사이의 인터페이스 계약이다. 스키마를 주입해 LLM이 "이런 도구를 호출할 수 있다"는 것을 알게 하고, tool_calls 형태로 의도를 표현하도록 유도한다. 실제 실행은 항상 ToolNode가 담당한다.


    이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.

Designed by Tistory.