外观
智能体可以调用外部工具,如天气 API、计算器、网页搜索、数据库查询等。结果以原始 JSON 形式返回。此模式向你展示如何为智能体发出的每次工具调用渲染结构化、类型安全的界面卡片,并带有加载状态和错误处理。
import { PatternEmbed } from "/snippets/pattern-embed.jsx"
工具调用是如何工作的
当 LangGraph 智能体决定需要外部数据时,它会作为 AI 消息的一部分发出一到多次工具调用。每次工具调用都包括:
- name:被调用的工具(例如
"get_weather"、"calculator") - args:传递给工具的结构化参数
- id:将该调用与其结果关联起来的唯一标识符
智能体运行时执行该工具,结果以 ToolMessage 的形式返回。useStream hook 将这一切统一为一个你可以直接渲染的 toolCalls 数组。
设置 useStream
第一步是将 useStream 连接到你的智能体后端。该 hook 返回响应式状态,包括一个 toolCalls 数组,它会随着智能体的流式输出而实时更新。
INFO
The code examples use useStream<typeof myAgent> for type-safe stream state. See Type inference for Python or JavaScript backends.
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 (
{stream.messages.map((msg) => (
<Message key={msg.id} message={msg} toolCalls={stream.toolCalls} />
))}
);
}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",
});
</script>
<template>
<Message
v-for="msg in stream.messages.value"
:key="msg.id"
:message="msg"
:tool-calls="stream.toolCalls.value"
/>
</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",
});
</script>
{#each stream.messages as msg (msg.id)}
<Message message={msg} toolCalls={stream.toolCalls} />
{/each}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 |
id | callId 的别名,与消息级工具调用匹配 |
namespace | 发出工具调用的命名空间 |
input | 智能体传给工具的结构化参数 |
args | input 的别名,与消息级工具调用匹配 |
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 (
{message.text}
{messageToolCalls.map((tc) => (
<ToolCard key={tc.callId} toolCall={tc} />
))}
);
}构建专用的工具卡片
不要倾倒原始 JSON,而是为每个工具构建专用的界面组件。使用 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 (
<CloudIcon />
<h3 className="font-semibold">{input.location}</h3>
{output.temperature}°F
{output.condition}
);
}加载和错误状态
始终处理待处理和错误状态,以便给用户清晰的反馈:
tsx
function LoadingCard({ name }: { name: string }) {
return (
<Spinner />
Running {name}...
);
}
function ErrorCard({ name, error }: { name: string; error?: unknown }) {
return (
<h3 className="font-semibold text-red-700">Error in {name}</h3>
{String(error ?? "Tool execution failed")}
);
}类型安全的工具参数
如果你的工具使用结构化 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 发生变化,你的界面组件会立即标记出类型错误。
在流式文本中内联渲染工具调用
工具调用通常会与流式文本交错到达。useStream hook 会让 toolCalls 与流保持同步,因此待处理卡片会在智能体一发出调用时就出现,早于工具执行完成。
这意味着用户会看到:
- AI 的文本在流式输入时显示
- 工具调用一发出就出现加载卡片
- 工具完成后,卡片更新为显示结果
INFO
工具调用会就地更新。同一个 callId 会从 "running" 转为 "finished"(或 "error"),因此你的界面会用新的状态重新渲染同一个组件。
处理多个并发工具调用
智能体可以并行调用多个工具。toolCalls 数组会同时包含多条 status: "running" 的条目。每条都会独立完成,因此你的界面应该优雅地处理部分完成:
tsx
function ToolCallList({ toolCalls }: { toolCalls: AssembledToolCall[] }) {
const pending = toolCalls.filter((tc) => tc.status === "running");
const completed = toolCalls.filter((tc) => tc.status === "finished");
return (
{completed.map((tc) => (
<ToolCard key={tc.callId} toolCall={tc} />
))}
{pending.map((tc) => (
<LoadingCard key={tc.callId} name={tc.name} />
))}
);
}最佳实践
在构建工具调用界面时遵循以下指南:
- 始终处理全部三种状态:
running、finished和error。用户绝不应该看到空白的卡片。 - 安全地验证结果。工具输出的类型是
unknown,直到你为特定卡片收窄它。 - 提供通用回退。并非每个工具都需要定制卡片。对未知工具名称渲染一个可折叠的 JSON 视图。
- 在加载期间显示工具名称和参数。用户想知道智能体正在做什么,即使结果还没到。
- 保持卡片紧凑。工具卡片与聊天消息内联排列。避免用过于庞大的组件压垮对话。