分类: 杂项

微信内置ocr私有部署

本文主要在linux使用
系统为Debian12
注意 在云服务器部署需要测试服务器是否可以使用,我在阿里云上测试 与本地效果差别特别大
别人封装好的docker镜像

docker pull golangboyme/wxocr:latest
docker run -d -p 5000:5000 --name wechat-ocr-api golangboyme/wxocr

依赖项目
https://github.com/swigger/wechat-ocr/tree/master/src
编译的时候需要用高本版的python 有一个最低版本我忘记了 用3.12以上肯定没问题

mkdir build
cd build 
cmake..
make

得到一个python的模块
其他相关的文件可以从微信的linux安装包找到

python调用代码

import wcocr
import os
import uuid
import base64
from flask import Flask, request, jsonify, render_template, send_from_directory

app = Flask(__name__)
wcocr.init("./wx/opt/wechat/wxocr", "./wx/opt/wechat")

@app.route("/ocr", methods=["POST"])
def ocr():
    try:
        # Get base64 image from request
        image_data = request.json.get("image")
        if not image_data:
            return jsonify({"error": "No image data provided"}), 400
        # Extract image type from base64 data
        image_type, base64_data = extract_image_type(image_data)
        if not image_type:
            return jsonify({"error": "Invalid base64 image data"}), 400

        # Create temp directory if not exists
        temp_dir = "temp"
        if not os.path.exists(temp_dir):
            os.makedirs(temp_dir)

        # Generate unique filename and save image
        filename = os.path.join(temp_dir, f"{str(uuid.uuid4())}.{image_type}")
        try:
            image_bytes = base64.b64decode(base64_data)
            with open(filename, "wb") as f:
                f.write(image_bytes)

            # Process image with OCR
            result = wcocr.ocr(filename)
            return jsonify({"result": result})

        finally:
            # Clean up temp file
            if os.path.exists(filename):
                os.remove(filename)

    except Exception as e:
        return jsonify({"error": str(e)}), 500

# 创建静态文件夹
static_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
if not os.path.exists(static_dir):
    os.makedirs(static_dir)

def extract_image_type(base64_data):
    # Check if the base64 data has the expected prefix
    if base64_data.startswith("data:image/"):
        # Extract the image type from the prefix
        prefix_end = base64_data.find(";base64,")
        if prefix_end != -1:
            return (
                base64_data[len("data:image/") : prefix_end],
                base64_data.split(";base64,")[-1],
            )
    return "png", base64_data

@app.route("/")
def index():
    return render_template("index.html")

# Handle unsupported methods for /ocr route
@app.route("/ocr", methods=["GET", "PUT", "DELETE", "PATCH"])
def unsupported_method():
    return jsonify({"error": "Method not allowed"}), 405

# Handle non-existent paths
@app.errorhandler(404)
def not_found(e):
    return jsonify({"error": "Resource not found"}), 404

if __name__ == "__main__":
    # 确保templates目录存在
    templates_dir = os.path.join(
        os.path.dirname(os.path.abspath(__file__)), "templates"
    )
    if not os.path.exists(templates_dir):
        os.makedirs(templates_dir)

    # 确保temp目录存在
    temp_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "temp")
    if not os.path.exists(temp_dir):
        os.makedirs(temp_dir)

    app.run(host="0.0.0.0", port=5000, threaded=True)

html文件

