Files
adminandClaude Fable 5 82b954b957 docs(deploy): BFF 手动重启 venv 绝对路径陷阱(2026-09-15 实测)
根因:venv 在部署根 install/venv,BFF 必须 cd sdk 起 app.main:app——
cd sdk 后相对路径 venv/bin/python 解析成不存在的
install/sdk/venv/bin/python(setsid failed to execute,端口静默 DOWN)。
修正 5f 示例命令(cd 前解析绝对路径)并补老进程 socket 失效场景。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 10:54:10 +08:00

37 KiB
Raw Permalink Blame History

PlantAI-rag-new 编译部署全流程手册(agent 自动执行版)

供后续 agent 逐条照做的完整流程:编译源代码 → 打包(含数据库加密)→ 部署到指定 IP 目标服务器 → 部署后完整质检。 硬性要求:目标服务器上是编译过且数据库加密后的版本,除编译白名单明文面(约 53 个入口/契约 .py)外不得出现任何不在公开范围内的明文代码。 所有命令在构建机(当前生产服务器)执行,除非步骤明确标注「目标机」。 零硬编码纪律:命令中的路径全部使用变量/脚本位置推导,不得把宿主路径写进任何新增脚本。

0. 前置事实与约定(agent 必读,不得想当然)

项 事实
构建机 ssh -p 26518 xzq@192.168.0.175(内网);仓库 /home/xzq/5090/teamWorkspace/zh/PlantAI-rag-new,分支 feature/bioflow-conversation-orchestration
构建机 Python 统一 venv /home/xzq/5090/teamWorkspace/zh/plantai-rag-new-venv/bin/python(py3.12.13),各编译脚本的 [python] 参数一律传它
编译纪律 全部 .py → Cython → gcc hidden visibility → strip → elf 校验 → 删除 .py 与 .c 源码;白名单明文:各树入口 main/api + config + schemas + db + qa.py + sse_relay.py + bioflow_proxy.py + 全部 __init__.py(含 yield 的 SSE 模块必须明文,断流崩溃历史教训)
目标机绑定策略 nginx 80 = 唯一客户入口;8090/8091/8092/8093/8094/8098/8099 全部 127.0.0.1(BFF 改绑见 §5b,启动脚本默认已是 127.0.0.1)
数据库加密现状 QA 的 KB 数据(断言 9.5G+2.7G)已具备加密工具链(tooling/kb/encrypt_kb.sh,AES-256-GCM + kb.key),但 tooling.env 当前 SKIP_KB=1(定版前不执行);Qdrant 向量与 Neo4j 冷拷按设计明文(检索数学/存储引擎限制,见 tooling/KB_ENCRYPT_GUIDE.md);KB 解密密钥的运行时消费方尚未实现(生产实测无 kb.key 引用),此为本手册遗留缺口(见 §7 已知限制)
明文残余 vstore 曾明文部署;已新增 tooling/compile/compile_vstore.sh(12 模块 .so + api.py 白名单)并纳入打包流水线;vstore 的 .env 含 VSTORE_KEY(Fernet 密文钥匙)不入包
生信内核 bioflow_kernel/(528M)不参与本流程——它编译进 8 个 Docker 镜像(镜像内编译),镜像随 build_all 独立分发(docker save/load);目标机要生信还需 24 个镜像(8 bioflow-* + 16 plantai-env-*)与 BIOFLOW_DATA 数据根,详见 docs/bioflow_image_build_playbook.md
打包产物目录 tooling/build/<VERSION>/install/(VERSION 默认读 tooling/tooling.env,当前 2.0.0)
已就绪辅助脚本 deploy/ollama_restart.sh(本地嵌入模型拉起)、vstore/vstore_restart.sh(路径自推导重启)已入包
nginx 代理模板 deploy/nginx/plantai.conf(SSE 长连接 + 大文件上传适配,随安装包发布到 install/nginx/)
176 已部署 2026-09-09 按本标准完成 176 部署(隧道B 接入,/srv/plantai 布局:install + qa-db + data + docgen;nginx 80 唯一入口;无 GPU、无 docker、无生信);2026-09-10 增补 MinerU CPU 裸部署(§5c)与 embedding 裸部署固化(§5d)
QA 数据库裸进程 systemd 裸进程为最终部署标准(无 docker):qdrant v1.18.3 + neo4j 5.15.0-community,全部 127.0.0.1;端口 27433/27434 + 19888/19885;数据在 <部署根>/qa-db/;安装脚本 install/qa-db/install_qdrant.sh、install_neo4j.sh(均免编译:官方静态二进制/tarball,支持本地 bin/ 离线分发);175 上 docker 容器(27333/19788)为其他项目共享服务、不动

1. 编译阶段(构建机)

1.1 预检

cd /home/xzq/5090/teamWorkspace/zh/PlantAI-rag-new
git status --short | head -5                    # 应干净(或只有预期改动);有意外改动先报 TL
git log --oneline -1
PY=/home/xzq/5090/teamWorkspace/zh/plantai-rag-new-venv/bin/python
$PY -c 'import cython, setuptools; print("toolchain ok")'
df -h /home | tail -1                            # 余量 ≥20G

1.2 五树编译(自动,约 10-20 分钟)

cd /home/xzq/5090/teamWorkspace/zh/PlantAI-rag-new
PY=/home/xzq/5090/teamWorkspace/zh/plantai-rag-new-venv/bin/python

# 逐树编译(也可直接跑 §2 的 build_all.sh,它会串行调全部)
for spec in "sdk sdk" "agent/qa qa" "agent/methodology methodology" "agent/legacy-rag legacy-rag"; do
  set -- $spec
  echo "== compile $2 =="
  bash tooling/compile/compile_$([ "$2" = "legacy-rag" ] && echo legacy || echo "$2").sh "$1" "tooling/build/compiled-$2" "$PY"
