阅读视图

发现新文章,点击刷新页面。

别再让大模型“胡说八道”了!LangChain 的 JsonOutputParser 教你驯服 AI 输出

从“前端模块化”到“AI输出格式”,我们都在和混乱做斗争**


🧠 引子:当 AI 开始“自由发挥”

你有没有遇到过这样的场景?

你辛辛苦苦写了一堆提示词(prompt),满怀期待地调用大模型,结果它回你一段:

“好的!关于 Promise,这是一个 JavaScript 中用于处理异步操作的对象。它的核心思想是……(省略 300 字小作文)”

而你真正想要的,只是一个干净、结构化的 JSON!

{
  "name": "Promise",
  "core": "用于处理异步操作的代理对象",
  "useCase": ["网络请求", "定时任务", "并发控制"],
  "difficulty": "中等"
}

是不是想砸键盘?别急——LangChain 的 JsonOutputParser 就是来拯救你的!


📦 背景小剧场:从前端模块化说起

在讲 LangChain 之前,咱们先穿越回“远古时代”的前端开发。

🕰️ 没有模块化的年代

<script src="a.js"></script>
<script src="b.js"></script>
<script>
  const p = new Person('张三', 18);
  p.sayName(); // 希望 a.js 里定义了 Person...
</script>

那时候,JS 文件之间靠“默契”共享变量,一不小心就全局污染、命名冲突、依赖顺序错乱……简直是“混沌宇宙”。

后来,Node.js 带来了 CommonJS,ES6 推出了 import/export,前端终于有了清晰的模块边界。

模块化 = 约定 + 结构 + 可预测性

而今天,我们在调用大模型时,也面临同样的问题:输出太“自由”,缺乏结构
于是,LangChain 给我们带来了“AI 世界的模块化工具”——OutputParser


🔧 LangChain 的救星:JsonOutputParser

JsonOutputParser 是 LangChain 提供的一个输出解析器,专门用来把 LLM 返回的“散文”变成结构化数据(比如 JSON)。配合 Zod(一个超好用的 TypeScript 校验库),还能自动校验字段类型、枚举值、数组结构等。

✨ 它能做什么?

  • 强制模型只输出 JSON(通过提示词约束)
  • 自动解析字符串为 JS 对象
  • 用 Zod Schema 验证数据合法性
  • 报错时告诉你哪里不符合预期(而不是默默返回 undefined)

🛠️ 实战:用 LangChain 解析“前端概念”

假设我们要让 AI 解释一个前端概念(比如 Promise),并返回标准化 JSON。

第一步:定义 Schema(用 Zod)

import { z } from 'zod';

const FrontendConceptSchema = z.object({
  name: z.string().describe("概念名称"),
  core: z.string().describe("核心要点"),
  useCase: z.array(z.string()).describe("常见使用场景"),
  difficulty: z.enum(['简单', '中等', '复杂']).describe("学习难度")
});

这就像给 AI 发了一份“填空试卷”,还规定了每道题只能填什么类型!

第二步:创建 Parser 和 Prompt

import { JsonOutputParser } from '@langchain/core/output_parsers';
import { PromptTemplate } from '@langchain/core/prompts';

const jsonParser = new JsonOutputParser(FrontendConceptSchema);

const prompt = PromptTemplate.fromTemplate(`
你是一个只会输出 JSON 的 API,不允许输出任何解释性文字。

⚠️ 你必须【只返回】符合以下 Schema 的 JSON:
- 不允许增加或减少字段
- 字段名必须完全一致
- 返回结果必须能被 JSON.parse 成功解析

{format_instructions}

前端概念:{topic}
`);

注意 {format_instructions} ——这是 JsonOutputParser 自动生成的格式说明,会告诉模型具体该怎么写 JSON!

比如它可能生成:

The output should be a markdown code snippet formatted in the following schema:

{
  "name": "string",
  "core": "string",
  "useCase": ["string", ...],
  "difficulty": "简单" | "中等" | "复杂"
}

第三步:组装 Chain 并调用

const chain = prompt.pipe(model).pipe(jsonParser);

const response = await chain.invoke({
  topic: 'Promise',
  format_instructions: jsonParser.getFormatInstructions(),
});

console.log(response);
// ✅ 得到干净的 JS 对象!

😂 为什么这很重要?——因为 AI 太“人性化”了!

大模型天生喜欢“聊天”,它总想多说几句:“亲,你还想知道 async/await 吗?”
但我们的程序需要的是确定性,不是“贴心客服”。

AI 的自由 = 程序员的噩梦
结构化的输出 = 自动化的基石

JsonOutputParser,相当于给 AI 戴上“嘴套”,只让它吐 JSON,不许废话!


🚀 进阶思考:不只是 JSON,更是契约

其实,JsonOutputParser 背后的思想,和前端模块化、API 设计、TypeScript 类型系统一脉相承:

明确输入输出,才能构建可靠系统。

当你用 Zod 定义 Schema 时,你不仅是在约束 AI,更是在建立人与 AI 之间的契约。这个契约让后续的数据处理、UI 渲染、数据库存储变得安全可靠。


