Ollama 结构化输出
让模型返回一段人读的文字很容易,让它返回一段程序能直接解析的数据就麻烦了——多一个逗号、少一个引号,json.loads 立刻抛异常。结构化输出(Structured Outputs)解决的就是这件事:把 JSON Schema 交给 Ollama,由它约束模型的生成过程,而不是生成完再祈祷格式对。
它适合三类活儿:从杂乱文本里抽字段、把图片描述成固定结构、让同一条提示词每次都返回一致形状的回复。
⚠️ 一个必须先知道的前提:Ollama 云端目前不支持结构化输出。下面的用法都跑在本地
localhost:11434上。
版本基线 v0.40.12,官方docs.ollama.com/capabilities/structured-outputs。
一、最简形态:format: "json"
只要在请求里加 format 字段,模型就会输出合法 JSON,但结构仍由它自己决定:
## format 设为 "json":保证是合法 JSON,但不保证字段名
curl -X POST http://localhost:11434/api/chat \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-oss",
"messages": [{"role": "user", "content": "用一句话介绍加拿大"}],
"stream": false,
"format": "json"
}'
from ollama import chat
response = chat(
model='gpt-oss',
messages=[{'role': 'user', 'content': '用一句话介绍加拿大'}],
format='json', # 只要合法 JSON,不约束结构
)
print(response.message.content)
import ollama from 'ollama'
const response = await ollama.chat({
model: 'gpt-oss',
messages: [{ role: 'user', content: '用一句话介绍加拿大' }],
format: 'json', // 同上
})
console.log(response.message.content)
实际项目里很少只用到这一步——因为下次运行返回的键名可能就变了。要真正稳定,得给 Schema。
二、给 Schema:format 换成对象
把 format 的值从字符串 "json" 换成一个标准的 JSON Schema 对象:
## format 传入完整 JSON Schema:字段名、类型、必填项全被约束
curl -X POST http://localhost:11434/api/chat \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-oss",
"messages": [{"role": "user", "content": "介绍加拿大"}],
"stream": false,
"format": {
"type": "object",
"properties": {
"name": {"type": "string"},
"capital": {"type": "string"},
"languages": {"type": "array", "items": {"type": "string"}}
},
"required": ["name", "capital", "languages"]
}
}'
官方给了一条很实用的建议:把同一份 Schema 也以字符串形式写进提示词。模型看到 format 里的结构约束,同时在提示词里再读一遍字段含义,两者互相印证,产出质量通常更稳——尤其字段名比较抽象的时候。
手写 Schema 容易出错,Python 和 JavaScript 各有更省事的路子。
Python:Pydantic 一把梭。 用 model_json_schema() 生成 Schema,用 model_validate_json() 校验回包,两行代码省掉手工拼对象:
## pip install ollama pydantic
from ollama import chat
from pydantic import BaseModel
class Country(BaseModel):
name: str
capital: str
languages: list[str]
response = chat(
model='gpt-oss',
messages=[{'role': 'user', 'content': '介绍加拿大'}],
format=Country.model_json_schema(), # 由模型类自动生成 Schema
)
country = Country.model_validate_json(response.message.content) # 顺手校验
print(country.capital) # 现在是强类型对象,不是裸 dict
JavaScript:Zod。 思路一样,z.toJSONSchema() 生成、parse 校验:
// npm install ollama zod
import ollama from 'ollama'
import * as z from 'zod'
const Country = z.object({
name: z.string(),
capital: z.string(),
languages: z.array(z.string()),
})
const response = await ollama.chat({
model: 'gpt-oss',
messages: [{ role: 'user', content: '介绍加拿大' }],
format: z.toJSONSchema(Country), // 由 Zod schema 生成 JSON Schema
})
const country = Country.parse(JSON.parse(response.message.content))
console.log(country.capital)
三、实战一:从自然语言里抽结构化数据
这是结构化输出最高频的用法。假设用户随手打了一段话:
"我养了两只猫,一只叫 Luna,一只叫 Loki…"
你想拿到一份结构化的宠物清单。定义好模型类,把"字段含义"交给类型注解,剩下的模型会填:
from ollama import chat
from pydantic import BaseModel
## 单只宠物:Optional 字段允许模型在信息缺失时返回 null
class Pet(BaseModel):
name: str
animal: str
age: int
color: str | None
favorite_toy: str | None
class PetList(BaseModel):
pets: list[Pet]
response = chat(
model='gpt-oss',
messages=[{'role': 'user', 'content': '我养了两只猫,一只叫 Luna,一只叫 Loki…'}],
format=PetList.model_json_schema(),
)
pets = PetList.model_validate_json(response.message.content)
for p in pets.pets:
print(p.name, p.animal, p.age)
注意 str | None 的用处:用户没提到颜色和玩具,模型就能合法返回 null,而不是硬编一个假值。该可选的字段一定要标可选,否则模型会为了满足 Schema 而编造内容,这是抽取任务最常见的脏数据来源。
四、实战二:视觉 + 结构化,输出确定的图像描述
视觉模型接受同一个 format 参数。两者组合起来,图片描述就能变成程序可用的数据,而不是一段散文:
from ollama import chat
from pydantic import BaseModel
from typing import Literal, Optional
class Object(BaseModel):
name: str
confidence: float
attributes: str
class ImageDescription(BaseModel):
summary: str
objects: list[Object]
scene: str
colors: list[str]
# Literal 把取值锁死成枚举,避免模型自创"清晨""黄昏"
time_of_day: Literal['Morning', 'Afternoon', 'Evening', 'Night']
setting: Literal['Indoor', 'Outdoor', 'Unknown']
text_content: Optional[str] = None
response = chat(
model='gemma4',
messages=[{
'role': 'user',
'content': '描述这张照片,并列出你检测到的物体。',
'images': ['path/to/image.jpg'],
}],
format=ImageDescription.model_json_schema(),
options={'temperature': 0}, # 视觉标注建议压到 0,减少随机性
)
image_description = ImageDescription.model_validate_json(response.message.content)
print(image_description.scene)
Literal 这个技巧值得记住:比起在提示词里写"请从早/中/晚/夜中选一个",用类型枚举约束显然更可靠,模型连"傍晚"这种边界词都没机会输出。
五、四条提高成功率的经验
优先级:用模型类而不是手写 Schema。 Pydantic / Zod 生成的 Schema 与校验逻辑同源,改一处两边同步,不会出现"Schema 改了校验没改"。
温度压到 0。 官方明确建议在需要确定性的场景设 temperature: 0。同样的输入反复调用,结果才稳定可比。
可选字段一律显式标可选。 见上面第三节,这是防止模型编造的关键。
OpenAI 兼容接口走 response_format。 如果你的代码是套 OpenAI SDK 的,不用改成 /api/chat,OpenAI 兼容层同样支持结构化输出,只是参数名是 response_format 而非 format。
想按 JSON 之外的逻辑控制生成,可以看 3.4 Ollama 决策(Decision);图片输入的完整用法见 3.5 Ollama 视觉(Vision)。