文章目录
-
- [一. 双层体系架构背景](#一. 双层体系架构背景)
-
- [1.1 概述](#1.1 概述)
- [1.2 为什么需要双层架构体系](#1.2 为什么需要双层架构体系)
-
- [1.2.1 背景:](#1.2.1 背景:)
- [1.2.2 举例说明:](#1.2.2 举例说明:)
- [1.2.3 小结](#1.2.3 小结)
- [1.3 为什么vllm没有前端语言层](#1.3 为什么vllm没有前端语言层)
- [1.4 sglang双层体系架构图](#1.4 sglang双层体系架构图)
- 二、双层体系架构背景
-
- [2.1 sglang前端语言层架构图](#2.1 sglang前端语言层架构图)
-
- [2.1.1 Public API Layer:DSL核心原语层:](#2.1.1 Public API Layer:DSL核心原语层:)
- [2.1.2 DSL / IR:把写法变成结构](#2.1.2 DSL / IR:把写法变成结构)
- [2.1.3 Interpreter / Executor:前端语言真正的执行内核](#2.1.3 Interpreter / Executor:前端语言真正的执行内核)
- [2.1.4 Backend Abstraction------语言语义与推理后端解耦](#2.1.4 Backend Abstraction——语言语义与推理后端解耦)
- [2.1.5 Runtime Adapter + SRT:语言世界与 Serving 世界的连接模块](#2.1.5 Runtime Adapter + SRT:语言世界与 Serving 世界的连接模块)
- [2.2 multi_dimensional_judge用例端到端说明](#2.2 multi_dimensional_judge用例端到端说明)
- [2.3 运行时优化机会:语言原语驱动的后端优化](#2.3 运行时优化机会:语言原语驱动的后端优化)
一. 双层体系架构背景
1.1 概述
SGLang 与 vLLM、TensorRT-LLM 等推理框架的核心差异在于:SGLang 不只提供高性能 serving engine,还提供面向 LLM 应用编排的前端语言层。它既能作为后端推理引擎使用,也能通过前端语言表达多轮、多分支、结构化的 LLM 交互程序。
- vLLM 等框架主要定位为 serving engine,把应用编排、状态管理和控制流交给客户端或上层框架。
- SGLang 同时提供 Frontend Language Layer 和 Backend Runtime Layer,两层可以协同工作,也可以按场景独立使用
sglang 的双层架构体系从README.md 说起: https://github.com/sgl-project/sglang/blob/main/README.md
- Backend Tutorial
- Frontend Tutorial
- README 文档将 "Backend Tutorial" 与 "Frontend Tutorial" 作为两个独立的章节列出,这是和vllm中的重要不同点。
根据SGLang 的论文 :SGLang: Efficient Execution of Structured Language Model

SGLang 由两个部分组成:前端语言和后端运行时。前端简化 LM 程序的编写,运行时加速这些程序的执行。二者可以协同工作,也可以分别服务不同入口。 - 前端语言层:负责怎么表达 LLM 交互程序
- SRT runtime 层:负责怎么高效执行这些程序
SGLang 的源码结构体现了这种双层体系:
- lang:frontend language,负责表达 LLM 应用程序。
- srt:backend engine,负责运行本地模型;SRT 即 SGLang Runtime
本课的核心目标,是带你建立 SGLang 的架构全景图。学完本章,你将能够:
- 清晰描述 SGLang 的双层体系:Frontend Language Layer 与 Backend Runtime Layer 的职责边界和协作方式
- 解释 SRT 的核心组件:TokenizerManager、Scheduler、DetokenizerManager 各自的角色和通信拓扑
- 区分三类入口 API:OpenAI-compatible API、Native HTTP API 和 Offline Engine API 的适用场景
- 了解 SGLang 架构中的重要模块,并能够从源码目录结构中快速定位关键模块
三类入口 API 可以这样理解:
- OpenAI-compatible API:面向兼容 OpenAI 客户端的服务入口,例如 /v1/chat/completions、/v1/completions、/v1/embeddings。
- Native HTTP API:面向 SGLang 原生服务能力的 HTTP 入口,例如 /generate、/encode。
- Offline Engine API:面向 Python 程序内离线推理和批处理的入口,核心类是 Engine
1.2 为什么需要双层架构体系
为什么需要双层架构体系?,本节重点讨论为什么 SGLang 需要前端语言层。
SGLang 的整体结构可以理解为两层:前端语言层负责表达 LLM 应用程序,后端运行时负责执行模型推理。前端语言层让开发者用 Python 编写多轮生成、分支探索、变量管理、结构化输出等逻辑;后端运行时负责实际的模型服务、请求处理和推理优化。
1.2.1 背景:
SGLang 的诞生恰逢 LLM 应用场景的质变期。2023 年中后期,LLM 不再仅仅是聊天机器人或文本生成器,而是开始承担更复杂的角色:自主智能体(Autonomous Agents)、多步推理系统、工具使用者、多模态处理器。这一趋势带来了几个关键挑战,而这些挑战正是 SGLang 前端语言层要解决的问题:
- 从单轮到多轮的范式转移。早期的 LLM 应用大多是单轮问答:用户输入一个问题,模型生成一个回答。但在更复杂的应用中,模型需要进行多轮生成调用,每一轮的结果都会影响后续提示的构造。
- 从无状态到有状态的编程模型。传统 LLM API 通常是无状态的:每次请求都是独立的,服务器不维护完整应用状态。但对于多轮对话和智能体应用,状态管理变得至关重要。系统需要维护对话历史、工具调用记录、中间推理结果,并在后续步骤中继续使用这些状态。
- 从串行到并行的执行模式。Tree-of-Thought 等推理技术要求同时探索多个推理路径,这需要在同一个提示状态下复制上下文,并行执行多个生成分支,再汇总结果。
- 从自由生成到结构化输出的需求。越来越多的应用要求 LLM 生成符合特定格式的输出,例如固定选项、整数、字符串、正则表达式约束文本或 JSON schema 约束文本。
上述问题的核心痛点在于:LLM 调用不再是孤立的单轮请求,而是复杂的多步骤工作流。一个 Agent 可能需要先规划工具调用,再并行执行多个子任务,最后汇总结果。这些步骤之间存在复杂的控制流、状态依赖和数据依赖关系。
上述问题的核心痛点在于:LLM 调用不再是孤立的单轮请求,而是复杂的多步骤工作流。一个 Agent 可能需要先规划工具调用,再并行执行多个子任务,最后汇总结果。这些步骤之间存在复杂的控制流、状态依赖和数据依赖关系。
这种设计的价值在于把应用逻辑和推理执行分开:
- 前端语言层负责描述多步骤、多状态、多分支的 LLM 程序;
- 后端运行时负责高效完成模型调用。对于开发者来说,前端语言层提供的是更自然的编程模型;
- 对于系统来说,后端仍然可以集中处理推理服务、采样参数、结构化约束和运行时优化。
1.2.2 举例说明:
- 它把 prompt 从"字符串"提升成"程序"
在没有前端语言层时,传统方式通常把 prompt 当作字符串来拼接:
json
prompt = f"""
You are a helpful assistant.
User: {question}
Assistant:
"""
SGLang 前端语言层的思路是把提示构造、角色消息和生成点写成一个程序:
python
from sglang import function, system, user, assistant, gen
@function
def qa(s, question):
s += system("You are a helpful assistant.")
s += user(question)
s += assistant(gen("answer", max_tokens=128))
这段代码看起来像普通 Python,但 system(...)、user(...)、assistant(...)、gen(...) 会先构造表达式对象。表达式通过 s += ... 提交到程序状态后,再由执行器逐步处理;遇到 gen(...) 这样的生成点时,执行器会调用后端完成模型生成,并把生成结果追加到当前上下文,同时保存到对应变量名中。
因此,这个函数表达的是一个包含角色、上下文、生成点和变量保存规则的推理程序。我们可以运行实例代码仓库中的 course2/qa_sglang_function.py 进行实验:
shell
python qa_sglang_function.py "用一句话介绍 SGLang。"
示例输出:
shell
SGLang 是一个用于高效表达和执行大语言模型推理流程的框架,支持用前端语言组织提示、生成点和程序状态。
- 它把 LLM 交互变成"状态机"而不是"一次请求"
从第一性原理看,多轮 LLM 应用并不是简单地把一段 prompt 发给模型,然后拿回一个结果。
真实的交互过程更像是一个不断推进的程序状态:
- 先把 system prompt、用户输入或历史对话写入当前上下文;
- 到达某个位置时,通过 gen(...) 触发一次模型生成;
- 生成结果被追加到当前上下文,并保存到变量中;
- 程序随后可以读取这个变量,并根据它继续追加上下文、进入条件分支、执行工具调用,或者发起下一轮生成。
也就是说,SGLang 表达的是由上下文状态、生成点、变量结果和后续逻辑共同组成的一条执行轨迹。这样一来,多轮对话、工具调用、分支判断和连续生成,都可以写成普通程序里的状态推进过程,而不是拆成许多彼此割裂的 prompt 拼接和 API 调用。
python
from sglang import function, system, user, assistant, gen
@function
def multi_turn(s):
s += system("You are a helpful assistant.")
s += user("先给我 3 个国家和首都。")
s += assistant(gen("first_answer"))
s += user("再来 3 个。")
s += assistant(gen("second_answer"))
这可以理解为一个连续状态演化过程:
python
state0 -> 写 system
state1 -> 写 user
state2 -> 生成 first_answer
state3 -> 再写 user
state4 -> 生成 second_answer
前端语言层的作用之一,是把上下文管理从开发者手工字符串操作中解放出来,变成明确的按照我们设定的程序状态推进。我们现在运行代码仓库中的 course2/multi_turn_sglang_function.py:
python multi_turn_sglang_function.py
示例输出:
第一轮回答:
1. 中国 - 首都:北京
2. 美国 - 首都:华盛顿特区
3. 法国 - 首都:巴黎
第二轮回答:
4. 日本 - 首都:东京
5. 巴西 - 首都:巴西利亚
6. 澳大利亚 - 首都:堪培拉
- 它让控制流进入 prompt 生成
普通 prompt 模板主要负责文本填充,表达复杂控制流会比较困难,但是真实的Agent应用经常需要:
- if/else
- for 循环
- 多分支探索
- 条件生成
- 失败回退
- 工具选择
SGLang 的前端语言层嵌入在 Python 中,因此可以直接使用 Python 控制流:
python
from sglang import function, user, assistant, gen
@function
def tool_use(s, question):
s += user(question)
s += assistant(
"To answer this question, I need to use "
+ gen("tool", choices=["calculator", "search engine"])
)
if s["tool"] == "calculator":
s += assistant("Expression: " + gen("expression"))
else:
s += assistant("Keyword: " + gen("keyword"))
这里的 gen("tool", choices=...) 会生成一个工具选择,并把结果保存到 tool 变量中。随后,程序可以通过 s"tool" 读取这个变量,并进入不同的 Python 分支。
我们现在运行课程仓库中的 course2/test_sglang_tool_use.py:
shell
python test_sglang_tool_use.py
示例输出:
json
{
"question": "What is 12345 * 6789?",
"tool": "calculator",
"expression": "12345 * 6789",
"keyword": null
}
- 它把"生成点"显式化,便于 runtime 优化
SGLang 前端语言层的一个关键价值,是把"上下文"和"生成点"分开表达。
在普通 prompt 模板里,开发者通常会先拼出一整段字符串,再把它作为一次请求发给模型。这样写当然可以工作,但程序结构不够清晰:哪些内容是固定上下文,哪里需要模型生成,生成结果保存到哪里,生成时使用什么约束,都需要开发者在外部代码里自行维护。
SGLang 用 gen(...) 显式标记生成位置:
shell
s += assistant(gen("answer"))
这段代码表达的是:前面的内容构成当前上下文,执行到这里时触发一次模型生成;生成结果会被追加到当前上下文中,并保存到 answer 变量,供后续程序继续读取和使用。
因此,gen(...) 的目的是在程序中声明一次模型生成。它可以携带采样参数、停止条件、固定选项、正则约束、JSON schema 约束等信息。执行器遇到这个生成点时,会把当前上下文和这些参数交给后端完成生成,并把结果写回程序状态。所以说,在这里的前端语言层负责把推理程序的结构表达清楚;后端 runtime 则可以在这些结构之上处理请求调度、缓存复用、批处理、结构化生成和流式输出。
- 它把"约束"变成一等公民
真实生产系统里,模型输出经常需要满足明确格式,而不是完全自由生成。常见需求包括固定枚举值、JSON 格式、正则约束、结构化字段和工具参数格式。
SGLang 允许把约束直接挂在生成点上。例如,情感分类只允许输出三个固定选项
python
from sglang import function, user, assistant, gen
@function
def classify_sentiment(s, text):
s += user(f"Text: {text}\nSentiment:")
s += assistant(gen("label", choices=["positive", "negative", "neutral"]))
这里的 choices 会让 gen(...) 表达一次受限选择,并把结果保存到 label 变量中。
对于格式更明确的输出,也可以使用正则或 JSON schema:
python
s += assistant(gen("number", regex=r"[0-9]+"))
s += assistant(gen("result", json_schema='{"type": "object"}'))
这些约束成为生成点本身的一部分。程序在描述这里要生成什么的同时,也描述这里的输出应该满足什么格式。我们现在运行仓库中course2/classify_sentiment_choices.py,运行命令如下:
shell
python course2/classify_sentiment_choices.py
示例输出:
json
{
"text": "I love this product. It works perfectly.",
"label": "positive"
}
- 它让并行和批处理成为语言层能力
LLM 应用里经常会出现共享上下文后的分支任务。例如,对同一输入做多个假设扩展,或者从一个共同前缀出发生成多条候选路径。共享前缀的 prefill 通常是重要成本之一,因此语言层能够表达"这些分支来自同一个状态",会给 runtime 留出优化空间。
SGLang 可以用 s.fork(...) 从当前程序状态创建多个分支:
python
from sglang import function, assistant, gen
@function
def expand_two_points(s):
s += assistant("这里有两个健康建议:1. 均衡饮食 2. 规律运动\n")
forks = s.fork(2)
forks[0] += assistant("展开第一点:" + gen("tip1"))
forks[1] += assistant("展开第二点:" + gen("tip2"))
forks.join()
s += assistant("总结:" + gen("summary"))
这段程序会先写入一段共同上下文,然后从同一个程序状态创建两个分支;两个分支分别生成 tip1 和 tip2,再通过 forks.join() 把分支变量汇回主状态,最后由主状态继续生成 summary。这种写法把共享上下文、分支生成、结果汇总的结构显式表达出来,为 runtime 后续进行调度、批处理和缓存复用等优化提供了清晰的程序结构。这个例子在course2/expand_two_points_fork.py当中,可以用以下的命令运行:
shell
python course2/expand_two_points_fork.py
json
{
"tip1": "均衡饮食是指摄入各种营养素,如蛋白质、碳水化合物、脂肪、维生素和矿物质,保持营养均衡,避免偏食或过度摄入某些食物。",
"tip2": "规律运动可以增强心肺功能,提高免疫力,预防慢性疾病。",
"summary": "保持均衡饮食和规律运动是维持健康生活的基石。"
}
1.2.3 小结
SGLang 大致可以分成两层来看:前端语言层负责表达 LLM 交互程序,SRT(SGLang Runtime)层负责高效执行模型推理。
前端语言层以 @function 程序为核心。开发者用 @function 定义一段 LLM 程序,在程序内通过对状态对象 s 调用接口来描述程序行为:
- s += ... 向上下文写入文本;
- s += gen(...) 触发生成,并把结果存到指定变量;gen(...) 通过 regex、json_schema、choices 等参数施加约束;
- s += select(...) 在一组候选项中挑选;
- s.fork(...) 从同一状态派生多条并行执行分支。
这些构造让开发者可以用程序化的方式描述上下文、生成点、变量、约束和分支。
这两层是分工关系:
- 前端语言层负责把"怎么写 LLM 程序"表达清楚,例如哪里写入上下文、哪里触发生成、生成结果保存到哪个变量、哪些输出需要约束、哪些路径来自同一个分支状态;
- runtime 层负责执行这些程序所需的底层能力,例如 tokenization、batching、prefix cache、chunked prefill、speculative decoding、multimodal preprocessing、streaming 和并行执行等
因此,SGLang 的特色不只是提供 OpenAI API 兼容的 serving 能力,也不只是追求推理速度。它更核心的目标,是把"如何编写 LLM 程序"和"如何高效执行 LLM 程序"放进同一套系统里对齐:前端让复杂的 LLM 交互更容易表达,后端让这些交互能够以高性能方式执行。
1.3 为什么vllm没有前端语言层
vLLM 和 SGLang 的差异,更适合理解成系统边界的不同,而非技术优劣:
- vLLM 是高吞吐、内存高效的推理引擎,面向已构造好的 prompt、token 或 OpenAI 格式 messages,负责高效执行。
- SGLang 在 runtime 之外多提供一层前端语言,用 @function、system/user/assistant(...)、gen(...)、fork(...) 把多轮对话、生成点、变量、分支和约束写成一个程序。
从入口看,vLLM 通过 LLM.generate(prompts, SamplingParams) 做离线推理,或用 OpenAI 兼容 server 接收 messages。因此 prompt 构造、对话历史维护、工具调用编排通常在调用方或上层框架完成;vLLM 从准备好的输入开始,专注调度、batching、KV cache、模型执行和吞吐。
SGLang 把这部分应用逻辑纳入同一套编程模型:system/user/assistant(...) 构造角色表达式,经 s += ... 提交后由 StreamExecutor 执行,维护当前文本、角色消息(OpenAI 格式)和变量。
多轮对话因此可写成连续的状态推进:写入 system、user,遇到 gen(...) 触发生成并存入变量,再继续下一轮,而作为对比 vLLM 的 OpenAI API 通常由客户端每次传入完整对话历史
python
client.chat.completions.create(
model="llama-3.1-8b",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Hello!"},
{"role": "assistant", "content": "Hi there!"},
{"role": "user", "content": "What's the capital of France?"},
],
)
vLLM 的离线 Python 接口也是围绕 prompt 输入发起生成:
python
from vllm import LLM, SamplingParams
llm = LLM(model="meta-llama/Llama-3.1-8B-Instruct")
sampling_params = SamplingParams(temperature=0.7)
outputs = llm.generate(
prompts=["System: You are helpful.\nUser: Hello!\nAssistant:"],
sampling_params=sampling_params,
)
这种设计把应用编排留在 vLLM 外部:开发者或上层框架负责维护对话历史、构造 prompt、组织工具调用;vLLM 负责把输入高效送入模型并返回结果。SGLang 则把"如何表达 LLM 程序"和"如何执行 LLM 推理"放在同一套系统中协同设计。
1.4 sglang双层体系架构图
SGLang 可以从两层来理解:上层是 Frontend Language Layer,下层是 Backend Runtime Layer,也就是 SRT。前端语言层面向 AI 应用开发者,负责表达"LLM 交互程序怎么写":
- 开发者用 @sgl.function 定义一个程序,用 system(...)、user(...)、assistant(...) 组织角色消息,用 gen(...) 标记生成点,用 choices、regex、json_schema 描述输出约束,用 s.fork(...) 表达分支执行。
- SRT 面向系统执行,负责把请求映射到模型推理流程中,处理 tokenization、多模态预处理、请求调度、batching、KV cache、模型执行、结构化生成和流式输出。
- 所以说:前端让复杂 LLM 工作流更容易表达,后端让这些工作流能够高效执行。
从整体链路看,SGLang 既可以通过前端语言层进入,也可以通过 HTTP API、OpenAI 兼容 API 或 Engine 等入口直接进入后端 runtime。
使用前端语言层时,@sgl.function 会把普通 Python 函数(首个参数为 s)包装成可运行的 SGL 程序;程序运行时会创建 ProgramState,函数体中的表达式通过 s += ... 提交给 StreamExecutor 执行。- 执行器会维护当前文本、角色消息、变量结果和生成状态;遇到 gen(...) 时,它会把当前上下文和采样/约束参数交给后端完成生成,并把结果写回程序状态。需要分支时,可以用 forks = s.fork(n) 创建多个子状态,再通过 forks.join() 把分支结果汇回主状态。

SRT 是 SGLang 的高性能推理执行层,核心组件包括 TokenizerManager、Scheduler 和 DetokenizerManager:
- TokenizerManager 负责接收请求、初始化 tokenizer 和多模态处理器,并管理请求进入系统的前置处理;
- Scheduler 负责请求队列、调度策略、模型前向执行和 KV cache 等运行时状态;
- DetokenizerManager 负责把输出 token IDs 转回可读文本,并支持增量解码和流式输出
实际部署中,这些组件可以按配置扩展为多个 worker 或多个并行 rank,并通过 ZMQ/IPC 等通道协作。这样,SGLang 把"如何编写 LLM 程序"和"如何高效执行 LLM 程序"放在同一套系统里对齐。
这种双层体系的价值在于解耦。前端程序通过 backend 抽象与具体执行环境连接,同一份 @sgl.function 程序可以在不同后端上运行:开发阶段接入 OpenAI 后端快速验证逻辑,生产阶段切换到本地 SRT 做高性能部署。
python
import sglang as sgl
@sgl.function
def analyze_topic(s, topic):
s += sgl.user(f"Analyze {topic} in depth")
s += sgl.assistant(sgl.gen("analysis", max_tokens=500))
# 开发阶段:用OpenAI快速验证
sgl.set_default_backend(sgl.OpenAI("gpt-4"))
state = analyze_topic.run(topic="quantum computing")
# 生产阶段:切换到本地Runtime高性能部署
sgl.set_default_backend(sgl.Runtime(
model_path="meta-llama/Llama-3.1-70B-Instruct",
tp_size=4
))
state = analyze_topic.run(topic="quantum computing")
二、双层体系架构背景
https://docs.sglang.io/references/frontend/frontend_tutorial.html
2.1 sglang前端语言层架构图
DSL(Domain-Specific Language,领域特定语言)是为特定问题域量身定制的编程语言。它只提供解决特定领域问题所需的语法和语义,因此表达方式更简洁、更贴近问题本身。
SGLang 前端语言层可以看作嵌入 Python 的 LLM 编程 DSL。开发者写出的代码仍然是 Python 函数,但函数内部可以使用 SGLang 提供的角色、生成、约束和分支接口来描述 LLM 交互程序:
SGLang 前端语言层也是这样的一种DSL:
python
import sglang as sgl
@sgl.function
def qa_program(s, question):
s += sgl.system("You are a helpful assistant.")
s += sgl.user(question)
s += sgl.assistant(sgl.gen("answer", max_tokens=256))
这段代码保留了 Python 函数的写法,执行时会进入 SGLang 的程序状态模型。@sgl.function 会把普通 Python 函数包装成 SglFunction,并记录函数参数信息。
真正运行时,SGLang 会创建 ProgramState,用来维护当前上下文、变量和消息状态。函数体中的 sgl.system(...)、sgl.user(...)、sgl.assistant(...)、sgl.gen(...) 等调用会构造表达式对象,这些表达式通过 s += ... 提交给 StreamExecutor,再由执行器逐步处理。
SGLang 前端语言层可以拆成 5 个连续层次:
- Public API Layer:开发者直接使用的接口,例如 @sgl.function、sgl.gen(...)、sgl.system(...)、sgl.user(...)、sgl.assistant(...)。
- Expression Objects:API 调用构造出的表达式对象,例如 SglExprList、SglGen、SglSelect、SglRoleBegin、SglRoleEnd。
- ProgramState / StreamExecutor:程序运行时维护当前文本、角色消息、变量、图片输入、分支状态,并逐步执行表达式。
- Backend Adapter:执行器遇到生成点时,把当前上下文和采样参数交给后端,例如 OpenAI 后端或 RuntimeEndpoint。
- Runtime / SRT:当后端是本地 Runtime 时,请求会进入 SRT,由后端运行时完成 tokenization、调度、模型执行、结构化生成和流式输出。
整体流程是:开发者先写出 Python 风格的 LLM 程序;程序运行时被展开为表达式对象,表达式对象在 ProgramState 中推动上下文、变量和生成状态变化,生成点最终被后端适配器转换成具体模型请求。

SGLang 前端 DSL 的设计可以概括为三个目标:函数化抽象、状态驱动、约束内建。
- 函数化抽象:@sgl.function 将 LLM 程序封装为可调用的 Python 函数,使复杂工作流可以复用和组合。
- 状态驱动:ProgramState 集中管理当前文本、角色消息、变量结果和分支状态,减少手工拼接字符串和维护全局状态的负担。
- 约束内建:通过 choices、regex、json_schema 等参数,把输出格式要求直接放到生成点上,减少对生成后校验与重试的依赖。
2.1.1 Public API Layer:DSL核心原语层:
- python/sglang/init.py
- python/sglang/lang/api.py
从 init.py 的导出列表可以清晰看到,前端语言 API 被分为几类:
- 核心原语:function、gen、gen_int、gen_string、select、image、video,用于定义 LLM 程序中的主要动作,例如封装函数、触发生成、限制输出类型、选择候选项,以及插入图片或视频输入。
- 角色结构:system、user、assistant 以及对应的 _begin / _end,用于组织多轮对话中的消息角色,并配合 chat template 生成模型所需的对话格式。
- 后端连接:Runtime、Engine、RuntimeEndpoint、set_default_backend,用于指定程序由哪个后端执行,例如本地 SRT Runtime、Engine 或其他兼容后端。
- 辅助能力:separate_reasoning、flush_cache、get_server_info,用于增强和管理运行过程,例如分离 reasoning 内容、清理缓存、查询服务信息等
回到前面的例子:
python
import sglang as sgl
@sgl.function
def chat(s):
s += sgl.system("you are helpful")
s += sgl.user("hello")
s += sgl.assistant(sgl.gen("answer"))
这里的 system(...)、user(...)、assistant(...)、gen(...) 是构造带语义的表达式对象的接口,例如角色表达式、生成表达式或表达式列表。这些表达式通过 s += ... 提交给 ProgramState 后,再由 StreamExecutor 逐步执行:执行器遇到普通文本时写入上下文,遇到角色表达式时按 chat template 更新消息结构,遇到 gen(...) 时调用后端生成并把结果保存到对应变量中。

因此,Public API Layer 解决的是"用户如何表达任务"的问题:哪些内容是角色消息,哪些位置是生成点,哪些位置是选择点,哪些地方挂载多模态输入,哪些生成点带有结构化约束。它的本质作用是把 prompt 从裸字符串提升为可执行的前端程序,并把生成、选择、多模态、角色等常见推理意图显式化。
生成原语:sgl.gen()
sgl.gen() 是前端语言层中最核心的生成原语。它用于声明一个生成点,并指定生成结果保存到哪个变量名中。sgl.gen() 本身不会立即请求模型;当这个表达式被提交给执行器后,StreamExecutor 会在执行到该生成点时调用后端完成生成。
基础生成示例:
sgl.gen("answer", max_tokens=256)
sgl.gen() 支持常见采样参数,例如 temperature、top_p、top_k、max_tokens、stop、stop_regex 等;也支持结构化约束参数,例如 choices、regex、json_schema。
固定选项约束:
sgl.gen("sentiment", choices=["positive", "negative", "neutral"])
正则约束:
sgl.gen(
"ip_address",
regex=r"((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)",
)
JSON Schema 约束:
python
import json
from pydantic import BaseModel
class Person(BaseModel):
name: str
age: int
sgl.gen("person", json_schema=json.dumps(Person.model_json_schema()))
regex 和 json_schema 约束会被传递到后端的 grammar / constrained decoding 机制中处理。SGLang 支持多种 grammar backend,例如 xgrammar(默认)、outlines、llguidance,各后端以各自方式完成约束编译和 token 级限制。对开发者来说,关键是把格式要求挂在生成点上,在生成过程中就约束输出格式。
角色管理原语
SGLang 提供了 sgl.system()、sgl.user()、sgl.assistant() 等角色管理原语,用于描述多轮对话中的角色结构。它们会构造角色相关的表达式;执行时,StreamExecutor 会结合后端的 chat_template 插入对应前后缀,并维护 OpenAI chat 格式的 messages_ 状态。
python
import sglang as sgl
@sgl.function
def structured_chat(s, user_input):
s += sgl.system("You are a code review assistant. Be concise.")
s += sgl.user(user_input)
s += sgl.assistant(sgl.gen("review", max_tokens=200))
s += sgl.user("Any security concerns?")
s += sgl.assistant(sgl.gen("security", max_tokens=150))

控制原语:fork与join
sgl.fork() 是 SGLang 的核心控制原语之一,它创建多个并行执行分支,每个分支拥有独立的程序状态副本;join() 则将分支结果聚合回主状态。
python
import sglang as sgl
@sgl.function
def parallel_analysis(s, topic):
s += sgl.system("You are a technology analyst with expertise in multiple domains.")
s += sgl.user(f"Provide a comprehensive analysis of '{topic}'.")
forks = s.fork(3)
forks[0] += sgl.user("From a technical architecture perspective:")
forks[0] += sgl.assistant(sgl.gen("technical", max_tokens=200))
forks[1] += sgl.user("From a business and market perspective:")
forks[1] += sgl.assistant(sgl.gen("business", max_tokens=200))
forks[2] += sgl.user("From a user experience perspective:")
forks[2] += sgl.assistant(sgl.gen("ux", max_tokens=200))
forks.join()
s += sgl.user(
"Based on the three perspectives above, provide a balanced summary."
)
s += sgl.assistant(sgl.gen("summary", max_tokens=250))
fork 的底层实现是状态拷贝:它会创建新的执行器,并复制当前的变量、文本、消息、角色和图片状态,使每个分支拥有独立的程序状态副本,可以独立推进并执行各自的 LLM 调用,最后在 join() 点将结果合并回主状态。
sgl.function 还支持 API 投机执行(API Speculative Execution)。当使用 OpenAI 云 API 后端时,可以将多个 sgl.gen() 调用合并为单次 API 请求,以减少多次 API 调用带来的开销。使用方式是在装饰器中设置 num_api_spec_tokens:
python
@sgl.function(num_api_spec_tokens=128)
def program(s):
...
2.1.2 DSL / IR:把写法变成结构
参考源码:python/sglang/lang/ir.py。
前端语言层的 Public API 调用返回的是 Python 对象。SGLang 会将这些对象进一步组织为显式的中间表示(IR),再交由解释器执行。引入 IR 作为前端写法与执行逻辑之间的承接层,主要有三点作用:
- 分离语义与控制流:Python 代码中的 f-string、条件分支、循环等语法,容易将 prompt 语义与程序控制流交织在一起。IR 将可执行的 prompt 语义显式化,使解释器能够独立处理这些结构。
- 提供可检查性:IR 具备明确的节点结构,可以被遍历、分析和优化,例如用于前缀提取与缓存。
- 保持后端无关:IR 描述的是程序意图,即"要完成什么";具体"如何发送请求、调用哪个后端"则由解释器和 Backend 层负责。

因此,SGLang 在 API 层和 Interpreter 层之间,插入了一层显式的中间表示,把"人写的 Python 风格代码"变成"系统可分析的程序结构"。从 ir.py 可以提炼出完整的 IR 类层次,其中每一个 IR 节点对应前端语言的一个语义动作。

关键设计:SglExpr.add 与表达式链
python
class SglExpr:
node_ct = 0
def __init__(self):
self.node_id = SglExpr.node_ct
self.prev_node = None
self.pid = None
SglExpr.node_ct += 1
def __add__(self, other):
if isinstance(other, str):
other = SglConstantText(other)
assert isinstance(other, SglExpr)
return self.concatenate_ir(self, other)
def concatenate_ir(self, a, b):
if isinstance(a, SglExprList):
if isinstance(b, SglExprList):
return SglExprList(a.expr_list + b.expr_list)
else:
return SglExprList(a.expr_list + [b])
elif isinstance(b, SglExprList):
return SglExprList([a] + b.expr_list)
return SglExprList([a, b])
这是整个 IR 层最核心的代码。SglExpr.add 被重载(配合 radd 处理左操作数是字符串的情况),使得:
python
s += system("you are helpful")
s += user(question)
s += assistant(gen("answer"))
这些 += 操作并不是在拼接字符串,而是在向程序状态提交 DSL 表达式,由解释器进一步展开为可执行的 IR 结构。具体来说:
- system("you are helpful") 会构造一个 SglExprList,其中包含 SglRoleBegin("system")、文本内容以及 SglRoleEnd("system") 等语义节点;
s += ... 中的 s 是当前程序的 ProgramState,所以这个操作会调用 ProgramState.iadd(),把右侧 DSL 表达式提交给 StreamExecutor,再由解释器按节点类型依次执行;- 多个表达式可以通过 + 组合成更大的 SglExprList,从而保留 prompt 片段、role 边界、生成请求等节点的顺序关系。
在追踪过程中,IR 节点还会记录 node_id 和 prev_node 等信息。node_id 用于标识节点,prev_node 用于表示执行链上的前驱依赖。借助这些信息,SGLang 可以通过 print_graph_dfs() 以 DFS 方式遍历并打印程序结构。

SglFunction:程序的包装器
python
# ir.py
class SglFunction:
def __init__(self, func, num_api_spec_tokens=None, bind_arguments=None):
self.func = func
self.num_api_spec_tokens = num_api_spec_tokens
self.bind_arguments = bind_arguments or {}
# Parse arguments
argspec = inspect.getfullargspec(func)
assert argspec.args[0] == "s", 'The first argument must be "s"'
self.arg_names = argspec.args[1:]
self.arg_defaults = argspec.defaults if argspec.defaults is not None else []
SglFunction 是用户函数的包装器。关键设计:
- 强制 s 作为第一个参数:所有 SGLang 函数都以 s(state)作为第一个参数,这是前端语言与执行状态之间的约定接口。
- 惰性求值:init 只保存函数对象并解析函数签名,不执行函数体。
- 参数绑定:bind() 会创建一个新的 SglFunction,把绑定参数保存到 bind_arguments;运行时再把这些参数合并到函数调用参数中。
- 执行入口:run() 构造默认采样参数、选择 backend,并把执行交给 run_program()
python
def run(self, *args, ..., stream=False, backend=None, ...):
from sglang.lang.interpreter import run_program
default_sampling_para = SglSamplingParams(...)
backend = backend or global_config.default_backend
return run_program(
self,
backend,
args,
kwargs,
default_sampling_para,
stream,
...
)
SglFunction 可以理解成:把一个普通 Python 函数包装成一个 SGLang 可执行程序对象。比如我们写的是普通函数:
python
import sglang as sgl
@sgl.function
def answer_question(s, question):
s += "Question: " + question + "\n"
s += "Answer: " + sgl.gen("answer", max_tokens=64)
装饰器 @sgl.function 会调用 SGLang 的 function() 装饰器,并返回一个 SglFunction 对象。可以把它理解成下面这个包装过程:
python
answer_question = SglFunction(answer_question)
包装之后,answer_question 不再只是普通 Python 函数,而是一个 SglFunction 对象。它保存了原函数、参数名、默认值、绑定参数等信息。但是这一步只是定义,在定义时函数体不会执行。
这里的 s 是 SGLang 在运行时创建并传入的程序状态对象,也就是 ProgramState。ProgramState 内部持有一个 StreamExecutor;真正累积 prompt 文本、chat messages、变量、生成结果和后端执行状态的对象是 StreamExecutor。
所以,用户函数里并不是在操作普通字符串,而是在通过 s 向底层的 StreamExecutor 提交文本、生成节点、角色边界等 IR 操作。我们可以把 s 理解为"当前这次 SGLang 程序执行的上下文入口":用户通过它把前端写法连接到实际的模型调用流程。
python
@sgl.function
def answer_question(s, question):
...
这一步只是在创建包装器。真正执行是在:
python
state = answer_question.run(question="What is SGLang?", backend=backend)
执行时流程大致是:

所以 s 不是用户自己传的普通参数,而是 SGLang 执行系统创建的程序状态对象。你在函数里写:
python
s += "Question: " + question
s += sgl.gen("answer")
本质上是在通过 ProgramState 把文本和生成节点提交给 StreamExecutor,再由执行器驱动后端模型完成生成。因此,SglFunction 是 SGLang 的程序对象,它把我们写的 Python 函数变成一个可以 run()、run_batch()、trace()、bind() 的 SGLang 程序入口。
SglSamplingParams:采样参数的显式化
python
# ir.py
@dataclasses.dataclass
class SglSamplingParams:
max_new_tokens: int = 128
temperature: float = 1.0
top_p: float = 1.0
top_k: int = -1
regex: Optional[str] = None
json_schema: Optional[str] = None
# ...
def to_srt_kwargs(self):
return {"max_new_tokens": ..., "temperature": ..., "regex": ..., ...}
def to_openai_kwargs(self):
return {"max_tokens": ..., "temperature": ..., ...}
def to_anthropic_kwargs(self):
return {"max_tokens": ..., "temperature": ..., ...}
SglSamplingParams 是 SGLang 前端用来集中表示采样配置的数据类。它把 max_new_tokens、temperature、top_p、top_k、stop、regex、json_schema 等生成参数统一收敛成明确字段,方便在 run()、sgl.gen()、解释器和 backend 之间传递。
它的核心作用有三点:
- 统一采样参数:把生成配置从零散字典整理成类型化字段。
- 支持默认值和局部覆盖:run() 会构造默认的 SglSamplingParams;sgl.gen() 也会为单个生成节点构造自己的 SglSamplingParams。执行生成节点时,解释器以默认参数为基础,再用 sgl.gen() 中显式传入的参数覆盖。
- 适配不同后端:
to_srt_kwargs()、to_openai_kwargs()、to_anthropic_kwargs() 会把同一组参数转换成不同 backend 需要的格式。Runtime backend 可以接收 regex、json_schema 等约束生成参数;OpenAI、Anthropic 等 backend 会按自身接口能力选择可传递的字段。
用户通常不需要手动创建 SglSamplingParams,调用 run() 或 sgl.gen() 时直接传普通关键字参数即可。下面是一个具体例子:
python
state = answer_question.run(
question="What is SGLang?",
max_new_tokens=128,
temperature=0.7,
top_p=0.9,
backend=backend,
)
run() 内部会把这些参数整理成默认采样配置:
python
default_sampling_para = SglSamplingParams(
max_new_tokens=max_new_tokens,
temperature=temperature,
top_p=top_p,
...
)
执行 sgl.gen("answer", ...) 时,解释器会以 run() 的默认参数为基础,再用 sgl.gen() 里的显式参数覆盖:
python
s += sgl.gen("answer", max_tokens=64, temperature=0.2)
执行这个生成节点时,解释器会以 run() 的默认参数为基础,再用 sgl.gen() 里显式传入的参数覆盖:
python
run() 默认参数:
max_new_tokens = 128
temperature = 0.7
top_p = 0.9
sgl.gen() 局部参数:
max_tokens = 64
temperature = 0.2
最终用于 answer 的参数:
max_new_tokens = 64
temperature = 0.2
top_p = 0.9
执行时,采样参数的流向可以理解为:

2.1.3 Interpreter / Executor:前端语言真正的执行内核
sglang/lang/interpreter.py
这一层负责把前端提交的 IR 节点逐步解释执行,并在执行过程中维护程序状态。
SGLang 程序不是简单地把一段 prompt 一次性拼好后再结束。它支持多轮 role、流式输出、fork() 分支、变量引用、选择与受限生成、多模态输入以及惰性提交等能力。这些能力都依赖动态状态:当前 role、已累积文本、变量是否完成、是否处于流式输出、是否存在 fork 分支,以及图像/视频等多模态内容是否已经写入消息结构。
因此,前端语言层需要一个 Interpreter / Executor 来持续解释 IR 节点、更新状态,并在合适时机调用 backend。它不是简单的 API 调用链,而是一个带状态的执行系统。举个例子:
shell
s += assistant(gen("answer"))
s += select("lang", ["zh", "en"])
真正执行时,gen("answer") 的结果要写入 variables"answer",生成文本要追加到 text_;如果处于流式模式,增量文本还要能被上层消费。随后执行 select() 时,当前上下文已经包含前面的生成结果。如果后面出现 s"answer",它还需要等待 answer 生成完成。
python
@sgl.function
def demo(s):
s += "请先给出一句回答:"
s += sgl.gen("answer", max_tokens=32)
s += "\n判断回答语言:"
s += sgl.select("lang", ["zh", "en"])
s += "\n刚才的回答是:"
s += s["answer"]
执行时,gen("answer") 会把生成结果写入 variables"answer",并追加到 text_。随后 select("lang", "zh", "en") 会基于当前上下文做选择。最后的 s"answer" 会读取前面生成的变量;如果变量还没完成,就会等待。

这里可以看出 ProgramState 和 StreamExecutor 的分工:用户函数里看到的 s 是 ProgramState,提供 s += ...、s"answer"、s.text() 等接口;真正维护执行状态并处理 IR 节点的是内部的 StreamExecutor。
StreamExecutor 内部维护完整的执行状态,包括 text_、messages_、variables、variable_event、cur_role、images_ 等,用来记录累积文本、对话历史、变量结果、同步事件、当前角色和多模态输入。
- StreamExecutor 状态机
python
# interpreter.py
class StreamExecutor:
def __init__(
self,
backend,
arguments,
default_sampling_para,
chat_template,
stream,
num_api_spec_tokens=None,
use_thread=True,
):
self.sid = uuid.uuid4().hex
self.backend = backend
self.arguments = arguments
self.default_sampling_para = default_sampling_para
self.stream = stream
self.variables = {}
self.variable_event = {}
self.meta_info = {}
self.is_finished = False
self.error_ = None
self.text_ = ""
self.messages_ = []
self.chat_template = chat_template or self.backend.get_chat_template()
self.cur_role = None
self.cur_role_begin_pos = None
self.images_ = []
self.cur_images = []
self.fork_start_text_pos = None
self.num_api_spec_tokens = num_api_spec_tokens
self.speculated_text = ""
self.use_thread = use_thread
if self.use_thread:
self.queue = queue.Queue()
self.worker = threading.Thread(target=...)
self.worker.start()
if stream:
self.stream_text_event = threading.Event()
self.stream_var_event = {}
else:
self.stream_text_event = None
self.stream_var_event = None
StreamExecutor 是前端语言层的核心执行器。它集中维护文本、消息、变量、role、多模态内容、fork 分支、流式事件和 backend 交互状态。用户函数中的 s 是 ProgramState,而 ProgramState 内部持有的正是这个 StreamExecutor。
- 执行入口:run_program
python
# interpreter.py
def run_program(
program,
backend,
func_args,
func_kwargs,
default_sampling_para,
stream,
sync=False,
use_thread=True,
):
if hasattr(backend, "endpoint"):
backend = backend.endpoint
assert backend is not None, "Please specify a backend"
func_kwargs.update(program.bind_arguments)
stream_executor = StreamExecutor(
backend,
func_kwargs,
default_sampling_para,
chat_template=None,
stream=stream,
num_api_spec_tokens=program.num_api_spec_tokens,
use_thread=use_thread,
)
state = ProgramState(stream_executor)
if stream:
t = threading.Thread(
target=run_internal,
args=(state, program, func_args, func_kwargs, sync),
)
t.start()
return state
else:
run_internal(state, program, func_args, func_kwargs, sync)
return state
run_program() 负责建立执行环境:它创建 StreamExecutor,再包装成 ProgramState,作为参数 s 传入用户函数。同时,它会合并 bind() 保存的参数,并根据 stream 选择执行方式:流式模式下启动后台线程并立即返回 state;同步模式下执行完成后再返回。
当用户函数开始执行后,s 就成为前端代码和执行器之间的入口。用户写入 s 的每一个表达式,都会先经过 ProgramState,再被转交给内部的 StreamExecutor。例如:
s += sgl.gen("answer")
这里的 s 是 ProgramState,+= 会触发它的 iadd() 方法:
python
def __iadd__(self, other):
if other is None:
raise ValueError("Tried to append None to state.")
self.stream_executor.submit(other)
return self
也就是说,前端表达式会被提交给内部的 stream_executor.submit()。submit() 会先为变量类节点初始化事件,然后根据 use_thread 决定执行方式:如果使用后台线程,就把表达式放入队列,后续有消费者来执行;如果不使用线程,就直接调用 _execute()。
python
def submit(self, expr):
self._init_var_event(expr)
if self.use_thread:
self.queue.put(expr)
else:
self._execute(expr)
因此,从用户写法到解释器分派的路径是:

- 核心分派逻辑:_execute
python
# interpreter.py
def _execute(self, other):
if isinstance(other, str):
other = SglConstantText(other)
assert isinstance(other, SglExpr)
if isinstance(other, SglConstantText):
self._execute_fill(other.value)
elif isinstance(other, SglGen):
self._execute_gen(other)
elif isinstance(other, SglSelect):
self._execute_select(other)
elif isinstance(other, SglExprList):
for x in other.expr_list:
self._execute(x)
elif isinstance(other, SglRoleBegin):
self._execute_role_begin(other)
elif isinstance(other, SglRoleEnd):
self._execute_role_end(other)
elif isinstance(other, SglImage):
self._execute_image(other)
elif isinstance(other, SglVideo):
self._execute_video(other)
elif isinstance(other, SglVariable):
self._execute_variable(other)
elif isinstance(other, SglVarScopeBegin):
self._execute_var_scope_begin(other)
elif isinstance(other, SglVarScopeEnd):
self._execute_var_scope_end(other)
elif isinstance(other, SglCommitLazy):
self._execute_commit_lazy_operations(other)
elif isinstance(other, SglConcateAndAppend):
# ...
elif isinstance(other, SglSeparateReasoning):
self._execute_separate_reasoning(other)
else:
raise ValueError(f"Unknown type: {type(other)}")
_execute() 是 Interpreter 的中央分派器。它根据 IR 节点类型选择对应的 _execute_xxx() 方法:

这些 _execute_xxx() 方法就是每种 IR 节点的具体语义实现:先读当前状态,再执行动作,最后更新 StreamExecutor 中的文本、消息、变量或事件。它们也划清了 Interpreter 和 Backend 的边界:Interpreter 负责解释前端语言语义、维护执行状态;Backend 负责对接真实推理系统,完成生成、选择和提交等操作。
2.1.4 Backend Abstraction------语言语义与推理后端解耦
sglang/lang/backend/base_backend.py
在前面的执行链路中,StreamExecutor._execute() 会把不同 IR 节点分派给对应的 _execute_xxx() 方法。当遇到需要模型能力的节点时,Interpreter 不会自己完成推理,而是调用 backend。例如:
- _execute_gen() 会调用 backend.generate();
- _execute_select() 会调用 backend.select(),流式生成时会调用 backend.generate_stream()
因此,BaseBackend 的价值,是把前端语言语义和具体推理服务实现分开。前端语言层保持稳定,后端实现可以替换,Interpreter 只面向统一 backend 协议编程。
python
@sgl.function
def classify_and_answer(s, question):
s += "判断问题语言:"
s += sgl.select("lang", ["zh", "en"])
s += "\n回答问题:"
s += sgl.gen("answer", max_tokens=64)
当执行到:
s += sgl.gen("answer", max_tokens=64)
时 s += sgl.gen("answer", max_tokens=64) 时,sgl.gen() 会创建 SglGen 节点,并通过 s += ... 提交给 StreamExecutor。
_execute() 识别到 SglGen 后分派给 _execute_gen(),再由 _execute_gen() 调用 backend.generate() 完成模型生成。结果会写入 variables["answer"],并追加到 text_。

前端语言只关心"生成、流式生成、候选选择、多模态输入、分支合并"等语义动作,不关心具体服务是本地 engine、远程 server、HTTP 接口还是 SDK 调用。因此,Interpreter 不直接依赖某个 serving 实现,而是依赖 BaseBackend 这层抽象协议,由它规定 backend 需要提供的能力。
python
class BaseBackend:
def generate(self, s, sampling_params):
raise NotImplementedError()
def generate_stream(self, s, sampling_params):
raise NotImplementedError()
def select(self, s, choices, temperature, choices_method=None):
raise NotImplementedError()
def concatenate_and_append(self, src_rids, dst_rid):
raise NotImplementedError()
def flush_cache(self):
pass
def get_server_info(self):
pass
SGLang 提供了多种 backend 实现,例如 RuntimeEndpoint、OpenAI、Anthropic、LiteLLM、VertexAI 等。多后端支持不是简单地适配不同请求格式,而是把统一的语言语义映射到不同后端能力上:
- Runtime backend 会把 StreamExecutor.text_ 和 SglSamplingParams 转成 /generate 请求;
- OpenAI backend 会根据 chat/completion 模式选择 messages_ 或 text_,并转换采样参数
sgl.set_default_backend() 是前端连接 backend 的入口:
python
def set_default_backend(backend):
global_config.default_backend = backend
当 run() 没有显式传入 backend 时,就会使用这个默认 backend。也可以在单次调用中直接传入 backend:
python
state = universal_qa.run(
question="What is the capital of France?",
backend=backend,
)
这可以理解为一种 Strategy 风格的设计:前端 DSL 定义程序逻辑,backend 提供执行策略。这样,同一份程序可以在不同运行环境中执行,backend 可以替换,应用中的不同模块也可以选择不同 backend。实际切换 backend 时,需要确认目标 backend 支持程序中用到的语义能力,例如 chat、select、受限生成、多模态输入或分支合并。
python
import sglang as sgl
@sgl.function
def universal_qa(s, question):
s += sgl.user(question)
s += sgl.assistant(sgl.gen("answer", max_tokens=100))
backends = [
sgl.OpenAI("gpt-3.5-turbo"),
sgl.RuntimeEndpoint("http://localhost:30000"),
]
for backend in backends:
state = universal_qa.run(
question="What is the capital of France?",
backend=backend,
)
print(state["answer"])
这段代码展示了 backend 抽象的核心思想:前端函数描述"要做什么",backend 决定"在哪里、用什么方式执行"。