
这阵子我的华硕路由器除了承担全家三十几台设备的接入任务又多了一个新身份——一台 7x24 小时运行的轻量级 AI 提示流编排器。准确说是把负责接收提示、组织上下文、调用模型、清洗结果的小型服务塞进了路由器让这台网络设备同时兼任家庭里的边缘网关。整个项目我按开源的方式持续推进目前已经做到系列第 12 版。这篇把思路、配置和踩坑全部捋一遍给准备在路由器这类轻量设备上做 AI 编排的朋友做个参考。这个方案适合谁呢如果你手头正好有一台现成的华硕路由器不想为一个小任务专门添置服务器如果你有模型接口但缺一个统一调度入口或者你想知道 Merlin 固件上的插件到底怎么组织、怎么跟着系统引导自动启动都可以把这篇耐心看下去。后面涉及的操作大多能在半天内完成难度不算特别大但是千万别跳步骤。1. 为什么把提示流编排器塞进华硕路由器1.1 这玩意儿到底解决什么问题我们日常遇到的最大问题不是说缺 AI 能力而是缺一个统一的中枢。今天想调用云端大模型生成摘要明天想接一个联网搜索问答接口后天又要给群里的机器人做意图分类。这些任务如果各自在客户端里写一遍很快会乱成一团密钥散落各处提示词改一句就要翻好几个文件。提示流编排器要做的事就是把“收到一段输入、选择提示词模板、补充上下文、调用模型、格式化输出”这个完整过程固化下来。当你有了一个编排器新接入一个任务只需要定义一个流程文件而不是从头写代码。路由器的优势在于它本来就是 7x24 小时在线的设备不占桌面空间功耗几乎可以忽略而且位置固定天然适合做家庭场景里的调度节点。可能有人会问路由器那点性能够用吗这里要澄清一个概念路由器上跑的是编排逻辑不是真正的大模型推理。我现在的做法是把重计算放到云端模型接口或者局域网里一台小推理机上路由器只负责调度和上下文组装。这种组合下一台双核 ARM 处理器、512MB 内存的路由器就能扛住几十个并发请求完全够家庭场景使用。1.2 华硕路由器加 Merlin天然的家用边缘节点华硕官方固件本身功能扎实但扩展性有限。Merlin 固件是整个华硕路由器生态里一个很出名的第三方分支它保留了原厂的 Web 管理界面和大部分网络功能同时开放了更多脚本入口和存储分区。可以这么理解Merlin 给原本封闭的路由器系统加了一扇能自己动手的门但整套体系的底子依旧是轻量 Linux 环境并不会变成一台笨重的服务器。我比较看重 Merlin 的三个特性。第一它有 /jffs 分区重启后数据不会丢适合放脚本和配置第二它提供 init-start、nat-start 这类钩子脚本系统启动到特定阶段会自动执行我们挂进去的任务第三社区维护的 Entware 软件仓库能直接安装 python3、nano、jq 这些工具。把这三个能力组合起来路由器就不再是只能通过浏览器点点点的盒子而是一个可编程的边缘节点。在实际动手之前建议你先理解 Merlin 插件的运作方式。所谓插件并不是像手机 App 那样有一个统一安装器大多数情况下插件就是一组带目录结构的脚本集合。我们后面生成的“AI 编排器插件”本质上是放在 /jffs/ai-gate 下的一个项目里面有启动脚本、配置文件、流程目录再通过钩子脚本让它跟着路由器一起跑。这种朴素的方式反而是 Merlin 生态里最不容易出问题的模式。1.3 编排器和常见自动化工具的区别看到“编排”这两个字很多人第一反应是 n8n、Node-RED 这类可视化流程工具。那类工具功能很强大但对路由器来说太重了。可视化编辑器要常驻 Web 服务拖拽节点也会消耗不少内存再加上它们往往依赖 Node.js 环境整体跑起来在低配置设备上并不舒适。我采用的方案是声明式流程文件加极简执行引擎。流程用 JSON 定义步骤类型包括模板拼接、模型调用、结果合并、条件跳转等执行引擎按顺序把每个步骤跑一遍。这样做的好处是流程可以被 Git 管理每次改动能够追溯也不需要依赖一个常驻的网页控制台。坏处自然是没有图形界面调试要靠在日志里翻找但对我们这种愿意折腾的人来说可控性其实更高。这个定位很重要后面写代码时也一直围绕“轻量”“可维护”来约束自己不要想着把功能堆得多豪华而是要保持路由器的资源有余量让服务能长期安静地跑下去。2. 基础环境准备把 Merlin 变成可编程平台2.1 版本、分区和基本约定准备阶段有点啰嗦但能省下后面大量返工。第一步先确认你手头的华硕路由器型号和 Merlin 固件版本。不同型号的 CPU、内存、存储空间差别很大尽量选择支持 Entware 的较新版本避免因为固件太老导致软件源不兼容。登录 SSH 后先用 df -h 和 cat /proc/meminfo 看一眼当前系统的可用空间和内存。我的硬件条件是 512MB 内存加一个 USB 扩展存储。实际规划时我把脚本和配置放在 /jffs 分区把软件包安装在 USB 存储上。这样即使路由器恢复出厂设置只要脚本目录还在能很快把服务拉起来。这里有一个额外提醒Entware 安装后在文件系统里一般会挂载到 /opt这不是咱们自己定义出来的路径而是软件仓库的默认约定后面所有 opkg 安装的软件都会汇总到这里。另外强烈建议在开始前把路由器的 SSH 登录方式设置好并确认你可以用普通用户身份执行命令。安装在 Merlin 上通常需要 root 权限所以 SSH 登录后我们看到的就是管理态环境。做好这一步后面写钩子脚本、改文件权限才不会处处受阻。2.2 安装 Entware 与运行时在华硕 Merlin 路由器上安装 Entware最省事的方式是先安装 amtm。amtm 是一个终端菜单工具输入 amtm 就能看到包括 Entware 安装在内的一堆功能。一路确认之后它会自动把软件仓库的初始化做完。这一步依赖正常的网络访问建议挑一个网络空闲的时段进行避免下载到一半中断。Entware 装好以后SSH 里执行下面这组命令把运行时要用的基础软件补上opkg update opkg install python3 python3-pip jq coreutils-nohup装完以后用 python3 --version 验证一下。为什么用 Python 而不是 Go 或者 Node原因很简单后续流程涉及大量字符串处理、JSON 解析和 HTTP 调用Python 写起来直接、逻辑清楚而且在 Entware 仓库里维护得比较及时。Node 也不是不行但对我来说Python 标准库已经足够不太需要依赖第三方框架这样在路由器上的运行负担会小很多。如果你手里的路由器存储空间比较紧张opkg 会提示磁盘不足。这时候优先检查 USB 是否挂载成功Entware 如果安装到了 USB 上空间一般不会成为瓶颈。要是确实太小就得把 USB 换成容量更大的盘尽量不要把软件包硬塞到系统分区里。2.3 目录结构与开机启动策略目录结构是这种小项目最容易乱的地方。我最终的目录规划是这样的/jffs/ai-gate/ ├── start.sh ├── config.json ├── gate.py ├── api_key ├── flows/ │ ├── summarize.json │ └── intent.json └── logs/ └── gate.log一个目录负责一件事配置集中放在同级的 JSON 文件里。日志单独开目录方便后续做清理。项目文件本身非常小几十 KB 的量级放进 /jffs 完全没问题。但日志会不断增长所以我在启动脚本里会做大小检查日志超过 2MB 就直接清空重来避免 jffs 分区被写满。开机启动方面华硕 Merlin 提供了 /jffs/scripts/init-start 和 /jffs/scripts/nat-start 两个常用钩子。init-start 在系统初始化阶段触发nat-start 在网络连接建立之后触发。因为编排器需要访问外网模型接口我把启动动作挂在 nat-start 上确保网络通了以后再拉起服务。如果你只是做本机离线流程挂在 init-start 上也行。3. 核心设计提示流编排器的四层结构3.1 接入层设计编排器最外面的入口是一个 HTTP 服务。我选择用 Python 标准库里的 http.server没有引入 Flask、FastAPI 这类框架原因还是那个路由器上的 Python 环境尽量少依赖第三方包否则某一天依赖装不上整个服务就起不来了。这个接入层只做三件事接收 JSON 请求、校验路径和令牌、把请求转交给编排引擎。它不应该包含任何业务逻辑更不应该直接读取文件系统或者执行系统命令。保持接入层足够薄安全风险也会随之降低。我甚至单独封装了一个 GatewayHandler 类里面只处理 HTTP 协议细节业务逻辑全部在 FlowEngine 里。监听地址方面我默认让服务监听 0.0.0.0:8899也就是局域网内其他设备也能调用。如果你不想让任何设备访问这个服务可以把地址改成 127.0.0.1只在路由器本机使用。考虑到我家里有智能家居设备需要持续调用我选择了局域网可访问同时在请求头上加一个简单的令牌校验。3.2 编排层设计编排层是整套系统的核心。我不会把流程逻辑硬写成一长串 if-else而是设计成步骤表。每个流程文件包含若干步骤每个步骤有 type、name、condition、input、output 这些字段。目前支持的步骤类型有 template、llm_call、merge、condition 等。执行引擎按照顺序遍历步骤遇到 condition 就做一次上下文判断条件不满足就跳过当前步骤最后把指定节点保存的内容作为输出。你可能已经发现了这种设计其实就是一个非常小的有向无环图但实现和调试都比完整 DAG 引擎简单得多。对于家庭场景这已经足够了。这么拆分以后加一个新任务基本不用改代码。我只需要在 flows 目录里多放一个 JSON 文件再调用 HTTP 接口时把流程名写在 URL 路径里编排器就会自动加载新的流程。举个例子给家人做一个每日天气总结流程就是“收集局域网传感器数据、合并上下文、调用模型、输出一段话”完全是可配置的。3.3 模型层设计模型层的职责很单一把统一格式的调用请求翻译成具体模型接口的格式。现在市面上的模型接口大多兼容 OpenAI 格式所以我的模型层默认支持这类接口。配置在 config.json 里维护包含 base_url、model、api_key 三个核心字段。除了云端接口我还会把局域网里的 Ollama 服务并入模型层。流程里的 llm_call 步骤不需要知道自己背后是云端还是本地模型它只负责提交一个 system prompt 和 user prompt拿回模型输出。真正选择模型、切换服务的事全部交给模型层处理。我实际使用中同一个流程从本地小模型切到云端模型只改一行配置就生效排障成本低了不少。这里有一个关键原则流程和模型解耦。流程文件里不要写任何具体模型的名字也不要写 api_key。这样整体结构干净而且迁移到另一台设备时只需要复制配置目录不用改动流程逻辑。3.4 数据层设计数据层负责缓存、历史和日志三块内容。同样的输入如果反复请求既浪费时间也浪费额度所以我在内存里加了一层简单缓存。以流程名加请求内容算出一个哈希 key十分钟以内的重复请求直接返回上一次的结果日志里标一个 hit 标志。这个设计看起来简单实际效果很好因为家庭场景里很多请求是轮询式、定期触发的重复率特别高。历史记录则是按 session_id 组织的。我会把同一个会话下的最近五轮问答保存下来超过五轮就丢弃最旧的用于需要上下文连续性的场景。日志方面我记录三样信息请求路径、耗时、模型返回状态码。这三个字段非常重要一开始可能觉得没什么等到后面出了问题没有日志你会完全无从下手。4. 实操实现一个路由器可用的最小编排服务4.1 配置文件先定下来好的设计必须有清晰的配置入口。我先贴 config.json所有可能变动的信息都集中在这里。密钥用环境变量方式注入不在配置文件里写明文。{ listen_host: 0.0.0.0, listen_port: 8899, token: change-me-to-your-own-token, default_model: { base_url: https://api.example.com/v1, model: gpt-4o-mini, api_key_env: AI_GATE_API_KEY }, cache_ttl: 600, history_max_turns: 5, flows_dir: /jffs/ai-gate/flows }看到 api.example.com 别直接照抄这只是占位符实际使用时要替换成你接的模型服务地址。api_key_env 表示从环境变量里读取密钥而不写进 JSON 文件。这样做的好处是config.json 可以放进版本管理甚至分享出去但密钥不会随意外泄。如果觉得环境变量管理麻烦也可以用单独一个 api_key 文件然后在启动脚本里注入。token 这个字段先保留默认值但正式上线前必须换掉。它是服务对内网设备做身份校验的最基础手段后面在安全一节再展开说。4.2 核心代码FlowEngine 和流程执行器下面这段代码是整套编排器的心脏我压缩到最小可运行的一个文件里。它只依赖 Python 标准库代码里注释尽量写清楚方便直接抄到项目里做二次修改。import json import os import time import hashlib import urllib.request import http.server FLOWS_DIR /jffs/ai-gate/flows CACHE {} class FlowEngine: def __init__(self, config): self.config config self.flows {} self._load_flows() def _load_flows(self): for f in os.listdir(FLOWS_DIR): if f.endswith(.json): key f[:-5] with open(os.path.join(FLOWS_DIR, f), encodingutf-8) as fh: self.flows[key] json.load(fh) def run(self, flow_name, payload, session_id): if flow_name not in self.flows: return {ok: False, error: flow not found} # 用请求内容做缓存 key cache_key hashlib.md5( (flow_name json.dumps(payload, ensure_asciiFalse)).encode() ).hexdigest() if cache_key in CACHE: hit_time, content CACHE[cache_key] if time.time() - hit_time self.config.get(cache_ttl, 600): return {ok: True, cached: True, content: content} # 初始化上下文 context {input: payload.get(text, )} for k, v in payload.items(): context[k] v for step in self.flows[flow_name].get(steps, []): step_type step[type] if step_type template: # 把上下文里的变量填进提示词模板 context[step[name]] step[prompt].format(**context) elif step_type merge: merged {} for key in step[keys]: merged[key] context.get(key) context[step[name]] merged elif step_type llm_call: prompt context.get(step.get(prompt_source, prompt), ) context[step[name]] self._call_llm(prompt) output context.get(self.flows[flow_name].get(output, output)) CACHE[cache_key] (time.time(), output) return {ok: True, cached: False, content: output} def _call_llm(self, prompt): cfg self.config[default_model] api_key os.environ.get(cfg.get(api_key_env, ), ) body { model: cfg[model], messages: [ {role: system, content: 你是轻量边缘网关助手回答保持简洁实用。}, {role: user, content: prompt}, ], temperature: 0.3, } req urllib.request.Request( cfg[base_url] /chat/completions, datajson.dumps(body).encode(), headers{ Content-Type: application/json, Authorization: Bearer api_key, }, ) with urllib.request.urlopen(req, timeout25) as resp: data json.loads(resp.read().decode()) return data[choices][0][message][content]下面是 HTTP 接入层的实现处理请求头和响应序列化。这里我把 token 校验放在最前面先判断再处理请求内容。class GateHandler(http.server.BaseHTTPRequestHandler): engine None def do_POST(self): token self.headers.get(X-Token, ) if token ! change-me-to-your-own-token: self.send_response(401) self.end_headers() return length int(self.headers.get(Content-Length, 0)) body json.loads(self.rfile.read(length)) flow self.path.strip(/) session_id body.get(session_id, default) result self.engine.run(flow, body, session_id) payload json.dumps(result, ensure_asciiFalse).encode() self.send_response(200) self.send_header(Content-Type, application/json; charsetutf-8) self.end_headers() self.wfile.write(payload) def log_message(self, fmt, *args): # 默认日志格式太吵交给业务层自己记录 pass if __name__ __main__: config_path /jffs/ai-gate/config.json config json.load(open(config_path, encodingutf-8)) # 从单独的文件注入密钥 key_path /jffs/ai-gate/api_key if os.path.exists(key_path): os.environ[AI_GATE_API_KEY] open(key_path).read().strip() GateHandler.engine FlowEngine(config) server http.server.ThreadingHTTPServer( (config[listen_host], config[listen_port]), GateHandler ) server.serve_forever()注意代码里 ThreadingHTTPServer 是并发模型每个请求开一条线程。对路由器来说并发太高会有风险所以我在生产部署时还给这段代码套了一个信号量把并发请求数限制在 10 个以内。超出部分让它排队等待比直接让线程爆炸要好得多。4.3 写两个真实可用的提示流模板流程文件放 /jffs/ai-gate/flows 目录文件名就是流程名。第一个流程是 summarize用来把一段长文本压缩成三条要点。第二个流程是 intent用来判断用户输入到底属于查询、命令还是闲聊。先看 summarize.json{ output: result, steps: [ { type: template, name: prompt, prompt: 把下面的内容压缩成三条要点每条不超过20字不要加序号前置说明\n{input} }, { type: llm_call, name: result, prompt_source: prompt } ] }这个流程的逻辑很容易看懂。第一步把输入文本塞进一个提示词模板生成完整 prompt第二步调模型把模型输出存到 result 变量里最后整个流程返回 result 作为输出。接下来是 intent.json{ output: result, steps: [ { type: template, name: prompt, prompt: 判断用户输入属于哪一类查询、命令、闲聊。只输出一个词。输入{input} }, { type: llm_call, name: result, prompt_source: prompt } ] }两个流程结构完全一样区别只在提示词模板不同。正因为流程执行逻辑统一了后面你完全可以自己复制出一个新流程只需要改 prompt 和输出字段。这也是我说“加新任务不需要改代码”的意思。4.4 启动脚本和首次验证服务代码和流程文件都有了接下来要让它跑起来。先写 start.sh#!/bin/sh export PATH/opt/bin:/opt/sbin:/usr/sbin:/usr/bin:/sbin:/bin cd /jffs/ai-gate || exit 1 # 读取 API 密钥并注入环境变量 export AI_GATE_API_KEY$(cat /jffs/ai-gate/api_key 2/dev/null) # 日志大小控制超过 2MB 就清空重来 if [ -f logs/gate.log ]; then size$(wc -c logs/gate.log) if [ $size -gt 2097152 ]; then mv logs/gate.log logs/gate.log.old fi fi nohup python3 gate.py logs/gate.log 21 echo $! /tmp/ai_gate.pid给脚本加上执行权限手动启动一次chmod x /jffs/ai-gate/start.sh sh /jffs/ai-gate/start.sh sleep 2 cat /tmp/ai_gate.pid tail -n 20 /jffs/ai-gate/logs/gate.log然后把启动动作挂进 nat-start 钩子。先创建 /jffs/scripts/nat-start 文件注意如果没有这个文件需要新建#!/bin/sh [ -f /jffs/ai-gate/start.sh ] /jffs/ai-gate/start.sh chmod x /jffs/scripts/nat-start重新拨号或者重启路由器后服务会自动拉起。首次验证我习惯用一条 curl 命令测试 summarize 流程curl -s http://127.0.0.1:8899/summarize \ -H Content-Type: application/json \ -H X-Token: change-me-to-your-own-token \ -d {text:今天我们讨论了三个议题第一个是设备发放第二个是数据备份第三个是排班表。整体来说大家希望把周期压缩到每周一次。} | jq正常返回的 JSON 大致是这样{ ok: true, cached: false, content: 1. 讨论设备发放细节\n2. 确定数据备份方案\n3. 排班周期压缩至每周一次 }content 的内容肯定因模型而异重点看 ok 字段是否为 true。如果返回 401先检查 X-Token 是否匹配如果提示 flow not found看看请求路径里的流程名和 flows 目录下的文件名是否一致。5. 常见问题与排障实录5.1 内存和闪存到底够不够用“路由器内存才 128MB跑得动吗”是我被问得最多的问题。我的实测是这个 Python 编排器本身占用大约 40MB 到 80MB 内存取决于流程数量和并发情况。如果你在 free -m 里看到可用内存还剩一半以上基本就可以跑。要是内存长期紧张优先做减法。关闭路由器上不需要的其他插件减少不必要的后台进程。还有一个容易被忽视的内存杀手是线程堆积ThreadingHTTPServer 每个请求开一条线程如果客户端频繁超时重试线程数会一路飙升。我给模型层和接入层都加了并发信号量总并发控制在 10 个以内内存占用立刻变得可控。闪存方面核心风险来自日志。Python 进程每次请求都会写一行日志如果不加限制/jffs 分区迟早被填满。我在 start.sh 里做了日志大小检查超过 2MB 就改名保留再配合系统里的 log cleaner基本能保证长期运行不爆盘。5.2 请求超时和网络不稳定怎么处理路由器环境里网络并不总是那么给力。云端模型接口偶尔会卡住十几秒甚至更久如果不做超时控制HTTP 请求会一直在那儿等待线程被占用后续流程全部排队。我在 _call_llm 里设置了 25 秒读取超时并且把连接超时单独设为 5 秒。超时之后还要有重试策略。我的做法是第一次失败等 2 秒重试第二次失败等 5 秒重试第三次还不成功就直接把错误返回给调用方同时把错误原因写进日志。重试逻辑放在模型层流程层不需要感知。这样每一个流程都能自动受益不会因为某个模型接口短暂抽风就影响全局。还有一种常见情况是请求本身很慢但不是超时比如模型生成长文本需要几十秒。针对这类场景我会在模板里经常控制输出长度或者在模型参数里限制 max_tokens。少让模型生成一堆废话也能带来明显的响应改善。5.3 安全基线必须守住把任何服务放到路由器上都相当于在自家门口多装了一把钥匙。安全底线我归纳为四条。第一端口不要直接暴露到公网除非你有完整的访问控制和加密方案否则默认只监听内网就算要尝试远程访问也先走已有的安全接入体系而不是简单映射端口。第二令牌必须换掉不能留着示例里的 change-me-to-your-own-token。第三API 密钥不要写进配置文件单独放文件并设置权限chmod 600 /jffs/ai-gate/api_key。第四编排器绝对不能拥有操作系统级别的控制能力。我这个项目里它只能读取配置、做模板拼接、调用模型接口、写日志不能执行任意命令也不能浏览任意文件。这样即使内网里有设备被入侵攻击者拿到的也只是一个功能受限的 AI 调用器而不是一个可以控制路由器的后门。安全这件事不需要做得很花哨但基础项必须到位。很多第一次做这类项目的人会忽略令牌和密钥权限等真实环境被搅乱以后才后悔那就太晚了。5.4 踩坑速查表下面这张表整理了我实际部署中遇到的典型问题和处理方式可以存下来当参考。现象可能原因处理方式服务启动后马上退出路径不对或 Python 缺模块先手动执行 python3 gate.py 看报错返回 401X-Token 与配置不匹配检查请求头字段名和 config.json 中 token请求总超时模型接口响应慢调大 timeout或换更小的模型内存占用不断上涨并发线程过多给模型层加信号量限制总并发路由器重启后服务没起来钩子脚本没有执行权限chmod x /jffs/scripts/nat-start/jffs 空间被占满日志文件增长设置日志大小上限定期轮转清理返回结果乱码模型返回带 BOM 或编码异常解码时指定 utf-8必要时去掉 \ufeff除了这张表还有一个特别细微但容易踩的坑云接口返回的 JSON 里偶尔会带非预期字符直接用 json.loads 解析可能报错。我一般都会在读取响应后先判断编码再对字符串做一次清洗。这些小问题不会让项目跑不起来但确实会让人调试时多花半天时间所以提前记录非常重要。6. 后续还能怎么延展6.1 要不要在路由器上直接跑模型我常被问到既然路由器已经能跑编排器了能不能在本地直接跑一个开源小模型我的观点是现阶段路由器上直接跑大模型生成并不现实。0.5B 到 1B 左右的量化小模型虽然几百 MB 就能放下但路由器 CPU 的推理速度太慢单条响应常常要几十秒实际体验很差。更有价值的方向是把路由器编排器和局域网里一台 8GB 内存的小主机配合使用。小主机跑 Ollama 负责真正的大模型推理路由器负责调度、上下文合并、缓存和外部接口接入。这样两台设备各司其职既能享受本地推理的私密性又能让路由器继续保持低负载。这个组合我实测跑了一段时间体验比单独压路由器要好很多。6.2 与家庭自动化平台和 AI Agent 协同编排器一旦稳定运行它能接的东西就很多了。最实用的是和家庭自动化平台联动传感器触发事件时先调用编排器把原始数据整理成一段自然语言再通过消息推送发到手机上。这样你夜间收到的不是一串干巴巴的数字而是“客厅湿度低于 30%建议打开加湿器”这样一句能直接理解的话。另一个方向是接入 AI Agent。可以把编排器当作 Agent 的工具执行端Agent 收到了用户意图调用编排器暴露的流程接口编排器决定调用哪个模型、带上哪段上下文。Agent 不需要理解每个模型接口的细节只需要知道有哪些流程名字整个协作立刻变得清晰。多模型协作也是顺理成章的事编排器同时接收不同模型的返回结果再做一轮汇总或者互检这在处理重要内容时很有价值。6.3 个人实际操作中的体会折腾这套东西让我最深的感触是路由器资源确实有限但恰恰因为有限才逼着我把模块拆得干净。不能把它当成普通服务器来用不能贪功能不能放任日志膨胀出了问题还要有快速回滚的路子。这种约束反过来对工程习惯是一种很好的训练。如果你准备复刻这个项目我强烈建议先跑通一个 hello world 流程请求进来、模板拼接、写日志、返回固定字符串把接入层和启动链路吃透再接模型。模型层跑顺了以后再叠加缓存和重试。每叠加一块就在家里真实跑两三天观察内存占用和日志输出。这样逐步推进编排器才能真正变成你可以依赖的轻量边缘网关而不是三天两头让人挠头的小玩具。手头有闲置华硕路由器、又对 AI 调度感兴趣的朋友不妨从这份开源折腾系列第 12 版入手。按照这篇的步骤半天时间足够把第一个流程跑起来。毕竟都是折腾折腾得有条理一点后面用起来就更明白。