<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>微信OCR文字识别工具</title>
    <style>
        :root {
            --primary-color: #07c160;
            --secondary-color: #576b95;
        }

        body {
            font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
            max-width: 1200px;
            margin: 0 auto;
            padding: 20px;
            background: #f5f5f5;
            color: #333;
        }

        .container {
            background: white;
            border-radius: 12px;
            padding: 30px;
            box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
        }

        h1 {
            color: var(--primary-color);
            text-align: center;
            margin-bottom: 30px;
        }

        .upload-section {
            border: 2px dashed #ddd;
            border-radius: 8px;
            padding: 30px;
            text-align: center;
            margin: 20px 0;
            transition: all 0.3s;
        }

        .upload-section:hover {
            border-color: var(--primary-color);
            background: #f8fff9;
        }

        .preview-image {
            max-width: 100%;
            max-height: 400px;
            margin: 20px 0;
            border-radius: 8px;
            display: none;
        }

        .input-group {
            margin: 20px 0;
        }

        input[type="file"],
        input[type="text"] {
            width: 100%;
            padding: 12px;
            border: 1px solid #ddd;
            border-radius: 6px;
            margin: 10px 0;
        }

        button {
            background: var(--primary-color);
            color: white;
            border: none;
            padding: 12px 30px;
            border-radius: 6px;
            cursor: pointer;
            font-size: 16px;
            transition: all 0.3s;
        }

        button:hover {
            opacity: 0.9;
            transform: translateY(-1px);
        }

        .result-table {
            width: 100%;
            border-collapse: collapse;
            margin-top: 20px;
            display: none;
        }

        .result-table th,
        .result-table td {
            padding: 12px;
            border: 1px solid #eee;
            text-align: left;
        }

        .result-table th {
            background: var(--secondary-color);
            color: white;
        }

        .loading {
            display: none;
            text-align: center;
            color: var(--primary-color);
            margin: 20px 0;
        }

        .error {
            color: #ff4d4f;
            margin: 10px 0;
            display: none;
        }

        .image-container {
            position: relative;
            margin: 20px 0;
            display: inline-block;
        }

        .text-box {
            position: absolute;
            border: 2px solid var(--primary-color);
            background-color: rgba(7, 193, 96, 0.1);
            cursor: pointer;
        }

        .text-tooltip {
            position: absolute;
            background: white;
            border: 1px solid #ddd;
            padding: 8px;
            border-radius: 4px;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
            z-index: 100;
            display: none;
            max-width: 300px;
            word-break: break-word;
        }

        .confidence {
            color: var(--primary-color);
            font-weight: bold;
        }
    </style>
</head>

