LLM 结构化输出进阶:withStructuredOutput 一行封装 Tool Call,从流式输出到 MySQL 落地

LLM 结构化输出进阶:withStructuredOutput 一行封装 Tool Call,从流式输出到 MySQL 落地

上一篇讲了 LLM 结构化输出的四种方案:JsonOutputParser、fromNamesAndDescriptions、fromZodSchema、Tool Call。其中 Tool Call 是最可靠的------但 model.bindTools 的写法有点"讨巧",语义不够清晰。LangChain 提供了一行封装的终极方案:model.withStructuredOutput(schema)。本文从 bindTools 到 withStructuredOutput 的进化讲起,覆盖流式结构化输出、XMLOutputParser 历史遗产、再到 MySQL 建表落库------完整走一遍"LLM 输出结构化数据 → 存入数据库"的工程链路。建议收藏后动手实操。


一、回顾:结构化输出的四种方案

1.1 为什么需要结构化输出

javascript 复制代码
LLM 输出 = 自由文本字符串

  问题:
  → 格式随机(可能带 markdown 代码块)
  → 字段名随机(name 还是"姓名"?)
  → 类型随机(birth_year 是 1879 还是 "1879"?)
  → JSON.parse 直接翻车

  目标:
  → 格式确定
  → 字段确定
  → 类型安全
  → 可靠解析

1.2 四种方案的进化

vbnet 复制代码
┌──────────────────────────────────────────────────────────────┐
│  结构化输出方案进化链                                         │
│                                                              │
│  ① JsonOutputParser                                         │
│  → 只说"请输出 JSON"                                        │
│  → 自动剥离 markdown 代码块                                 │
│  → 不约束字段名和类型                                       │
│  → 可靠性 ★★                                               │
│                                                              │
│  ② StructuredOutputParser.fromNamesAndDescriptions          │
│  → 字段名 + 描述                                            │
│  → 但所有值都是 string                                     │
│  → 不支持嵌套结构                                           │
│  → 可靠性 ★★★                                              │
│                                                              │
│  ③ StructuredOutputParser.fromZodSchema                     │
│  → Zod Schema 精确类型                                      │
│  → 嵌套对象 / 数组 / 可选字段                              │
│  → 解析时类型校验                                           │
│  → 可靠性 ★★★★                                             │
│                                                              │
│  ④ Tool Call(bindTools)                                   │
│  → 利用模型原生 Tool Calling 能力                           │
│  → Schema 通过 API 传递                                     │
│  → tool_calls[0].args 直接是结构化对象                      │
│  → 不需要 parse                                             │
│  → 可靠性 ★★★★★                                            │
│  → 但写法"讨巧",语义不够清晰                              │
│                                                              │
│  ⑤ withStructuredOutput(本文主角)                          │
│  → 一行封装 Tool Call                                       │
│  → 语义清晰:我要"带结构化输出的模型"                       │
│  → 支持 invoke / stream / batch                            │
│  → 底层仍然是 Tool Call                                    │
└──────────────────────────────────────────────────────────────┘

  本文主线:
  → Tool Call 的"讨巧写法" → withStructuredOutput 的"语义化封装"
  → 流式结构化输出(一边生成一边出结构化数据)
  → XMLOutputParser(XML 时代的遗产)
  → MySQL 落库(结构化数据的最终归宿)

二、withStructuredOutput:语义化的终极封装

2.1 bindTools 的"讨巧"写法

javascript 复制代码
// tool-call-args.mjs --- 上一课的 Tool Call 写法
import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
import { z } from 'zod';

const model = new ChatOpenAI({
  modelName: process.env.MODEL_NAME,
  apiKey: process.env.OPENAI_API_KEY,
  temperature: 0,
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL,
  },
});

const scientistSchema = z.object({
  name: z.string().describe('科学家的姓名'),
  birth_year: z.number().describe('出生年份'),
  nationality: z.string().describe('国籍'),
  fields: z.array(z.string()).describe('研究领域列表'),
});

// 绑定一个"假的"工具 ------ 不是为了调用它
// 而是利用 Tool Call 的 Schema 校验能力
const modelWithTool = model.bindTools([
  {
    name: 'extract_scientist_info',
    description: '提取和结构化科学家的详细信息',
    schema: scientistSchema,
  },
]);

