This article has an English version available.

为什么我的 LLM 结构化输出效果很差?

让大型语言模型(LLM)输出结构化结果对许多真实应用至关重要,但这依然是使用 LLM 时最具挑战的部分之一。

在我使用不同结构化输出方法的经验中,我发现不存在通用的解决方案。本文讨论我实践过的三种方式:工具调用 API、基于 XML 的方法,以及自定义领域特定语言(DSL)。它们各有优势与局限,适用于不同场景。

1. 工具调用 API

这是最常见的 LLM 结构化输出方法。但有些 LLM 对工具调用 API 的支持并不好,我们可能需要用 ReAct 来实现。我做了一个 Python 中间层,行为上类似工具调用 API,但底层使用提示词: https://github.com/BeautyyuYanli/tooluser

工具调用 API 最大的问题在于,它与 LLM 的其他部分使用了不同的训练方式。

文本回复、推理、工具调用输出是分别训练的,所以我们经常遇到 LLM 思路一致、结果却完全不同的情况(尤其是在 O 系列早期版本和 DeepSeek R1 上)。工具使用 API 也存在类似问题,只是更隐蔽一些。

一个例子是角色扮演类应用。LLM 既需要有良好的文学表达,也要输出角色的想法、动作等结构化内容。如果在这些场景使用工具调用 API,LLM 的文学表达会完全消失。

2. 通用 XML

为了解决上述问题,我发现 XML 是一种有效的方法。XML 本身对文本表达很友好(没有引号、没有转义问题),这也是 HTML 被发明的原因。

对于 LLM,我只需要用提示词描述 XML 输出结构,再用一个校验器验证 LLM 的文本回复即可。使用 XML 可以让 LLM 在结构化输出的同时保持出色的文学表达。我写了一个简单的 Python 库供自己使用: https://github.com/BeautyyuYanli/qwq-tag

3. 自定义 DSL

与文学表达相对的另一种用例,是让 LLM 生成复杂的结构化输出。如果结构是 JSON 或像 PostgreSQL 这样的主流 DSL,LLM 往往能轻松产出结果。但在我的场景里,我要生成的是 ISO GQL(面向图数据库的类 SQL 图查询语言)。LLM 对它了解有限,很难正确理解我的指令。这会带来两个问题:如何验证输出,以及如何帮助 LLM 生成更好的结果?

核心思路是建立中间表达。我设计了两步流程:第一步把指令翻译成伪代码,第二步再从伪代码生成 DSL。它类似在回答前的 <think> 过程,但我让模型把伪代码当作 <think> 内容。这种方法能显著提升输出质量。

为了验证 LLM 的输出,我会把 DSL 映射成 JSON,然后用工具调用 API 生成 JSON。另一种方法是使用基于 AST 的校验器。

但如果 LLM 很难生成正确的语法,就需要不断试错修正,这可能会损害最终结果的质量。另外,写出一个带有清晰、人类/LLM 可读错误信息的高质量校验器并不容易。

LLM 的引导式生成(如 vLLM 支持的方案)也许是另一个很有希望的方向。详见: https://docs.vllm.ai/en/stable/features/structured_outputs.html

结语

每一种结构化输出方法都对应不同需求与优先级。最终的选择取决于你的具体场景。