☰
TypeChat Coffee Shop 示例深度解析:用类型 Schema 把自然语言订单转成结构化 JSON
2026/9/25 11:55:47 网站建设 项目流程
  • 大模型
  • AI 应用
  • 后端

【免费下载链接】TypeChat

TypeChat is a library that makes it easy to build natural language interfaces using types.

项目地址:https://gitcode.com/gh_mirrors/ty/TypeChat
点击查看免费下载

导读

Coffee Shop 是 TypeChat(基于 TypeScript 的"用类型构建自然语言界面"库)中极具代表性的示例:它把用户随口说出的咖啡点单需求,通过一个纯 TypeScript 类型 Schema 翻译成结构化的 JSON 订单对象,并以UnknownText类型优雅兜底无法识别的输入。读完本文,你将掌握 TypeChat 的 Schema 建模思路、完整的环境配置与运行流程,以及从createJsonTranslator到 JSON 校验、失败修复的底层调用链。

Coffee Shop 示例在 TypeChat 中的定位

在 typescript/examples/README.md 列出的系列示例中,Coffee Shop 被描述为"一个咖啡店的智能代理,将用户意图翻译成咖啡订单条目列表"。与最基础的 Sentiment 情感分类示例(TypeChat 的 "hello world")相比,Coffee Shop 的独特价值在于:

  • 对象集合建模:一个自然语言句子可能包含多个商品、多个选项,需要翻译成一组"名词"(line items);
  • 判别式联合类型:用type字段区分商品类别与选项类别,让模型可以精确地为每个 token 归类;
  • 容错设计:专门用UnknownText类型承接无法匹配任何已知类型的输入,形成"已知清单 + 未知兜底"的双通道结构。

从仓库根目录的 README.md 可以看到,TypeChat 的核心主张是"类型即协议":把 TypeScript 类型当作 LLM 与程序之间的共享接口。Coffee Shop 就是这一主张的最小完整落地。

理解 Schema:订单本质是"名词集合"

Coffee Shop 的核心思想在 typescript/examples/coffeeShop/README.md 中一句话点明:将用户意图捕获为一组"名词"(nouns),这里的名词是咖啡订单中的条目,合法的条目从Cart类型开始定义。

顶层容器:Cart

typescript/examples/coffeeShop/src/coffeeShopSchema.ts 定义了整个订单的顶层结构:

export interface Cart { items: (LineItem | UnknownText)[]; }

items是一个数组,每个元素要么是可执行的订单条目(LineItem),要么是无法理解的内容(UnknownText)。这决定了模型输出永远是"一组条目",天然支持一句自然语言中包含多件商品。

兜底类型:UnknownText

// Use this type for order items that match nothing else export interface UnknownText { type: "unknown", text: string; // The text that wasn't understood }

UnknownText是 Coffee Shop 区别于大多数 Schema 设计的关键:当用户输入中出现roses are red、two lawnmowers这类与菜单无关的内容时,模型不必强行把它塞进某个商品类型,而是可以诚实地标注为unknown并保留原文。这意味着:

  • 校验永远"有处安放"——任何输入都能映射到Cart的结构;
  • 业务层可以精确感知"哪些话没被理解",从而给出针对性反馈(见下文main.ts中的处理逻辑)。

商品与选项的判别式联合

Product类型把商品聚合为四种可判别联合:

export type Product = BakeryProducts | LatteDrinks | EspressoDrinks | CoffeeDrinks;

每种商品都带有type字段作为判别标记,例如:

export interface LatteDrinks { type: "LatteDrinks"; name: "cappuccino" | "flat white" | "latte" | "latte macchiato" | "mocha" | "chai latte"; temperature?: CoffeeTemperature; size?: CoffeeSize; // The default is "grande" options?: (Milks | Sweeteners | Syrups | Toppings | Caffeines | LattePreparations)[]; }

值得注意的几个 Schema 设计细节:

  • 字面量联合即枚举:name字段用字符串字面量联合列出全部可售品名("cappuccino" | "flat white" | ...),LLM 在翻译时被约束只能从中取值,从源头杜绝了"编造菜单";
  • 可选字段与默认值注释:size?: CoffeeSize并注释// The default is "grande"。TypeScript 类型本身没有默认值概念,但注释会随 Schema 文本一起进入 Prompt,引导模型对未提及的尺寸按默认值处理或省略字段;
  • 选项按商品类别差异化:不同饮品开放不同的选项集合——拿铁类(LatteDrinks)可选牛奶、甜味剂、糖浆、配料、咖啡因与制备方式(LattePreparations),而浓缩咖啡类(EspressoDrinks)则用Creamers替代Milks,尺寸枚举也从CoffeeSize换成更贴切的EspressoSize("solo" | "doppio" | "triple" | "quad")。

选项量化:OptionQuantity

部分选项(如甜味剂、糖浆、配料)还带有一个量化字段:

export type OptionQuantity = "no" | "light" | "regular" | "extra" | number;

这是一个"枚举 + 数字"的混合类型,正好覆盖了自然语言的表达空间:no foam(no)、light whipped cream(light)、a pack of sugar(1,以 number 表示)、three pumps of vanilla(3)都能被统一收纳。从 typescript/examples/coffeeShop/src/input.txt 可以看到大量这类表达,例如i'd like a light nutmeg espresso、a flat white with five pumps of caramel syrup。

菜单全貌一览

Schema 中完整定义了四类商品、九类选项/制备方式与四种枚举,汇总如下:

类别类型名关键字段
烘焙BakeryProductsname(4 种 muffin / bagel),options(BakeryOptions或BakeryPreparations)
拿铁类LatteDrinks6 种品名,temperature/size,选项含Milks、Sweeteners、Syrups、Toppings、Caffeines、LattePreparations
浓缩咖啡类EspressoDrinks4 种品名,选项含Creamers等
咖啡类CoffeeDrinksamericano/coffee
糖浆Syrups10 种口味,optionQuantity?
咖啡因Caffeinesregular/half caf等 5 档
牛奶Milks7 种奶
奶精Creamers10 种,含half and half、heavy cream
配料Toppingscinnamon、foam、ice等
制备方式LattePreparationsfor here cup、with room、dry、wet等
甜味剂Sweetenersequal、sugar、splenda等

此外还有四个类型别名:CoffeeTemperature(hot | extra hot | warm | iced)、CoffeeSize(short | tall | grande | venti)、EspressoSize、OptionQuantity。

运行效果:从自然语言到 JSON

typescript/examples/coffeeShop/README.md 给出了最直观的运行示例。当输入:

☕> we'd like a cappuccino with a pack of sugar

模型输出:

{ "items": [ { "type": "lineitem", "product": { "type": "LatteDrinks", "name": "cappuccino", "options": [ { "type": "Sweeteners", "name": "sugar", "optionQuantity": "regular" } ] }, "quantity": 1 } ] }

这个输出清晰地展示了三层结构:

  1. 外层是Cart.items,本例只含一个LineItem;
  2. 商品层product命中LatteDrinks,品名为cappuccino;
  3. 选项层options中sugar被归类为Sweeteners,并把"a pack of sugar"量化为optionQuantity: "regular"(而不是数字1)——这体现了模型对"一包糖"的口语化理解,而非机械计数。

quantity: 1表明"we'd like a ..."整体对应一件商品。若输入包含多件商品(如input.txt中的i'd like a tall decaf latte iced a grande cappuccino double espresso and a warmed poppyseed muffin sliced in half),items数组会相应包含多个LineItem。

手把手运行 Coffee Shop 示例

第一步:准备环境

Coffee Shop 示例依赖 Node.js 与 npm。按 typescript/examples/README.md 的指引,在本仓库的typescript目录下安装依赖:

cd typescript npm install

(该 README 同时也介绍了 GitHub Codespaces 的云开发环境方案,本地运行只需 Node.js 18.16.0 LTS 或更新版本。)

第二步:构建示例

在仓库根目录执行:

npm run build-all

该命令会同时构建 TypeChat 库本体与全部示例。Coffee Shop 示例自身的构建脚本定义在 typescript/examples/coffeeShop/package.json 中:

"scripts": { "build": "tsc -p src", "postbuild": "copyfiles -u 1 src/**/*Schema.ts src/**/*.txt dist" }

注意postbuild会把Schema.ts与input.txt一并复制到dist目录——这是运行时fs.readFileSync读取 Schema 文本所必需的(见下文main.ts分析)。

第三步:配置模型凭据

示例通过createLanguageModel(process.env)从环境变量选择 OpenAI 或 Azure OpenAI 端点。推荐在typescript目录下创建.env文件:

# For OpenAI OPENAI_MODEL=... OPENAI_API_KEY=... # For Azure OpenAI AZURE_OPENAI_ENDPOINT=... AZURE_OPENAI_API_KEY=...

各变量含义如下:

变量说明
OPENAI_MODELOpenAI 模型名(如gpt-3.5-turbo或gpt-4)
OPENAI_API_KEYOpenAI API Key
OPENAI_ENDPOINTOpenAI API 端点,可选,默认https://api.openai.com/v1/chat/completions
OPENAI_ORGANIZATIONOpenAI 组织 ID,可选,默认空
AZURE_OPENAI_ENDPOINTAzure OpenAI REST API 完整 URL(含 deployment 与 api-version)
AZURE_OPENAI_API_KEYAzure OpenAI API Key

如果端点需要经过代理访问(例如受限网络区域),还可以在.env中配置:

HTTPS_PROXY=http://127.0.0.1:7890 # Optional: comma-separated hosts that should bypass the proxy NO_PROXY=localhost,127.0.0.1

第四步:运行

进入coffeeShop目录,支持两种运行方式:

交互模式——从终端逐条输入:

node ./dist/main.js

文件模式——逐行处理输入文件(示例自带的 input.txt 与 input2.txt):

node ./dist/main.js ./dist/input.txt

注意示例的main指向dist/main.js,文件参数也要用构建后复制到dist中的input.txt路径。交互模式下输入quit或exit结束会话(该行为由processRequests实现,见下文)。

input.txt与input2.txt的内容设计也值得玩味:前者包含 47 条由易到难的句子,覆盖单品、多件、尺寸调整、重复表达、数字泵数、法语/丹麦语(un petit cafe、en lille kaffe)以及完全无关的句子(roses are red、two lawnmowers, a grande latte and a tall tree),用于系统性考验 Schema 的边界;后者则浓缩了其中最有挑战性的 8 条,适合快速回归。

代码级剖析:main.ts 的完整链路

typescript/examples/coffeeShop/src/main.ts 仅约 43 行,却串起了 TypeChat 的全流程:

import { createJsonTranslator, createLanguageModel } from "typechat"; import { createTypeScriptJsonValidator } from "typechat/ts"; import { processRequests } from "typechat/interactive"; import { Cart } from "./coffeeShopSchema"; const dotEnvPath = findConfig(".env"); assert(dotEnvPath, ".env file not found!"); dotenv.config({ path: dotEnvPath }); const model = createLanguageModel(process.env); const schema = fs.readFileSync(path.join(__dirname, "coffeeShopSchema.ts"), "utf8"); const validator = createTypeScriptJsonValidator<Cart>(schema, "Cart"); const translator = createJsonTranslator(model, validator);

1. 环境变量加载

findConfig(".env")从当前目录向上逐级查找.env文件,找不到时直接assert失败并提示.env file not found!,然后由dotenv注入环境变量。

2. 把 Schema 源码作为"字符串协议"加载

fs.readFileSync把coffeeShopSchema.ts的源码文本读入内存——这正是 TypeChat 的设计精髓:Schema 不是编译期类型,而是运行时被原样拼进 Prompt 的"共享协议"。它同时被用作:

  • LLM 翻译时的格式约束(进入 Prompt);
  • TypeScript 编译器校验时的类型来源(进入内存编译器)。

3. 创建校验器与翻译器

createTypeScriptJsonValidator<Cart>(schema, "Cart")返回一个TypeChatJsonValidator,其中"Cart"是目标类型名;createJsonTranslator(model, validator)把它们组合成翻译器。两者的接口定义在 typescript/src/typechat.ts。

4. 请求处理与结果消费

processRequests("☕> ", process.argv[2], async (request) => { const response = await translator.translate(request); if (!response.success) { console.log(response.message); return; } const cart = response.data; console.log(JSON.stringify(cart, undefined, 2)); if (cart.items.some(item => item.type === "unknown")) { console.log("I didn't understand the following:"); for (const item of cart.items) { if (item.type === "unknown") console.log(item.text); } return; } processOrder(cart); console.log("Success!"); });

processRequests("☕> ", process.argv[2], callback)来自 typescript/src/interactive/interactive.ts:当传入第二个命令行参数(输入文件路径)时,逐行读取文件并调用回调;否则进入交互循环,直到用户输入quit或exit。

回调里值得注意的业务逻辑:UnknownText不只是 Schema 的兜底,也是业务分支的入口——一旦items中出现type === "unknown",程序不会打印Success!,而是列出所有未被理解的原文,把"翻译不确定性"显式暴露给用户。这正是 README 强调该类型"捕获不匹配现有类型的用户输入"的落地应用。

Result判别联合(success标志 +data/message)定义在 typescript/src/result.ts,是贯穿 TypeChat 所有 API 的返回约定。

底层原理:TypeChat 如何完成"翻译 + 校验 + 修复"

翻译循环与自动修复

typescript/src/typechat.ts 中的translate是核心。它首先构造请求 Prompt(createRequestPrompt):

You are a service that translates user requests into JSON objects of type "Cart" according to the following TypeScript definitions:

{Schema 源码}

The following is a user request: """ {用户输入} """ The following is the user request translated into a JSON object with 2 spaces of indentation and no properties with the value undefined:

然后进入循环:取模型回复 → 用首尾{/}截取 JSON 片段 →JSON.parse→ 交给 validator 校验。若校验失败且attemptRepair为true(默认值,见 typescript/src/typechat.ts),则追加一轮"修复对话"(createRepairPrompt):

The JSON object is invalid for the following reason: """ {校验器返回的错误信息} """ The following is a revised JSON object:

把首次校验错误回灌给模型重试一次。这是 TypeChat 显著降低 LLM 输出格式错误率的关键机制。translate还暴露了stripNulls开关(默认false):部分模型(如 gpt-3.5-turbo)倾向于给可选属性填null,开启后会在校验前递归删除 null 值属性(typescript/src/typechat.ts)。

内存中的 TypeScript 编译器校验

typescript/src/ts/validate.ts 实现了"用 TypeScript 编译器校验 JSON"的机制:validate把 JSON 对象转换成一段临时模块代码

import { Cart } from './schema'; const json: Cart = { ... };

再用内存中的ts.createProgram组合lib.d.ts(一个精简的内置类型声明,见 typescript/src/ts/validate.ts)、schema.ts(即读入的 Schema 源码)和json.ts三份虚拟文件进行编译,收集语法与语义诊断作为校验错误。这意味着任何 TypeScript 类型层面的违规——拼错品名、漏掉必填字段、选项类型不符——都会被编译器精确捕获。仓库中的 validate.test.ts 等测试文件正是围绕这套校验器展开的。

语言模型封装与重试策略

createLanguageModel(typescript/src/model.ts)根据环境变量自动选择 OpenAI(OPENAI_API_KEY)或 Azure OpenAI(AZURE_OPENAI_API_KEY)实现;底层complete封装了 fetch 请求、temperature: 0、n: 1参数、瞬时错误(429/500/502/503/504)重试(默认最多 3 次、间隔 1000ms)、单请求超时(默认 10 分钟)与响应体大小上限(默认 100MB)等健壮性细节(typescript/src/model.ts)。

进阶探索:从 Coffee Shop 走向更复杂的 Schema

Coffee Shop 是学习 TypeChat Schema 建模的最佳起点,沿着 typescript/examples/README.md 推荐的阅读顺序可以继续深入:

  • Restaurant 示例:与 Coffee Shop 同属"订单翻译"范式,但 Schema 更复杂,用文本(prose)文件展示简单与高级模型在处理复合句、干扰信息与修正表达时的差异;
  • Math 示例:把计算题翻译成调用四则运算 API 的"程序",展示 TypeChat 的程序生成能力,是理解program.ts与model.ts关系的进阶案例;
  • Drawing 示例:把绘画请求翻译成结构化形状(box、ellipse、arrow)再渲染为 SVG;
  • Music 示例:把每个用户意图翻译成一系列 JSON 动作,构成一个简单的数据流程序。

这些示例共同印证了 Coffee Shop 所体现的方法论:先定义好"名词世界"(类型 Schema),再让 LLM 在约束内自由发挥——类型即协议,校验即防线。

小结

Coffee Shop 示例用不到 110 行的 Schema 加 43 行的main.ts,完整演示了 TypeChat 的核心闭环:TypeScript 类型作为 Prompt 协议约束 LLM 输出 → 内存 TypeScript 编译器校验 JSON → 失败时自动回灌错误修复。Cart的联合类型设计、UnknownText的容错兜底、OptionQuantity的"枚举 + 数字"量化,都是可以迁移到任意垂直领域(点单、预约、检索、客服)的通用建模范式。对想用"类型驱动"方式构建自然语言界面的开发者而言,这个示例既是入门教材,也是可复用的工程模板。

  • 大模型
  • AI 应用
  • 后端

【免费下载链接】TypeChat

TypeChat is a library that makes it easy to build natural language interfaces using types.

项目地址:https://gitcode.com/gh_mirrors/ty/TypeChat
点击查看免费下载

相关推荐

上一篇:蔚蓝档案鼠标指针主题:为Windows桌面注入动漫灵魂的完整指南
下一篇:Electric 与 Phoenix LiveView 实战:把 Postgres 实时同步进 LiveView Stream,无需手写查询与变更处理

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询