Skip to content

工具调用 ​

使用丰富、类型安全的 UI 卡片展示智能体工具调用

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

工具调用的工作原理 ​

当 LangGraph 智能体判定自身需要外部数据时,它会在 AI 消息中发出一项或多项工具调用(tool calls)。每个工具调用均包含:

  • name:被调用的工具名称(例如 "get_weather"、"calculator")
  • args:传递给工具的结构化参数
  • id:将调用与其结果关联起来的唯一标识符

智能体运行时会执行该工具,其结果以 ToolMessage 形式返回。useStream Hook 将所有这些内容统一为一个可直接渲染的单一 toolCalls 数组。

设置 useStream ​

第一步是将 useStream 连接到你的智能体后端。该 Hook 返回包含 toolCalls 数组的响应式状态,该状态会在智能体流式传输过程中实时更新。

NOTE

代码示例使用 useStream<typeof myAgent> 来获取类型安全的流状态。请参阅 Python 或 JavaScript 后端的类型推断。

tsx
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>
  );
}
vue
<script setup lang="ts">
import { useStream } from "@langchain/vue";

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

const stream = useStream<typeof myAgent>({
  apiUrl: AGENT_URL,
  assistantId: "tool_calling",
});
`&lt;/script&gt;`

<template>
  <div>
    <Message
      v-for="msg in stream.messages.value"
      :key="msg.id"
      :message="msg"
      :tool-calls="stream.toolCalls.value"
    />
  </div>
</template>
svelte
<script lang="ts">
  import { useStream } from "@langchain/svelte";

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

  const stream = useStream<typeof myAgent>({
    apiUrl: AGENT_URL,
    assistantId: "tool_calling",
  });
`&lt;/script&gt;`

<div>
  {#each stream.messages as msg (msg.id)}
    <Message message={msg} toolCalls={stream.toolCalls} />
  {/each}
</div>
ts
import { Component } from "@angular/core";
import { injectStream } from "@langchain/angular";

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

@Component({
  selector: "app-chat",
  template: `
    @for (msg of stream.messages(); track msg.id) {
      <app-message [message]="msg" [toolCalls]="stream.toolCalls()" />
    }
  `,
})
export class ChatComponent {
  stream = injectStream<typeof myAgent>({
    apiUrl: AGENT_URL,
    assistantId: "tool_calling",
  });
}

AssembledToolCall 类型 ​

toolCalls 数组中的每一项都是一个 AssembledToolCall 对象:

ts
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发出工具调用的命名空间
input智能体传递给工具的结构化参数
argsinput 的别名,匹配消息级别的工具调用
output调用成功后的工具输出,运行中或出错后为 null
status生命周期状态:"running"、"finished" 或 "error"
error工具调用失败时的错误详细信息

按消息过滤工具调用 ​

一条 AI 消息可能会触发多个工具调用,而你的聊天中可能包含许多 AI 消息。为了在每条消息下方渲染正确的工具卡片,可以通过将 callId 与消息的 tool_calls 数组进行匹配来过滤:

tsx
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 来选择正确的卡片:

tsx
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} />;
  }
}

天气卡片示例 ​

tsx
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>
  );
}

加载与错误状态 ​

始终处理挂起和错误状态,以便向用户提供清晰的反馈:

tsx
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 工具类型来获取完全类型化的 args:

ts
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 与 WeatherToolCall.args 现在均为 { location: string }

TIP

使用 ToolCallFromTool 可以为你提供编译时类型安全。如果工具的 Schema 发生变更,你的 UI 组件将立即提示类型错误。

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

工具调用通常与流式文本交替到达。useStream Hook 会让 toolCalls 与流保持同步,因此只要智能体发出调用,在工具尚未执行完毕之前,挂起状态的卡片就会立即呈现。

这意味着用户将看到:

  1. 实时流式传输中的 AI 文本
  2. 工具调用发出的那一刻显示的加载卡片
  3. 工具执行完成后卡片更新显示的结果

NOTE

工具调用会就地更新。同一个 callId 会从 "running" 状态转变为 "finished"(或 "error"),因此你的 UI 会携带新状态重新渲染同一个组件。

处理多个并发工具调用 ​

智能体可以并行调用多个工具。toolCalls 数组将同时包含多个 status: "running" 的条目。每个调用都会独立解析完成,因此你的 UI 应当优雅地处理部分完成的情况:

tsx
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 时,请遵循以下指导原则:

  • 始终处理所有三种状态:running、finished 和 error。用户绝不应该看到空白卡片。
  • 安全地验证结果。在为特定卡片收窄类型之前,工具输出的类型均为 unknown。
  • 提供通用的兜底展示。并非每个工具都需要定制卡片。对于未知的工具名称,可以渲染一个可折叠的 JSON 视图。
  • 在加载期间显示工具名称和参数。即使在结果返回之前,用户也想知道智能体正在做什么(what)。
  • 保持卡片紧凑。工具卡片与聊天消息内联显示。避免使用过大的组件打乱对话体验。