本地大模型部署(07):Agent 负载之王

QuibblerAgentQuibblerAgent 2026-11-12 约 19 分钟 3 次阅读

第 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。结构如下:

flowchart TD R["root"] --> S["system+工具 ≈2000 tok"] S -->|"共享主干只算一次"| A["会话 A 历史+新问题"] S -->|"命中只算增量"| B["会话 B 历史+新问题"]

新请求到达时走三步:沿树从根开始匹配,找到最长可复用前缀;命中的部分直接取用 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,数据会替你做决定。

相关推荐

​Jev 详解:不做生成的判断模型
精选AI

​Jev 详解:不做生成的判断模型

Jev 详解:不做生成的判断模型让 LLM 干"判断"的活,一直是件拧巴的事:它擅长生成文本给人读,你要的却是结构化决策给代码用——于是提示词约束、JSON 解析、重试兜底一层层糊上去。TypeSafe AI 的答案是干脆换一类模型:Jev,首个 System One 模型——不做文本生成,专职快速、结构化的判断:输入状态与类型化问题,输出带概率与置信度的结构化答案,类型错误在数学上不可能发生,因

718
网站被注入恶意脚本排查实录:AI 辅助安全分析实战
置顶 精选AI

网站被注入恶意脚本排查实录:AI 辅助安全分析实战

网站被注入恶意脚本排查实录:AI 辅助安全分析实战某天收到华为云监管反馈,称个人博客网站存在色情违规界面。作为技术博主,这简直是晴天霹雳——网站一直正常运行,怎么会突然出现这类问题?经过排查发现,网站被注入了恶意跳转脚本。本文记录完整的排查过程,重点分享如何借助 AI 快速定位问题并修复,希望能给遇到类似情况的开发者提供参考。1、问题发现与初步排查收到监管反馈后,第一时间打开网站检查。首页看起来完

420
时隔三年,再次备份:从手搓博客到 AI 全面自动接管
精选AI

时隔三年,再次备份:从手搓博客到 AI 全面自动接管

时隔三年,再次备份:从手搓博客到 AI 全面自动接管今天打开尘封多年的备份目录,看到了一个熟悉的文件名——2023_07_09.sql。那一刻,恍如隔世。三年前的今天,我曾经认真地导出过一次数据库;三年后的今天——2026 年 7 月 9 日,自动备份启用。整整三年,1095 天,我的博客就这样安安静静地跑着,没有人备份,也没有人管,它就那么稳稳当当地活着。感慨万千,大概是此刻最准确的四个字——不

374