<body>
    <div class="container">
        <h1>微信OCR文字识别工具</h1>

        <!-- 上传区域 -->
        <div class="upload-section">
            <div class="input-group">
                <input type="file" id="fileInput" accept="image/*">
                <p>或拖拽图片到此区域</p>
                <input type="text" id="urlInput" placeholder="输入图片URL地址">
            </div>
            <button onclick="processImage()">开始识别</button>
        </div>

        <!-- 图片预览 -->
        <div class="image-container" id="imageContainer">
            <img id="preview" class="preview-image">
            <!-- 文本框将在这里动态添加 -->
        </div>

        <!-- 加载状态 -->
        <div class="loading" id="loading">识别中,请稍候...</div>

        <!-- 错误提示 -->
        <div class="error" id="error"></div>

        <!-- 结果显示 -->
        <table class="result-table" id="resultTable">
            <thead>
                <tr>
                    <th>文本内容</th>
                    <th>置信度</th>
                    <th>位置信息 (左, 上, 右, 下)</th>
                </tr>
            </thead>
            <tbody id="resultBody"></tbody>
        </table>

        <!-- 使用说明 -->
        <h2>API接口说明</h2>
        <h3>请求方式</h3>
        <pre>POST /ocr</pre>

        <h3>请求示例</h3>
        <pre>
{
  "image": "BASE64_ENCODED_IMAGE_DATA"
}</pre>

        <h3>返回示例</h3>
        <pre id="responseSample"></pre>
    </div>

    <script>
        // 默认的API地址
        const API_ENDPOINT = window.location.origin + '/ocr';

        // 初始化拖放功能
        const uploadSection = document.querySelector('.upload-section');
        uploadSection.addEventListener('dragover', (e) => {
            e.preventDefault();
            uploadSection.style.backgroundColor = '#f0fff0';
        });

        uploadSection.addEventListener('drop', (e) => {
            e.preventDefault();
            uploadSection.style.backgroundColor = '';
            const file = e.dataTransfer.files[0];
            handleFile(file);
        });

        // 处理文件选择
        document.getElementById('fileInput').addEventListener('change', function (e) {
            handleFile(e.target.files[0]);
        });

        // 处理文件上传
        async function handleFile(file) {
            if (!file) return;
            if (!file.type.startsWith('image/')) {
                showError('请上传图片文件');
                return;
            }

            // 显示预览图片
            const reader = new FileReader();
            reader.onload = (e) => {
                document.getElementById('preview').src = e.target.result;
                document.getElementById('preview').style.display = 'block';

                // 清除之前的文本框
                const imageContainer = document.getElementById('imageContainer');
                const existingBoxes = imageContainer.querySelectorAll('.text-box, .text-tooltip');
                existingBoxes.forEach(box => box.remove());
            };
            reader.readAsDataURL(file);
        }

        // 开始处理图像
        async function processImage() {
            const file = document.getElementById('fileInput').files[0];
            const url = document.getElementById('urlInput').value;
            let base64Data = '';

            try {
                showLoading();
                clearError();

                if (file) {
                    base64Data = await fileToBase64(file);
                } else if (url) {
                    base64Data = await urlToBase64(url);
                } else {
                    showError('请选择图片或输入图片URL');
                    return;
                }

                // 发送请求
                const response = await fetch(API_ENDPOINT, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({ image: base64Data })
                });

                const data = await response.json();
                handleResponse(data);
            } catch (error) {
                showError(`请求失败:${error.message}`);
            } finally {
                hideLoading();
            }
        }

        // 处理响应数据
        function handleResponse(data) {
            // 处理新的响应结构
            const resultData = data.result || data;

            if (resultData.errcode !== 0) {
                showError(`识别失败,错误码:${resultData.errcode}`);
                return;
            }

            // 显示结果表格
            const tbody = document.getElementById('resultBody');
            tbody.innerHTML = '';
            resultData.ocr_response.forEach(item => {
                const row = document.createElement('tr');
                row.innerHTML = `
                    <td>${item.text}</td>
                    <td>${(item.rate * 100).toFixed(2)}%</td>
                    <td>(${item.left.toFixed(1)}, ${item.top.toFixed(1)}, 
                        ${item.right.toFixed(1)}, ${item.bottom.toFixed(1)})</td>
                `;
                tbody.appendChild(row);
            });
            document.getElementById('resultTable').style.display = 'table';

            // 在图片上绘制识别区域
            drawTextBoxes(resultData.ocr_response, resultData.width, resultData.height);
        }

        // 在图片上绘制文本框
        function drawTextBoxes(ocrResults, originalWidth, originalHeight) {
            const imageContainer = document.getElementById('imageContainer');
            const preview = document.getElementById('preview');

            // 清除之前的文本框
            const existingBoxes = imageContainer.querySelectorAll('.text-box, .text-tooltip');
            existingBoxes.forEach(box => box.remove());

            // 获取图片的实际显示尺寸和位置
            const imgRect = preview.getBoundingClientRect();
            const containerRect = imageContainer.getBoundingClientRect();

            // 计算图片相对于容器的偏移
            const offsetX = imgRect.left - containerRect.left;
            const offsetY = imgRect.top - containerRect.top;

            // 计算缩放比例
            const scaleX = imgRect.width / originalWidth;
            const scaleY = imgRect.height / originalHeight;

            // 为每个识别结果创建文本框
            ocrResults.forEach((item, index) => {
                // 创建文本框
                const textBox = document.createElement('div');
                textBox.className = 'text-box';

                // 精确定位文本框,考虑图片在容器中的偏移
                const left = item.left * scaleX + offsetX;
                const top = item.top * scaleY + offsetY;
                const width = (item.right - item.left) * scaleX;
                const height = (item.bottom - item.top) * scaleY;

                // 设置文本框位置和大小
                textBox.style.left = `${left}px`;
                textBox.style.top = `${top}px`;
                textBox.style.width = `${width}px`;
                textBox.style.height = `${height}px`;
                textBox.dataset.index = index;

                // 创建提示框
                const tooltip = document.createElement('div');
                tooltip.className = 'text-tooltip';
                tooltip.innerHTML = `
                    <div>${item.text}</div>
                    <div class="confidence">置信度: ${(item.rate * 100).toFixed(2)}%</div>
                `;

                // 添加鼠标事件
                textBox.addEventListener('mouseenter', function (e) {
                    tooltip.style.left = `${e.pageX - imageContainer.offsetLeft + 10}px`;
                    tooltip.style.top = `${e.pageY - imageContainer.offsetTop + 10}px`;
                    tooltip.style.display = 'block';
                });

                textBox.addEventListener('mousemove', function (e) {
                    tooltip.style.left = `${e.pageX - imageContainer.offsetLeft + 10}px`;
                    tooltip.style.top = `${e.pageY - imageContainer.offsetTop + 10}px`;
                });

                textBox.addEventListener('mouseleave', function () {
                    tooltip.style.display = 'none';
                });

                imageContainer.appendChild(textBox);
                imageContainer.appendChild(tooltip);
            });
        }

        // 工具函数
        function fileToBase64(file) {
            return new Promise((resolve, reject) => {
                const reader = new FileReader();
                reader.onload = () => resolve(reader.result);
                reader.onerror = error => reject(error);
                reader.readAsDataURL(file);
            });
        }

        async function urlToBase64(url) {
            const response = await fetch(url);
            const blob = await response.blob();
            return fileToBase64(blob);
        }

        function showLoading() {
            document.getElementById('loading').style.display = 'block';
        }

        function hideLoading() {
            document.getElementById('loading').style.display = 'none';
        }

        function showError(message) {
            const errorDiv = document.getElementById('error');
            errorDiv.textContent = message;
            errorDiv.style.display = 'block';
        }

        function clearError() {
            document.getElementById('error').style.display = 'none';
        }

        // 初始化示例响应显示
        document.getElementById('responseSample').textContent = JSON.stringify({
            "result": {
                "errcode": 0,
                "height": 258,
                "imgpath": "temp/0cfbda36-a05d-4cba-9a72-cec6833d305d.png",
                "ocr_response": [
                    {
                        "bottom": 41.64999771118164,
                        "left": 33.6875,
                        "rate": 0.9951504468917847,
                        "right": 164.76248168945312,
                        "text": "API接口说明",
                        "top": 18.98750114440918
                    }
                ],
                "width": 392
            }
        }, null, 2);
    </script>
