Documentation Index
Fetch the complete documentation index at: https://nvd-54.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
结构化输出让智能体返回类型化的、机器可读的数据而非纯文本。你不再渲染单个字符串,而是获得一个结构化对象,可以将其映射到任何 UI:卡片、表格、图表、逐步分解或领域特定的渲染器。
什么是结构化输出?
智能体不再返回自由格式的文本响应,而是使用工具调用来返回符合预定义 schema 的结构化对象。这为你提供了:
- 类型安全的数据:将响应解析为已知的 TypeScript 类型
- 精确的渲染控制:用各自的 UI 处理方式渲染每个字段
- 一致的格式:无论底层模型如何,每个响应都遵循相同的结构
智能体通过调用一个”结构化输出”工具来实现,该工具的参数包含响应数据。工具本身不执行任何逻辑——它纯粹是返回类型化数据的载体。
- 产品对比:功能表格、优缺点列表、评分
- 数据分析:带有指标、细分和亮点的摘要
- 分步指南:带描述和代码片段的有序指令
- 食谱:食材、步骤、时间和营养信息
- 数学和科学:用 LaTeX 渲染公式、逐步推导
- 旅行规划:带日期、地点和费用估算的行程
定义 schema
为智能体返回的结构化数据定义 TypeScript 类型。此 schema 的形状决定了你如何渲染 UI。
以下是食谱助手的示例:
interface Ingredient {
name: string;
amount: string;
unit: string;
}
interface RecipeStep {
instruction: string;
duration?: string;
}
interface Recipe {
title: string;
description: string;
servings: number;
ingredients: Ingredient[];
steps: RecipeStep[];
totalTime: string;
}
| 字段 | 类型 | 描述 |
|---|
title | string | 食谱名称 |
description | string | 菜品简短摘要 |
servings | number | 份数 |
ingredients | Ingredient[] | 带数量和单位的食材列表 |
steps | RecipeStep[] | 有序的制作步骤 |
totalTime | string | 预计总准备和烹饪时间 |
你的 schema 可以是任何形式。无论形状如何,模式的工作方式相同。
从消息中提取结构化输出
结构化输出存在于最后一条 AIMessage 的 tool_calls 数组中。通过找到 AI 消息并访问第一个工具调用的参数来提取:
import { AIMessage } from "@langchain/core/messages";
function extractStructuredOutput<T>(messages: any[]): T | null {
const aiMessages = messages.filter(AIMessage.isInstance);
if (aiMessages.length === 0) return null;
const lastAI = aiMessages[aiMessages.length - 1];
const toolCall = lastAI.tool_calls?.[0];
if (!toolCall) return null;
return toolCall.args as T;
}
结构化输出工具调用可能在智能体完成流式输出之前没有填充 args。流式输出期间,args 可能部分填充或未定义。渲染前始终检查完整性。
设置 useStream
定义一个与你的智能体状态 schema 匹配的 TypeScript 接口,并将其作为类型参数传递给 useStream,以获得类型安全的状态值访问。在以下示例中,将 typeof myAgent 替换为你的接口名称:
import type { BaseMessage } from "@langchain/core/messages";
interface AgentState {
messages: BaseMessage[];
}
import { useStream } from "@langchain/react";
import { AIMessage } from "@langchain/core/messages";
function RecipeChat() {
const stream = useStream<typeof myAgent>({
apiUrl: "http://localhost:2024",
assistantId: "recipe_assistant",
});
const recipe = extractStructuredOutput<Recipe>(stream.messages);
return (
<div>
{!recipe && !stream.isLoading && (
<PromptInput onSubmit={(text) =>
stream.submit({ messages: [{ type: "human", content: text }] })
} />
)}
{stream.isLoading && <LoadingIndicator />}
{recipe && <RecipeCard recipe={recipe} />}
</div>
);
}
渲染结构化数据
一旦你有了类型化的对象,构建一个将每个字段映射到合适 UI 元素的组件。这是模式的核心:将结构化数据转化为专用界面。
function RecipeCard({ recipe }: { recipe: Recipe }) {
return (
<div className="recipe-card">
<div className="recipe-header">
<h3>{recipe.title}</h3>
<p className="recipe-description">{recipe.description}</p>
<div className="recipe-meta">
<span>{recipe.servings} 份</span>
<span>{recipe.totalTime}</span>
</div>
</div>
<div className="recipe-ingredients">
<h4>食材</h4>
<ul>
{recipe.ingredients.map((ing, i) => (
<li key={i}>
<strong>{ing.amount} {ing.unit}</strong> {ing.name}
</li>
))}
</ul>
</div>
<div className="recipe-steps">
<h4>制作步骤</h4>
{recipe.steps.map((step, i) => (
<div key={i} className="step">
<div className="step-number">步骤 {i + 1}</div>
<p className="step-instruction">{step.instruction}</p>
{step.duration && (
<span className="step-duration">{step.duration}</span>
)}
</div>
))}
</div>
</div>
);
}
相同的方法适用于任何领域。将每个字段映射到最能表示它的 UI 元素:
| 数据类型 | 渲染策略 |
|---|
| 纯文本 | 段落、标题、列表项 |
| 数字/指标 | 统计卡片、进度条、徽章 |
| 数组 | 列表、表格、网格 |
| 嵌套对象 | 嵌套卡片、手风琴部分 |
| Markdown | Markdown 渲染器(如 react-markdown) |
| LaTeX/数学 | KaTeX 或 MathJax |
| 日期/时间 | 格式化时间戳、相对时间 |
| URL | 链接、嵌入式预览 |
处理流式部分数据
在流式输出期间,工具调用参数可能是不完整的 JSON。在提取逻辑中进行保护:
function extractStructuredOutput<T>(
messages: any[],
requiredFields: string[] = [],
): T | null {
const aiMessages = messages.filter(AIMessage.isInstance);
if (aiMessages.length === 0) return null;
const lastAI = aiMessages[aiMessages.length - 1];
const toolCall = lastAI.tool_calls?.[0];
if (!toolCall?.args) return null;
const args = toolCall.args as Record<string, unknown>;
const hasRequired = requiredFields.every(
(field) => args[field] !== undefined
);
if (requiredFields.length > 0 && !hasRequired) return null;
return args as T;
}
使用 requiredFields 参数等到关键字段填充后再渲染:
const recipe = extractStructuredOutput<Recipe>(stream.messages, [
"title",
"ingredients",
"steps",
]);
流式输出期间渐进式渲染
不必等待完整的结构化输出,可以在字段到达时就渲染它们。这让用户在智能体仍在生成时就能获得即时反馈:
function ProgressiveRecipeCard({ messages }: { messages: any[] }) {
const partial = extractStructuredOutput<Partial<Recipe>>(messages);
if (!partial) return null;
return (
<div className="recipe-card">
{partial.title && <h3>{partial.title}</h3>}
{partial.description && <p>{partial.description}</p>}
{partial.ingredients && partial.ingredients.length > 0 && (
<div className="recipe-ingredients">
<h4>食材</h4>
<ul>
{partial.ingredients.map((ing, i) => (
<li key={i}>
{ing.amount} {ing.unit} {ing.name}
</li>
))}
</ul>
</div>
)}
{partial.steps && partial.steps.length > 0 && (
<div className="recipe-steps">
<h4>制作步骤</h4>
{partial.steps.map((step, i) => (
<div key={i} className="step">
<div className="step-number">步骤 {i + 1}</div>
<p>{step.instruction}</p>
</div>
))}
</div>
)}
</div>
);
}
当 schema 具有自然的从上到下的顺序时,渐进式渲染效果最好:标题、然后描述、然后详情。智能体通常按 schema 顺序生成字段,因此 UI 自然地逐步填充。
重置和重新提交
要让用户在查看结果后提交新查询,添加一个开始新线程的按钮:
{recipe && (
<button onClick={() => stream.switchThread(null)}>
重新开始
</button>
)}
这会清除当前对话,让用户开始全新的交互。
最佳实践
- 渲染前验证:始终在渲染前检查必需字段是否存在,因为流式输出可能传递部分数据
- 使用通用提取函数:用类型和必需字段参数化你的提取逻辑,使其可跨不同 schema 工作
- 渐进式渲染:在字段到达时就显示,而不是等待完整对象,让用户看到即时反馈
- 提供回退表示:如果字段支持富渲染(LaTeX、Markdown、图表),在 schema 中也包含纯文本等效物作为回退
- 尽量保持 schema 扁平:深度嵌套的 schema 更难渐进式渲染,在部分流式输出期间更容易中断
- 匹配 UI 与数据:选择最能表示每个字段类型的渲染策略(数组用表格,嵌套对象用卡片,状态字段用徽章)
将这些文档连接到 Claude、VSCode 等工具,通过 MCP 获取实时答案。