식당 키오스크에서 메뉴 이름만 "세트"라고 적혀 있고 설명이 없으면 무엇을 눌러야 할지 모릅니다. 사진과 구성 설명이 있어야 원하는 메뉴를 고릅니다.
모델이 도구를 고를 때도 같은 일이 생깁니다. 모델은 함수 코드를 볼 수 없고 도구의 이름과 설명과 인자 설명만 읽고 고릅니다. 모델이 엉뚱한 도구를 부르면 모델의 성능부터 의심하기 쉽지만 먼저 도구의 설명서를 확인해야 합니다.
그림은 도구마다 설명이 차지하는 범위를 원으로 그렸습니다. 질문(Q)이 원 두 개 안에 들어가면 그 둘이 후보(질문에 쓸 수 있어 보이는 도구)가 되어 모델은 둘 사이에서 망설입니다.
도구 설명서에 적힌 설명(모델이 읽는 글)
LangChain에서는 함수 위에 @tool을 붙이면 도구가 됩니다. 이때 모델에게는 도구의 설명서가 갑니다. 이름과 설명과 인자 설명을 JSON으로 적은 도구 정의입니다. 함수 이름은 도구 이름이 되고 docstring(함수 맨 위의 설명 글)은 도구 설명이 되며 타입 힌트(ticket_id: str 같은 변수 종류 표시)는 인자의 종류가 됩니다.
그림에서 같은 색 부분끼리 짝입니다. 설명서의 input_schema는 인자의 모양을 적는 칸이고 required는 꼭 넣어야 하는 인자 목록입니다. 설명서는 요청에 실제로 실리는 도구 정의를 줄여 옮긴 것입니다.
설명서는 langchain 1.4와 langchain-anthropic 1.7로 실제 요청을 확인해 줄여 옮긴 것입니다. docstring의 Args 칸은 parse_docstring=True를 줄 때 인자 설명으로 읽힙니다.
from langchain.tools import tool
# 1 이름: 함수 이름
# 2 설명: docstring 첫 문단
# 3 인자 설명: Args 칸
# 4 인자 종류: 타입 힌트
@tool(parse_docstring=True)
def get_ticket(ticket_id: str) -> str:
"""티켓 번호로 처리 상태와 도착 예정일을 조회한다.
번호를 알 때만 쓴다. 번호를 모르면 list_my_tickets를 먼저 쓴다.
Args:
ticket_id: T-1042 같은 티켓 번호.
"""
return lookup(ticket_id)get_ticket처럼 동사로 시작하고 영문 소문자와 밑줄로 쓰면 회사마다 호환이 잘 된다고 LangChain 문서가 권합니다. 다른 이름을 쓰고 싶으면 @tool("이름")으로 바꿉니다.Args: 칸에 적으면 인자마다 붙습니다. 값의 예시(T-1042)를 함께 적으면 모델이 형식을 맞춰 씁니다.설명이 겹치는 도구가 여럿이면 모델은 어느 것을 불러야 할지 헷갈립니다. Anthropic은 도구를 만들 때 겹치는 도구를 합치거나 이름 앞에 서비스 이름을 붙여 구분하라고 권합니다. 버튼으로 두 방법을 비교해 보세요.
도구가 돌려준 결과는 모델이 다음 호출 때 읽는 글입니다. 데이터베이스 한 줄을 통째로 돌려주면 모델이 쓰지 않을 칸까지 읽을 글이 되어 컨텍스트(모델이 한 번에 볼 수 있는 글의 양)를 차지합니다.
Anthropic은 도구가 모델에게 꼭 필요한 정보만 돌려주라고 권하고 Claude Code에서는 도구 응답을 기본 25,000토큰으로 제한한다고 밝혔습니다. 티켓을 여러 개 조회할 때 응답 방식에 따라 컨텍스트가 얼마나 차는지 확인해 보세요.
설명을 위한 예시 값입니다. 통째로 돌려주면 티켓 한 줄에 600토큰, 필요한 칸만 돌려주면 40토큰이라고 가정했고 유리 상자는 20만 토큰 창 기준입니다.
직접 짠 루프에서는 도구 오류를 결과로 돌려주는 코드를 우리가 넣었습니다. create_agent는 기본 설정이 다릅니다. 모델이 인자를 잘못 써서 도구 함수 안에서 오류가 나면 그 예외가 그대로 밖으로 나가 Agent가 멈춥니다. 직접 확인했습니다.
오류를 모델에게 돌려주려면 도구 호출을 감싸는 훅(hook, 중간에 끼워 넣는 코드)을 하나 답니다. @wrap_tool_call로 예외를 잡아 오류 표시가 붙은 ToolMessage(도구 결과를 담는 메시지)를 만들어 돌려주면 모델이 오류 문장을 읽고 인자를 고칩니다. 오류 문장에는 무엇이 틀렸고 어떻게 고치는지 적어야 합니다.
설명을 위한 예시 장면입니다. 두 쪽 모두 모델이 처음에 get_ticket("T1042")처럼 틀린 번호를 씁니다.
# 5 도구가 실패하면 예외 대신 오류 결과로 바꾼다
@wrap_tool_call
def tool_errors(request, handler):
try:
return handler(request)
except Exception as e:
return ToolMessage(
content=str(e), status="error",
tool_call_id=request.tool_call["id"])
# 6 에이전트에 끼워 넣는다
agent = create_agent(model, tools=[get_ticket],
middleware=[tool_errors])
# 7 도구 결과의 상태와 최종 답을 찍는다
result = agent.invoke({"messages": [{"role": "user", "content": "내 티켓 상태 알려줘"}]})
for m in result["messages"]:
if m.type == "tool":
print(m.status, m.content)
print(result["messages"][-1].content)출력은 예시 답입니다. 코드는 langchain 1.4 기준이고 import와 모델 준비는 생략했습니다.
handler(request)가 도구를 실제로 실행합니다. 그 안에서 예외가 나면 except가 받아 오류 문장을 담은 ToolMessage를 대신 돌려줍니다. status="error"는 이 결과가 실패라는 표시입니다.middleware는 에이전트가 도는 중간에 끼워 넣는 코드 목록입니다. 도구 호출 앞뒤를 감싸는 훅은 이 목록에 넣습니다. 훅을 더 자세히 쓰는 법은 이후 글에서 다룹니다.result["messages"]에는 주고받은 메시지가 순서대로 들어 있습니다. 도구 결과 메시지의 status가 error인지 success인지 찍어 보면 오류가 한 번 났다가 고쳐진 것이 보입니다.설명을 위한 예시 데이터입니다. 모델이 쓴 요청서는 사용자가 바뀌어도 같고 결과만 달라집니다.
"내 티켓 목록"을 보려면 도구가 로그인한 사용자를 알아야 합니다. 이 값을 모델이 인자로 채우게 하면 모델이 다른 사람의 이름을 쓸 수도 있습니다. 모델이 쓴 값은 틀릴 수 있으니 권한이 걸린 정보는 모델 밖에서 받는 편이 안전합니다.
도구 함수에 runtime: ToolRuntime 인자를 달면 호출할 때 코드가 넘긴 context를 꺼낼 수 있습니다. LangChain 문서에 따르면 이 인자는 모델에게 숨겨집니다. 위 설명서 그림에서 runtime이 벽에 막힌 것과 같습니다.
from dataclasses import dataclass
from langchain.tools import tool, ToolRuntime
# 8 로그인한 사용자 정보의 모양
@dataclass
class UserContext:
user_id: str
# 9 runtime 인자는 모델에게 보이지 않는다
@tool
def list_my_tickets(runtime: ToolRuntime[UserContext]) -> str:
"""로그인한 사용자가 연 티켓 번호 목록을 돌려준다."""
return ", ".join(find_tickets(runtime.context.user_id))
# 10 만들 때 모양을, 부를 때 값을 넘긴다
agent = create_agent(model, tools=[list_my_tickets],
context_schema=UserContext)
result = agent.invoke(messages, context=UserContext(user_id="kim"))
print(result["messages"][-1].content)출력은 예시 답입니다. 사용자가 lee면 도구가 T-1043을 돌려주고 답도 그에 맞게 바뀝니다.
@dataclass는 값을 담는 클래스를 짧게 쓰게 해 주는 Python 기능이고 이 클래스가 곧 context_schema(context의 모양 설명)입니다.runtime인 인자는 예약된 이름이라서 모델 설명서에서 빠지고 호출 때 LangChain이 채워 줍니다. runtime.context로 값을 읽습니다.create_agent에는 모양(context_schema)을 주고 invoke에는 실제 값(context=)을 줍니다. 값을 바꿔 호출하면 같은 Agent가 사용자마다 다른 결과를 냅니다.모델은 코드가 아니라 설명서를 읽고 도구를 고릅니다.
이름, 설명, 결과, 오류, 사용자 정보까지 다섯 곳이 모두 설명서의 일부입니다. 다음 글에서는 이 도구를 Agent에 연결해 루프를 맡기는 create_agent를 알아보겠습니다.