☰
把AI网关装进华硕路由器:家庭边缘算力的提示流编排实战
2026/11/2 4:52:49 网站建设 项目流程

把 AI 引擎塞进华硕路由器,听起来像整活,其实是在解决一个很现实的麻烦:家里每台设备都想调用 AI,手机、NAS、Home Assistant、智能音箱,各自直连不同的模型 API,提示词模板乱成一锅粥,鉴权和限流更是没人管。我干脆在 Merlin 固件上做了一个 AI 提示流编排器,把它定位成整个家庭的轻量边缘网关,让所有内网设备统一走一个入口。这篇文章是开源系列的第 12 篇,完整记录我从 0 到 1 打造这套系统的过程:包括提示流编排器的设计、Merlin 插件如何把 Python 服务常驻在路由器上、网关层怎么做鉴权和模型路由,以及我在 GT-AX6000 上实测踩到的一堆坑。

1. 为什么把 AI 网关放在路由器:家庭边缘算力的天然枢纽

1.1 一件很别扭的事逼我动手

我家里的智能设备不算多,但 AI 调用场景很分散:NAS 上跑着一个定时脚本,让大模型帮忙整理下载目录的杂乱文件名;Home Assistant 想用自然语言控制灯光,得单独接一个对话 Agent;我自己写的几个小工具,每个都维护一套 API Key 和 prompt。

真正让我动手的是一次“接口事故”:某个脚本的天级 key 过期了,一整晚的监控日志没有按时归纳,我第二天早上才发现。把 key 分散在十几个脚本里,想统一换就得到处改,出问题也没法从全局视角排查。当时我就在想,既然家里有一台 7x24 小时不关机、总是处于网络要道上的设备——路由器,为什么不把 AI 能力收编到它上面?

这个想法一开始也不确定靠不靠谱。路由器在大多数人心里的形象是“能上网就行”,和 AI 扯上关系未免太勉强。但仔细想一遍架构后,我意识到这不是把大模型塞进路由器,而是把“AI 请求的调度和治理”长在路由器上。这个区别非常关键。

1.2 路由器的三个独特优势

第一,位置优势。路由器是内网设备访问互联网的必经点,所有设备本来就要经过它。做一个内网 AI 网关,设备不需要装 Agent,不需要改网络配置,只要把请求的地址指向192.168.1.1:8088就行。第二,常驻优势。家庭服务器有人会关机,笔记本会合盖,路由器几乎不会主动关。AI 网关的价值在于“随时在线”,放在路由器上天然满足。第三,生态优势。华硕路Merlin固件提供了 jffs 分区、entware 软件源、用户脚本钩子,能让我用自己的脚本和 Python 程序在固件上长期存在,而不是刷一个一次性工具就完事。

这套组合下来,路由器不再只是转包网络数据的硬件,而是一个能承载轻量边缘计算的家庭基础设施节点。

1.3 边界:路由器不是用来跑推理大模型的

把 AI 引擎塞进路由器,不等于在路由器上跑一个 7B 模型。GT-AX6000 有 1GB 内存、四核 2.0GHz 处理器,这在路由器里算高配,但和大模型推理服务器完全不是一个量级。我的设计里,路由器主要负责三件事:接收请求、按提示流编排顺序调用不同后端、把结果整理后返回。

真正的大模型推理放在局域网里的 GPU 主机或者云端 API。路由器只做编排和网关,也就是“AI 引擎的管理者”,而不是“AI 引擎本身”。这样既保留了边缘网关的低延迟和集中管理价值,又不会因为资源限制把系统压垮。如果有极简的本地小模型需求,比如 0.5B 的量化分类模型,也可以临时加载,但要把它当成“偶尔用一次的慢速后端”,而不是常驻服务。

2. 提示流编排器:把杂乱的请求变成可控流水线

2.1 四个核心单元:入口、模板、路由、出口

提示流编排器这个名字听起来复杂,拆开看就是一条处理流水线。我把它抽象成四个单元:入口接收原始请求,模板负责把请求和系统上下文拼成最终提示词,路由决定请求交给哪个模型后端,出口做结果清洗和格式化。