</body>

</html>

Agent、RAG、Function Call与MCP

Agent(智能体)
RAG(检索增强生成)
Function Call(函数调用)
MCP(模型上下文协议)

特性 MCP (模型上下文协议) RAG (检索增强生成) Agent (智能体) Function Call (函数调用)
核心思想 标准化 AI 与外部数据/工具的通信协议 检索外部知识 + 增强提示 + 生成回答 LLM驱动的自主决策与任务执行系统 LLM请求执行外部预定义函数/工具的能力
本质 协议/规范 技术框架/方法 系统/应用范式 模型能力/特性
通俗比喻 标准化的 USB 接口 写论文前先查资料 能干的私人助理 助理按指令使用 App
关系链 可作为 Agent 调用工具的底层标准 常被 Agent 用作获取知识的手段 核心指挥官,使用 RAG/Function Call 等工具 Agent 执行具体动作的基本手段

简单来说,它们的关系就像:

• Agent (智能体) 是那个目标导向的项目经理/大脑。
• RAG 和 Function Call 是它工具箱里的得力工具:RAG 负责查资料、找依据;Function Call 负责执行具体操作、调用外部 API。
• MCP 则致力于提供一个标准化的接口规范,让 Agent 能更方便、更统一地接入和使用各种工具(无论是 RAG 功能还是其他 Function Call 实现的工具)。

• 这是啥? Function Call 是 LLM 的一项内置“特异功能”。它允许 LLM 在需要的时候,请求外部程序帮它做点事。注意,是“请求”,不是“亲自做”。
• 为啥要它? 因为 LLM 自己查不了实时股价、订不了机票、发不了邮件。有了 Function Call,LLM 就能“指挥”其他工具来完成这些操作。
• 通俗比喻: 就像你让智能音箱帮你“查下今天北京天气”。音箱(LLM)自己感知不到天气,但它知道要去调用“天气查询”这个App(预定义的函数/工具)。它生成指令(“查北京天气”),App 执行后把结果(“晴,25度”)告诉音箱,音箱再用自然语言告诉你。
• 简单例子: 你问 AI:“AAPL 股价多少?” AI 判断需要查实时数据,于是生成一个“请求”:{调用函数: "查股价", 参数: {"股票代码": "AAPL"}}。外部程序收到请求,查询API,返回结果 {"价格": 180.50}。AI 再根据这个结果回答你:“苹果当前股价是 180.50 美元。”

• 这是啥? RAG (Retrieval-Augmented Generation) 是一种让 AI 回答更靠谱的技术框架。简单说,就是在 AI 回答问题 之前,先让它去指定的资料库(比如公司内部文档、最新的行业报告)里查找 (Retrieval) 相关信息。
• 为啥要它? 防止 AI一本正经地“胡说八道”(专业术语叫“幻觉”),让它的回答基于最新的、准确的、特定的事实依据。
• 通俗比喻: 好比你写论文要引用最新数据。你不会光凭记忆(LLM 的内部知识)瞎写,而是会先去图书馆或数据库查资料 (检索),把找到的关键信息整合 (增强)进你的论据里,最后才下笔写作 (生成)。RAG 就是让 AI 也学会这种“先查再答”的好习惯。
• 简单例子: 你问 AI:“我们公司最新的报销政策是啥?” RAG 系统先去公司内部知识库检索“报销政策”文档,找到相关段落。然后把这些段落和你的问题一起“喂”给 AI,AI 参考着这些最新政策,给你一个准确的回答。

