📎 配套脚本附件正文未内嵌 · 共 7 个 · 点开可在线查看
📂 查看全部 126 个脚本附件 →
⚠️ 脚本内含明文凭据(与本页正文一致),请勿在公网环境直接运行。
K8s · 从零部署

Ubuntu 单机从驱动装到 Kubernetes:H20 上跑 vLLM、Dify 和监控

一台刚装好的 Ubuntu 22.04.5 + H20:从显卡驱动、Docker 装到 Kubernetes 1.37.1,再把 vLLM、Xinference、New-API、Dify、Langfuse 和监控逐个拉起来。

Ubuntu 单机从驱动装到 Kubernetes:H20 上跑 vLLM、Dify 和监控

目录

  1. 一、安装配置
  2. 二、功能操作案例
  3. 写文档时核对到的数据

本文按一台刚装好的 Ubuntu 22.04.5 来写。机器就是这台学习机:主机名 prod102,地址 10.0.1.47,内核 5.15.0-130-generic,数据盘 /usr/cluster,显卡 NVIDIA H20 550.120。读者从驱动和 Docker 开始,装好 Kubernetes 1.37.1,再把 vLLM、Xinference、New-API、Dify、Langfuse、Prometheus、Grafana、DCGM 和导航页放进这个单节点。

写文档时这台机器已经在跑。下面的脚本会在发现现有集群或现有容器时直接退出。不要在现网上再执行一遍。

登录密码、数据库密码和 dify-gateway 令牌按现网值写在本文。Langfuse 的 NEXTAUTH_SECRET、SALT、ENCRYPTION_KEY、Redis 口令、MinIO 口令和 kubeadm 加入令牌不写。合同原文不放进仓库。

一、安装配置

1. 目标

浏览器打开 Dify。对话从 Dify 到 New-API,再到 vLLM 的 qwen3.6-27b。知识库向量和重排走 Xinference 的 bge-m3、bge-reranker-v2-m3。追踪进 Langfuse。显卡进 Prometheus 和 Grafana。

四个命名空间:

命名空间工作负载
llmvLLM、Xinference、New-API、导航页
difyDify 1.17.1 全套
langfuseLangfuse 4、Postgres、ClickHouse、Redis、MinIO
observabilityPrometheus、Grafana、DCGM Exporter

两张卡共用这一张 H20。Pod 里设置 NVIDIA_VISIBLE_DEVICES=all,靠 Docker 的 nvidia 运行时看见显卡。清单里不写 nvidia.com/gpu,否则没有时间分片的设备插件会把第二份 GPU 工作负载卡住。

getServerStatus 留在宿主机 systemd,监听 0.0.0.0:8777。Dify 工具地址用 http://10.0.1.47:8777。宿主机 80 端口的 Nginx 不属于这套大模型服务。

2. 系统、驱动和目录

核对过的版本:驱动 nvidia-driver-550 550.120-0ubuntu0.22.04.1,nvidia-container-toolkit 1.17.8-1。nvidia-smi 看到 NVIDIA H20,显存 97871 MiB。

/models 是指向 /usr/cluster/models 的符号链接。Docker 数据根在 /usr/cluster/docker。根分区只留系统,模型和镜像都放数据盘。

文件:scripts/01-准备系统.sh

bash
#!/bin/bash
# 空白的 Ubuntu 22.04 使用。不要在已经跑着业务的 prod102 上再执行。
set -eu
export DEBIAN_FRONTEND=noninteractive

if [ "$(hostname)" = "prod102" ] && [ -f /root/.kube/config ]; then
  echo "prod102 上已经有集群。这份脚本是给空机器的,停在这里。"
  exit 1
fi

apt-get update
apt-get install -y ca-certificates curl gnupg python3

apt-get install -y nvidia-driver-550
if ! nvidia-smi >/dev/null 2>&1; then
  echo "驱动已安装。重启后再继续,直到 nvidia-smi 能看到 NVIDIA H20。"
  exit 0
fi

curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey \
  | gpg --dearmor --yes -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
curl -fsSL https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list \
  | sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' \
  > /etc/apt/sources.list.d/nvidia-container-toolkit.list
apt-get update
apt-get install -y nvidia-container-toolkit=1.17.8-1

modprobe br_netfilter || true
cat > /etc/modules-load.d/k8s.conf << 'EOF'
br_netfilter
EOF
cat > /etc/sysctl.d/k8s.conf << 'EOF'
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF
sysctl --system >/dev/null

mkdir -p /usr/cluster/models /usr/cluster/docker /opt/new-api/data /opt/new-api/logs \
  /usr/cluster/dify/xinference /usr/cluster/server-status /usr/cluster/llm-nav \
  /usr/cluster/observability/metrics
ln -sfn /usr/cluster/models /models
echo PREP_OK

驱动装完如果 nvidia-smi 还看不见卡,先重启,再跑后面的脚本。

3. Docker

服务器上的 Docker 是 28.0.1。docker info 里数据根是 /usr/cluster/docker,cgroup 驱动是 systemd,默认运行时是 nvidia。/etc/docker/daemon.json 里目前有四条镜像加速。空白机器要把数据根和 nvidia 运行时一起写进去,否则数据会落到 /var/lib/docker。

文件:scripts/02-安装Docker.sh

bash
#!/bin/bash
# 空白机器安装 Docker 28.0.1。数据放在 /usr/cluster,默认运行时用 nvidia。
# 不要在已经跑着业务的 prod102 上执行。
set -eu
export DEBIAN_FRONTEND=noninteractive

if [ "$(hostname)" = "prod102" ] && [ -S /var/run/docker.sock ]; then
  echo "prod102 上 Docker 已经在跑。这份脚本是给空机器的,停在这里。"
  exit 1
fi

install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
  | gpg --dearmor --yes -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu jammy stable" \
  > /etc/apt/sources.list.d/docker.list
apt-get update
ver=$(apt-cache madison docker-ce | awk '/28.0.1/{print $3; exit}')
if [ -z "$ver" ]; then
  echo "apt 里没有 docker-ce 28.0.1"
  exit 1
fi
apt-get install -y "docker-ce=$ver" "docker-ce-cli=$ver" containerd.io

