通过结构化输出约束大语言模型:Ollama、Qwen3 与 Python 或 Go

从Ollama获取结构化输出的几种方法

目录

大型语言模型(LLMs) 功能强大,但在生产环境中,我们很少希望得到自由形式的段落。 相反,我们希望获得可预测的数据:属性、事实或可以输入到应用程序中的结构化对象。 这就是LLM结构化输出

不久前,Ollama引入了结构化输出支持 (公告),使得可以限制模型的响应以匹配JSON模式。 这为诸如整理LLM功能、模型基准测试或自动化系统集成等任务解锁了一致的数据提取流程。

排成一行的鸭子

在本文中,我们将涵盖:

  • 什么是结构化输出及其重要性
  • 从LLM获取结构化输出的简单方法
  • Ollama的新功能是如何工作的
  • 提取LLM能力的示例:

什么是结构化输出?

通常,LLMs生成自由文本:

“模型X支持通过思维链进行推理,具有200K上下文窗口,并能使用英语、中文和西班牙语。”

这虽然可读,但难以解析

相反,通过结构化输出,我们可以要求一个严格的模式:

{
  "name": "Model X",
  "supports_thinking": true,
  "max_context_tokens": 200000,
  "languages": ["English", "Chinese", "Spanish"]
}

这种JSON很容易验证、存储在数据库中或输入到用户界面中。


从LLM获取结构化输出的简单方法

有时LLMs会理解模式是什么,并可以要求LLM使用特定模式返回JSON输出。 Qwen3模型由阿里巴巴优化,用于推理和结构化响应。你可以明确指示它以JSON格式响应

示例1:使用Qwen3与ollama在Python中,请求带有模式的JSON

import json
import ollama

prompt = """
你是一个结构化数据提取器。
只返回JSON。
文本:"Elon Musk 53岁,住在奥斯汀。"
模式:{ "name": string, "age": int, "city": string }
"""

response = ollama.chat(model="qwen3", messages=[{"role": "user", "content": prompt}])
output = response['message']['content']

# 解析JSON
try:
    data = json.loads(output)
    print(data)
except Exception as e:
    print("解析JSON时出错:", e)

输出:

{"name": "Elon Musk", "age": 53, "city": "Austin"}

使用Pydantic进行模式验证

为了避免格式错误的输出,你可以在Python中使用Pydantic模式进行验证。

from pydantic import BaseModel

class Person(BaseModel):
    name: str
    age: int
    city: str

# 假设'output'是来自Qwen3的JSON字符串
data = Person.model_validate_json(output)
print(data.name, data.age, data.city)

这确保输出符合预期的结构。


Ollama的结构化输出

Ollama现在允许你在format参数中传递一个模式。模型随后仅能以符合该模式的JSON格式进行响应 (文档)。

在Python中,你通常使用Pydantic定义模式,并让Ollama使用该模式作为JSON模式。


示例2:提取LLM功能元数据

假设你有一个描述LLM能力的文本片段:

“Qwen3具有强大的多语言支持(英语、中文、法语、西班牙语、阿拉伯语)。它允许推理步骤(思维链)。上下文窗口为128K tokens。”

你想要结构化数据:

from pydantic import BaseModel
from typing import List
from ollama import chat

class LLMFeatures(BaseModel):
    name: str
    supports_thinking: bool
    max_context_tokens: int
    languages: List[str]

prompt = """
分析以下描述并仅以JSON格式返回模型的功能。
模型描述:
'Qwen3具有强大的多语言支持(英语、中文、法语、西班牙语、阿拉伯语)。
它允许推理步骤(思维链)。
上下文窗口为128K tokens。'
"""

resp = chat(
    model="qwen3",
    messages=[{"role": "user", "content": prompt}],
    format=LLMFeatures.model_json_schema(),
    options={"temperature": 0},
)

print(resp.message.content)

可能的输出:

{
  "name": "Qwen3",
  "supports_thinking": true,
  "max_context_tokens": 128000,
  "languages": ["English", "Chinese", "French", "Spanish", "Arabic"]
}

示例3:比较多个模型

将多个模型的描述输入并提取为结构化格式:

from typing import List

