# Structured Outputs

## 概述

Structured Outputs 是指通过预定义格式约束，使大模型生成内容严格遵循指定结构。该功能可显著提升生成结果的可控性，适用于需要精确格式输出的场景（如API调用、数据解析、代码生成等），同时支持动态语法扩展，平衡灵活性与规范性。

FastDeploy 支持使用 [XGrammar](https://xgrammar.mlc.ai/docs/) 后端生成结构化输出。

支持输出格式

- `json格式`: 输出内容为标准的 JSON 格式
- `正则表达式格式（regex）`: 精确控制文本模式（如日期、邮箱、身份证号等），支持复杂正则表达式语法
- `选项格式（choice）`: 输出严格限定在预定义选项中
- `语法格式（grammar）`: 使用扩展 BNF 语法定义复杂结构，支持字段名、类型及逻辑关系的联合约束
- `结构标签格式（structural_tag）`: 通过标签树定义结构化输出，支持嵌套标签、属性校验及混合约束

## 使用方式

服务启动时，可以通过 `--guided-decoding-backend` 参数指定期望使用的后端，如果指定 `auto`, FastDeploy 会自动选择合适的后端。

### OpenAI 接口

FastDeploy 支持 OpenAI 的 [Completions](https://platform.openai.com/docs/api-reference/completions) 和 [Chat Completions](https://platform.openai.com/docs/api-reference/chat) API，OpenAI 通过 `response_format` 参数指定 Structured Outputs 的输出格式。

FastDeploy 支持通过指定 `json_object` 来保证输出为json格式，如果想指定 JSON 内部具体参数名、类型等信息，可以通过 `json_schema` 来指定详细信息，详细使用方法可以参考示例。

```python
import openai

port = "8170"
client = openai.Client(base_url=f"http://127.0.0.1:{port}/v1", api_key="null")

completion = client.chat.completions.create(
    model="null",
    messages=[
        {
            "role": "user",
            "content": "生成一个包含以下信息的JSON对象：中国四大发明名称、发明朝代及简要描述（每项不超过50字）",
        }
    ],
    response_format={"type": "json_object"}
)
print(completion.choices[0].message.content)
print("\n")
```

输出

```
{"inventions": [{"name": "造纸术", "dynasty": "东汉", "description": "蔡伦改进造纸术，用树皮、麻头等为原料，成本低廉，推动文化传播。"}, {"name": "印刷术", "dynasty": "唐朝", "description": "雕版印刷术在唐朝出现，后毕昇发明活字印刷术，提高印刷效率。"}, {"name": "火药", "dynasty": "唐朝", "description": "古代炼丹家偶然发明，后用于军事，改变了战争方式和格局。"}, {"name": "指南针", "dynasty": "战国", "description": "最初称司南，后发展为指南针，为航海等提供方向指引。"}]}
```

以下示例要求模型返回一个 book 信息的 json 数据，json 数据中必须包含`author、title、genre`三个字段，且`genre`必须在给定 `BookType` 中。

```python
import openai
from pydantic import BaseModel
from enum import Enum

class BookType(str, Enum):
    romance = "Romance"
    historical = "Historical"
    adventure = "Adventure"
    mystery = "Mystery"
    dystopian = "Dystopian"

class BookDescription(BaseModel):
    author: str
    title: str
    genre: BookType

port = "8170"
client = openai.Client(base_url=f"http://127.0.0.1:{port}/v1", api_key="null")

completion = client.chat.completions.create(
    model="null",
    messages=[
        {
            "role": "user",
            "content": "生成一个JSON，描述一本中国的著作，要包含作者、标题和书籍类型。",
        }
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "book-description",
            "schema": BookDescription.model_json_schema()
        },
    },
)
print(completion.choices[0].message.content)
print("\n")
```

输出

```
{"author": "曹雪芹", "title": "红楼梦", "genre": "Historical"}
```

`structural_tag` 可以提取输入内容中的重点信息，并调用指定方法，返回预定义的结构化输出。以下示例展示了通过 `structural_tag` 获取指定时区并输出格式化结果。

```python
import openai

port = "8170"
client = openai.Client(base_url=f"http://127.0.0.1:{port}/v1", api_key="null")

content_str = """
你有以下函数可以调用：

{
    "name": "get_current_date",
    "description": "根据给定的时区获取当前日期和时间",
    "parameters": {
        "type": "object",
        "properties": {
            "timezone": {
                "type": "string",
                "description": "获取当前日期和时间的时区, 例如: Asia/Shanghai",
            }
        },
        "required": ["timezone"],
    }
}

如果你选择只调用函数，请按照以下格式回复：
<{start_tag}={function_name}>{parameters}{end_tag}
其中

start_tag => `<function`
parameters => 一个JSON字典，其中函数参数名为键，函数参数值为值。
end_tag => `</function>`

这是一个示例，
<function=example_function_name>{"example_name": "example_value"}</function>

注意：
- 函数调用必须遵循指定的格式
- 必须指定所需参数
- 一次只能调用一个函数
- 将整个函数调用回复放在一行上

你是一个人工智能助理，请回答下列问题。
"""

completion = client.chat.completions.create(
    model="null",
    messages=[
        {
            "role": "system",
            "content": content_str,
        },
        {
            "role": "user",
            "content": "你今天去上海出差",
        }
    ],
    response_format={
        "type": "structural_tag",
        "structures": [
            {
                "begin": "<function=get_current_date>",
                "schema": {
                    "type": "object",
                    "properties": {
                        "timezone": {
                            "type": "string",
                            "description": "获取当前日期和时间的时区, 例如: Asia/Shanghai",
                        }
                    },
                    "required": ["timezone"],
                },
                "end": "</function>",
            }
        ],
        "triggers": ["<function="],
    },
)
print(completion.choices[0].message.content)
print("\n")
```

输出

```
<function=get_current_date>{"timezone": "Asia/Shanghai"}</function>
```

对于 `choice、grammar、regex`格式，FastDeploy 使用 `extra_body` 参数支持，`choice` 较为简单，指定后模型会在预定义选项中选择一个最优项。在 FastDeploy 中，可以通过 `guided_choice` 指定一组与定义选项，示例如下

```python
import openai

port = "8170"
client = openai.Client(base_url=f"http://127.0.0.1:{port}/v1", api_key="null")

completion = client.chat.completions.create(
    model="null",
    messages=[
        {"role": "user", "content": "深圳的地标建筑是什么？"}
    ],
    extra_body={"guided_choice": ["深圳平安国际金融中心", "中国华润大厦（春笋大厦）", "深圳京基100", "深圳地王大厦"]},
)
print(completion.choices[0].message.content)
print("\n")
```

输出

```
深圳地王大厦
```

`regex` 允许用户通过正则表达式限制输出格式，通常用于需要精确控制文本格式的场景。以下示例展示了通过模型生成一个标准格式的网络地址的过程。

```python
import openai

port = "8170"
client = openai.Client(base_url=f"http://127.0.0.1:{port}/v1", api_key="null")

completion = client.chat.completions.create(
    model="null",
    messages=[
        {
            "role": "user",
            "content": "生成一个标准格式的网络地址，包括协议、域名。\n",
        }
    ],
    extra_body={"guided_regex": r"^https:\/\/www\.[a-zA-Z]+\.com\/?$\n"},
)
print(completion.choices[0].message.content)
print("\n")
```

输出

```
https://www.example.com/
```

`grammar` 允许用户通过 EBNF(Extended Backus-Naur Form) 语法描述一个限制规则，以下示例展示了通过 `guided_grammar` 限制模型输出一段html代码。

```python
import openai

port = "8170"
client = openai.Client(base_url=f"http://127.0.0.1:{port}/v1", api_key="null")

html_h1_grammar = """
    root ::= html_statement

    html_statement ::= "<h1" style_attribute? ">" text "</h1>"

    style_attribute ::= " style=" dq style_value dq

    style_value ::= (font_style ("; " font_weight)?) | (font_weight ("; " font_style)?)

    font_style ::= "font-family: '" font_name "'"

    font_weight ::= "font-weight: " weight_value

    font_name ::= "Arial" | "Times New Roman" | "Courier New"

    weight_value ::= "normal" | "bold"

    text ::= [A-Za-z0-9 ]+

    dq ::= ["]
"""

completion = client.chat.completions.create(
    model="null",
    messages=[
        {
            "role": "user",
            "content": "生成一段html的代码，对以下标题加粗、Times New Roman字体。标题：ERNIE Bot",
        }
    ],
    extra_body={"guided_grammar": html_h1_grammar},
)
print(completion.choices[0].message.content)
print("\n")
```

输出

```
<h1 style="font-family: 'Times New Roman'; font-weight: bold">ERNIE Bot</h1>
```

### OpenAI beta 接口

在 OpenAI Client 1.54.4 版本之后，提供了 `client.beta.chat.completions.parse` 接口，通过该接口，实现了与 Python 原生类型更好的支持。以下示例展示了使用该接口直接获取 `pydantic` 类型的结构化输出。

```python
from pydantic import BaseModel
import openai

class Info(BaseModel):
    addr: str
    height: int

port = "8170"
client = openai.Client(base_url=f"http://127.0.0.1:{port}/v1", api_key="null")

completion = client.beta.chat.completions.parse(
    model="null",
    messages=[
        {"role": "user", "content": "上海东方明珠在上海市浦东新区世纪大道1号，高度为468米，请问上海东方明珠的地址和高度是多少？"},
    ],
    response_format=Info,
)

message = completion.choices[0].message
print(message)
print("\n")
assert message.parsed
print("地址:", message.parsed.addr)
print("高度:", message.parsed.height)
```

输出

```
ParsedChatCompletionMessage[Info](content='{"addr": "上海市浦东新区世纪大道1号", "height": 468}', refusal=None, role='assistant', annotations=None, audio=None, function_call=None, tool_calls=None, parsed=Info(addr='上海市浦东新区世纪大道1号', height=468), reasoning_content=None)


地址: 上海市浦东新区世纪大道1号
高度: 468
```