done
# 注意 vstore 的编译脚本是 compile_vstore.sh:
bash tooling/compile/compile_vstore.sh vstore tooling/build/compiled-vstore "$PY"

# 每棵树编译完成后断言(关键质检点):
#   - .so 数量 = 脚本输出 modules 数;- 无 .c 残留;- 白名单 .py 存在
find tooling/build/compiled-sdk -name '*.so' | wc -l     # 期望 33(含私有资料模块)
find tooling/build/compiled-vstore -name '*.so' | wc -l  # 期望 12

1.3 编译产物 import 冒烟(每树)

PY=/home/xzq/5090/teamWorkspace/zh/plantai-rag-new-venv/bin/python
( cd tooling/build/compiled-sdk && PYTHONPATH=. $PY -c "import app.main" )
( cd tooling/build/compiled-vstore && PYTHONPATH=. $PY -c "import vstore.api" )
( cd tooling/build/compiled-qa && PYTHONPATH=.:./src $PY -c "import scripts.qa_v1_1_73_server" )

2. 数据库加密阶段(构建机;SKIP_KB=1 时按「尚未定版」跳过,但必须人工确认跳过原因)

cd /home/xzq/5090/teamWorkspace/zh/PlantAI-rag-new
grep SKIP_KB tooling/tooling.env
# SKIP_KB=1 → 跳过本节并记录「数据库未定版」;SKIP_KB=0 → 执行:
df -h /home | tail -1                              # ≥200G
ls -la tooling/build/2.0.0/kb.key                  # 若无:VERSION=2.0.0 bash tooling/crypto/init_keys.sh
bash tooling/kb/encrypt_kb.sh 2.0.0                # 1-2 小时,setsid 后台
# 产物断言:knowledge-2.0.0.tar + .sha256 存在;断言字段已 enc:v1 前缀

3. 一键打包阶段(build_all.sh 串行:编译→双审计→冒烟→KB→组装→SHA256+泄露门禁)

cd /home/xzq/5090/teamWorkspace/zh/PlantAI-rag-new
SKIP_KB=1 bash tooling/build_all.sh 2.0.1 2>&1 | tee /tmp/build_all.log
# 关键成功标志(逐条核对,任一不满足即失败):
grep -E 'compile\+harden complete|审计|泄露检查: 净|构建完成' /tmp/build_all.log
ls tooling/build/2.0.1/install/            # 应含 sdk qa methodology legacy-rag vstore ui start_all.sh ollama_restart.sh vstore_restart.sh SHA256SUMS
find tooling/build/2.0.1/install -name '*.so' | wc -l   # 期望 371+12
find tooling/build/2.0.1/install -name '*.py' | wc -l   # 期望 53(仅白名单)

4. 发布包组装(含镜像分发与 QA 运行面补齐)

cd /home/xzq/5090/teamWorkspace/zh/PlantAI-rag-new/tooling/build/2.0.1/install

# 4.1 QA 运行面补齐(编译树未自动含 prompts/env):
cp -r ../../../../agent/qa/prompts qa/prompts
cp ../../../../agent/qa/env/prod.env.example qa/env/prod.env.example

# 4.2 生信镜像导出(目标机要生信才做;约 20G+):
docker save -o /tmp/plantai-bioflow-images.tar $(docker images --format '{{.Repository}}:{{.Tag}}' | grep -E '^(bioflow|plantai-env)-')

# 4.2b 环境镜像全量同步原则(2026-09-14 起为部署原则,必做):
#   175 上全部 26 个环境类镜像(8 bioflow-* + 16 plantai-env-* +
#   plantai-allenv + plantai-protected)必须部署到 176——扩展分析的
#   runtime_image 可选范围 = 176 本地镜像全集。新增/修复镜像后必须同步,
#   并在 playbook 台账登记。流式传输(逐镜像,勿 tar 中转):
#     docker save <ref> | ssh -i ~/.ssh/id_ed25519_p2p -p 26518 xzq@192.168.0.176 'docker load'
#   修复镜像覆盖同名 tag 前先回退保存旧 tag(如 :dev-before-xxx)。
#   activate.d 陷阱:镜像构建后排查 conda env activate.d/*.sh 的宿主
#   绝对路径注入(PYTHONPATH 断链,panfamily 案例),必要时 COPY 模块进镜像。

# 4.3 目标机备份(同机/异机部署前备份;可选):
#   目标机:cp -r <deploy-root> <deploy-root>.bak.$(date +%s)

# 4.4 传输(示例 rsync;把 <target-host> 换成目标机地址):
rsync -av --progress tooling/build/2.0.1/install/ <target-host>:/srv/plantai/

5. 目标机部署

5.0 QA 数据库(systemd 裸进程,先于业务服务)

cd /srv/plantai/install
bash qa-db/install_qdrant.sh    # 需 sudo;默认装 /usr/local/bin/qdrant + 27433/27434
bash qa-db/install_neo4j.sh     # 需 sudo;默认装 /opt/neo4j-community-5.15.0 + 19888/19885
# 全新部署先按 5.0b 同步数据库文件再启动;换库用 qa-db/replace_db.sh

5.0b 数据库数据同步与整库替换(数据与安装包双制品分开传输)

数据库文件不进 install 安装包(否则每次发布拖 82G 且整树覆盖会波及在用的库)。 全新部署/整库更新时,按本小节显式同步到目标机 <部署根>/qa-db/;程序发布只传 install/。