mkdir -p /usr/cluster/docker /etc/docker
cat > /etc/docker/daemon.json << 'EOF'
{
  "data-root": "/usr/cluster/docker",
  "exec-opts": ["native.cgroupdriver=systemd"],
  "default-runtime": "nvidia",
  "runtimes": {
    "nvidia": {
      "path": "nvidia-container-runtime"
    }
  },
  "registry-mirrors": [
    "https://docker.1ms.run",
    "https://docker.xuanyuan.me",
    "https://docker.m.daocloud.io",
    "https://hub-mirror.c.163.com"
  ]
}
EOF
systemctl enable docker
systemctl restart docker
docker info --format 'root={{.DockerRootDir}} cgroup={{.CgroupDriver}} runtime={{.DefaultRuntime}}'
echo DOCKER_OK

装完核对这三行:

text
root=/usr/cluster/docker cgroup=systemd runtime=nvidia

4. 模型权重

权重目录 /models/Qwen3.6-27B。里面有 ModelScope 的 configuration.json,也有 Hugging Face 模型卡。模型 ID 是 Qwen/Qwen3.6-27B,config.json 里的架构是 Qwen3_5ForConditionalGeneration。

文件:scripts/04-下载模型.sh

bash
#!/bin/bash
# 把 Qwen3.6-27B 下到 /models。目录里已有 ModelScope 的 configuration.json
# 和 Hugging Face 模型卡。不要在权重已经齐的机器上再下一份。
set -eu
if [ -f /models/Qwen3.6-27B/config.json ]; then
  echo "权重已在 /models/Qwen3.6-27B"
  exit 0
fi
mkdir -p /usr/cluster/models
ln -sfn /usr/cluster/models /models
pip3 install -U modelscope
modelscope download --model Qwen/Qwen3.6-27B --local_dir /models/Qwen3.6-27B
test -f /models/Qwen3.6-27B/config.json
echo MODEL_OK

vLLM 挂载这个目录到容器内 /model,并设置 HF_HUB_OFFLINE=1,启动后不再访问外网拉权重。

5. Kubernetes 单节点

版本:kubelet / kubeadm / kubectl 1.37.1-1.1,cri-dockerd 0.4.8。接口 https://10.0.1.47:6443,只绑在这块网卡上。kubeconfig 在 /root/.kube/config,权限 600。使用 kubectl 前要带上 KUBECONFIG=/root/.kube/config。

容器运行时走现有 Docker,socket 是 unix:///run/cri-dockerd.sock。pause 镜像用阿里云的 registry.aliyuncs.com/google_containers/pause:3.10.2,并打上 registry.k8s.io/pause:3.10 这个标签。这个标签和 drop-in 要在 kubeadm init 之前就位。

Pod 网段在 kubeadm 里写成 10.244.0.0/16。本机网桥实际分配 10.244.0.0/24,网关 10.244.0.1,网桥名 cni0。Service 网段 10.96.0.0/12,CoreDNS 是 10.96.0.10。节点名 prod102,装完后去掉 control-plane 的 NoSchedule 污点,业务 Pod 才能调度上来。网络用这份网桥配置,安装脚本里不部署 Flannel。

文件:scripts/03-安装单节点.sh

bash
#!/bin/bash
# 空白机器安装 Kubernetes 1.37.1 单节点。
# 运行时是 cri-dockerd 0.4.8,网络是本机网桥。不要再装 Flannel。
# 不要在已经跑着业务的 prod102 上执行。
set -eu
export DEBIAN_FRONTEND=noninteractive
export KUBECONFIG=/root/.kube/config

if [ "$(hostname)" = "prod102" ] && [ -f /etc/kubernetes/admin.conf ]; then
  echo "prod102 上控制面已经在。这份脚本是给空机器的,停在这里。"
  exit 1
fi

KVER=1.37.1-1.1
IMG_REPO=registry.aliyuncs.com/google_containers
WORKDIR=/usr/cluster/k8s
mkdir -p "$WORKDIR" /etc/cni/net.d /opt/cni/bin

echo "=== cri-dockerd 0.4.8 ==="
if ! /usr/local/bin/cri-dockerd --version 2>/dev/null | grep -q '0.4.8'; then
  curl -fL --retry 3 -o "$WORKDIR/cri-dockerd-0.4.8.amd64.tgz" \
    https://github.com/Mirantis/cri-dockerd/releases/download/v0.4.8/cri-dockerd-0.4.8.amd64.tgz
  tar -xzf "$WORKDIR/cri-dockerd-0.4.8.amd64.tgz" -C "$WORKDIR"
  install -m 0755 "$WORKDIR/cri-dockerd/cri-dockerd" /usr/local/bin/cri-dockerd
fi
if [ ! -f /lib/systemd/system/cri-docker.service ] && [ ! -f /etc/systemd/system/cri-docker.service ]; then
  cat > /etc/systemd/system/cri-docker.socket << 'EOF'
[Unit]
Description=CRI Docker Socket for the API
PartOf=cri-docker.service

[Socket]
ListenStream=%t/cri-dockerd.sock
SocketMode=0660
SocketUser=root
SocketGroup=root

[Install]
WantedBy=sockets.target
EOF
  cat > /etc/systemd/system/cri-docker.service << 'EOF'
[Unit]
Description=CRI Interface for Docker Application Container Engine
After=network-online.target docker.service
Wants=network-online.target
Requires=cri-docker.socket

[Service]
Type=notify
ExecStart=/usr/local/bin/cri-dockerd --container-runtime-endpoint fd://
ExecReload=/bin/kill -s HUP $MAINPID
TimeoutSec=0
RestartSec=2
Restart=always
Delegate=yes
KillMode=process

[Install]
WantedBy=multi-user.target
EOF
fi
systemctl mask cri-dockerd.service >/dev/null 2>&1 || true
systemctl daemon-reload
systemctl enable cri-docker.socket cri-docker.service