入口不关心请求来自哪个设备,只把它抽象成一个 JSON 上下文对象。模板单元用类似 Jinja2 的语法,把路由器状态、时间、用户输入等变量填进预设的模板里。路由单元按规则选择后端,比如“短文本分类走本地小模型”“长文档摘要走局域网 GPU 主机”“复杂指令走云端 API”。出口单元把模型返回的原始内容做后处理,比如抽取 JSON 字段、转成 Markdown、截断超长文本。

这四步串起来就是一条 Flow。一个 Flow 可以只包含“模板→模型→输出”,也可以包含多个模型调用和工具调用。比如一次网络巡检报告,先调用一个工具读取路由器状态,再让模型根据这些数据生成说明文本。

2.2 一个具体流程:路由器状态巡检自动报告

我最早上线的一个 Flow 是“每日网络报告”。流程是这样的:每天早上 8 点,路由器上的 cron 触发网关的/v1/flows/report接口,编排器收集 WAN 连接时间、CPU 温度、内存占用、每个设备的历史连接信息,填充到模板里,然后调用云端 API 生成一段通俗的巡检总结,最后推送到 Home Assistant 的仪表盘。

这个流程的价值不是“让大模型生成一段话”,而是把原本分散在脚本、API Key、手动检查里的零散操作,固化成了一个可复用的服务。换个设备想获取同样报告,不需要复制脚本,只要向网关发起同样请求就行。

模板的一个关键细节是:不要在请求里让用户直接写提示词。设备端只传结构化数据,提示词完全由路由器上维护。这样前端再乱,后端模板是受控的,不会出现“注入”问题,也方便统一升级提示词。我开始时图省事,允许请求里带prompt字段,后来一个脚本误传了超长内容,直接撑爆了请求体,我才改成白名单模板模式。

2.3 为什么不用现成的 Chain 或 Agent 框架

市面上有 LangChain、Dify、n8n 这类框架,功能很全,但我最后没有在路由器上跑它们。原因很简单:路由器环境太苛刻。这些框架要装一堆依赖,动辄几百 MB,在 1GB 内存的设备上启动后,留给请求处理和日志的余量就很小了。

自己写一个编排器其实不难,关键只是几个接口:加载 Flow 定义、执行步骤、调度后端、记录日志。我只需要保持代码足够简洁,让 flow 的定义更像数据,而不是代码。

我用一个 JSON 文件描述 Flow,例如report这个流程长这样:

{ "flows": { "report": { "steps": [ { "id": "collect_status", "type": "tool", "name": "router_status" }, { "id": "build_prompt", "type": "template", "template": "一、网络状态概览:{{network|safe}}\n二、设备列表:{{devices|safe}}\n请生成简短的巡检总结。" }, { "id": "call_model", "type": "llm", "backend": "cloud_api", "temperature": 0.3 }, { "id": "format_md", "type": "stringify", "format": "markdown" } ] } } }

执行器按顺序跑,每步的输入来自上一步的输出和全局上下文的合并。这种写法没有复杂的依赖关系,却能覆盖我 80% 的使用场景。

3. Merlin 插件实战:让 Python 服务在固件里安全常驻

3.1 环境准备三板斧:jffs、entware、swap

要在华硕路由器上跑自研服务,首先要刷 Merlin 固件。华硕官固虽然也能开 SSH,但用户可写分区和软件源基本不可用,Merlin 开放了/jffs分区和脚本钩子,这是整个插件体系的地基。

第一步是开启 jffs 分区。在路由器后台的“Tools → System Settings”里勾选 Enable JFFS partition。jffs 是路由器上持久化的可写区域,固件升级后通常会被清空,所以它只放插件本体和配置,不做日志和缓存。

