以编程方式使用文档

无头工具让你的 agent 调用那些实际执行必须在 用户应用中而非服务器上执行的工具。Agent 仍然看到一个正常的工具 schema, 但实现位于前端,这样它可以访问浏览器 API 如 IndexedDB、地理位置、剪贴板、画布或文件选择器。

当数据应该保留在本地设备时,这种模式特别有用。 此页面上的 playground 示例使用了一个基于浏览器内存的小型工具包,由 IndexedDB 支持,外加一个完全在客户端运行的地理位置工具。

无头工具的工作原理

从高层次来看,无头工具将工具 schema 与仅限浏览器的实现分离。

1. 在 agent 上注册一个工具,该工具立即调用 interrupt() 以将执行推迟到前端。 2. 在前端定义中镜像相同的工具名称和参数字段。 3. 在前端使用 .implement(...) 实现匹配的工具并传递 它们给 useStream({ tools: [...] }). 4. 当 agent 调用匹配的工具时,客户端处理该操作并 使用工具结果恢复中断的运行。

在 agent 上注册工具

playground 定义了一组遵循相同模式的小型客户端工具: agent 暴露一个工具 schema,前端处理实际的 execution.

在服务器上定义正常工具,立即调用 interrupt(),然后在 前端中镜像相同的工具名称和参数字段到一个 前端 tools.ts file.

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.",
    args_schema=MemoryPutInput,
)
def memory_put(key: str, value: Any, runtime: ToolRuntime) -> Any:
    return _interrupt_for_client(
        "memory_put",
        {"key": key, "value": value},
        runtime,
    )


@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(),
)

每个工具使用前端可以处理的结构化有效载荷中断,然后 返回运行恢复时提供的值。在客户端镜像相同的工具名称和 schema,以便前端可以附加实现。

// Mirror the Python tool names and schemas on the client.

  name: "memory_put",
  description: "Store a memory in the user's browser.",
  schema: z.object({
    key: z.string(),
    value: z.unknown(),
  }),
});

  name: "memory_get",
  description: "Look up a memory stored in the user's browser.",
  schema: z.object({
    key: z.string(),
  }),
});

  name: "geolocation_get",
  description: "Get the user's current location from the browser.",
  schema: z.object({
    save: z.boolean().optional(),
  }),
});

实现浏览器行为

将仅限客户端的行为放在单独的模块中,并使用 .implement(...)附加它。真正的 playground 包含一个更完整的 IndexedDB 存储,具有 搜索、列表、过期和删除操作。以下示例展示了 相同形状但更高层级:

  geolocationGet as geolocationGetDefinition,
  memoryGet as memoryGetDefinition,
  memoryPut as memoryPutDefinition,
} from "./tools";

async function saveMemory(key: string, value: unknown) {
  localStorage.setItem(`agent-memory:${key}`, JSON.stringify(value));
}

async function getMemory(key: string) {
  const value = localStorage.getItem(`agent-memory:${key}`);
  return value ? JSON.parse(value) : null;
}

  await saveMemory(key, value);
  return { success: true, key };
});

  const value = await getMemory(key);
  return value === null ? { found: false, key } : { found: true, key, value };
});

  async ({ save = true }) => {
    const position = await new Promise((resolve, reject) =>
      navigator.geolocation.getCurrentPosition(resolve, reject),
    );

    const location = {
      latitude: position.coords.latitude,
      longitude: position.coords.longitude,
      accuracy: position.coords.accuracy,
    };

    if (save) {
      await saveMemory("user_location", location);
    }

    return location;
  },
);

将实现连接到 useStream

将实现的工具传递给 useStream。当代理发出匹配的工具 调用时,钩子运行客户端实现并为你恢复运行。

定义一个与你的代理状态模式匹配的 TypeScript 接口,并将其作为 类型参数传递给 useStream 以获得对状态值的类型安全访问:

  messages: BaseMessage[];
}
const AGENT_URL = "http://localhost:2024";

  const stream = useStream({
    apiUrl: AGENT_URL,
    assistantId: "headless_tools",
    tools: [memoryPut, memoryGet, geolocationGet],
  });

  return ;
}
<script setup lang="ts">




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

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

<template>

</template>
<script lang="ts">




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

  const { messages, toolCalls } = useStream({
    apiUrl: AGENT_URL,
    assistantId: "headless_tools",
    tools: [memoryPut, memoryGet, geolocationGet],
  });
</script>
const AGENT_URL = "http://localhost:2024";

@Component({
  selector: "app-chat",
  template: `
    <app-chat-view
      [messages]="stream.messages()"
      [toolCalls]="stream.toolCalls()"
    />
  `,
})

  stream = useStream({
    apiUrl: AGENT_URL,
    assistantId: "headless_tools",
    tools: [memoryPut, memoryGet, geolocationGet],
  });
}

内联渲染工具活动

操场将每个记忆或地理位置操作渲染为独立的卡片,并 在输入附近保留一个小型记忆统计面板。关键步骤是将每个 条目与 stream.toolCalls 触发它的 AI 消息对应起来:

function Message({ message, toolCalls }: {
  message: AIMessage,
  toolCalls: ToolCallWithResult[]
}) {
  const messageToolCalls = toolCalls.filter((tc) =>
    message.tool_calls?.some((call) => call.id === tc.call.id),
  );

  return (

      {message.text && <p>{message.text}</p>}
      {messageToolCalls.map((tc) => (

      ))}

  );
}

这在与 工具调用的更丰富的 UI 模式结合时效果特别好,其中每个工具结果可以 作为专门的卡片而不是原始 JSON 进行渲染。

使用案例

当工作依赖于仅在以下环境中存在的 API 或数据时使用无头工具 client:

  • - IndexedDB 或本地内存中的 localStorage
  • - 设备 API,如地理位置、剪贴板、摄像头或文件选择器
  • - Canvas、音频或其他仅限浏览器的渲染原语
  • - 应保留在用户设备上的隐私敏感数据
  • - 需要直接访问内存中前端状态的 UI 操作

最佳实践

- 保持工具小型化和类型化。优先使用多个窄工具而非一个通用工具 “运行任意浏览器代码”工具。 - 返回 JSON 可序列化的结果。不要尝试返回 DOM 节点、文件 句柄或其他不可序列化的浏览器对象。 - 共享定义,分离实现。代理和客户端应就 工具名称和架构达成一致,但只有客户端应加载浏览器 API。 - 在 UI 中显示工具状态。使用 stream.toolCallsonTool 来显示 待处理、成功和错误状态。 - 在需要时添加审查。对于敏感的客户端操作,将此模式与 结合使用 Human-in-the-loop.