echo "=== pause 镜像,写在 kubeadm init 之前 ==="
docker pull "$IMG_REPO/pause:3.10.2"
docker tag "$IMG_REPO/pause:3.10.2" registry.k8s.io/pause:3.10
mkdir -p /etc/systemd/system/cri-docker.service.d
cat > /etc/systemd/system/cri-docker.service.d/pause.conf << 'EOF'
[Service]
ExecStart=
ExecStart=/usr/local/bin/cri-dockerd --container-runtime-endpoint fd:// --pod-infra-container-image=registry.aliyuncs.com/google_containers/pause:3.10.2
EOF
systemctl daemon-reload
systemctl restart cri-docker.socket
systemctl restart cri-docker.service

echo "=== kubeadm 1.37.1 ==="
install -d -m 0755 /etc/apt/keyrings
curl -fsSL https://mirrors.aliyun.com/kubernetes-new/core/stable/v1.37/deb/Release.key \
  | gpg --dearmor --yes -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
cat > /etc/apt/sources.list.d/kubernetes.list << 'EOF'
deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://mirrors.aliyun.com/kubernetes-new/core/stable/v1.37/deb/ /
EOF
apt-get update
systemctl stop kubelet || true
systemctl disable kubelet || true
apt-get install -y -o Dpkg::Options::=--force-confnew \
  kubelet="$KVER" kubeadm="$KVER" kubectl="$KVER" conntrack socat
apt-mark hold kubelet kubeadm kubectl
systemctl unmask kubelet || true

cat > "$WORKDIR/kubeadm-config.yaml" << 'EOF'
apiVersion: kubeadm.k8s.io/v1beta4
kind: InitConfiguration
localAPIEndpoint:
  advertiseAddress: 10.0.1.47
  bindPort: 6443
nodeRegistration:
  criSocket: unix:///run/cri-dockerd.sock
  imagePullPolicy: IfNotPresent
  name: prod102
  kubeletExtraArgs:
    - name: node-ip
      value: 10.0.1.47
---
apiVersion: kubeadm.k8s.io/v1beta4
kind: ClusterConfiguration
kubernetesVersion: v1.37.1
imageRepository: registry.aliyuncs.com/google_containers
controlPlaneEndpoint: 10.0.1.47:6443
networking:
  podSubnet: 10.244.0.0/16
  serviceSubnet: 10.96.0.0/12
apiServer:
  certSANs:
    - 10.0.1.47
    - prod102
  extraArgs:
    - name: bind-address
      value: 10.0.1.47
EOF

echo "=== 网桥,写在 init 之前,节点才能 Ready ==="
cat > /etc/cni/net.d/10-bridge.conflist << 'EOF'
{
  "cniVersion": "1.0.0",
  "name": "k8s-pod-network",
  "plugins": [
    {
      "type": "bridge",
      "bridge": "cni0",
      "isGateway": true,
      "ipMasq": true,
      "promiscMode": true,
      "ipam": {
        "type": "host-local",
        "ranges": [[{"subnet": "10.244.0.0/24"}]],
        "routes": [{"dst": "0.0.0.0/0"}]
      }
    },
    {
      "type": "portmap",
      "capabilities": {"portMappings": true}
    }
  ]
}
EOF

kubeadm config images pull --config "$WORKDIR/kubeadm-config.yaml"
kubeadm init --config "$WORKDIR/kubeadm-config.yaml" --ignore-preflight-errors=FileExisting-tc
mkdir -p /root/.kube
cp /etc/kubernetes/admin.conf /root/.kube/config
chmod 600 /root/.kube/config
systemctl restart kubelet
kubectl taint nodes prod102 node-role.kubernetes.io/control-plane:NoSchedule- || true
kubectl wait --for=condition=Ready node/prod102 --timeout=180s
kubectl get nodes -o wide
echo INSTALL_OK

节点 Ready 之后,kubectl get node 应看到 prod102、v1.37.1、docker://28.0.1。

6. 工作负载

共同约定:strategy: Recreate,nodeName: prod102,imagePullPolicy: IfNotPresent,enableServiceLinks: false。需要访问宿主机时,hostAliases 把 host.docker.internal 指到 10.0.1.47。对外端口用 hostPort,和原来的端口保持一致。vLLM 不设就绪探针,模型加载要几分钟。

文件:scripts/05-部署组件.sh、config/namespaces.yaml、config/llm.yaml、config/observability.yaml、config/dify-nginx.yaml

bash
#!/bin/bash
# 空白机器上创建命名空间和工作负载。不要在现网 prod102 上执行。
set -eu
export KUBECONFIG=/root/.kube/config
ROOT=$(cd "$(dirname "$0")/.." && pwd)

if kubectl get deploy -n llm vllm >/dev/null 2>&1; then
  echo "llm/vllm 已经在。这份脚本是给空集群的,停在这里。"
  exit 1
fi

kubectl apply -f "$ROOT/config/namespaces.yaml"
kubectl apply -f "$ROOT/config/llm.yaml"
kubectl apply -f "$ROOT/config/observability.yaml"
kubectl apply -f "$ROOT/config/dify-nginx.yaml"
echo APPLY_OK
echo "Dify 和 Langfuse 的其余工作负载见本文的服务表。数据库口令在 config/数据库口令.env。"
echo "Langfuse 加密密钥和 Redis 口令不要写进仓库,在服务器上生成后做成 Secret。"
yaml
apiVersion: v1
kind: Namespace
metadata:
  name: llm
---
apiVersion: v1
kind: Namespace
metadata:
  name: dify
---
apiVersion: v1
kind: Namespace
metadata:
  name: langfuse
---
apiVersion: v1
kind: Namespace
metadata:
  name: observability
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vllm
  namespace: llm
  labels:
    app: vllm
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: vllm
  template:
    metadata:
      labels:
        app: vllm
    spec:
      nodeName: prod102
      enableServiceLinks: false
      hostAliases:
        - ip: 10.0.1.47
          hostnames: ["host.docker.internal"]
      containers:
        - name: vllm
          image: docker.m.daocloud.io/vllm/vllm-openai:latest
          imagePullPolicy: IfNotPresent
          command: ["vllm", "serve"]
          args:
            - /model
            - --trust-remote-code
            - --dtype
            - bfloat16
            - --tensor-parallel-size
            - "1"
            - --enable-auto-tool-choice
            - --tool-call-parser
            - qwen3_xml
            - --reasoning-parser
            - qwen3
            - --max-model-len
            - "65536"
            - --gpu-memory-utilization
            - "0.70"
            - --max-num-seqs
            - "8"
            - --served-model-name
            - qwen3.6-27b
            - --enable-prefix-caching
            - --host
            - 0.0.0.0
            - --port
            - "8000"
          env:
            - name: HF_HUB_OFFLINE
              value: "1"
            - name: NVIDIA_VISIBLE_DEVICES
              value: all
            - name: NVIDIA_DRIVER_CAPABILITIES
              value: all
          ports:
            - containerPort: 8000
              hostPort: 8000
          volumeMounts:
            - name: model
              mountPath: /model
            - name: dshm
              mountPath: /dev/shm
      volumes:
        - name: model
          hostPath:
            path: /models/Qwen3.6-27B
            type: Directory
        - name: dshm
          emptyDir:
            medium: Memory
            sizeLimit: 8Gi
