跳转到内容
搜索文档

客户端工具

最后更新 查看 MarkdownAgent 设置

Think 支持在浏览器中执行的工具。客户端在聊天请求 body 中发送可序列化的工具 schema,Think 将其与服务端工具合并;当 LLM 调用客户端工具时,调用会路由到客户端执行。

定义客户端工具

对于动态客户端工具,将 tools 传给 useAgentChat。带 execute 函数的工具会作为客户端执行工具注册到服务端:

const { messages, sendMessage } = useAgentChat({
	agent,
	tools: {
		getUserTimezone: {
			description: "Get the user's timezone from their browser",
			parameters: {},
			execute: async () => {
				return Intl.DateTimeFormat().resolvedOptions().timeZone;
			},
		},
		getClipboard: {
			description: "Read text from the user's clipboard",
			parameters: {},
			execute: async () => {
				return navigator.clipboard.readText();
			},
		},
	},
});
const { messages, sendMessage } = useAgentChat({
	agent,
	tools: {
		getUserTimezone: {
			description: "Get the user's timezone from their browser",
			parameters: {},
			execute: async () => {
				return Intl.DateTimeFormat().resolvedOptions().timeZone;
			},
		},
		getClipboard: {
			description: "Read text from the user's clipboard",
			parameters: {},
			execute: async () => {
				return navigator.clipboard.readText();
			},
		},
	},
});

客户端工具是服务端上没有 execute 的工具——它们只有 schema。当 LLM 为其产生工具调用时,Think 将其路由到客户端。

大多数应用应在服务端定义工具,并用 onToolCall 处理仅浏览器可执行的逻辑。tools 选项最适合浏览器在运行时决定可用工具表面的 SDK 或平台。

通过子 agent RPC chat() 路径的客户端工具

当父 agent 通过 RPC 用 chat() 委托给 Think 子 agent(而非浏览器 WebSocket)时,没有 WebSocket 承载 clientTools 或回传工具结果。改为通过 ChatOptions 传递:

await child.chat(message, callback, {
	signal,
	clientTools: [
		{
			name: "get_user_timezone",
			description: "Get the caller's timezone",
			parameters: { type: "object" },
		},
	],
	onClientToolCall: async ({ toolName, input }) => {
		// Run the client tool wherever the parent can — return its output.
		return runClientTool(toolName, input);
	},
});
await child.chat(message, callback, {
	signal,
	clientTools: [
		{
			name: "get_user_timezone",
			description: "Get the caller's timezone",
			parameters: { type: "object" },
		},
	],
	onClientToolCall: async ({ toolName, input }) => {
		// Run the client tool wherever the parent can — return its output.
		return runClientTool(toolName, input);
	},
});
  • clientTools 为该轮注册工具 schema,与 WebSocket clientTools 字段相同。
  • onClientToolCall 执行客户端工具调用并返回输出。模型可调用客户端工具、接收结果并继续——全部在单次 chat() 调用内完成。

若省略 onClientToolCall,工具会注册但没有结果:模型的调用通过流回调浮现,轮次以悬空工具调用结束(RPC 流回调本身没有入站结果通道)。若希望往返完成,请提供 onClientToolCall

行为说明

  • 恢复: schema 与 onClientToolCall 执行器仅每轮次有效,永不持久化(执行器是随 isolate 消亡的实时 RPC 引用;与 WebSocket 路径不同,驱逐后没有客户端重放 tool-result)。若驱逐在客户端工具调用进行中打断轮次,聊天恢复会将孤立调用标记为错误(与服务端工具相同),模型继续。要干净重跑,父级需再次用 clientToolsonClientToolCall 调用 chat()
  • 错误:onClientToolCall 抛出,失败会以工具错误(output-error)浮现给模型,轮次继续——不会崩溃。
  • 序列化: onClientToolCall 的返回值成为工具输出,必须是 JSON 可序列化(经 RPC 传回并进入模型上下文)。
  • 无审批门控: RPC 客户端工具通过 onClientToolCall 立即执行。WebSocket 审批流程(needsApproval)不适用于此路径——如需门控,在执行器内实现。
  • 名称优先级: 客户端工具在服务端工具之后合并,因此与服务端工具同名的客户端工具(例如工作区工具)会覆盖该轮——与 WebSocket 路径相同。
  • 中止: 通过 signal 中止轮次会停止循环,但进行中的 onClientToolCall 本身不会被取消;当前调用兑现后轮次结束。

审批流程

在客户端上用 onToolCall 处理浏览器端工具执行:

useAgentChat({
	agent,
	onToolCall: async ({ toolCall, addToolOutput }) => {
		if (toolCall.toolName === "read") {
			const result = await readFromBrowser(toolCall.input);
			addToolOutput({
				toolCallId: toolCall.toolCallId,
				output: result,
			});
		}
	},
});
useAgentChat({
	agent,
	onToolCall: async ({ toolCall, addToolOutput }) => {
		if (toolCall.toolName === "read") {
			const result = await readFromBrowser(toolCall.input);
			addToolOutput({
				toolCallId: toolCall.toolCallId,
				output: result,
			});
		}
	},
});

自动续传

收到客户端工具结果后,Think 无需新用户消息即自动继续对话。续传轮次的 TurnContextcontinuation: true,可在 beforeTurn 中调整模型或工具选择。

当一轮产生多个客户端工具调用时,Think 会等待所有结果再开始单次续传,而不是每个结果各开一次。若续传已挂起时收到立即恢复请求,会挂接到挂起的续传而非重复启动;服务端 needsApproval 续传在审批记录后可靠恢复。

等待人类时经受重启

Durable Object 随时可能被驱逐,包括在轮次暂停于审批提示或客户端工具调用时。由于 Think 默认启用 chatRecovery,SDK 将此类轮次视为等待人类而非卡住。它会停放轮次而非失败,用户最终审批或工具结果会恢复对话。

哪些交互免除恢复预算,请参阅 等待人类的轮次不会被封存

消息并发

messageConcurrency 属性控制聊天轮次已活跃时重叠用户提交的行为。

策略 行为
"queue" 排队每个提交并按顺序处理。默认。
"latest" 仅保留最新重叠提交;被取代的提交仍持久化用户消息但不启动模型轮次
"merge" 排队重叠提交,然后在最新排队轮次运行前将其尾部用户消息合并为一次合并轮次
"drop" 完全忽略重叠提交。消息不持久化。
{ strategy: "debounce", debounceMs?: number } 尾沿取最新,带静默窗口(默认 750ms)。
import { Think } from "@cloudflare/think";

export class SearchAgent extends Think {
	messageConcurrency = "latest";
	getModel() {
		/* ... */
	}
}
import { Think } from "@cloudflare/think";
import type { MessageConcurrency } from "@cloudflare/think";

export class SearchAgent extends Think<Env> {
	override messageConcurrency: MessageConcurrency = "latest";
	getModel() {
		/* ... */
	}
}

多标签广播

Think 向所有已连接 WebSocket 客户端广播流式响应。多个浏览器标签连接到同一 agent 时,所有标签实时看到流式响应。工具调用状态(待处理、结果、审批)广播到所有标签。

编程式 chat() 轮次与 clearMessages() 也会向已连接 useAgentChat 客户端广播消息更新,浏览器客户端无需重连即可保持同步。

这篇文档对您有帮助吗?