const response = await modelWithTool.invoke('介绍一下爱因斯坦');
console.log(response.tool_calls[0].args);
// { name: "爱因斯坦", birth_year: 1879, nationality: "德国", fields: ["物理学"] }
vbscript 复制代码
bindTools 的问题:

  ┌──────────────────────────────────────────────────────────┐
│  ① 语义不清晰                                            │
│  → 我们根本不想"调用工具"                                │
│  → 我们想要"结构化输出"                                  │
│  → 但必须假装定义了一个工具                              │
│  → 还要给工具起名、写描述(extract_scientist_info)      │
│                                                          │
│  ② 结果藏得深                                            │
│  → 结果在 response.tool_calls[0].args                   │
│  → 层层嵌套:response → tool_calls → [0] → args        │
│  → 还要担心 tool_calls 可能为空                          │
│  → 需要加守卫判断                                       │
│                                                          │
│  ③ 心智负担                                              │
│  → 初学者不理解"为什么绑定工具能结构化"                 │
│  → 代码可读性差                                          │
│  → 团队协作时容易误解                                    │
└──────────────────────────────────────────────────────────┘

2.2 withStructuredOutput 一行搞定

javascript 复制代码
// with-structured-output.mjs --- 语义化升级
import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
import { z } from 'zod';

const model = new ChatOpenAI({
  modelName: process.env.MODEL_NAME,
  apiKey: process.env.OPENAI_API_KEY,
  temperature: 0,
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL,
  },
});

const scientistSchema = z.object({
  name: z.string().describe('科学家的姓名'),
  birth_year: z.number().describe('出生年份'),
  nationality: z.string().describe('国籍'),
  fields: z.array(z.string()).describe('研究领域列表'),
});

// 一行封装:我要一个"带结构化输出的模型"
const structuredModel = model.withStructuredOutput(scientistSchema);

// 调用方式跟普通模型一模一样
const result = await structuredModel.invoke('介绍一下爱因斯坦');
console.log(result);
// { name: '爱因斯坦', birth_year: 1879, nationality: '德国', fields: ['物理学'] }

console.log('------------------');
console.log(JSON.stringify(result, null, 2));
// {
//   "name": "爱因斯坦",
//   "birth_year": 1879,
//   "nationality": "德国",
//   "fields": ["物理学"]
// }
ini 复制代码
withStructuredOutput 的核心改进:

  ┌──────────────────────────────────────────────────────────┐
│  bindTools(讨巧写法)                                   │
│                                                          │
│  const modelWithTool = model.bindTools([                  │
│    { name: '...', description: '...', schema }            │
│  ]);                                                      │
│  const result = await modelWithTool.invoke(prompt);       │
│  const data = response.tool_calls[0].args;  // 层层取     │
│                                                          │
│  withStructuredOutput(语义化写法)                       │
│                                                          │
│  const structuredModel = model.withStructuredOutput(      │
│    scientistSchema                                        │
│  );                                                       │
│  const result = await structuredModel.invoke(prompt);     │
│  // result 直接就是结构化对象,不用取                     │
│                                                          │
│  差异:                                                   │
│  → 不需要给工具起名、写描述                              │
│  → 不需要从 tool_calls[0].args 里取                      │
│  → invoke 返回的就是结构化对象                           │
│  → 语义清晰:这个模型"输出结构化数据"                    │
└──────────────────────────────────────────────────────────┘

2.3 底层原理:仍然是 Tool Call

scss 复制代码
withStructuredOutput 只是"糖衣"

  ┌──────────────────────────────────────────────────────────┐
│  withStructuredOutput 的底层实现                          │
│                                                          │
│  model.withStructuredOutput(schema)                       │
│    │                                                      │
│    │  内部做了这些事:                                    │
│    │  ① 根据 schema 自动生成工具定义                     │
│    │     → 工具名:根据 schema 自动生成                   │
│    │     → 工具描述:自动生成                             │
│    │     → schema:原样传入                               │
│    │  ② 调用 bindTools 绑定工具                          │
│    │     → 相当于替你写好了 bindTools                     │
│    │  ③ 自动解析 tool_calls                               │
│    │     → invoke 时自动提取 tool_calls[0].args           │
│    │     → 直接返回结构化对象                             │
│    │  ④ 支持流式输出                                     │
│    │     → stream() 也能用                                │
│    │                                                      │
│  → 本质:bindTools 的封装,省去了重复的样板代码           │
│  → 注释原话:"tool call 讨巧的做法,升级为语义化更好的    │
│              withStructuredOutput,底层 tool call"        │
└──────────────────────────────────────────────────────────┘

