第 3 篇里用 llama-server 起过 OpenAI 兼容的本地 API,单机自用足够。可一旦把它接进 Agent 循环,画面就变了:每次工具返回、每轮用户追问,客户端都把 system prompt、工具定义、全部历史原样重发,服务端从头把 prefill 再算一遍——同样的两三千 token,一天被重复计算几百次。Agent 的响应延迟和电费,大头就烧在这。
本篇是《本地大模型部署实战》第 7 篇,主角 SGLang(当前版本 0.5.22,2026 年 10 月 16 日发布)和它的核心机制 RadixAttention:把 KV Cache 组织成一棵基数树,让所有请求自动共享一切可共享的前缀。它正是为 Agent 这类负载而生。本文按"负载特征 → 原理 → 收益账 → 上手 → 结构化输出 → 对比与选型 → 接入 LangGraph"组织全文。
1、Agent 负载的特征
先看清对手长什么样。一个跑在 LangGraph 里的 Agent,每次调用 LLM,prompt 由四段拼成:
- system prompt:角色与规则,几百 token
- 工具定义:若干函数的 JSON Schema,一两千 token
- 对话历史:此前所有轮次的消息与工具结果
- 当前输入:新的用户问题或工具返回
前三段在多轮之间逐字不变,第四段只增不改。换句话说,Agent 的 prompt 是一个不断变长的共享前缀,每轮真正新增的内容常常不到十分之一。
这类负载还有两个放大器。一是并发会话:多个 Agent 实例用同一套 system prompt 与工具集,前缀完全相同。二是中途恢复:LangGraph 的 checkpointer 把会话状态持久化,线程恢复后重放的 prompt 与中断前一致。三个特征叠加,服务端看到的就是"少数几个长前缀 + 高频增量请求"。
而 prefill 恰好是最贵的一段:它对整个前缀逐层算注意力,属于计算密集型,直接决定首 token 延迟(TTFT,Time To First Token)。不做缓存,第 20 轮就要为 10000 个 token 里 96% 的"旧账"重新买单。
vLLM 不是没有对策,它的 Automatic Prefix Caching(APC)久已有之。但它的组织粒度对 Agent 负载不算最优,差异第 2 节拆开讲。
注意:前缀复用要求 token 序列逐字一致。system prompt 里嵌时间戳、随机请求 ID 这类每轮都变的字段,会把共享前缀拦腰打断。这是实践中命中率归零最常见的原因。
2、RadixAttention 原理
2.1、基数树的组织方式
RadixAttention 的思路一句话讲完:KV Cache 不按请求管理,按前缀共享。数据结构是基数树(radix tree),一种压缩前缀树,每个节点存一段 token 序列以及它们对应的 KV Cache。结构如下:
新请求到达时走三步:沿树从根开始匹配,找到最长可复用前缀;命中的部分直接取用 KV Cache,跳过 prefill;不匹配的尾部作为新节点插入树中。会话 A 的第 N+1 轮会在"会话 A 分支"末端命中,只算新增的几百 token;多会话则共享 system 与工具定义那段主干。
显存吃紧时,树按 LRU(Least Recently Used,最近最少使用) 从叶子向上淘汰冷分支,热会话的缓存自然留存。这一切对调用方完全透明:不用声明前缀、不用传缓存键,SGLang 调度器默认就按最长前缀优先排请求。机制细节出自 LMSYS 的 RadixAttention 博文与 SGLang 论文。
2.2、与 vLLM 缓存的差异
vLLM 的底座是 PagedAttention:KV Cache 切成固定大小的 block(默认 16 token),APC 在其上对每个 block 算内容哈希、串成哈希链,请求之间靠"哈希链相同"复用。两边对比如下:
| 维度 | RadixAttention | vLLM APC |
|---|---|---|
| 组织结构 | 基数树,节点存 token 段 | block 哈希链 |
| 匹配粒度 | token 级,任意长度 | block 级,16 token 对齐 |
| 结构开销 | 一棵树统一管理 | 哈希表加引用计数 |
| 淘汰方式 | LRU 按分支回收 | 引用计数归零后回收 |
差异落到实践是两点。其一,粒度:基数树 token 级命中;block 方案要对齐到 16 的倍数,尾部零头重算——对每轮增量只有几百 token 的 Agent,零头占比不小。其二,调度:SGLang 的调度器与树协同,倾向把命中同一分支的请求排在相邻批次,缓存局部性更好。完全相同的 prompt 反复请求,两者差距不大;前缀多变、多轮交错的场景才是分水岭。
3、多轮对话的收益账
设一个具体场景算账:固定前缀 2000 token(system 600 + 工具定义 1400),每轮新增 400 token(用户输入、工具结果与回复)。第 N 轮的 prompt 长度就是 2000 + 400N。两种策略的每轮 prefill 计算量对比如下:
| 轮次 | Prompt 长度 | 无缓存 prefill | 树命中后 prefill |
|---|---|---|---|
| 1 | 2400 | 2400 | 2400 |
| 5 | 4000 | 4000 | 400 |
| 10 | 6000 | 6000 | 400 |
| 20 | 10000 | 10000 | 400 |
| 20 轮累计 | — | 124000 | 10000 |
第 1 轮树上无缓存,全量在所难免;从第 2 轮起每轮只算 400 的增量。20 轮累计,prefill 计算量从 124000 降到 10000,约 12 倍的差距,且轮次越深差距越大——第 20 轮单轮是 25 倍。若有 5 个会话共享同一模板,主干那 2000 token 在会话之间也只算一次,这正是基数树跨请求复用的意义。
两个边界要说清。第一,省的是 prefill,decode 阶段每轮仍要逐 token 生成,输出长度不变;收益体现在 TTFT 与吞吐上,不改变单 token 生成速度。第二,这笔账成立的前提是前缀 append-only:历史按时间追加没问题;若模板把最新消息排在最前,每轮前缀都变,树就帮不上忙。
4、安装与启动验证
4.1、安装
SGLang 服务端面向 Linux,Windows 机器走 WSL2 或 Docker;NVIDIA 主线要求 CUDA 12.x 环境。官方安装文档与 GitHub 仓库都有各平台的现成命令,pip 安装一行搞定。
pip install -U "sglang[all]"
[all] 会连推理引擎 sglang[srt] 与 OpenAI 兼容前端一起装上。只想要客户端 DSL 的话,装裸的 sglang 即可。
4.2、启动与验证
起服务只要一条命令,模型以 Qwen3-8B 为例:
python -m sglang.launch_server \
--model-path Qwen/Qwen3-8B \
--host 0.0.0.0 --port 30000 \
--mem-fraction-static 0.85
--model-path 直接写 HuggingFace 仓库名,首次启动自动下载权重;--mem-fraction-static 控制权重加 KV Cache 占用的显存比例,留一点余量给激活值;RadixAttention 默认开启,不需要任何参数。首次启动要编译 CUDA graph,日志停到下面这行就算就绪:
Capture cuda graph end. Time elapsed: 18.32 s
The server is fired up and ready to roll!
之后重启秒级起服。端点是标准 OpenAI 风格,先用 curl 验证连通。
curl -s http://localhost:30000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen/Qwen3-8B",
"messages": [{"role": "user", "content": "你好"}],
"max_tokens": 32
}'
返回结构与 OpenAI 完全一致,节选如下:
{
"choices": [
{"finish_reason": "stop",
"message": {"role": "assistant", "content": "你好!有什么可以帮你的吗?"}}
],
"usage": {"prompt_tokens": 6, "completion_tokens": 9, "total_tokens": 15}
}
能拿到回包,服务就通了。旧客户端从 vLLM 或 llama-server 迁移,改一行 base_url 指到 http://localhost:30000/v1 即可。
5、结构化输出能力
SGLang 起家的卖点其实是前端 DSL(一套描述生成流程的小语言),约束解码是它的第一性能力。如今大多数人只用它的推理引擎,这套能力原样继承:服务端原生支持 response_format,语法后端默认 XGrammar,JSON Schema、正则、EBNF 约束都能接,细节见结构化输出文档。
Agent 从中最受益的一点:工具调用参数必须可解析。给输出套一层 JSON Schema,就不再出现"差一个引号导致 JSON 解析失败"的场面。示例如下:
schema = {
"type": "object",
"properties": {
"city": {"type": "string"},
"condition": {"type": "string"},
"rain": {"type": "boolean"},
},
"required": ["city", "condition", "rain"],
}
resp = client.chat.completions.create(
model="Qwen/Qwen3-8B",
messages=[{"role": "user", "content": "解析:明天北京小雨转多云"}],
response_format={"type": "json_schema",
"json_schema": {"name": "weather", "schema": schema}},
)
print(resp.choices[0].message.content)
返回保证是满足 schema 的 JSON 字符串,直接 json.loads 后进工具路由。XGrammar 把 schema 编译成高效约束机,LMSYS 的测试里让 JSON 约束解码提速 3 到 10 倍,开启约束对吞吐的影响基本可以忽略。
6、与 vLLM 的对比口径
先把口径立住:推理引擎的跑分结论高度依赖负载形状——前缀分布、并发度、模型、量化,换一个变量结论就可能反转。公开数据只用来定方向。
方向大致是:Particula 2026 年 3 月的横评测得 SGLang 在 H100 上整体吞吐高出约 29%,前缀复用重的负载(Agent 循环、few-shot)最高拉开 6.4 倍;而单轮独立 prompt 的场景,双方差距缩小到个位数百分比。vLLM 这边,APC 在"完全相同 prompt 反复请求"的简单模式上能追平大部分差距;前缀多变的多轮与 RAG 负载下,基数树的 token 级命中仍占优。
注意:以上数字是特定硬件与特定负载下的公开结果,不是承诺。选型前请用自己真实流量压一遍,尤其是你们 Agent 的平均轮次与前缀长度分布。
给工程结论:多轮共享前缀的 Agent 负载,SGLang 赢面明确;单轮高并发批处理(离线打标、批量摘要),两者都在第一梯队,选生态更熟的那个即可。vLLM 仓库在隔壁,两边都值得收藏。
7、选型决策建议
| 场景 | 推荐 | 理由 |
|---|---|---|
| 多 Agent 并发长会话 | SGLang | 前缀复用充分,TTFT 低 |
| 单轮批量离线处理 | 两者皆可 | 无共享前缀,差距小 |
| 结构化输出密集 | SGLang | XGrammar 默认集成 |
| 单机轻量自用 | llama.cpp | 第 3 篇路线更省心 |
| 多机张量并行 | 查当期文档 | 两边支持矩阵迭代都快 |
两个引擎的 API 层都是 OpenAI 兼容,切换成本约等于改一行 base_url。真正的沉没成本在运维脚本与监控面板,先小流量试跑,再全量切换。
8、接入 LangGraph Agent
本地 Agent 全栈的最后一环,是把推理服务接回编排层:LangGraph 管状态与流程,SGLang 管推理与前缀复用,中间用 OpenAI 兼容协议粘合,LangGraph 文档对自定义 endpoint 有完整说明。核心组合如下:
from langchain_openai import ChatOpenAI
from langgraph.checkpoint.memory import MemorySaver
from langgraph.prebuilt import create_react_agent
llm = ChatOpenAI(model="Qwen/Qwen3-8B", temperature=0,
base_url="http://localhost:30000/v1",
api_key="EMPTY") # SGLang 不校验 key,填非空即可
agent = create_react_agent(llm, tools=[search_tool],
checkpointer=MemorySaver())
config = {"configurable": {"thread_id": "demo-1"}}
agent.invoke({"messages": [("user", "SGLang 最新版本是多少")]}, config)
thread_id 对应的会话每次工具循环、每次恢复,消息列表都整体发给 SGLang,基数树自动命中历史前缀。checkpointer 保证会话可恢复,RadixAttention 保证恢复后的重放不付全量 prefill,两个机制一拍即合。这也是本博客 Agent 系列的标准后端组合:编排用 LangGraph,模型用本地 SGLang 服务。
9、总结
回顾全文要点:
- Agent 负载 = 少数长共享前缀 + 高频增量请求,prefill 是成本大头
- RadixAttention 用基数树做 token 级前缀复用,LRU 淘汰,默认开启
- 20 轮会话累计 prefill 可降一个数量级,轮次越深收益越大
python -m sglang.launch_server一条命令起 OpenAI 兼容服务- XGrammar 默认兜底结构化输出,工具参数解析不再靠运气
- 选型:多轮 Agent 选 SGLang,单轮批处理两者皆可
动手练习:起一个 SGLang 服务,写一段约 2000 token 的固定 system prompt 存进 system.txt,循环 10 轮对话,观察服务日志的变化。
from openai import OpenAI
client = OpenAI(base_url="http://localhost:30000/v1", api_key="EMPTY")
system = open("system.txt", encoding="utf-8").read() # 固定长前缀
history = [{"role": "system", "content": system}]
for i in range(10):
history.append({"role": "user", "content": f"第 {i + 1} 个问题"})
r = client.chat.completions.create(
model="Qwen/Qwen3-8B", messages=history, max_tokens=64)
history.append({"role": "assistant",
"content": r.choices[0].message.content})
服务日志的 Prefill 行能直接看到命中情况,节选如下:
Prefill batch. #new-seq: 1, #new-token: 52, #cached-token: 3094,
#running-req: 1, #queue-req: 0
#cached-token 从首轮的 0 涨到几千,对应每轮 TTFT 明显缩短。对照组:把当前时间戳拼进 system prompt 再跑一遍,看命中如何归零——这一组对照做完,前缀缓存的价值就有体感了。对于正在跑多 Agent 应用的同学,建议拿一份真实会话日志分别压测 SGLang 与现有引擎的 TTFT,数据会替你做决定。

