
1. 项目缘起与整体架构设计1.1 为什么要把 AI 引擎塞进路由器这个项目的出发点其实很朴素家里已经有一台常年开机的华硕路由器跑着 Merlin 固件性能过剩而我手上又有大量轻量级的 AI 调用需求——比如定时抓取 RSS 做摘要、局域网内的设备日志做异常检测、智能家居的语音指令做意图解析。这些任务如果全部丢到云端一是延迟高二是隐私数据出局域网心里不踏实三是长期跑下来 API 费用也不便宜。于是就有了这个想法能不能把路由器本身当成一个边缘网关在上面跑一个轻量的 AI 提示流编排器把本地小模型或者远端大模型的调用统一管起来路由器 7x24 小时开机、功耗低、天然处于网络边界这三点决定了它是做边缘推理调度的理想载体。而 Merlin 固件基于 Linux支持 Entware 包管理可以装 Go 运行时这就给了我们落地的基础。需要先明确一点路由器 CPU 是 ARM 架构内存通常只有 256MB 到 1GB算力非常有限。所以这里的AI 引擎不是指在路由器上跑大模型推理而是指在路由器上跑一个编排层——负责提示词模板管理、请求路由、上下文拼接、结果缓存、失败重试真正的推理还是交给局域网内的推理服务器或者远端 API。这个定位一定要先摆正否则后面选型会走偏。1.2 整体架构分层拆解整个系统我把它分成四层从下往上依次是硬件与系统层华硕路由器 Merlin 固件 Entware 环境提供 Linux 用户空间和包管理能力。运行时层Go 编译的静态二进制直接跑在路由器上不依赖外部动态库这是关键选型。编排核心层提示流引擎负责模板渲染、变量注入、多步链路编排、结果聚合。接入层HTTP 接口 局域网服务发现让内网其他设备可以调用这个网关。为什么运行时选 Go 而不是 Python这是整个项目里最重要的一个决策。Python 在路由器上跑需要完整的解释器和大量依赖光是把 CPython 塞进 Entware 就要占掉几十 MB而且启动慢、内存占用高。Go 编译出来是单个静态二进制交叉编译到 ARM 后通常只有 8MB 到 15MB启动毫秒级内存常驻可以控制在 20MB 以内。对于路由器这种资源紧张的环境Go 几乎是唯一合理的选择。1.3 提示流编排器的核心概念在动手之前得先把提示流这个概念讲清楚。所谓提示流就是把一次 AI 调用拆解成若干个可复用的节点每个节点做一件小事节点之间通过数据流连接。一个典型的流可能是这样的输入节点接收原始文本比如一条 RSS 标题。预处理节点清洗文本、截断长度、提取关键词。模板节点把处理后的文本填入预设的提示词模板。推理节点调用本地或远端模型。后处理节点解析返回结果、格式化、写缓存。输出节点返回给调用方或推送到其他服务。这样设计的好处是每个节点都可以独立测试、独立替换。比如今天用本地小模型明天想换成远端大模型只需要改推理节点的配置其他节点完全不用动。这就是编排器相对于写死一个脚本的核心价值。2. 路由器环境准备与 Go 运行时落地2.1 Merlin 固件与 Entware 环境搭建华硕路由器刷 Merlin 固件这一步我就不展开讲了网上教程很多重点说几个容易踩坑的地方。刷完之后第一件事是开启 JFFS 分区和 SSH这两个开关在系统管理页面里。JFFS 是用来持久化存储的Entware 和我们的程序都要装在这里否则重启就没了。开启 SSH 之后用终端连上去先确认 U 盘或者外接存储已经挂载。Entware 强烈建议装在 U 盘上因为路由器自带的闪存写入寿命有限频繁读写容易坏。挂载点通常是/tmp/mnt/sda1这种路径具体用df -h看一下。安装 Entware 的标准流程是下载安装脚本然后执行装完之后/opt目录就是 Entware 的根。这里有个细节Entware 的安装脚本会往/jffs/scripts里写启动项确保每次开机自动挂载。如果你换了 U 盘记得把旧的启动项清理掉否则会出现挂载冲突。注意Entware 安装完成后务必执行一次opkg update否则后续装包会因为索引过期而失败。这一步在弱网环境下可能要重试几次。2.2 交叉编译 Go 程序到 ARM 架构路由器是 ARM 架构具体是 armv7 还是 aarch64 取决于型号。先用uname -m确认一下。老一点的型号多是armv7l新旗舰基本是aarch64。这个信息决定了交叉编译时的GOARCH和GOARM参数。交叉编译的命令大致是这样# 针对 armv7 GOOSlinux GOARCHarm GOARM7 CGO_ENABLED0 go build -ldflags-s -w -o ai-gateway-armv7 . # 针对 aarch64 GOOSlinux GOARCHarm64 CGO_ENABLED0 go build -ldflags-s -w -o ai-gateway-arm64 .这里有几个关键点必须解释清楚。CGO_ENABLED0是必须的因为路由器上没有完整的 C 库和交叉编译工具链开启 CGO 会导致链接失败或者运行时找不到动态库。-ldflags-s -w是去掉符号表和调试信息能把二进制体积压缩 30% 左右对于路由器存储空间来说很值得。-s去掉符号表-w去掉 DWARF 调试信息代价是没法用 gdb 调试但生产环境本来也不在路由器上调试。编译完之后用scp或者winscp传到路由器上记得chmod x给执行权限。传之前先在本地用file命令确认一下架构对不对传错了会报Exec format error这个错误排查起来很浪费时间。2.3 依赖精简与静态链接策略Go 程序虽然号称静态链接但如果用了net包做 DNS 解析在某些系统上还是会依赖 libc。解决办法是编译时加上-tags netgo强制使用 Go 自带的纯 Go DNS 解析器。这个细节很多人不知道结果程序在路由器上跑起来 DNS 解析失败排查半天。GOOSlinux GOARCHarm64 CGO_ENABLED0 go build -tags netgo -ldflags-s -w -o ai-gateway .另外第三方库的选择也要克制。能用标准库解决的就别引第三方每引一个库都可能带来额外的依赖和体积。比如 HTTP 客户端用标准库的net/http就够了JSON 处理用encoding/json没必要上那些花哨的框架。我实测下来一个功能完整的编排器纯标准库实现编译出来也就 12MB 左右。3. 提示流编排核心实现3.1 节点抽象与数据流模型编排器的核心是一个节点接口。我定义了一个非常简单的接口type Node interface { Process(ctx context.Context, input map[string]interface{}) (map[string]interface{}, error) Name() string }所有节点都实现这个接口输入输出都是map[string]interface{}。为什么用 map 而不是强类型结构因为在编排场景下节点之间的数据格式是动态的用 map 更灵活。代价是类型安全靠运行时保证所以每个节点内部要做好类型断言和错误处理。数据流模型我用的是有向无环图。每个流定义里包含节点列表和边列表边表示数据流向。执行时从入度为 0 的节点开始按拓扑排序依次执行。这里要注意环检测如果用户配置的流有环必须在加载时就报错不能等到运行时死循环。type Flow struct { ID string Nodes map[string]Node Edges []Edge } type Edge struct { From string To string }拓扑排序用 Kahn 算法就够了实现简单复杂度 O(VE)。执行时每个节点的输出合并到全局上下文下游节点从上下文里取自己需要的字段。这种设计让节点之间解耦新增节点类型不需要改执行引擎。3.2 提示词模板引擎设计模板引擎是编排器的灵魂。我选的是 Go 标准库的text/template没有引第三方模板库。原因很简单标准库的模板功能足够用而且零依赖。模板语法支持变量替换、条件判断、循环对于提示词拼接来说完全够。一个典型的提示词模板长这样const summaryTemplate 你是一个文本摘要助手。请对以下内容进行摘要控制在 {{.MaxWords}} 字以内。 内容 {{.Content}} 要求 - 保留关键信息 - 使用简洁的书面语 - 不要添加原文没有的内容 模板渲染时把上下文传进去就行。这里有个经验模板里的变量名要和上游节点的输出字段名对齐否则渲染出来是空值。我建议在流加载阶段做一次静态检查扫描模板里的变量引用确认上游节点确实会输出这些字段。这个检查能省掉大量运行时调试时间。模板缓存也很重要。每次渲染都重新解析模板是浪费我用一个sync.Map缓存解析后的*template.Templatekey 是模板内容的哈希。实测下来缓存之后渲染耗时从微秒级降到纳秒级对于高频调用场景很有意义。3.3 多步链路编排与上下文传递多步链路是编排器区别于普通 API 封装的关键。举个例子一个新闻摘要并分类的流可能是这样的抓取节点从 RSS 源拉取最新条目。清洗节点去掉 HTML 标签截断到 2000 字。摘要节点调用模型生成摘要。分类节点把摘要再喂给模型做分类。存储节点把结果写入本地 SQLite。这里第 4 步依赖第 3 步的输出第 5 步依赖第 3、4 步的输出。上下文传递就是把这些中间结果串起来。我的做法是每个节点执行完后把输出以节点名为前缀写入全局上下文比如summary.text、classify.label。下游节点通过{{.summary.text}}这种路径引用。上下文的大小要控制。如果某个节点输出了几 MB 的文本全部塞进上下文会导致内存暴涨。我的策略是给上下文设一个总大小上限比如 1MB超过就报错。同时建议节点输出时做截断只保留下游真正需要的字段。提示上下文传递时大文本建议存到临时文件上下文里只放文件路径。这样既省内存又方便排查问题。4. 边缘网关的推理路由与缓存4.1 本地推理与远端 API 的路由策略边缘网关的一个核心能力是推理路由根据任务类型、输入长度、延迟要求决定这次调用走本地还是走远端。路由策略我设计成可配置的规则表规则条件路由目标说明输入长度 500 字 且 任务为分类本地小模型延迟低隐私好输入长度 2000 字远端大模型本地模型上下文不够任务为摘要 且 非敏感远端大模型质量优先任务为敏感数据本地模型数据不出局域网规则匹配用简单的顺序匹配第一条命中的规则生效。规则表存在配置文件里改完 reload 即可不用重启进程。这个设计让路由策略可以随时调整比如白天走本地省成本晚上走远端要质量。本地推理服务我用的是一个跑在局域网 NAS 上的推理服务器暴露 HTTP 接口。网关通过内网 IP 调用它延迟通常在几十毫秒。远端 API 则走 HTTPS延迟取决于网络状况。路由层要做的就是把这两种调用统一成同一个接口上层节点不用关心底层走的是哪条路。4.2 结果缓存与去重机制AI 调用是昂贵的无论是算力还是费用。缓存能省掉大量重复调用。我的缓存策略是基于输入哈希的键值缓存func cacheKey(node string, input map[string]interface{}) string { data, _ : json.Marshal(input) hash : sha256.Sum256(append([]byte(node), data...)) return hex.EncodeToString(hash[:]) }缓存存储用 SQLite因为路由器上装 SQLite 很方便而且支持持久化。缓存条目带 TTL过期自动清理。TTL 的设置要看任务类型分类任务结果稳定TTL 可以设长一点比如 24 小时摘要任务如果输入经常变TTL 设短一点比如 1 小时。去重是缓存的副产品。如果两个请求的输入哈希相同第二个请求直接命中缓存不用再调模型。这在批量处理场景下效果显著我实测过一个 1000 条的批处理任务去重后实际调用只有 300 多次省了 70% 的算力。注意缓存键的计算要包含模型版本和提示词模板版本否则模型升级后还在用旧缓存结果会不一致。我踩过这个坑排查了很久才发现是缓存没失效。4.3 失败重试与降级方案边缘环境不稳定是常态网络抖动、模型服务重启、内存不足都可能让调用失败。所以重试和降级是必须的。我的重试策略是指数退避第一次失败等 1 秒第二次等 2 秒第三次等 4 秒最多重试 3 次。重试只针对可恢复错误比如超时、连接失败对于参数错误这种不可恢复的直接返回。降级方案分两级第一级是本地模型失败时切到远端第二级是远端也失败时返回一个兜底的默认结果。兜底结果不是随便编的而是预先配置好的模板比如分类任务兜底返回未知摘要任务兜底返回原文前 100 字。这样至少保证调用方不会拿到空结果。func (r *Router) Call(ctx context.Context, req Request) (Response, error) { for i : 0; i maxRetries; i { resp, err : r.doCall(ctx, req) if err nil { return resp, nil } if !isRetryable(err) { break } time.Sleep(backoff(i)) } return r.fallback(req), nil }5. 实操部署与联调全流程5.1 从编译到上线的完整步骤把整个流程串一遍方便你照着做。假设你已经有一台刷好 Merlin 的路由器并且装好了 Entware。第一步本地编译。确认路由器架构执行对应的交叉编译命令得到二进制文件。编译前记得go mod tidy确保依赖干净。第二步传输文件。用 scp 把二进制传到路由器的/opt/bin目录同时把配置文件传到/opt/etc/ai-gateway/。配置文件里包含模型地址、API 密钥、路由规则、缓存路径等。第三步设置权限和启动脚本。给二进制加执行权限然后在/opt/etc/init.d/下写一个启动脚本让 Entware 的 init 系统管理它。启动脚本要处理 start、stop、restart 三个动作。#!/bin/sh ENABLEDyes PROCSai-gateway ARGS-config /opt/etc/ai-gateway/config.yaml PREARGS DESCAI Gateway PATH/opt/sbin:/opt/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin . /opt/etc/init.d/rc.func第四步启动并验证。执行启动脚本然后用ps确认进程在跑用curl调一下健康检查接口。健康检查接口返回 200 就说明服务起来了。第五步配置开机自启。Merlin 的/jffs/scripts/post-mount里加上启动命令确保每次挂载 U 盘后自动拉起服务。5.2 局域网联调与接口测试服务起来之后从局域网另一台机器测试。先测健康检查再测一个最简单的流。我用 curl 举例curl -X POST http://192.168.1.1:8080/flow/summarize \ -H Content-Type: application/json \ -d {content: 这是一段测试文本用于验证摘要流是否正常工作。}如果返回了摘要结果说明整条链路通了。如果报错按下面的顺序排查先看网关日志再看推理服务日志最后看网络连通性。日志我建议打到/opt/var/log/ai-gateway.log用tail -f实时看。联调阶段最容易出问题的是编码。路由器默认 locale 可能是 POSIX中文会乱码。解决办法是在启动脚本里设置LANGen_US.UTF-8或者在程序里强制用 UTF-8 处理字符串。我遇到过中文摘要返回乱码的情况就是 locale 没设对。5.3 性能压测与资源占用观察上线前做一次压测心里有底。用ab或者wrk从局域网另一台机器打请求观察路由器的 CPU 和内存。我的实测数据是单核 ARMv7 路由器并发 10 的情况下QPS 能到 50 左右CPU 占用 40%内存常驻 25MB。这个性能对于家庭场景完全够用。压测时重点看两个指标内存是否持续增长内存泄漏和延迟是否稳定有没有 GC 抖动。Go 的 GC 在低内存环境下可能比较激进可以通过GOGC环境变量调整。我一般设GOGC50让 GC 更早触发避免内存峰值过高。提示路由器内存紧张时可以给进程设GOMEMLIMIT让 Go 运行时主动控制内存上限。这个参数在 Go 1.19 之后才有效果很明显。6. 常见问题排查与避坑经验6.1 编译与部署阶段的高频问题问题一Exec format error。这是架构不匹配的典型症状。用file命令看二进制架构用uname -m看路由器架构两者必须一致。armv7 和 armv6 也不通用别搞混。问题二no such file or directory但文件明明存在。这通常是动态链接器缺失导致的。虽然设了CGO_ENABLED0但如果用了某些特殊库还是可能引入动态依赖。用ldd检查一下如果显示 not a dynamic executable 就对了。问题三程序启动后立刻退出日志为空。可能是配置文件路径不对或者配置文件格式错误。建议启动时把配置内容打印到日志方便确认。问题四DNS 解析失败。前面提过加-tags netgo编译。如果还不行检查/etc/resolv.conf是否存在Entware 环境下这个文件可能需要手动创建。6.2 运行时性能与稳定性问题内存泄漏排查。Go 程序内存泄漏通常来自 goroutine 泄漏或者全局 map 无限增长。用pprof抓一下堆快照对比不同时间点的对象数量。路由器上跑 pprof 要注意别把内存打爆了建议只在排查时临时开启。请求超时。边缘环境下超时很常见。检查推理服务的响应时间如果本地模型响应超过 5 秒考虑换更小的模型或者优化提示词。远端 API 超时则检查网络必要时调整超时阈值。缓存不生效。检查缓存键的计算逻辑确认输入序列化后是稳定的。map 的序列化顺序在 Go 里是随机的如果直接序列化 map 会导致缓存键不稳定。解决办法是先把 map 的 key 排序再序列化。问题现象可能原因排查方法解决方案启动即退出配置错误查看启动日志修正配置文件中文乱码locale 未设echo $LANG设置 UTF-8缓存命中率低键不稳定打印缓存键排序后序列化内存持续增长goroutine 泄漏pprof 堆快照修复泄漏点调用超时模型响应慢看推理日志换模型或优化提示词6.3 我踩过的几个真实坑第一个坑是U 盘挂载顺序。有次重启后服务起不来排查发现 U 盘挂载点从sda1变成了sdb1启动脚本里写死的路径失效了。解决办法是用 UUID 挂载或者在启动脚本里动态查找挂载点。第二个坑是Entware 包冲突。我装了一个版本的 SQLite后来又装了个依赖它的包结果把 SQLite 升级了导致我的程序链接的库版本不匹配。教训是生产环境装包要克制装之前先看依赖树。第三个坑是日志把闪存写满。程序日志默认打到 JFFS跑了一周把分区写满了路由器直接卡死。后来改成日志打到 U 盘并且加了 logrotate。这个坑很隐蔽因为前期完全没症状等到写满就晚了。第四个坑是并发调用把模型服务打挂。压测时并发设太高本地推理服务直接 OOM。后来在网关层加了并发限流用带缓冲的 channel 做信号量最大并发控制在模型服务能承受的范围内。7. 后续扩展方向与个人体会这套东西跑了大半年稳定性还不错。后续我打算往几个方向扩展。一是加一个简单的 Web 管理界面现在改配置还得 SSH 上去编辑文件不太方便。二是支持流的热加载改完配置不用重启进程。三是把缓存层换成更高效的方案SQLite 在并发高的时候还是有点吃力。另外一个想法是把多个路由器组成一个小集群每个跑一部分流通过内网互相调用。这样单台路由器的负载就降下来了而且天然有冗余。不过这个复杂度上来了得先解决服务发现和负载均衡的问题。我个人在实际操作中的体会是边缘计算这件事最大的挑战不是算法而是资源约束下的工程取舍。在服务器上随便用的方案到了路由器上可能完全跑不通。每做一个决策都要问自己这个依赖能不能去掉这个内存能不能省这个延迟能不能接受正是这些约束逼着我把代码写得更精简、更扎实。如果你也想在资源受限的设备上跑 AI 相关的服务我的建议是先从最小的可运行版本开始跑通了再逐步加功能别一上来就设计一个大而全的架构那样大概率会在某个环节卡死。