2.4 bindTools vs withStructuredOutput 对比

维度 bindTools withStructuredOutput
语义 绑定工具(讨巧) 结构化输出(清晰)
工具定义 手动写 name + description 自动生成
结果获取 tool_calls[0].args 层层取 invoke 直接返回
空结果处理 需手动守卫 内部处理
流式支持 需手动处理 原生支持
代码量
可读性
适用场景 真的要调用工具 只要结构化输出
arduino 复制代码
选型建议:

  → 只要"结构化输出" → withStructuredOutput
  → 真的要"调用工具" → bindTools + 手动执行工具函数
  → withStructuredOutput 不是替代 bindTools
  → 而是"只想拿结构化数据"时的最佳选择

三、流式结构化输出

3.1 为什么需要流式结构化?

css 复制代码
普通流式输出的问题:

  → LLM 生成的文本逐 chunk 推送
  → 但内容是"自由文本"
  → 不是结构化数据
  → 前端无法按字段消费

  流式结构化输出的目标:

  → 一边生成,一边输出结构化数据
  → 用户实时看到结果在"填充"
  → 流结束后拿到完整结构化对象

  ┌──────────────────────────────────────────────────────────┐
│  流式结构化 vs 普通流式                                   │
│                                                          │
│  普通流式(model.stream)                                │
│  chunk 1: "莫"                                           │
│  chunk 2: "扎"                                           │
│  chunk 3: "特"                                           │
│  → 是文本片段,不是结构                                  │
│                                                          │
│  流式结构化(withStructuredOutput + stream)              │
│  chunk 1: { name: "爱", ... }      // 部分填充            │
│  chunk 2: { name: "爱因", ... }                          │
│  chunk 3: { name: "爱因斯坦", birth_year: 1... }         │
│  ...                                                     │
│  最终:  { name: "爱因斯坦", birth_year: 1879, ... }       │
│  → 是结构化的对象,逐步填充                               │
└──────────────────────────────────────────────────────────┘

3.2 完整实现

javascript 复制代码
// stream-with-structured-output.mjs --- 流式结构化输出
import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
import { z } from 'zod';

const model = new ChatOpenAI({
  modelName: process.env.MODEL_NAME,
  apiKey: process.env.OPENAI_API_KEY,
  temperature: 0,
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL,
  },
});

const schema = z.object({
  name: z.string().describe('姓名'),
  birth_year: z.number().describe('出生年份'),
  death_year: z.number().describe('死亡年份'),
  nationality: z.string().describe('国籍'),
  occupation: z.string().describe('职业'),
  famous_works: z.array(z.string()).describe('著名作品列表'),
  biography: z.array(z.string()).describe('简短传记'),
});

// 一行:结构化输出 + 流式
const structuredModel = model.withStructuredOutput(schema);

const prompt = '详细介绍莫扎特的信息。';
console.log('流式 结构化输出演示');

try {
  const stream = await structuredModel.stream(prompt);
  let chunkCount = 0;
  let result = null;

  console.log('接收流式数据...\n');

  // for await 消费结构化流
  for await (const chunk of stream) {
    chunkCount++;
    result = chunk; // 最后一个 chunk 是完整结构化结果
    console.log(JSON.stringify(chunk, null, 2));
  }

  console.log('\n共接收', chunkCount, '个数据块');
  console.log('最终结构化结果:');
  console.log(result);
} catch (err) {
  console.error('流式结构化输出出错:', err);
}
javascript 复制代码
代码解析:

  ① model.withStructuredOutput(schema)
  → 结构化输出模型
  → 同时支持 invoke 和 stream

  ② structuredModel.stream(prompt)
  → 流式调用
  → 返回异步可迭代对象
  → 每个 chunk 是部分结构化数据

  ③ for await (const chunk of stream)
  → 逐个消费 chunk
  → JSON.stringify 方便查看

  ④ result = chunk
  → 最后一次赋值是完整结果
  → 流结束后 result 是完整的结构化对象

  ⑤ 与普通 stream 的区别
  → 普通 stream:chunk 是文本片段
  → 结构化 stream:chunk 是部分结构数据
  → 最终结果类型安全

3.3 流式结构化的应用场景