第二步是装 entware。entware 是嵌入式设备的软件包管理器,通过install_entware脚本一键装到/opt,装完之后用/opt/bin/opkg install python3 python3-pip就能获得完整的 Python 环境。这一步要注意,entware 默认可能不启用 swap,但对 1GB 内存的路由器来说,swap 是个保命手段。建议在安装 entware 后用swapon挂一个交换文件,文件放在 U 盘上比放在 jffs 上合理——jffs 写入次数有限,频繁交换会缩短寿命。

3.2 插件生命周期与 WebGUI 挂载

Merlin 的用户脚本钩子都在/jffs/scripts/下,比较常用的是services-start、post-mount、nat-start。我的插件把主程序放在/jffs/addons/ai-gw/,启动脚本也被调用:

# /jffs/scripts/services-start # 在系统服务启动后运行 /jffs/addons/ai-gw/ai-gw.sh start

ai-gw.sh负责 start、stop、restart、watchdog 四个动作,用 PID 文件管理进程。启动时后台拉起 uvicorn 和 FastAPI 服务,监听0.0.0.0:8088。

WebGUI 挂载是另一个需求。Merlin 的定义页面是 ASP 文件,我的做法是把一个简洁的ai_gw.asp放到/jffs/addons/ai-gw/,然后通过 JavaScript 向管理页面的菜单数组里插入“AI Gateway”入口。这个操作不需要改固件核心文件,升级修复通常不会受影响。页面里可以做三件事:查看 Flow 运行日志、编辑 Flow 配置、测试某个 Flow 是否正常。

3.3 进程守护、开机启动与看门狗

路由器偶尔会重启,服务必须能自动恢复。我把启动命令写进 services-start 后,重启基本不用管。但进程运行中如果因为内存压力被杀掉,光靠启动钩子是不够的,需要看门狗。我用了 Merlin 自带的 cru 命令,每 5 分钟检查一次 PID:

cru add aigw-watchdog "*/5 * * * * /jffs/addons/ai-gw/ai-gw.sh watchdog"

watchdog 逻辑很简单:如果 PID 文件里的进程不存在,就重新拉起来;如果存在但响应超时,就杀掉重启。这套机制看着土,在路由器环境里比任何微服务框架都可靠。

4. 轻量边缘网关层:统一 API、鉴权、限流与可观测

4.1 网关与编排器的分工

编排器解决的是“一个请求怎么被处理”,网关解决的是“谁有权限请求”。两者容易混为一谈,但拆开更清晰。网关层是一个 HTTP API,负责鉴权、限流、请求日志;编排器是网关内部的服务核心,负责执行 Flow。

内网设备调用时,请求先到网关层,检查请求头里的 API Key,再检查这个 Key 一分钟内已经用了多少次。如果 key 无效,返回 401;如果超了频次,返回 429;通过后,网关把请求体里的上下文提取出来,交给编排器执行对应 Flow。

这套设计在家庭环境中看起来有点“过度”,但一旦设备多了,效果立刻体现出来:某个 NAS 脚本写了个死循环几分钟内刷了上百次模型调用,限流直接把它挡在门外,模型账单没被刷爆。

4.2 FastAPI 核心代码骨架

我选择了 FastAPI,但走了个弯路,这个后面避坑部分细说。网关的核心代码并不长:

# app.py from fastapi import FastAPI, Request, HTTPException from flow_engine import RunFlow app = FastAPI() API_KEYS = { "homeassistant_key": "home_assistant", "nas_key": "nas_script", } @app.post("/v1/flows/{flow_name}") async def run_flow(flow_name: str, request: Request): token = request.headers.get("Authorization", "").removeprefix("Bearer ") if token not in API_KEYS: raise HTTPException(401, "invalid api key") body = await request.json() context = body.get("context", {}) context["_source_device"] = API_KEYS[token] result = await RunFlow(flow_name, context) return {"ok": True, "result": result}

生产环境里我加了限流依赖,用一个全局字典记录每个 key 的调用时间戳,窗口内超过阈值就拒绝。字典放内存就行,不用 Redis,路由器上跑 Redis 太奢侈了。

4.3 多模型后端配平与降级策略

