本地400路AI并发对话工作台:调度、上下文与资源隔离实战

发布时间:2026/9/4 4:50:37
本地400路AI并发对话工作台:调度、上下文与资源隔离实战 把四百多条不同上下文的 AI 对话同时跑在本地同一台机器上还要保证它们不会相互干扰、不会把显存撑爆、不会中途“集体失忆”这是我最近折腾出来的一个开源项目也是这篇文章想讲清楚的核心。我先说结论本地跑大量并发对话瓶颈往往不在模型本身的“智商”而在调度、上下文管理和资源隔离。开源工作台解决的最核心问题其实是一句话——把“N 个同时存在的 Agent 会话”管成一件可控的事而不是开一堆脚本各自为战。你可以把它理解成一个偏底层的“调度壳子”上面接 Ollama、vLLM 这类本地推理服务下面按会话维度统一管理记忆、工具调用、重试、超时和持久化。这个项目适合两类人一类是想做本地私密 AI 服务但发现单条对话根本不够用的开发者另一类是维护批量 AI 任务、需要几百个并行上下文做处理的技术爱好者。下面从设计思路、核心实现、部署压测到踩坑排查完整展开。1. 为什么需要 400 个并发对话问题场景回顾很多朋友第一次听到“400 个 AI 对话同时跑”时会觉得我在做某种压力表演。但实际场景里这种需求非常真实你可能有几十个不同领域的知识库问答入口每个入口背后要有独立的上下文不能互相串味可能要做批量网页摘要、批量文章初稿、几百个不同角色的对话模拟也可能是在做多代理协作的验证一个主控 Agent 需要同时盯住大量子任务。这些需求本质上都不是“一次性发 400 个 prompt”而是 400 个独立的、有状态的、会连续多轮的对话。每个对话要记住自己说过什么要去调用不同的工具要按各自节奏走完流程。真正难的地方在于管理状态而不是发请求。1.1 你需要的不是“聊天机器人”而是“会话态 Agent 运行时”普通调用大模型的姿势很简单拼一个 promptPOST 给模型等返回结束。可一旦把一个请求拆成“用户发起—Agent 思考—调用工具—再思考—给结果”这种多轮流程事情就变复杂了。中间任何一步失败都可能让整个任务的上下文断裂。我在早期版本里踩过一个典型的坑用一张大表去存每个会话的历史消息写了个循环一批接一批地调模型。表面看能跑但一个问题出现后所有会话开始排队队列越来越长最后系统完全失去实时性。原因就是我把它当成“批处理任务”在跑而不是当成“会话态运行时”在跑。后来我把整个模型重构成一个常驻服务每个会话都是一个独立运行单元有自己的状态、自己的轮次上限、自己的重试策略。外部只看得到两个接口建立会话和把消息推给指定会话。具体内部怎么调度、怎么裁剪历史外部完全不用关心。1.2 为什么强调“本地”数据不出内网也方便做模型自由切换强调本地部署原因首先是隐私。很多项目里流转的对话内容并不适合传到外部服务比如内部知识库片段、未发布的产品描述、客户反馈数据。把这些内容放到本地模型上处理至少在链路层面可以把数据控制在自己手里。第二个原因是成本模型不一样。本地部署也许单次响应质量赶不上顶级商业大模型但一旦并发数量到达几百路按调用量计费的成本会非常惊人。一次 400 路的全量任务如果每路跑 20 轮就是 8000 次模型调用放在云端按 token 计费跑一轮普通开发者基本吃不消。自己有一张消费级显卡或者一张云主机 GPU时序不敏感的场景就能无限复用。第三点本地部署可以随时换模型基底。我早期用 7B 级别的小模型跑通后整体切到一个更大的 14B 模型只改一行配置。如果是绑定外部 API 做这种切换通常要连带改代码也不会这么自由。1.3 为什么不能靠“多线程循环调用”解决有人会说这个需求直接拿 Python 的 ThreadPoolExecutor 或者 async 并发请求模型接口不就行了我最初的版本就是类似的思路后来发现把会话逻辑和模型调用逻辑混在一起会面临三个绕不开的问题会话上下文需要共享内存占用没有边界。每个会话都持有独立的 token 序列靠普通变量管理很容易在 100 路时就开始飘在 400 路时彻底失控。失败恢复没有统一策略。某一个会话超时重试是整个脚本重跑还是只重试那个会话没有会话级边界就只能全部重来。没有优先级。几百个会话里有些是交互式的用户正等着回话有些是后台慢任务晚几秒没关系。普通循环不会区分只能一刀切排队。所以这个工作台本质上不是在“调用模型”而是在维护一批“有状态、有优先级、可中断恢复的长任务”。后面的架构设计也都是围绕这句话展开的。2. 工作台整体设计把智能体当“有状态的协程”来管在设计整体架构时我参考了普通后端里很成熟的思路——把每个会话看成一个“协程”而不是一坨需要同步执行的数据块。这种思路在 Web 服务器里已经用了很多年只不过我们把“请求”换成了“对话轮次”把“响应”换成了“模型生成结果”。在这个体系里核心不再是怎么调用模型而是模型调用之外的那些“会话生命周期管理”。模型推理本身交给本地推理服务去处理工作台负责的是更上层的编排。2.1 会话即协程一个 Agent 是一个可暂停的任务我先明确一个概念边界一个会话Session代表一段独立的对话历史可以对应用户、任务或者业务实体一个“Agent”是这个会话对应的智能体配置包括系统提示词、可用工具列表、模型参数、最大轮次等等。每个会话的最后状态可以有两种存在形式如果是流式对话它处于“等待下一轮”状态如果需要周期扫描任务它处于“挂起等待触发”状态。这就是协程模型。Python 的 asyncio 天然支持这种写法一个 Agent 会话可以写成一段 async 函数中间每次调用模型或工具时 await 一下挂起并让出 CPU等结果回来再继续跑。几百个这样的协程同时存在对内存的消耗远远小于几百个线程也避免了线程切换开销。工作台核心的骨架大致是这个样子# 描述逻辑不代表完整源码 class AgentSession: def __init__(self, session_id, agent_config): self.session_id session_id self.agent_config agent_config self.history: list[dict] [] self.round_left agent_config.max_rounds self.status pending # pending/running/paused/done/error async def run(self, runtime): while self.round_left 0 and self.status running: prompt runtime.build_prompt(self) response await runtime.call_model(sessionself, messagesprompt) self.history.append(response) tool_calls runtime.parse_tool_calls(response) if not tool_calls: break for call in tool_calls: tool_result await runtime.execute_tool(call) self.history.append(tool_result) self.round_left - 1 if self.round_left 0: self.status done这段骨架基本体现了工作台对“Agent”的理解它是一个有循环、有工具调用能力、有轮次上限的独立单元。实际代码里还会加上断点续跑、优先级抢占、上下文裁剪但主体就是这个循环。2.2 调度层与模型适配层分离在早期版本里模型调用和调度逻辑是耦合的。每个会话内部直接写“调用 Ollama”或者“调用 vLLM”的代码。后来发现只要需要支持第二个模型后端整个代码就开始到处贴分支十分难受。重构后我做了严格分层最外层是 API 服务层负责接收用户消息、创建会话、查询会话状态中间是调度层负责维护所有 AgentSession控制并发信号量处理重试和超时最底层是模型适配层把 vLLM、Ollama、OpenAI 兼容接口统一为一个chat_completion()方法。这种分层带来的直接好处是切换推理后端永远只改一个配置文件代码完全不用动。我后来在本地同时挂过两个后端服务一个负责小模型快速响应一个负责大模型处理复杂任务工作台只要给不同会话配置不同的后端名称即可负担非常小。# 工作台后端的简单配置示意一个生产实例可以挂多个模型后端 model_backends: chat-fast: type: openai_compatible base_url: http://127.0.0.1:8000/v1 api_key: local-dummy model: qwen2.5-7b-instruct max_concurrent: 32 timeout: 120 chat-strong: type: openai_compatible base_url: http://127.0.0.1:9000/v1 api_key: local-dummy model: qwen2.5-14b-instruct max_concurrent: 16 timeout: 1802.3 为什么要统一成 OpenAI 兼容格式访问这个决定争议不大现在绝大多数本地推理服务包括 Ollama、vLLM、LocalAI 等都提供 OpenAI 兼容的/v1/chat/completions接口。统一成这个格式后工作台代码里不用关心底层是哪种推理框架。从使用角度这套协议的核心结构非常稳定一个messages数组包含system、user、assistant角色可选tools参数返回内容里也可能带回工具调用。所有本地推理框架只要把数据映射到这套协议上工作台就能跑。如果某天你想换一个更偏门、不支持 OpenAI 格式的推理后端也只需要自己写一个适配器类把消息列表转换成那个后端的输入格式。这个成本被收敛在一个小文件里总比散落在各处强。2.4 并发控制不要让 400 个请求同时打到模型后端设置“能同时跑 400 个对话”不意味着在某个瞬间发出 400 个模型请求。这样做不仅没有意义还会瞬间耗尽显存更可能让推理后端进入崩溃状态。我的设计是给每个模型后端配置一个信号量Semaphore控制真正在途的请求并发数。假如后端配置max_concurrent: 32那么同一时刻只有最多 32 个会话在调用模型其余 300 多个会话会安全排队。这样既保证了总体任务量又不会把推理服务冲垮。# 并发控制示意 class ModelBackend: def __init__(self, base_url, max_concurrent): self.semaphore asyncio.Semaphore(max_concurrent) async def chat_completion(self, messages, **kwargs): async with self.semaphore: # 真正的 HTTP 请求超时、重试都在这里处理 return await self._http_post(messages, **kwargs)这个设计一开始可能会让某些人失望400 个会话其实是“排队”跑而不是并行飞起。但真正接触过生产系统的人都清楚排队本身就是合理性的一部分。让 400 个任务以每秒几十个的速率平稳推进远好过同时涌上去把显存打满后全部失败。队列里同样需要优先级。交互式会话的请求可以优先插队后台任务走默认流程。我就直接在消息对象上放了一个priority字段调度器每次先取优先级高的任务发放信号量。3. 核心实现细节上下文管理、工具调度与状态持久化这一部分是很多人容易低估的地方。400 个会话只要不发生上下文错乱其实就是一个调度问题可一旦某个会话把另一个会话的历史消息带进去或者历史被无限撑大整体运行就会快速失控。3.1 多轮 Agent 循环的“上下文拼接”到底怎么处理每个会话在调用模型前需要从历史消息里拼出一个 prompt。理论上直接把history全量塞给模型最简单但跑到 100 轮以后光历史就能把一个模型窗口塞爆。所以我在工作台定义了一个“消息裁剪器”。它有三种策略滑动窗口只保留最近 N 轮对话最早的消息直接丢弃摘要折叠当历史超过阈值先用一次轻量模型调用把早期历史压缩成一个摘要再注入到系统提示词里混合模式系统提示词 最近详细消息 早期摘要。实际效果最好的是混合模式。系统提示词里放任务定义和长期记忆摘要最近几轮保持完整这样模型既能理解高层目标又有足够的上下文处理当前回复。def build_prompt_from_history(history, summary, max_recent_rounds6): sys_msg { role: system, content: f{AGENT_PRESET}\n长期记忆摘要\n{summary}\n请基于摘要和最近对话继续。 } recent history[-max_recent_rounds * 2:] # 一轮至少两条消息 return [sys_msg] recent值得提醒的是摘要折叠不能太频繁。如果每条消息都做一次摘要会平白增加大量模型调用反而拖慢整体速度。我的做法是摘要只在一个会话的历史消息条数超过阈值时触发一次后续新增消息按滑动窗口方式追加。3.2 Token 预算与上下文隔离怎么避免“串线”和“爆窗”400 个会话放在同一个进程里最怕的情况就是不同会话的历史互相污染。为了防止串线我做了下面几件事每个会话在整个生命周期里只通过session_id访问自己的历史任何列表更新都在加锁的上下文内完成日志输出统一带上session_id前缀排查问题时能快速看出是哪一路出了问题每个会话结束后立即释放历史引用不让内存无限增长。Token 预算方面工作台会按“组”计算。比如一个后端限制最大上下文 32768 tokens那我就在配置里给每个会话预留 4096 tokens。这样即使后端同时处理多个会话每个会话也只会占用自己的配额。如果某个会话连续两次裁剪后仍然超限工作台会直接报“context overflow”错误并把这段会话标记为需要人工介入。很多本地推理框架比如 vLLM提供了 Prefix Caching 功能能缓存相同前缀的 prefill 结果。工作台特意把所有会话的系统提示词设计成由“公共部分 私有部分”组成公共部分放在最前面。这样 400 个会话的系统提示词前 80% 可能完全相同推理后端就能通过前缀缓存省下大量重复计算。这个优化在实测中带来的收益非常明显后面压测部分会提到。3.3 工具调用不要让工具阻塞整个循环真正把“聊天记录”变成“智能体工作台”的关键是工具调用。没有工具的会话只能复读有工具才能完成真实操作比如查数据库、抓网页、读写文件、调用内部接口。工具调用设计上我经历了一次重大返工。最初版本是把工具结果直接拼成普通字符串塞回历史然后让模型“自由理解”。结果小模型经常分不清哪一段是工具返回哪一段是真正需要回答的内容经常把工具输出当成自己要生成的答案看起来非常滑稽。后来我改成了结构化工具调用。模型返回一旦包含tool_calls工作台就解析出name和arguments把工具实际执行结果用独立的tool角色消息写回历史。这样大部分模型都能严格按照 OpenAI 定义的工具协议理解上下文。{ tool_calls: [ { id: call_001, type: function, function: { name: web_search, arguments: {\query\: \本地AI工作台并发优化\} } } ] }工具执行的超时需要在会话外统一控制。我给每个工具执行包了一层asyncio.wait_for超过 15 秒就返回一个工具超时错误而不是让整个会话卡住。毕竟某个工具挂了不应该拖垮其他 399 个会话。3.4 状态持久化全量记录在磁盘热数据在内存400 个会话如果把所有历史都放在内存里每个会话可能几千 tokens很快能吃掉几个 GB。更要命的是一旦进程崩溃这些数据全部消失前面的工作全白费。我的方案是采用双通道状态存储内存中只维护“最近活跃的 50 个会话”的完整历史其余会话从磁盘恢复每个会话每完成一轮就把追加的消息写入 JSONL 或 SQLite保证崩溃后能恢复最近状态。这里有一个务实的小技巧批量持久化而不是每轮每条都立刻写库。用一个异步队列把所有更新消息推给一个写入协程每秒批量 flush 一次。这样既降低了 I/O 压力也不会因为频繁写库拖慢整体节奏。class SessionStore: def __init__(self, db_path): self._queue asyncio.Queue() self._db_path db_path async def writer_loop(self): while True: batch [] while len(batch) 64: try: item self._queue.get_nowait() batch.append(item) except asyncio.QueueEmpty: break if batch: self._write_batch(batch) await asyncio.sleep(0.5) async def append_message(self, session_id, message): await self._queue.put((session_id, message))为什么用 SQLite 而不是更重的数据库因为大部分场景都是单机运行SQLite 零配置支持并发写队列写入已经足够稳定。真要做到多机部署把这段逻辑换成 Redis Stream 或 PostgreSQL 也不会太困难。4. 本地部署实操从零跑出一个能扛并发的工作台理论聊完实际操作有很多“坑”藏在不显眼的地方。我尽量把从硬件准备到服务启动的完整流程写清楚。4.1 硬件基线别一上来就盯显存很多教程只讲显存但实际上 400 个并发会话的瓶颈往往先到内存和磁盘。先说显存。跑一个 7B 到 14B 量级的模型建议至少准备 16GB 显存最好 24GB。低于这个规格也不是不能跑比如用 CPU 推理配合小模型但整体体验会明显下降。显存的主要消耗有三部分模型权重、KV Cache、推理过程里的临时激活值。权重是固定的KV Cache 会随着并发量上升而快速增长这也是为什么要控制并发信号量。然后是内存。400 个会话就算每个只保留 1 万字符的历史也要占掉 4MB×400 1.6GB。这还没算 Python 运行时的对象开销所以建议内存不低于 32GB。我实际跑 14B 模型加 400 个会话时内存占用大概在 12GB 到 18GB 之间波动。磁盘建议用 NVMe SSD。会话日志和检查点会持续写入机械硬盘扛不住高并发写入。如果说显存决定推理能不能跑那 SSD 决定的是长期运行会不会卡。4.2 模型推理后端怎么选Ollama、vLLM 还是其他我同时用过 Ollama 和 vLLM 跑同一套工作台两者定位不太一样。Ollama 胜在部署简单下载模型一条命令搞定对显存配置的自动优化也做得不错。很适合快速验证和 CPU 环境。但它在大规模并发上的控制能力比较有限需要靠外部环境变量调并行度。如果并发任务只有几十路Ollama 很省心。vLLM 适合真正上规模的场景它专门为高吞吐推理做过优化支持 Continuous Batching、Prefix Caching、PagedAttention。用同一块显卡vLLM 能支撑的并发请求数通常比 Ollama 更高。代价是部署复杂一些需要启动一个独立的 API 服务。我的经验是先拿 Ollama 跑通工作台功能再切到 vLLM 做压测。功能验证阶段不必纠结性能把工作台自身的逻辑调对切到 vLLM 后把主要精力放在调并发参数上。4.3 从配置文件到一次完整启动我假设你已经准备好了一台带 NVIDIA 显卡的 Linux 机器装了 Docker 和 NVIDIA Container Toolkit。第一步先启动一个 vLLM 服务加载一个中尺寸的对话模型作为示例docker run --runtime nvidia --gpus all \ -v ~/models:/models \ -p 8000:8000 \ vllm/vllm-openai:latest \ --model /models/qwen2.5-14b-instruct \ --max-model-len 32768 \ --gpu-memory-utilization 0.85 \ --enable-prefix-caching这里有几个参数很关键。--gpu-memory-utilization 0.85表示允许用到 85% 的显存剩下留给系统显示等用途一般不建议拉满到 0.98很容易触发 ECC 或驱动问题。--enable-prefix-caching开启前缀缓存在前文那套“公共前缀 私有后缀”的提示词设计下提升会非常明显。--max-model-len 32768限制了最大上下文长度同时也限制了 KV Cache 可能占用的空间如果显存紧张可以改小。如果没有 Docker用 pip 安装 vllm 后执行类似命令也可以。启动后先确认一下接口可用curl http://127.0.0.1:8000/v1/models看到模型列表后再启动工作台服务。工作台的配置要做到最少改动即可连接后端一般会在config.yaml里声明模型后端地址和 token 预算。启动后直接建一个测试会话发送第一条消息确认能正常收到回复。从实际经验来看第一次启动最容易出的问题不是代码而是网络地址写错。比如把 vLLM 的地址写成http://127.0.0.1:8000却忘了路径必须是/v1工作台发出请求后会收到 404。排查方法很简单先 curl 一下/v1/models能通再继续。4.4 配置 400 个会话并启动压测服务正常后就可以建大量会话了。我提供的压测脚本逻辑并不复杂循环 400 次每次创建一个会话并发送不同的启动 prompt然后用队列记录所有会话的回调结果。async def create_sessions_and_run(): results [] for i in range(400): session_id fstress-{i} results.append( create_and_run_session( session_idsession_id, starting_messagef第 {i} 个测试任务开始请简单介绍你自己并等我的下一步指令。 ) ) # 不急着同时启动分批并发创建 batch_results [] for start in range(0, 400, 20): batch_results.extend(await asyncio.gather(*results[start:start20])) return batch_results是的即使最终目标是 400 路我也不会一次性gather全部。一次创建 20 个会话等它们进入稳定状态后再创建下一批这样服务端不容易在刚开始时就打满资源。真正的高并发发生在后续轮次而不是建立会话的瞬间。5. 压测实录数据说话优化到底有没有效果上面这套系统不是一次调通的。我第一次跑 400 路并发时延迟惨不忍睹还频繁超时报错。后来通过持续调整才逐渐稳定下来。这里放一组有代表性的压测数据供同样想跑高并发的朋友参考。测试环境如下一张 24GB 显存的显卡vLLM 跑 14B 量化模型上下文长度设为 32K显存利用率 0.85开启前缀缓存。一共创建 400 个会话每个会话简单做 3 轮交互看整体完成情况。并发配置总耗时成功轮次超时/失败平均单轮延迟显存峰值信号量 64一次性全发8 分 20 秒1187132.4 秒21.8 GB信号量 32分批创建7 分 05 秒120001.7 秒19.2 GB信号量 32分批创建前缀缓存5 分 40 秒120001.2 秒18.5 GB信号量 16分批创建前缀缓存6 分 12 秒120000.9 秒17.4 GB这份表格能读出几个关键结论第一并发信号量不是越大越好。64 路看起来激进但一次涌入的 request 太多反而让部分请求排队等候时间变长最终总耗时更高。32 路在这个硬件条件下是一个比较甜的平衡点。第二前缀缓存在多会话场景下收益非常明显。同样 32 路并发开启前缀缓存后平均单轮延迟从 1.7 秒降到了 1.2 秒总耗时少了近一分半钟。这个优化不需要改任何业务代码只是配置项的问题。第三牺牲一点总吞吐能换更低的单轮延迟。16 路并发时单个请求的延迟更短但因为同时处理的会话数少了总完成时间反而更长。在实际业务里到底选哪一档要看目标更偏向“多长时间全部跑完”还是“单次交互多快返回”。对 400 路的定义也可以更严谨一点这里说的 400 个对话同时在线指的是系统里存在 400 个活跃会话它们的任务会被调度器分批推进。真正某一瞬间在处理推理的可能只有 16 到 32 个。对大多数业务场景来说这个在线数量已经非常够用用户不会关心后台是不是所有会话都在同一毫秒跑。6. 常见问题与排查技巧实录这里把我在开发和实际运行中印象最深的问题整理成一份速查表每个问题都给出排查路径。现象直接原因处理方式高并发下大量请求超时后端并发能力达到上限调低工作台的信号量并发数或升级显存显存 OOM 崩溃会话上下文太多KV Cache 暴涨缩短max-model-len减少每个会话保留的历史轮数多个会话回复内容互相串会话历史隔离失败检查每个会话是否通过唯一 session_id 访问历史列表日志中同一个历史出现在两个会话持久化写入顺序错乱保证写入逻辑按 session_id 加锁模型完全记不住前面内容滑动窗口裁剪太狠调大窗口轮数或引入摘要折叠工具调用返回乱格式小模型结构化能力弱降低 tools 数量缩短工具描述或在提示词里强调 JSON 格式启动后 curl 后端通但工作台不通base_url 路径缺失/v1检查模型后端地址是否带/v1/chat/completions400 会话跑一半服务重启没有开启持久化启用 JSONL/SQLite 写入启动时从磁盘恢复未完成任务6.1 一个问题案例并发一高就开始连环超时有一次我从 100 路加到 300 路时发现大量会话返回 504。最初以为是后端排队时间过长因为 vLLM 的默认队列策略确实会在请求过多时丢弃部分请求。排查时我先看了 vLLM 的日志发现并没有显存报错只有大量请求排队超时。这说明不是资源不够而是请求数量已经超出服务自身的队列能容忍的时限。解法主要是两条路把工作台的信号量从 64 降到 32同时在调用层设置更长的 HTTP 超时时间并把重试策略从“立刻重试”改成 5 秒后的指数退避。这么做之后系统不再出现一压就崩的情况。慢是慢一点但至少每个会话最终都能走完而不是连环失败后全部重来。6.2 一个容易忽略的点上下文裁剪会改变 token 计数滑动窗口的实现要特别小心 token 计数。最早我按照“保留最近 10 轮”来裁剪但没考虑到每轮消息长短差异很大。有的用户消息只有一行有的工具返回结果有几千行裁剪后的实际 token 数仍然可能超出模型的上下文窗口。后来我改成以 token 数作为裁剪依据而不是轮数。在代码里每次构建 prompt 之前调用一个快速的 tokenizer 统计长度超过预算就从头丢弃旧消息。这样虽然耗费一点点计算时间但稳定得多基本杜绝了“context overflow”错误。6.3 再一个容易忽略的点400 路压测时 Python asyncio 的默认限制很多用 Python 写异步程序的人会漏掉一个非常隐蔽的问题aiohttp默认的连接池大小是 100。即使你在代码里创建了 400 个协程同一时刻也只有 100 个连接能发出去其余协程会阻塞等待连接释放。这里有一个经验教训如果你发现并发始终突破不了某个数值先检查 HTTP 客户端连接池是不是成了隐藏瓶颈。使用aiohttp时可以通过自定义TCPConnector(limit0)把限制放开或者直接确认连接池参数大于预设的信号量值。connector aiohttp.TCPConnector(limit128) async with aiohttp.ClientSession(connectorconnector) as session: # 后续请求都会复用这个连接池我的信号量是 32 时连接池默认 100 就已经够用了但一旦把信号量调到 128 以上默认连接池就成了下一个瓶颈。这种问题从业务日志里看不出来因为所有任务都在“正常排队”只不过排队位置在连接池而不是你要处理的地方。6.4 状态恢复与优雅退出最后一个建议是给工作台加一个优雅退出机制。收到 SIGTERM 信号时先把所有运行中的会话状态落盘再停止新的调度最后退出进程。我早期的版本直接kill进程导致很多会话跑到一半状态丢失重启后无法继续只能全部重跑。对于 400 路的批量任务来说这种损失相当心疼。现在我在代码里注册了信号回调退出前会把所有会话的未完成历史写入数据库。下次启动时用户可以选择恢复这些会话继续跑。7. 开源后的一些经验感悟这个项目开源后我被问到最多的一个问题是“直接拿它去对接 OpenAI 兼容的云端服务不也行吗”确实可以因为协议层是通用的只是我从一开始把默认路径设成了本地推理后端。但如果你问我最想分享哪条经验我会说是“并发设计要克制”。在我验证过的场景里真正让系统变稳的不是堆硬件而是做好三层限制信号量限制同时打向模型的请求数连接池限制 HTTP 客户端的并发连接数上下文裁剪限制每个会话的 token 数。这三层限制都存在的时候400 路会话只是个规模数字少了任何一层它都会变成一场事故。另外开源项目最让我意外的收获是上下文剪裁策略被真实用户反过来推荐了更好的写法。以前我觉得保留最近 N 轮是最稳的方案后来有用户在实际运维里告诉我对结构化任务而言把第一轮的目标声明固定保留在上下文里比保留任意历史轮次都更重要。实践下来确实如此。很多小模型只要记得住最开始的任务目标哪怕中间细节被裁剪掉一部分最终结果也不会偏太远。如果你准备自己复刻一个类似的本地智能体工作台我会建议你先把“单会话跑通、多会话跑稳、批量会话跑快”三个目标分开验证不要一上来就追求 400 路的极限。单会话阶段把工具调用和上下文拼接调对多会话阶段检查状态隔离和并发控制最后再考虑规模和压测。一步一步来这个项目带来的不仅是运行 400 个会话的技术能力更是一整套关于高并发状态化任务管理的工程思路。