简介

在家用旧服务器挖到宝?本文带你 30 分钟完成一套「私有大模型+飞书机器人」全流程:用 1Panel 一键拉起 Ollama 和 Llama3.1:8b,Node.js 编写回调服务,再通过 FRP 实现无公网 IP 穿透。无需昂贵云 GPU,内网部署也能让同事在飞书里@AI 秒回消息。全程命令行复制即跑,附带踩坑记录,新手也能一次成功!
以下是2025年推荐的轻量级开源LLM模型列表,优化为在低资源设备(低显存GPU或纯CPU)上运行,参数规模1B-8B,量化版本(如Q4_K_M)通过Ollama或llama.cpp运行。内存需求基于2048 token上下文。

模型名称 参数规模 优劣 最低配置 进阶配置
Phi-3 Mini 3.8B : 高效推理,擅长代码、数学、Q&A;低内存占用,适合边缘设备;速度快(~20 tokens/s)。 : 深度任务不如大模型;可能需微调。 纯CPU: 8GB RAM,现代多核CPU(如i5+);Q4量化,~2-4GB占用,速度3-5 tokens/s。 GPU: 6-8GB VRAM(如RTX 3060),16GB RAM;速度20 tokens/s,支持长上下文。
Mistral 7B 7B : 省电70%,推理快(CPU上40 tokens/s);适合文本生成、总结、翻译;Apache 2.0许可。 : MMLU准确率83%,复杂多模态弱。 纯CPU: 4GB RAM(Q4量化),多核CPU;~4-7GB占用,速度10-15 tokens/s。 GPU: 8-12GB VRAM(如RTX 4060),16GB RAM;vLLM优化,适合实时聊天。
Llama 3.1 8B 8B : 多语言(200+种,95%准确),性能与效率平衡;强于内容创建、翻译。 : 资源需求较高;量化后精度略降。 纯CPU: 8GB RAM(Q4量化),i5+ CPU;~4-8GB占用,速度8-12 tokens/s。 GPU: 12GB VRAM(如RTX 3060),32GB RAM;支持70B变体需多GPU。
Gemma 4B 4B : 多模态(文本+视觉),128K上下文;适合RAG、聊天助手;边缘高效。 : 基本任务为主,复杂推理弱;需许可。 纯CPU: 4GB RAM(Q4量化),入门CPU;~3-6GB占用,速度中等。 GPU: 6-7GB VRAM(如GTX 1650),16GB RAM;多语言任务效率高。
TinyLlama 1.1B : 超小,适合移动/IoT;低延迟,文本生成/总结;极低资源占用。 : 任务深度有限,高级推理弱。 纯CPU: 2GB RAM,任意现代CPU;~1-2GB占用,速度3-5 tokens/s。 GPU: 4GB VRAM(如集成显卡),8GB RAM;适合脚本自动化。
Qwen 2.5 7B 7B : 多模态(文本+图像),强数学/代码;文档理解优秀。 : 大变体资源高;多语言非完美。 纯CPU: 16GB RAM(Q4量化),Ryzen 5+;~4-7GB占用。 GPU: 4GB VRAM(如MX550),32GB RAM;适合图像生成任务。
DeepSeek R1 8B 8B : 推理/代码优秀(数学98%准确);适合研究/分析;低计算足迹。 : 资源稍高;需优化量化。 纯CPU: 8GB RAM(Q4量化),i7+;~4-7GB占用。 GPU: 6GB VRAM(如RTX 3060),32GB RAM;适合金融/科学模拟。

Ps:本文章使用Llama 3.1 8B来作为演示。

部署步骤

一、1Panel安装ollama

  • 安装1Panel
bash -c "$(curl -sSL https://resource.fit2cloud.com/1panel/package/v2/quick_start.sh)"
  • 安装ollama
    首先,在应用市场找到ollama,然后点击安装,如图:
    在这里插入图片描述
    然后,拉取模型:
ollama run llama3.1:8b

在这里插入图片描述
模型拉取成功后,我们就愉快的完成了第一步。
如果需要直接在本地客户端使用Page Assist访问,则需要更改参数添加环境变量,然后点击重建:
在这里插入图片描述

二、Node.js中间件编写(必需)

该中间件仅为本博客测试使用的demo,可以自行更换更加适合自己的哈!