模型后端我用一个统一接口抽象,每个后端都是“传入 messages,返回文本”。云端 API 是最常用的后端,局域网内的 GPU 主机用 Ollama 暴露兼容接口,本地小模型单独封装为一个慢速后端。

后端规划可以参考这个表:

后端类型部署位置典型用途单请求延迟稳定性
云端 API远程复杂生成、长文档摘要、对话1 到 3 秒依赖公网
局域网 GPUNAS 或台式机私有数据理解、代码补全1 到 4 秒高
本地小模型路由器极简分类、意图识别8 到 15 秒低,按需加载

我还在网关里加了降级策略:主后端请求失败或超时,自动切换备用后端,并在返回结果里加一个_backend_used字段,方便判断实际走了哪条路。

5. 在 GT-AX6000 上压测:数据、玄学与调优顺序

5.1 资源占用实测

我的测试设备是华硕 GT-AX6000,双 2.5G 网口,四核 2.0GHz,1GB RAM。上一节提到的基础网关,在空闲状态下内存占用大约 48MB,CPU 几乎为 0。一次云端 API 调用的过程里,内存峰值大约增加 5MB,CPU 短时间冲到 6% 左右。如果同时触发路由器状态采集和模型调用,峰值能到 20% 到 30%。

这么说吧,网关本身的开销完全可以忽略,真正的瓶颈不在路由器,而是模型后端和请求体大小。我把单次请求体限制在 256KB 以内,防止某个设备传一个巨大的历史记录把内存打满。

5.2 内存不够怎么办:swap、精简依赖、控制并发

1GB 内存听着够用,但路由器本身还要跑各种系统服务,真正空闲内存只有几百 MB。第一次压测时我开了 8 个并发请求,其中 3 个请求需要同时统计设备列表,内存直接飙到 900MB,随后内核开始杀进程。

之后我做了三件事:第一,挂 swap 文件,放 U 盘,1GB 大小;第二,给网关加信号量,限制同时执行的 Flow 数量不超过 4 个;第三,把不必要的依赖全砍掉,能用标准库的就不用第三方包。FastAPI 的异步事件循环在并发场景下很合适,但如果是同步阻塞的函数,比如读取路由器状态时要执行 nvram 命令,我就会把它丢进线程池执行,避免阻塞事件循环导致请求排队假死。

5.3 网络侧调优:超时、连接池、局域网延迟

内网调用网关通常延迟在 1 到 2 毫秒,真正耗时的是模型后端。云端 API 的 HTTP 连接如果每次都新建,在高频场景下握手开销很大,所以我用httpx.AsyncClient做了连接池,并设置超时:连接超时 5 秒,读取超时 30 秒。

一个容易被忽略的参数是 DNS 解析超时。路由器上的 DNS 如果偶尔慢一下,请求会卡在解析阶段。我在 app 启动时就把云端 API 的 IP 固定缓存,避免每次请求都做 DNS 查询。这招在高并发时很有效。

6. 从 jffs 写满到进程暴毙:我记下的六笔账

6.1 jffs 写满之后 WebUI 罢工

第一次上线没几天,路由器后台页面打开变慢,甚至空白。我第一反应是插件有问题,毕竟刚装上。后来 SSH 进去才发现/jffs使用率已经到了 99%,原来是 Python 运行时产生的缓存和 uvicorn 请求日志全部落在 jffs 上。

