文档
CodeRabbit
Cloudflare
AG Grid
Netlify
Neon
WorkOS
Clerk
Convex
Electric
PowerSync
Sentry
Railway
Prisma
Strapi
Unkey
CodeRabbit
Cloudflare
AG Grid
Netlify
Neon
WorkOS
Clerk
Convex
Electric
PowerSync
Sentry
Railway
Prisma
Strapi
Unkey
类引用
函数引用
接口引用
类型别名引用
变量引用
指南

客户端工具

客户端工具在浏览器中执行,能够实现 UI 更新、本地存储访问和浏览器 API 交互。与服务器工具不同,客户端工具在其服务器定义中没有 execute 函数。

mermaid

何时使用客户端工具

  • UI 更新:显示通知、更新表单、切换可见性
  • 本地存储:保存用户偏好、缓存数据
  • 浏览器 API:访问地理位置、摄像头、剪贴板
  • 状态管理:更新 React/Vue/Solid 状态
  • 导航:更改路由、滚动到部分

工作原理

  1. LLM 调用工具:LLM 决定调用客户端工具
  2. 服务器检测:服务器检测到该工具没有 execute 函数
  3. 客户端通知:服务器发送 tool-input-available chunk 到浏览器
  4. 客户端执行:浏览器的 onToolCall 回调函数被触发,参数为
    • toolName:要执行的工具名称
    • input:解析后的参数
  5. 结果返回:客户端执行该工具并返回结果
  6. 服务器更新:结果被发送回服务器并添加到对话中
  7. LLM 继续:LLM 接收到结果并继续对话

定义客户端工具

客户端工具使用相同的 toolDefinition() API,但使用 .client() 方法

typescript
// tools/definitions.ts - Shared between server and client
import { toolDefinition } from "@tanstack/ai";
import { z } from "zod";

export const updateUIDef = toolDefinition({
  name: "update_ui",
  description: "Update the UI with new information",
  inputSchema: z.object({
    message: z.string().describe("Message to display"),
    type: z.enum(["success", "error", "info"]).describe("Message type"),
  }),
  outputSchema: z.object({
    success: z.boolean(),
  }),
});

export const saveToLocalStorageDef = toolDefinition({
  name: "save_to_local_storage",
  description: "Save data to browser local storage",
  inputSchema: z.object({
    key: z.string().describe("Storage key"),
    value: z.string().describe("Value to store"),
  }),
  outputSchema: z.object({
    saved: z.boolean(),
  }),
});

使用客户端工具

服务器端

为了让 LLM 访问客户端工具,请在创建聊天时将工具定义(而非实现)传递给服务器

typescript
// api/chat/route.ts
import { chat, toServerSentEventsStream } from "@tanstack/ai";
import { openaiText } from "@tanstack/ai-openai";
import { updateUIDef, saveToLocalStorageDef } from "@/tools/definitions";

export async function POST(request: Request) {
  const { messages } = await request.json();

  const stream = chat({
    adapter: openaiText("gpt-5.2"),
    messages,
    tools: [updateUIDef, saveToLocalStorageDef], // Pass definitions
  });

  return toServerSentEventsStream(stream);
}

客户端

创建具有自动执行和完全类型安全的客户端实现

typescript
// app/chat.tsx
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
import { 
  clientTools, 
  createChatClientOptions, 
  type InferChatMessages 
} from "@tanstack/ai-client";
import { updateUIDef, saveToLocalStorageDef } from "@/tools/definitions";
import { useState } from "react";

function ChatComponent() {
  const [notification, setNotification] = useState(null);

  // Step 1: Create client implementations
  const updateUI = updateUIDef.client((input) => {
    // Update React state - fully typed!
    setNotification({ message: input.message, type: input.type });
    return { success: true };
  });

  const saveToLocalStorage = saveToLocalStorageDef.client((input) => {
    localStorage.setItem(input.key, input.value);
    return { saved: true };
  });

  // Step 2: Create typed tools array (no 'as const' needed!)
  const tools = clientTools(updateUI, saveToLocalStorage);

  const chatOptions = createChatClientOptions({
    connection: fetchServerSentEvents("/api/chat"),
    tools,
  });

  // Step 3: Infer message types for full type safety
  type ChatMessages = InferChatMessages<typeof chatOptions>;

  const { messages, sendMessage, isLoading } = useChat(chatOptions);

  // Step 4: Render with full type safety
  return (
    <div>
      {messages.map((message) => (
        <MessageComponent key={message.id} message={message} />
      ))}
      {notification && (
        <div className={`notification ${notification.type}`}>
          {notification.message}
        </div>
      )}
    </div>
  );
}