class ModelComparison(BaseModel):
    models: List[LLMFeatures]

prompt = """
将每个模型的功能提取为JSON。

1. Llama 3.1支持推理。上下文窗口为128K。语言:仅英语。
2. GPT-4 Turbo支持推理。上下文窗口为128K。语言:英语、日语。
3. Qwen3支持推理。上下文窗口为128K。语言:英语、中文、法语、西班牙语、阿拉伯语。
"""

resp = chat(
    model="qwen3",
    messages=[{"role": "user", "content": prompt}],
    format=ModelComparison.model_json_schema(),
    options={"temperature": 0},
)

print(resp.message.content)

输出:

{
  "models": [
    {
      "name": "Llama 3.1",
      "supports_thinking": true,
      "max_context_tokens": 128000,
      "languages": ["English"]
    },
    {
      "name": "GPT-4 Turbo",
      "supports_thinking": true,
      "max_context_tokens": 128000,
      "languages": ["English", "Japanese"]
    },
    {
      "name": "Qwen3",
      "supports_thinking": true,
      "max_context_tokens": 128000,
      "languages": ["English", "Chinese", "French", "Spanish", "Arabic"]
    }
  ]
}

这使得根据模型功能进行基准测试、可视化或筛选变得非常简单。


示例4:自动检测缺失项

你甚至可以在字段缺失时允许null值:

from typing import Optional

class FlexibleLLMFeatures(BaseModel):
    name: str
    supports_thinking: Optional[bool]
    max_context_tokens: Optional[int]
    languages: Optional[List[str]]

这确保即使某些信息未知,你的模式仍然有效。


优势、注意事项与最佳实践

通过Ollama(或任何支持它的系统)使用结构化输出提供了许多优势——但也有一些注意事项。

优势

  • 更强的保证:模型被要求符合JSON模式,而不是自由文本。
  • 更容易解析:你可以直接使用json.loads或使用Pydantic / Zod进行验证,而不是使用正则表达式或启发式方法。
  • 基于模式的演进:你可以对模式进行版本控制,添加字段(带有默认值),并保持向后兼容性。
  • 互操作性:下游系统期望结构化数据。
  • 确定性(在低温时更好):当温度较低(例如0)时,模型更有可能严格遵守模式。Ollama的文档推荐这样做。

注意事项与陷阱

  • 模式不匹配:模型可能仍然偏离——例如,缺少必需的属性、重新排序键或包含额外字段。你需要验证。
  • 复杂的模式:非常深或递归的JSON模式可能会让模型困惑或导致失败。
  • 提示中的歧义:如果提示模糊,模型可能会错误地猜测字段或单位。
  • 模型之间的不一致性:某些模型可能在遵守结构化约束方面表现更好或更差。
  • 令牌限制:模式本身会增加提示或API调用的令牌成本。

最佳实践与技巧(来自Ollama博客 + 经验)

  • 使用**Pydantic(Python)Zod(JavaScript)**定义你的模式并自动生成JSON模式。这可以避免手动错误。
  • 在提示中始终包含指令,如**“仅以JSON格式响应”“不要包含评论或额外文本”**。
  • 使用temperature = 0(或非常低)以最小化随机性并最大化模式遵守。Ollama推荐确定性。
  • 在解析JSON失败或模式验证失败时,始终进行验证并可能回退(例如重试或清理)。
  • 从一个更简单的模式开始,然后逐步扩展。不要一开始过于复杂。
  • 包含有帮助但受限制的错误指令:例如,如果模型无法填充必需字段,响应null而不是省略它(如果模式允许)。

Go示例1:提取LLM功能

这是一个简单的Go程序,它要求Qwen3提供有关LLM功能的结构化输出。

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"

	"github.com/ollama/ollama/api"
)

type LLMFeatures struct {
	Name             string   `json:"name"`
	SupportsThinking bool     `json:"supports_thinking"`
	MaxContextTokens int      `json:"max_context_tokens"`
	Languages        []string `json:"languages"`
}

