外观
结构化输出(Structured output)让智能体返回类型化的、机器可读的数据,而不是纯文本。你获得的不是一个字符串,而是一个结构化的对象,可以映射到任何界面(UI):卡片、表格、图表、逐步拆解或领域特定的渲染器。
import { PatternEmbed } from "/snippets/pattern-embed.jsx"
什么是结构化输出?
智能体不是返回自由形式的文本响应,而是通过工具调用来返回符合预定义模式的结构化对象。这为你带来:
- 类型安全的数据:将响应解析为已知的 TypeScript 类型
- 精确的渲染控制:为每个字段提供各自的界面渲染方式
- 一致的格式:无论底层模型是什么,每个响应都遵循相同的结构
智能体通过调用一个"结构化输出"工具来实现这一点,该工具的参数中包含响应数据。工具本身不执行任何逻辑,纯粹是返回类型化数据的载体。
使用场景
- 产品对比:功能表格、优缺点列表、评分
- 数据分析:包含指标、明细和要点的摘要
- 逐步指南:带有描述和代码片段的有序指令
- 食谱:食材、步骤、时间和营养成分
- 数学与科学:用 LaTeX 渲染的公式、逐步推导
- 旅行规划:包含日期、地点和费用估算的行程安排
定义模式
为智能体返回的结构化数据定义一个 TypeScript 类型。此模式的形状决定了你如何渲染界面。
以下是内嵌演示所使用的数学解答模式:
ts
interface MathSolution {
problem: string; // The original math problem
steps: {
explanation: string;
latex: string; // Optional display math for this step
}[]; // Step-by-step derivation
finalAnswer: string; // Plain-text final answer
finalAnswerLatex: string; // LaTeX representation of the final answer
}你的模式可以是任何内容。无论形状如何,此模式的工作方式都相同。
从消息中提取结构化输出
结构化输出位于最后一条 AIMessage 的 tool_calls 数组中。通过找到 AI 消息并访问第一个工具调用的参数来提取它:
ts
import { AIMessage } from "langchain";
function extractStructuredOutput<T>(messages: any[]): T | null {
const aiMessage = messages.find(AIMessage.isInstance);
const toolCall = aiMessage?.tool_calls?.[0];
if (!toolCall) return null;
return toolCall.args as T;
}INFO
在智能体完成流式输出之前,结构化输出的工具调用的 args 可能尚未填充。在流式输出期间,args 可能部分填充或为 undefined。渲染前务必检查其完整性。
设置 useStream
将 useStream 连接到你的结构化输出智能体,然后读取 stream.messages,并从最新的 AIMessage 工具调用中提取类型化的载荷。在 args 完整后渲染你的自定义界面,在 stream.isLoading 为 true 时显示加载状态(工具参数可能会逐渐流式到达),并使用 stream.submit() 发送下一个提示词。
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";
import { AIMessage } from "langchain";
function MathSolutionChat() {
const stream = useStream<typeof myAgent>({
apiUrl: "http://localhost:2024",
assistantId: "structured_output_latex",
});
const solution = extractStructuredOutput<MathSolution>(stream.messages);
return (
{!solution && !stream.isLoading && (
<PromptInput onSubmit={(text) =>
stream.submit({ messages: [{ type: "human", content: text }] })
} />
)}
{stream.isLoading && <LoadingIndicator />}
{solution && <SolutionCard solution={solution} />}
);
}vue
<script setup lang="ts">
import { useStream } from "@langchain/vue";
import { AIMessage } from "langchain";
import { computed } from "vue";
const stream = useStream<typeof myAgent>({
apiUrl: "http://localhost:2024",
assistantId: "structured_output_latex",
});
const solution = computed(() =>
extractStructuredOutput<MathSolution>(stream.messages.value)
);
function handleSubmit(text: string) {
stream.submit({ messages: [{ type: "human", content: text }] });
}
</script>
<template>
<PromptInput v-if="!solution && !stream.isLoading" @submit="handleSubmit" />
<LoadingIndicator v-if="stream.isLoading" />
<SolutionCard v-if="solution" :solution="solution" />
</template>svelte
<script lang="ts">
import { useStream } from "@langchain/svelte";
import { AIMessage } from "langchain";
const stream = useStream<typeof myAgent>({
apiUrl: "http://localhost:2024",
assistantId: "structured_output_latex",
});
const solution = $derived(extractStructuredOutput<MathSolution>(stream.messages));
function handleSubmit(text: string) {
stream.submit({ messages: [{ type: "human", content: text }] });
}
</script>
{#if !solution && !stream.isLoading}
<PromptInput on:submit={(e) => handleSubmit(e.detail)} />
{/if}
{#if stream.isLoading}
<LoadingIndicator />
{/if}
{#if solution}
<SolutionCard {solution} />
{/if}ts
import { Component, computed } from "@angular/core";
import { injectStream } from "@langchain/angular";
@Component({
selector: "app-math-solution-chat",
template: `
@if (!solution() && !stream.isLoading()) {
<prompt-input (onSubmit)="handleSubmit($event)" />
}
@if (stream.isLoading()) {
<loading-indicator />
}
@if (solution()) {
<solution-card [solution]="solution()" />
}
`,
})
export class MathSolutionChatComponent {
stream = injectStream<typeof myAgent>({
apiUrl: "http://localhost:2024",
assistantId: "structured_output_latex",
});
solution = computed(() =>
extractStructuredOutput<MathSolution>(this.stream.messages())
);
handleSubmit(text: string) {
this.stream.submit({
messages: [{ type: "human", content: text }],
});
}
}渲染结构化数据
获得类型化的对象后,构建一个将每个字段映射到 合适界面元素的组件。这是此模式的核心:将结构化 数据转化为专门构建的界面。
tsx
function LatexBlock({ latex }: { latex: string }) {
return {latex}; // Render with KaTeX or MathJax.
}
function SolutionCard({ solution }: { solution: MathSolution }) {
return (
<h3>{solution.problem}</h3>
{solution.steps.map((step, i) => (
{step.explanation}
{step.latex && <LatexBlock latex={step.latex} />}
))}
{solution.finalAnswer}
{solution.finalAnswerLatex && <LatexBlock latex={solution.finalAnswerLatex} />}
);
}处理部分流式数据
在流式输出期间,工具调用的参数可能是不完整的 JSON。在提取逻辑中防范这一点:
ts
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 参数等待关键字段填充完毕后再渲染:
ts
const solution = extractStructuredOutput<MathSolution>(stream.messages, [
"problem",
"steps",
"finalAnswer",
]);在流式输出期间渐进式渲染
与其等待完整的结构化输出,不如在字段到达时渲染它们。这会在智能体仍在生成时为用户提供即时反馈:
tsx
function ProgressiveSolutionCard({ messages }: { messages: any[] }) {
const partial = extractStructuredOutput<Partial<MathSolution>>(messages);
if (!partial) return null;
return (
{partial.problem && <h3>{partial.problem}</h3>}
{partial.steps && partial.steps.length > 0 && (
<h4>Steps</h4>
{partial.steps.map((step, i) => (
Step {i + 1}
{step.explanation}
{step.latex && <LatexBlock latex={step.latex} />}
))}
)}
{partial.finalAnswer && {partial.finalAnswer}}
);
}TIP
当模式具有自然的自上而下的顺序时,渐进式渲染效果很好:先是题目,然后是推导步骤,最后是最终答案。智能体通常会按模式顺序生成字段,因此界面会自然地逐步填充。
最佳实践
- 渲染前先验证:因为流式输出可能传送部分数据,所以渲染前务必检查必需字段是否存在
- 使用通用提取函数:用类型和必需字段将提取逻辑参数化,以便它适用于不同的模式
- 渐进式渲染:在字段到达时显示它们,而不是等待完整的对象,让用户看到即时反馈
- 提供回退表示:如果某个字段支持富渲染(LaTeX、Markdown、图表),请在模式中同时加入纯文本等价形式作为回退
- 尽可能保持模式扁平:深度嵌套的模式更难渐进式渲染,也更可能在部分流式输出期间出错
- 让界面匹配数据:选择最能代表每种字段类型的渲染策略(数组用表格、嵌套对象用卡片、状态字段用徽标)