Starrn

如何让大语言模型输出 JSON

llm
发布于

已过时,OpenAI 推出了 JSON Mode 来让 LLM 输出 JSON,可以参考:https://platform.openai.com/docs/guides/text-generation/json-mode

前言

在过去的一段时间里,我们似乎处在人工智能的浪潮中。大语言模型(LLM)在越来越多的应用场景中落地,逐渐成为生活和工作中不可或缺的一部分。我们都知道 LLM 擅长用自然语言来解答和分析问题。但是,想要让 LLM 与业务程序进行衔接,目前还需要具备稳定输出 JSON 或 YAML 等结构化数据的能力。

当然我们可以在 Prompt 中强调想要的输出格式,比如 JSON

请从给定文本中提取以下信息,并以 JSON 对象的形式返回:
- name
- major
- school
给定文本如下:
"""
学生姓名叫张三,专业:计算机科学,来自清华大学
"""

LLM 可能会按照你的预期,输出你想要的的 JSON 结构,像这样:

{
"name": "张三",
"major": "计算机科学",
"school": "清华大学"
}

也可能会额外附带一些描述语句,像下面这样,不稳定的输出就很难给到代码来直接使用。

在给定文本中,提取了以下信息:
```json
{
"name": "张三",
"major": "计算机科学",
"school": "清华大学"
}
```

TypeChat

微软为了优化上面 JSON 输出问题开源的一个 Node.js 包,官网地址:https://microsoft.github.io/TypeChat/ 。需要在和 LLM 交互之前,提前用 TypeScript 定义所需数据的 schema 描述。

export interface Student {
// 学生姓名
name: string;
// 学生专业
major: string;
// 学生的大学名称
school: string;
}

TypeChat 在背后的实现分为三部分:

  1. 读取 schema 中的 TypeScript 类型,填充到内置的 prompt 内。
  2. 附带上用户的 request 向 LLM 发起请求,等待 LLM 回复完毕。
  3. 使用 Zod 来校验 LLM 的回复是否符合 schema 中的定义。 如果不符合的话,就会发起一次 createRepairPrompt 来尝试修复 JSON 数据。
typescript/src/ts/program.ts
export function createProgramTranslator(
8 collapsed lines
model: TypeChatLanguageModel,
schema: string
): TypeChatJsonTranslator<Program> {
const validator = createTypeScriptJsonValidator<Program>(schema, "Program");
validator.createModuleTextFromJson = createModuleTextFromProgram;
const translator = createJsonTranslator<Program>(model, validator);
translator.createRequestPrompt = createRequestPrompt;
translator.createRepairPrompt = createRepairPrompt;
return translator;
function createRequestPrompt(request: string) {
return (
`You are a service that translates user requests into programs represented as JSON using the following TypeScript definitions:\n` +
`\`\`\`\n${programSchemaText}\`\`\`\n` +
`The programs can call functions from the API defined in the following TypeScript definitions:\n` +
`\`\`\`\n${validator.getSchemaText()}\`\`\`\n` +
`The following is a user request:\n` +
`"""\n${request}\n"""\n` +
`The following is the user request translated into a JSON program object with 2 spaces of indentation and no properties with the value undefined:\n`
);
}
function createRepairPrompt(validationError: string) {
return (
`The JSON program object is invalid for the following reason:\n` +
`"""\n${validationError}\n"""\n` +
`The following is a revised JSON program object:\n`
);
}
}
typescript/src/typechat.ts
async function translate(
request: string,
promptPreamble?: string | PromptSection[]
30 collapsed lines
) {
const preamble: PromptSection[] =
typeof promptPreamble === "string"
? [{ role: "user", content: promptPreamble }]
: promptPreamble ?? [];
let prompt: PromptSection[] = [
...preamble,
{ role: "user", content: typeChat.createRequestPrompt(request) },
];
let attemptRepair = typeChat.attemptRepair;
while (true) {
const response = await model.complete(prompt);
if (!response.success) {
return response;
}
const responseText = response.data;
const startIndex = responseText.indexOf("{");
const endIndex = responseText.lastIndexOf("}");
if (!(startIndex >= 0 && endIndex > startIndex)) {
return error(`Response is not JSON:\n${responseText}`);
}
const jsonText = responseText.slice(startIndex, endIndex + 1);
let jsonObject;
try {
jsonObject = JSON.parse(jsonText) as object;
} catch (e) {
return error(e instanceof SyntaxError ? e.message : "JSON parse error");
}
if (typeChat.stripNulls) {
stripNulls(jsonObject);
}
const schemaValidation = validator.validate(jsonObject);
const validation = schemaValidation.success
? typeChat.validateInstance(schemaValidation.data)
: schemaValidation;
if (validation.success) {
return validation;
}
if (!attemptRepair) {
return error(
`JSON validation failed: ${validation.message}\n${jsonText}`
);
7 collapsed lines
}
prompt.push({ role: "assistant", content: responseText });
prompt.push({
role: "user",
content: typeChat.createRepairPrompt(validation.message),
});
attemptRepair = false;
}
}

可以看到 TypeChat 的思路还是很巧妙的,类似于 Function Calling ,同时在背后实现了校验、重试等逻辑。 但是,最终能否稳定输出 JSON 取决于 LLM 模型本身的能力。如果模型的能力不足,使用过程中可能会频繁触发 createRepairPrompt 或者导致任务失败,会对最终的用户体验产生影响,可以按照 TypeChat - 使用技巧 这一章的建议来进行优化。

另外,想要减少重试和失败的次数,同时对 JSON 的依赖没那么高,也可以考虑使用 YAML 甚至 Markdown 来输出所需内容的格式。相较于JSON,这些格式更加宽容,容错率更高,不会因为一个符号的输出问题而导致整个内容提取和校验失败,坏处就是解析和校验的成本相较于 JSON 更高。

当然,OpenAI 也考虑到了这样的需求,推出了JSON Mode 让 LLM 输出 JSON。但是,并不支持使用 JSON Schema 来约束输出,目前感觉实用价值不太高。

最后编辑于