arduino 复制代码
流式结构化输出的场景:

  ① 实时表单填充
  → AI 生成内容时,表单字段实时填充
  → 用户看到"正在填写"的过程
  → 体验更流畅

  ② 实时信息展示
  → 电影信息卡片逐步渲染
  → 人物简介逐步完善
  → 减少等待感

  ③ 长内容结构化
  → 生成报告时边生成边出结构化
  → 每生成一段就结构化一段
  → 最终完整对象

  ④ AI Agent 工具编排
  → Agent 调用工具的参数逐步生成
  → 用户看到参数在"组装"
  → 可提前取消或干预

四、XMLOutputParser:XML 时代的数据交换遗产

4.1 XML vs JSON:数据交换标准演进

javascript 复制代码
XML 时代:

  → XML(Extensible Markup Language)可扩展标记语言
  → 老一代的数据交换标准
  → 结构严谨,标签成对

  <h1>title<span>副标题<b>文强恋爱了</b></span></h1>
  → 标签嵌套表示层级
  → 有开必有闭
  → 机器可读,但冗长

  JSON 时代:

  → JSON(JavaScript Object Notation)
  → 现代数据交换的事实标准
  → 轻量、简洁、易读

  fetch 后端 API → 返回 JSON 格式
  → 前后端分离的默认格式
  → 浏览器原生 JSON.parse

  ┌──────────────────────────────────────────────────────────┐
│  XML vs JSON                                              │
│                                                          │
│                XML                  JSON                 │
│  ──────────────────────────────────────────               │
│  格式        <tag>value</tag>      "key": value           │
│  可读性      较差                 好                     │
│  体积        大(标签冗余)        小                     │
│  解析        需要解析器            JSON.parse             │
│  类型        全文本               有类型                 │
│  注释        支持                 不支持                 │
│  命名空间    支持                 不支持                 │
│  标准        老一代               现代事实标准           │
│  历史角色    数据交换初代标准      数据交换事实标准       │
│  现状        配置文件、SOAP 等     API 数据交换默认       │
└──────────────────────────────────────────────────────────┘

4.2 XMLHttpRequest 名字的由来

javascript 复制代码
为什么叫 XMLHttpRequest?它跟 XML 有关系吗?

  → 2000 年前后,XML 是数据交换标准
  → IE 5 推出了 XMLHttpRequest 对象
  → 最初的设计就是"用 XML 传输数据"
  → 名字里带着 XML

  但后来:
  → 实际使用中 JSON 取代了 XML
  → XHR 传 JSON 也完全没问题
  → 名字保留了下来(历史包袱)
  → 所以现在用 XHR 时根本看不到 XML

  类似的历史遗产:
  → .js 后缀(JavaScript,不是 Java)
  → npm(Node Package Manager,不止管理 Node 包)
  → AJAX(Asynchronous JavaScript And XML,早就不传 XML 了)

  ┌──────────────────────────────────────────────────────────┐
│  XMLHttpRequest 名字的真相                                │
│                                                          │
│  XMLHttpRequest = 老时代数据交换的产物                    │
│  → 最初设计:用 XML 做 AJAX 数据格式                     │
│  → 现实发展:JSON 取代 XML                                │
│  → 名字保留:历史包袱,约定俗成                           │
│  → 今天的 XHR:fetch 的底层,传 JSON                     │
│                                                          │
│  注释原话:                                              │
│  "fetch 后端api, 返回json 格式 数据交换的事实标准        │
│   XMLHttpRequest 老时代数据交换 xml ajax"                 │
└──────────────────────────────────────────────────────────┘

4.3 XMLOutputParser 完整实现

javascript 复制代码
// xml-output-parser.mjs --- XML 结构化输出
import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
import { XMLOutputParser } from '@langchain/core/output_parsers';

const model = new ChatOpenAI({
  modelName: process.env.MODEL_NAME,
  apiKey: process.env.OPENAI_API_KEY,
  temperature: 0,
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL,
  },
});

const parser = new XMLOutputParser();

const question = `
请提取一下文本中的任务信息:爱因斯坦生于1879年,是一位伟大的物理学家。
${parser.getFormatInstructions()}
`;
console.log(question);

try {
  console.log('正在调用大模型....\n');
  const response = await model.invoke(question);
  console.log(response.content);

  // XML → JavaScript 对象
  const result = await parser.parse(response.content);
  console.log(result);
} catch (err) {
  console.error('解析错误:', err);
}
xml 复制代码
XMLOutputParser 的作用:

  ┌──────────────────────────────────────────────────────────┐