const express = require('express');
const crypto = require('crypto');
const axios = require('axios');
const NodeCache = require('node-cache');
const { v4: uuidv4 } = require('uuid');
const winston = require('winston');

const app = express();
app.use(express.json());

// 配置参数
const CONFIG = {
  FEISHU_APP_ID: process.env.FEISHU_APP_ID || "your_app_id",
  FEISHU_APP_SECRET: process.env.FEISHU_APP_SECRET || "your_app_secret",
  OLLAMA_API_BASE: process.env.OLLAMA_API_BASE || "http://localhost:11434",
  PORT: process.env.PORT || 3000,
  REQUEST_TIMEOUT: parseInt(process.env.REQUEST_TIMEOUT) || 10000,
  CACHE_TTL: parseInt(process.env.CACHE_TTL) || 300, // 5分钟
  MAX_RETRIES: parseInt(process.env.MAX_RETRIES) || 3
};

// 初始化缓存 - 用于消息去重和访问令牌缓存
const cache = new NodeCache({ stdTTL: CONFIG.CACHE_TTL, checkperiod: 60 });

// 初始化日志
const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' }),
    new winston.transports.Console({
      format: winston.format.simple()
    })
  ]
});

// 请求重试机制
const axiosWithRetry = axios.create();
axiosWithRetry.interceptors.response.use(undefined, async (err) => {
  const config = err.config;
  if (!config || !config.retry) {
    return Promise.reject(err);
  }
  
  config.retryCount = config.retryCount || 0;
  const shouldRetry = config.retryCount < config.retry;
  
  if (shouldRetry) {
    config.retryCount += 1;
    const delay = Math.pow(2, config.retryCount) * 1000;
    await new Promise(resolve => setTimeout(resolve, delay));
    return axiosWithRetry(config);
  }
  
  return Promise.reject(err);
});

// 飞书URL验证和事件处理
app.post('/feishu/webhook', async (req, res) => {
  const { challenge, token, type, header } = req.body;
  
  // 1. 处理URL验证
  if (type === 'url_verification') {
    logger.info("Received verification request");
    return res.json({ challenge });
  }

  // 2. 验证请求签名(可选)
  if (!verifyFeishuSignature(req)) {
    logger.warn("Invalid signature detected");
    return res.status(401).json({ error: "Invalid signature" });
  }

  // 3. 处理消息去重
  const eventId = header.event_id;
  if (cache.has(`event_${eventId}`)) {
    logger.info(`Duplicate event ${eventId} received, ignoring`);
    return res.json({ code: 0, msg: "success" });
  }
  cache.set(`event_${eventId}`, true);

  // 4. 立即响应飞书,避免超时
  res.json({ code: 0, msg: "success" });

  // 5. 异步处理消息事件
  if (header.event_type === 'im.message.receive_v1') {
    processMessageEvent(req.body).catch(err => {
      logger.error("Error processing message event:", err);
    });
  }
});

// 处理消息事件的异步函数
async function processMessageEvent(eventData) {
  const { event } = eventData;
  const message = event.message;
  const chatId = message.chat_id;
  const messageId = message.message_id;
  const msgContent = JSON.parse(message.content);
  const userInput = msgContent.text;

  logger.info(`Processing message ${messageId} from chat ${chatId}`);

  try {
    // 调用Ollama API
    const ollamaResponse = await axiosWithRetry.post(
      `${CONFIG.OLLAMA_API_BASE}/api/generate`,
      {
        model: "llama3.1:8b",
        prompt: userInput,
        stream: false
      },
      {
        timeout: CONFIG.REQUEST_TIMEOUT,
        retry: CONFIG.MAX_RETRIES
      }
    );

    const ollamaReply = ollamaResponse.data.response;

    // 发送回复到飞书
    await replyToFeishu(chatId, ollamaReply, messageId);
    logger.info(`Successfully replied to message ${messageId}`);
  } catch (error) {
    logger.error("Error processing message:", error);
    await replyToFeishu(
      chatId, 
      "抱歉,处理您的请求时出错了,请稍后重试。", 
      messageId
    ).catch(err => {
      logger.error("Failed to send error message to Feishu:", err);
    });
  }
}