# 1) 从源机(现网/构建机)同步数据库到目标机(显式步骤,与 install 包传输并列):
rsync -av --progress <源机>:/<源部署根>/qa-db/qdrant/storage/ /srv/plantai/qa-db/qdrant/storage/
rsync -av --progress <源机>:/<源部署根>/qa-db/neo4j/data/     /srv/plantai/qa-db/neo4j/data/
# 2) 属主对齐(install 脚本也会做;rsync 后建议立即执行):
sudo chown -R <运行用户>:<运行用户> /srv/plantai/qa-db/{qdrant,neo4j}
# 3) 启动/重启裸进程(探活 curl 27433/collections 与 bolt 19888):
sudo systemctl restart qdrant-plantai neo4j-plantai
# 4) 整库替换(用户高频操作,数据目录换新,自动探活+回滚):
sudo bash qa-db/replace_db.sh qdrant <新storage目录>
sudo bash qa-db/replace_db.sh neo4j   <新data目录>
  • 位置约定:数据根 = <部署根>/qa-db/(默认 /srv/plantai/qa-db;可用 QA_DB_ROOT env 覆盖,install 脚本与 replace_db.sh 都认)。
  • 免编译说明:qdrant 官方 release 静态二进制、neo4j 官方 community tarball,均无需编译;版本校验 = qdrant --version(v1.18.3)与 neo4j version(5.15.0)。受限网络用 qa-db/bin/ 离线载体(install 包自带,无则自动回退下载含国内镜像源)。
  • 运行用户:RUN_USER 默认 xzq(sudo 装 systemd 时自动取 SUDO_USER);systemd 单元已配 WorkingDirectory 与重启策略。

5.1 环境准备

#  - 安装 py3.12 venv 到 <安装根>/venv(start.sh/start_all.sh 均按 $ROOT/venv/bin/python 解析;175 旧路径为回退不再新建)
#  - 依赖安装:pip install torch==2.13.0 --index-url https://download.pytorch.org/whl/cpu 先行(PyPI 无 +cpu 构建),
#    其余依赖用构建机 venv 的 pip freeze 清单安装
#  - 编辑 .env(176 实战模板):
#      sdk/.env   = DOCGEN_LLM_*、VSTORE_BASE_URL=http://127.0.0.1:8094、BIOFLOW_ENABLED=false、
#                   BFF_NEO4J_URI_NORMALIZED=bolt://127.0.0.1:19888(裸进程机必填!默认值是 docker 口 19788)
#      vstore/.env= VSTORE_KEY、VSTORE_ROOT=<部署根>/docgen、VSTORE_EMBED_URL=http://127.0.0.1:11434、VSTORE_EMBED_MODEL=bge-m3:567m
#                   并复制一份到 <安装根>/.env(vstore_restart.sh 从安装根 source)
#      qa|methodology|legacy-rag/env/prod.env = 路径类键(*_ROOT/PYTHONPATH/reranker/data)改目标机布局,密钥原样
#  - methodology/legacy-rag 的 data 目录(data/evaluation/*、data/pc、data/split)是运行时依赖,须随部署复制
#  - KB 密钥(若 SKIP_KB=0):把 kb.key 放到目标机并 chmod 600

cd /srv/plantai/install
bash ollama_restart.sh          # 本地嵌入模型(已运行则跳过;缺失模型会提示 ollama pull bge-m3:567m)
bash start_all.sh               # 五服务 + 生信自动编排(2026-09-12 起):先拉 ollama → QA/methodology/legacy/vstore → 探测同级 bioflow/bioflow_service/restart.sh(存在则启动/重启+8098 探活,不存在跳过)→ BFF
# 生信首次部署(仅新装;此后 start_all 自动拉起):
docker load -i /tmp/plantai-bioflow-images.tar
# 创建 bioflow_service 部署树 + .env(DOCGEN_LLM_*、BIOFLOW_IMAGE_MAP_PATH 指向 bioflow_kernel/assets 内的 stage_image_map.json)+ bash bioflow_service/restart.sh
  • Ollama 离线携带(目标机连不上 ollama.com 时唯一路径):只拷二进制不够——完整布局为 /opt/ollama/bin/ollama(实体)+ /usr/local/bin/ollama(symlink 指回它)+ /opt/ollama/lib/ollama/(llama-server 与 ggml/cuda runner;缺失时报 llama-server binary not found)。模型拷 ~/.ollama/models/(bge-m3:567m 需 manifests + 对应 blob 两个文件,约 1.2G)。无 GPU 机器嵌入为 CPU 推理(单次约 50s,功能正常)。
  • 下载源可达性(2026-09-09 实测):pypi/github/gh-proxy.com/ghfast.top/download.pytorch.org/apt 可达;ollama.com、registry.ollama.ai、dist.neo4j.org、objects.githubusercontent.com 不可达——neo4j 用 qa-db/bin/ 离线 tarball(已入包),qdrant 下载走 gh-proxy 回退。
  • 内网直连传输:两机同内网时用目标机 26518 端口 + 专用密钥(如 id_ed25519_p2p)直连 rsync,不经跳板机;/srv/plantai 须先在目标机建好并 chown 部署用户。

5b. nginx 安装与 80 端口代理(目标机,从零开始)

目标机此前没有 nginx。本节让 80 端口成为唯一客户入口;8090 等全部服务只回环可达。 切换顺序是硬约束:先装 nginx 并经 80 验证反代正常,再执行 BFF 改绑,避免客户访问断档。

# 1. 安装(apt/yum 两系通用探测;需 root 或 sudo)
command -v apt-get >/dev/null && (apt-get update && apt-get install -y nginx) \
  || (yum install -y nginx)

