헤드리스 도구

헤드리스 도구 (Headless tools)

헤드리스 도구(headless tools) 패턴은 도구의 스키마는 에이전트(서버)가 갖고, 실제 실행은 프론트엔드(클라이언트)가 수행하게 하는 방식이에요. 특히 데이터가 사용자 기기의 로컬에 남아 있어야 할 때 유용하죠. 이 페이지의 플레이그라운드 예제는 IndexedDB로 구현된 작은 브라우저 메모리 툴킷과, 전적으로 클라이언트에서 실행되는 지오로케이션(geolocation) 도구를 사용해요.

이 패턴은 특히 Tool calling의 더 풍부한 UI 패턴과 잘 어울립니다. 각 도구 결과가 원시 JSON 대신 전문화된 카드로 렌더링될 수 있으니까요.

출처: LangChain 공식 문서 — headless-tools

에이전트에 도구 등록하기 (Register the tool on the agent)

플레이그라운드는 같은 패턴을 따르는 작은 클라이언트 측 도구 집합을 정의해요. 에이전트가 도구 스키마를 노출하고, 프론트엔드가 실제 실행을 담당하죠. 서버에는 즉시 interrupt()를 호출하는 일반적인 도구를 정의하고, 프론트엔드 tools.ts 파일에 같은 도구 이름과 인자 필드를 미러링(mirror)합니다.

서버 측 agent.py부터 볼게요:

from typing import Any

from langchain import create_agent
from langchain.tools import ToolRuntime, tool
from langgraph.checkpoint.memory import MemorySaver
from langgraph.types import interrupt
from pydantic import BaseModel


class MemoryPutInput(BaseModel):
    key: str
    value: Any


class MemoryGetInput(BaseModel):
    key: str


class GeolocationGetInput(BaseModel):
    save: bool = True


def _interrupt_for_client(
    tool_name: str,
    args: dict[str, Any],
    runtime: ToolRuntime,
) -> Any:
    return interrupt({
        "type": "tool",
        "tool_call": {
            "id": runtime.tool_call_id,
            "name": tool_name,
            "args": args,
        },
    })


@tool(
    "memory_put",
    description="Store a memory in the user's browser.",
)

이어서 나머지 도구 정의와 에이전트 생성입니다:

    )


@tool(
    "memory_get",
    description="Look up a memory stored in the user's browser.",
    args_schema=MemoryGetInput,
)
def memory_get(key: str, runtime: ToolRuntime) -> Any:
    return _interrupt_for_client("memory_get", {"key": key}, runtime)


@tool(
    "geolocation_get",
    description="Get the user's current location from the browser.",
    args_schema=GeolocationGetInput,
)
def geolocation_get(runtime: ToolRuntime, save: bool = True) -> Any:
    return _interrupt_for_client(
        "geolocation_get",
        {"save": save},
        runtime,
    )

agent = create_agent(
    model="openai:gpt-5.5",
    tools=[memory_put, memory_get, geolocation_get],
    checkpointer=MemorySaver(),
)

핵심 포인트는 서버 도구가 실제 로직을 수행하지 않고 interrupt()로 클라이언트에게 실행을 넘긴다는 거예요. 이제 클라이언트 쪽 tools.ts에서 같은 도구 이름과 스키마를 미러링합니다:

import * as z from "zod";
import { tool } from "langchain";

// Mirror the Python tool names and schemas on the client.
export const memoryPut = tool({
  name: "memory_put",
  description: "Get the user's current location from the browser.",
  schema: z.object({
    save: z.boolean().optional(),
  }),
});

도구의 실제 구현(IndexedDB 접근, 브라우저 지오로케이션)은 이 클라이언트 측 정의에 담겨요.

useStream에 구현 연결하기 (Wire the implementations into useStream)

구현된 도구들을 useStream에 넘겨주세요. 에이전트가 일치하는 도구 호출을 내보내면, 훅(hook)이 클라이언트 구현을 실행하고 런을 재개해 줍니다.

import { useStream } from "@langchain/react";

import { geolocationGet, memoryGet, memoryPut } from "./impl";
import type { AgentState } from "./types";

const AGENT_URL = "http://localhost:2024";

export function Chat() {
  const stream = useStream<AgentState>({
    apiUrl: AGENT_URL,
    assistantId: "headless_tools",
    tools: [memoryPut, memoryGet, geolocationGet],
  });
}

도구 활동 인라인 렌더링 (Render tool activity inline)

도구가 실행되는 동안 그 활동을 UI에 인라인으로 렌더링할 수 있어요. 이는 Tool calling의 더 풍부한 UI 패턴에서 특히 잘 동작하는데, 각 도구 결과를 원시 JSON 대신 전문화된 카드로 렌더링할 수 있기 때문이에요.

모범 사례 (Best practices)

  • 서버 도구는 스키마만, 클라이언트 도구는 구현을 담당하게 해서 역할을 깔끔히 나누세요.
  • 도구 이름과 인자 필드를 정확히 미러링하세요. 클라이언트 스키마가 서버와 어긋나면 도구 호출이 매칭되지 않아요.
  • 로컬 데이터 프라이버시가 중요할 때 활용하세요 — 데이터가 기기를 벗어나지 않는 게 이 패턴의 핵심 가치죠.
  • 도구 실행이 사람의 승인을 필요로 하는 흐름과 결합할 때는 Human-in-the-loop 패턴을 함께 살펴보세요.

더 알아보기 (Learn more)