│  getFormatInstructions()                                  │
│  → 告诉 LLM "请按 XML 格式输出"                          │
│  → 生成 XML Schema 指令                                  │
│  → 拼接到 prompt 末尾                                    │
│                                                          │
│  parser.parse(response.content)                          │
│  → 接收 LLM 的 XML 文本输出                              │
│  → 解析成 JavaScript 对象                                │
│  → 类似 JSON.parse 但针对 XML                            │
│                                                          │
│  输出示例:                                               │
│  LLM 返回:                                               │
│  <tasks>                                                  │
│    <task>                                                 │
│      <name>爱因斯坦</name>                                │
│      <year>1879</year>                                   │
│    </task>                                                │
│  </tasks>                                                 │
│                                                          │
│  解析后:                                                 │
│  {                                                        │
│    tasks: {                                              │
│      task: {                                             │
│        name: "爱因斯坦",                                 │
│        year: "1879"                                      │
│      }                                                    │
│    }                                                      │
│  }                                                        │
└──────────────────────────────────────────────────────────┘

  为什么还要学 XML 输出?
  → 了解历史:XML 曾是数据交换标准
  → 兼容旧系统:某些老系统仍用 XML
  → 完整认知:JSON 不是唯一的结构化格式
  → 面试谈资:XMLHttpRequest 名字的由来

4.4 结构化输出方案全景

sql 复制代码
现在共有六种结构化方案:

  ┌──────────────────────────────────────────────────────────┐
│  Output Parser 家族                                      │
│  ├── JsonOutputParser(最简单)                          │
│  ├── StructuredOutputParser.fromNamesAndDescriptions     │
│  ├── StructuredOutputParser.fromZodSchema(天花板)      │
│  └── XMLOutputParser(XML 格式)                         │
│                                                          │
│  Tool 家族                                                │
│  ├── model.bindTools(底层,讨巧写法)                   │
│  └── model.withStructuredOutput(语义化封装)             │
│                                                          │
│  选型:                                                   │
│  → 常规生产:withStructuredOutput + Zod                  │
│  → 需要流式:withStructuredOutput + stream               │
│  → 老系统对接:XMLOutputParser                           │
│  → 模型不支持 Tool Call:fromZodSchema                   │
└──────────────────────────────────────────────────────────┘

五、MySQL 数据落地:建库建表

5.1 mysql2/promise

javascript 复制代码
// create-table.mjs --- MySQL 建库建表
import mysql from 'mysql2/promise';

async function main() {
  const connectionConfig = {
    host: 'localhost',
    port: 3307,           // Docker 映射端口(上篇文章的 MySQL 容器)
    user: 'root',
    password: '123456',
    multipleStatements: true,
  };

  const connection = await mysql.createConnection(connectionConfig);

  try {
    // ① 创建数据库(不存在才创建)
    await connection.query(`
      CREATE DATABASE IF NOT EXISTS hello CHARACTER SET utf8mb4
      COLLATE utf8mb4_unicode_ci;
    `);

    // ② 切换到数据库
    await connection.query('USE hello;');

    // ③ 创建表(不存在才创建)
    await connection.query(`
      CREATE TABLE IF NOT EXISTS friends (
        id INT AUTO_INCREMENT PRIMARY KEY,
        name VARCHAR(50) NOT NULL,
        gender VARCHAR(10),                -- 性别
        birth_date DATE,                   -- 出生日期
        company VARCHAR(100),              -- 公司
        title VARCHAR(100),                -- 职位
        phone VARCHAR(20),                 -- 当前手机号
        wechat VARCHAR(50)                 -- 微信号
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
    `);
    console.log('数据库和表创建成功');
  } catch (err) {
    console.error('建库建表失败:', err);
  } finally {
    // ④ 关闭连接
    await connection.end();
  }
}

main().catch((err) => console.error('执行失败:', err));
javascript 复制代码
mysql2/promise 是什么?

  ┌──────────────────────────────────────────────────────────┐
│  mysql2 的两个版本                                        │
│                                                          │
│  mysql2(回调版)                                        │
│  → const mysql = require('mysql2')                       │
│  → connection.query(sql, callback)                       │
│  → 回调地狱问题                                          │
│                                                          │
│  mysql2/promise(Promise 版)                            │
│  → import mysql from 'mysql2/promise'                    │
│  → await connection.query(sql)                           │
│  → 配合 async/await 使用                                │
│  → 现代写法,推荐使用                                    │
│                                                          │
│  本文使用 Promise 版                                     │
│  → 所有操作都是 await                                   │
│  → 代码更清晰                                            │
└──────────────────────────────────────────────────────────┘

