代理可以调用天气 API、计算器、网页搜索、数据库查询等外部工具。结果以原始 JSON 表示。这个模式展示如何为代理发起的每个工具调用渲染结构化、类型安全的 UI 卡片,并包含加载状态和错误处理。

工具调用的工作原理

当 LangGraph 代理判断需要外部数据时,它会作为 AI 消息的一部分发出一个或多个 tool calls。每个工具调用包含:
  • name:被调用的工具(例如 "get_weather""calculator"
  • args:传给工具的结构化参数
  • id:将调用与结果关联起来的唯一标识符
代理运行时执行工具,结果会以 ToolMessage 返回。useStream hook 会将这些内容统一到一个可直接渲染的 toolCalls 数组中。

设置 useStream

第一步是将 useStream 连接到你的代理后端。该 hook 返回响应式状态,其中包括一个 toolCalls 数组,会随代理流式传输实时更新。
The code examples use useStream<typeof myAgent> for type-safe stream state. See Type inference for Python or JavaScript backends.
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>
  );
}

AssembledToolCall 类型

toolCalls 数组中的每个条目都是一个 AssembledToolCall 对象:
interface AssembledToolCall<
  TName extends string = string,
  TInput = unknown,
  TOutput = unknown,
> {
  name: TName;
  callId: string;
  id: string;
  namespace: string[];
  input: TInput;
  args: TInput;
  output: TOutput | null;
  status: "running" | "finished" | "error";
  error: string | undefined;
}
属性描述
name工具名称(例如 "get_weather"
callId与 AI 消息的 tool_calls 条目匹配的唯一 ID
idcallId 的别名,与消息级工具调用匹配
namespace发出工具调用的 namespace
input代理传给工具的结构化参数
argsinput 的别名,与消息级工具调用匹配
output成功调用后的工具输出;运行中或出错后为 null
status生命周期状态:"running""finished""error"
error工具调用失败时的错误详情

按消息筛选工具调用

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

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

构建专用工具卡片

不要直接倾倒原始 JSON,而是为每个工具构建专用 UI 组件。使用 name 选择正确卡片:
function ToolCard({ toolCall }: { toolCall: AssembledToolCall }) {
  if (toolCall.status === "running") {
    return <LoadingCard name={toolCall.name} />;
  }

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

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

Weather card 示例

function WeatherCard({
  input,
  output,
}: {
  input: { location: string };
  output: { temperature: number; condition: string };
}) {
  return (
    <div className="rounded-lg border p-4">
      <div className="flex items-center gap-2">
        <CloudIcon />
        <h3 className="font-semibold">{input.location}</h3>
      </div>
      <div className="mt-2 text-3xl font-bold">{output.temperature}°F</div>
      <p className="text-muted-foreground">{output.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>Running {name}...</span>
    </div>
  );
}

function ErrorCard({ name, error }: { name: string; error?: unknown }) {
  return (
    <div className="rounded-lg border border-red-300 bg-red-50 p-4">
      <h3 className="font-semibold text-red-700">Error in {name}</h3>
      <p className="text-sm text-red-600">
        {String(error ?? "Tool execution failed")}
      </p>
    </div>
  );
}

类型安全的工具参数

如果你的工具使用结构化 schema 定义,可以使用 ToolCallFromTool utility type 获取完全类型化的 args
import { tool } from "@langchain/core/tools";
import { z } from "zod";

const getWeather = tool(async ({ location }) => { /* ... */ }, {
  name: "get_weather",
  description: "Get the current weather for a location",
  schema: z.object({
    location: z.string().describe("City name"),
  }),
});

type WeatherToolCall = ToolCallFromTool<typeof getWeather>;
// WeatherToolCall.input and WeatherToolCall.args are now { location: string }
使用 ToolCallFromTool 可以获得编译期安全性。如果工具 schema 发生变化,你的 UI 组件会立即标记类型错误。

将工具调用与流式文本内联渲染

工具调用通常会与流式文本交错到达。useStream hook 会让 toolCalls 与流保持同步,因此代理一发出调用,待处理卡片就会出现,即使工具尚未执行完成。 这意味着用户会看到:
  1. AI 文本流式传入
  2. 工具调用发出时立即出现加载卡片
  3. 工具完成后,卡片更新并显示结果
工具调用会原地更新。同一个 callId 会从 "running" 转换为 "finished"(或 "error"),因此你的 UI 会用新状态重新渲染同一个组件。

处理多个并发工具调用

代理可以并行调用多个工具。toolCalls 数组会同时包含多个 status: "running" 的条目。每个条目都会独立完成,因此你的 UI 应该优雅处理部分完成状态:
function ToolCallList({ toolCalls }: { toolCalls: AssembledToolCall[] }) {
  const pending = toolCalls.filter((tc) => tc.status === "running");
  const completed = toolCalls.filter((tc) => tc.status === "finished");

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

最佳实践

构建工具调用 UI 时,请遵循这些准则:
  • 始终处理三种状态runningfinishederror。用户不应看到空白卡片。
  • 安全验证结果。工具输出在你为特定卡片缩窄类型之前会被类型化为 unknown
  • 提供通用回退。不是每个工具都需要定制卡片。为未知工具名渲染可折叠的 JSON 视图。
  • 加载期间显示工具名称和 args。即使结果尚未到达,用户也想知道代理正在做什么。
  • 保持卡片紧凑。工具卡片会内联出现在聊天消息中。避免用过大的小组件压倒对话。