func main() {
	client, err := api.ClientFromEnvironment()
	if err != nil {
		log.Fatal(err)
	}

	prompt := `
  分析以下描述并仅以JSON格式返回模型的功能。
  描述:
  "Qwen3具有强大的多语言支持(英语、中文、法语、西班牙语、阿拉伯语)。
  它允许推理步骤(思维链)。
  上下文窗口为128K tokens。"
  `

	// 定义结构化输出的JSON模式
	formatSchema := map[string]any{
		"type": "object",
		"properties": map[string]any{
			"name": map[string]string{
				"type": "string",
			},
			"supports_thinking": map[string]string{
				"type": "boolean",
			},
			"max_context_tokens": map[string]string{
				"type": "integer",
			},
			"languages": map[string]any{
				"type": "array",
				"items": map[string]string{
					"type": "string",
				},
			},
		},
		"required": []string{"name", "supports_thinking", "max_context_tokens", "languages"},
	}

	// 将模式转换为JSON
	formatJSON, err := json.Marshal(formatSchema)
	if err != nil {
		log.Fatal("转换格式模式时出错:", err)
	}

	req := &api.GenerateRequest{
		Model:   "qwen3:8b",
		Prompt:  prompt,
		Format:  formatJSON,
		Options: map[string]any{"temperature": 0},
	}

	var features LLMFeatures
	var rawResponse string
	err = client.Generate(context.Background(), req, func(response api.GenerateResponse) error {
		// 累加内容
		rawResponse += response.Response

		// 仅在响应完成时解析
		if response.Done {
			if err := json.Unmarshal([]byte(rawResponse), &features); err != nil {
				return fmt.Errorf("JSON解析错误: %v", err)
			}
		}
		return nil
	})
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("解析的结构: %+v\n", features)
}

要编译并运行这个Go示例程序——假设我们有这个main.go文件在一个名为ollama-struct的文件夹中, 我们需要在这个文件夹内执行以下命令:

# 初始化模块
go mod init ollama-struct
# 拉取所有依赖
go mod tidy
# 构建并执行
go build -o ollama-struct main.go
./ollama-struct

示例输出

解析的结构: {Name:Qwen3 SupportsThinking:true MaxContextTokens:128000 Languages:[English Chinese French Spanish Arabic]}

Go示例2:比较多个模型

你可以扩展这个功能以提取模型列表进行比较。

  type ModelComparison struct {
		Models []LLMFeatures `json:"models"`
	}

	prompt = `
	从以下模型描述中提取功能并以JSON格式返回:

	1. PaLM 2: 该模型推理能力有限,专注于基本的语言理解。它支持8,000 tokens的上下文窗口。它主要支持英语。
	2. LLaMA 2: 该模型推理能力中等,可以处理一些逻辑任务。它可以处理4,000 tokens的上下文。它支持英语、西班牙语和意大利语。
	3. Codex: 该模型在编程和代码分析方面具有强大的推理能力。它有16,000 tokens的上下文窗口。它支持英语、Python、JavaScript和Java。

	返回一个包含所有模型的"models"数组的JSON对象。
	`

	// 定义模型比较的JSON模式
	comparisonSchema := map[string]any{
		"type": "object",
		"properties": map[string]any{
			"models": map[string]any{
				"type": "array",
				"items": map[string]any{
					"type": "object",
					"properties": map[string]any{
						"name": map[string]string{
							"type": "string",
						},
						"supports_thinking": map[string]string{
							"type": "boolean",
						},
						"max_context_tokens": map[string]string{
							"type": "integer",
						},
						"languages": map[string]any{
							"type": "array",
							"items": map[string]string{
								"type": "string",
							},
						},
					},
					"required": []string{"name", "supports_thinking", "max_context_tokens", "languages"},
				},
			},
		},
		"required": []string{"models"},
	}

	// 将模式转换为JSON
	comparisonFormatJSON, err := json.Marshal(comparisonSchema)
	if err != nil {
		log.Fatal("转换比较模式时出错:", err)
	}

	req = &api.GenerateRequest{
		Model:   "qwen3:8b",
		Prompt:  prompt,
		Format:  comparisonFormatJSON,
		Options: map[string]any{"temperature": 0},
	}

	var comp ModelComparison
	var comparisonResponse string
	err = client.Generate(context.Background(), req, func(response api.GenerateResponse) error {
		// 累加内容
		comparisonResponse += response.Response

		// 仅在响应完成时解析
		if response.Done {
			if err := json.Unmarshal([]byte(comparisonResponse), &comp); err != nil {
				return fmt.Errorf("JSON解析错误: %v", err)
			}
		}
		return nil
	})
	if err != nil {
		log.Fatal(err)
	}

	for _, m := range comp.Models {
		fmt.Printf("%s: 上下文=%d, 语言=%v\n", m.Name, m.MaxContextTokens, m.Languages)
	}

