1 Tool Binding이란
Tool Binding은 LLM이 응답 생성 중에 외부 함수(도구)를 동적으로 호출할 수 있도록, 함수의 시그니처·설명·입력 스키마를 LLM에 사전 등록하는 기법이다.
- 핵심 구성: Tool 정의(name, description, args_schema, function) + bind_tools(LLM에 등록)
- 목적: 정적 파이프라인의 한계(분기 매번 코드 수정)를 넘어 LLM 추론으로 도구를 선택
- 호환 모델: OpenAI Function Calling, Anthropic Tool Use, Azure OpenAI tool_choice
Tool Binding은 LangChain의 bind_tools나 LangGraph의 ToolNode로 구현된다. LLM은 응답 본문 대신 도구 호출 명령(tool call) 을 반환할 수 있고, 호스트 코드는 그 명령을 실제 함수 호출로 디스패치한다.
2 정적 파이프라인의 한계 (MINERVA 현재 상태)
MINERVA의 DataStandardizerAgent는 supervisor 패턴을 이미 적용했지만, 하위 에이전트 호출 순서는 코드에 고정되어 있다.
# src/agents/data_standardizer/agent.py (현재)
def run(self, query: Query) -> Response:
recommender = self._ensure_recommender()
mode = _resolve_mode(query) # 1. 모드 결정 (정적 분기)
raw_text, docs = recommender.run(query.text, mode=mode) # 2. RAG 추천 (항상 실행)
processed = _apply_post_processing(raw_text, ...) # 3. 후처리 (항상 실행)
if _is_full_format(processed): # 4. 조건부 감사
processed = self._auditor.audit_and_fix(processed)
return self._build_response(processed, docs, query, ...)이 구조의 문제:
| 문제 | 결과 |
|---|---|
| 새 도구 추가 시 코드 수정 필수 | 도메인 우선순위 변경, 약어 사전 추가 등 운영 변경마다 PR |
| 무조건 호출 | 코드 모드인데 도메인 분류가 불필요해도 ALBERT 200MB 모델 로드 |
| 분기 폭증 | mode 외에 사용자 의도(요약/표준화/감사만/예외 처리)별 if-else가 누적 |
| LLM 강점 미활용 | “감사가 필요한지”, “어떤 도구부터 호출할지”는 본질적으로 추론 문제 |
3 왜 Tool Binding이 필요한가
Tool Binding은 분기 책임을 코드에서 LLM으로 이동시킨다. 사용자가 “이 표에서 도메인 그룹만 다시 검토해줘”라고 요청하면 LLM이 RAG 추천을 건너뛰고 도메인 감사만 호출한다. 이때 코드는 도구를 등록하기만 하고, 호출 순서를 결정하지 않는다.
정적 파이프라인 vs Tool Binding 비교.
| 측면 | 정적 파이프라인 (현재) | Tool Binding (목표) |
|---|---|---|
| 도구 호출 결정 | 코드의 if/else | LLM 추론 |
| 새 도구 추가 비용 | 코드 수정 + 라우팅 분기 | 도구 등록만 |
| 부분 실행 | 별도 mode/flag 추가 필요 | LLM이 필요한 도구만 선택 |
| 실패 처리 | 매 분기마다 try/except | ToolNode 통합 핸들러 |
| 관측성 | 메서드 호출 수동 로깅 | tool_call 자동 기록 |
| 비결정성 | 입력 같으면 결과 같음 | LLM 판단에 따라 변동 |
마지막 항목이 trade-off다. Tool Binding은 유연성을 얻는 대신 결정성을 잃는다. 따라서 모든 호출을 LLM에 위임할 필요는 없고, 반드시 거쳐야 하는 단계는 코드 분기로 두고 선택적 단계만 도구로 노출한다.
4 MINERVA의 도구 후보
현재 sub_agent 메서드를 도구 후보로 분해한다. 각 도구는 단일 책임이어야 하고, 입출력이 Pydantic 스키마로 정의 가능해야 한다.
| 도구 이름 | 책임 | 입력 | 출력 |
|---|---|---|---|
search_standardization_docs |
표준화 원칙 RAG | query: str, mode: “data”|“code”, k: int | docs: list[Document], context: str |
classify_domain_group |
ALBERT 도메인 분류 | term_korean: str | (group: str, confidence: float) |
generate_physical_name |
영문 약어 → 물리명 | logical_name: str, domain: str | physical_name: str |
audit_domain_priority |
도메인 우선순위 감사 | table_md: str, priority_yml_path: str | corrections: list[dict] |
inject_docstring |
코드에 docstring 주입 | code: str, mapping: dict | enriched_code: str |
compose_full_format |
단건 3섹션 포맷 조립 | answer: str, citations: list, agent_data: dict | formatted: str |
이 6개로 현재 data_standardizer 파이프라인을 모두 커버한다. recommender.run()은 search_standardization_docs + LLM 호출 자체로 분해되며, _apply_post_processing은 classify_domain_group + generate_physical_name의 반복 호출로 표현된다.
5 Tool Schema 정의
LangChain v1의 @tool 데코레이터로 정의하면 함수 시그니처와 docstring이 자동으로 LLM 프롬프트에 변환된다.
# 주요 패키지 (2026년 기준)
# langchain >= 1.1, langchain-openai >= 1.1, langgraph >= 1.0
from langchain_core.tools import tool
from pydantic import BaseModel, Field
from typing import Literal
class ClassifyDomainArgs(BaseModel):
term_korean: str = Field(description="분류할 한글 논리명 (예: '환자식별번호')")
@tool(args_schema=ClassifyDomainArgs)
def classify_domain_group(term_korean: str) -> dict:
"""한글 논리명을 ALBERT 분류기로 도메인 그룹에 매핑한다.
반환: {"group": "환자정보", "confidence": 0.87}
confidence < 0.6 이면 group="미분류" 로 반환한다.
"""
from agents.data_standardizer.sub_agents.domain_classifier import classify
group, conf = classify(term_korean)
return {"group": group, "confidence": conf}핵심 원칙:
- docstring이 LLM이 보는 설명이다. 도구 사용 조건과 반환 의미를 명확히 적는다
- args_schema는 LLM이 채우는 입력이다. Field description으로 예시를 준다
- 반환은 직렬화 가능한 dict/str이어야 한다. Pydantic 모델이면
.model_dump()로 변환한다
6 ToolNode와 ReAct 루프
LangGraph의 create_react_agent는 LLM 노드와 ToolNode를 자동으로 연결한다.
from langchain_openai import AzureChatOpenAI
from langgraph.prebuilt import create_react_agent
from dotenv import load_dotenv
load_dotenv()
llm = AzureChatOpenAI(model="gpt-4.1", temperature=0.0)
tools = [
search_standardization_docs,
classify_domain_group,
generate_physical_name,
audit_domain_priority,
inject_docstring,
compose_full_format,
]
agent = create_react_agent(
model=llm,
tools=tools,
state_schema=DataStandardizerState, # C8 (State 설계) 참조
prompt="당신은 데이터 표준화 도우미다. ...",
)
# 호출
result = agent.invoke({"messages": [{"role": "user", "content": "환자식별번호의 표준 메타데이터를 추천해줘"}]})내부적으로 일어나는 일:
사용자 질문 → LLM 노드 (도구 호출 결정)
│
├─ tool_calls=[search_standardization_docs(...)] → ToolNode 실행 → 결과
│ └─ LLM 노드 재호출 (도구 결과 반영)
│
├─ tool_calls=[classify_domain_group(...), generate_physical_name(...)] → 병렬 실행
│ └─ LLM 노드 재호출
│
├─ tool_calls=[audit_domain_priority(...)] → 단일 실행
│ └─ LLM 노드 재호출
│
└─ tool_calls=[] → 종료, 최종 응답 반환
tool_calls가 비어 있으면 ReAct 루프가 종료된다. 이 종료 조건을 LLM이 결정한다는 점이 정적 파이프라인과의 가장 큰 차이다.
7 강제 호출과 선택적 호출
모든 도구를 LLM에 맡기면 비용·지연·예측 가능성이 모두 악화된다. 두 가지 패턴으로 통제한다.
7.1 사전 도구 강제 (tool_choice)
표준화 답변에 RAG 인용이 반드시 필요하다면 첫 도구는 강제로 검색을 호출한다.
7.2 노드 분리 (필수 단계는 코드, 선택 단계만 ToolNode)
from langgraph.graph import StateGraph, END
builder = StateGraph(DataStandardizerState)
builder.add_node("search", search_node) # 강제 — 코드 호출
builder.add_node("agent", agent_node) # ReAct 루프
builder.add_node("tools", ToolNode(tools=optional_tools)) # 선택적 도구만
builder.set_entry_point("search")
builder.add_edge("search", "agent")
builder.add_conditional_edges("agent", should_continue, {"tools": "tools", END: END})
builder.add_edge("tools", "agent")
graph = builder.compile()search_node는 항상 실행되고, audit_domain_priority나 inject_docstring은 LLM이 필요하다고 판단할 때만 호출된다. 이 하이브리드 구조가 MINERVA 같은 실무 시스템에 적합하다.
8 실패 처리
도구 호출은 외부 의존(ALBERT 모델, RAG 인덱스, sg-data-standardization 패키지)을 포함하므로 실패 가능성이 높다. ToolNode의 기본 동작은 예외를 ToolMessage(error 표시)로 변환해 LLM에 다시 전달하는 것이다.
@tool
def classify_domain_group(term_korean: str) -> dict:
"""..."""
try:
group, conf = classify(term_korean)
return {"group": group, "confidence": conf}
except FileNotFoundError as e:
# ALBERT 모델 미로드 — LLM에 명시적으로 알린다
return {"error": "domain_classifier_unavailable", "fallback": "skip"}
except Exception as e:
return {"error": str(e), "fallback": "retry"}LLM은 이 에러 메시지를 읽고 포기·재시도·다른 도구 사용을 결정한다. 이 점이 정적 파이프라인의 try/except와 다르다 — 정적 코드는 미리 정의한 fallback만 실행하지만, LLM은 컨텍스트를 보고 동적으로 결정한다.
9 관측성 통합
C9(Phase C-1: 에러 전파)와 metrics_logger를 도구 호출에도 적용한다.
@tool
def classify_domain_group(term_korean: str) -> dict:
"""..."""
start = time.time()
try:
result = classify(term_korean)
return {"group": result[0], "confidence": result[1]}
finally:
log_tool_call(
tool_name="classify_domain_group",
input_size=len(term_korean),
latency_ms=int((time.time() - start) * 1000),
success=True, # 위 try 결과로 분기
)운영에서 보고 싶은 지표:
- 도구별 호출 빈도 (어떤 도구가 LLM에 가장 자주 선택되는가)
- 도구별 평균 지연 (병목 식별)
- 도구별 에러율 (외부 의존 안정성)
- 사용자 1명당 도구 호출 횟수 (ReAct 루프가 너무 길지 않은지)
이 데이터는 runs.jsonl의 extras 필드에 도구 호출 배열로 누적되며, monitoring 라우터의 별도 지표로 노출된다.
10 응용 분야
Tool Binding이 MINERVA에 가져올 변화를 시나리오로 정리한다.
| 시나리오 | 정적 파이프라인 동작 | Tool Binding 동작 |
|---|---|---|
| 표만 다시 감사해달라는 요청 | 전체 RAG → 후처리 → 감사 (RAG·후처리 낭비) | LLM이 audit_domain_priority만 호출 |
| 코드 입력에 도메인 분류 불필요 판단 | 항상 ALBERT 로드 (200MB) | LLM이 코드 모드에서 classify_domain_group 생략 |
| 신규 도구(약어 사전 검색) 추가 | agent.py 분기 추가 + 테스트 수정 | @tool 데코레이터로 등록만 |
| 사용자 의도가 모호 (“정리해줘”) | 의도 분류 코드 추가 | LLM이 도구 시퀀스를 동적 구성 |
| 부분 실패 (ALBERT 다운) | 전체 요청 실패 또는 하드코딩 fallback | LLM이 에러 메시지 보고 검색 도구로 우회 |
11 마이그레이션 전략
기존 DataStandardizerAgent를 한 번에 교체하지 않는다. C10(BaseAgent v2)에서 정의한 v1/v2 어댑터를 활용해 점진적으로 전환한다.
- 도구 추출:
sub_agents/post_processing/tables.py의 함수들을@tool로 래핑. 기존 호출 경로는 유지 - 이중 실행 (shadow mode): 정적 파이프라인 결과를 정답으로 두고, ToolNode 결과를 비교 로깅. 차이가 임계값 이하가 될 때까지 튜닝
- A/B 실험으로 전환: arm A=정적, arm B=ToolNode. citation rate·latency·만족도를 비교. 유의미하게 같거나 더 나으면 ToolNode로 흡수
- 정적 분기 제거: A/B에서 승리한 후 기존 if-else를 삭제. 도구 정의만 남는다
이 4단계는 experiments.py(C-4 A/B 프레임워크)와 metrics_logger.py(C-9 관측성)를 그대로 재활용한다. 즉 Phase C-3 전환은 새 인프라를 요구하지 않고 기존 인프라 위에서 진행된다.
12 관련 주제
선행 지식 (같은 시리즈)
- BaseAgent 계약 v2 – LangGraph 호환 인터페이스
- LangGraph 기초 – StateGraph, Node, Edge
- State 설계 – TypedDict와 reducer
후속 주제 (Phase C-3)
- ReAct 루프 – Reasoning과 Acting의 반복으로 풀어내기 – Tool Binding 위에서 LLM이 도구 시퀀스를 동적 결정
- 멀티스텝 플래닝 – Plan-and-Execute – 사전 계획 후 실행 (작성 예정)
- 에이전트 위임 – Supervisor가 sub_agent를 호출하는 패턴 – Supervisor → sub-agent (작성 예정)
다른 카테고리 연결
- Bind-Tools (LangChain v1) – 도구 바인딩 기본 문법
- LangGraph ToolNode – ToolNode 동작 원리
- LangGraph Agentic RAG – 도구 바인딩이 RAG에 들어간 첫 응용
- ReAct Agent (LangChain v1) – create_react_agent 사용법
- MCP 기반 도구 통합 – 외부 도구를 표준 프로토콜로 노출