• 这是啥? Agent(智能体)是一个更高级、更自主的 AI 系统。它以 LLM 作为核心“大脑”,不仅能理解你的目标,还能自己思考、规划步骤,并主动调用工具(比如上面说的 RAG 和 Function Call)来执行任务,与外部环境互动。
• 为啥要它? 为了完成那些光靠聊天解决不了的复杂任务,比如“帮我规划下周去上海的出差行程,包括订机票酒店,并把日程发给我”。
• 通俗比喻: Agent 就像一个超级能干的私人助理。你给个目标,它自己就能拆解任务、查信息(可能用 RAG 查公司差旅标准,用 Function Call 查航班酒店)、做决策、执行操作(用 Function Call 调用预订 API),最后给你结果。它是有自主“行动力”的。
• 简单例子: 你让 Agent:“分析一下竞品 X 的最新动态,写个简报。” Agent 会自己规划:① 搜索最新新闻(调用 Function Call);② 查内部研究报告(调用 RAG);③ 分析总结信息(LLM 大脑);④ 生成简报(调用 Function Call)。

• 这是啥? MCP (Model Context Protocol) 是 Anthropic 公司(就是搞出 Claude 那个)在 2024 年底提出并开源的一种标准化通信协议。它定义了一套规则,让 AI 应用(客户端)能以统一的方式,与各种外部数据源或工具(服务器)进行交互。
• 为啥要它? 想象一下,如果每个工具都有自己独特的接口,那 Agent 想用多个工具时,岂不是要学 N 种“方言”?MCP 就是想统一这个接口标准,让工具“即插即用”。
• 通俗比喻: MCP 就像是给 AI 大脑和外部工具之间制定了一个通用的 USB 接口标准。无论是本地文件系统、数据库,还是 Slack、GitHub 这些应用,只要它们提供符合 MCP 标准的“服务器”,AI 应用(客户端)就能轻松连接并使用它们的功能,无需为每个工具单独适配。
• 简单例子: 在支持 MCP 的编辑器里,你可以让 AI“把我 /docs 目录最新的 Markdown 文件总结一下,发到 Slack 的 #general 频道”。编辑器(MCP 客户端)通过 MCP 协议,与本地的“文件系统 MCP 服务器”和“Slack MCP 服务器”沟通,协调完成整个任务。

支持 MCP 的客户端/服务器:

• 客户端: Claude Desktop App, Cursor, Windsurf, Cherry Studio 等 AI 编辑器或应用。
• 服务器: Anthropic 官方和社区提供了针对 Google Drive, Slack, GitHub, Git, Postgres, Puppeteer, Milvus (向量数据库), Firecrawl (网页抓取) 等的开源 MCP 服务器实现。开发者也可以根据 MCP 规范自定义服务器。目前,为安全起见,MCP 服务器通常在本地运行。

SGLang 多节点集群部署Qwen系列大模型

比起Ollama的方便,有些时候高并发更重要,因此这篇文章将实现在两台电脑(双节点)上部署 SGLang(当然如果你还有多余的也可以加进来当节点),运行 Qwen2.5-7B-Instruct 模型,实现本地资源的充分利用。

硬件

• 节点 0:IP 192.168.0.12,1 个 英伟达显卡
• 节点 1:IP 192.168.0.13,1 个 英伟达显卡

模型

Qwen2.5-7B-Instruct,FP16 下约需 14GB 显存,使用 --tp 2 后每 GPU 约 7GB(权重)+ 2-3GB(KV 缓存)。

网络

两节点通过以太网(TCP)通信,网络接口为 eno1。

不量化

使用 FP16 精度以保留最大精度,显存占用较高,需优化配置。

操作系统

• 推荐 Ubuntu 20.04/22.04 或其他 Linux 发行版(Windows 不推荐,需 WSL2)
• 两节点最好是一致的环境,当然os的环境不是必须,但是Python的环境需要一样

网络连通性

