这两天 Hacker News 和 Reddit r/LocalLLaMA 上,一个很适合开发者跟进的话题是:有人尝试在 48GB Apple Silicon Mac 上用 SlotStream 思路运行 104GB 级的 Qwen3.8-Flash-Next,并给出大约每秒十几个 token 的演示。参考来源:Hacker News、Reddit r/LocalLLaMA、GitHub Trending、Hugging Face Daily Papers。
先把预期说清楚:这不是“消费级笔记本秒变推理集群”。本地跑超大模型的价值,是让开发者在离线、私有、可调试的环境里验证 agent、RAG、代码生成和长上下文策略。真正要服务多人在线,仍然需要 GPU、推理引擎、队列和监控。但如果你的目标是把一个 agent 产品原型跑通,48GB Mac 加低内存加载已经足够有研究价值。
为什么这件事值得写成工坊
本地 LLM 过去的主线是量化:从 FP16 到 INT8、INT4,再到更激进的混合量化。问题是,量化只解决权重体积的一部分。模型越大,实际运行时还会被三件事卡住:加载峰值、KV cache、上下文里的工具噪声。
加载峰值指的是模型文件在进入可推理状态前,可能短暂占用比最终常驻更多的内存。KV cache 指的是每生成一个 token,模型都要保存注意力相关状态。上下文里的工具噪声则来自 agent 场景:一个普通问答只塞用户问题和系统提示,一个 agent 会塞工具 schema、历史计划、检索片段、执行观察和错误栈。
SlotStream 这类思路有意思的地方在于,它把问题从“模型能不能放进内存”改成“模型权重和运行状态能不能按需要分段进入工作集”。对本地开发者来说,这意味着我们需要写的不只是启动命令,还要写一套推理预算。
目标架构
这篇文章用一个保守架构做演示。底层模型服务可以替换成 llama.cpp、MLX、Ollama 的实验分支,或者支持分段权重加载的自定义 runner。上层我们写一个 agent 网关,负责四件事:
- 限制上下文窗口
- 压缩工具说明
- 记录每轮 token 预算
- 在本地模型过慢时降级到更短计划
目录可以这样组织:
local-qwen-agent/
model-server.ts
budget.ts
tools.ts
agent-loop.ts
prompts.ts
下面的代码不绑定某个具体底层 runner。你只要把 generate 函数替换成自己的本地推理 HTTP 接口即可。
第一步:建立推理预算
本地大模型最怕无意识地把上下文塞满。我们先写一个预算器,把系统提示、工具说明、检索内容和历史观察分开计数。
type ContextPart = {
kind: "system" | "tools" | "memory" | "retrieval" | "observation" | "user";
text: string;
priority: number;
};
const APPROX_CHARS_PER_TOKEN = 3;
function approxTokens(text: string) {
return Math.ceil(text.length / APPROX_CHARS_PER_TOKEN);
}
function fitContext(parts: ContextPart[], maxTokens: number) {
const sorted = [...parts].sort((a, b) => b.priority - a.priority);
const kept: ContextPart[] = [];
let used = 0;
for (const part of sorted) {
const cost = approxTokens(part.text);
if (used + cost <= maxTokens) {
kept.push(part);
used += cost;
}
}
return {
used,
text: kept.map((part) => part.text).join("\n\n"),
};
}
这里故意使用粗略估算。开发阶段不需要每次都调用 tokenizer,只要能稳定避免把窗口顶爆即可。生产环境可以换成模型对应 tokenizer。
第二步:压缩工具说明
Agent 工具说明经常比用户问题更长。很多框架会把完整 JSON schema 全塞进去,结果本地模型还没开始思考,窗口已经被工具占掉一大块。我们改用“工具卡片”。
type ToolCard = {
name: string;
purpose: string;
args: string[];
returns: string;
risk: "read" | "write" | "external";
};
function renderToolCards(tools: ToolCard[]) {
return tools
.map((tool) => {
return [
`tool: ${tool.name}`,
`purpose: ${tool.purpose}`,
`args: ${tool.args.join(", ")}`,
`returns: ${tool.returns}`,
`risk: ${tool.risk}`,
].join("\n");
})
.join("\n\n");
}
工具卡片的好处是稳定、短、可缓存。模型第一次规划时只需要知道工具能做什么,不需要看到每个字段的完整 schema。真正调用工具前,网关再做结构化校验。
第三步:写本地模型客户端
假设你的本地 runner 暴露一个 OpenAI 兼容接口,我们写一个很薄的客户端。关键参数是 max_tokens 和 temperature,不要在本地 agent 里让模型无限生成。
type GenerateOptions = {
prompt: string;
maxTokens: number;
temperature: number;
};
async function generate(options: GenerateOptions) {
const res = await fetch("http://127.0.0.1:8080/v1/completions", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
model: "qwen3.8-flash-next-local",
prompt: options.prompt,
max_tokens: options.maxTokens,
temperature: options.temperature,
}),
});
if (!res.ok) {
throw new Error(`local_model_error:${res.status}`);
}
return res.json() as Promise<{ choices: Array<{ text: string }> }>;
}
如果底层是 MLX 或 llama.cpp,接口字段可能不同,但上层思想不变:每次调用都要有硬预算。
第四步:建立短回合 agent loop
本地大模型更适合短回合、强约束的 agent loop。不要让它一次计划二十步。每轮只允许做一件事:回答、调用工具、或者要求更多信息。
type AgentDecision =
| { type: "answer"; content: string }
| { type: "tool"; name: string; input: Record<string, unknown> }
| { type: "clarify"; question: string };
function parseDecision(text: string): AgentDecision {
try {
return JSON.parse(text) as AgentDecision;
} catch {
return {
type: "answer",
content: text.trim(),
};
}
}
async function runAgent(userInput: string, tools: ToolCard[]) {
const parts: ContextPart[] = [
{ kind: "system", priority: 100, text: "You are a local coding agent. Return JSON only." },
{ kind: "tools", priority: 80, text: renderToolCards(tools) },
{ kind: "user", priority: 100, text: userInput },
];
const context = fitContext(parts, 6144);
const result = await generate({
prompt: context.text,
maxTokens: 512,
temperature: 0.2,
});
return parseDecision(result.choices[0]?.text ?? "");
}
这个 loop 很朴素,但适合本地模型。你可以把每轮工具观察压缩成三行:工具名、关键结果、错误状态。不要把原始 JSON 巨量返回直接塞回模型。
第五步:处理慢速和失败
本地推理每秒十几个 token 时,用户体验来自节奏管理。模型慢不可怕,可怕的是没有边界。建议增加三个策略。
第一,设置首 token 超时。如果模型 20 秒没有返回任何内容,直接中断并降级到小模型摘要。
第二,限制每个任务的最大轮数。比如工具调用最多 4 轮,超过后输出当前证据和未完成项。
第三,缓存工具卡片和常用系统提示。即使底层无法做真正 prefix cache,上层也能避免重复构造长 prompt。
伪代码如下:
type RunLimits = {
maxTurns: number;
maxPromptTokens: number;
maxOutputTokens: number;
};
const LOCAL_LIMITS: RunLimits = {
maxTurns: 4,
maxPromptTokens: 6144,
maxOutputTokens: 512,
};
SlotStream 部署时看哪些指标
不要只看 tokens per second。至少记录五类指标。
加载耗时:从进程启动到第一轮可推理。分段加载如果让启动时间大幅变长,就要评估是否值得。
峰值内存:包括模型权重、运行时、KV cache 和系统其他进程。macOS 内存压力变黄以后,体验会明显波动。
首 token 延迟:agent 产品里,用户更容易感知首 token,而不是总吞吐。
工具回合成本:每次工具调用后重新组织上下文,会增加延迟。工具越多,越需要摘要。
失败恢复时间:模型输出坏 JSON、工具失败、上下文过长时,系统能否快速回到可用状态。
结论
Qwen3.8-Flash-Next 这类模型在 48GB Mac 上可运行,真正说明的是本地 AI 开发边界正在外扩。开发者不用把所有实验都交给云端,也不用为了验证 agent 架构先买昂贵 GPU。
但本地大模型不是免费午餐。你要用工程手段换可用性:权重分段加载降低启动和常驻压力,短工具卡片减少上下文浪费,固定 token 预算保护 KV cache,短回合 agent loop 保证失败可控。
我的建议是先把它当成“私有 agent 实验台”。在本机验证工具选择、RAG 摘要、代码生成和权限策略;等流程稳定后,再决定哪些任务值得迁移到云端高吞吐推理。这样本地大模型的价值就不是炫技,而是让你更快发现 agent 系统真正的瓶颈。