5.2 连接配置解析

yaml 复制代码
connectionConfig 配置项:

  host: 'localhost'
  → MySQL 服务器地址
  → 本机连接用 localhost

  port: 3307
  → MySQL 端口
  → 默认是 3306
  → 3307 说明是 Docker 容器映射的端口
  → (上篇 Docker 文章:3307:3306 映射)

  user: 'root'
  → MySQL 用户名

  password: '123456'
  → 密码
  → 生产环境用环境变量,不要硬编码!

  multipleStatements: true
  → 允许一次执行多条 SQL 语句
  → 用 ; 分隔
  → 注意:也会带来 SQL 注入风险,生产慎用

5.3 建库:CREATE DATABASE

sql 复制代码
CREATE DATABASE IF NOT EXISTS hello CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
sql 复制代码
建库语句解析:

  CREATE DATABASE
  → 创建数据库

  IF NOT EXISTS
  → 已存在则不创建
  → 幂等操作(执行多次结果一样)
  → 脚本可重复运行

  CHARACTER SET utf8mb4
  → 字符集 utf8mb4
  → 支持所有 Unicode 字符(包括 emoji)
  → 4 字节 UTF-8 编码
  → 现代 MySQL 的事实标准

  COLLATE utf8mb4_unicode_ci
  → 排序规则
  → utf8mb4 的 Unicode 通用排序
  → ci = case insensitive(不区分大小写)

  为什么用 utf8mb4 而不是 utf8?
  → MySQL 的 utf8 其实是 utf8mb3(3 字节)
  → 存不了 emoji 和生僻字
  → utf8mb4 才是完整的 UTF-8
  → 从 MySQL 5.5.3 开始支持,现在是默认推荐

5.4 建表:CREATE TABLE

