资讯动态

本地优先架构:AI助手7x24小时稳定运行的实战指南

发布时间:2026/10/19 15:41:30 来源:尧图企业网站定制
把AI助手接到第三方API上三分钟就能跑通但想让它7x24小时无人值守地稳定运行我折腾了整整两个月。最惨的一次凌晨四点被告警吵醒某第三方API服务异常我的机器人一晚上全部卡在重试循环里不仅任务全废还白白烧掉了大半个月的额度。那之后我把整套架构推倒重来核心原则就四个字本地优先——本地跑得动的坚决不外出Ollama做主力推理引擎第三方API只做降级外援中间加了一个我自己写的调度程序代号Clawdbot由它决定每个任务走本地还是走外援。这套系统从搭建到现在已经连续稳定运行了一个多月运行期间我没再半夜爬起来救火。下面把设计思路、配置参数和踩过的坑完整捋一遍给也想自建“长期在线AI助手”的朋友做参考。这套东西并不高深但能演示和能长期跑之间隔着大量工程细节。文章里涉及选型、性能底线、任务调度、故障恢复和实测数据全程按实际可复现的方式来讲。1. 第三方API当主食的教训为什么一定要本地优先1.1 那次凌晨四点爬起来改接口的经历最早一版方案很简单机器人收到任务直接调第三方API传提示词等结果返回。跑demo时一切正常响应质量也不错。但上线一周后就出事了。那天半夜上游服务开始不稳定表现为超时和偶发5xx。我的代码没有设置超时时间用的是无限重试逻辑——失败就重试重试再失败就再重试。结果一夜之间所有任务像多米诺骨牌一样堆在队列里每次重试都在真实消耗token额度。早上起来一看任务全废当日额度直接见底用户侧收到一堆重复消息。那次事故给我留下三个刻在脑子里的教训外部依赖必须假设它会挂超时和失败处理是基本盘不是可选配置。重试必须有限次且带退避策略。无限重试等于把成本账户交给对方决定。核心服务的命脉不能完全押在不受自己控制的服务上。API可以外包但控制权必须捏在自己手里。1.2 本地优先不等于离线三层架构怎么搭很多人听到本地优先就以为是完全断网、与世隔绝其实不是。本地优先的准确定义是默认使用本地推理能力只有当本地明确搞不定时才按需调用第三方API。这是一种架构策略不是技术洁癖。重构之后我用了三层结构调度层Clawdbot常驻进程负责从队列取任务、判断任务类型、路由到本地还是外援、记录任务状态、调用外部工具。它相当于公司前台所有请求先进这里。推理层主力是Ollama承载的本地模型第三方API作为冷备外援。相当于后厨本地厨师负责日常菜贵宾来了才请外援大厨。服务层SQLite存任务和状态HTTP接口对外通知走webhook定时任务负责清理和备份。这是后勤物流保障整体转得动。每一层独立部署、独立守护。调度层挂了推理层还在推理层挂了服务层还能收任务进队列。某个环节出问题最坏情况是当前处理不了而不是整个系统瘫痪。1.3 哪些任务不能硬扛本地API外援的边界本地模型的客观能力天花板是存在的尤其是参数规模有限的模型。硬扛的结果是回复质量差、用户反复追问反而浪费更多算力。我用的判断标准很朴素重复、模式化、有明确格式要求的任务一律本地需要大量背景知识、复杂推理或最新知识的任务才考虑外援。本地主力处理意图识别、FAQ问答、文本分类、摘要、关键信息提取、JSON生成、工具调用参数构造。外援兜底处理复杂代码调试、长文档组合推理、需要前沿知识的问答、多步骤开放规划。实际跑下来九成以上的日常任务本地就能解决真正需要外援的情况远没有想象中多。这也是本地优先能成立的前提。2. Ollama部署与性能底线7x24的基础设施2.1 安装与守护让服务自己活下来环境背景是一台Linux服务器带一块消费级NVIDIA显卡这也是绝大多数自建者会用的配置。Ollama安装本身不复杂官方脚本执行完服务就自动起来了。装好后先确认服务状态再拉第一个模型# 查看ollama服务状态 systemctl status ollama # 拉取模型先拿一个7B级别起步足够 ollama pull 7B级别模型名 # 验证能否正常对话 ollama run 7B级别模型名有一个配置你必须检查Ollama默认只监听127.0.0.1这是对的。不要为了图方便改成0.0.0.0否则等于把裸的推理服务暴露到局域网甚至公网。代理调度进程在同一台机器上用本地回环地址互通就完全够了。Ollama装好之后自带systemd服务正常情况下开机自启、崩溃自动拉起。但有一个高频坑有人习惯手动跑ollama serve来验证如果同时systemd服务也在跑两个进程会抢同一个端口后面起来的那个报错前面那个看似正常实则配置对不上。所以所有验证都通过ollama命令行和systemd状态来做不要额外手启服务。2.2 模型选型与显存测算别被越大越好带偏模型选择和显存预算直接挂钩这里给一张我的实操对照表量化级别统一按Q4_K_M参数量级显存占用建议显卡响应速度参考适用场景7B/8B约4.5~5.5GB8GB以上快日常够用意图识别、分类、摘要、轻量对话13B/14B约8~9GB16~24GB中等质量优先的通用任务30B级别约18~20GB24GB以上偏慢离线高难度推理、复杂生成先说结论从能跑的最小档开始压测跑不动再升级不要一上来就追求大模型。7x24小时运行场景里稳定输出远比偶尔惊艳重要。模型一旦超过显存容量Ollama会退化成CPU内存推理单次响应几十秒甚至更久整个调度链路都会被拖死。量化等级也值得说一句。不是所有量化都能用我的习惯是Q4_K_M起步这是质量与显存占用的甜点区域。Q2级别虽然更省显存但生成质量崩得厉害在大模型上尤其明显。另外一些高分模型在满血精度下的表现在Q4量化后会有可感知的下降但作为7x24助理够用了。实测参考在16GB显存级别显卡上跑14B模型Q4量化下生成速度大约40~50 tokens/s单次200 token的回复约5秒跑7B模型则明显更快通常20~35 tokens/s以上。这个量级对绝大多数任务型请求是够用的。2.3 并发与上下文两个默认配置必须改Ollama有两个默认配置如果你按默认值直接跑7x24迟早踩坑。第一个是默认并发数为1第二个是默认上下文长度只有2048。单并发意味着同时只能处理一个请求第二个请求只能排队。短时间没什么感觉但助手跑起来之后同时进来几个任务的情况很常见。我用systemctl edit ollama在service里加上环境变量[Service] EnvironmentOLLAMA_NUM_PARALLEL4 EnvironmentOLLAMA_KEEP_ALIVE-1OLLAMA_NUM_PARALLEL4表示同一模型同时服务4个请求。OLLAMA_KEEP_ALIVE-1表示模型常驻显存不被自动释放。默认keep_alive是5分钟——如果长时间没有请求模型会被卸载下一次请求就要重新加载冷加载耗时可能十几秒甚至几十秒在无人值守场景非常致命。上下文长度也需要调。2048的上下文对现代对话应用太短聊几轮就开始截断。一般是把num_ctx调到8192或16384。在模型调用层面传参ollama run 模型名 --num_ctx 8192这里必须提醒并发和上下文都是显存大户。上下文从2048涨到8192KV Cache占用的额外显存可能达到2GB以上4路并发同时跑长上下文显存消耗会明显增长。以14B模型Q4量化为例同时开4路并行、上下文1638416GB显存已经非常紧张建议用8192上下文压住。调完参数要做压测。我写了一个简单脚本用并发请求去捅本地Ollama记录响应时延# 10个并发请求观察耗时分布 for i in $(seq 1 10); do curl -s http://127.0.0.1:11434/api/generate \ -d {model:模型名,prompt:用一句话介绍自己,stream:false} \ -o /dev/null -w %{time_total}\n done wait实测数据是单路请求约1.8秒4路并发时单路约2.5秒10路并发时开始明显排队。所以我把并行数定格在4这个值在16GB显存下不会把显存打爆又能保证多任务同时处理时不至于堵死。3. 调度器Clawdbot任务路由、降级与成本闸门3.1 调度循环与任务状态机调度器是整个系统的中枢本质是一个常驻Python进程。它的核心不是模型而是任务状态管理。任务先落库再被拿出来处理完成之后把结果写回。数据表字段包括任务ID、类型、载荷、状态、重试次数、创建时间、完成时间、路由标记本地还是外援。状态机五态pending - running - done / failed / dead。用SQLite而不是纯内存队列原因很实际无人值守意味着进程随时可能被重启如果任务状态全在内存里重启一次就全乱了。落库之后进程重启可以从pending状态继续捡任务做到秒级恢复。核心循环大概长这样import sqlite3, time, ollama_client, api_client def pop_pending_task(conn): cur conn.execute( SELECT id, type, payload FROM tasks WHERE statuspending ORDER BY created_at LIMIT 1 ) row cur.fetchone() if row: conn.execute(UPDATE tasks SET statusrunning WHERE id?, (row[0],)) conn.commit() return row def route_task(task): if should_use_local(task): return ollama_client.generate(task[payload]) else: return api_client.generate_with_budget(task[payload]) def main(): conn sqlite3.connect(/opt/clawdbot/data/tasks.db) while True: task pop_pending_task(conn) if task is None: time.sleep(1) continue try: result route_task(task) save_result(conn, task[0], result, statusdone) except Exception as e: retry_or_dead(conn, task[0], e)调度进程本身用systemd守护后面第4章会详细展开。这里先强调一点能落库的事就不要留在内存里这是无人值守的第一原则。3.2 本地失败后如何优雅切换到第三方API本地优先不等于绝不外援。Ollama也会挂比如OOM、模型未加载、显存不足、超时等等。关键是切换要优雅不能一刀切。我的降级策略是一张决策表场景动作本地成功返回结果记录routelocal本地超时/报错丢弃本次请求标记retry1等待下次轮询重试重试仍失败检查预算闸门通过后转第三方API第三方API也失败任务进入dead队列触发告警等待人工处理预算超额禁止外援只用本地降级响应任务标记budget_exceeded这里有几个细节值得展开。重试必须幂等每个任务有唯一任务ID重试前先检查该任务有没有已经执行过的副作用操作。发消息、写文件这类动作尤其危险——重复执行等于给用户发两遍同样的内容。我的做法是有副作用的操作单独记录在effects表里执行前先查这个表。外援请求必须有硬超时。我设置的是60秒超过直接放弃。你可能会想模型还在生成再等等就出来了——在无人值守场景等待的代价是任务队列持续积压代价远大于超时丢弃。另外调度器会为每个任务记录route字段。这样事后统计本地承接率、外援占比、外援成本都非常方便不用猜哪个请求烧了多少额度。3.3 预算控制与缓存让7x24不烧钱第三方API一旦成为日常依赖成本失控只是时间问题。我第一次事故就是重试逻辑把额度打爆。所以后来在调度器里加了一个预算闸门所有外援请求都必须先过闸门def check_budget(requested_tokens): now time.time() hour_used get_usage(hour, now) day_used get_usage(day, now) if hour_used requested_tokens HOUR_BUDGET: return False if day_used requested_tokens DAY_BUDGET: return False return True预算有三级单次请求最大token数、每小时总token数、每天总token数。任何一条超了外援请求直接拒绝任务降级到本地模型处理。除了预算闸门我还加了结果缓存。大量请求是重复的或者高度相似的比如用户反复问同一个FAQ。简单hash输入文本命中缓存就直接返回历史结果既省时间又省钱。缓存命中率实测有三成左右。这些机制加起来的效果是成本从每天看账单才知道变成每个任务之前就能预估外援占比被压到6%以内剩下的94%全由本地承担电费可忽略不计。3.4 上下文管理的取舍7x24小时运行还有一个隐形杀手上下文无限膨胀。长期对话如果所有历史都堆进上下文显存、延迟、费用三方面同时爆炸。必须在调度层做主动管理。我用的是滑动窗口摘要归档方案保留最近10轮完整对话这是模型能直接看到的部分。每累计10轮调用本地模型把更早的对话压缩成一段摘要存进SQLite。新请求构建上下文时只带摘要 最近10轮而不是从头到尾全带上。识别到对话主题切换时强制归档当前上下文并重新开始避免A话题的内容污染B话题的上下文。这个方案不需要向量数据库不需要额外组件一个字段、一段压缩逻辑就够。上过RAG之后你会发现大多数轻量场景用摘要最近窗口已经能解决上下文管理的问题过度设计往往带来更多维护负担。4. 无人值守的工程细节守护、日志与自愈4.1 systemd守护与健康检查调度进程Clawdbot用systemd托管这是它半夜不会躺平的基础保障。完整的unit文件[Unit] DescriptionClawdbot Agent Afternetwork-online.target ollama.service Wantsnetwork-online.target [Service] Userdev Groupdev WorkingDirectory/opt/clawdbot ExecStart/usr/bin/python3 /opt/clawdbot/main.py Restartalways RestartSec5 EnvironmentOLLAMA_HOSThttp://127.0.0.1:11434 EnvironmentCLAWDBOT_ENVprod NoNewPrivilegestrue [Install] WantedBymulti-user.targetRestartalways保证进程退出后5秒自动拉起。Afterollama.service保证Ollama起来之后调度器才启动避免启动顺序错乱。光有进程守护还不够还要有健康检查。我的做法是给调度器加一个/healthz接口定时的检查脚本每隔30秒请求一次连续3次失败就触发重启#!/bin/bash FAILED0 for i in $(seq 1 3); do if curl -sf http://127.0.0.1:8000/healthz /dev/null; then FAILED0 break else FAILED$((FAILED 1)) sleep 10 fi done if [ $FAILED -ge 3 ]; then systemctl restart clawdbot fi这个健康检查不能只看进程活着。我的/healthz会检查两件事进程状态和队列积压数。如果队列积压超过阈值说明调度线程卡死了进程看似活着实际不干活一样要重启。这是很多人会忽略的假死场景。定时执行健康检查用cron就行不复杂*/1 * * * * /usr/local/bin/healthcheck.sh /var/log/healthcheck.log 214.2 结构化日志与告警通知无人值守最怕的是出了问题不知道。日志是故障排查的第一线索但如果日志是散落的print字符串出问题时会非常痛苦。我统一改用JSON行日志每条一行结构化字段{ts:2025-01-15T10:00:00Z,level:info,task_id:a3f2c1,model:local,latency_ms:2100,tokens:420,route:local,result:ok}好处很直接grep按字段搜按任务ID聚合全链路日志统计平均时延就不用再写正则去抠字符串。配合jq可以秒级做数据分析。日志轮转也必须配否则JSON日志以每天几百MB的速度增长几天就能把磁盘写满。logrotate配置/var/log/clawdbot/*.log { daily rotate 14 compress delaycompress missingok notifempty sharedscripts }告警通知走webhook调度器内置一个简单的notify()函数触发条件是失败率超过5%持续10分钟、dead队列非空、磁盘使用率超80%、预算闸门触发外援拒绝。特别要注意防告警风暴同一事件10分钟内只发一次用简单的字典去重否则半夜会被几十条重复告警轰炸。4.3 状态备份与磁盘空间管理SQLite作为任务数据库要在高并发下长期稳定运行必须开启WAL模式PRAGMA journal_modeWAL;WAL模式允许读和写并发执行不会出现database is locked的可怕报错。备份直接用SQLite自带的.backup命令每天定时全量备份到独立磁盘保留7天sqlite3 /opt/clawdbot/data/tasks.db .backup /backup/tasks_$(date %F).db磁盘空间管理是经验教训Ollama模型文件、日志、数据库分开评估。模型文件是好几个GB的固定占用日志会缓慢增长数据库相对平稳。我建议日志和数据盘分开挂载否则日志把数据盘写满数据库直接不可写整个调度链路就瘫痪了。再加上一个简单的磁盘水位检查命令超阈值就触发告警df -h / /opt /var/log | awk NR1 {if ($50 80) print 磁盘告警, $0}5. 连续运行一个月的实测数据与故障速查5.1 资源水位与响应时延实测这套系统连续运行了32天把关键数据整理成一张表给想入坑的朋友一个量级参考指标实测值连续运行天数32天总请求量81237次平均本地响应时延2.3秒本地承接率94.2%第三方外援占比5.8%整体失败率0.6%最大单日请求量5680次平均显存占用11.6GB响应时延分布p50约1.8秒p95约4.2秒p99约9.6秒。也就是说绝大部分请求在2秒左右返回偶尔的长请求也不会超过10秒。对7x24运行场景来说这个时延完全可以接受。本地承接率94.2%这个数据很关键。它说明日常任务的真实构成里绝大多数并不是非大模型不可的高难度请求。把这类请求留在本地把6%不到的复杂请求外援既能保证质量又能把成本压到可以忽略的程度。5.2 高发故障与处理一览表一个月跑下来遇到的故障不算多但每个都值得记录。整理成速查表后续再遇到能秒级定位现象根因处理Ollama进程被杀OOM显存不够或并发偏高降低OLLAMA_NUM_PARALLEL或换更小模型systemd自动拉起半夜请求全部超时模型被释放冷加载耗时OLLAMA_KEEP_ALIVE-1常驻显存磁盘空间爆满JSON日志未轮转配置logrotate日志独立挂载第三方API成本超标无限重试有限重试预算闸门dead队列队列积压但不消费调度进程假死进程活着没干活/healthz检查队列深度超阈值重启SQLite报database is locked未开WAL或并发写冲突开启WAL保持单写者模式有一个故障之前没提单独说Ollama进程被系统OOM Killer杀掉时systemd会立刻把它拉起来但模型需要重新加载显存需要重新分配。如果此时调度器没有做好超时控制一批请求会同时打进来形成新一轮OOM。所以OOM之后的重启不是终点必须同时降低并发或减少上下文长度否则会陷入重启-满载-再被杀的循环。5.3 我保留的三连重启应急能力哪怕前面的机制已经很完善我还是保留了三个应急手段并定期演练。管这叫三连重启第一板斧systemctl restart clawdbot。大部分假死、卡顿问题一轮重启就解决了。重要的是系统要具备自动拉起能力而不是依赖我半夜爬起来敲命令。第二板斧一键切换纯本地模式。有个开关关掉所有第三方API调用系统只用本地模型应答。即使外部API完全不可用核心服务也不会中断只是复杂任务返回降级提示。第三板斧冷备脚本。一个独立于Clawdbot的最简Python脚本直接读队列、直接调Ollama、直接回复。当主调度进程复杂到短期排查不了问题时用这个脚本顶着保证助手还在线。这几套能力每两周手动演练一次。不要等到真出事才发现冷备脚本里有语法错误或者切换开关根本没生效——这种事我干过很狼狈。折腾这套系统这么久我最大的体会是让AI助手7x24小时稳定运行真正的难点既不在模型选型也不在API对接而在工程细节。你要把每一个能想到的故障场景提前试一遍把每一次重启、切换、备份都变成肌肉记忆。最后再提一个建议如果你刚开始搞不要急着接第三方API。把Ollama一个模型、一个调度进程、一个健康检查脚本这条最简链路先跑上三天确认重启、日志、降级、备份都正常再逐步加外援和高级功能。无人值守的本质不是智能而是确定性。这套本地优先架构的价值正是在于把不确定性挡在系统之外。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