모델이 도구를 잘못 고르는 이유는 설명서에 있다

식당 키오스크에서 메뉴 이름만 "세트"라고 적혀 있고 설명이 없으면 무엇을 눌러야 할지 모릅니다. 사진과 구성 설명이 있어야 원하는 메뉴를 고릅니다.

모델이 도구를 고를 때도 같은 일이 생깁니다. 모델은 함수 코드를 볼 수 없고 도구의 이름과 설명과 인자 설명만 읽고 고릅니다. 모델이 엉뚱한 도구를 부르면 모델의 성능부터 의심하기 쉽지만 먼저 도구의 설명서를 확인해야 합니다.

그림은 도구마다 설명이 차지하는 범위를 원으로 그렸습니다. 질문(Q)이 원 두 개 안에 들어가면 그 둘이 후보(질문에 쓸 수 있어 보이는 도구)가 되어 모델은 둘 사이에서 망설입니다.

설명
질문

도구 설명서에 적힌 설명(모델이 읽는 글)

함수 하나가 설명서 한 장이 됩니다

LangChain에서는 함수 위에 @tool을 붙이면 도구가 됩니다. 이때 모델에게는 도구의 설명서가 갑니다. 이름과 설명과 인자 설명을 JSON으로 적은 도구 정의입니다. 함수 이름은 도구 이름이 되고 docstring(함수 맨 위의 설명 글)은 도구 설명이 되며 타입 힌트(ticket_id: str 같은 변수 종류 표시)는 인자의 종류가 됩니다.

그림에서 같은 색 부분끼리 짝입니다. 설명서의 input_schema는 인자의 모양을 적는 칸이고 required는 꼭 넣어야 하는 인자 목록입니다. 설명서는 요청에 실제로 실리는 도구 정의를 줄여 옮긴 것입니다.

설명서는 langchain 1.4와 langchain-anthropic 1.7로 실제 요청을 확인해 줄여 옮긴 것입니다. docstring의 Args 칸은 parse_docstring=True를 줄 때 인자 설명으로 읽힙니다.

tools.pypython
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)
  1. 이름은 함수 이름을 그대로 씁니다. get_ticket처럼 동사로 시작하고 영문 소문자와 밑줄로 쓰면 회사마다 호환이 잘 된다고 LangChain 문서가 권합니다. 다른 이름을 쓰고 싶으면 @tool("이름")으로 바꿉니다.
  2. 설명은 모델이 이 도구를 언제 쓸지 고르는 가장 큰 단서입니다. 하는 일뿐 아니라 쓸 때와 쓰지 않을 때를 함께 적습니다. 위 예시는 번호를 모를 때 다른 도구로 보냅니다.
  3. 인자 설명은 Args: 칸에 적으면 인자마다 붙습니다. 값의 예시(T-1042)를 함께 적으면 모델이 형식을 맞춰 씁니다.
  4. 타입 힌트가 인자의 종류를 정합니다. LangChain 문서는 타입 힌트가 필수라고 안내합니다. 힌트가 없으면 어떤 값을 넣을지 모델에게 알려 줄 수 없습니다.

도구 이름이 비슷하면 합치거나 서비스 이름을 붙입니다

설명이 겹치는 도구가 여럿이면 모델은 어느 것을 불러야 할지 헷갈립니다. Anthropic은 도구를 만들 때 겹치는 도구를 합치거나 이름 앞에 서비스 이름을 붙여 구분하라고 권합니다. 버튼으로 두 방법을 비교해 보세요.

도구가 돌려주는 글은 필요한 만큼만 돌려줍니다

도구가 돌려준 결과는 모델이 다음 호출 때 읽는 글입니다. 데이터베이스 한 줄을 통째로 돌려주면 모델이 쓰지 않을 칸까지 읽을 글이 되어 컨텍스트(모델이 한 번에 볼 수 있는 글의 양)를 차지합니다.

Anthropic은 도구가 모델에게 꼭 필요한 정보만 돌려주라고 권하고 Claude Code에서는 도구 응답을 기본 25,000토큰으로 제한한다고 밝혔습니다. 티켓을 여러 개 조회할 때 응답 방식에 따라 컨텍스트가 얼마나 차는지 확인해 보세요.


        

설명을 위한 예시 값입니다. 통째로 돌려주면 티켓 한 줄에 600토큰, 필요한 칸만 돌려주면 40토큰이라고 가정했고 유리 상자는 20만 토큰 창 기준입니다.

도구가 실패하면 기본 설정은 예외를 던집니다

직접 짠 루프에서는 도구 오류를 결과로 돌려주는 코드를 우리가 넣었습니다. create_agent는 기본 설정이 다릅니다. 모델이 인자를 잘못 써서 도구 함수 안에서 오류가 나면 그 예외가 그대로 밖으로 나가 Agent가 멈춥니다. 직접 확인했습니다.

