19、LangChain 前端:模式 => 工具调用
本文聚焦 Agent 工具调用的前端可视化方案,提供 React/Vue/Svelte/Angular 全框架适配代码,包含加载状态、错误处理及类型安全最佳实践。
文章目录
1. Tool Calling 工作原理
LangGraph Agent 可调用外部工具(天气 API、计算器、网页搜索、数据库查询等),工具调用流程如下:
- Agent 决策需要外部数据时,会在 AI 消息中包含 工具调用指令
- 工具调用指令包含:
name(工具名)、args(结构化参数)、id(唯一标识) - Agent 运行时执行工具,返回结果封装为
ToolMessage useStream钩子将工具调用、执行状态、结果统一为toolCalls数组,前端可直接渲染
核心优势:无需手动处理工具调用与结果的关联,
useStream已实现状态同步
2. 环境搭建:useStream 配置
首先安装核心依赖(以 npm 为例):
# 核心依赖
npm install @langchain/core @langchain/react
# 类型支持(TypeScript 项目)
npm install -D @types/langchain__core
# 工具参数校验(可选,推荐)
npm install zod
2.1 类型定义(TypeScript)
import type { BaseMessage } from "@langchain/core/messages";
// 匹配 Agent 状态结构的接口
interface AgentState {
messages: BaseMessage[]; // 消息列表(包含 AI 消息、人类消息、工具消息)
}
2.2 基础流式组件(React 示例)
import { useStream } from "@langchain/react";
import { AIMessage } from "@langchain/core/messages";
import { Message } from "./Message"; // 后续实现的消息组件
import { ToolCallWithResult } from "@langchain/react/dist/types"; // 工具调用类型
// Agent 服务地址(替换为你的实际地址)
const AGENT_URL = "http://localhost:2024";
export function ToolCallingChat() {
// 初始化流式连接(指定工具调用 Agent ID)
const stream = useStream<AgentState>({
apiUrl: AGENT_URL,
assistantId: "tool_calling", // 替换为你的工具调用 Agent ID
});
return (
<div className="chat-container max-w-3xl mx-auto p-4">
{/* 渲染消息列表,传递工具调用状态 */}
{stream.messages.map((msg) => (
<Message
key={msg.id}
message={msg}
toolCalls={stream.toolCalls as ToolCallWithResult[]}
/>
))}
</div>
);
}
3. 核心类型:ToolCallWithResult 详解
stream.toolCalls 数组中的每个元素均为 ToolCallWithResult 类型,包含工具调用全生命周期信息:
类型定义
interface ToolCallWithResult {
call: {
id: string; // 唯一标识(与 AI 消息的 tool_calls.id 对应)
name: string; // 工具名(如 "get_weather"、"calculator")
args: Record<string, unknown>; // 工具参数(结构化数据)
};
result: ToolMessage | undefined; // 工具执行结果(ToolMessage 类型)
state: "pending" | "completed" | "error"; // 执行状态
}
字段说明
| 字段 | 描述 |
|---|---|
call.id |
唯一标识,用于关联 AI 消息与工具调用结果 |
call.name |
工具名称,用于区分不同工具(如 “get_weather” 对应天气查询工具) |
call.args |
工具的结构化参数(如天气查询的 { location: "北京" }) |
result |
工具执行结果,工具完成后为 ToolMessage 实例,包含 content 字段 |
state |
生命周期状态:pending(执行中)、completed(成功)、error(失败) |
4. 按消息过滤工具调用
一个 AI 消息可能触发多个工具调用,需通过 call.id 关联消息与对应的工具调用,确保工具卡片渲染在正确的消息下方:
import { AIMessage, ToolMessage } from "@langchain/core/messages";
import { ToolCallWithResult } from "@langchain/react/dist/types";
import { ToolCard } from "./ToolCard"; // 后续实现的工具卡片组件
interface MessageProps {
message: AIMessage | ToolMessage;
toolCalls: ToolCallWithResult[];
}
export function Message({ message, toolCalls }: MessageProps) {
// 仅处理 AI 消息(工具调用由 AI 触发)
if (AIMessage.isInstance(message)) {
// 过滤当前消息触发的工具调用(通过 id 匹配)
const messageToolCalls = toolCalls.filter((tc) =>
message.tool_calls?.some((t) => t.id === tc.call.id)
);
return (
<div className="message-bubble mb-4 p-4 border rounded-lg bg-white shadow-sm">
{/* 渲染 AI 消息文本 */}
<p className="text-gray-800 mb-3">{message.content || "正在调用工具..."}{" "}</p>
{/* 渲染当前消息对应的工具卡片 */}
<div className="tool-cards space-y-3">
{messageToolCalls.map((tc) => (
<ToolCard key={tc.call.id} toolCall={tc} />
))}
</div>
</div>
);
}
// 人类消息直接渲染文本
return (
<div className="message-bubble mb-4 p-4 border rounded-lg bg-blue-50">
<p className="text-blue-800">{message.content}</p>
</div>
);
}
5. 定制化工具卡片实现
避免渲染原始 JSON,为不同工具设计专属 UI 卡片,通过 call.name 动态匹配对应的卡片组件:
5.1 工具卡片入口组件
import { ToolCallWithResult } from "@langchain/react/dist/types";
import { WeatherCard } from "./WeatherCard";
import { CalculatorCard } from "./CalculatorCard";
import { SearchCard } from "./SearchCard";
import { LoadingCard } from "./LoadingCard";
import { ErrorCard } from "./ErrorCard";
import { GenericToolCard } from "./GenericToolCard";
interface ToolCardProps {
toolCall: ToolCallWithResult;
}
export function ToolCard({ toolCall }: ToolCardProps) {
// 执行中状态:渲染加载卡片
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} />;
// 未知工具:渲染通用卡片(展示 JSON 数据)
default:
return <GenericToolCard toolCall={toolCall} />;
}
}
5.2 示例:天气工具卡片(WeatherCard)
import { ToolMessage } from "@langchain/core/messages";
// 可使用图标库(如 react-icons)
import { FaCloud, FaSun, FaRain } from "react-icons/fa";
interface WeatherCardProps {
args: { location: string }; // 工具参数(结构化)
result: ToolMessage; // 工具执行结果
}
export function WeatherCard({ args, result }: WeatherCardProps) {
// 安全解析 JSON 结果(避免解析失败崩溃)
let weatherData: { temperature: number; condition: string } | null = null;
try {
weatherData = JSON.parse(result.content as string);
} catch (err) {
return <div className="p-3 border rounded text-red-500">天气数据解析失败</div>;
}
// 根据天气状况选择图标
const getWeatherIcon = () => {
switch (weatherData?.condition.toLowerCase()) {
case "sunny":
return <FaSun className="text-yellow-500" size={24} />;
case "rainy":
return <FaRain className="text-blue-500" size={24} />;
default:
return <FaCloud className="text-gray-500" size={24} />;
}
};
return (
<div className="rounded-lg border p-4 bg-gradient-to-r from-sky-50 to-blue-50">
<div className="flex items-center gap-3 mb-2">
{getWeatherIcon()}
<h3 className="font-semibold text-lg">{args.location} 天气</h3>
</div>
<div className="flex items-baseline gap-2">
<span className="text-3xl font-bold text-gray-800">
{weatherData?.temperature}°C
</span>
<span className="text-gray-600">{weatherData?.condition}</span>
</div>
</div>
);
}
5.3 示例:计算器工具卡片(CalculatorCard)
import { ToolMessage } from "@langchain/core/messages";
interface CalculatorCardProps {
args: { expression: string }; // 计算器表达式(如 "100 + 20 * 3")
result: ToolMessage;
}
export function CalculatorCard({ args, result }: CalculatorCardProps) {
let calculationResult: { value: number } | null = null;
try {
calculationResult = JSON.parse(result.content as string);
} catch (err) {
return <div className="p-3 border rounded text-red-500">计算结果解析失败</div>;
}
return (
<div className="rounded-lg border p-4 bg-gradient-to-r from-gray-50 to-gray-100">
<h3 className="font-semibold mb-2">计算器</h3>
<div className="text-gray-700 mb-1">表达式:{args.expression}</div>
<div className="text-xl font-bold text-gray-900">
结果:{calculationResult?.value}
</div>
</div>
);
}
6. 加载/错误状态处理
为提升用户体验,必须处理 pending(执行中)和 error(失败)状态,提供清晰的反馈:
6.1 加载卡片(LoadingCard)
import { FaSpinner } from "react-icons/fa";
interface LoadingCardProps {
name: string; // 工具名
}
export function LoadingCard({ name }: LoadingCardProps) {
return (
<div className="flex items-center gap-2 rounded-lg border p-4 animate-pulse bg-gray-50">
<FaSpinner className="animate-spin text-blue-500" size={18} />
<span className="text-gray-600">正在执行 {name} 工具...</span>
</div>
);
}
6.2 错误卡片(ErrorCard)
import { ToolMessage } from "@langchain/core/messages";
import { FaExclamationCircle } from "react-icons/fa";
interface ErrorCardProps {
name: string;
error?: ToolMessage; // 错误信息(可能为 undefined)
}
export function ErrorCard({ name, error }: ErrorCardProps) {
return (
<div className="rounded-lg border border-red-300 bg-red-50 p-4">
<div className="flex items-center gap-2 mb-1">
<FaExclamationCircle className="text-red-500" size={18} />
<h3 className="font-semibold text-red-700">{name} 工具执行失败</h3>
</div>
<p className="text-sm text-red-600">
{error?.content ?? "未知错误,请稍后重试"}
</p>
</div>
);
}
6.3 通用卡片(GenericToolCard)
为未知工具提供通用渲染方案,展示原始参数和结果( collapsible 折叠面板):
import { useState } from "react";
import { ToolCallWithResult } from "@langchain/react/dist/types";
import { FaChevronDown, FaChevronUp } from "react-icons/fa";
interface GenericToolCardProps {
toolCall: ToolCallWithResult;
}
export function GenericToolCard({ toolCall }: GenericToolCardProps) {
const [isExpanded, setIsExpanded] = useState(false);
return (
<div className="rounded-lg border p-4 bg-gray-50">
<div
className="flex items-center justify-between cursor-pointer"
onClick={() => setIsExpanded(!isExpanded)}
>
<h3 className="font-semibold text-gray-800">{toolCall.call.name} 工具</h3>
{isExpanded ? <FaChevronUp size={16} /> : <FaChevronDown size={16} />}
</div>
{isExpanded && (
<div className="mt-3 space-y-2 text-sm">
<div>
<span className="font-medium">参数:</span>
<pre className="mt-1 p-2 bg-white rounded border text-gray-700 overflow-x-auto">
{JSON.stringify(toolCall.call.args, null, 2)}
</pre>
</div>
<div>
<span className="font-medium">结果:</span>
<pre className="mt-1 p-2 bg-white rounded border text-gray-700 overflow-x-auto">
{JSON.stringify(toolCall.result?.content, null, 2)}
</pre>
</div>
</div>
)}
</div>
);
}
7. 类型安全:工具参数校验
使用 zod 定义工具参数 schema,结合 ToolCallFromTool 类型,实现前端参数类型安全:
7.1 工具定义(后端/共享层)
import { tool } from "@langchain/core/tools";
import { z } from "zod";
// 定义天气工具参数 schema
const weatherToolSchema = z.object({
location: z.string().describe("城市名称(如:北京、上海)"),
unit: z.optional(z.enum(["celsius", "fahrenheit"]).describe("温度单位,默认摄氏度"))
});
// 定义天气工具
export const getWeather = tool(
async ({ location, unit = "celsius" }) => {
// 调用天气 API 获取数据(示例逻辑)
const response = await fetch(`https://api.weatherapi.com/v1/current.json?key=YOUR_KEY&q=${location}`);
const data = await response.json();
return JSON.stringify({
temperature: unit === "celsius" ? data.current.temp_c : data.current.temp_f,
condition: data.current.condition.text
});
},
{
name: "get_weather",
description: "获取指定城市的实时天气",
schema: weatherToolSchema // 关联参数 schema
}
);
7.2 前端类型推导
import { ToolCallFromTool } from "@langchain/core/tools";
import { getWeather } from "./tools";
// 从工具定义推导工具调用类型(参数自动类型安全)
type WeatherToolCall = ToolCallFromTool<typeof getWeather>;
// 此时 WeatherToolCall.call.args 类型为:
// {
// location: string;
// unit?: "celsius" | "fahrenheit" | undefined;
// }
// 在组件中使用
function WeatherCardTyped({ toolCall }: { toolCall: WeatherToolCall }) {
// args 自动提示 location 和 unit,类型错误会在编译时报错
const { location, unit = "celsius" } = toolCall.call.args;
const tempUnit = unit === "celsius" ? "°C" : "°F";
// ... 其余逻辑
}
8. 流式文本与工具调用联动
useStream 会同步处理流式文本和工具调用,实现以下交互效果:
- AI 文本流式输出时,工具调用指令实时触发
- 工具调用一触发,立即渲染
pending状态卡片 - 工具执行完成后,自动更新为
completed状态并展示结果
关键特性
- 工具卡片与流式文本实时联动,无需手动刷新
- 同一
call.id贯穿全生命周期,状态更新时组件自动重渲染 - 支持文本与工具调用** interleaved(交错)** 输出
示例效果
AI 消息:正在为你查询北京的天气...
[加载中] 正在执行 get_weather 工具...
(1秒后)
[完成] 北京 天气
25°C 晴
9. 并发工具调用处理
Agent 支持并行调用多个工具(如同时查询天气和执行计算),前端需处理多个 pending 状态的工具卡片:
并发工具调用列表组件
import { ToolCallWithResult } from "@langchain/react/dist/types";
import { ToolCard } from "./ToolCard";
interface ToolCallListProps {
toolCalls: ToolCallWithResult[];
}
export function ToolCallList({ toolCalls }: ToolCallListProps) {
// 按状态分组(已完成在前,执行中在后)
const completedCalls = toolCalls.filter(tc => tc.state === "completed");
const pendingCalls = toolCalls.filter(tc => tc.state === "pending");
const errorCalls = toolCalls.filter(tc => tc.state === "error");
return (
<div className="space-y-3">
{/* 已完成的工具调用 */}
{completedCalls.map(tc => (
<ToolCard key={tc.call.id} toolCall={tc} />
))}
{/* 执行中的工具调用 */}
{pendingCalls.map(tc => (
<ToolCard key={tc.call.id} toolCall={tc} />
))}
{/* 执行失败的工具调用 */}
{errorCalls.map(tc => (
<ToolCard key={tc.call.id} toolCall={tc} />
))}
</div>
);
}
使用方式
// 在 Message 组件中替换原工具卡片渲染逻辑
<div className="tool-cards space-y-3">
<ToolCallList toolCalls={messageToolCalls} />
</div>
10. 生产环境最佳实践
- 完整状态覆盖:必须处理
pending、completed、error三种状态,避免空白卡片 - 安全解析 JSON:工具结果为字符串,
JSON.parse()必须包裹在try/catch中,提供 fallback UI - 通用卡片兜底:为未知工具提供通用渲染方案,避免 UI 崩溃
- 加载状态透明化:显示工具名称和参数,让用户知道 Agent 正在执行的操作
- 卡片样式紧凑:工具卡片嵌入聊天消息,避免过大尺寸影响对话体验
- 类型安全优先:使用
zod定义工具 schema,结合ToolCallFromTool实现编译时类型校验 - 性能优化:
- 避免频繁重渲染(使用 React.memo/Vue computed 缓存组件)
- 长文本结果使用虚拟滚动(如超过 1000 字的网页搜索结果)
- 可访问性:
- 为工具卡片添加 ARIA 标签(如
aria-label="天气工具结果") - 加载状态添加进度提示,支持键盘导航
- 为工具卡片添加 ARIA 标签(如
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)