首页 / 资讯中心 / 文章详情

OpenClaw+千问3.5-9B智能体24小时长稳压测实战

OpenClaw+千问3.5-9B智能体24小时长稳压测实战 ★ FEATURED ARTICLE
1. 先搞清楚这场压测到底在测什么标题里那几个词——“OpenClaw”“千问3.5-9B”“持续运行24小时”——听起来像是某种硬核跑分但真正动手之前你得先问自己一个问题我要的结论是什么是“这个组合能不能跑”还是“这个组合能不能连续跑一天不出事”这两件事的技术难度差了至少一个数量级。我用 OpenClaw 这个开源智能体执行框架挂上本地部署的千问3.5-9B做了一次完整的24小时长跑压力测试中途踩了不少坑也推翻了自己不少想当然的判断。这篇就把整场测试的设计思路、实测数据、排查过程和可复现的配置全摊开讲。如果你只是想让模型回答几个问题那随便一台机器装个推理服务就够了这篇文章对你意义不大。但如果你打算把智能体真正放到生产环境里、让它无人值守地连续处理任务那你一定会遇到显存缓慢爬升、上下文越滚越大、延迟突然跳变、工具调用陷入死循环这些问题——它们不会在五分钟的演示里暴露只会在第七个小时、第十三个小时、第二十个小时突然给你来一下。这篇报告针对的就是这些“长跑型”故障。适合有一定部署基础、正在做本地智能体落地的开发者也适合只是想了解大模型长稳测试该怎么设计指标和埋点的同学核心计算和脚本我都会写清楚照着改就能跑。我先把结论性的判断放在前面方便你带着问题往下读这次测试里纯推理层面的稳定性非常好24小时零 OOM、零进程崩溃真正拖垮整体可用性的是智能体侧的上下文管理和重试逻辑。换句话说模型的锅不大框架的锅不小。这也是为什么单纯测“模型吞吐”意义有限——真实负载里模型只占整条链路的一小段。1. 测试目标和负载模型别拿短问答冒充压力测试1.1 为什么“能跑起来”是最没有信息量的结论任何一次压测如果负载设计和真实使用场景脱节那测出来的数字再漂亮也是自欺欺人。我见过太多人做的所谓压力测试就是拿几个固定 prompt 循环打接口看服务会不会挂。这种做法只能验证“进程会不会崩”验证不了“系统会不会退化”。真正的长跑型问题几乎全部来自状态累积对话历史越存越长、KV 缓存碎片越来越多、工具调用的中间结果堆在内存里没人清、日志文件把磁盘写满、文件描述符一点点漏光。所以我给自己定的第一条原则是压测负载必须保留真实场景里的“脏东西”。真实用户不会每次都问一个干干净净的新问题他们会追问、会纠正、会让智能体去查资料再汇总、会在同一个会话里聊上三十轮。这些行为叠加起来才是压力和短跑测试的分水岭。基于这个判断我把负载拆成了三类请求按真实比例混流而不是用单一类型刷数据。三类请求的具体设计如下这也是后面所有数据的基础你要复现的话直接照这个比例配置就行短问答型占比 45%单轮输入 80-200 token期望输出 200-400 token。模拟“问个概念、查个定义”这类高频轻负载主要用来观察基础延迟和吞吐。工具链路型占比 40%强制触发 3-5 次工具调用OpenClaw 会拆任务、调检索、调代码执行沙箱、汇总再输出。输入长度浮动大300-1500 token中间结果会反复塞回上下文。这是最容易出问题的一类也是最接近真实使用的一类。长上下文型占比 15%单次输入 6000-8000 token接近 max_model_len 上限用来压 KV 缓存和前缀缓存的命中率。比例定成 45/40/15 不是拍脑袋。我观察了一批真实会话日志日常使用中轻问答确实占大头但只要涉及“帮我做件事”工具链路就会成为耗时和失败的主要来源。如果按 1:1:1 平均分短问答会被过度放大整体指标虚高如果只测工具链路又会让并发压力失真因为长上下文的资源占用和前两类完全不是一个量级。1.2 三档并发的设计逻辑并发档位我设了 1、4、8 三档每档跑 8 小时中间重叠推进最终拼成完整的 24 小时曲线。为什么是 8 而不是更高因为这是 24GB 显存单卡在 8K 上下文下的实际上限附近——再往上加KV 缓存池会被撑爆测出来的就不是“稳定性”而是“OOM 触发时间”了。这里有个容易被忽略的点并发数不等于请求数。我一开始用固定 QPS 去打结果发现智能体框架本身有排队机制8 个并发槽位里有 3 个长期被长上下文请求占着实际打给模型的并发只有 5 左右。后来改成信号量控制 请求队列深度监控才能真正看清“槽位占用”和“模型并发”的差异。如果你也做类似测试务必把这两层分开埋点否则你会得到一个“看起来并发很高但模型很闲”的诡异结论。注意并发档位切换时不要直接改配置重启服务那会把显存状态清空掩盖掉长时间运行才有的碎片化问题。正确做法是保持服务进程不动只调压测端的并发数让服务在同一个进程生命周期内经历多档压力。这次测试的三个并发档就是在同一个服务实例上依次跑完的。1.3 什么叫“持续可用”指标必须先定义没有指标定义的压测报告等于没写。我这次盯的核心指标分四层从外到内依次是可用性、延迟、资源、失败模式。层级指标采集方式判定标准可用性请求成功率、进程存活时长压测端统计 进程守护脚本成功率 ≥ 99%进程零重启延迟TTFT、端到端 P50/P95/P99压测端埋点P95 不超过基线的 1.5 倍资源显存峰值、系统内存、GPU 利用率、温度5 秒一次采样显存不触及上限内存增长 5%/24h失败模式超时、上下文截断、工具死循环、空返回日志分类打标单类占比 1%TTFT首 token 延迟这个指标必须单独拎出来因为它和端到端延迟反映的是完全不同的东西。TTFT 主要由 prefill 阶段决定跟输入长度强相关端到端还要加上解码时间和工具调用耗时。很多报告只报平均延迟看到数字没涨就以为一切正常其实 TTFT 可能已经从 0.3 秒涨到了 2 秒——用户体感上就是“按下回车之后卡住了”这种退化比吞吐下降更致命。还有一点基线必须自己跑。不要拿别人的跑分数据当参照硬件、量化方案、推理引擎版本、甚至显卡驱动版本都会影响结果。我这次是在服务刚启动、显存最干净的状态下先跑了 200 条混合请求把结果作为基线后面所有对比都基于这个基线。2. 环境与参数把不确定性提前压掉2.1 硬件清单与显存预算的算法先把环境摆清楚。这套配置不是最优解是“大多数个人开发者和小团队手上能凑出来”的典型配置项目规格说明GPU单卡 24GB 显存消费级非数据中心卡系统内存64GB DDR5智能体框架和工具沙箱都吃内存CPU16 核主要给工具调用和文本处理用存储1TB NVMe日志和中间文件写入频繁系统Ubuntu 22.04驱动和推理引擎兼容性最好推理引擎PagedAttention 系、OpenAI 兼容接口支持连续批处理和前缀缓存显存预算是整个部署里最容易翻车的一环很多人是“先启动报 OOM 了再降参数”纯属浪费时间。我的做法是先算再跑公式很简单总显存 ≈ 权重显存 KV 缓存 激活与框架开销权重显存按量化精度算9B 参数FP16 每参数 2 字节约 18GBINT8 约 9GB4-bit 量化约 5.4GB。KV 缓存的公式是KV 显存 2 × 层数 × KV头数 × head_dim × 序列长度 × 并发数 × 精度字节数按千问3.5-9B 这个量级的典型结构估40 层、GQA 8 个 KV 头、head_dim 128FP16 精度下每 token 的 KV 占用是 2 × 40 × 8 × 128 × 2 163840 字节约 160KB。8K 上下文单序列就是 1.25GB8 并发就是 10GB。再加上激活值和框架开销 2-3GB总量就逼近 24GB 红线了。算完这笔账结论很清楚FP16 权重配 8 并发 8K 上下文单卡 24GB 装不下。于是只能在两个方向压缩——降权重量化精度或者降并发。我选了前者因为并发降下来吞吐就没法看了。2.2 量化档位的取舍为什么不选最激进的量化档位这块我做了三组对比测试数据如下同一批混合请求并发 4量化方案权重显存可用 KV 池平均解码速度短问答准确率工具调用参数正确率FP1618.0GB严重不足38 tok/s基准基准INT89.2GB约 9GB52 tok/s-0.4%-0.7%4-bitQ4_K_M5.4GB约 14GB61 tok/s-2.1%-4.8%数据很说明问题。4-bit 量化在纯问答上损失还能接受但工具调用参数正确率掉了接近 5 个百分点——这在智能体场景里是灾难性的因为一次工具调用参数错误就会引发重试重试又会让上下文膨胀形成连锁反应。我最终选了 INT8显存省了一半解码速度反而比 FP16 快因为显存带宽压力小了精度损失在可接受范围内。这个选择背后的逻辑值得多说两句。很多人默认“量化越激进越快”其实在显存受限的单卡场景里瓶颈往往不是算力而是显存带宽和 KV 池容量。4-bit 权重确实更小但反量化本身有开销而且量化后的模型在小 batch 下算力利用率上不去。INT8 在这个规模上是个甜点权重省一半精度几乎无损KV 池也够用。提示如果你的场景里工具调用占比很低、主要是问答4-bit 是划算的但只要涉及结构化输出、函数参数生成、多步规划就别省这点显存了。压测报告里我建议同时列出两档数据让读者自己按场景选。2.3 服务端关键参数每个都要说清为什么参数配置我列了个表每一项都标注了取值理由你可以直接抄参数取值理由max_model_len8192覆盖 95% 真实请求再高 KV 池就不够分gpu_memory_utilization0.85留 15% 给激活值和碎片0.95 在长跑中必崩max_num_seqs8与压测并发档位上限对齐避免排队雪崩enable_prefix_caching开启长上下文型请求重复前缀多实测 TTFT 降 30%swap_space4GB极少数超长请求的兜底宁可慢不可崩max_tokens1024硬性截断防止单条请求无限生成拖垮队列gpu_memory_utilization这一项我要重点说。很多人为了“榨干显卡”会设到 0.92 甚至 0.95短时间跑确实没事但连续跑几个小时之后显存碎片化会让实际可用空间不断缩水最后在某个不起眼的时刻突然 OOM。0.85 是个经验值牺牲了大约 8% 的理论吞吐换来了整个长跑的零 OOM。这笔交易很值。max_tokens的硬截断也很有必要。智能体框架有个特点它会在某次工具调用失败后尝试“解释一下为什么失败”如果模型陷入复读或者过度解释单条请求可能生成几千 token。没有硬截断的话一个异常请求就能把整个队列堵死几十秒。2.4 监控埋点不埋点等于白测监控这块我分了四路并行采集每路 5 秒一次统一打时间戳后落盘方便事后对齐GPU 侧显存已用、显存总量、GPU 利用率、温度、功耗。用nvidia-smi的查询模式循环采样别用watch那个精度不够。系统侧内存占用区分 RSS 和 cache、swap 使用、磁盘写入速率、打开的文件描述符数量。文件描述符这个指标特别容易被忽略但工具调用频繁的框架最容易在这里漏。服务侧队列深度、活跃序列数、KV 缓存使用率、前缀缓存命中率。这些通常能从推理引擎的指标接口拿到。应用侧每次请求的 TTFT、总耗时、输入输出 token 数、工具调用次数、重试次数、失败原因标签。四路数据要靠统一的时间戳对齐否则事后根本对不上“延迟飙升的那一分钟显存到底发生了什么”。我一开始偷懒只采了 GPU 和应用两层结果第 11 小时出现延迟尖峰时完全无法定位只能重跑。这个教训很贵别重复。3. 24小时实录曲线里藏着什么3.1 第 0-2 小时热身期的假象最危险前两个小时一切完美得不像话。并发 1 和 4 混跑P95 端到端延迟稳定在 1.8 秒左右TTFT 中位数 0.31 秒显存占用从 15.2GB 缓慢爬到 15.6GB看起来稳如老狗。这个阶段最大的陷阱是让人误以为测试已经成功了。我见过太多人跑到这一步就截图发结论了。实际上热身期的数据几乎没有参考价值因为此时上下文都是新的、KV 缓存是干净的、所有缓存都在冷启动后的最优状态。真正的退化要到四五个小时之后才开始显形。这两小时我唯一做对的事是建立了基线。200 条混合请求跑完我记录下了每类请求的 TTFT 分位数、端到端分位数、平均输出 token 数、工具调用平均轮次。后面所有异常判断都是以这份基线为锚点的比如“P95 超过基线 1.5 倍就告警”。没有基线你看到延迟从 1.8 涨到 2.4 时根本不知道该不该管。热身期还暴露了一个小问题工具调用型请求的首次执行明显偏慢平均 6.2 秒而后续同类请求只要 3.8 秒。排查下来是代码执行沙箱首次启动要加载依赖属于冷启动开销。这个不影响长稳但会拉高整体 P99我在统计时把前 20 条工具请求单独剔除了避免污染基线。3.2 第 2-8 小时系统内存的慢性爬坡第 2 小时开始升到并发 8问题立刻来了。GPU 显存曲线还算平稳15.6GB 到 17.1GB 缓慢爬升但系统内存的曲线是一条几乎笔直向上的斜线从 18GB 一路爬到 41GB六小时涨了 23GB平均每小时接近 4GB。这个斜率如果不变24 小时就是 90GB 以上64GB 内存必爆。但奇怪的是进程没有崩延迟也没明显变化。这就很典型了——内存泄漏在早期不会影响性能只会在某个临界点突然杀死你。我用tracemalloc抓了两次快照做 diff定位到增长主要来自两个地方一是 OpenClaw 的任务历史记录每次工具调用都会把完整的中间结果追加到会话上下文里而且从不清理二是代码执行沙箱每次运行都会创建一个临时目录目录对象被引用着没释放虽然文件本身被删了但 Python 对象还在。第一个问题是设计问题不是 bug。智能体需要保留历史才能做多轮推理但“保留”和“无限保留”是两回事。我的处理是加了个滑动窗口 摘要压缩机制会话超过 12 轮后把最早的 4 轮用模型自己总结成一段 200 字以内的摘要替换掉原始消息。这样上下文长度被控制在有界范围内语义信息也保留了大半。第二个问题纯粹是资源管理疏忽。修法是给沙箱加了个上下文管理器退出时强制shutil.rmtree并清空引用同时限制同时存在的临时目录数量上限为 32超了就按 LRU 清理。改完之后重跑这一段系统内存曲线从“每小时涨 4GB”变成了“稳定在 22-25GB 之间小幅波动”。这个修复的收益是决定性的——没有它后面 16 小时根本跑不完。注意内存排查不要只看总量要分 RSS、堆内存、缓存三块看。我一开始看到总量涨就以为是泄漏结果发现有一部分是文件页缓存属于正常行为白折腾了半天。用tracemalloc看 Python 堆用/proc/meminfo看整体两个对照着看才准。3.3 第 8-16 小时上下文膨胀与显存碎片这一段是整场测试最难熬的部分也是最有价值的部分。系统内存的问题解决后我以为可以安心跑完结果第 9 小时开始GPU 显存的锯齿变得非常明显在 17GB 到 21.5GB 之间反复跳动而且每次跳动的谷底越来越高——从 17.1GB 升到 18.3GB再升到 19.6GB。这是典型的显存碎片化特征。碎片化的成因不复杂。PagedAttention 系引擎虽然用分页管理 KV 缓存但当请求的序列长度分布极不均匀时我的负载里 15% 是 8K 长请求85% 是短请求块的分配和回收会产生大量不连续的空闲块。长请求进来时找不到连续的块引擎就会触发缓存驱逐驱逐本身又会产生新的碎片。我做的第一件事是验证这个判断把长上下文型请求的占比从 15% 临时降到 3%重跑了两小时。显存锯齿明显变缓谷底从 19.6GB 回落到 17.8GB。基本可以确认是长度分布导致的碎片。但真实负载不能砍掉长请求所以只能从两个方向缓解。一是开启前缀缓存让长请求里重复的系统提示和工具定义部分复用已有块实测长请求的 prefill 时间从 2.8 秒降到 1.9 秒KV 占用也降了约 12%。二是把max_num_seqs从 8 降到 6牺牲一部分并发度换取更充裕的块分配空间。这一手有点反直觉——降并发反而让整体吞吐上升了因为减少了驱逐带来的重算开销。还有一个细节值得记下来第 13 小时左右出现过一次连续 47 秒的“假死”所有请求都卡住不动之后又自己恢复了。日志里没有任何报错。我一开始怀疑是死锁后来对齐监控数据才发现那 47 秒 GPU 利用率是 100%显存占用瞬间冲到 22.8GB——是某个长请求触发了大规模缓存驱逐和重新 prefill把计算资源全占了。这种“非错误型卡顿”是最难排查的因为它不留任何错误日志只能靠资源曲线反推。3.4 第 16-24 小时长尾延迟与重试风暴最后 8 小时是最能暴露框架层问题的一段。整体成功率还在 99.2%看起来不错但P99 端到端延迟从基线的 8.4 秒涨到了 31.7 秒P95 从 4.1 秒涨到 6.9 秒。平均值几乎没变——这就是长尾问题的典型特征平均值会骗人。拆解这 31.7 秒的构成发现其中 22 秒花在了工具调用的重试链上。具体机制是这样的某个检索类工具偶发返回空结果OpenClaw 的策略是“结果为空就换个查询词重试”但没有设置重试上限。于是模型不停地改写查询、调用工具、拿到空结果、再改写最多的一次连续重试了 11 轮才因为上下文长度触顶而终止。这个问题有两个层面。表层是缺重试上限直接加一个“同一工具连续失败 3 次就降级”的规则即可。深层是缺少失败感知——框架无法区分“工具真的没找到”和“工具调用参数写错了”两种情况都走同一套重试逻辑导致本该快速失败的情况被拖成了长尾。我的修法分三步给每类工具单独设重试上限检索类 3 次、代码执行 2 次、数据读取 1 次在重试前强制做一次“失败归因”让模型用一句话说明上次为什么失败避免无脑重复超过上限后走降级路径直接返回“未能完成原因如下”而不是继续硬刚。改完之后重跑最后 4 小时P99 从 31.7 秒降到 12.1 秒。另外一个发现是失败会传染。第 19 小时有一波集中失败12 分钟内失败了 34 次但前后时段都正常。查日志发现是某个共享的工具实例进入了异常状态连接池耗尽所有走这个实例的请求全部超时而超时又触发了重试重试进一步加剧了连接池压力形成正反馈。这种雪崩在单机测试里不明显一旦上多实例就是灾难。修法是给工具实例加熔断连续失败 5 次就快速失败 30 秒期间请求直接走降级。4. 数据复盘瓶颈到底在哪一层4.1 分阶段指标汇总把 24 小时的数据按阶段切开对比看最清楚。下表是修复前前 16 小时和修复后后 8 小时采用新配置的汇总注意修复后的并发档位和修复前不完全一样所以吞吐数据不能直接横比重点看延迟分位数和失败率时段并发TTFT 中位数端到端 P50P95P99成功率显存峰值0-2h 基线1/40.31s2.1s4.1s8.4s99.8%15.6GB2-8h80.42s2.6s4.8s10.2s99.5%17.1GB8-16h80.58s3.4s6.2s18.6s99.3%21.5GB16-20h 修复前80.71s4.1s6.9s31.7s99.2%22.8GB20-24h 修复后80.46s2.8s5.1s12.1s99.6%19.4GB这张表里有两个值得盯的信号。第一TTFT 的退化幅度远小于端到端延迟。从 0.31 秒到 0.71 秒涨了 129%而 P99 从 8.4 秒涨到 31.7 秒涨了 277%。这说明模型侧的 prefill 能力基本稳定问题主要出在后端的编排和重试上。第二显存峰值和延迟呈明显正相关。21.5GB 以上时 P99 开始失控19.4GB 以下就恢复正常。这给了我们一个很实用的操作阈值把显存占用控制在总容量的 80% 以下长尾表现会好很多。4.2 显存曲线背后的三个真实原因单看“显存涨了”没有意义得拆开看是谁涨的。我把 8-16 小时那段曲线拆成三个成分成分占增量比例成因是否可消除KV 缓存块碎片约 52%长度分布不均块不连续部分可缓解不可完全消除会话历史未压缩约 31%上下文无限追加可通过摘要压缩消除引擎内部缓存池约 17%前缀缓存、临时张量属于正常开销保留这个拆解改变了我的优化优先级。原本我以为主要是历史消息太长结果碎片才是大头。而碎片这个东西靠“清理”是清不掉的只能靠控制长度分布和预留空间来缓解。所以最终方案是“降并发 开前缀缓存 预留 15% 显存”而不是“定期清理缓存”——事实上频繁调用显存清理接口反而会加剧碎片因为每次清理后重新分配都是从零开始。4.3 失败模式分类与占比24 小时里总共产生了约 4.7 万次请求含重试失败 318 次总失败率 0.67%。按原因分类如下失败类型次数占比典型表现根因请求超时12138.1%客户端主动断开长请求 队列排队叠加工具重试耗尽8727.4%返回“未能完成”缺重试上限与失败归因上下文超限截断5818.2%输出被截断或答非所问历史未压缩触达 max_model_len工具实例异常3912.3%连接超时连接池耗尽引发雪崩解析错误134.0%结构化输出格式不合法量化后格式稳定性下降这个分布说明一件事模型的可用性不是瓶颈编排逻辑才是。五类失败里只有“上下文超限”和“解析错误”跟模型直接相关加起来不到四分之一。剩下四分之三全是框架层的问题。这也是我给所有做智能体落地的朋友的第一条建议先把你自己的重试逻辑、上下文管理、资源回收写扎实再去调模型参数。5. 踩坑与排查手册5.1 问题速查表把这次遇到的所有问题整理成一张速查表遇到对应现象可以直接对照现象最可能原因快速验证方法处理动作系统内存线性上涨历史记录或临时对象未释放tracemalloc两次快照 diff加滑动窗口、用上下文管理器显存锯齿且谷底抬升KV 块碎片化临时降低长请求占比重测开前缀缓存、降 max_num_seqs平均延迟正常但 P99 飙高长尾重试链按请求拆解耗时构成设重试上限、加失败归因突然全员卡住无报错大规模缓存驱逐占满算力对齐 GPU 利用率与显存曲线提升显存余量、削峰集中时段批量失败共享资源进入异常态看失败是否集中于同一工具实例熔断 快速失败 降级输出格式偶发不合法量化精度损失对比不同量化档的解析成功率换 INT8 或加格式校验重试文件描述符持续增长工具调用未关闭句柄ls /proc/PID/fdwc -l 定时采样5.2 几个文档里不会写的经验第一压测要在同一个进程生命周期内完成。中途重启服务会把所有累积状态清空你测出来的就是“新鲜状态下的性能”毫无意义。我这次三个并发档位都是在同一个服务进程上跑的正是因为这样才暴露出碎片化问题。如果你的测试必须重启比如改了配置那就在报告里明确标注别把重启后的数据混进长跑曲线。第二日志要按请求 ID 串联别按时间戳。时间戳在并发场景下极不可靠几十个请求交织在一起你根本无法从日志流里还原单条请求的完整链路。我的做法是每个请求生成一个短 ID所有环节模型调用、工具调用、重试、失败都带上这个 ID事后按 ID 聚合就能看到完整故事。这一步在排查重试风暴时救了命。第三别迷信“平均值”和“成功率”。99.2% 的成功率听起来很美但那 0.8% 里可能包含了所有最糟糕的体验。真正要盯的是 P99 和“最差单条请求耗时”这两个数字才代表用户最坏情况下的感受。第四温度墙是隐形杀手。我这次没遇到严重降频但在第 14 小时观察到 GPU 温度摸到 78 度时解码速度有大约 6% 的下滑。如果你的机箱散热一般长跑时务必把温度也纳入监控别等性能莫名下降才去查。第五测试脚本本身的资源占用要单独评估。压测端如果和被测服务跑在同一台机器上脚本自己的内存和 CPU 占用会污染数据。我这次是把压测端跑在另一台机器上通过内网打请求虽然多了一层网络开销但数据干净得多。如果只能用单机至少把压测进程的 CPU 亲和性绑到不同的核上。第六准备一个“熔断开关”。长跑测试最怕的是跑到一半系统进入不可恢复状态然后你只能干等着或者强行杀掉。我的做法是加了一个外部看门狗当连续 100 次请求失败或显存占用超过 95% 持续 60 秒就自动把并发降到 1 并记录现场让系统有时间自我恢复同时保留证据供事后分析。6. 可复现的脚本与配置6.1 压测驱动脚本的核心结构压测端我用 asyncio 写的核心是三类请求混流 信号量控制并发 每请求埋点。下面是最小可用版本把请求构造部分替换成你自己的场景即可import asyncio, time, random, json, uuid import httpx ENDPOINT http://127.0.0.1:8000/v1/chat/completions CONCURRENCY 8 DURATION 24 * 3600 sem asyncio.Semaphore(CONCURRENCY) records [] def build_short(): return {messages: [{role: user, content: 解释一下什么是注意力机制200字以内}], max_tokens: 400, temperature: 0.7} def build_tool_chain(): # 真实场景里这里换成带工具定义的请求强制触发多轮调用 return {messages: [{role: user, content: 查一下最近的季度数据并汇总成表格}], max_tokens: 1024, temperature: 0.3} def build_long(): filler 以下是背景资料。 * 900 # 构造接近 8K 的输入 return {messages: [{role: user, content: filler 请总结要点}], max_tokens: 512, temperature: 0.5} TYPE_POOL [(build_short, 0.45), (build_tool_chain, 0.40), (build_long, 0.15)] def pick_payload(): r random.random() acc 0 for fn, w in TYPE_POOL: acc w if r acc: return fn() return build_short() async def one_request(client, req_id): payload pick_payload() payload[stream] True t0 time.perf_counter() ttft None tokens 0 status ok try: async with client.stream(POST, ENDPOINT, jsonpayload, timeout120) as resp: async for line in resp.aiter_lines(): if not line.startswith(data:): continue chunk line[5:].strip() if chunk [DONE]: break if ttft is None: ttft time.perf_counter() - t0 tokens 1 except Exception as e: status type(e).__name__ total time.perf_counter() - t0 records.append({id: req_id, ttft: ttft, total: total, tokens: tokens, status: status}) async def worker(client, deadline): while time.perf_counter() deadline: async with sem: await one_request(client, uuid.uuid4().hex[:8]) await asyncio.sleep(random.uniform(0.05, 0.3)) # 模拟真实思考间隔 async def main(): deadline time.perf_counter() DURATION async with httpx.AsyncClient(limitshttpx.Limits(max_connections64)) as client: await asyncio.gather(*[worker(client, deadline) for _ in range(CONCURRENCY * 2)]) with open(records.jsonl, w) as f: for r in records: f.write(json.dumps(r) \n) asyncio.run(main())几个实现上的细节值得说。sem控制的是“同时在处理的请求数”而 worker 数量我设成了并发的 2 倍这样在某个请求阻塞时还有别的 worker 能顶上不会出现“并发没打满”的假象。streamTrue是为了精确测 TTFT非流式请求拿不到这个指标。records存在内存里最后一次性落盘避免频繁 IO 干扰测试但如果跑很久要注意内存占用超过 10 万条就分批写。6.2 长跑配置模板这是最终稳定运行的配置24 小时零崩溃的那一版model: qwen3.5-9b-int8 max_model_len: 8192 gpu_memory_utilization: 0.85 max_num_seqs: 6 enable_prefix_caching: true swap_space: 4 enforce_eager: false agent: max_tool_rounds: 8 # 单次任务最多 8 轮工具调用 history_window: 12 # 保留最近 12 轮原始消息 summary_trigger: 12 # 超过后触发摘要压缩 summary_keep: 4 # 把最早的 4 轮压成摘要 retry_policy: retrieval: 3 code_exec: 2 data_read: 1 circuit_breaker: fail_threshold: 5 open_seconds: 30 sandbox: max_concurrent_dirs: 32 cleanup_on_exit: true max_exec_seconds: 20max_num_seqs: 6这个值是从 8 降下来的前面说过原因。history_window和summary_trigger配合把上下文长度锁死在有界范围内。circuit_breaker那段是防雪崩的关键参数不用照抄但机制一定要有。6.3 告警阈值建议最后给一份阈值参考长跑测试时挂上出问题能第一时间知道指标告警阈值处置动作显存占用 90% 持续 60s降并发、清理会话系统内存24h 增长 8%抓内存快照分析P95 端到端延迟 基线 1.5 倍检查重试链和队列深度成功率5 分钟内 98%检查工具实例健康度文件描述符 5000排查句柄泄漏GPU 温度 80 度检查散热、考虑降频这套阈值不是绝对值你的硬件和场景不同基线也不同但判断逻辑是通用的先有基线再设相对阈值别看绝对值。整场 24 小时跑下来我最大的体会是硬件和模型其实比想象中靠谱得多——INT8 的千问3.5-9B 在单卡上连跑一天推理侧没出过任何硬故障真正的风险全在编排层。那些“跑了 11 个小时突然卡住”“第 19 小时批量失败”的问题回过头看都是可以在设计阶段避免的给重试加上限、给上下文加窗口、给共享资源加熔断、给显存留余量。这四件事做完智能体从“能演示”到“能长跑”之间的距离比你想象的要短。
阅读完成 · 觉得有帮助?
咨询建站