---
apiVersion: v1
kind: Service
metadata:
  name: vllm
  namespace: llm
spec:
  selector:
    app: vllm
  ports:
    - name: p8000
      port: 8000
      targetPort: 8000
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: xinference
  namespace: llm
  labels:
    app: xinference
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: xinference
  template:
    metadata:
      labels:
        app: xinference
    spec:
      nodeName: prod102
      enableServiceLinks: false
      containers:
        - name: xinference
          image: xprobe/xinference:latest
          imagePullPolicy: IfNotPresent
          command: ["xinference-local", "-H", "0.0.0.0", "--log-level", "info"]
          env:
            - name: XINFERENCE_HOME
              value: /data
            - name: XINFERENCE_MODEL_SRC
              value: modelscope
            - name: NVIDIA_VISIBLE_DEVICES
              value: all
            - name: NVIDIA_DRIVER_CAPABILITIES
              value: all
          ports:
            - containerPort: 9997
              hostPort: 9997
          volumeMounts:
            - name: data
              mountPath: /data
      volumes:
        - name: data
          hostPath:
            path: /usr/cluster/dify/xinference
            type: DirectoryOrCreate
---
apiVersion: v1
kind: Service
metadata:
  name: xinference
  namespace: llm
spec:
  selector:
    app: xinference
  ports:
    - name: p9997
      port: 9997
      targetPort: 9997
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: new-api
  namespace: llm
  labels:
    app: new-api
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: new-api
  template:
    metadata:
      labels:
        app: new-api
    spec:
      nodeName: prod102
      enableServiceLinks: false
      containers:
        - name: new-api
          image: calciumion/new-api:latest
          imagePullPolicy: IfNotPresent
          command: ["/new-api"]
          args: ["--log-dir", "/app/logs"]
          ports:
            - containerPort: 3000
              hostPort: 3000
          volumeMounts:
            - name: data
              mountPath: /data
            - name: logs
              mountPath: /app/logs
      volumes:
        - name: data
          hostPath:
            path: /opt/new-api/data
            type: DirectoryOrCreate
        - name: logs
          hostPath:
            path: /opt/new-api/logs
            type: DirectoryOrCreate
---
apiVersion: v1
kind: Service
metadata:
  name: new-api
  namespace: llm
spec:
  selector:
    app: new-api
  ports:
    - name: p3000
      port: 3000
      targetPort: 3000
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nav
  namespace: llm
  labels:
    app: nav
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: nav
  template:
    metadata:
      labels:
        app: nav
    spec:
      nodeName: prod102
      containers:
        - name: nav
          image: nginx:latest
          imagePullPolicy: IfNotPresent
          command: ["/docker-entrypoint.sh"]
          args: ["nginx", "-g", "daemon off;"]
          ports:
            - containerPort: 80
              hostPort: 8090
          volumeMounts:
            - name: html
              mountPath: /usr/share/nginx/html
      volumes:
        - name: html
          hostPath:
            path: /usr/cluster/llm-nav
            type: DirectoryOrCreate
---
apiVersion: v1
kind: Service
metadata:
  name: nav
  namespace: llm
spec:
  selector:
    app: nav
  ports:
    - name: p80
      port: 80
      targetPort: 80
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: prometheus
  namespace: observability
  labels:
    app: prometheus
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: prometheus
  template:
    metadata:
      labels:
        app: prometheus
    spec:
      nodeName: prod102
      enableServiceLinks: false
      containers:
        - name: prometheus
          image: prom/prometheus:v3.2.1
          imagePullPolicy: IfNotPresent
          command: ["/bin/prometheus"]
          args:
            - --config.file=/etc/prometheus/prometheus.yml
            - --storage.tsdb.path=/prometheus
            - --storage.tsdb.retention.time=15d
          ports:
            - containerPort: 9090
              hostPort: 9090
          volumeMounts:
            - name: config
              mountPath: /etc/prometheus/prometheus.yml
              subPath: prometheus.yml
            - name: data
              mountPath: /prometheus
      volumes:
        - name: config
          hostPath:
            path: /usr/cluster/observability/metrics/prometheus.yml
            type: File
        - name: data
          hostPath:
            path: /usr/cluster/docker/volumes/observability_prometheus_data/_data
            type: DirectoryOrCreate
---
apiVersion: v1
kind: Service
metadata:
  name: prometheus
  namespace: observability
spec:
  selector:
    app: prometheus
  ports:
    - name: p9090
      port: 9090
      targetPort: 9090
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: grafana
  namespace: observability
  labels:
    app: grafana
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: grafana
  template:
    metadata:
      labels:
        app: grafana
    spec:
      nodeName: prod102
      enableServiceLinks: false
      containers:
        - name: grafana
          image: grafana/grafana:11.5.2
          imagePullPolicy: IfNotPresent
          command: ["/run.sh"]
          env:
            - name: GF_SECURITY_ADMIN_USER
              value: admin
            - name: GF_SECURITY_ADMIN_PASSWORD
              value: Gf-gpu-2026
            - name: GF_SERVER_ROOT_URL
              value: http://10.0.1.47:3001
          ports:
            - containerPort: 3000
              hostPort: 3001
          volumeMounts:
            - name: datasources
              mountPath: /etc/grafana/provisioning/datasources
            - name: dashboards-prov
              mountPath: /etc/grafana/provisioning/dashboards
            - name: dashboards
              mountPath: /var/lib/grafana/dashboards
            - name: data
              mountPath: /var/lib/grafana
      volumes:
        - name: datasources
          hostPath:
            path: /usr/cluster/observability/metrics/grafana/provisioning/datasources
            type: Directory
        - name: dashboards-prov
          hostPath:
            path: /usr/cluster/observability/metrics/grafana/provisioning/dashboards
            type: Directory
        - name: dashboards
          hostPath:
            path: /usr/cluster/observability/metrics/grafana/dashboards
            type: Directory
        - name: data
          hostPath:
            path: /usr/cluster/docker/volumes/observability_grafana_data/_data
            type: DirectoryOrCreate
