5장. LLM 외부 도구 연동
ToolMessage 로 결과 돌려주기
은행 창구에서 번호표를 뽑으면, 일이 끝났을 때 직원이 "37번 손님"이라고 부릅니다. 번호가 있어야 여러 손님의 일이 섞이지 않습니다. 모델이 도구 호출을 여러 개 한꺼번에 요청하면 결과도 여러 개가 돌아오는데, 이때 번호표 노릇을 하는 것이 tool_call_id 입니다. 결과를 담는 메시지는 ToolMessage 이고, 요청에 붙어 있던 id 를 그대로 적어 돌려줍니다.
네 상자는 도구 호출 한 번에 메시지 목록에 쌓이는 차례입니다. 손님의 질문(human), 도구 호출 요청이 든 모델의 답(ai), 같은 요청 번호를 단 결과(tool), 결과를 읽은 모델의 최종 답(ai)입니다. 모델을 두 번째로 부를 때는 앞의 셋을 모두 보냅니다. 여기서 번호표가 tool_call_id, 창구에서 건네는 서류가 ToolMessage 입니다. 비유와 다른 점은, 번호가 맞지 않으면 은행은 다시 부르면 되지만 모델 서비스는 요청 번호와 짝이 맞지 않는 ToolMessage 가 오면 오류를 낸다는 것입니다.
메시지의 네 역할
| 역할(type) | 메시지 | 누가 만드나 | 이 장에서 담는 것 |
|---|---|---|---|
| system | SystemMessage | 우리 코드 | "가격은 반드시 도구로 확인한다" 같은 규칙 |
| human | HumanMessage | 손님 | 질문 |
| ai | AIMessage | 모델 | 글 답 또는 tool_calls |
| tool | ToolMessage | 우리 코드 | 도구 결과와 tool_call_id |
짧은 예시
도구에 tool_calls 의 요청 하나를 통째로 넘기면, LangChain 이 같은 id 를 단 ToolMessage 를 만들어 돌려줍니다. 모델은 부르지 않습니다.
from langchain_core.messages import AIMessage, HumanMessage
from langchain_core.tools import tool
PRICES = {"P01": 39, "P06": 33} # 예시라 상품 두 개의 가격(만 원)만 적어 둔다
@tool
def calc_price(code: str, adults: int, children: int = 0) -> str:
"""상품코드와 인원으로 총 여행 요금(만 원)을 계산한다. 아동 요금은 어른의 80% 이다."""
price = PRICES[code]
return f"{code} 어른 {adults}명, 아동 {children}명: {round(price * adults + price * 0.8 * children, 1)}만 원"
ai = AIMessage("", tool_calls=[{"name": "calc_price", "args": {"code": "P06", "adults": 2, "children": 2}, "id": "call_1"}])
messages = [HumanMessage("P06 에 어른 2명, 아이 2명이면 얼마예요?"), ai]
for call in ai.tool_calls:
# 인자 사전 대신 요청(call)을 통째로 넘기면 ToolMessage 가 돌아온다
result = calc_price.invoke(call)
print(type(result).__name__, "| id:", result.tool_call_id, "| 결과:", result.content)
messages.append(result)
print("둘째로 모델에 보낼 메시지:", [m.type for m in messages])ToolMessage | id: call_1 | 결과: P06 어른 2명, 아동 2명: 118.8만 원
둘째로 모델에 보낼 메시지: ['human', 'ai', 'tool']calc_price.invoke(call) 은 계산 결과 글을 ToolMessage 에 담고, 요청의 id(call_1)를 tool_call_id 에 옮겨 적었습니다. 메시지 목록은 human, ai, tool 의 세 개가 되었고, 이 목록을 그대로 모델에 보내면 모델이 결과를 읽고 최종 답을 씁니다.
코드가 의미하는 것
이 코드는 은행 창구에서 번호표(call_1)를 단 서류를 받아 서류철(메시지 목록)에 끼우는 것입니다. 요청을 통째로 도구에 넘기고, 돌아온 ToolMessage 를 메시지 목록에 붙입니다.
| 코드 | 하는 일 | 챗봇에서 뜻하는 것 |
|---|---|---|
PRICES = {...} 와 @tool def calc_price | 가격 두 개로 계산하는 작은 도구를 만든다 | 상품 표 대신 쓴 축소판입니다. 도구 안에서 무엇을 하든 모델에게 가는 것은 돌려준 글뿐입니다 |
messages = [HumanMessage(...), ai] | 질문과 도구 호출 요청을 목록에 담는다 | 둘째 모델 호출에 보낼 기록의 앞부분입니다 |
calc_price.invoke(call) | 요청을 통째로 넘겨 실행한다 | 인자 사전이 아니라 요청을 넘기면 LangChain 이 결과를 ToolMessage 에 담고 id 를 tool_call_id 에 옮겨 줍니다 |
messages.append(result) | 결과를 목록 끝에 붙인다 | 결과가 그 요청이 든 AIMessage 바로 뒤에 놓입니다. 놓는 순서도 짝 맞추기의 일부입니다 |
결과에서 읽을 것은 두 가지입니다.
- 번호표가 옮겨 붙었습니다. 결과의
id: call_1은 우리가 따로 적지 않았는데도 요청의 번호와 같습니다. 요청이 여럿이어도 이 번호로 결과를 가립니다. - 기록은 human, ai, tool 세 칸입니다. 그림의 네 상자 가운데 셋이 쌓였고, 남은 하나(최종 답 ai)는 이 목록으로 모델을 한 번 더 불러야 생깁니다.
주의할 점
- 요청이 여러 개면 결과도 요청마다 하나씩, 모두 돌려주어야 합니다. 하나라도 빠지면 모델 서비스가 "답하지 않은 요청이 있다"는 오류를 냅니다.
ToolMessage는 그 요청이 든AIMessage바로 뒤에 둡니다. 순서가 바뀌면 모델이 어느 요청의 결과인지 짝을 맞추지 못합니다.- 인자 사전(
{"code": "P06", ...})만 넘겨invoke하면ToolMessage가 아니라 결과 글만 돌아옵니다. 실습 1에서 이렇게 도구를 직접 시험해 봅니다.