结构化输出让代理返回类型化、机器可读的数据,而不是纯文本。你不再渲染单个字符串,而是得到一个结构化对象,可将其映射到任何 UI:卡片、表格、图表、逐步拆解,或领域专用渲染器。

什么是结构化输出?

代理不返回自由格式文本响应,而是使用工具调用返回符合预定义 schema 的结构化对象。这让你获得:
  • 类型安全数据:将响应解析为已知 TypeScript 类型
  • 精确渲染控制:用各自 UI 方式渲染每个字段
  • 一致格式:无论底层模型如何,每个响应都遵循相同结构
代理通过调用“structured output”工具来完成此操作,该工具的参数包含响应数据。工具本身不执行任何逻辑,只是返回类型化数据的载体。

用例

  • 产品对比:功能表、优缺点列表、评分
  • 数据分析:包含指标、拆解和重点的摘要
  • 分步指南:带说明和代码片段的有序步骤
  • 食谱:食材、步骤、时间安排和营养信息
  • 数学和科学:用 LaTeX 渲染公式、逐步推导
  • 旅行规划:包含日期、地点和费用估算的行程

定义 schema

为代理返回的结构化数据定义 TypeScript 类型。该 schema 的形状决定你如何渲染 UI。 下面是嵌入式演示使用的数学解答 schema:
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
}
schema 可以是任何形状。无论形状如何,此模式的工作方式都相同。

从消息中提取结构化输出

结构化输出位于最后一个 AIMessagetool_calls 数组中。找到 AI 消息,并访问第一个工具调用的参数即可提取它:
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;
}
结构化输出工具调用可能要到代理完成流式传输后才会填充 args。在流式传输期间,args 可能只被部分填充或为 undefined。渲染前始终检查完整性。

设置 useStream

useStream 连接到结构化输出代理,然后读取 stream.messages,并从最新 AIMessage 工具调用中提取类型化 payload。args 完整后渲染自定义 UI;当 stream.isLoading 为 true 时显示加载状态(工具参数可能会逐步流入);使用 stream.submit() 发送下一个提示词。
The code examples use useStream<typeof myAgent> for type-safe stream state. See Type inference for Python or JavaScript backends.
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 (
    <div>
      {!solution && !stream.isLoading && (
        <PromptInput onSubmit={(text) =>
          stream.submit({ messages: [{ type: "human", content: text }] })
        } />
      )}
      {stream.isLoading && <LoadingIndicator />}
      {solution && <SolutionCard solution={solution} />}
    </div>
  );
}

渲染结构化数据

获得类型化对象后,构建一个组件,将每个字段映射到合适的 UI 元素。这是该模式的核心:将结构化数据转换为专门构建的界面。
function LatexBlock({ latex }: { latex: string }) {
  return <div className="latex-block">{latex}</div>; // Render with KaTeX or MathJax.
}

function SolutionCard({ solution }: { solution: MathSolution }) {
  return (
    <div className="solution-card">
      <h3>{solution.problem}</h3>
      <ol>
        {solution.steps.map((step, i) => (
          <li key={i}>
            <span>{step.explanation}</span>
            {step.latex && <LatexBlock latex={step.latex} />}
          </li>
        ))}
      </ol>
      <strong>{solution.finalAnswer}</strong>
      {solution.finalAnswerLatex && <LatexBlock latex={solution.finalAnswerLatex} />}
    </div>
  );
}

处理局部流式数据

流式传输期间,工具调用参数可能是不完整 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 solution = extractStructuredOutput<MathSolution>(stream.messages, [
  "problem",
  "steps",
  "finalAnswer",
]);

在流式传输期间渐进式渲染

不要等待完整结构化输出,而是随着字段到达逐步渲染。这会在代理仍在生成时给用户即时反馈:
function ProgressiveSolutionCard({ messages }: { messages: any[] }) {
  const partial = extractStructuredOutput<Partial<MathSolution>>(messages);
  if (!partial) return null;

  return (
    <div className="solution-card">
      {partial.problem && <h3>{partial.problem}</h3>}

      {partial.steps && partial.steps.length > 0 && (
        <div className="solution-steps">
          <h4>Steps</h4>
          {partial.steps.map((step, i) => (
            <div key={i} className="step">
              <div className="step-number">Step {i + 1}</div>
              <p>{step.explanation}</p>
              {step.latex && <LatexBlock latex={step.latex} />}
            </div>
          ))}
        </div>
      )}

      {partial.finalAnswer && <strong>{partial.finalAnswer}</strong>}
    </div>
  );
}
当 schema 具有自然的从上到下顺序时,渐进式渲染效果很好:先是问题,然后是推导步骤,最后是最终答案。代理通常按 schema 顺序生成字段,因此 UI 会自然填充。

最佳实践

  • 渲染前验证:渲染前始终检查必需字段是否存在,因为流式传输可能交付局部数据
  • 使用通用提取函数:用类型和必需字段参数化提取逻辑,使其可跨不同 schema 工作
  • 渐进式渲染:字段到达时就显示,而不是等待完整对象,让用户看到即时反馈
  • 提供 fallback 表示:如果字段支持富渲染(LaTeX、Markdown、图表),也在 schema 中包含纯文本等价形式作为 fallback
  • 尽量保持 schema 扁平:深层嵌套 schema 更难渐进式渲染,也更可能在局部流式传输期间出错
  • 让 UI 匹配数据:选择最能表示各字段类型的渲染策略(数组用表格、嵌套对象用卡片、状态字段用徽章)