---
apiVersion: v1
kind: Service
metadata:
  name: grafana
  namespace: observability
spec:
  selector:
    app: grafana
  ports:
    - name: p3000
      port: 3000
      targetPort: 3000
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: dcgm-exporter
  namespace: observability
  labels:
    app: dcgm-exporter
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: dcgm-exporter
  template:
    metadata:
      labels:
        app: dcgm-exporter
    spec:
      nodeName: prod102
      enableServiceLinks: false
      containers:
        - name: dcgm-exporter
          image: nvidia/dcgm-exporter:3.3.8-3.6.0-ubuntu22.04
          imagePullPolicy: IfNotPresent
          command: ["/usr/local/dcgm/dcgm-exporter-entrypoint.sh"]
          env:
            - name: NVIDIA_VISIBLE_DEVICES
              value: all
            - name: NVIDIA_DRIVER_CAPABILITIES
              value: all
          ports:
            - containerPort: 9400
              hostPort: 9400
---
apiVersion: v1
kind: Service
metadata:
  name: dcgm-exporter
  namespace: observability
spec:
  selector:
    app: dcgm-exporter
  ports:
    - name: p9400
      port: 9400
      targetPort: 9400
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  namespace: dify
  labels:
    app: nginx
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      nodeName: prod102
      enableServiceLinks: false
      containers:
        - name: nginx
          image: nginx:latest
          imagePullPolicy: IfNotPresent
          command: ["sh", "-c"]
          args:
            - "cp /docker-entrypoint-mount.sh /docker-entrypoint.sh && sed -i 's/\\r$//' /docker-entrypoint.sh && chmod +x /docker-entrypoint.sh && /docker-entrypoint.sh"
          ports:
            - containerPort: 80
              hostPort: 8088
            - containerPort: 443
              hostPort: 18443
          volumeMounts:
            - name: entry
              mountPath: /docker-entrypoint-mount.sh
            - name: confd
              mountPath: /etc/nginx/conf.d
            - name: nginx-conf
              mountPath: /etc/nginx/nginx.conf.template
            - name: proxy-conf
              mountPath: /etc/nginx/proxy.conf.template
            - name: https-conf
              mountPath: /etc/nginx/https.conf.template
            - name: ssl
              mountPath: /etc/ssl
            - name: certbot-conf
              mountPath: /etc/letsencrypt
            - name: certbot-live
              mountPath: /etc/letsencrypt/live
            - name: certbot-www
              mountPath: /var/www/html
      volumes:
        - name: entry
          hostPath:
            path: /usr/cluster/dify/src/docker/nginx/docker-entrypoint.sh
            type: File
        - name: confd
          hostPath:
            path: /usr/cluster/dify/src/docker/nginx/conf.d
            type: Directory
        - name: nginx-conf
          hostPath:
            path: /usr/cluster/dify/src/docker/nginx/nginx.conf.template
            type: File
        - name: proxy-conf
          hostPath:
            path: /usr/cluster/dify/src/docker/nginx/proxy.conf.template
            type: File
        - name: https-conf
          hostPath:
            path: /usr/cluster/dify/src/docker/nginx/https.conf.template
            type: File
        - name: ssl
          hostPath:
            path: /usr/cluster/dify/src/docker/nginx/ssl
            type: Directory
        - name: certbot-conf
          hostPath:
            path: /usr/cluster/dify/src/docker/volumes/certbot/conf
            type: DirectoryOrCreate
        - name: certbot-live
          hostPath:
            path: /usr/cluster/dify/src/docker/volumes/certbot/conf/live
            type: DirectoryOrCreate
        - name: certbot-www
          hostPath:
            path: /usr/cluster/dify/src/docker/volumes/certbot/www
            type: DirectoryOrCreate
---
apiVersion: v1
kind: Service
metadata:
  name: nginx
  namespace: dify
spec:
  selector:
    app: nginx
  ports:
    - name: p80
      port: 80
      targetPort: 80
    - name: p443
      port: 443
      targetPort: 443

vLLM 参数:bfloat16,上下文 65536,gpu-memory-utilization 0.70,max-num-seqs 8,服务名 qwen3.6-27b,共享内存 8Gi。/dev/shm 用 Memory 类型的 emptyDir。

Xinference 第一次启动要初始化管理员,用户名 admin,密码 Xinf-bge-2026。然后在容器里安装 sentence-transformers:

bash
kubectl -n llm exec deploy/xinference -- pip install -i https://pypi.tuna.tsinghua.edu.cn/simple sentence-transformers

加载两个模型时 n_gpu 写 1,download_hub 写 modelscope。不带令牌访问 /v1/models 会返回 401,这表示进程在、接口要求登录。

json
POST http://10.0.1.47:9997/v1/models?wait_ready=true
{
  "model_name": "bge-m3",
  "model_type": "embedding",
  "n_gpu": 1,
  "download_hub": "modelscope"
}
json
POST http://10.0.1.47:9997/v1/models?wait_ready=true
{
  "model_name": "bge-reranker-v2-m3",
  "model_type": "rerank",
  "n_gpu": 1,
  "download_hub": "modelscope"
}

bge-m3 的向量维度是 1024。

Dify 源码和官方 Docker 文件放在 /usr/cluster/dify/src/docker,版本 1.17.1。公开地址六项都是 http://10.0.1.47:8088:CONSOLE_API_URL、CONSOLE_WEB_URL、SERVICE_API_URL、APP_API_URL、APP_WEB_URL、FILES_URL。Nginx 对外 8088,TLS 对外 18443。向量库用 pgvector。数据库用户 postgres,库名 dify,口令见下一节。

