NAS低功耗运行Local Agent:llama.cpp RPC集群与NAS私有知识库落地方案

3次阅读
没有评论

共计 4057 个字符,预计需要花费 11 分钟才能阅读完成。

很多极客在折腾 NAS 时都有过类似的冲动:既然 NAS 里存着我这十几年来的代码库、个人笔记、财务文档甚至扫描件,为什么不直接在 NAS 里塞一个 Local LLM,让它充当 7 ×24 小时随时调用的本地知识库 Agent?然而现实往往很骨感——绝大多数家用与轻量办公 NAS 采用的都是 N100、J6413 或者老款至强 / 嵌入式 AMD CPU,直接跑 7B 大模型,Token 生成速度往往只能爬到每秒 1~2 个字,CPU 占用率更是常年拉满到 100%,风扇狂转伴随功耗起飞,实用性几乎为零。

折腾了几周后,我推翻了原本“全塞进 NAS 纯 CPU 硬跑”的方案,改用了一套极具性价比且低功耗的混合架构:NAS 负责存储、向量检索引擎(Chroma/Qdrant)与 Agent 工作流编排,通过 llama.cpp 的分布式 RPC 机制或轻量流式网关,将推理计算卸载到内网的闲置台式机 / 工作站 GPU 上。即使工作站关机休眠,NAS 也能自动唤醒(WOL)或降级处理,兼顾了存储安全、低功耗常驻与高吞吐响应。

一、架构拆解:为什么不是传统的 Docker 直跑?

在传统的 All-in-One 思路中,大家习惯直接在群晖或 Unraid 上用 Docker 拉起 Ollama。但在低功耗 NAS 上,这种架构存在三个致命痛点:

  • 内存带宽瓶颈:大语言模型推理极度依赖内存吞吐。NAS 常见的单 / 双通道 DDR4/DDR5 带宽通常在 30~50 GB/ s 左右,而一块入门级显卡显存带宽往往都在 300 GB/ s 以上。内存带宽卡死,无论 CPU 核心怎么超频,推理就是快不起来。
  • 上下文扩展(Context Window)爆内存:Agent 检索任务动辄需要塞入 8k 甚至 16k 的上下文,长上下文导致的 KV Cache 膨胀会迅速吃光 NAS 仅有的 16G 或 32G 内存,引发 OOM 并拖垮其他轻量容器(如 Plex/Jellyfin、Nextcloud)。
  • 存储冷热分离脱节:NAS 最核心的价值是海量稳定存储与高速 NVMe 缓存,而不是高浮点计算。把计算与存储死死绑在同一个物理节点上是本末倒置。

因此,优化的核心逻辑非常简单:NAS 保留所有“有状态”组件(持久化向量库、文件解析、Agent 上下文调度、只读挂载知识库),而计算部分通过 llama.cpp RPC 或局域网 API 按需向工作机发起调用。平时工作站休眠,NAS 待机功耗仅 15W~20W;发起复杂检索任务时动态请求计算卡,既保证了数据不出内网,又彻底解决了性能瓶颈。

二、NAS 端核心服务部署(Docker Compose)

在 NAS 端,我们使用 Docker 部署核心基座:轻量向量数据库 Qdrant、文档解析与工作流引擎以及用于流式中转的 API 网关。以下是我目前在用的精简编排配置:

version: '3.8'

services:
  # 轻量高效的 Rust 向量数据库,单容器内存占用常驻<100MB
  qdrant:
    image: qdrant/qdrant:v1.9.0
    container_name: local-qdrant
    restart: unless-stopped
    ports:
      - "6333:6333"
    volumes:
      - /volume1/docker/qdrant/storage:/qdrant/storage:z
    environment:
      - QDRANT__SERVICE__MAX_REQUEST_SIZE_MB=64

  # NAS 本机构建轻量 Embedding 服务(CPU 运行小模型完全不卡)tei-embedding:
    image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.5
    container_name: local-tei
    restart: unless-stopped
    ports:
      - "8081:80"
    volumes:
      - /volume1/docker/huggingface_cache:/data:z
    command: >
      --model-id BAAI/bge-small-zh-v1.5
      --port 80
      --max-batch-tokens 8192

  # 局域网反向代理与 WOL 唤醒脚本宿主
  agent-bridge:
    image: python:3.11-slim
    container_name: agent-bridge
    restart: unless-stopped
    working_dir: /app
    volumes:
      - /volume1/docker/agent-bridge:/app:rw
      - /volume1/documents:/documents:ro # 只读挂载个人核心资料
    ports:
      - "8000:8000"
    command: >
      sh -c "pip install -i https://pypi.tuna.tsinghua.edu.cn/simple fastapi uvicorn requests httpx wakeonlan &&
             python main.py"