// Messages component with full type safety
function MessageComponent({ message }: { message: ChatMessages[number] }) {
  return (
    <div>
      {message.parts.map((part) => {
        if (part.type === "text") {
          return <p>{part.content}</p>;
        }
        
        if (part.type === "tool-call") {
          // ✅ part.name is narrowed to specific tool names
          if (part.name === "update_ui") {
            // ✅ part.input is typed as { message: string, type: "success" | "error" | "info" }
            // ✅ part.output is typed as { success: boolean } | undefined
            return (
              <div>
                Tool: {part.name}
                {part.output && <span>✓ Success</span>}
              </div>
            );
          }
        }
      })}
    </div>
  );
}

自动执行

客户端工具在模型调用它们时会自动执行。无需手动 onToolCall 回调!流程如下:

  1. LLM 调用客户端工具
  2. 服务器发送 tool-input-available chunk 到浏览器
  3. 客户端自动执行匹配的工具实现
  4. 结果被发送回服务器
  5. 对话使用结果继续

类型安全优势

同构架构提供完整的端到端类型安全

typescript
messages.forEach((message) => {
  message.parts.forEach((part) => {
    if (part.type === "tool-call" && part.name === "update_ui") {
      // ✅ TypeScript knows part.name is literally "update_ui"
      // ✅ part.input is typed as { message: string, type: "success" | "error" | "info" }
      // ✅ part.output is typed as { success: boolean } | undefined
      
      console.log(part.input.message); // ✅ Fully typed!
      
      if (part.output) {
        console.log(part.output.success); // ✅ Fully typed!
      }
    }
  });
});

工具状态

客户端工具会经历一小部分可观察的生命周期状态,你可以将其显示在 UI 中以指示进度

  • awaiting-input — 模型打算调用该工具,但参数尚未到达。
  • input-streaming — 模型正在流式传输工具参数(可能存在部分输入)。
  • input-complete — 已接收到所有参数,并且该工具正在执行。
  • completed — 该工具已完成;part.output 包含结果(或错误详细信息)。

使用这些状态来显示加载指示器、流式传输进度以及最终的成功/错误反馈。下面的示例将每个状态映射到简单的 UI 消息。

typescript
function ToolCallDisplay({ part }: { part: ToolCallPart }) {
  if (part.state === "awaiting-input") {
    return <div>🔄 Waiting for arguments...</div>;
  }
  
  if (part.state === "input-streaming") {
    return <div>📥 Receiving arguments...</div>;
  }
  
  if (part.state === "input-complete") {
    return <div>✓ Arguments received, executing...</div>;
  }
  
  if (part.output) {
    return <div>✅ Tool completed successfully</div>;
  }
  
  return null;
}

混合工具

工具可以同时为服务器和客户端实现,从而实现灵活的执行

typescript
// Define once
const addToCartDef = toolDefinition({
  name: "add_to_cart",
  description: "Add item to shopping cart",
  inputSchema: z.object({
    itemId: z.string(),
    quantity: z.number(),
  }),
  outputSchema: z.object({
    success: z.boolean(),
    cartId: z.string(),
  }),
});

// Server implementation - Store in database
const addToCartServer = addToCartDef.server(async (input) => {
  const cart = await db.carts.create({
    data: { itemId: input.itemId, quantity: input.quantity },
  });
  return { success: true, cartId: cart.id };
});

// Client implementation - Update local wishlist
const addToCartClient = addToCartDef.client((input) => {
  const wishlist = JSON.parse(localStorage.getItem("wishlist") || "[]");
  wishlist.push(input.itemId);
  localStorage.setItem("wishlist", JSON.stringify(wishlist));
  return { success: true, cartId: "local" };
});

// Server: Pass definition for client execution
chat({ adapter: openaiText('gpt-5.2'), messages: [], tools: [addToCartDef] }); // Client will execute

// Or pass server implementation for server execution
chat({ adapter: openaiText('gpt-5.2'), messages: [], tools: [addToCartServer] }); // Server will execute

最佳实践

  • 保持客户端工具简单 - 由于客户端工具在浏览器中运行,请避免繁重的计算或大型依赖项,这些可能会使你的包大小膨胀。
  • 优雅地处理错误 - 在你的工具实现中定义清晰的错误处理,并在你的输出模式中返回有意义的错误消息。
  • 响应式地更新 UI - 使用你的框架的状态管理(例如 React/Vue/Solid)来响应工具执行来更新 UI。
  • 保护敏感数据 - 切勿将敏感数据(如 API 密钥或个人信息)存储在本地存储中或通过客户端工具公开。
  • 提供反馈 - 使用工具状态向用户通报正在进行的操作以及客户端工具执行的结果(加载微调器、成功消息、错误警报)。
  • 一切都进行类型化 - 利用 TypeScript 和 Zod 模式实现从工具定义到实现再到使用的完全类型安全。

常见用例

  • UI 更新 - 显示通知、更新表单、切换可见性
  • 本地存储 - 保存用户偏好、缓存数据
  • 浏览器 API - 访问地理位置、摄像头、剪贴板
  • 状态管理 - 更新 React/Vue/Solid 状态
  • 导航 - 更改路由、滚动到部分
  • 分析 - 跟踪用户交互

下一步