오류를 모델에게 돌려주려면 도구 호출을 감싸는 훅(hook, 중간에 끼워 넣는 코드)을 하나 답니다. @wrap_tool_call로 예외를 잡아 오류 표시가 붙은 ToolMessage(도구 결과를 담는 메시지)를 만들어 돌려주면 모델이 오류 문장을 읽고 인자를 고칩니다. 오류 문장에는 무엇이 틀렸고 어떻게 고치는지 적어야 합니다.

기본 설정 모델 도구 Agent 중단사용자는 답을 못 받음 wrap_tool_call로 감싸면 모델 도구 + 방패 오류 쪽지 인자 수정 답 "T1042 티켓이 없습니다. 번호는 T-1042 같은 형식입니다."

설명을 위한 예시 장면입니다. 두 쪽 모두 모델이 처음에 get_ticket("T1042")처럼 틀린 번호를 씁니다.

agent.pypython
# 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)
터미널
$ python agent.py error T1042 티켓이 없습니다. 번호는 T-1042 같은 형식입니다. success T-1042: 부품 대기, 도착 예정 10월 18일 T-1042는 부품 대기 중입니다.

출력은 예시 답입니다. 코드는 langchain 1.4 기준이고 import와 모델 준비는 생략했습니다.

  1. 예외를 결과로 바꿉니다. handler(request)가 도구를 실제로 실행합니다. 그 안에서 예외가 나면 except가 받아 오류 문장을 담은 ToolMessage를 대신 돌려줍니다. status="error"는 이 결과가 실패라는 표시입니다.
  2. 에이전트에 달아 줍니다. middleware는 에이전트가 도는 중간에 끼워 넣는 코드 목록입니다. 도구 호출 앞뒤를 감싸는 훅은 이 목록에 넣습니다. 훅을 더 자세히 쓰는 법은 이후 글에서 다룹니다.
  3. 결과를 확인합니다. result["messages"]에는 주고받은 메시지가 순서대로 들어 있습니다. 도구 결과 메시지의 status가 error인지 success인지 찍어 보면 오류가 한 번 났다가 고쳐진 것이 보입니다.
로그인한 사용자

설명을 위한 예시 데이터입니다. 모델이 쓴 요청서는 사용자가 바뀌어도 같고 결과만 달라집니다.

사용자 정보는 모델에게 묻지 않고 런타임에서 꺼냅니다

"내 티켓 목록"을 보려면 도구가 로그인한 사용자를 알아야 합니다. 이 값을 모델이 인자로 채우게 하면 모델이 다른 사람의 이름을 쓸 수도 있습니다. 모델이 쓴 값은 틀릴 수 있으니 권한이 걸린 정보는 모델 밖에서 받는 편이 안전합니다.

도구 함수에 runtime: ToolRuntime 인자를 달면 호출할 때 코드가 넘긴 context를 꺼낼 수 있습니다. LangChain 문서에 따르면 이 인자는 모델에게 숨겨집니다. 위 설명서 그림에서 runtime이 벽에 막힌 것과 같습니다.

tools.pypython
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)
터미널
$ python tools.py T-1042가 열려 있습니다.

출력은 예시 답입니다. 사용자가 lee면 도구가 T-1043을 돌려주고 답도 그에 맞게 바뀝니다.

  1. context의 모양입니다. 사용자 번호처럼 호출마다 달라지는 값을 담을 클래스를 만듭니다. @dataclass는 값을 담는 클래스를 짧게 쓰게 해 주는 Python 기능이고 이 클래스가 곧 context_schema(context의 모양 설명)입니다.
  2. runtime 인자입니다. 이름이 runtime인 인자는 예약된 이름이라서 모델 설명서에서 빠지고 호출 때 LangChain이 채워 줍니다. runtime.context로 값을 읽습니다.
  3. 두 곳에서 넘깁니다. create_agent에는 모양(context_schema)을 주고 invoke에는 실제 값(context=)을 줍니다. 값을 바꿔 호출하면 같은 Agent가 사용자마다 다른 결과를 냅니다.

도구를 만들 때 확인할 것

모델은 코드가 아니라 설명서를 읽고 도구를 고릅니다.

이름, 설명, 결과, 오류, 사용자 정보까지 다섯 곳이 모두 설명서의 일부입니다. 다음 글에서는 이 도구를 Agent에 연결해 루프를 맡기는 create_agent를 알아보겠습니다.

이름하는 일이 보이게 설명쓸 때와 쓰지 않을 때까지 결과필요한 만큼만 오류고치는 방법까지 사용자 정보runtime으로 받기

참고 자료

출처 5개 펼치기