单卡 3060 跑 27B 大模型做客服:4 类"不回答"的真实定位法,与一道生产级限流闸门

2026-09-25 · AI智工坊 · 工程实测

我们自己的 AI 客服 / 数字人接待系统,后端主力是一个跑在单张 RTX 3060 上的 27B 本地模型。它的好处很直接:调用成本为零、数据不出内网、不受任何第三方接口配额影响;代价也很直接——单卡只有一份算力,谁先抢到谁赢。

上线之后我们遇到的每一次"模型不正常",最后追下来几乎都不是模型出了问题,而是调度与输出预算出了问题。这篇文章把定位法、判据、以及最后真正治本的那道限流闸门全部摊开,数字全部来自机器上的实测输出,不做任何估算性描述。

一、先记死两个判据,能省 90% 弯路

本地模型"不正常",不外乎下面两类。把这张表背下来,遇到报警先对号,不要先动模型:

现象真因一键判据
慢 / 超时 / 500 / 时好时坏被别的调用方占满算力(超长上下文 + 输出不限长 + 多路并发)槽位接口里 n_prompt_tokens 上万 且 params.max_tokens = -1
回答为空 / 只剩几十字碎片 / 答非所问开了"思考"开关但预算不够,思考把 token 吃满,正文被挤空finish_reason=length 且 content 为空,reasoning_content 很长

这两条覆盖了我们遇到的绝大多数故障。下面给出逐步定位的完整流程。

二、五步定位法:每步一条命令

第 1 步:确认服务活着(10 秒)

curl -s -m 8 http://<模型机IP>:8091/health      # {"status":"ok"}
curl -s -m 8 http://<模型机IP>:8091/v1/models   # 确认模型名与 n_ctx(我们是 65536)

第 2 步:看槽位——这一步定生死

curl -s -m 8 http://<模型机IP>:8091/slots

逐槽看四项:is_processing / n_prompt_tokens / params.max_tokens / params.stream。

第 3 步:量一把"干净速度"做对照基准

curl -s -m 120 http://<模型机IP>:8091/v1/chat/completions \
 -H 'Content-Type: application/json' -d '{"model":"<模型名>",
 "messages":[{"role":"user","content":"从1数到200,用逗号隔开"}],
 "max_tokens":400,"temperature":0,"stream":false,
 "chat_template_kwargs":{"enable_thinking":false}}'

用 usage.completion_tokens ÷ 耗时 算速度。我们的基准是 32~34 token/秒(RTX 3060 + 2.13bpw 量化)。如果实测低于 5 token/秒,那是被占满了,不是模型退化——不要据此判定硬件故障。

第 4 步:找出是谁在打(两个方向都要查)

# 模型机侧:看远端 IP
ss -tnp | grep 8091

# 嫌疑机侧(Windows):拿 pid 再拿命令行
netstat -ano | findstr ":8091"
wmic process where "ProcessId=<pid>" get Name,CommandLine /format:list

我们的实锤:一台机器上的桌面客户端把 3.4~4.7 万 token 的会话上下文、2~3 路并发、且不限输出长度地打进了推理端口。它不是恶意,只是没人告诉它"这是共享资源"。

Windows 节点排障提示:不要用 PowerShell 多行命令(会被吞行),netstat + wmic 走 cmd 最稳。

第 5 步:看 GPU 是否只是在"陪跑"

nvidia-smi --query-gpu=utilization.gpu,memory.used,temperature.gpu,power.draw,clocks.sm --format=csv

util 100% + 功耗贴上限(我们是 170W)+ 降频原因显示软件功耗封顶,都属于正常满载表现,只能佐证"确实有人在狠跑",不能据此说显卡坏了。

三、比"抢算力"更隐蔽的杀手:超长请求会吃掉整个上下文池

这一点值得单独拎出来说,因为它伪装成了"模型报错"。

当上下文窗口是一个被所有槽位共享的总池时(我们的配置是 65536,且不显式指定并行数时该实现默认开 4 个槽),几个超长请求就能把它吃干:我们实测 slot0=46663 加 slot1=16794,两者相加就占掉了约 95% 的池子。此后任何新请求都会被服务端直接顶回 500 Context size has been exceeded。

看到这个 500,不要去查模型,直接去槽位接口找那几路超长请求。

四、调用侧的三条铁律

  1. 写作 / 文档类(要长正文) → 必须显式关闭思考,并给足输出上限。注意:把 enable_thinking:false 放在请求顶层可能压不住思考,要放进模板参数里(chat_template_kwargs)——我们实测顶层关不掉,仍然撞上截断。
  2. 对话类 → 用阶梯重试:①开思考、上限 1200 → ②开思考、上限 3000 → ③关思考、上限 2500。合格判据 = content 非空 且(finish_reason=="stop" 或长度 ≥ 400)。只判"有返回"是不够的。
  3. 绝对不要 max_tokens=-1;绝对不要把 3 万 token 以上的整段会话上下文丢给单卡 27B。

五、一个反直觉的实测:自动任务根本不该跑在本地

很多人以为"本地模型免费,那就把所有自动任务都塞给它"。我们量化过一次真实请求,结果很反直觉——自动任务的每一次模型请求,都固定背着一个巨大的框架开销:

总量 ≈ 48,093 token
  ├─ 工具定义   30,993
  ├─ 系统提示   14,381
  └─ 真实对话    2,718   (仅 2 条消息)

那 3.1 万 token 的工具定义是运行时全局注入的,跟任务本身写什么完全无关,裁减任务内容也减不掉。

由此得到两个硬结论:

处置:①探活 / 自愈类任务 → 改成纯脚本(系统计划任务),彻底不占用模型;②确实需要模型判断的 → 该任务切到线上模型。别再往本地塞自动任务。

顺带一个省时间的判据:若任务失败信息里出现"请求被拒 / refusal"这类字样,说明是请求超长被顶回,不要当成任务逻辑出错去改代码。

六、治本:给本地模型加一道访问闸门

定位法只能救火,真正让这件事从"救火"变成"运行"的,是在模型前面加一层轻量网关:监听另一个端口(我们是 8092),转发到真正的推理端口 8091。好处是调用方零改动——只需要把 URL 的端口换掉,规则全部在闸门里统一执行。

规则我们的生产值
全局并发 / 单来源并发2 / 2
排队超时 → 返回 429120 秒
生成长度缺省或不限长 → 强制 2500;硬上限 3000
单请求硬超时 → 返回 504150 秒
提示词上限 → 返回 41324000 token(估算器已按中文校准,误差约 ±2%)
默认关思考开(客户端显式指定时尊重客户端)
观测GET /gw/stats 看实时排队与拒绝计数

⚠️ 闸门只有"所有人都必须走它"时才成立。如果推理端口还对外可达,任何人直连一下就绕过了全部规则。

🔴 实战验证过的两个大坑

坑 1:非流式响应必须回 Content-Length,否则客户端会挂死。在 HTTP/1.1 keep-alive 下,如果转发响应时既不回长度也不分块,用 keep-alive 的客户端会一直等"响应结束",挂到超时。现象极具迷惑性:curl 一切正常,换成 keep-alive 的 HTTP 客户端后整条链路卡死,日志里只看到一行 200 OK 然后没有下文。修法是透传上游的 Content-Length;上游没有长度信息时才用分块传输。

坑 2:透明转接比逐个改配置省事得多。收口旧端口之后,凡是写死旧端口的旧配置、脚本、乃至自动任务的提示词都会连不上。与其逐个改长文本,不如在模型机上把外部对旧端口的访问透明重定向到闸门:

iptables -t nat -C PREROUTING -p tcp --dport 8091 -j REDIRECT --to-ports 8092 2>/dev/null \
  || iptables -t nat -A PREROUTING -p tcp --dport 8091 -j REDIRECT --to-ports 8092

效果是:谁写旧端口都会被闸门接管,零配置改动、零遗漏。重定向保留源 IP,所以按来源限流照常有效。

七、显存这条线也别忘:同卡只能容一个

27B 常驻时会占掉几乎全部显存(我们的实测是 12.3 GB 里用掉 11.8 GB,只剩约 60 MiB)。我们曾在一台机器上发现一个数字人相关的残留服务每 5 秒抢一次显存,重启计数已累积到 13267 次,直接导致 27B 重启时报 cudaMalloc failed: out of memory 起不来。

排障铁律:27B 起不来,先看 nvidia-smi --query-compute-apps,别怀疑模型文件。

八、结论:本地模型能干什么、不能干什么

场景单卡 3060 + 27B(实测单路 33 tok/s、显存常年贴满)
短问答 / 单次判定 / 数字人客服✅ 可以,是本地模型最合适的活
多轮智能体任务、长文档生成❌ 不行——几十次调用 + 上下文一路滚大,必然排队
探活 / 自愈类自动任务❌ 不该用模型,一条 curl 就能判

把这几条落成规范之后,我们这边的分工就清晰了:本地负责零成本的轻活,线上模型只留给真正需要长文与复杂推理的重活。本地不是"免费的万能解",它是一份需要限流、需要纪律、需要观测的共享资源——把它当成共享资源来管理,它才真的稳定。

查看《AI 起步工具包》→

常见问题

本地部署的大模型经常"不回答"或答非所问,是不是模型能力不行?

绝大多数不是。我们的实测结论是:所谓"不正常"九成出在两件事上——要么被别的调用方占满算力(表现是变慢、超时、500、时好时坏),要么开了思考模式但输出预算不够(表现是回答为空、只剩碎片、答非所问)。前者看槽位接口就能确认,后者看 finish_reason 是不是 length、正文是不是空。

本地大模型变慢,第一反应应该是重启模型服务吗?

不应该。先看槽位:如果某一一路或几路请求的提示词上万 token、输出上限是 -1、还开着流式,那就是被别人占满了,不是模型退化。盲目重启会打断别人的在办请求,而且治不了根——调用方下次照样打进来。正确顺序是先定位是谁在打,再上并发与长度限制。

接口报 500 Context size has been exceeded,该怎么查?

不要去查模型本身,直接去槽位接口找那几路超长请求。当上下文窗口是所有槽位共享的总池时,超长请求会把它吃掉——我们实测两路请求就占掉了约 95%。治本方法是给服务入口加提示词长度上限。

单卡 3060 跑 27B 大概什么速度、能撑多少并发?

我们实测:2.13bpw 量化的 27B,单路短请求约 32~34 token/秒,显存占 11.8/12.3 GB。这个配置只能当"轻量共享服务"——短问答、单次判定、数字人客服可以;多轮智能体任务不行。

为什么给本地模型配置的自动任务经常失败?

因为自动任务的每次模型请求都固定背着约 4.8 万 token 的框架开销(其中工具定义 3.1 万、系统提示 1.4 万,真正的对话内容只有 2 千多)。工具定义是全局注入的,裁任务也减不掉。所以上下文上限低于约 2.4 万的本地方案跑不了;放开上限也没用,因为单卡预填充 4.8 万 token 每次要几分钟。探活、自愈类任务应该改成纯脚本。