• 节点 0(192.168.0.12)和节点 1(192.168.0.13)可互相 ping 通:

ping 192.168.0.12  # 从节点 1
ping 192.168.0.13  # 从节点 0

• 端口 50000(分布式初始化)和 30000(HTTP 服务器)未被防火墙阻挡:

sudo ufw allow 50000
sudo ufw allow 30000

• 确认网络接口 eno1:

# 具体网卡根据实际调整
ip addr show eno1

若 eno1 不存在,替换为实际接口(如 eth0 或 enp0s3)。

GPU 驱动和 CUDA

• 安装 NVIDIA 驱动(版本 ≥ 470)和 CUDA Toolkit(推荐 12.x):

nvidia-smi  # 确认驱动和 CUDA 版本

输出应显示 英伟达和 CUDA 版本(如 12.4)。

若未安装,参考 NVIDIA 官网 自行安装即可:

Python 环境

• Python 3.9+(推荐 3.10)
• 两节点需一致的 Python 版本:

python3 --version

磁盘空间

• Qwen2.5-7B-Instruct 模型约需 15GB 磁盘空间
• 确保 /opt/models/Qwen/Qwen2.5-7B-Instruct 路径有足够空间

在两节点上分别安装 SGLang 和依赖。以下步骤在每台电脑上执行。

创建虚拟环境(conda)

conda create -n sglang_env python=3.10
conda activate  sglang_env

安装 SGLang

备注: 安装过程会自动安装 对应显卡相关的依赖,如 torch,transformers,flashinfer等

pip install --upgrade pip
pip install uv
uv pip install "sglang[all]>=0.4.5" --find-links https://flashinfer.ai/whl/cu124/torch2.5/flashinfer-python

验证安装:

python -m sglang.launch_server --help

应显示 SGLang 的命令行参数帮助信息。

下载 Qwen2.5-7B-Instruct 模型

国外使用 huggingface,国内使用 modelscope
在两节点上下载模型到相同路径(如 /opt/models/Qwen/Qwen2.5-7B-Instruct):

pip install modelscope
modelscope download Qwen/Qwen2.5-7B-Instruct --local-dir /opt/models/Qwen/Qwen2.5-7B-Instruct

或手动从 Hugging Face 或者 modelscope 下载并解压到指定路径。确保两节点模型文件一致。

配置双节点部署

使用张量并行(--tp 2)将模型分布到 2 个 GPU(每节点 1 个)。以下是详细的部署步骤和命令。

部署命令

• 节点 0(IP: 192.168.0.12):

NCCL_IB_DISABLE=1 NCCL_P2P_DISABLE=1 GLOO_SOCKET_IFNAME=eno1 NCCL_SOCKET_IFNAME=eno1 python3 -m sglang.launch_server \
  --model-path /opt/models/Qwen/Qwen2.5-7B-Instruct \
  --tp 2 \
  --nnodes 2 \
  --node-rank 0 \
  --dist-init-addr 192.168.0.12:50000 \
  --disable-cuda-graph \
  --host 0.0.0.0 \
  --port 30000 \
  --mem-fraction-static 0.7

• 节点 1(IP: 192.168.0.13):

NCCL_IB_DISABLE=1 NCCL_P2P_DISABLE=1 GLOO_SOCKET_IFNAME=eno1 NCCL_SOCKET_IFNAME=eno1 python3 -m sglang.launch_server \
  --model-path /opt/models/Qwen/Qwen2.5-7B-Instruct \
  --tp 2 \
  --nnodes 2 \
  --node-rank 1 \
  --dist-init-addr 192.168.0.12:50000 \
  --disable-cuda-graph \
  --host 0.0.0.0 \
  --port 30000 \
  --mem-fraction-static 0.7

注意: 如果出现 OOM的情况则调整 --mem-fraction-static 参数,默认是 0.9,改为 0.7 即可。0.9 调整到0.7 时 当前7B模型 占用显存直接下降 2G左右。
CUDA Graph 会额外分配少量显存(通常几百 MB)来存储计算图。如果显存接近上限,启用 CUDA Graph 可能触发 OOM 错误。

参数说明

以下是命令中每个参数的详细解释,结合你的场景:

环境变量