示例输出

PaLM 2: 上下文=8000, 语言=[English]
LLaMA 2: 上下文=4000, 语言=[English Spanish Italian]
Codex: 上下文=16000, 语言=[English Python JavaScript Java]

顺便说一下,qwen3:4b在这些示例中表现良好,与qwen3:8b相同。

Go开发人员的最佳实践

  • 将温度设置为0以实现最大的模式遵守。
  • 使用json.Unmarshal进行验证并在解析失败时回退。
  • 保持模式简单——深度嵌套或递归的JSON结构可能会导致问题。
  • 允许可选字段(在Go结构体标签中使用omitempty)如果预期数据缺失。
  • 添加重试如果模型偶尔生成无效JSON。

完整示例 - 使用LLM规格绘制图表(逐步:从结构化JSON到比较表格)

llm-chart

  1. 为你要提取的数据定义模式

使用Pydantic,你可以同时(a)为Ollama生成JSON模式和(b)验证模型的响应。

from pydantic import BaseModel
from typing import List, Optional

class LLMFeatures(BaseModel):
    name: str
    supports_thinking: bool
    max_context_tokens: int
    languages: List[str]
  1. 要求Ollama返回符合该模式的JSON

format=中传递模式,并将温度调低以实现确定性。

from ollama import chat

prompt = """
提取每个模型的功能。仅返回匹配模式的JSON。
1) Qwen3支持思维链;128K上下文;英语、中文、法语、西班牙语、阿拉伯语。
2) Llama 3.1支持思维链;128K上下文;英语。
3) GPT-4 Turbo支持思维链;128K上下文;英语、日语。
"""

resp = chat(
    model="qwen3",
    messages=[{"role": "user", "content": prompt}],
    format={"type": "array", "items": LLMFeatures.model_json_schema()},
    options={"temperature": 0}
)

raw_json = resp.message.content  # JSON列表的LLMFeatures
  1. 验证与规范化

在生产环境中使用前始终验证。

from pydantic import TypeAdapter

adapter = TypeAdapter(list[LLMFeatures])
models = adapter.validate_json(raw_json)  # -> list[LLMFeatures]
  1. 构建比较表(pandas)

将验证后的对象转换为DataFrame,可以排序、筛选并导出。

import pandas as pd

df = pd.DataFrame([m.model_dump() for m in models])
df["languages_count"] = df["languages"].apply(len)
df["languages"] = df["languages"].apply(lambda xs: ", ".join(xs))

# 重新排列列以提高可读性
df = df[["name", "supports_thinking", "max_context_tokens", "languages_count", "languages"]]

# 保存为CSV以供进一步使用
df.to_csv("llm_feature_comparison.csv", index=False)
  1. (可选)快速可视化

简单的图表可以帮助你快速查看模型之间的差异。

import matplotlib.pyplot as plt

plt.figure()
plt.bar(df["name"], df["max_context_tokens"])
plt.title("按模型的最大上下文窗口(tokens)")
plt.xlabel("模型")
plt.ylabel("最大上下文令牌")
plt.xticks(rotation=20, ha="right")
plt.tight_layout()
plt.savefig("max_context_window.png")

TL;DR

通过Ollama的新结构化输出支持,你可以将LLMs不仅视为聊天机器人,还可以视为数据提取引擎

上面的示例展示了如何自动提取有关LLM功能(如推理支持、上下文窗口大小和支持的语言)的结构化元数据——这些任务否则需要脆弱的解析。

无论你是构建LLM模型目录评估仪表板还是AI驱动的研究助手,结构化输出都能使集成变得顺畅、可靠且适合生产。

有用的链接