Skip to main content
智能体可以调用外部工具,例如天气 API、计算器、网络搜索、数据库查询等。结果以原始 JSON 格式返回。本模式将展示如何为智能体进行的每次工具调用渲染结构化、类型安全的 UI 卡片,并包含加载状态和错误处理。

工具调用的工作原理

当 LangGraph 智能体决定需要外部数据时,它会作为 AI 消息的一部分发出一个或多个工具调用。每个工具调用包含:
  • name:被调用的工具名称(例如 "get_weather""calculator"
  • args:传递给工具的结构化参数
  • id:将调用与其结果关联的唯一标识符
智能体运行时执行该工具,结果以 ToolMessage 形式返回。useStream 钩子将所有内容统一到一个 toolCalls 数组中,您可以直接渲染。

设置 useStream

第一步是将 useStream 连接到您的智能体后端。该钩子返回响应式状态,包括一个 toolCalls 数组,该数组会在智能体流式传输时实时更新。 定义一个与智能体状态模式匹配的 TypeScript 接口,并将其作为类型参数传递给 useStream,以便类型安全地访问状态值。在以下示例中,将 typeof myAgent 替换为您的接口名称:
import type { BaseMessage } from "@langchain/core/messages";

interface AgentState {
  messages: BaseMessage[];
}
import { useStream } from "@langchain/react";

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

export function Chat() {
  const stream = useStream<typeof myAgent>({
    apiUrl: AGENT_URL,
    assistantId: "tool_calling",
  });

  return (
    <div>
      {stream.messages.map((msg) => (
        <Message key={msg.id} message={msg} toolCalls={stream.toolCalls} />
      ))}
    </div>
  );
}

ToolCallWithResult 类型

toolCalls 数组中的每个条目都是一个 ToolCallWithResult 对象:
interface ToolCallWithResult {
  call: {
    id: string;
    name: string;
    args: Record<string, unknown>;
  };
  result: ToolMessage | undefined;
  state: "pending" | "completed" | "error";
}
属性描述
call.id与 AI 消息的 tool_calls 条目匹配的唯一 ID
call.name工具名称(例如 "get_weather"
call.args智能体传递给工具的结构化参数
resultToolMessage 响应,工具完成后可用
state生命周期状态:运行时为 "pending",成功时为 "completed",失败时为 "error"

按消息筛选工具调用

一条 AI 消息可能触发多个工具调用,而您的聊天记录可能包含多条 AI 消息。为了在每个消息下渲染正确的工具卡片,请通过将 call.id 与消息的 tool_calls 数组进行匹配来筛选:
function Message({
  message,
  toolCalls,
}: {
  message: AIMessage;
  toolCalls: ToolCallWithResult[];
}) {
  const messageToolCalls = toolCalls.filter((tc) =>
    message.tool_calls?.find((t) => t.id === tc.call.id)
  );

  return (
    <div>
      <p>{message.content}</p>
      {messageToolCalls.map((tc) => (
        <ToolCard key={tc.call.id} toolCall={tc} />
      ))}
    </div>
  );
}

构建专用工具卡片

与其转储原始 JSON,不如为每个工具构建专用的 UI 组件。使用 call.name 来选择正确的卡片:
function ToolCard({ toolCall }: { toolCall: ToolCallWithResult }) {
  if (toolCall.state === "pending") {
    return <LoadingCard name={toolCall.call.name} />;
  }

  if (toolCall.state === "error") {
    return <ErrorCard name={toolCall.call.name} error={toolCall.result} />;
  }

  switch (toolCall.call.name) {
    case "get_weather":
      return <WeatherCard args={toolCall.call.args} result={toolCall.result} />;
    case "calculator":
      return (
        <CalculatorCard args={toolCall.call.args} result={toolCall.result} />
      );
    case "web_search":
      return <SearchCard args={toolCall.call.args} result={toolCall.result} />;
    default:
      return <GenericToolCard toolCall={toolCall} />;
  }
}

天气卡片示例

function WeatherCard({
  args,
  result,
}: {
  args: { location: string };
  result: ToolMessage;
}) {
  const data = JSON.parse(result.content as string);

  return (
    <div className="rounded-lg border p-4">
      <div className="flex items-center gap-2">
        <CloudIcon />
        <h3 className="font-semibold">{args.location}</h3>
      </div>
      <div className="mt-2 text-3xl font-bold">{data.temperature}°F</div>
      <p className="text-muted-foreground">{data.condition}</p>
    </div>
  );
}

加载和错误状态

始终处理待处理和错误状态,以便向用户提供清晰的反馈:
function LoadingCard({ name }: { name: string }) {
  return (
    <div className="flex items-center gap-2 rounded-lg border p-4 animate-pulse">
      <Spinner />
      <span>正在运行 {name}...</span>
    </div>
  );
}

function ErrorCard({ name, error }: { name: string; error?: ToolMessage }) {
  return (
    <div className="rounded-lg border border-red-300 bg-red-50 p-4">
      <h3 className="font-semibold text-red-700">{name} 出错</h3>
      <p className="text-sm text-red-600">
        {error?.content ?? "工具执行失败"}
      </p>
    </div>
  );
}

类型安全的工具参数

如果您的工具是使用结构化模式定义的,您可以使用 ToolCallFromTool 实用类型来获取完全类型化的 args
import { tool } from "@langchain/core/tools";
import { z } from "zod";

const getWeather = tool(async ({ location }) => { /* ... */ }, {
  name: "get_weather",
  description: "获取指定地点的当前天气",
  schema: z.object({
    location: z.string().describe("城市名称"),
  }),
});

type WeatherToolCall = ToolCallFromTool<typeof getWeather>;
// WeatherToolCall.call.args 现在为 { location: string }
使用 ToolCallFromTool 可以提供编译时安全性。如果工具模式发生变化,您的 UI 组件会立即标记类型错误。

在流式文本中内联渲染工具调用

工具调用通常与流式文本交错到达。useStream 钩子保持 toolCalls 与流同步,因此待处理卡片会在智能体发出调用后立即显示,甚至在工具执行完成之前。 这意味着用户会看到:
  1. AI 的文本随着流式传输而显示
  2. 工具调用发出时立即显示加载卡片
  3. 工具完成后,卡片更新以显示结果
工具调用会原地更新。相同的 call.id 会从 "pending" 过渡到 "completed"(或 "error"),因此您的 UI 会使用新状态重新渲染同一组件。

处理多个并发工具调用

智能体可以并行调用多个工具。toolCalls 数组将同时包含多个 state: "pending" 的条目。每个条目独立解析,因此您的 UI 应优雅地处理部分完成的情况:
function ToolCallList({ toolCalls }: { toolCalls: ToolCallWithResult[] }) {
  const pending = toolCalls.filter((tc) => tc.state === "pending");
  const completed = toolCalls.filter((tc) => tc.state === "completed");

  return (
    <div className="space-y-2">
      {completed.map((tc) => (
        <ToolCard key={tc.call.id} toolCall={tc} />
      ))}
      {pending.map((tc) => (
        <LoadingCard key={tc.call.id} name={tc.call.name} />
      ))}
    </div>
  );
}

最佳实践

构建工具调用 UI 时,请遵循以下准则:
  • 始终处理所有三种状态pendingcompletederror。用户永远不应看到空白卡片。
  • 安全地解析结果。工具结果以字符串形式到达。将 JSON.parse() 包装在 try/catch 中,并在解析失败时显示回退内容。
  • 提供通用回退。并非每个工具都需要定制的卡片。对于未知的工具名称,渲染一个可折叠的 JSON 视图。
  • 在加载期间显示工具名称和参数。用户希望知道智能体正在做什么,即使在结果到达之前。
  • 保持卡片紧凑。工具卡片内嵌在聊天消息中。避免使用过大的小组件淹没对话。