# 2. 放置配置模板(conf.d 目录因发行版而异:先看 nginx -t 输出里的 include 路径,
#    常见 /etc/nginx/conf.d/ 或 /etc/nginx/conf.d/*.conf)
cp /srv/plantai/install/nginx/plantai.conf /etc/nginx/conf.d/plantai.conf
# 如 server_name 需要固定域名/内网 IP:sed -i 's/server_name _;/server_name 192.168.x.x;/' /etc/nginx/conf.d/plantai.conf

# 3. 语法校验 + 启用
nginx -t
systemctl enable --now nginx 2>/dev/null || nginx   # 无 systemd 的发行版直接 nginx 启动

# 4. 防火墙放行 80(如目标机有 ufw/firewalld)
ufw allow 80/tcp 2>/dev/null || true
firewall-cmd --add-service=http --permanent && firewall-cmd --reload 2>/dev/null || true

# 5. 经 80 验证反代(此时 8090 仍是旧绑定也没关系——80 → 127.0.0.1:8090 已通)
curl -s http://127.0.0.1:80/ | grep -o '<title>[^<]*'
curl -s http://127.0.0.1:80/api/health | python3 -m json.tool | head -5

# 6. BFF 改绑 127.0.0.1(start_all.sh 与 bff_restart.sh 默认已是 127.0.0.1):
#    用新版 start_all.sh 重启,或 bash deploy/bff_restart.sh(无参即 127.0.0.1)
ss -tln | grep ':8090 '    # 期望 127.0.0.1:8090,不再有 0.0.0.0

# 7. 内网最终验证
curl -s http://<目标机内网IP>:80/ | grep -o '<title>[^<]*'

nginx 模板关键适配(勿删勿改,详见 deploy/nginx/plantai.conf 注释):

  • SSE(/api/qa/chat/stream、/api/bioflow/*):proxy_buffering off + proxy_read_timeout 3600s(> BFF 看门狗 1200s),否则问答/生信流会被 nginx 缓冲或掐断。
  • 大文件上传(上传无大小限制;生信基因组数百 MB):client_max_body_size 0 + proxy_request_buffering off + proxy_send_timeout 600s。
  • 静态资源由 BFF 自身托管(ui/dist),nginx 只做反代,不自管静态目录。

5c. MinerU CPU 裸部署(目标机,无 GPU/无 docker/无 conda)

MinerU 以裸服务形态部署:构建机 conda env 目录整体搬运(env 内 bin/python 自包含,部署后不依赖 conda 二进制), 目标机仅需一个实体目录 + 一份模型缓存 + 一个 mineru.json 配置。2026-09-10 已在 176 全量验证(见下方冒烟)。

形态与位置约定:

  • 环境目录:/srv/plantai/mineru-cpu/(175 minerupy312 conda env 搬运,8.1G,含 torch 2.11.0+cu128 与 mineru 3.1.13);MINERU_CMD=/srv/plantai/mineru-cpu/bin/mineru。
  • 模型缓存:/srv/plantai/mineru-models/(175 ~/.cache/modelscope 同步,3.6G;PDF-Extract-Kit-1.0 与 MinerU2.5-Pro-2604-1.2B 两仓库)。
  • 模型本地化配置:~/mineru.json(或 MINERU_TOOLS_CONFIG_JSON 指向任意路径),models-dir 把 pipeline/vlm 两个 repo 映射到实体路径——mineru 3.1.13 的 MINERU_MODEL_SOURCE=local 依赖此映射,缺失时报 'NoneType' object has no attribute 'get'。

搬运步骤(175 构建机 → 176 目标机,P2P 内网直连):

# 1) 打包(注意 GNU tar 1.35 用 --numeric-owner 而非 --numeric-ids)
tar --numeric-owner -C /home/xzq/miniconda3/envs -cf /home/xzq/minerupy312-env.tar minerupy312

# 2) 传输(rsync 模板见 §5.1 内网直连;模型缓存与 env 包可并行传)
rsync -a --numeric-ids -e 'ssh -i ~/.ssh/id_ed25519_p2p -p 26518 -o BatchMode=yes' \
  /home/xzq/minerupy312-env.tar xzq@192.168.0.176:/srv/plantai/minerupy312-env.tar
rsync -a --numeric-ids -e 'ssh -i ~/.ssh/id_ed25519_p2p -p 26518 -o BatchMode=yes' \
  /home/xzq/.cache/modelscope/ xzq@192.168.0.176:/srv/plantai/mineru-models/

# 3) 解包(目标机;--strip-components=1 去掉 env 顶层目录名)
mkdir -p /srv/plantai/mineru-cpu
tar -xf /srv/plantai/minerupy312-env.tar -C /srv/plantai/mineru-cpu --strip-components=1

# 4) 重写 shebang(env 内 bin/* 的 shebang 是构建机绝对路径,不改则 bin/mineru 直接调用失败)
sed -i 's|#!/home/xzq/miniconda3/envs/minerupy312/bin/python|#!/srv/plantai/mineru-cpu/bin/python|' /srv/plantai/mineru-cpu/bin/*

modelscope 缓存软链陷阱(必做):modelscope 缓存顶层是真名→转义名的绝对路径软链 (如 PDF-Extract-Kit-1.0 -> /home/xzq/.cache/modelscope/hub/models/OpenDataLab/PDF-Extract-Kit-1___0)。 rsync 原样保留后软链指向 175 旧路径,在目标机是断链。目标机上需删除软链、把转义名实体目录复制回真名:

cd /srv/plantai/mineru-models/hub/models/OpenDataLab
rm PDF-Extract-Kit-1.0 MinerU2.5-Pro-2604-1.2B
cp -a PDF-Extract-Kit-1___0 PDF-Extract-Kit-1.0
cp -a MinerU2___5-Pro-2604-1___2B MinerU2.5-Pro-2604-1.2B

目标机 mineru.json(运行用户家目录;或放部署根并用 MINERU_TOOLS_CONFIG_JSON 显式指向):

{"models-dir": {
  "pipeline": "/srv/plantai/mineru-models/hub/models/OpenDataLab/PDF-Extract-Kit-1.0",
  "vlm": "/srv/plantai/mineru-models/hub/models/OpenDataLab/MinerU2.5-Pro-2604-1.2B"}}

prod.env 增量键(methodology/legacy-rag 的 env/prod.env,只加键值不改其他):

MINERU_CMD=/srv/plantai/mineru-cpu/bin/mineru
MINERU_DEVICE_MODE=cpu
METHODOLOGY_MINERU_DEVICE_MODE=cpu          # r3:显式设备开关(关键,见生效时序)
MINERU_MODEL_SOURCE=local
MINERU_TOOLS_CONFIG_JSON=/home/xzq/mineru.json
METHODOLOGY_MINERU_GPU_PREFLIGHT=0
METHODOLOGY_MINERU_RUNTIME_MODE=single
V15_MINERU_BACKEND=pipeline
# LLM 方法学提取阶段(r3,2026-09-10;两个 env 都加):
PLANTAI_GF_PYTHON_ENV=/srv/plantai/install/venv
DEEPSEEK_API_KEY=<与 sdk/.env DOCGEN_LLM_API_KEY 同款>
DEEPSEEK_BASE_URL=https://api.deepseek.com
DEEPSEEK_MODEL=deepseek-v4-flash
BBAV5_CAPABILITY_CATALOG=<部署根>/data/bbav5_capability_catalog.json

生效时序:r3 修复(2026-09-10)后代码已显式注入设备模式——控制键是 METHODOLOGY_MINERU_DEVICE_MODE (设 cpu 时注入 MINERU_DEVICE_MODE=cpu 并清空 CUDA_VISIBLE_DEVICES;不设则自动探测,175 GPU 机行为不变)。 176 实测三个必须:①MINERU_DEVICE_MODE 直读键不够(被 build_mineru_runtime 忽略),必须加 METHODOLOGY_MINERU_DEVICE_MODE=cpu; ②extraction 主上游是 legacy-rag 8093(BFF 8000 不存在时回落 8093),methodology 与 legacy-rag 两个 prod.env 都要加; ③LLM 阶段另需(batch 脚本 2026-09-10 已随包补入 install/*/scripts/run_methodology_batch.py): PLANTAI_GF_PYTHON_ENV=<目标机 venv>(GF python 路径,默认是 175 路径必炸)、DEEPSEEK_API_KEY/DEEPSEEK_BASE_URL/DEEPSEEK_MODEL、 BBAV5_CAPABILITY_CATALOG=<目标机 catalog 路径>(catalog 36K 数据文件随部署 rsync 到部署根 data/)。 176 全链实测:MinerU CPU 解析 22 页 ~67s → LLM 方法学提取 → completed 100%。