✅ 总结:三句话记住 JsonOutputParser

  1. AI 天生爱啰嗦,JsonOutputParser 让它闭嘴只吐 JSON。
  2. Zod Schema 是“法律”,parser 是“警察”,确保输出合法合规。
  3. 结构化输出 = 自动化流水线的第一块砖。

🎯 从零搭建一个 React Todo 应用:父子通信、状态管理与本地持久化全解析!

“写 Todo 是程序员的成人礼。”

如果你刚刚入坑 React,或者想巩固组件通信、状态提升、本地存储等核心概念,那么恭喜你!这篇文章将带你手把手打造一个功能完整、结构清晰、代码优雅的 React Todo 应用,并深入浅出地解释背后的原理。

更重要的是——我们不用 Redux、不用 Context、不用任何花里胡哨的库,只用 React 原生 Hooks + 父子通信,就能写出可维护、可扩展的代码!


🧠 为什么 Todo 应用值得认真对待?

别小看这个“加任务、删任务、标记完成”的小玩意儿。它完美涵盖了现代前端开发的三大核心问题:

  1. 状态管理(谁持有数据?谁修改数据?)
  2. 组件通信(父子怎么传?兄弟怎么聊?)
  3. 副作用处理(比如自动保存到 localStorage)

而 React 的哲学是:状态提升 + 单向数据流。听起来高大上?其实很简单——让父组件当“管家”,子组件只负责“汇报”和“展示”


🏗️ 项目结构预览

我们的应用由三个子组件构成:

  • TodoInput:输入新任务
  • TodoList:展示并操作任务列表
  • TodoStats:显示统计信息 & 清除已完成

它们都共享同一个状态:todos[]。这个数组由父组件 App 统一管理,并通过 props 传递给子组件。

✨ 这就是“状态提升”(Lifting State Up)的经典实践!


🔌 父子通信:React 的“单向数据流”哲学

👨‍👧 父 → 子:通过 props 传递数据

<TodoList 
  todos={todos} 
  onDelete={deleteTodo}
  onToggle={toggleTodo}
/>

父组件把 todos 数组和几个修改函数作为 props 传给子组件。子组件只能读,不能改——就像孩子只能看菜单,不能自己进厨房炒菜。

👧‍→👨 子 → 父:通过回调函数“打报告”

子组件想修改数据?必须调用父组件传来的函数:

// 在 TodoInput 中
onAdd(inputValue); // 相当于:“爸,我想加个任务!”

// 在 TodoList 中
onToggle(todo.id); // “爸,这个任务我搞定了!”

这种模式确保了数据流向清晰、可预测,避免了“状态混乱”的噩梦。

💡 小贴士:React 不支持 Vue 那样的 v-model 双向绑定,因为它认为“显式优于隐式”。虽然多写两行代码,但逻辑更透明!


🧩 兄弟组件如何“隔空对话”?

TodoInputTodoList 是兄弟,它们之间没有直接通信!所有交互都通过共同的父组件 App 中转:

  1. TodoInput 调用 onAdd → 父组件更新 todos
  2. 父组件把新 todos 传给 TodoList → 列表自动刷新

这就是所谓的 “间接通信” ——看似绕路,实则解耦。兄弟组件互不依赖,未来拆分或替换都超轻松!


💾 自动保存到 localStorage:useEffect 的妙用

用户辛辛苦苦加了一堆任务,结果一刷新全没了?那可不行!

我们用 useEffect 监听 todos 变化,自动存到本地:

useEffect(() => {
  localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);

同时,初始化时从 localStorage 读取:

const [todos, setTodos] = useState(() => {
  const saved = localStorage.getItem('todos');
  return saved ? JSON.parse(saved) : [];
});

🎉 用户体验瞬间拉满:关掉浏览器再打开,任务还在!妈妈再也不用担心我丢三落四了~


🎨 样式方案:Stylus + Vite,简洁又高效

我们用 Stylus 写样式(缩进语法,少写大括号),配合 Vite 极速构建。.styl 文件清爽易读:

.todo-app
  max-width: 600px
  margin: 0 auto
  padding: 20px
  
  .completed
    text-decoration: line-through
    color: #888

Vite 的 HMR(热更新)快如闪电,改一行样式,浏览器秒级响应——开发幸福感爆棚!


🧪 完整代码亮点回顾

  • 状态集中管理:所有 todos 操作在 App 中定义
  • 函数式更新setTodos([...todos, newTodo]) 避免闭包陷阱
  • 条件渲染completed > 0 && <button> 避免无效按钮
  • 语义化 JSX<label> 包裹 checkbox,提升可访问性
  • 性能友好:无多余状态,无复杂计算

🤔 思考:为什么不用 Context 或 Zustand?

对于小型应用(如 Todo),过度设计反而增加复杂度。Context 适合跨多层组件共享状态,Zustand 适合大型状态树。而我们的场景——三个组件 + 一个状态数组,用 props 足矣!

🚀 记住:简单即强大。能用 props 解决的问题,就别急着上状态管理库!


🎁 结语:你的第一个 React 应用,也可以很优雅

通过这个 Todo 应用,你不仅学会了组件通信,更理解了 React 的核心思想:状态驱动视图、单向数据流、组合优于继承

下次面试官问:“React 组件怎么通信?” 你可以微微一笑,掏出这个项目说:

“看,我的 Todo,麻雀虽小,五脏俱全。”

❌