• NCCL_IB_DISABLE=1:禁用 InfiniBand,因为你的节点通过以太网通信
• NCCL_P2P_DISABLE=1:禁用 GPU 间的 P2P 通信(如 NVLink),因为 GPU 在不同电脑上
• GLOO_SOCKET_IFNAME=eno1:指定 GLOO 分布式通信的网络接口(替换为实际接口,如 eth0)
• NCCL_SOCKET_IFNAME=eno1:指定 NCCL 通信的网络接口

SGLang 参数

• --model-path /opt/models/Qwen/Qwen2.5-7B-Instruct:模型权重路径,两节点必须一致
• --tp 2:张量并行,使用 2 个 GPU(每节点 1 个)。模型权重和计算任务平分到 2 个 GPU
• --nnodes 2:指定 2 个节点。tp_size (2) ÷ nnodes (2) = 1 GPU 每节点,满足整除要求
• --node-rank 0 / --node-rank 1:节点编号,节点 0 和 1 分别设置为 0 和 1
• --dist-init-addr 192.168.0.12:50000:分布式初始化地址,指向节点 0 的 IP 和端口,两节点一致
• --disable-cuda-graph:禁用 CUDA 图
• --trust-remote-code:允许加载 Qwen 模型的远程代码(Hugging Face 模型必需),模型下载好的直接不用这个参数即可
• --host 0.0.0.0:服务器监听所有网络接口,允许外部访问
• --port 30000:HTTP 服务器端口,可根据需要调整(若冲突,节点 1 可设为 --port 30001)
• --mem-fraction-static 0.7:KV 缓存池占用 70% 显存(默认 0.9),降低显存占用以适配 低显存显卡

如果选择量化

使用 --quantization 或 --torchao-config. SGLang 支持以下基于 torchao 的量化方法。["int8dq", "int8wo", "fp8wo", "fp8dq-per_tensor", "fp8dq-per_row", "int4wo-32", "int4wo-64", "int4wo-128", "int4wo-256"]

请注意:

使用--quantization fp8量化,PyTorch 的 FP8(8 位浮点)量化功能(依赖 torch._scaled_mm)要求 GPU 的 CUDA 计算能力(Compute Capability)达到 8.9 或 9.0 以上,或者使用 AMD ROCm MI300+ 架构.因此如果你的显卡不咋地则不建议使用这个参数。

• 节点 0(IP: 192.168.0.12):

NCCL_IB_DISABLE=1 NCCL_P2P_DISABLE=1 GLOO_SOCKET_IFNAME=eno1 NCCL_SOCKET_IFNAME=eno1 python3 -m sglang.launch_server \
  --model-path /opt/models/Qwen/Qwen2.5-7B-Instruct \
  --tp 2 \
  --nnodes 2 \
  --node-rank 0 \
  --dist-init-addr 192.168.0.12:50000 \
  --disable-cuda-graph \
  --torchao-config int4wo-32 \
  --host 0.0.0.0 \
  --port 30000 \
  --mem-fraction-static 0.7

• 节点 1(IP: 192.168.0.13):

NCCL_IB_DISABLE=1 NCCL_P2P_DISABLE=1 GLOO_SOCKET_IFNAME=eno1 NCCL_SOCKET_IFNAME=eno1 python3 -m sglang.launch_server \
  --model-path /opt/models/Qwen/Qwen2.5-7B-Instruct \
  --tp 2 \
  --nnodes 2 \
  --node-rank 1 \
  --dist-init-addr 192.168.0.12:50000 \
  --disable-cuda-graph \
  --torchao-config int4wo-32 \
  --host 0.0.0.0 \
  --port 30000 \
  --mem-fraction-static 0.7

部署完成

如下在主节点出现类似下面日志则启动成功,如果有异常 则自己看日志排查