CPU 冒烟(关键验收,176 实测通过):

env MINERU_DEVICE_MODE=cpu MINERU_MODEL_SOURCE=local \
    MINERU_TOOLS_CONFIG_JSON=/home/xzq/mineru.json \
    MODELSCOPE_CACHE=/srv/plantai/mineru-models CUDA_VISIBLE_DEVICES= \
  /srv/plantai/mineru-cpu/bin/mineru -p <测试PDF> -o /tmp/mineru-smoke -b pipeline -l ch < /dev/null
# 断言:输出目录出现 *.md 且内容非空
find /tmp/mineru-smoke -name '*.md' -exec head -5 {} \;
  • stdin-EOF 陷阱:mineru 3.1.13 CLI 内置本地 API 子进程带 MINERU_API_SHUTDOWN_ON_STDIN_EOF=1 关闭监视器; 后台冒烟若不显式 < /dev/null(或经 nohup/setsid 使 stdin 立即 EOF),服务会在任务中途主动 shutdown,报「Async task failed」且错误为空。 methodology 以 subprocess 调 MINERU_CMD 时同样要保证 stdin 非管道 EOF(修复代码会显式传 MINERU_DEVICE_MODE 并规避此问题)。
  • 实测数据(176,AMD EPYC 7542 128 核 / 251G 内存):22 页英文 PDF 全程 67s(模型初始化约 4s); 输出 markdown 74KB 与 175 GPU 机基线逐字节一致(md5 相同)。CPU 比 GPU 慢数倍属预期(layout/OCR 全走 CPU 推理)。
  • mineru --version 输出 mineru, version 3.1.13;torch 2.11.0+cu128 在无 GPU 机 import 正常(CPU 路径),无需重装 CPU torch。
  • 备用路径(整目录搬运冒烟失败时才走):目标机 python3 -m venv /opt/mineru-cpu-venv 裸装(CPU torch + mineru 3.1.13), 模型缓存与 mineru.json 同上。本次整目录搬运一次通过,未启用。

5d. 本地向量库 embedding 模型部署(Ollama 裸服务,离线)

176 上 embedding 已裸部署(实体进程,非 docker):Ollama 官方二进制 + bge-m3:567m,vstore 已指向它。本节为文档固化。

离线布局(Ollama 三件套):

  • /opt/ollama/bin/ollama(实体二进制,38M)
  • /usr/local/bin/ollama -> /opt/ollama/bin/ollama(symlink,进 PATH)
  • /opt/ollama/lib/ollama/(llama-server + libggml-cpu-*.so runner + cuda_v12/cuda_v13;缺失时报 llama-server binary not found)

