无头工具让你的 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.toolCalls 和 onTool 来显示 待处理、成功和错误状态。 - 在需要时添加审查。对于敏感的客户端操作,将此模式与 结合使用 Human-in-the-loop.