Dify Nginx 的解析器写 10.96.0.10。上游用集群全域名,短名在 Nginx 里不会走 Pod 的 search 域:

上游地址
APIapi.dify.svc.cluster.local:5001
Webweb.dify.svc.cluster.local:3000
WebSocketapi-websocket.dify.svc.cluster.local:5001
插件plugin-daemon.dify.svc.cluster.local:5002

plugin-daemon 的 Service 要同时暴露 5002 和 5003。镜像 EXPOSE 只有 5003,Nginx 代理的是 5002。

其余 Dify、Langfuse 工作负载用同一套 Deployment 模板:镜像和数据目录按下表挂 hostPath,环境变量放进名为 <应用名>-env 的 Secret。数据库口令用本文的值。Langfuse 加密项和 Redis 口令在服务器上生成,权限 600,不写进仓库。

命名空间名字镜像数据目录
difyapi / worker / api-websocketlanggenius/dify-api:1.17.1/usr/cluster/dify/src/docker/volumes/app/storage
difyworker-beat同上追踪补丁文件
difyweblanggenius/dify-web:1.17.1无
difydb-postgrespostgres:15-alpinevolumes/db/data
difypgvectorpgvector/pgvector:pg16volumes/pgvector/data
difyredisredis:6-alpinevolumes/redis/data
difysandboxlanggenius/dify-sandbox:0.2.15volumes/sandbox
difyplugin-daemonlanggenius/dify-plugin-daemon:0.6.10-localvolumes/plugin_daemon
difyssrf-proxy / agent-ssrf-proxyubuntu/squid:latestssrf_proxy 配置
difyagent-backendlanggenius/dify-agent-backend:1.17.1无
difylocal-sandboxlanggenius/dify-agent-local-sandbox:1.17.1Docker volume _data
langfuselangfuse-webdocker.langfuse.com/langfuse/langfuse:4无,hostPort 3100
langfuselangfuse-workerdocker.langfuse.com/langfuse/langfuse-worker:4hostPort 127.0.0.1:3030
langfusepostgresdocker.io/postgres:17langfuse_langfuse_postgres_data
langfuseclickhousedocker.io/clickhouse/clickhouse-server:25.12ClickHouse 数据卷
langfuseredisdocker.io/redis:7Redis 数据卷
langfuseminiodocker.io/minio/minio:latesthostPort 9092 到容器 9000

api、worker、worker-beat、api-websocket 把 config/langfuse_trace.py 挂到:

text
/app/api/.venv/lib/python3.12/site-packages/dify_trace_langfuse/langfuse_trace.py

文件里的标记是 # AGENT_TOOL_SPANS_V2。工具调用的 Observation 名字来自这段补丁。

内网放行写在 Dify 环境里,只认这一行赋值,不要去改注释里的同名文字:

text
SSRF_PROXY_ALLOW_PRIVATE_IPS=172.17.0.0/16,10.0.0.0/8

Prometheus 抓取文件是 config/prometheus.yml。vLLM 目标是 10.0.1.47:8000,DCGM 目标是 dcgm-exporter:9400。Grafana 数据源是同命名空间的 http://prometheus:9090。看板目录在服务器 /usr/cluster/observability/metrics/grafana/dashboards。

端口:

地址服务
http://10.0.1.47:3000New-API
http://10.0.1.47:8000vLLM
http://10.0.1.47:9997Xinference
http://10.0.1.47:8088Dify
http://10.0.1.47:3100Langfuse
http://10.0.1.47:3001Grafana
http://10.0.1.47:9090Prometheus
http://10.0.1.47:9400DCGM
http://10.0.1.47:8090导航页
http://10.0.1.47:8777getServerStatus

7. 服务器状态接口

程序用系统 Python 听在宿主机 8777。systemd 单元名 server-status。

文件:scripts/06-服务器状态.sh、config/server-status/app.py

bash
#!/bin/bash
# 在宿主机安装 getServerStatus。它不是容器,端口 8777。
# 不要停掉现网上已经 active 的 server-status。
set -eu
if systemctl is-active --quiet server-status; then
  echo "server-status 已在运行"
  exit 0
fi
ROOT=$(cd "$(dirname "$0")/.." && pwd)
mkdir -p /usr/cluster/server-status
cp "$ROOT/config/server-status/app.py" /usr/cluster/server-status/app.py
cp "$ROOT/config/server-status/server-status.service" /etc/systemd/system/server-status.service
systemctl daemon-reload
systemctl enable server-status
systemctl restart server-status
curl -fsS "http://127.0.0.1:8777/server/status?server_name=gpu-server-01"
echo
echo STATUS_OK
python
#!/usr/bin/env python3
"""作业 1 的模拟服务器状态接口。operationId 对应 Langfuse 里的 getServerStatus。"""
import json
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import parse_qs, urlparse


class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        parsed = urlparse(self.path)
        if parsed.path == "/health":
            self._send({"status": "ok"})
            return
        if parsed.path == "/server/status":
            name = (parse_qs(parsed.query).get("server_name") or ["unknown"])[0]
            self._send(
                {
                    "server": name,
                    "cpu": "45%",
                    "memory": "62%",
                    "gpu_util": "78%",
                    "gpu_memory": "18.2/24 GB",
                }
            )
            return
        self.send_response(404)
        self.end_headers()

    def _send(self, payload):
        body = json.dumps(payload, ensure_ascii=False).encode()
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, fmt, *args):
        print("%s - %s" % (self.address_string(), fmt % args), flush=True)


if __name__ == "__main__":
    ThreadingHTTPServer(("0.0.0.0", 8777), Handler).serve_forever()

OpenAPI 的 servers.url 写 http://10.0.1.47:8777,operationId 是 getServerStatus。完整文件是 config/server-status-openapi.yaml。返回值是固定的模拟数:CPU 45%,内存 62%,GPU 利用率 78%,显存 18.2/24 GB。

8. 账号和口令

系统账号密码
New-APIadminL4o4986784$
Difyadmin@example.comDify-rag-2026
GrafanaadminGf-gpu-2026
Langfuseadmin@example.comLf-trace-2026
XinferenceadminXinf-bge-2026

数据库口令在 config/数据库口令.env:

text
# 可以写进文档的数据库口令。Redis 口令、Langfuse 加密密钥不在这里。
DIFY_POSTGRES_USER=postgres
DIFY_POSTGRES_PASSWORD=difyai123456
DIFY_POSTGRES_DB=dify
PGVECTOR_PASSWORD=difyai123456
LANGFUSE_POSTGRES_USER=postgres
LANGFUSE_POSTGRES_PASSWORD=ebb9cb3a5365bc5ffd41349860dc48c8
LANGFUSE_POSTGRES_DB=postgres
CLICKHOUSE_USER=clickhouse
CLICKHOUSE_PASSWORD=b5e2a69b99ac76c35f7f8d5742ce2ea6
CLICKHOUSE_DB=default

dify-gateway 是用户 admin 的第 3 号令牌,状态启用,额度无限,永不过期,只允许模型 qwen3.6-27b。完整密钥:

text
sk-C0P9V9kwPfXDvQCfaVzSLCJRUmioNmxYud1uqsLXkBqZTFxK

New-API 渠道 id 1,名字 vLLM-Qwen3.6-27B,地址 http://10.0.1.47:8000,模型 qwen3.6-27b。额度换算:QuotaPerUnit=500000,500000 额度等于 1 美元。

Dify 登录接口的密码要先做 base64,请求头 X-CSRF-Token 要等于 cookie 里的 csrf_token。

二、功能操作案例

案例 1:New-API 渠道和 dify-gateway

  1. 打开 http://10.0.1.47:3000,用 admin 登录。
  2. 渠道指向 http://10.0.1.47:8000,模型名 qwen3.6-27b。
  3. 令牌页确认 dify-gateway。密钥就是上一节那一把。额度无限,过期时间 -1,模型限制只放行 qwen3.6-27b。
  4. 金额和过期都在这把令牌上改。Dify 的 Agent 页面没有这两项。关掉无限额度后,花费记在令牌上;保持无限时,花费记在 admin 余额上。

以后新建的 Agent 或工作流,只要还用工作区默认的 qwen3.6-27b,对话都走这把令牌。向量和重排不走它,它们走 Xinference。

案例 2:Dify 公开地址、模型和内网放行

初始化管理员 admin@example.com / Dify-rag-2026。六项公开 URL 都带 :8088。

文件:scripts/07-接入模型.py

python
#!/usr/bin/env python3
"""记录工作区模型注册。对话走 New-API 的 dify-gateway,向量和重排走 Xinference。

现网已经注册过。再跑一次如果返回 Can't add same credential,停下来即可。
不要创建第二把名为 dify-gateway 的令牌。
"""
import json
import urllib.request

DIFY = "http://127.0.0.1:8088"
NEW_API = "http://10.0.1.47:3000/v1"
XINFERENCE = "http://10.0.1.47:9997"
GATEWAY_KEY = "sk-C0P9V9kwPfXDvQCfaVzSLCJRUmioNmxYud1uqsLXkBqZTFxK"

llm = {
    "model": "qwen3.6-27b",
    "model_type": "llm",
    "credentials": {
        "mode": "chat",
        "context_size": "65536",
        "max_tokens_to_sample": "8192",
        "function_calling_type": "tool_call",
        "stream_function_calling": "supported",
        "display_name": "qwen3.6-27b",
        "endpoint_url": NEW_API,
        "api_key": GATEWAY_KEY,
    },
}
embedding = {
    "model": "bge-m3",
    "model_type": "text-embedding",
    "credentials": {
        "server_url": XINFERENCE,
        "model_uid": "bge-m3",
    },
}
rerank = {
    "model": "bge-reranker-v2-m3",
    "model_type": "rerank",
    "credentials": {
        "server_url": XINFERENCE,
        "model_uid": "bge-reranker-v2-m3",
    },
}


def show(title, body):
    print(title)
    print(json.dumps(body, ensure_ascii=False, indent=2))


show("对话模型", llm)
show("向量模型", embedding)
show("重排模型", rerank)
print("保存:POST /console/api/workspaces/current/model-providers/<provider>/models/credentials")
print("供应商:langgenius/openai_api_compatible/openai_api_compatible")
print("供应商:langgenius/xinference/xinference")
print("然后把这三项设成工作区默认模型。")

对话模型上下文 65536,单次输出上限 8192,函数调用类型 tool_call。默认重排模型是 bge-reranker-v2-m3。

SSRF_PROXY_ALLOW_PRIVATE_IPS=172.17.0.0/16,10.0.0.0/8。工具请求经过 Squid 的 3128。从 api Pod 访问 http://10.0.1.47:8777/server/status?server_name=gpu-server-01 应返回上面的模拟指标。

案例 3:合同知识库和 Agent

合同文件先在 Dify 页面上传。仓库里不放原文。

知识库名称 阿里云合同条款。高质量索引,bge-m3,中文,自动分段,混合检索,向量权重 0.7,关键词权重 0.3,top_k 4,打开 Rerank,模型 bge-reranker-v2-m3。权限 all_team_members。现网这份库是 1 个文档、27190 字、75 个片段,id 71042108-5f9a-4856-a0db-4359e8e7fa98。

Agent 名称同样是 阿里云合同条款。现网 id 01a1236c-4cc3-7fb9-bfce-c730a318bdf9,应用 d2e3f57b-3901-45de-bd9a-d51feb849f6e。访问地址:

text
http://10.0.1.47:8088/agent/gQPVqCdcyKoW82iZ

知识检索卡片名 Retrieval 1。查询模式用 generated_query,这样模型会按用户问题调用 knowledge_base_search。set_name 填 Retrieval 1。

文件:scripts/08-合同知识库.py