排查链路很简单:df -h看分区占用,du -sh /jffs/*逐层找大文件,最后定位到__pycache__和日志文件。修复方案是把PYTHONDONTWRITEBYTECODE=1加到环境变量,所有日志重定向到/tmp,并且套一个tail -c 1M的日志截断命令。只要服务重启或者路由器重启,/tmp会被清空,不用担心日志无限增长。

6.2 entware 里的 Python 版本卡住依赖

我在路由器上pip install fastapi时遇到了最烦的问题:pydantic v2 需要编译 Rust 扩展,而 entware 环境里没有 Rust 工具链,pip 直接报错。折腾一晚后,解法是把 fastapi 锁在老版本,pydantic 也锁在 1.x:

pip install "fastapi<0.100" "pydantic<2" uvicorn httpx

老版本能力完全够用。所以如果你也要复刻,先不用追求最新版本,先跑通更重要。

6.3 固件升级不丢配置的黑魔法

Merlin 固件升级时,jffs 内容有可能丢。官方说法是“不保证保留”,实际升级有时保留,有时清空,完全看具体版本。吃了两次亏后,我写了备份脚本,升级前跑一下,打包整个插件目录和配置文件到 U 盘和局域网共享目录:

tar czf /mnt/usb/backup/ai-gw-$(date +%Y%m%d).tgz -C /jffs/addons ai-gw

升级后如果没有了,就直接解压恢复。这套操作比想象中可靠,现在每次升级固件前我都固定执行。

6.4 防火墙重启清掉自定义规则

做网关时,我一开始想给外网访问留一个口子,于是把端口转发的规则手动加到了系统 iptables 里。后来网页上随便点了一下防火墙重启,规则就被清掉了,服务从外网彻底失联。

这个坑的教训是:只要涉及防火墙规则,就不要手工iptables临时添加,而是把规则的挂载逻辑放到/jffs/scripts/nat-start钩子里,让系统启动和防火墙重启时都自动执行。不过对大多数用户,我反而建议别把 AI Gateway 暴露到公网,内网设备通过局域网 IP 调用已经完全够用,外网接口只会增加被扫描和滥用的风险。

6.5 并发请求把内存打爆的假死现场

最早版本没有并发控制,我在本地写了个循环脚本连续 20 个请求模拟家里设备群爆发。结果到第 11 个请求时,进程还在响应,但速度急剧下降,最后 SSH 指令都要等好几秒才能回显。这是内存压力导致系统在做 swap 交换,CPU 都在等磁盘 IO。

排查方式比较简单:登录路由器看free -m,swap 使用率接近 100%,再看进程的 RSS,确认是主进程膨胀。修复就是前面说的信号量控制 + 限制请求体大小。代码层面:

import asyncio sem = asyncio.Semaphore(4) async def run_with_limit(flow_name, context): async with sem: return await RunFlow(flow_name, context)

这半行代码,比任何参数调优都管用。

6.6 进程死了没人管

有一次我手动 kill 掉 uvicorn 进程,忘记重启,第二天看 Home Assistant 仪表盘,网络报告整个缺失。原因不是 bug,而是我没配看门狗。

后来把 watchdog 加到 cru 定时任务后,这个问题彻底消失。我甚至故意 kill 了几次进程测试,五分钟左右服务会自动拉起来,日常使用基本无感。

如果你打算长期跑这套系统,第一个要写好的不是功能,而是“万一挂了怎么办”的兜底逻辑。

6.7 路由器本身变卡时先分清是谁的责任

最后一条比较玄学。系统跑了一段时间后,路由器后台偶尔会变卡,很多人第一反应是 AI Gateway 有问题。但我仔细查了才发现,真正吃资源的是某个下载工具的离线任务,它把 CPU 跑满了。AI Gateway 本身的内存和 CPU 占用都有限,但在它上面花的排查时间一点都不冤枉。

我现在养成了一个习惯:在 WebGUI 页面里,除了 Flow 配置,还会显示一个“最近 5 分钟系统负载”迷你面板。这样一旦有人说“路由器卡了”,我能先看面板确认网关负载,再决定下一步从哪里查。

如果你也想复刻这套系统,我的建议是从一个最小闭环开始,先手动启动 Python 服务,跑通一个“模板+云端API”的 Flow,再挂 services-start 和看门狗。不要一上来就布置多模型后端和复杂 Flow,那样出了问题很难定位。路由器这个环境教给我的核心一件事:少即是多。能少装一个依赖,就少一个坑;能让流程数据化,就别写进代码里;能自动恢复,就别指望人记得手动重启。这套网关跑了大半年,已经成了我家智能系统里最不起眼但最稳定的一个环节。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询