
1. 项目概述为什么要在路由器上跑AI编排折腾这个项目之前我的桌面上堆了三台智能设备一台主力工作机、一台吃灰的树莓派、一台24小时不关机的华硕路由器。每次想调一下AI模型、改一下提示词模板都要先打开电脑、连上SSH、再跑一堆命令。时间久了我就想能不能把最常开的这台路由器变成AI调度中心于是就有了把AI提示流编排器直接塞进Merlin固件的想法。华硕路由器配上Merlin插件后本身就跑着Linux有完整的用户态环境虽然内存和CPU都不算富裕但处理轻量级的HTTP代理、消息转发、提示词模板管理完全够用。更关键的是路由器本来就在网络的入口处让AI引擎靠近数据源头响应延迟反而比绕道云端低不少。这个项目解决的核心需求有三个一是把分散在本地和远程的AI接口统一管理起来二是把提示词的编写、版本切换做到随时可调三是让家里或者小办公室里的设备通过路由器这个节点就能直接使用AI能力不需要额外再开一台常驻服务器。适合的人群很明确已经刷了Merlin固件、具备基础SSH操作能力的折腾派以及想在边缘设备上试水轻量AI网关的开发者。文章会从硬件准备讲到代码落地再把坑位和优化方案一并交底动手能力强的朋友跟着做就能复现。考虑到这是一系列文章的第十二篇我会刻意少讲基础环境配置多讲编排器本身的架构设计。如果你还没接触过Merlin插件开发建议先翻翻系列前面的文章或者直接看章节2里的最小插件骨架。2. 环境准备Merlin插件开发最小框架2.1 确认路由器硬件与刷机状态并不是所有华硕路由器都适合跑AI编排器内存太小的型号比如128MB的旧款跑起Python解释器就够呛了。我这里实测用的是华硕RT-AX86U512MB内存、双核1.5GHz的处理器刷了最新的Merlin固件386系列。刷机本身不复杂官网下载固件后在后台直接升级即可但注意不要降级到官方旧版本否则Merlin会锁死Wi-Fi区域代码。刷完之后第一时间开启SSH和JFFS分区这两个都是在“系统管理 → 系统设置”里开的JFFS开启后才能在下一次重启后保留我们写的脚本。这里强调一句JFFS分区本质上是路由器闪存里划出来的可写区域容量只有几十MB所以编排器本体必须做得尽量精简依赖的Python库能砍就砍。我建议连接SSH后先查看分区大小和剩余空间命令如下df -h /jffs正常会看到类似/dev/mtdblock9挂载在/jffs下总容量约70MB可用空间看剩余多少。如果低于20MB那就得考虑把用不到的功能卸载掉或者干脆为核心库换一个更小的实现。2.2 插件脚本组织方式与启动逻辑Merlin的开机启动脚本位于/jffs/scripts/目录下最常用的是post-mount挂载后执行、nat-start启动NAT时执行和services-start系统服务启动后执行。为了不污染系统原有逻辑我习惯在services-start里加一行加载我们的编排器启动脚本如下#!/bin/sh /jffs/ai-orchestrator/start.sh 这行命令用最简单的方式把编排器放到后台运行但有个前提start.sh必须可执行。同时在post-mount里也要留一个自动创建依赖目录的操作因为JFFS分区在开机时可能还没有完全挂载顺序错了会导致路径不存在。更稳妥的做法是先在post-mount中判断目录是否存在不存在则创建再软链接到固定路径这样后续所有脚本都能保持路径一致。启动脚本里重点是环境变量的设置。因为路由器的运行环境比较精简BusyBox自带的vi都很难用我建议直接在电脑上写好文件再用scp传送权限设置为0755。对于Python环境Merlin固件默认不带Python需要安装Entware这是又一个坑——Entware把Python安装在了/opt/bin/python3如果直接在启动脚本里写python会找不到命令必须显式指定。#!/bin/sh export PATH/opt/bin:/opt/sbin:/usr/sbin:/usr/bin:/sbin:/bin export LD_LIBRARY_PATH/opt/lib:/usr/lib nohup /opt/bin/python3 /jffs/ai-orchestrator/main.py \ --port 8765 \ --config /jffs/ai-orchestrator/config.yaml \ /tmp/ai-orch.log 21 这段脚本里我把日志输出到了/tmp目录因为经常读写JFFS会缩短闪存寿命日志这种高频写入放临时目录更合理。cron定时清理日志的任务也可以加上避免一条日志撑爆内存盘。3. 提示流编排器的核心数据流设计3.1 从HTTP请求到AI引擎的链路编排器的主体是一个轻量HTTP服务我用Python标准库里的http.server实现没有引入Flask或者其他框架。原因很简单标准库在Entware里一定有而Flask及其依赖Werkzeug、Jinja2等体积大安装过程还会把JFFS空间吃掉一大块。实测下来Python自带的ThreadingHTTPServer配合BaseHTTPRequestHandler处理每秒十几次的请求毫无压力。一次典型的提示流请求完整路径是这样的智能家居设备或手机上的客户端向路由器发送POST请求请求头带上API密钥Body是JSON格式包含提示词内容、模型偏好、消息历史。编排器收到请求后先做身份校验再根据config.yaml里的路由规则决定调用哪一个大模型接口同时把对话历史和上下文注入到请求中。最终把上游模型返回的结果经过统一格式包装回传给客户端。整个过程的链路图用文字表达就是这样客户端 → HTTP POST /api/v1/complete → 鉴权模块 → 路由模块 → 模型适配层 → 外部AI引擎 ↓ ↓ 本地缓存/日志 响应标准化 ← 响应处理这个链路里最容易忽略的是时间预算。路由器CPU不强在编排器上做逻辑运算可以接受但如果加入重型的prompt模板渲染比如每秒钟需要处理几千个token速度就会非常难看。所以我把提示词模板的渲染只做简单的字符串替换复杂的语义解析一律不上把计算压力留给上游AI引擎。3.2 关键代码路由表与提示词模板管理提示流编排器最拿得出手的能力是动态路由。可以预先在配置文件中定义多条规则比如根据客户端IP、请求路径、关键词命中来转发到不同的大模型服务。下面是我在config.yaml里写的示例routes: - name: local-llm match: host: 192.168.50.0/24 path_prefix: /api/v1/complete target: type: http url: http://192.168.50.10:8080/inference - name: cloud-gpt match: header: x-ai-vendor value: openai target: type: http url: https://api.openai.com/v1/completions headers: Authorization: Bearer ${OPENAI_API_KEY}代码里对应的路由匹配函数我用的是简单规则引擎而不是正则因为业务里条件就那几条用正则反而容易踩逃逸字符的坑。核心逻辑如下def route_request(self, path, headers, remote_ip): for rule in self.routes: if not self._match_host(rule.get(match, {}), remote_ip): continue if not self._match_path(rule[match], path): continue if not self._match_header(rule[match], headers): continue return rule[target] return self.default_target编写这段代码时我犯过一个小错误本来想用IPAddress计算子网归属但路由器上没装ipaddress串口解析库后发现可以用整数转换比较性能反而更好。这里也提醒大家尽量用Python标准库能实现的逻辑不要图方便依赖额外的第三方包。模板管理模块也值得说说。我在templates/目录下放了几个.tmpl文件每个文件就是一个提示词模板支持{{variable}}格式的占位符底层用string.Template替换完全够用。有一个细节是模板的文件名我直接用模型的代号比如gpt-3.5.tmpl、claude-v2.tmpl这样在路由配置里就不用写冗长的路径改模型只需改路由表里的template字段。4. 轻量边缘网关给路由器加一个AI入口4.1 网关与编排器的职责划分很多人在设计时习惯把编排器和网关混在一起其实在路由器这个资源受限的环境里职责划分必须非常清晰。我的设计是把轻量边缘网关看成是一层皮肤它只负责接收外部请求、做基本的安全过滤、限流和密钥校验然后把解析后的标准数据结构传给后端的编排引擎。编排引擎再负责路由、模板渲染和缓存。两者通过本地Unix Domain Socket通信不走网络栈这样既快又省资源。网关部分的实现同样没有用框架直接基于BaseHTTPRequestHandler。端口我固定监听8765客户端设备需要调用时就向http://router.lan:8765/api/ai/generate发送请求。这个端口的访问控制我加了两层一层是防火墙规则只允许局域网访问另一层是应用层的API Key校验避免局域网里其他设备被滥用。防火墙规则通常放在nat-start脚本里iptables -I INPUT -p tcp --dport 8765 --src 192.168.50.0/24 -j ACCEPT iptables -I INPUT -p tcp --dport 8765 -j DROP这样一来即使网关服务本身有bug外网也无法探测到端口。网络安全这件事在边缘设备上尤其重要因为路由器本身就是网络边界。4.2 限流与并发控制实际使用中如果客户端不小心发了大量请求路由器内存会被打满甚至触发OOM导致重启。所以我在网关模块里加了简单的令牌桶限流器每秒钟允许的请求数默认是5这个值可以根据路由器负载手动调整。令牌桶的实现很短class TokenBucket: def __init__(self, rate5, capacity10): self.capacity capacity self.tokens capacity self.rate rate self.last time.monotonic() def consume(self, n1): now time.monotonic() self.tokens min(self.capacity, self.tokens (now - self.last) * self.rate) self.last now if self.tokens n: self.tokens - n return True return False这段代码虽然没做多线程同步但对于一个每秒请求不超过几十次的家用网络环境GIL已经帮我们挡住了大部分竞争问题。如果你要在生产环境复用记得加上threading.Lock。另外并发模式我选择了ThreadingHTTPServer每进来一个请求就新建一个线程。华硕路由器的Python线程切换开销可以接受但线程数必须封顶。我重写了ThreadingHTTPServer的process_request方法利用threading.BoundedSemaphore限制活跃线程数。实测同时跑10个线程时CPU占用在50%左右内存增加不到20MB完全在承受范围内。5. 实操过程从零部署到跑通第一个请求5.1 一键部署脚本与依赖准备为了让整个过程可以复现我把部署步骤写成了一个自动化脚本。核心步骤是通过opkg安装Python、pip安装YAML解析器因为解析config.yaml需要PyYAML这个库很小、模板文件复制到指定目录、最后启动服务。脚本的关键部分如下#!/bin/sh # 部署编排器到Merlin路由器 OPKG/opt/bin/opkg $OPKG update $OPKG install python3 python3-pip python3-yaml mkdir -p /jffs/ai-orchestrator/templates cp -r ./templates/*.tmpl /jffs/ai-orchestrator/templates/ chmod x /jffs/ai-orchestrator chmod x /jffs/scripts/services-start /jffs/ai-orchestrator/start.sh重点提示一下python3-yaml包在Entware里叫py3-yaml如果直接写python3-yaml会装不上。不同版本的Entware包名有差异我建议在安装前先搜索一遍opkg list | grep yaml看到包名后再安装。另外如果你的路由器之前已经启用了虚拟内存swap那部署后最好把swap文件留到路由器重启后再挂载否则某些版本的Merlin会在启动时找不到swap路径导致出错。5.2 首次请求的完整Sherlock流程部署完成后可以用iPhone或者电脑上发一个curl请求来验证。这里我写了一个标准的测试命令curl -X POST http://192.168.50.1:8765/api/v1/complete \ -H Content-Type: application/json \ -H X-AI-Router-Key: testkey123 \ -d {prompt: 给路由器写一首绝句, model: gpt-3.5, history: []}如果服务正常启动你会看到一个JSON响应其中reply字段包含AI生成的内容。这个过程我实测了多款路由器固件主要问题都集中在SSH会话的字符编码如果你从Windows直接用ssh命令中文提示词可能会乱码。解决方案是在路由器端设置环境变量LANGzh_CN.UTF-8或者在Windows下使用MobaXterm这样的工具。首次请求的时候我建议开着日志观察一个完整的请求周期tail -f /tmp/ai-orch.log日志里会显示每一步的执行时间以及路由命中了哪条规则。如果能正常看到请求和响应说明编排器和网关已经串起来了。6. 常见问题与排查技巧实录6.1 问题快查表现象可能原因解决方案启动脚本不生效权限未设置chmod x /jffs/scripts/services-startPython服务启动失败ImportError: No module named yaml安装py3-yaml局域网无法访问端口iptables规则未生效手动执行nat-start脚本响应速度极慢并发线程被阻塞调整令牌桶rate查看上游AI服务延迟日志刷屏内存占满循环打印日志日志级别调为WARNINGJFFS空间不足日志写在JFFS分区日志目录改到/tmp这些现象里最隐蔽的是第一个很多朋友改了配置文件却不生效其实是因为Entware的Python和系统Shell环境变量没有导出导致找不到python3命令。我建议把配置写在start.sh里通过export PATH显式声明。6.2 我踩过的坑OpenSSL库不兼容有段时间部署后请求特别不稳定经常报fread错误。排查半天发现是Entware的OpenSSL版本和Merlin固件自带的版本冲突导致Python调用ssl模块时产生段错误。最终解决方案是安装独立的py3-openssl包并且在启动脚本里加一句LD_PRELOAD强制加载新库。这个坑比较冷门不过如果你拿到的问题日志里出现了SSL字样基本可以照着这个思路查。另外路由器上的DNS解析也可能影响外部AI接口调用。如果上游是云服务需要在/etc/resolv.conf里确认DNS生效。我遇到过因为Merlin自带广告屏蔽插件把AI服务域名误杀的情况那时所有请求都会超时但curl测试IP地址又正常。在日志里看到一个反复出现的getaddrinfo错误就把域名加进了白名单才恢复正常。6.3 性能调优心得跑了一段时间后我总结出三个优化点。第一把模板解析结果做缓存每次请求不再重复读取文件这能减少一半的I/O开销。第二针对长对话只保留最近十条消息作为上下文避免历史消息膨胀导致的内存压力。第三使用gzip压缩HTTP响应虽然华硕路由器CPU要为此付出一点儿解压成本但总体带宽消耗下降明显尤其是在跑远程模型时效果显著。如果上游AI引擎本身跑在局域网内比如局域网内有一台安装了Ollama的机器那么这台路由器的编排器反而变成了一个稳定的反向代理和路由入口。我在实际项目中就用它管理了本地大模型和云端模型根据提示词里的主题词走了两条不同的链路。效果很直观本地模型处理速度快的请求几乎感觉不到延迟云端模型需要走外网才有一些波动。7. 扩展思路后续还要做些什么目前这个编排器已经稳定运行了一个多月中间只因为一次升级Merlin固件而重启过。功能上我还在考虑加入一个简单的Web管理界面直接通过路由器自带的反向代理跳转这样就不需要每次改模板都连SSH敲命令了。另外一个方向是把OSPF或者路由表同步整合进来让编排器根据网络状态自动选择最优的AI引擎不过这在家庭环境里有点杀鸡用牛刀如果是做办公网络边缘网关倒是值得一试。个人做这个项目的最大体会是边缘设备的算力虽然有限但可用性极高而且在网络入口做编排的思路天然比在云端做省了一层延迟。下一步我打算把提示词模板的同步放到本地Git仓库里每次改动后用钩子自动重载配置避免手动操作带来的出错概率。如果你也正在路由器上折腾类似的事情遇到有意思的坑欢迎留言交流我会把代表性的问题补进后续的系列文章里。