1. 项目缘起与整体架构拆解把 AI 推理能力塞进一台家用路由器这个想法最早来自一个很现实的痛点家里和工作室里跑着好几台设备手机、平板、笔记本、各种智能终端想让它们都能用上大模型能力但又不希望每个设备都单独配一套 API Key、单独装客户端、单独维护对话历史。更关键的是很多本地设备根本没有能力跑模型而把数据全部往云端送延迟和隐私都是问题。于是我就琢磨能不能让路由器这个永远在线、全家共享的设备承担起 AI 请求的统一入口和调度角色。这个项目的核心目标很明确在 Merlin 固件的华硕路由器上跑一个轻量级的边缘网关把 AI 引擎的调用能力封装成标准接口让局域网内所有设备都能通过统一入口访问 AI 服务。整个方案由三部分组成Merlin 插件负责在路由器上部署和守护网关进程Go 语言编写的边缘网关负责请求编排、提示流管理和多引擎路由AI 引擎则可以是本地小模型也可以是远程大模型 API。选择 Go 作为网关开发语言理由很直接——编译产物是静态二进制体积小、无运行时依赖、交叉编译方便这对路由器这种资源受限环境几乎是刚需。为什么是 Merlin 而不是原厂固件因为 Merlin 保留了华硕原厂固件的稳定性同时开放了 JFFS 分区和自定义脚本入口可以挂载 U 盘扩展存储、开机自启自定义服务。原厂固件虽然也能通过一些方式跑脚本但可用的持久化存储和启动钩子非常有限稍微复杂一点的服务就撑不住。Merlin 的 JFFS2 分区加上 entware 包管理基本能把一台路由器变成一台微型 Linux 服务器这是整个方案能落地的前提。架构上我把它分成四层。最底层是硬件和固件层也就是华硕路由器加 Merlin 固件提供 CPU、内存、存储和网络基础。往上是运行环境层包括 JFFS 分区、entware、Go 运行时。再往上是网关服务层这是核心负责 HTTP 服务、提示流编排、引擎适配、会话管理。最上层是接入层局域网设备通过 HTTP 或 WebSocket 接入网关网关再根据配置把请求路由到不同的 AI 引擎。这个分层的好处是每一层职责清晰替换任何一层都不影响其他层比如以后想换一个更强的路由器或者想换一个不同的 AI 引擎都只需要动对应那一层。这里有个关键设计决策值得展开说为什么要在路由器上做编排而不是简单做转发如果只是转发那路由器就是个反向代理价值有限。编排意味着网关要理解请求的结构能根据提示词模板、上下文长度、引擎能力做动态路由。比如一个简单的问答请求可以走本地小模型一个需要长上下文和复杂推理的请求就走远程大模型。这种编排能力才是把路由器从通道变成大脑的关键。而且编排逻辑放在网关上所有接入设备都不需要关心后端到底用的是哪个引擎接口统一、体验一致。从影响范围来看这个方案适合几类人一是家里有多个智能设备、希望统一 AI 入口的技术爱好者二是做 IoT 或边缘计算开发、需要本地 AI 能力的开发者三是想学习边缘网关和 Go 网络编程的初学者。它不适合追求极致推理性能的场景毕竟路由器 CPU 算力有限本地只能跑很小的模型重活还是得靠远程引擎。但作为请求编排层和统一入口路由器的位置和在线时长优势是其他设备比不了的。2. 环境准备与 Merlin 插件机制详解2.1 路由器选型与固件刷写要点不是所有华硕路由器都适合跑这个方案选型时重点看三个指标闪存容量、内存大小和 CPU 架构。闪存至少要 128MB因为 Merlin 固件本身加上 JFFS 分区会占用不少空间内存建议 512MB 起步256MB 虽然能跑但会很紧张网关进程加上系统本身很容易触发 OOMCPU 架构优先选 ARM因为 Go 对 ARM 的交叉编译支持最成熟MIPS 虽然也能编译但部分依赖库支持不完整。我实测下来比较稳的几款是 RT-AX88U、RT-AX86U 和 GT-AX11000这几款都是 ARM 架构、内存充足、Merlin 支持完善。如果手头是较老的型号比如 RT-AC68U也能跑但要注意内存限制网关配置里要把并发数和缓存调小。刷 Merlin 固件的流程这里不展开官方有详细教程核心就是下载对应型号的固件文件通过路由器管理界面上传刷写刷完后在系统设置里启用 JFFS 分区和自定义脚本。注意刷固件前务必备份原厂配置刷写过程中绝对不能断电否则可能变砖。刷完后第一次启动会比较慢耐心等待。启用 JFFS 后路由器上会多出一个/jffs目录这是持久化存储区重启不丢失。但 JFFS 分区容量有限通常只有几十 MB所以大文件要放到外接 U 盘上。我的做法是在 U 盘上建一个/mnt/ai-gateway目录存放网关二进制和模型文件JFFS 里只放启动脚本和配置这样既保证持久化又不占用宝贵的内置存储。2.2 entware 安装与 Go 运行环境搭建Merlin 本身不带包管理器需要先装 entware。entware 是一个为嵌入式设备设计的轻量包管理系统安装方式是在路由器上插一个 U 盘然后通过 SSH 登录执行官方提供的一键安装脚本。安装完成后/opt目录会被挂载到 U 盘上所有通过 entware 安装的软件都放在这里。装好 entware 后Go 运行环境有两种方案。第一种是直接用 entware 里的 Go 包优点是安装简单缺点是版本可能偏旧。第二种是自己在开发机上交叉编译好静态二进制直接拷到路由器上运行这也是我推荐的方案。因为网关程序一旦编译成静态二进制路由器上根本不需要装 Go 环境省空间也省内存。交叉编译的命令大概是这样# 在开发机上执行目标平台是 ARM64 路由器 GOOSlinux GOARCHarm64 CGO_ENABLED0 go build -ldflags-s -w -o ai-gateway ./cmd/gateway这里几个参数都有讲究。CGO_ENABLED0是关键禁用 CGO 才能保证编译出纯静态二进制不依赖路由器上的任何 C 库。-ldflags-s -w是去掉符号表和调试信息能把二进制体积缩小 30% 左右对路由器存储很友好。编译出来的文件通常只有十几 MB拷到 U 盘上就能直接跑。2.3 Merlin 插件目录结构与启动钩子Merlin 的自定义插件机制核心是/jffs/scripts/目录下的一系列钩子脚本。最常用的是services-start这个脚本在系统服务启动完成后执行适合用来拉起我们的网关进程。还有services-stop用于停止服务wan-start在 WAN 口连接后触发firewall-start在防火墙规则加载后执行。我的插件目录结构是这样的/jffs/ ├── scripts/ │ ├── services-start # 启动网关 │ ├── services-stop # 停止网关 │ └── ai-gateway.sh # 网关管理脚本 ├── configs/ │ └── gateway.yaml # 网关配置 └── logs/ └── gateway.log # 运行日志services-start里做的事情很简单就是调用管理脚本启动网关并且加上延迟确保网络就绪#!/bin/sh sleep 30 /jffs/scripts/ai-gateway.sh start那个sleep 30是我踩过坑之后加的。早期版本没加延迟结果网关启动时 WAN 口还没完全就绪导致远程引擎的请求全部失败。后来加了 30 秒延迟稳定运行再没出过问题。管理脚本ai-gateway.sh负责进程的启动、停止、重启和状态检查用start-stop-daemon或者简单的nohup加 PID 文件都能实现。3. 轻量边缘网关的核心实现3.1 Go 网关的模块划分与依赖选型网关程序我按职责拆成五个模块HTTP 服务模块、提示流编排模块、引擎适配模块、会话管理模块和配置管理模块。HTTP 服务用标准库net/http就够了没必要上 Gin 或 Echo 这类框架因为路由器上追求的是极致轻量标准库完全能胜任而且没有额外依赖。路由用http.ServeMux加上简单的前缀匹配够用且清晰。引擎适配模块是重点我定义了一个统一的接口type Engine interface { Name() string Chat(ctx context.Context, req ChatRequest) (ChatResponse, error) HealthCheck(ctx context.Context) error }所有引擎不管是本地的还是远程的都实现这个接口。本地引擎可能调用一个跑在 U 盘上的小模型推理服务远程引擎则封装 HTTP 调用。这样编排层完全不关心底层是什么引擎只面向接口编程。新增一个引擎只需要实现这个接口然后注册进去符合开闭原则。会话管理模块负责维护对话上下文。路由器内存有限不能无限保存历史我的做法是每个会话只保留最近 N 轮对话N 可配置默认 10 轮。超出部分自动截断并且对历史做摘要压缩把长对话压缩成一段简短摘要这样既保留上下文又不爆内存。这个摘要压缩用的是规则加轻量模型的方式规则负责提取关键信息轻量模型负责生成摘要。3.2 提示流编排的核心逻辑提示流编排是这个项目的灵魂。所谓提示流就是把用户的原始输入经过一系列处理步骤最终变成发给 AI 引擎的提示词。这些步骤包括意图识别、模板选择、上下文注入、参数调整、安全检查。意图识别我用的是关键词加轻量分类的方式。路由器上跑不起复杂的分类模型所以先用关键词规则做粗分类比如包含翻译就归到翻译意图包含总结就归到总结意图。粗分类之后如果本地有小模型再用小模型做一次精分类。这样两级分类既保证了速度又保证了准确度。模板选择根据意图来定。每个意图对应一组提示词模板模板里用占位符标记需要填充的部分。比如翻译意图的模板可能是请把下面的内容翻译成{target_lang}{content}。模板存在配置文件里可以随时修改不用重新编译。上下文注入是把会话历史拼接到当前请求里。这里有个技巧不是把所有历史都拼进去而是根据当前意图选择相关的历史。比如当前是翻译意图就只注入之前的翻译历史不注入闲聊历史。这样能显著减少 token 消耗对远程引擎来说直接省钱。参数调整是根据请求复杂度动态设置引擎参数。简单请求用低温度、短输出复杂请求用高温度、长输出。这个判断逻辑也是基于意图和输入长度来的。实测下来这套动态参数比固定参数的效果好不少尤其是响应速度和成本控制。3.3 多引擎路由与故障转移多引擎路由的配置我放在 YAML 里结构大概是engines: - name: local-small type: local endpoint: http://127.0.0.1:8081 priority: 1 max_context: 2048 - name: remote-large type: remote endpoint: https://api.example.com/v1 priority: 2 max_context: 32000 routing: rules: - intent: chat max_input_len: 500 engine: local-small - intent: chat max_input_len: 100000 engine: remote-large路由规则按顺序匹配第一条匹配上的就用对应引擎。这样简单请求走本地复杂请求走远程既省成本又保证质量。故障转移是路由层的内置能力如果选中的引擎健康检查失败或者请求超时自动降级到下一个优先级的引擎。健康检查我设置的是每 30 秒一次超时 5 秒连续失败 3 次就标记为不可用。实操心得故障转移的降级顺序要提前想清楚。我的配置是本地引擎失败降级到远程远程失败降级到本地形成互补。但要注意避免循环降级所以降级只做一层不会无限往下找。3.4 资源限制与性能调优路由器资源有限网关必须做好资源限制。我用 Go 的runtime包设置GOMAXPROCS限制 CPU 使用用debug.SetMemoryLimit限制内存上限。并发请求数用带缓冲的 channel 做信号量控制超过上限的请求直接返回 429避免把路由器拖垮。性能调优方面几个关键参数我反复调过。HTTP 服务的读写超时设成 30 秒空闲超时 60 秒。连接池大小设成 20因为路由器并发能力有限设太大反而增加调度开销。日志级别默认用 info调试时才开 debug因为写日志本身也消耗 IO。实测下来在 RT-AX88U 上网关能稳定支撑每秒 5 到 10 个请求对于家庭和工作室场景完全够用。4. 实操部署全流程与关键配置4.1 从零开始的部署步骤部署流程我整理成了一套可复现的步骤。第一步刷好 Merlin 固件并启用 JFFS 和自定义脚本。第二步插上 U 盘安装 entware确认/opt挂载正常。第三步在开发机上交叉编译网关二进制拷到 U 盘的/mnt/ai-gateway目录。第四步把配置文件和启动脚本放到 JFFS 对应目录。第五步给脚本加执行权限重启路由器验证自启。这里有个细节容易忽略U 盘的挂载点在不同型号上可能不一样有的是/mnt/sda1有的是/tmp/mnt/sda1。我的做法是在启动脚本里用df命令动态查找挂载点而不是写死路径。这样换设备也不用改脚本。# 动态查找 U 盘挂载点 USB_MOUNT$(df | grep -E /mnt/|/tmp/mnt/ | grep -v jffs | awk {print $NF} | head -n 1)4.2 配置文件详解与参数计算配置文件里几个关键参数需要根据路由器实际情况计算。并发数建议设成 CPU 核心数的 2 倍比如双核路由器设 4四核设 8。缓存大小建议不超过可用内存的 30%比如 512MB 内存的路由器缓存上限设 150MB 左右。会话历史轮数根据内存来内存紧张就设 5 轮宽裕就设 10 到 15 轮。超时参数也要算。本地引擎响应快超时设 10 秒足够远程引擎受网络影响超时设 30 到 60 秒。健康检查间隔设 30 秒这个值太小会增加路由器负担太大则故障发现不及时。重试次数设 2 次配合故障转移基本能覆盖大部分临时故障。4.3 开机自启与进程守护开机自启靠services-start钩子但光有自启不够还要有进程守护。我用的是一个简单的守护逻辑管理脚本每隔 60 秒检查一次进程是否存活不存活就重新拉起。这个检查用 cron 实现在/jffs/configs/crontab里加一行*/1 * * * * /jffs/scripts/ai-gateway.sh check守护脚本的 check 逻辑是读 PID 文件用kill -0检查进程是否存在不存在就调用 start。这里要注意 PID 文件的清理进程异常退出时 PID 文件可能残留所以 start 之前要先清理旧 PID 文件。注意cron 任务在 Merlin 上默认可能没启用需要在系统设置里确认 cron 服务已开启。另外 cron 的执行环境变量和交互式 shell 不同脚本里要用绝对路径不能依赖 PATH。4.4 局域网接入与接口测试网关跑起来后局域网设备通过http://路由器IP:端口/v1/chat接入。接口设计成兼容主流 API 格式这样现有的客户端不用改代码就能用。测试时先用 curl 验证基本连通性curl -X POST http://192.168.1.1:8080/v1/chat \ -H Content-Type: application/json \ -d {message: 你好, session_id: test-001}返回正常后再测试多轮对话、故障转移和并发。并发测试我用ab或wrk在局域网内压测观察路由器 CPU 和内存占用。实测在 10 并发下CPU 占用约 40%内存增加约 50MB表现稳定。5. 常见问题排查与避坑经验5.1 启动失败与进程崩溃排查最常见的问题是网关启动后马上退出。排查思路是看日志日志在/jffs/logs/gateway.log。如果日志是空的说明进程根本没起来可能是二进制架构不对或者权限问题。用file命令检查二进制架构用chmod x确认可执行权限。如果日志里有 panic通常是配置解析失败或者端口被占用检查配置文件格式和端口占用情况。进程运行一段时间后崩溃多半是内存问题。用dmesg看有没有 OOM killer 的记录如果有说明内存超了要调小缓存和并发数。还有一种情况是 U 盘掉盘导致二进制文件不可读这种要看系统日志里有没有 USB 断开重连的记录如果有可能是 U 盘供电不足换个质量好点的 U 盘或者加个带供电的 USB hub。5.2 网络请求超时与引擎不可达远程引擎请求超时先确认路由器本身能不能访问外网用ping和curl测试。如果路由器能访问但网关不能检查网关的 DNS 配置因为网关进程可能没继承系统的 DNS 设置。我的做法是在配置里显式指定 DNS 服务器不依赖系统默认。本地引擎不可达通常是本地推理服务没起来或者端口不对。检查本地服务的监听地址如果是127.0.0.1那网关同机访问没问题如果是0.0.0.0要注意防火墙规则。Merlin 的防火墙默认会拦截部分入站请求需要在firewall-start里加放行规则。5.3 内存泄漏与长时间运行稳定性长时间运行后内存持续增长这是 Go 程序常见的问题多半是 goroutine 泄漏或者缓存没清理。排查用pprof在网关里开一个调试端口用go tool pprof分析堆内存。我遇到过的一个典型问题是 HTTP 连接没关闭导致连接池里的连接越积越多。解决办法是在每个请求处理完后显式关闭 response body并且设置连接池的空闲超时。缓存清理也很关键。会话缓存我设置了 TTL超过 30 分钟没活动的会话自动清理。这个清理用后台 goroutine 定时执行避免缓存无限增长。实测加上 TTL 清理后网关连续运行一周内存增长不超过 20MB非常稳定。5.4 常见问题速查表问题现象可能原因排查方法解决方案启动即退出架构不匹配/权限不足file查架构ls -l查权限重新交叉编译chmod x运行中崩溃内存超限dmesg查 OOM调小缓存和并发数远程请求超时DNS 或网络问题curl测试连通性显式配置 DNS本地引擎不可达服务未启动/防火墙检查监听和防火墙规则启动服务放行端口内存持续增长goroutine 泄漏pprof分析堆内存关闭连接加 TTL 清理U 盘掉盘供电不足查系统日志 USB 记录换 U 盘或加供电 hub5.5 独家避坑技巧汇总第一个技巧是关于日志轮转的。网关日志如果不轮转几天就能把 JFFS 写满导致系统异常。我的做法是用logrotate或者简单的脚本每天检查日志大小超过 10MB 就切割并删除旧日志。这个细节很多教程都不提但实际部署中非常关键。第二个技巧是关于配置热加载的。早期版本改配置要重启网关很麻烦。后来我加了配置热加载用fsnotify监听配置文件变化变化后自动重新加载。这样调参不用重启体验好很多。但要注意热加载时的并发安全配置读取要加读写锁。第三个技巧是关于时间同步的。路由器重启后时间可能不准导致 HTTPS 请求证书校验失败。解决办法是在启动脚本里先同步时间用ntpd或者ntpclient同步一次再启动网关。这个坑我踩过排查了半天才发现是时间问题。第四个技巧是关于二进制更新的。更新网关版本时不能直接覆盖正在运行的二进制文件因为文件被占用会写失败。正确做法是先停进程再替换文件再启动。我的管理脚本里专门做了这个逻辑更新时自动处理。6. 功能扩展与进阶玩法6.1 接入本地小模型的可行方案路由器上跑本地模型现实的选择是量化后的小模型参数量在 0.5B 到 1.5B 之间。推理框架用 llama.cpp 的 ARM 优化版本编译成静态二进制后放到 U 盘上。实测在 RT-AX88U 上跑一个 0.5B 的量化模型生成速度大约每秒 3 到 5 个 token做简单的意图分类和摘要够用做复杂对话就力不从心了。本地模型和远程引擎的配合是关键。我的策略是本地模型负责轻量任务比如意图识别、关键词提取、简单问答远程引擎负责重任务。这样既发挥了本地模型的低延迟优势又保证了复杂任务的质量。本地模型的加载用内存映射方式避免一次性加载到内存减少内存压力。6.2 多设备接入与权限管理局域网设备多了之后需要做权限管理。我在网关里加了简单的 API Key 机制每个设备或每个用户分配一个 Key网关根据 Key 做限流和权限控制。Key 存在配置文件里支持按 Key 设置不同的速率限制和可用引擎。限流用令牌桶算法实现每个 Key 一个桶桶大小和补充速率可配置。这样能防止某个设备疯狂请求把路由器拖垮。实测下来给每个设备设每秒 2 个请求的限制既不影响正常使用又能有效防止滥用。6.3 与智能家居场景的结合网关跑在路由器上天然适合和智能家居结合。我把它接入了家里的自动化系统语音助手说一句话请求先到网关网关判断意图后决定是本地处理还是转发远程。比如开灯这种简单指令本地直接处理帮我写个购物清单就转发远程引擎。这样响应快而且断网时基础功能还能用。这个场景的扩展空间很大。比如可以做一个家庭知识库把家里的设备手册、常用信息存到网关的本地存储里问答时优先查本地知识库查不到再走远程。这样既快又省流量隐私数据也不出局域网。6.4 监控与可观测性建设网关跑起来后需要监控它的运行状态。我在网关里暴露了一个/metrics接口输出请求数、延迟、错误率、内存占用等指标。然后用一个轻量的监控脚本定时抓取存到本地文件里需要时用简单的图表工具可视化。日志方面我做了结构化日志每条日志都是 JSON 格式包含时间戳、级别、模块、消息和上下文字段。这样排查问题时能快速过滤和检索。日志级别支持运行时动态调整不用重启就能开 debug。实操心得监控指标不要贪多路由器资源有限采集太多指标本身就成了负担。我最后只保留了 8 个核心指标足够定位大部分问题。7. 项目复盘与个人体会这个项目从想法到稳定运行前后折腾了大概两个月中间踩的坑比预想的多得多。最大的体会是在资源受限设备上做开发和在服务器上完全是两种思路。服务器上可以随便加内存加 CPU路由器上每一 MB 内存、每一个 CPU 周期都要精打细算。这种约束反而逼着我把代码写得更精简、把架构设计得更清晰。另一个体会是关于够用就好的哲学。一开始我总想着把功能做全结果网关越来越臃肿路由器越来越吃力。后来做减法砍掉了很多华而不实的功能只保留核心的编排和路由能力反而稳定性和体验都上去了。边缘计算场景下简单可靠比功能丰富重要得多。最后分享一个小技巧如果你也想在路由器上跑自定义服务建议先在虚拟机或者旧设备上把逻辑跑通再往路由器上移植。因为路由器上调试非常不方便没有完整的开发工具链日志查看也麻烦。先在开发环境把问题都解决掉移植时只处理环境差异效率会高很多。这个项目后续还可以往多路由器组网、边缘节点协同的方向扩展等我有新的实践再分享。