SGLang 编程模型与结构化输出
更新于 2026-04-10
为什么需要编程模型
现实中的 LLM 应用很少是”输入 prompt → 输出文本”这么简单。一个典型的 RAG pipeline 可能是:
- 用户提问 → LLM 生成搜索 query
- 检索文档 → 拼接到上下文
- LLM 基于上下文生成答案
- 对答案做自我检查(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 程序的结构,自动进行优化:
- 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
SGLang 的解决方案是在 token 采样阶段引入有限状态机 (FSM) 约束:
- JSON Schema → 正则表达式:将用户定义的输出格式转化为正则表达式
- 正则 → FSM:编译为有限状态机,每个状态对应解析器的一个位置
- FSM 指导采样:每步根据当前 FSM 状态,计算合法 token 集合,将非法 token 的 logit 设为 -∞
结果:100% 格式合规。无论 LLM 的”创造力”如何发挥,FSM mask 保证了每个输出 token 都符合目标 schema。
Token Mask 生成过程
具体来看 FSM 如何在每一步决定合法 token:
整个过程是预编译的:FSM 在首次加载 schema 时就构建好,运行时只需 O(1) 查表获取当前状态的合法 token 集合,几乎不增加推理延迟。
Jump-Forward 优化
FSM 约束虽然保证了正确性,但每个 token 仍需走完整的 LLM forward pass——即使某些位置的输出是确定的(如 JSON 的 {, ", : 等结构字符)。
SGLang 的 Jump-Forward 优化识别 FSM 中的确定性状态(只有一条出边的状态),直接跳过这些位置,不走 LLM:
对于结构化输出,确定性片段占比通常 40-70%(JSON 的大量结构字符都是确定的),这意味着 Jump-Forward 可以跳过大量 forward pass,实现 2-5x 加速。
性能对比
综合正确率和速度:
核心取舍:
- 无约束最快但不可靠——适合对格式要求宽松的场景
- FSM-guided几乎完美但略慢——token mask 计算和 vocabulary 扫描有开销
- FSM + Jump-Forward兼顾正确率和速度——是 SGLang 的默认推荐方案
实际使用方式
前面介绍了 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_format 或 extra_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_schema、regex、ebnf 中的一种。
方式二: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_schema、regex、ebnf)直接写在 sampling_params 字典里,效果和 API 方式完全一致。
对比总结
| 使用方式 | 通信方式 | 适用场景 | 约束传递 |
|---|---|---|---|
| OpenAI 兼容 API | HTTP (/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 的编程模型解决了两个核心问题:
- 执行效率:通过 DSL 原语(gen/select/fork/join)让引擎理解应用逻辑,配合 RadixAttention 最大化前缀复用
- 输出可靠性:通过 FSM-guided generation + Jump-Forward 优化,实现 100% 格式合规且速度更快
从更大的视角看,SGLang 代表了 LLM 推理引擎从”通用 API 服务”向”可编程推理平台”的演进——它不只是更快地生成文本,而是让推理引擎理解并优化整个 LLM 应用的执行流程。
延伸阅读
- 想回顾前缀缓存的基础?阅读 前缀缓存与 RadixAttention
- 想了解推理引擎的全景?阅读 推理引擎全景