sql 复制代码
CREATE TABLE IF NOT EXISTS friends (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(50) NOT NULL,
  gender VARCHAR(10),
  birth_date DATE,
  company VARCHAR(100),
  title VARCHAR(100),
  phone VARCHAR(20),
  wechat VARCHAR(50)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
sql 复制代码
friends 表字段设计:

  ┌────────────┬────────────────────────────────────────────┐
│  字段        类型 + 约束        说明                      │
├────────────┼────────────────────────────────────────────┤
│  id          INT AUTO_INCREMENT  主键,自增              │
│             PRIMARY KEY                                  │
│  name        VARCHAR(50) NOT NULL  姓名,必填           │
│  gender      VARCHAR(10)          性别                   │
│  birth_date  DATE                 出生日期               │
│  company     VARCHAR(100)         公司                   │
│  title       VARCHAR(100)         职位                   │
│  phone       VARCHAR(20)          当前手机号             │
│  wechat      VARCHAR(50)          微信号                 │
└────────────┴────────────────────────────────────────────┘

  设计要点:

  ① id INT AUTO_INCREMENT PRIMARY KEY
  → 整数自增主键
  → 每插入一行自动 +1
  → 唯一标识每一行
  → 数据库自动生成,无需手动指定

  ② name VARCHAR(50) NOT NULL
  → 姓名,最多 50 字符
  → NOT NULL:必填字段
  → 不能为空

  ③ gender VARCHAR(10)
  → 性别,没有 NOT NULL
  → 可选字段
  → 可以为 NULL

  ④ birth_date DATE
  → 日期类型
  → 不是字符串!是真正的日期类型
  → 可以用日期函数处理

  ⑤ 手机号/微信号用 VARCHAR
  → 不用数字类型!
  → 因为:可能带 +86 前缀、可能有空格
  → 数字类型会丢前导零
  → 电话/ID 类数据一律用字符串
ini 复制代码
表级设置:

  ENGINE=InnoDB
  → 存储引擎
  → InnoDB:支持事务、外键、行级锁
  → MySQL 5.5+ 默认引擎
  → 生产环境标准选择

  DEFAULT CHARSET=utf8mb4
  → 表默认字符集
  → 与数据库一致
  → 保证中文、emoji 都能存

5.5 建表后的数据操作

javascript 复制代码
// 插入数据
await connection.query(`
  INSERT INTO friends (name, gender, birth_date, company, title)
  VALUES (?, ?, ?, ?, ?)
`, ['张三', '男', '1990-01-01', '字节跳动', '前端工程师']);

// 查询数据
const [rows] = await connection.query(`
  SELECT * FROM friends WHERE company = ?
`, ['字节跳动']);
console.log(rows);
sql 复制代码
注意参数化查询:

  ✅ 正确写法(参数化):
  connection.query('SELECT * FROM friends WHERE id = ?', [1])

  ❌ 错误写法(字符串拼接):
  connection.query(`SELECT * FROM friends WHERE id = ${id}`)

  → ? 是占位符
  → 参数通过数组传入
  → 防止 SQL 注入
  → 这是安全底线!

六、完整链路:LLM 结构化输出 → MySQL 存储

6.1 场景设计

sql 复制代码
场景:AI 人物信息提取 + 落库

  流程:
  ┌──────────────────────────────────────────────────────────┐
│                                                          │
│  用户输入文本                                             │
│  "爱因斯坦生于1879年,是一位伟大的物理学家..."            │
│    │                                                      │
│    ▼                                                      │
│  withStructuredOutput 提取结构化数据                       │
│  → { name, birth_year, nationality, fields }              │
│    │                                                      │
│    ▼                                                      │
│  MySQL 建表(friends/scientists)                         │
│  → CREATE TABLE IF NOT EXISTS ...                        │
│    │                                                      │
│    ▼                                                      │
│  INSERT 落库                                               │
│  → 参数化插入                                             │
│    │                                                      │
│    ▼                                                      │
│  查询验证                                                  │
│  → SELECT * FROM scientists                              │
│                                                          │
│  = LLM 结构化输出 → 数据库持久化                          │
└──────────────────────────────────────────────────────────┘

6.2 完整代码

javascript 复制代码
import 'dotenv/config';
import mysql from 'mysql2/promise';
import { ChatOpenAI } from '@langchain/openai';
import { z } from 'zod';

// ① 结构化输出模型
const model = new ChatOpenAI({
  modelName: process.env.MODEL_NAME,
  apiKey: process.env.OPENAI_API_KEY,
  temperature: 0,
  configuration: { baseURL: process.env.OPENAI_BASE_URL },
});

const scientistSchema = z.object({
  name: z.string().describe('科学家的姓名'),
  birth_year: z.number().describe('出生年份'),
  nationality: z.string().describe('国籍'),
  fields: z.array(z.string()).describe('研究领域列表'),
});

const structuredModel = model.withStructuredOutput(scientistSchema);

// ② 建表 + 落库
async function saveToMySQL(scientist) {
  const connection = await mysql.createConnection({
    host: 'localhost',
    port: 3307,
    user: 'root',
    password: process.env.MYSQL_PASSWORD,
  });

  try {
    await connection.query(`CREATE DATABASE IF NOT EXISTS ai_data
      CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`);
    await connection.query('USE ai_data');

    await connection.query(`
      CREATE TABLE IF NOT EXISTS scientists (
        id INT AUTO_INCREMENT PRIMARY KEY,
        name VARCHAR(50) NOT NULL,
        birth_year INT,
        nationality VARCHAR(50),
        fields VARCHAR(500),
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
    `);

    await connection.query(`
      INSERT INTO scientists (name, birth_year, nationality, fields)
      VALUES (?, ?, ?, ?)
    `, [
      scientist.name,
      scientist.birth_year,
      scientist.nationality,
      scientist.fields.join(','),
    ]);

    console.log('已存入数据库:', scientist.name);
  } catch (err) {
    console.error('落库失败:', err);
  } finally {
    await connection.end();
  }
}

// ③ 主流程
async function main() {
  const result = await structuredModel.invoke('介绍一下爱因斯坦');
  console.log('结构化输出:', result);
  await saveToMySQL(result);
}

main().catch(console.error);
sql 复制代码
完整链路总结:

  ① withStructuredOutput(schema)
  → LLM 输出结构化数据(类型安全)

  ② CREATE DATABASE / CREATE TABLE
  → 准备存储结构(幂等,可重复运行)

  ③ INSERT 参数化插入
  → 结构化数据落库(防止 SQL 注入)

  ④ SELECT 查询
  → 验证数据已持久化

  → 这就是 LLM 应用落地的标准链路:
    自由文本 → 结构化 → 数据库

七、总结

7.1 知识体系图

sql 复制代码
LLM 结构化输出进阶
│
├── withStructuredOutput(语义化终极封装)
│   ├── 一行:model.withStructuredOutput(schema)
│   ├── 底层:封装了 Tool Call(bindTools 的糖衣)
│   ├── 支持 invoke / stream / batch
│   ├── 结果直接返回结构化对象(不用取 tool_calls)
│   └── vs bindTools:语义清晰、代码少、可读性高
│
├── 流式结构化输出
│   ├── structuredModel.stream(prompt)
│   ├── 每个 chunk 是部分结构化数据
│   ├── 最后一个 chunk 是完整结果
│   ├── 实时填充表单/卡片
│   └── 最终类型安全
│
├── XMLOutputParser(历史遗产)
│   ├── XML:老一代数据交换标准
│   ├── JSON:现代数据交换事实标准
│   ├── XMLHttpRequest 名字的由来(历史包袱)
│   ├── parser.parse(xml) → JavaScript 对象
│   └── 场景:兼容老系统
│
├── MySQL 落库
│   ├── mysql2/promise(Promise 版 API)
│   ├── connectionConfig(host/port/user/password)
│   ├── CREATE DATABASE IF NOT EXISTS
│   │   ├── utf8mb4(完整 UTF-8,支持 emoji)
│   │   └── utf8mb4_unicode_ci(排序规则)
│   ├── CREATE TABLE IF NOT EXISTS
│   │   ├── INT AUTO_INCREMENT PRIMARY KEY
│   │   ├── VARCHAR(n) NOT NULL
│   │   ├── DATE(日期类型)
│   │   ├── ENGINE=InnoDB(事务/行锁)
│   │   └── DEFAULT CHARSET=utf8mb4
│   └── 参数化查询(? 占位符,防 SQL 注入)
│
├── 完整链路
│   ├── 用户输入自由文本
│   ├── withStructuredOutput 提取结构化数据
│   ├── MySQL 建表(幂等)
│   ├── INSERT 参数化落库
│   └── SELECT 验证持久化
│
└── 核心认知
    ├── withStructuredOutput = Tool Call 的语义化封装
    ├── 流式结构化 = 实时性 + 类型安全
    ├── XML 是历史,JSON 是现在
    └── 结构化输出的终点是"数据落地"

7.2 一句话总结

withStructuredOutput 是 Tool Call 的语义化封装:model.withStructuredOutput(schema) 一行代码,底层自动完成"定义工具 → 绑定工具 → 解析 tool_calls"的全部流程,invoke 直接返回类型安全的结构化对象,还支持 stream() 流式结构化输出------每个 chunk 是部分结构数据,最后一个 chunk 是完整结果。再加上 XMLOutputParser 了解 XML 时代的数据交换遗产(XMLHttpRequest 名字的由来),以及 MySQL 的建库建表(utf8mb4 + InnoDB + 参数化查询),就构成了 LLM 应用的完整数据链路:自由文本 → 结构化输出 → 数据库持久化。用一句话概括:结构化输出解决"AI 输出不可控"的问题,数据库解决"数据不落地"的问题,两者结合才是 LLM 应用的工程化闭环。


如果这篇文章对你有帮助,欢迎点赞收藏

相关推荐
阿黎梨梨2 小时前
LangGraph 核心机制:从状态管理到人机协同
人工智能·langchain
不好听6132 小时前
分支与循环:把"下一跳"交给状态和 LLM——LangGraph 系列之三
langchain
GISMagic3 小时前
2.从零制作第一个天气 Agent:理解 Prompt、Skill 与 LangChain
ai·langchain·prompt·agent
Flynt18 小时前
"只输出 JSON"根本不够:LLM 结构化输出的坑,我实测了 60 次调用
llm·json
xn713319 小时前
Funes Agent Memory 实测:Codex 长期记忆召回、旧记忆污染与 no-answer 边界
人工智能·llm·ai编程
William一直在路上19 小时前
GPT-6 Astra 研究报告——模型能力、API 演进与 Agent/MCP 生态影响分析
人工智能·gpt·llm·openai·astra
玉宇夕落20 小时前
从InMemory内存记忆到文件持久化,手把手教你管理AI的“记忆”
llm
神秘的猪头20 小时前
新版 LangChain Agent 核心架构:State、Context、ToolRuntime 与 Middleware
langchain·llm·fastapi
神秘的猪头20 小时前
新版 LangChain Agent 入门:从 `bind_tools + while` 到 `create_agent`
langchain·fastapi