// 发送消息回飞书(支持重试)
async function replyToFeishu(chatId, text, messageId) {
  const accessToken = await getFeishuAccessToken();
  const replyUrl = 'https://open.feishu.cn/open-apis/im/v1/messages';
  
  try {
    await axiosWithRetry.post(
      replyUrl,
      {
        receive_id: chatId,
        msg_type: "text",
        content: JSON.stringify({ text: text }),
        uuid: uuidv4() // 防止重复发送
      },
      {
        params: { receive_id_type: 'chat_id' },
        headers: {
          'Authorization': `Bearer ${accessToken}`,
          'Content-Type': 'application/json'
        },
        timeout: CONFIG.REQUEST_TIMEOUT,
        retry: CONFIG.MAX_RETRIES
      }
    );
  } catch (error) {
    logger.error("Failed to reply to Feishu:", error.response?.data || error.message);
    throw error;
  }
}

// 获取飞书访问令牌(带缓存)
async function getFeishuAccessToken() {
  const cacheKey = 'feishu_access_token';
  const cachedToken = cache.get(cacheKey);
  
  if (cachedToken) {
    return cachedToken;
  }

  const url = 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/';
  
  try {
    const response = await axios.post(url, {
      app_id: CONFIG.FEISHU_APP_ID,
      app_secret: CONFIG.FEISHU_APP_SECRET
    }, {
      timeout: CONFIG.REQUEST_TIMEOUT
    });

    const { tenant_access_token, expire } = response.data;
    
    // 缓存令牌,提前60秒过期
    cache.set(cacheKey, tenant_access_token, expire - 60);
    
    return tenant_access_token;
  } catch (error) {
    logger.error("Failed to get Feishu access token:", error.response?.data || error.message);
    throw error;
  }
}

// 飞书请求签名验证(可选)
function verifyFeishuSignature(req) {
  // 在实际生产环境中,应该验证飞书请求的签名
  // 这里简化处理,实际应根据飞书文档实现签名验证
  // 参考: https://open.feishu.cn/document/ukTMukTMukTM/ukTNz4SO2MjL5czM/auth-v3/auth/request-authentication
  return true; // 生产环境中应实现真正的签名验证
}

// 健康检查端点
app.get('/health', (req, res) => {
  res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() });
});

// 关闭处理
process.on('SIGTERM', () => {
  logger.info('SIGTERM received, shutting down gracefully');
  server.close(() => {
    logger.info('Process terminated');
  });
});

// 启动服务器
const server = app.listen(CONFIG.PORT, () => {
  logger.info(`Server is running on port ${CONFIG.PORT}`);
});

module.exports = app; // 用于测试

主要作用
接收:使用用一个 HTTP 服务器监听飞书平台发送过来的消息事件。
处理:理解这个消息,然后调用你本地的 Ollama API(通过 axios 库发送请求)。
发送:接收 Ollama 返回的文本,再按照飞书要求的格式打包好,发送回飞书平台。
重要提示
将你的应用App ID 和 你的应用App Secret 替换为你在飞书开放平台创建应用时获取的实际值。
如果 Ollama 没有和这段 Node.js 代码运行在同一台机器上,请将 OLLAMA_API_BASE 中的 localhost 改为之后你运行的frp的端口。
依赖安装

npm install express axios
npm install node-cache uuid@^8.3.2 winston

启动

#直接启动
node index.js
#守护进程启动
npm install -g pm2
pm2 start index.js --name feishu-bot

在这里插入图片描述

三、frp穿透

  • 下载 frp
    可以参考这篇博客:https://blog.csdn.net/weixin_57025326/article/details/135466796?fromshare=blogdetail&sharetype=blogdetail&sharerId=135466796&sharerefer=PC&sharesource=weixin_57025326&sharefrom=from_link

  • 服务端配置

# 基础连接端口,frpc 必须连这里
bind_port = 20000

# HTTPS 虚拟主机统一入口(单端口模式)
vhost_https_port = 20001

# 客户端令牌,frpc 必须填相同值
token = "123456"

# 日志落盘路径,空 = 不写文件
log_file = "/var/log/frps.log"

# 日志级别:trace/debug/info/warn/error
log_level = "debug"

# 只保留最近 3 天日志,自动轮转
log_max_days = 3

# 开启心跳校验,防止假死连接
authentication_heartbeats = true

