本站内容由 AI 生成,可能存在错误。如发现问题,欢迎到 GitHub Issues 反馈。

SGLang 编程模型与结构化输出

SGLang 编程模型与结构化输出

更新于 2026-04-10

为什么需要编程模型

现实中的 LLM 应用很少是”输入 prompt → 输出文本”这么简单。一个典型的 RAG pipeline 可能是:

  1. 用户提问 → LLM 生成搜索 query
  2. 检索文档 → 拼接到上下文
  3. LLM 基于上下文生成答案
  4. 对答案做自我检查(self-consistency:同一问题采样 3 次取多数)

传统做法是多次调用 API,每次手动拼接上下文。问题:

  • 重复计算:每次调用都要重新处理整个 prompt(包括已经算过的前缀)
  • 串行等待:即使步骤 4 的三次采样可以并行,也只能逐次调用
  • 格式不可靠:要求 LLM 输出 JSON,但它经常格式错误

SGLang 的核心洞察:如果推理引擎能理解应用的执行逻辑,就能端到端优化整个 pipeline

SGLang DSL 核心原语

SGLang 定义了一组简洁的原语来描述 LLM 程序:

原语作用示例
gen生成文本s = sgl.gen("分析", max_tokens=100)
select从选项中选择s = sgl.select("判断", ["正面","负面"])
fork并行分支s1, s2 = sgl.fork(s, 2)
join汇合分支result = sgl.join([s1, s2])
append拼接上下文s = sgl.append("额外信息...")

这些原语看起来简单,但组合起来能表达复杂的推理流程:

# SGLang DSL 示例: 多步推理
s = sgl.gen("分析", max_tokens=100)
s = sgl.select("判断", ["正面","负面","中性"])
s1, s2 = sgl.fork(s, 2)
s1 = sgl.gen("解释1", max_tokens=50)
s2 = sgl.gen("解释2", max_tokens=50)
result = sgl.join([s1, s2])
result = sgl.append("综合结论:")
result = sgl.gen("结论", max_tokens=80)
gen("分析")select("判断")fork(2)gen("解释1")gen("解释2")join()append("综合结论:")gen("结论")genselectfork/joinappend

关键在于:这不只是语法糖。SGLang 的执行引擎会分析 DSL 程序的结构,自动进行优化:

  • fork 的两个分支共享前缀 → RadixAttention 自动复用 KV Cache
  • select 的所有候选项共享前缀 → batch 化处理
  • append 不触发重新计算 → 直接在已有 KV Cache 上追加

约束解码的问题

让 LLM 输出结构化数据(如 JSON)是最常见的需求之一。但自由生成的问题很多:

// 期望的输出
{"name": "Alice", "age": 30}

// 实际 LLM 可能生成的
{"name": Alice, "age": "thirty"}   // 缺引号,类型错误
{"name": "Alice" "age": 30}         // 缺逗号
{"name": "Alice"}                    // 缺少 required 字段

Prompt engineering 只能缓解但无法根除这个问题——LLM 的 token 采样过程本质上是概率性的,没有结构约束。

FSM-Guided Generation

FSM 编译流水线FSM 约束解码编译流水线预编译:首次加载 schema 时构建,运行时 O(1) 查表JSON Schema{"type":"object",...}正则表达式\{"name":"[^"]+",…FSM有限状态机Token Mask合法 token 集合结果:100% 格式合规,非法 token logit = -∞

SGLang 的解决方案是在 token 采样阶段引入有限状态机 (FSM) 约束:

  1. JSON Schema → 正则表达式:将用户定义的输出格式转化为正则表达式
  2. 正则 → FSM:编译为有限状态机,每个状态对应解析器的一个位置
  3. FSM 指导采样:每步根据当前 FSM 状态,计算合法 token 集合,将非法 token 的 logit 设为 -∞