python
#!/usr/bin/env python3
"""合同知识库和 Agent 提示词。合同原文不在仓库里,先在页面上传。

现网知识库 id 是 71042108-5f9a-4856-a0db-4359e8e7fa98。
空白环境上传后用接口返回的新 id,不要拿这个旧 id 再索引一份。
"""
DATASET_NAME = "阿里云合同条款"
PROVIDER = "langgenius/xinference/xinference"
SYSTEM_PROMPT = """# 角色
你是【阿里云框架合同解析智能Agent】,服务采购、IT、法务。只解析阿里云框架协议、服务条款、补充协议、订单、账单、SLA、免责、费用、终止、数据安全。其他问题请用户找对应负责人。

# 检索
需要合同原文时,调用一次 knowledge_base_search:set_name 为「Retrieval 1」,query 只用条款主题加关键词。
本轮对话已经引用过该条款原文时,直接作答。
没有命中时,只回复:【知识库未检索到本合同相关条款,请查阅原件或咨询法务/阿里云客户经理】。

# 作答
结论必须带章节或条款号,并摘一句原文。立场是甲方。框架与订单或补充协议冲突时,写明以订单/补充协议为准。
一问一答只答所问:加粗结论、一句原文、风险色标、一条建议。
用户要求总结或报告时,输出三块:【核心信息摘要】【关键条款清单】【风险清单与建议】。报告优先看:主体与期限、计费与最低消费、SLA 与赔付上限、数据与保密、责任上限与间接损失、终止与数据留存、管辖与单方变更。
🔴高风险:责任上限过低、免除重大损失、数据迁出限制、无 SLA 赔付、自动续费无提前告知、单方变更条款。
🟡中风险:表述模糊、期限不清、流程繁琐、赔付条件苛刻。
🟢低风险:常规商业条款。
每条风险包含:原文摘抄、风险说明、我方建议。
只做条款解读、提取、对比、风险识别和摘要。签署结论留给法务。
每次回复末尾加:本内容仅为合同条款辅助解析,不构成法律意见,合同签署前务必由公司法务最终审核。
只有用户要求总结或报告时,最后再问:是否需要整理成精简关键条款清单?"""

retrieval_model = {
    "search_method": "hybrid_search",
    "reranking_enable": True,
    "reranking_mode": "reranking_model",
    "reranking_model": {
        "reranking_provider_name": PROVIDER,
        "reranking_model_name": "bge-reranker-v2-m3",
    },
    "top_k": 4,
    "score_threshold_enabled": False,
    "weights": {
        "weight_type": "customized",
        "vector_setting": {
            "vector_weight": 0.7,
            "embedding_provider_name": PROVIDER,
            "embedding_model_name": "bge-m3",
        },
        "keyword_setting": {"keyword_weight": 0.3},
    },
}

init_body = {
    "name": DATASET_NAME,
    "indexing_technique": "high_quality",
    "doc_form": "text_model",
    "doc_language": "Chinese",
    "embedding_model": "bge-m3",
    "embedding_model_provider": PROVIDER,
    "process_rule": {"mode": "automatic"},
    "retrieval_model": retrieval_model,
}

knowledge_set = {
    "id": "retrieval-1",
    "name": "Retrieval 1",
    "description": "阿里云框架合同原文及附件",
    "query": {"mode": "generated_query"},
    "retrieval": {
        "mode": "multiple",
        "top_k": 4,
        "reranking_mode": "reranking_model",
        "reranking_enable": True,
        "reranking_model": {"provider": PROVIDER, "model": "bge-reranker-v2-m3"},
    },
    "metadata_filtering": {"mode": "disabled"},
}

print("系统提示词字数", len(SYSTEM_PROMPT))
print("创建知识库 POST /console/api/datasets/init")
print("查询模式用 generated_query。user_query 会在页面打开时查固定句子。")
print("保存草稿 PUT /console/api/agent/<id>/composer ,save_strategy=save_to_current_version")
print("发布 POST /console/api/agent/<id>/publish")

系统提示词 674 个字符,标记句是「本轮对话已经引用过」。保存草稿用 save_to_current_version。发布走 /publish。save_as_new_version 会被拒绝。

配置页要出现「知识检索」。Dify 1.17.1 的前端把 ENABLE_AGENT_KNOWLEDGE_RETRIEVAL 编译成关闭。官方 web 镜像重新创建后,这个开关回到关闭,需要再改前端包里的 !1 为 !0。现网这次迁进集群时,web Pod 用的是原镜像,页面补丁没有带进去。

召回可以拿这三句看分:保密信息约 0.94,承诺保底消费约 0.87,责任上限约 0.05。责任上限分数低,是因为合同里没有贴得很近的表述。检索不到就按提示词回复知识库没有这条,不编造。

案例 4:Langfuse 里看全链路和 Token

Langfuse 登录 http://10.0.1.47:3100,admin@example.com / Lf-trace-2026。Dify 应用的追踪开关在运行时配置里,关掉时提问不会送出追踪。合同 Agent 要先打开开关再提问。

一条已经对上的问答:

项值
trace / message373f2dcb-fd1e-4a4a-84d4-871050011a4b
时间2026-10-10 14:58(北京时间)
用户原话其实吧 这只是纠纷的时候才会刨根稳定的一个合同 平时除了折扣没啥用
模型qwen3.6-27b
token提示 26763,补全 862,合计 27625
New-API 日志第 125 条,额度 13813
金额13813 / 500000 = 0.027626 美元

同一分钟还有一笔额度 1692,合 0.003384 美元。两笔合计 0.031010 美元。Dify 的模型凭证没有单价,messages.total_price 是 0。新的对话会带上 token;美元数看 New-API 的额度,除非在模型凭证里填上单价。

案例 5:getServerStatus 出现在 Langfuse

工具按 config/server-status-openapi.yaml 注册到「运维巡检 Agent」。现网这个 Agent 的 id 是 01a124c7-9fce-70a8-9efa-41b2a6f5b7e7,应用 e4d614f6-de22-4f4c-b58e-caeb0504b121。服务器地址用 http://10.0.1.47:8777。

在对话里让它查询 gpu-server-03。Langfuse 里 Observation 的 Name 应等于 getServerStatus。这个名字来自 OpenAPI 的 operationId,再由 # AGENT_TOOL_SPANS_V2 写进追踪。

案例 6:Grafana 看显卡

打开 http://10.0.1.47:3001,admin / Gf-gpu-2026。数据源 Prometheus 指向 http://prometheus:9090。DCGM 在 9400 提供显存、温度、利用率。vLLM 的 /metrics 由 Prometheus 向 10.0.1.47:8000 抓取。看板文件放在 /usr/cluster/observability/metrics/grafana/dashboards。

写文档时核对到的数据