# 900 秒内没心跳则踢掉客户端(秒)
authentication_timeout = 900

# 每个客户端最多复用 5 条连接
max_pool_count = 5
  • 客户端配置
# ========== 通用配置 ==========
# frps 服务器地址(域名或 IP)
server_addr = youip
# frps 监听端口,与服务器 bind_port 保持一致
server_port = 20000
# 与服务器相同的令牌,验证通过才允许连接
token = 123456

# ========== Ollama 穿透 ==========
[ollama]
# 协议类型:HTTPS 虚拟主机
type = https
# 本机 Ollama 监听端口
local_port = 11434
# 远程 HTTPS 入口端口(与 frps vhost_https_port 相同)
remote_port = 20001
# 访问域名(需解析到 frps 服务器)
custom_domains = www.test1.com
# 把 HTTPS 请求转成 HTTP 再发给本地
plugin = https2http
# 本地真实服务地址
plugin_local_addr = 127.0.0.1:11434
# 改写 Host 头为 127.0.0.1(规避部分服务校验)
plugin_host_header_rewrite = 127.0.0.1
# 证书与私钥路径( frps 侧插件需要)
plugin_crt_path = /usr/local/frp/ssl/test.pem
plugin_key_path = /usr/local/frp/ssl/test.key

# ========== 中间件穿透 ==========
[Node.js]
type = https
# 本机监听端口
local_port = 3000
# 同样使用 20001 端口,靠域名区分
remote_port = 20001
# 不同子域名
custom_domains = test.test1.com
plugin = https2http
plugin_local_addr = 127.0.0.1:3000
plugin_host_header_rewrite = 127.0.0.1
plugin_crt_path = /usr/local/frp/ssl/test.pem
plugin_key_path = /usr/local/frp/ssl/test.key

Ps:我使用的是云服务器做的frps服务,所以配置好后,还需要将相应的端口在安全组或防火墙开通。
上面的证书,如果你有真实域名的话,可以参考下面的链接使用acme申请证书:https://blog.csdn.net/weixin_57025326/article/details/136286399?fromshare=blogdetail&sharetype=blogdetail&sharerId=136286399&sharerefer=PC&sharesource=weixin_57025326&sharefrom=from_link

四、飞书应用创建

  • 登录 飞书开放平台
    在这里插入图片描述
  • 点击“创建企业自建应用”,填写应用名称和描述。
  • 进入应用后,在左侧找到 “权限管理”,为你的机器人添加以下权限
    im:message (获取用户发给机器人的消息)
    im:message:send_as_bot (以机器人身份发送消息)
  • 在左侧找到 “事件订阅”,进行配置
    请求网址 URL:填写你的服务器公网 URL,并加上路径,例如 https://your-public-domain.com:20001/feishu/webhook。飞书会向这个 URL 发送验证请求和消息事件。
    订阅事件:添加 接收消息 事件。
  • 发布
    在左侧找到 “应用发布” -> “版本管理与发布”,创建并发布一个版本。发布后,在飞书客户端中搜索你的应用名称并添加。

结论

折腾完这个项目,Llama 3.1 8B在1Panel的Ollama容器里跑得挺顺,Node.js服务跟飞书对接也还行,FRP把内网外网打通还是挺好玩的,虽然有点笨,但不是不能用。接下来想试试压缩模型省内存,或者加点多模态功能。有啥好玩的点子?评论区来聊!

###############################################################################################################

今日推荐

小说:《没钱修什么仙》
小说简介:老者:“你想报仇?”
少年:“我被强者反复侮辱,被师尊视为垃圾,我怎么可能不想报仇?”
老者摸了摸少年的脑袋,叹道:“好孩子,我来传功给你吧。”
少年惊道:“前辈!这怎么行?”
老者伸出手:“把你手机给我。”
少年看着手机上的变化,震惊道:“前辈!这哪里来的百年功力?”
老者微微一笑:“好孩子,这是你在天庭的备用功力,以后急用的时候随用随取,别再被人侮辱了。”
少年皱眉:“这不是法力贷吗?我怕……”
老者:“天庭是大平台,新用户借百年功力有30天免息,日息最低半天功力,还没你吐纳一周天多。”
……
张羽冷哼一声,关掉了上面的广告。
在这里插入图片描述

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