输出:(点击合法 token 开始生成)
START{ 已打开"key"name": 分隔"val字符串内容"val 结束} 关闭END{"n,a,m,e":"a-z..."}EOS
Token 词表(绿色=合法,灰色=非法):

结果:100% 格式合规。无论 LLM 的”创造力”如何发挥,FSM mask 保证了每个输出 token 都符合目标 schema。

Token Mask 生成过程

具体来看 FSM 如何在每一步决定合法 token:

JSON Schema: { "type": "object", "properties": { "name": { "type": "string" } }, "required": ["name"] }
已生成: (空)
FSM 状态
OBJECT_START
合法字符集
{
Token Mask:
{
"
hello
[
123
true
💡 JSON Object 必须以 { 开头,只有 { 合法
1 / 5

整个过程是预编译的:FSM 在首次加载 schema 时就构建好,运行时只需 O(1) 查表获取当前状态的合法 token 集合,几乎不增加推理延迟。

Jump-Forward 优化

Jump-Forward 优化Jump-Forward 优化:跳过确定性 Token确定性状态(FSM 只有一条出边)可直接跳过,无需 LLM forward passToken类型{跳过"跳过nameLLM"跳过:跳过 跳过"跳过AliceLLM"跳过,跳过 跳过"跳过ageLLM"跳过:跳过 跳过30LLM}跳过直接填入确定性 token(可跳过)— 14/18需 LLM 生成 — 4/18跳过率 78% → 2-5x 加速

FSM 约束虽然保证了正确性,但每个 token 仍需走完整的 LLM forward pass——即使某些位置的输出是确定的(如 JSON 的 {, ", : 等结构字符)。

SGLang 的 Jump-Forward 优化识别 FSM 中的确定性状态(只有一条出边的状态),直接跳过这些位置,不走 LLM:

无约束
Step 1: 无约束逐 token 生成每个 token 都需要采样 → 可能产出非法 JSON → 17 次采样{"name": "Alice"}问题: 可能生成 {"{"}"name: Alice{"}"} 等非法 JSON速度: 17 次 LLM forward pass

对于结构化输出,确定性片段占比通常 40-70%(JSON 的大量结构字符都是确定的),这意味着 Jump-Forward 可以跳过大量 forward pass,实现 2-5x 加速

性能对比

综合正确率和速度:

结构化输出合规率对比无约束45%Regex-guided82%FSM-guided99%FSM + Jump-Forward99%合规率 (%)

核心取舍:

  • 无约束最快但不可靠——适合对格式要求宽松的场景
  • FSM-guided几乎完美但略慢——token mask 计算和 vocabulary 扫描有开销
  • FSM + Jump-Forward兼顾正确率和速度——是 SGLang 的默认推荐方案

实际使用方式

SGLang 架构SGLang 前后端分离架构Client 端OpenAI API/v1/chat/completionsNative Engine进程内调用DSL Frontend@sgl.functionServer 端RadixAttentionFSM CompilerJump-Forward模型推理约束参数 (json_schema / regex / ebnf) 为 per-request,不需要配置文件

前面介绍了 DSL 原语和 FSM 约束解码的原理,但具体怎么把这些能力用起来?SGLang 采用前后端分离架构:server 端加载模型负责推理,client 端通过 HTTP API 发送请求。约束参数是 per-request 的——不需要配置文件,每个请求自己决定用什么 schema。

启动 Server

# 启动 SGLang 推理服务器,加载指定模型
python -m sglang.launch_server \
  --model-path meta-llama/Meta-Llama-3.1-8B-Instruct \
  --port 30000 \
  --grammar-backend xgrammar   # 语法后端,默认 xgrammar,可选 outlines/llguidance

Server 启动后暴露 OpenAI 兼容端点(/v1/chat/completions)和 SGLang 原生端点(/generate),所有 FSM 编译、Jump-Forward 优化都在 server 端自动生效。

方式一:OpenAI 兼容 API

最主流的使用方式。用标准 OpenAI SDK 即可调用,结构化输出约束通过 response_formatextra_body 传入:

from openai import OpenAI

client = OpenAI(base_url="http://localhost:30000/v1", api_key="none")

# JSON Schema 约束 —— 通过 OpenAI 标准字段 response_format
response = client.chat.completions.create(
    model="meta-llama/Meta-Llama-3.1-8B-Instruct",
    messages=[{"role": "user", "content": "法国首都的信息,用JSON格式"}],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "capital_info",
            "schema": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "population": {"type": "integer"}
                },
                "required": ["name", "population"]
            }
        }
    }
)

# 正则约束 —— 通过 extra_body(SGLang 扩展字段)
response = client.chat.completions.create(
    model="meta-llama/Meta-Llama-3.1-8B-Instruct",
    messages=[{"role": "user", "content": "法国的首都是?"}],
    extra_body={"regex": "(Paris|London|Berlin)"}
)

# EBNF 语法 —— 通过 extra_body(SGLang 扩展字段)
response = client.chat.completions.create(
    model="meta-llama/Meta-Llama-3.1-8B-Instruct",
    messages=[{"role": "user", "content": "欧洲城市信息"}],
    extra_body={"ebnf": """root ::= city " is the capital of " country
city ::= "Paris" | "London" | "Berlin"
country ::= "France" | "England" | "Germany" """}
)

注意三种约束互斥——每个请求只能指定 json_schemaregexebnf 中的一种。

方式二:Native Offline Engine

不走 HTTP,在同一个 Python 进程内加载模型直接推理,适合批量离线场景:

import sglang as sgl
import json
from pydantic import BaseModel, Field

class CapitalInfo(BaseModel):
    name: str = Field(..., pattern=r"^\w+$")
    population: int = Field(...)

llm = sgl.Engine(model_path="meta-llama/Meta-Llama-3.1-8B-Instruct")

outputs = llm.generate(
    ["Tell me about the capital of France in JSON."],
    {"temperature": 0, "json_schema": json.dumps(CapitalInfo.model_json_schema())}
)

约束参数(json_schemaregexebnf)直接写在 sampling_params 字典里,效果和 API 方式完全一致。

对比总结

使用方式通信方式适用场景约束传递
OpenAI 兼容 APIHTTP (/v1/chat/completions)在线服务、现有 OpenAI 代码迁移response_format / extra_body
Native Engine进程内调用批量离线推理sampling_params 字典
DSL Frontend (@sgl.function)HTTP + session 管理复杂多步编排(fork/join)Python 代码中内联

前两种方式覆盖了绝大多数场景。DSL Frontend 主要用于需要 fork/join 前缀复用的复杂编排,日常结构化输出用 OpenAI API 即可。

总结

SGLang 的编程模型解决了两个核心问题:

  1. 执行效率:通过 DSL 原语(gen/select/fork/join)让引擎理解应用逻辑,配合 RadixAttention 最大化前缀复用
  2. 输出可靠性:通过 FSM-guided generation + Jump-Forward 优化,实现 100% 格式合规且速度更快

从更大的视角看,SGLang 代表了 LLM 推理引擎从”通用 API 服务”向”可编程推理平台”的演进——它不只是更快地生成文本,而是让推理引擎理解并优化整个 LLM 应用的执行流程。

延伸阅读