[2025-04-17 14:50:24 TP0] Attention backend not set. Use flashinfer backend by default.
[2025-04-17 14:50:24 TP0] Disable chunked prefix cache for non-MLA backend.
[2025-04-17 14:50:24 TP0] Init torch distributed begin.
[2025-04-17 14:50:32 TP0] sglang is using nccl==2.21.5
[2025-04-17 14:50:32 TP0] Custom allreduce is disabled because this process group spans across nodes.
[2025-04-17 14:50:32 TP0] Init torch distributed ends. mem usage=0.11 GB
[2025-04-17 14:50:32 TP0] Load weight begin. avail mem=11.44 GB
Loading safetensors checkpoint shards:   0% Completed | 0/4 [00:00<?, ?it/s]
Loading safetensors checkpoint shards:  25% Completed | 1/4 [00:00<00:01,  2.71it/s]
.....
[2025-04-17 14:50:34 TP0] Load weight end. type=Qwen2ForCausalLM, dtype=torch.bfloat16, avail mem=4.22 GB, mem usage=7.22 GB.
[2025-04-17 14:50:34 TP0] KV Cache is allocated. #tokens: 29357, K size: 0.39 GB, V size: 0.39 GB
[2025-04-17 14:50:34 TP0] Memory pool end. avail mem=3.09 GB
...
[2025-04-17 14:50:36] INFO:     Uvicorn running on http://0.0.0.0:30000 (Press CTRL+C to quit)
[2025-04-17 14:50:37] INFO:     127.0.0.1:32902 - "GET /get_model_info HTTP/1.1" 200 OK
[2025-04-17 14:50:37 TP0] Prefill batch. #new-seq: 1, #new-token: 6, #cached-token: 0, token usage: 0.00, #running-req: 0, #queue-req: 0, 
[2025-04-17 14:50:39] INFO:     127.0.0.1:32908 - "POST /generate HTTP/1.1" 200 OK
[2025-04-17 14:50:39] The server is fired up and ready to roll!

从主节点机器访问服务器, 查看模型信息,至此部署完成。

curl http://192.168.0.12:30000/v1/models

访问测试

curl http://192.168.0.12:30000/v1/chat/completions   -H "Content-Type: application/json"   -d '{
      "model": "Qwen2.5-7B-Instruct", 
      "messages": [{
          "role": "user", 
          "content": "你是谁?"
      }], 
      "temperature": 0.3
  }'

Dify 社区版Docker部署-(私有化部署知识库1)

克隆代码

git clone https://github.com/langgenius/dify.git

启动Dify

进入 Dify 源代码的 Docker 目录

cd dify/docker
复制环境配置文件
cp .env.example .env
启动 Docker 容器

根据你系统上的 Docker Compose 版本,选择合适的命令来启动容器。你可以通过 $ docker compose version 命令检查版本,详细说明请参考 Docker 官方文档:

如果版本是 Docker Compose V2,使用以下命令:

docker compose up -d

如果版本是 Docker Compose V1,使用以下命令:

docker-compose up -d

更新 Dify

进入 dify 源代码的 docker 目录,按顺序执行以下命令:

cd dify/docker
docker compose down
git pull origin main
docker compose pull
docker compose up -d

同步环境变量配置 (重要!)
如果 .env.example 文件有更新,请务必同步修改你本地的 .env 文件。

检查 .env 文件中的所有配置项,确保它们与你的实际运行环境相匹配。你可能需要将 .env.example 中的新变量添加到 .env 文件中,并更新已更改的任何值。

修改默认端口

EXPOSE_NGINX_PORT=80
EXPOSE_NGINX_SSL_PORT=443

ChatGPT 代理规则不生效,一直被获取真实 IP

按理说让*.openai.com走代理,甚至直接开全局模式,ChatGPT不应该还能获得用户的真实IP了,但实际上还是报错1020。遇到此类情况,该如何解决呢?

1. 关闭浏览器的 HTTP/3

ChatGPT 网站启用了HTTP/3协议,该协议是基于UDP协议的QUIC协议实现的,读起来起来很绕嘴是吧?
简单来说,如果代理工具不支持嗅探 HTTP/3 协议,或者没有启用 UDP 功能,任意一项都导致 ChatGPT 网站可以获得你的真实 IP。
因此,关闭 Chrome 或 Edge 浏览器的 HTTP/3 功能,只使用 HTTP/1、HTTP/2,就是最简单的解决方法。

关闭步骤

复制chrome://flags/#enable-quic在 Chrome 或 Edge 浏览器地址栏打开,将 Experimental QUIC protocol 设置为Disabled ,然后单击页面最底部的Relaunch重启浏览器生效。

ChatGPT 代理规则

与 ChatGPT 相关的域名如下,如果你的代理功能可以匹配域名后缀,就直接将如下后缀加入代理名单。如果只支持泛域名匹配,就前域名前面加上.后放入代理名单,比如 .openai.com。具体还是看你所用代理工具的匹配规则。

openai.com
challenges.cloudflare.com
ai.com
stripe.com

经过以上两步,我们就实现了非全局模式下,顺利使用免费的 ChatGPT 网页版了。

1 2 3 4