bge-m3 模型位置:~/.ollama/models/(manifests + blobs,1.1G;bge-m3:567m = 1 个 manifest + 对应 blob)。

vstore env 指向(vstore/.env,与 §5.1 一致):

VSTORE_EMBED_URL=http://127.0.0.1:11434
VSTORE_EMBED_MODEL=bge-m3:567m

服务进程:/usr/local/bin/ollama serve 常驻实体进程(pgrep -af ollama);拉起脚本 install/ollama_restart.sh。

离线安装法(目标机连不上 ollama.com/registry.ollama.ai 时唯一路径):

  1. 源机把 /opt/ollama/ 整目录与 /usr/local/bin/ollama symlink 打包(tar --numeric-owner);
  2. 模型目录 ~/.ollama/models/ 一并 tar(注意属主:目标机同 UID 用户解包,否则 chown);
  3. 目标机解包到相同路径,ollama serve 启动后 ollama list 应见 bge-m3:567m(否则 manifests 与 blobs 不匹配)。

性能事实(176 无 GPU,CPU 推理):

  • 短句(12 字)单次嵌入 4.1s;780 字长文本 1.0s(2026-09-10 实测,模型常驻后);
  • 单篇文档全量嵌入(多 chunk 累计)约 52s 属历史实测值——功能正常,纯性能差距,无 GPU 机器属预期。

5e. 论文图片本地源部署(figures 双源,2026-09-11 起部署必做)

BFF 答案配图走双源:本地 MinerU 图片优先,PMC 外网兜底。本地源三个条件缺一 即静默降级为纯 PMC(无报错、无日志,唯一症状是配图变少或全走外网)。

  1. env 两键(sdk/.env,与 BFF 同 env,BFF 重启后生效):

    PLANTAI_FIGURES_MINERU_ROOT=/srv/plantai/data/evaluation/methodology_upload_jobs
    PLANTAI_FIGURES_REGISTRY=/srv/plantai/data/knowledge_base/paper_metadata_registry_v1_20260717/paper_metadata_registry_full.jsonl
    
  2. registry 文件传输:源机 knowledge_base/paper_metadata_registry_v1_20260717/ paper_metadata_registry_full.jsonl(16.9MB)经 p2p 传到目标机上述路径;该文件 是 pmcid→paper_id 映射,缺失时本地索引即使有图也映射不出 PMCID。

  3. BFF 重启:必须手动 kill 旧 BFF(见 §5f 陷阱)。