这里有两个配置细节是踩坑换来的经验:

  1. Embedding 单独剥离给 NAS CPU:不要把向量化(Embedding)交给外部 GPU。使用 HuggingFace 的 Text-Embeddings-Inference(TEI)运行bge-small-zh-v1.5,NAS 那颗 N100 处理一个段落只需几毫秒,完全不需要唤醒独立显卡,NAS 全天候索引新文件几乎零感知。
  2. 私有资料务必以只读(:ro)方式挂载 :Agent 赋予了代码执行和文件读取能力,为了避免 Prompt Injection 导致本地文件被误删,挂载本地存储池时一定要强制打上:ro 标签。

三、动态唤醒与计算卸载实现

在 agent-bridge 中,我们编写一个轻量级代理服务:负责监听 Agent 发来的 LLM 推理请求。如果局域网内的 GPU 服务器未在线,先通过 WOL 魔法包将其唤醒;如果显卡在线,直接流式转发 llama.cpp 的 OpenAI 兼容接口。

# agent-bridge/main.py 片段
import time
import httpx
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
from wakeonlan import send_magic_packet

app = FastAPI()

GPU_HOST = "http://192.168.1.150:8080" # 远端工作站的 llama-server 地址
GPU_MAC = "AA:BB:CC:DD:EE:FF"

def ensure_worker_online():
    """检查远端 LLM 服务是否存活,不在线则唤醒"""
    try:
        r = httpx.get(f"{GPU_HOST}/health", timeout=1.0)
        if r.status_code == 200:
            return True
    except (httpx.ConnectError, httpx.TimeoutException):
        pass

    # 发送 WOL 唤醒包
    print("[Agent-Bridge] GPU 节点离线,发送 WOL 魔法包...")
    send_magic_packet(GPU_MAC)
    
    # 轮询等待节点上线,超时 45 秒
    start = time.time()
    while time.time() - start < 45:
        time.sleep(3)
        try:
            r = httpx.get(f"{GPU_HOST}/health", timeout=1.5)
            if r.status_code == 200:
                print("[Agent-Bridge] GPU 节点已恢复就绪!")
                return True
        except Exception:
            continue
    raise RuntimeError("GPU 节点未能如期拉起,任务中止")

@app.post("/v1/chat/completions")
async def chat_proxy(request: Request):
    ensure_worker_online()
    body = await request.body()
    client = httpx.AsyncClient(timeout=120.0)
    
    req = client.build_request(
        method="POST",
        url=f"{GPU_HOST}/v1/chat/completions",
        headers={k: v for k, v in request.headers.items() if k.lower() != "host"},
        content=body
    )
    res = await client.send(req, stream=True)
    return StreamingResponse(res.aiter_raw(),
        status_code=res.status_code,
        headers=dict(res.headers)
    )

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

通过这套仅几十行的 FastAPI 中转代理,NAS 对外暴露标准 OpenAI 接口(http://nas-ip:8000/v1),你的 Dify、FastGPT 或自建 LangChain 脚本只要把基地址指向 NAS 即可,完全不需要关心后端算力节点是否在线。

四、实测吞吐与日常功耗对比

我在实验室环境中对两种部署策略进行了实测,模型统一选用Qwen2.5-7B-Instruct-Q4_K_M,测试上下文为 2000 tokens 的中文技术文档分析:

部署方案 常驻整机功耗 推理耗时 (TTFT) 生成速度 (Tokens/s) 体验评价
NAS 全 CPU 硬跑 (N100 16G) 约 26W (满载狂转) 9.8 秒 1.4 t/s 极度卡顿,无法用于实时 Agent
NAS 存储 + RTX 3060 远端按需唤醒 平时 14W / 激活 140W 0.4 秒 38.2 t/s 秒级响应,Agent 工具调用极其丝滑

可以看到,虽然把算力全部压给 NAS 看似“极客感”十足,但 1.4 tokens/ s 的速度在多步 Agent 规划中几乎是不可用的——Agent 光是解析一轮 Tool Call 可能就要花掉半分钟,很容易触发上层客户端超时重试。而改用 NAS 管理状态与检索 + 动态代理外部算力 的拓扑后,日常待机功耗几乎不受影响,同时获得了完全生产级的推理吞吐。

总结与避坑心得

在低功耗 NAS 上玩大模型与 Agent,最忌讳的就是把工作站全套微服务原封不动搬进 Docker 里。把 静态文件检索、Embedding 模型、向量数据落盘 这些轻吞吐、重 IO 的任务交给 NAS 的 NVMe 固态盘,NAS 能以不到 15W 的功耗稳稳吃下;而把昂贵的矩阵计算通过网络边界丢给具备专用 Tensor Core 的显卡节点,才是家庭与工作室私有 AI 落地的最优解。

最后提一个实操排障细节:如果你在 Windows 主机上使用 llama-server.exe 作为后端,记得在网卡高级设置中开启“Magic Packet 唤醒”并关闭“快速启动”,否则休眠状态下的网卡可能会彻底断电,导致 NAS 发送的 WOL 数据包石沉大海。

正文完
 0
评论(没有评论)