헤드리스 도구
헤드리스 도구 (Headless tools)
헤드리스 도구(headless tools) 패턴은 도구의 스키마는 에이전트(서버)가 갖고, 실제 실행은 프론트엔드(클라이언트)가 수행하게 하는 방식이에요. 특히 데이터가 사용자 기기의 로컬에 남아 있어야 할 때 유용하죠. 이 페이지의 플레이그라운드 예제는 IndexedDB로 구현된 작은 브라우저 메모리 툴킷과, 전적으로 클라이언트에서 실행되는 지오로케이션(geolocation) 도구를 사용해요.
이 패턴은 특히 Tool calling의 더 풍부한 UI 패턴과 잘 어울립니다. 각 도구 결과가 원시 JSON 대신 전문화된 카드로 렌더링될 수 있으니까요.
에이전트에 도구 등록하기 (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 패턴을 함께 살펴보세요.