验证:用 /tmp 假任务目录受控测试——demo_job/mineru/paper/txt/*_content_list.json (type=image 条目)+ methodology_papers.csv(paper_id 取 registry 内带 pmcid 的), 加载 figures 模块 _build_local_index() 应产出 source=local 条目;或部署后真实上传 一篇 registry 内论文后提问验证。

已知限制(2026-09-11 实测):本地图片只对「paper_id 在 registry 中」的任务命中; 历史 demo_paper/e2e_check 测试任务不产生本地图片索引。

5f. 部署陷阱:BFF 重启必须手动 kill(start_all 不替换在跑进程)

start_all.sh 不杀已运行的 BFF:8090 被旧进程占住时,脚本内 uvicorn 新进程因端口 占用静默退出,自检看到的仍是旧进程(新 env 不生效)。步骤:

pgrep -f "uvicorn app.main:app" | xargs kill     # 1. 杀旧 BFF(杀不掉用 kill -9;老进程可能活着但 socket 已失效,ss 看不到监听)
cd /srv/plantai/install && set -a && . sdk/.env && set +a   # 2. 先回部署根 source env
( cd sdk && setsid /srv/plantai/install/venv/bin/python -m uvicorn app.main:app \
  --host 127.0.0.1 --port 8090 > /tmp/plantai-sdk.log 2>&1 < /dev/null & ) && disown -a
# 3. 验证:ss 与 health 都要查(不能只看进程)
ss -tln | grep ':8090 ' && curl -s http://127.0.0.1:8090/api/health

venv 路径陷阱(2026-09-15 实测):venv 在部署根 /srv/plantai/install/venv,而 BFF 必须 cd sdk 才能找到 app.main:app——必须在 cd sdk 之前把 python 解析成绝对路径 (或 ../venv/bin/python),否则 cd sdk && venv/bin/python 解析成不存在的 install/sdk/venv/bin/python(setsid: failed to execute venv/bin/python: No such file or directory, 错误在 /tmp/plantai-sdk.log,端口静默 DOWN)。start_all 模板与此处均已按此纪律修正。

5g. r4 部署清单(2026-09-12,六项缺陷修复轮)

  1. QA 双域相关性阈值(qa/env/prod.env,可选;不设则用代码默认值):

    QA_MIN_VECTOR_SCORE=0.45   # 检索模式向量域(Qdrant score_threshold 同源)
    QA_MIN_SOURCE_SCORE=0.25   # chat 模式最终域(rerank 后过滤)
    
  2. MinerU 依赖 accelerate:目标机 mineru 环境需 pip install accelerate——缺失 症状为启动期失败「requires accelerate」(r4 的自动重试会报中文可读错误但不会自愈)。

  3. figures 两键 + registry:见 §5e。

  4. vstore 内容去重:vault.add 按 SHA256 覆盖旧 doc,无需 env;存量重复数据如需清理 走 rebuild-index/rebuild-chunks 端点。

  5. r4 前端 dist:ui/dist 重构建(含 intent 词典「总结/概括/归纳」、KgPane 空态提示、 SourceRow 标题 fallback、DocPane 状态机修复)。

6. 部署后完整质检(agent 逐条执行并记录结果,全过才算部署成功)

# 6.1 进程与端口
ss -tln | grep -E ':(8090|8091|8092|8093|8094|8098)'    # 8090=0.0.0.0,其余 127.0.0.1

# 6.0b QA 数据库裸进程专项
systemctl is-active qdrant-plantai neo4j-plantai          # 均 active
ss -tln | grep -E ':(27433|27434|19888|19885)'            # 全部 127.0.0.1
curl -s http://127.0.0.1:27433/collections | python3 -m json.tool | head -8   # 含 plant_srt_pc2025_v3_20260817_normalized
curl -s http://127.0.0.1:8091/health | python3 -m json.tool | head -8          # neo4j_entities 6438960 / qdrant_points 13040841
# 经 19888 bolt 登录验证(凭据用 QA 运行中进程的 NEO4J_PASSWORD,与容器一致):
/opt/neo4j-community-5.15.0/bin/cypher-shell -a bolt://127.0.0.1:19888 -u neo4j -p "$NEO4J_PASSWORD" "MATCH (n) RETURN count(n);"

# 6.1b nginx 专项
systemctl is-active nginx 2>/dev/null || pgrep -x nginx    # nginx 存活
ss -tln | grep ':80 '                                     # 80 端口监听(0.0.0.0 或目标机内网 IP)
curl -s --max-time 5 http://127.0.0.1:80/ | grep -o '<title>[^<]*'          # 经 80 首页可达
curl -s --max-time 10 -N -X POST http://127.0.0.1:80/api/qa/chat/stream \
  -H 'Content-Type: application/json' \
  -d '{"question":"水稻SPL基因家族的功能研究有哪些关键进展?","use_llm":true,"session_id":"nginx-smoke"}' \
  | head -c 300          # 期望收到 "event: progress" 或 "event: complete" 帧(SSE 未被缓冲)
printf 'nginx 上传冒烟' > /tmp/nginx-upload-smoke.txt
curl -s --max-time 120 -X POST http://127.0.0.1:80/api/docgen/library \
  -F 'file=@/tmp/nginx-upload-smoke.txt' -F 'name=nginx上传冒烟' -F 'as_template=false' | head -c 200
# 期望返回 ok:true + items(经 80 上传可用);记录 library_id 便于清理(DELETE /api/docgen/library/{id})

# 6.2 健康检查
curl -s http://127.0.0.1:8090/api/health | python3 -m json.tool     # status ok、upstreams 全 ok
curl -s http://127.0.0.1:8094/api/vstore/health                      # ok、count 与 chunk_count

# 6.3 无明文代码核验(目标机部署树上执行,硬性门槛)
cd <deploy-root>
echo "剩余 .py 应为白名单(53 个以内):"; find sdk qa methodology legacy-rag vstore -name '*.py' | wc -l
echo "应无 .c 残留:"; find . -name '*.c' | wc -l
echo "应无宿主路径泄漏:"; grep -rn '/home/xzq' sdk/app/main.py sdk/app/config.py 2>/dev/null | wc -l   # 期望 0

# 6.4 数据库加密核验(SKIP_KB=0 时)
# 断言:断言文件 enc:v1 前缀;kb.key 权限 600;目标机无明文 jsonl/parquet

# 6.5 功能冒烟(私有资料问答全链)
curl -s -X POST http://127.0.0.1:8090/api/qa/private -H 'Content-Type: application/json' \
  -d '{"question":"批量导入压测样例 序号 38 是什么?"}' | python3 -m json.tool   # hit:true + items + 带 [资料n] 的 answer

# 6.6 回归
curl -s -X POST http://127.0.0.1:8090/api/qa/chat/stream -H 'Content-Type: application/json' \
  -d '{"question":"水稻SPL基因家族的功能研究有哪些关键进展?","use_llm":true,"session_id":"smoke"}' | head -c 200
curl -s 'http://127.0.0.1:8094/api/vstore/search?q=测试&as_template=false' | python3 -m json.tool  # doc 级检索回归

6.7 功能自测(部署完成后必须实际跑通,非仅探活)

§6.1-6.6 证明「起得来」,本节证明「用得了」。以下五项在 2026-09-09 的 176 部署上全部实跑通过; 新目标机部署完成后同样必须逐项执行并记录结果,任何一项不过不得交付。

6.7.1 QA 完整 LLM 答案(不能只收到 progress 帧就收工)

curl -s --max-time 300 -N -X POST http://127.0.0.1:80/api/qa/chat/stream \
  -H 'Content-Type: application/json' \
  -d '{"question":"水稻SPL基因家族的功能研究有哪些关键进展?","use_llm":true,"session_id":"selftest"}' \
  | grep -E '^event:' | sort | uniq -c
# 期望:多个 progress + 至少 1 个 complete;complete 的 data.answer 为完整 markdown 答案(数百字以上)

6.7.2 新文档入库闭环(上传→嵌入→检索→清理,无 GPU 机器嵌入单次约 50s)

printf 'PlantAI 部署自测文档 独特标志串 QA_SELFTEST_SENTINEL 水稻氮素利用效率' > /tmp/selftest-doc.txt
LIB_ID=$(curl -s --max-time 120 -X POST http://127.0.0.1:80/api/docgen/library \
  -F 'file=@/tmp/selftest-doc.txt' -F 'name=部署自测文档' -F 'as_template=false' \
  | python3 -c 'import json,sys; print(json.load(sys.stdin)["items"][0]["library_id"])')
echo "library_id=$LIB_ID"
curl -s --max-time 30 'http://127.0.0.1:8094/api/vstore/search?q=水稻氮素利用效率&as_template=false' \
  | python3 -m json.tool | head -20          # 期望命中「部署自测文档」
curl -s -X DELETE "http://127.0.0.1:8090/api/docgen/library/$LIB_ID" | head -c 100   # 清理

6.7.3 legacy-rag 真实图谱查询(注意冷缓存 COUNT 聚合 >60s,curl 超时必须 ≥120s)

curl -s --max-time 120 http://127.0.0.1:8093/api/v1/kg/stats | python3 -m json.tool
# 期望 status=success 且 relations 计数与源机一致(176 实测:SUPPORTED_BY 13092194 / ASSERTS 13040841)
curl -s --max-time 120 'http://127.0.0.1:8093/api/v1/kg/search?q=SPL&limit=3' | head -c 200

6.7.4 methodology 真实任务流

curl -s --max-time 60 http://127.0.0.1:8092/api/v1/methodology/status | python3 -m json.tool
# 期望 200 且 papers_table/batch_dir 指向目标机实际布局(相对路径已由 env 解析为绝对路径)

6.7.5 生信(部署了生信时必做)——镜像冒烟 + 金标 E2E 实跑

# a) 镜像冒烟 + 金标等价性(真实 docker 计算,约 5-15 分钟;无 GPU 更久):
cd <生信部署根>/bioflow_kernel
BIOFLOW_DATA=<生信部署根>/bioflow_data CPU=16 MEM=64g \
  bash tests/smoke_golden.sh bioflow-gf:dev
# 期望 4 步全绿:[1/4] python 版本与镜像约定一致;[2/4] ldd 0 缺库;[3/4] CLEAN;
#   [4/4] GOLDEN_DIFF_OK 24 IDs(与金标 expected ids diff 为空)
# b) 意图分流经 80 全链:
curl -s --max-time 90 -N -X POST http://127.0.0.1:80/api/bioflow/chat/stream \
  -H 'Content-Type: application/json' \
  -d '{"message":"帮我跑水稻 SPL 家族鉴定","session_id":"selftest-bioflow"}' | head -c 800
# 期望:event: plan_ready 携带 ≥2 条候选路径(stages 链非空);问答句应被分流到 QA 而非生信

自测结论记录:以上五项全部通过后,在部署记录中留一行「§6.7 功能自测 5/5 通过」,并注明金标 24 ID 结果与 两机一致性对比(QA 答案/图谱计数/金标 diff 与源机对照)。

6.8 normalized 图谱索引(缺陷 #32,2026-09-10 起部署必做)

背景:176 的 normalized 实例(Entity/ASSERTS,bolt 19888)没有 name_norm 索引, BFF /api/kg/entities 前缀/等值查询会退化为全标签扫描(175 同源实例实测 634 万 Entity 等值/前缀约 2.8~2.9s,超出 S2 <1s 验收阈值)。索引脚本 deploy/qa-db/create_neo4j_indexes.sh 幂等补齐两个 RANGE 索引后命中索引。

# 显式传参(推荐;176 裸进程实例端口 19888):
bash deploy/qa-db/create_neo4j_indexes.sh bolt://127.0.0.1:19888 neo4j '<BFF_NEO4J_PASSWORD_NORMALIZED>'
# 或读环境变量:
NEO4J_URI=bolt://127.0.0.1:19888 NEO4J_PASSWORD='<密码>' bash deploy/qa-db/create_neo4j_indexes.sh

创建的索引(名称固定,BFF 健康探测按名校验):

  • plantai_normalized_entity_name_norm(Entity.name_norm,RANGE,BFF 等值+前缀查询)
  • plantai_normalized_asserts_relation_category(ASSERTS.relation_category,RANGE, 图谱关系类别概览聚合)

校验:GET /api/health 的 Neo4j 详情不再出现「name_norm 索引缺失」; 或直接跑脚本自带 SHOW INDEXES 输出,两索引 state 均 ONLINE。

7. 已知限制与遗留缺口(agent 不得自行补,报 TL 裁决)

  1. SKIP_KB=1:数据库未定版,KB 加密打包当前跳过;真实交付前需 TL 拍板「定版」并执行 §2 全量加密(1-2 小时)。
  2. KB 解密密钥运行时消费方未实现:加密包部署后尚无代码在启动时用 kb.key 解密断言数据(生产实测 QA 无 kb.key 引用)——这是「加密后数据库可被系统使用」的前提缺口,需要另行立项(QA 侧 .so 金标树改造或 BFF 解密层)。
  3. Neo4j normalized 数据(8.2G)按设计明文冷拷(存储引擎限制,08-24 起无写入),属公开范围内例外,见 KB_ENCRYPT_GUIDE.md。
  4. 构建机与目标机若为同一台机器(内网 192.168.0.175 现状),部署就是替换现有 plantai-rag-new-deploy-unified/ 部署树;执行前先备份、执行后按 §6 全量质检。
  5. 本手册基于 2026-09-08 服务器实测编写;如工具链或目录结构有变,先对照仓库实际脚本修正本手册再执行。
  6. 当前生产服务器(192.168.0.175)仍是 0.0.0.0:8090 直连形态,尚未执行 §5b 的 nginx 迁移——启动脚本默认值已改为 127.0.0.1,但现网进程仍是旧绑定;迁移时按 §5b 顺序执行(先 nginx 后改绑),不要直接无参重启 bff_restart.sh 以免客户访问断档。
  7. start_all.sh 不会替换在跑的 BFF(2026-09-11 实测):BFF 更新 env/代码后必须手动 kill + 重启,详见 §5f。
  8. 175 上 QA 数据库 docker 容器(27333/19788)为其他项目共享服务,不随 PlantAI 部署流程管理;PlantAI-rag-new 一律走裸进程(§5.0/5.0b);遗留库(26333/18788)现网无引用,清理需用户单独命令。