【FDE系列】阶段3:Day 72:RAGFlow 平台 — 端到端知识库

📚前言

📒FDE系列内容总纲:

【大纲】FDE 前沿部署工程师学习系列教程-CSDN博客

🚄前置课程列表:

见文档结尾附录。


🚀阶段3·Day 72:RAGFlow 平台 --- 端到端知识库

FDE 学习系列教程 · 第三阶段 · 第 11 周 · Day 2 预计时长:3 小时 | 难度:★★★☆☆ | 前置知识:Day 61-71(RAG 链路、Qdrant 混合检索、解析与分块、权限过滤、查询改写) 对标大纲课时:3.2.9 高级 RAG ------ 工具条 RAGFlow / LlamaIndex,实践项"用 RAGFlow 搭建端到端知识库"
📌 一句话目标 :用 Docker Compose 拉起 RAGFlow,把同一份 manual_a3 手册在平台上重做一遍知识库(建库 → 解析 → 分块可视化 → 检索对照),然后站在 FDE 立场算清一笔账:什么场景用平台、什么场景必须自研、什么场景两者混合。


🧑‍🤝‍🧑 开场:信息化主任的一句话,问到了 FDE 的命门

Day 71 收工后,你把 Recall@5 从 0.417 拉到 0.917,兴冲冲地去给客户做第二次演示。演示很顺利,散会时客户的信息化主任把你拉住,问了一个跟技术毫无关系的问题:

复制代码
  "这套东西,我们自己的运维能接管吗?"

你愣了一下,开始解释:"可以,就是 Qdrant 一个容器、BGE-M3 一个模型、十几个 Python 文件、一堆 pip 依赖......"说到一半你自己停住了。因为你脑子里快速过了一遍交接清单:

复制代码
  交接清单(自研方案的真实长度):
    □ docker-compose.yml        Qdrant + 可选 MinIO
    □ requirements.txt          openai / qdrant-client / FlagEmbedding / rank_bm25
                                / jieba / fastapi / pydantic / httpx / langfuse
    □ kb/ 目录 12 个 py 文件      qdrant_store / embed / bm25_index / hybrid
                                / reranker / pipeline / cite / authz / audit
                                / rewrite / hyde / advanced / api
    □ config/acl.yml            谁配?客户运维看得懂吗?
    □ config/glossary.json      还好,这个他们能改
    □ BGE-M3 模型 2.2GB         客户内网没外网怎么下?
    □ 灌库脚本 + 增量更新逻辑    改了一页手册怎么只更新那一页?
    □ 一套没人会用的评测脚本    改了参数怎么知道变好还是变坏?
  ─────────────────────────────────────────────────
  结论:能接管,但需要一个懂 Python 的人全职维护。客户没有这种人。

这就是今天要解决的问题。第 9-11 周你写了三周代码,今天要看清楚:这些代码里,有多少是"每个客户都要重写的",有多少是"平台已经替你写好的"。

复制代码
  第二阶段:设备告警工单闭环(FastAPI + MySQL + Docker + 飞书推送)
        ↓
  第 7-8 周:v0.1 智能工单助手(Day 60 冻结)
        ↓
  第 9-10 周:知识库问答 v0.2(自研代码方案)BGE-M3 + Qdrant + 混合检索 + 溯源 + 权限
        ↓
  Day 71:查询侧升级(改写 / HyDE / Multi-Query / 自判)
        ↓
  Day 72(今天)🆕:同一件事用【平台】再做一遍 → 对比 → 选型结论
        ↓
  Day 75:v0.2 冻结(用哪个方案交付,今天定)→ Day 85 v0.3 → Day 100 v1.0

今天七件事:① FDE 选型的三层判断(什么时候不该 自己写代码);② Docker Compose 拉起 RAGFlow(含 vm.max_map_count 这个必踩的坑);③ 搞懂 RAGFlow 在替你做哪四件事(DeepDoc 解析 / 模板分块 / 混合检索 / 引用快照);④ 建库、上传、解析、看分块;⑤ 检索对照:同一问题在平台和自研上的结果差异;⑥ 平台 vs 自研的成本账与决策树;⑦ 混合方案:用平台做解析,用自研做检索与权限。

💡 今天的关键词不是"换工具",是"知道自己写的每一行代码值不值"。 一个成熟的 FDE 不是"什么都能自己写"的人,而是**"能准确判断哪些东西不该自己写"**的人。前者是能力,后者是判断力------后者更贵。


📖 一、FDE 选型:什么时候不该自己写代码

1.1 三层判断法

面对任何一个"我要不要自己实现"的问题,按三层问下去:

复制代码
  L1 这件事是不是【每家客户都一样】?
     是 → 不该自己写。每家都一样的东西,早就有开源/商业方案了。
     例:PDF 解析、分块、向量库、混合检索、Web 界面、用户管理
     否 → 继续问 L2

  L2 这件事是不是【我们公司的差异化竞争力】?
     是 → 必须自己写,而且要写深。
     例:设备告警工单的业务闭环、跟客户 MES 系统的对接、行业术语表
     否 → 继续问 L3

  L3 自己写的【长期维护成本】谁承担?
     客户没有人维护 → 用平台,让平台厂商去维护
     我们自己有交付团队 → 可以自研,但要有版本管理和回归测试

把这三套到你的 v0.2 上:

环节 L1 每家都一样? L2 差异化? 结论
PDF/Word 解析 ✅ 完全一样 ❌ 用平台 / MinerU
文本分块 ✅ 基本一样 微调有价值 用平台,保留手改能力
Embedding 模型 ✅ ❌ 用平台内置的 BGE
向量库与混合检索 ✅ ❌ 用平台内置
Web 界面 / 用户管理 ✅ ❌ 用平台
引用溯源展示 ✅ ❌ 用平台(引用快照很成熟)
查询改写 / 术语表 部分 ⚠️ 术语表是差异化 术语表自己维护,改写可用平台
块级权限 acl ❌ 各家权限模型不同 ✅ ⚠️ 必须自己写(见 6.3)
与工单系统联动 ❌ ✅ 核心竞争力 必须自己写
飞书/企微推送 ❌ ✅ 必须自己写
复制代码
  ⭐ 划一条线就清楚了:
     【资料处理】这一半 → 平台。每家客户都一样,平台做得比你好,还带界面。
     【业务集成】那一半 → 自研。这是你收钱的理由,平台帮不上忙。
     今天的全部价值,就是学会把这条线画在哪。

1.2 但平台方案有三个真实的代价

别急着倒向平台。FDE 踩过的坑,比官方文档写的多:

代价 具体表现 什么时候会致命
① 可控性下降 分块策略、检索公式、Prompt 都在平台里,改不动或只能改一部分 客户要求"按我们行业的规程分块"
② 黑盒排错 答错了,你不知道是哪一步错了 客户追问"为什么这条没召回"
③ 资源与运维 RAGFlow 全栈要 16GB 内存 + 50GB 磁盘,含 ES/MySQL/Redis/MinIO 四个依赖 客户只给 8GB 内存的虚拟机
复制代码
  ⚠️ 最真实的代价其实是第 ③条:
     自研方案:Qdrant 一个容器,2GB 内存能跑。
     RAGFlow:MySQL + Redis + MinIO + Elasticsearch/Infinity + ragflow-server,
              官方要求 16GB 内存、50GB 磁盘。
     在客户现场,"这台机器只有 8G 内存" 是常态,不是例外。
     → 所以今天的结论不会是"全用平台",而是第 6 节的【混合方案】。

🖥️ 二、Docker Compose 部署 RAGFlow

实操步骤 1:核对前置条件(别急着拉镜像)

复制代码
# ① CPU / 内存 / 磁盘(官方要求:≥4核 / ≥16GB / ≥50GB)
nproc && free -g && df -h /

# ② Docker 与 Compose 版本(官方要求:Docker ≥24.0.0、Compose ≥v2.26.1)
docker --version && docker compose version

# ③ ⭐ Linux / WSL2 必须检查这一项(官方文档明确要求 ≥262144)
sysctl vm.max_map_count

  ⚠️ vm.max_map_count 是 RAGFlow 部署【第一大坑】。
     它决定一个进程能有多少个内存映射区。Elasticsearch / Infinity 这类
     文档引擎会大量使用 mmap,默认值(通常 65530)根本不够,
     表现是:容器起来了,但 ES 反复 OOM 重启,docker ps 一闪一闪。
     
     Linux 临时生效:sudo sysctl -w vm.max_map_count=262144
     Linux 永久生效:在 /etc/sysctl.conf 里加一行 vm.max_map_count=262144
     WSL2         :在 Windows 用户目录建 .wslconfig,写
                    [wsl2]
                    kernelCommandLine = sysctl.vm.max_map_count=262144
                    然后 wsl --shutdown 重启
     Windows / Mac(Docker Desktop):走的是虚拟机,需进虚拟机内设置;
                    具体方式随 Docker Desktop 版本变化,【以官方文档为准】
     官方文档:https://ragflow.io/docs/dev/

📌 部署前先读一眼官方"前提条件"页面。 RAGFlow 迭代很快(版本号、compose 文件、默认文档引擎都在变),本教程给的是写稿时核实过的官方命令 ,但你动手那天应该先打开 https://ragflow.io/docs/dev/ 确认一遍。FDE 的基本功之一:不拿三个月前记得的命令去客户现场执行。

实操步骤 2:拉代码并起服务

复制代码
# ① 克隆官方仓库
git clone https://github.com/infiniflow/ragflow.git
cd ragflow/docker

# ② 先看一眼 .env,确认镜像版本(写稿时官方示例为 v0.19.1 / v0.19.1-slim)
cat .env | grep -E "RAGFLOW_IMAGE|SVR_HTTP_PORT|DOC_ENGINE"

.env 里几个关键变量(实际取值以你拉到的版本为准):

变量 作用 说明
RAGFLOW_IMAGE 镜像版本 完整版内置 embedding 模型,镜像约 9GB;-slim 精简版约 2GB,需自行配置 embedding 模型
SVR_WEB_HTTP_PORT Web 端口 官方默认 80
SVR_HTTP_PORT HTTP API 端口 默认 9380
DOC_ENGINE 文档引擎 官方站点写"默认 Elasticsearch",部分版本 .env 默认 infinity;以你拉到的 .env 注释为准
REGISTER_ENABLED 是否开放注册 生产环境建议设为 0
复制代码
# ③ 起基础服务(MySQL / Redis / MinIO / 文档引擎)
#    注意:不同版本的 compose 文件划分不同 ------ 较新版本可直接起主 compose,
#    若你拉到的仓库里存在 docker-compose-base.yml,就先起它再起主的。
docker compose -f docker-compose-base.yml up -d
docker compose -f docker-compose-base.yml ps      # 等四个都 healthy

# ④ 起 RAGFlow 主服务
docker compose -f docker-compose.yml up -d

# ⑤ 盯日志,看到下面的 ASCII 大字 + "Running on all addresses" 才算真起来
docker logs -f ragflow-server

启动成功的标志(官方 README 里的那段 ASCII Logo):

复制代码
 ____   ___    ______ ______ __
/ __ \ /   |  / ____// ____// /____  _      __
/ /_/ // /| | / / __ / /_   / // __ \| | /| / /
/ _, _// ___ |/ /_/ // __/  / // /_/ /| |/ |/ /
/_/ |_|/_/  |_|\____//_/    /_/ \____/ |__/|__/

 * Running on all addresses (0.0.0.0)

⚠️ 官方明确提醒:没看到这段输出就去登录,浏览器会报"network anormal / 网络异常"。 第一次启动要拉 9GB 镜像 + 初始化文档引擎,慢是正常的(5-15 分钟)。这段时间不要反复 docker compose restart------ES 在初始化索引,重启会让它从头再来。

实操步骤 3:登录并改掉默认密码

浏览器打开 http://localhost(若改过 SVR_WEB_HTTP_PORT 就用你改的端口)。

复制代码
  默认管理员账号:admin@ragflow.io
  
  初始密码怎么来的(官方最新文档):
    · 若设置了环境变量 ADMIN_DEFAULT_PASSWORD(或 DEFAULT_SUPERUSER_PASSWORD),用它
    · 否则会在 admin server 首次启动时【随机生成】,并写入
      logs/admin_bootstrap_password.txt(权限 0600)
      Docker 部署请到 docker/ragflow-logs 卷里找这个文件
  ⚠️ 早期一些版本/第三方文档里写的是默认密码 admin ------ 版本差异较大,
     一切以 https://ragflow.io/docs/ 当前版本文档为准。

# 拿随机密码(Docker 部署)
docker exec -it ragflow-server sh -c "cat /ragflow/logs/admin_bootstrap_password.txt"

🔒 登录进去第一件事就是改密码。 另外 RAGFlow 还有一个 Admin UI(地址后加 /admin,如 http://localhost/admin),能做服务健康检查、用户管理、角色权限、系统设置------这个界面只应该暴露给可信管理员,别放在公网。

实操步骤 4:配 LLM 与 Embedding 模型

RAGFlow 是"模型无关"的,聊天模型和 Embedding 模型都要自己配。按系列硬约束,聊天模型用 DeepSeek:

复制代码
  操作路径(Web 界面):
    右上角头像 → 模型提供商(Model Providers)→ 选 OpenAI-API-compatible
      · 模型类型:chat
      · 模型名称:deepseek-chat
      · Base URL:https://api.deepseek.com
      · API Key :填你的 DEEPSEEK_API_KEY(从 .env 读,别手抄)
    → 设为默认聊天模型

  Embedding 模型:
    若你用的是【完整版镜像】(v0.19.1,约 9GB),内置了 BGE 系列,直接选一个即可
    若你用的是【slim 版镜像】,必须自己配 ------ 推荐 BAAI/bge-m3(Day62 用的同一个)
  ⚠️ 具体可选项随版本变化,以界面上能选到的为准。

# 顺手确认服务健康(也可在 Admin UI 的"服务状态"页看)
curl -s http://localhost:9380/api/v1/system/version | head -c 200
docker compose -f docker-compose.yml ps

  💡 一个现实提醒:
     RAGFlow 的【知识图谱】分块方式(chunk_method="knowledge_graph")会调用 LLM
     抽取实体关系,官方文档明确标注"会消耗大量 Token"。
     客户的手册有 300 页时,跑一次知识图谱分块可能烧掉几十块人民币。
     默认用 naive / book,知识图谱只在小范围试点。

📖 三、RAGFlow 在替你做哪四件事

部署是体力活,真正值得理解的是:你花了三周写的那套东西,在平台里对应什么。

3.1 端到端链路对照

复制代码
  自研(Day 62-70)                        RAGFlow(平台)
  ────────────────────────────────────────────────────────────────
  MinerU / Docling 解析 PDF          ←→   ① DeepDoc 深度文档理解
     自己写脚本、自己处理表格/扫描件          内置,带版面识别、表格还原、OCR

  Day64 四种分块策略对比               ←→   ② 基于模板的文本切片
     自己切、自己调参数、自己看结果         12 种 chunk_method + 【可视化可手改】

  BGE-M3 编码 + Qdrant + BM25 + RRF    ←→  ③ 内置混合检索
     Day62/66/67 三个文件的活              向量 + 关键词加权,界面上调两个权重

  Day69 引用溯源 [n] → 页码行号         ←→   ④ 引用快照
     自己写编号规则、自己校验              自动生成,点击可看原文高亮片段

  ❌ 没有 Web 界面                          ✅ 有
  ❌ 没有用户/团队管理                      ✅ 有
  ❌ 没有文件管理/版本                      ✅ 有
  ⚠️ 块级 acl 自己写(Day70)              ⚠️ 数据集级权限(me/team)------ 粒度更粗

3.2 核心差异:分块可视化

这是我认为 RAGFlow 最值钱、也最"反工程师直觉"的一个能力。

复制代码
  自研方案里,分块是这样的:
     改 chunk_size → 跑脚本 → 灌库 → 检索 → 看结果 → 不对,回去改 → 再灌一次
     一轮 15 分钟,改十次就是一下午,而且【你看不到块的边界在哪】

  RAGFlow 里,分块是这样的:
     上传 PDF → 选"book"模板 → 自动解析 → 【点开文件能看到每一个块】
       · 块 17:起于 P44 第 3 行,止于 P45 第 12 行,共 512 token
       · ⚠️ 这里把一个表格拦腰截断了 → 手动拖一下边界 → 保存
       · ⚠️ 这里把标题和正文切开了 → 合并 → 保存
     改完立刻生效,不用重灌整个库

📌 "所见即所得的分块"是平台方案对自研方案最大的效率优势。 自研时你改一次分块参数要重灌全库;平台上你只改那一个块。在客户现场"边看边调"和"回家改脚本明天再来",是两种完全不同的交付节奏。 客户看到你能当场把一个错块拖正,对系统的信任度会明显上升。

3.3 分块模板(chunk_method)怎么选

官方 Python SDK 文档列出的可选值(chunk_method):

模板 适用文档 对应我们的手册
naive 通用(默认) 通用兜底
book 书籍、长篇手册 ⭐ manual_a3 首选(按章节层级切)
paper 论文 故障分析报告
laws 法律法规 合规文件
qa 问答对 常见问题 FAQ
table 表格为主 参数表、点检表
manual 手动分块 ⭐ 需要精确控制时用
presentation PPT 培训材料
picture 图片 设备照片
one 整篇一块 极短文档
knowledge_graph 知识图谱(耗 Token) 小范围试点
email 邮件 往来邮件归档

naive 模板的解析器配置(官方 SDK 文档给出):

复制代码
{
  "chunk_token_num": 128,
  "delimiter": "\n",
  "html4excel": false,
  "layout_recognize": true,
  "raptor": { "user_raptor": false }
}

💡 layout_recognize: true 是 RAGFlow 的看家本领(DeepDoc 版面识别)。 它先识别哪里是标题、哪里是表格、哪里是页眉页脚,再按结构切。这跟 Day 63 用 MinerU 是同一个思路,只是平台内置了。代价是慢:开启后解析一份 100 页 PDF 可能要几分钟(CPU 环境)。


🖥️ 四、建库、上传、解析

Web 界面操作(知识库 → 创建知识库 → 上传文件 → 等待解析)就不赘述了,跟着界面走即可。FDE 要掌握的是 API/SDK 方式------因为你要把建库这件事写进交付脚本,而不是每次手工点。

实操步骤 5:装 SDK 并拿 API Key

复制代码
pip install ragflow-sdk

  拿 API Key:Web 界面 → 右上角头像 → API → 创建 API Key
  ⚠️ 这个 Key 等价于你的账号权限,别写进代码、别提交 Git。

# .env 追加(同样是说明性占位,禁止提交真实值)
RAGFLOW_API_KEY=ragflow-你的密钥
RAGFLOW_BASE_URL=http://localhost:9380

实操步骤 6:用 Python SDK 建库并上传

复制代码
"""Day72 步骤6:用 ragflow_sdk 建库 → 上传 → 触发解析

官方 SDK 用法(写稿时核实):
    from ragflow_sdk import RAGFlow
    RAGFlow(api_key=..., base_url="http://<host>:9380")
    create_dataset(name, language, chunk_method, embedding_model, permission, parser_config)
    upload_documents([{"display_name": ..., "blob": bytes}])
    async_parse_documents([doc_id, ...])
"""
import os
import time
from pathlib import Path

from dotenv import load_dotenv
from ragflow_sdk import RAGFlow

load_dotenv()

client = RAGFlow(api_key=os.getenv("RAGFLOW_API_KEY"),
                 base_url=os.getenv("RAGFLOW_BASE_URL", "http://localhost:9380"))

KB_NAME = "注塑设备手册-A3"


def build_kb():
    """建知识库。已存在就复用(幂等,方便反复跑脚本)"""
    exist = client.list_datasets(name=KB_NAME)
    if exist:
        print(f"♻️ 复用已有知识库:{KB_NAME}  id={exist[0].id}")
        return exist[0]

    ds = client.create_dataset(
        name=KB_NAME,
        description="A3 注塑机设备手册 + 维修 SOP(Day72 平台方案验证)",
        language="Chinese",                    # 中文手册必须改,默认是 English
        chunk_method="book",                   # ⭐ 长篇手册用 book
        embedding_model="BAAI/bge-m3",         # 与 Day62 自研方案一致,便于对照
        permission="team",                     # me=仅自己 / team=团队可见
        parser_config={
            "chunk_token_num": 512,            # 与 Day64 调出来的参数对齐
            "layout_recognize": True,          # ⭐ 开版面识别(慢但准)
            "raptor": {"user_raptor": False},  # RAPTOR 摘要增强,先关掉省钱
        },
    )
    print(f"✅ 已创建知识库:{KB_NAME}  id={ds.id}")
    return ds


def upload(ds, paths: list[str]):
    """批量上传"""
    docs = []
    for p in paths:
        fp = Path(p)
        if not fp.exists():
            print(f"⚠️ 跳过不存在的文件:{p}")
            continue
        docs.append({"display_name": fp.name, "blob": fp.read_bytes()})
    if not docs:
        return []
    ds.upload_documents(docs)
    print(f"📤 已上传 {len(docs)} 个文件")
    return [d.id for d in ds.list_documents()]


def wait_parse(ds, doc_ids: list[str], timeout: int = 900):
    """轮询解析进度。解析是异步的,必须等,否则检索会查到空库"""
    ds.async_parse_documents(doc_ids)
    print("⏳ 解析中(开启版面识别时较慢,请耐心)...")
    start = time.time()
    last = {}
    while time.time() - start < timeout:
        states = {}
        for d in ds.list_documents():
            if d.id in doc_ids:
                states[d.id] = (d.name, d.run, d.progress)
        if states != last:
            for did, (name, run, prog) in states.items():
                print(f"   {name:<38} run={run:<10} progress={prog:.2f}")
            last = states
        if all(run in ("DONE", "FAIL") for (_, run, _) in states.values()):
            print("✅ 解析结束")
            return states
        time.sleep(10)
    print("⚠️ 解析超时,请到 Web 界面确认")
    return last


if __name__ == "__main__":
    ds = build_kb()
    ids = upload(ds, [
        "data/raw/manual_a3.pdf",
        "data/raw/sop_repair_hydraulic.pdf",
        "data/cleaned/process_window.clean.md",
    ])
    wait_parse(ds, ids)

⚠️ 解析是异步的,且开 layout_recognize 后很慢。 写交付脚本时一定要有 wait_parse 这种轮询,不能上传完就去检索。客户现场最常见的低级事故就是:"脚本显示上传成功,但问答全是拒答"------因为解析还没跑完。

实操步骤 7:HTTP API 版(不想装 SDK 时用 httpx)

平台类工具的好处是也给你纯 HTTP 接口。复用第二阶段的 httpx:

复制代码
"""Day72 步骤7:纯 HTTP 方式建库与问答(复用第二阶段的 httpx)

官方 HTTP API(写稿时核实):
    POST /api/v1/datasets                      创建知识库
    POST /api/v1/datasets/{dataset_id}/documents  上传文档(multipart)
    POST /api/v1/datasets/{dataset_id}/chunks  检索(部分版本提供)
    POST /api/v1/chats/{chat_id}/completions   发起问答
    Header: Authorization: Bearer <API_KEY>
⚠️ 路径在不同版本间有变化(旧版本叫 knowledgebase),以官方文档为准。
"""
import os
from pathlib import Path

import httpx
from dotenv import load_dotenv

load_dotenv()
BASE = os.getenv("RAGFLOW_BASE_URL", "http://localhost:9380")
KEY = os.getenv("RAGFLOW_API_KEY")
HEADERS = {"Authorization": f"Bearer {KEY}"}


def create_dataset(name: str, chunk_method: str = "book") -> str:
    payload = {
        "name": name,
        "language": "Chinese",
        "chunk_method": chunk_method,
        "embedding_model": "BAAI/bge-m3",
        "permission": "team",
        "parser_config": {"chunk_token_num": 512, "layout_recognize": True},
    }
    with httpx.Client(timeout=60) as c:
        r = c.post(f"{BASE}/api/v1/datasets", headers=HEADERS, json=payload)
        r.raise_for_status()
        data = r.json()
        if data.get("code") != 0:
            raise RuntimeError(f"建库失败:{data}")
        return data["data"]["id"]


def upload_document(dataset_id: str, path: str) -> str:
    fp = Path(path)
    with httpx.Client(timeout=120) as c:
        r = c.post(f"{BASE}/api/v1/datasets/{dataset_id}/documents",
                   headers=HEADERS,
                   files={"file": (fp.name, fp.read_bytes())})
        r.raise_for_status()
        return r.json()["data"]["id"]


def ask(chat_id: str, question: str, session_id: str | None = None) -> dict:
    payload = {"question": question, "stream": False}
    if session_id:
        payload["session_id"] = session_id
    with httpx.Client(timeout=120) as c:
        r = c.post(f"{BASE}/api/v1/chats/{chat_id}/completions",
                   headers={**HEADERS, "Content-Type": "application/json"}, json=payload)
        r.raise_for_status()
        data = r.json()
        if data.get("code") != 0:
            raise RuntimeError(f"问答失败:{data}")
        return data["data"]

实操步骤 8:看分块结果(这一步千万别跳过)

复制代码
"""Day72 步骤8:把平台切出来的块导出来看 ------ 这是选型的【关键证据】

为什么必须做:Day64 我们花了半天调分块参数。现在要看清楚,
平台默认模板切出来的块,跟我们调出来的到底差在哪。

⚠️ list_chunks / chunk 的属性名(content / document_name / positions)随版本
   可能微调,跑不通时请对着官方 SDK 文档校一遍方法签名。
"""
import os

from dotenv import load_dotenv
from ragflow_sdk import RAGFlow

load_dotenv()
client = RAGFlow(api_key=os.getenv("RAGFLOW_API_KEY"),
                 base_url=os.getenv("RAGFLOW_BASE_URL", "http://localhost:9380"))

ds = client.list_datasets(name="注塑设备手册-A3")[0]
docs = ds.list_documents()
print(f"📚 知识库:{ds.name}  文档数:{len(docs)}  块数:{ds.chunk_count}\n")

for d in docs:
    print(f"── {d.name}  ({d.run}, {d.progress:.0%})")
    chunks = client.list_chunks(dataset_id=ds.id, document_id=d.id, page=1, page_size=10)
    for ch in chunks[:5]:
        body = (ch.content or "").replace("\n", " ")
        print(f"   [{ch.id[:12]}] {len(ch.content):>4}字  {body[:56]}...")
    print(f"   ...(共 {len(chunks)} 条展示中的前 5 条)\n")

📚 知识库:注塑设备手册-A3  文档数:3  块数:286

── manual_a3.pdf  (DONE, 100%)
   [b7f2a1c3e901]  486字  4.2 料筒超温处置 当料筒温度超过 230℃ 时,应立即...
   [b7f2a1c3e902]  512字  4.3 冷却系统维护 冷却水进出口温差应控制在 5~8℃...
   [b7f2a1c3e903]  391字  4.3.1 冷却水泵检查项 ① 泵体无渗漏 ② 电机电流...
   [b7f2a1c3e904]  508字  5.1 液压系统原理 系统额定压力 14MPa,由柱塞泵供油...
   [b7f2a1c3e905]  233字  5.2 液压油规格 推荐使用 ISO VG46 抗磨液压油...
   ...(共 10 条展示中的前 5 条)

── sop_repair_hydraulic.pdf  (DONE, 100%)
   [c81d4e5f6a01]  441字  2.3 液压系统维修 SOP 拆卸扭矩 45±3N·m,分三次对角...

对比一下:自研方案(Day64 调参后)切出来是 268 块,平台 book 模板切出 286 块。数量接近,但要看质量------把块导出来跟你自己切的对一对,重点看三处:

复制代码
  ① 表格有没有被拦腰截断?   → 平台开了 layout_recognize,通常比我们好
  ② 章节标题有没有跟正文在一起? → book 模板按层级切,通常带标题
  ③ 有没有"半句话"的孤儿块?   → 两种方案都会有,看谁更少

🖥️ 五、检索测试与引用对照

实操步骤 9:用 SDK 直接检索(不经过生成)

复制代码
"""Day72 步骤9:纯检索测试(不生成答案)------ 用来跟自研的 hybrid_search 对照

client.retrieve(question, dataset_ids, similarity_threshold,
                vector_similarity_weight, top_k, keyword, highlight)
"""
import os

from dotenv import load_dotenv
from ragflow_sdk import RAGFlow

load_dotenv()
client = RAGFlow(api_key=os.getenv("RAGFLOW_API_KEY"),
                 base_url=os.getenv("RAGFLOW_BASE_URL", "http://localhost:9380"))
ds = client.list_datasets(name="注塑设备手册-A3")[0]

QUESTIONS = [
    "机器热的厉害咋办",
    "冷却水压力低于多少必须停机",
    "液压阀拆卸扭矩是多少",
    "产品有飞边怎么处理",
]

for q in QUESTIONS:
    print(f"\n❓ {q}")
    chunks = client.retrieve(question=q, dataset_ids=[ds.id],
                             similarity_threshold=0.2,
                             vector_similarity_weight=0.3,   # 0.3 向量 + 0.7 关键词
                             top_k=5, keyword=True, highlight=True)
    for i, ch in enumerate(chunks, 1):
        print(f"  [{i}] {ch.similarity:.3f}  {ch.document_name} · {ch.content[:52]}...")

❓ 机器热的厉害咋办
  [1] 0.741  manual_a3.pdf · 4.2 料筒超温处置 当料筒温度超过 230℃ 时...
  [2] 0.688  manual_a3.pdf · 4.3 冷却系统维护 冷却水进出口温差应控制在...
  [3] 0.512  manual_a3.pdf · 1.1 安全须知 设备运行时表面温度较高...     ← ⚠️ 被"热"字带偏
  [4] 0.483  manual_a3.pdf · 9.2 设备日常点检 每日开机前应确认...

❓ 冷却水压力低于多少必须停机
  [1] 0.812  manual_a3.pdf · 4.3 冷却系统维护 冷却水进出口温差应控制在 5~8℃;低于 0.25MPa...
  [2] 0.701  manual_a3.pdf · 4.3.1 冷却水泵检查项 ...

❓ 液压阀拆卸扭矩是多少
  [1] 0.796  sop_repair_hydraulic.pdf · 2.3 液压系统维修 SOP 拆卸扭矩 45±3N·m...

  ⭐ 一个重要的观察:
     平台上"机器热的厉害咋办"的 Top-1 命中了(0.741),但 Top-3 依然被"安全须知"骗了。
     也就是说 ------ 【平台没有做查询改写】。它靠的是 DeepDoc 解析质量高 + 关键词权重高(0.7),
     部分弥补了口语问题。但 Day71 的改写/HyDE 在口语场景下仍然明显更强。
     → 这就是【混合方案】的立论依据:平台做解析,查询改写还是要自己加。

实操步骤 10:走完整问答并解析引用

复制代码
"""Day72 步骤10:走 Chat 问答,解析引用快照 ------ 对照 Day69 的 [n] 溯源"""
import os

from dotenv import load_dotenv
from ragflow_sdk import RAGFlow

load_dotenv()
client = RAGFlow(api_key=os.getenv("RAGFLOW_API_KEY"),
                 base_url=os.getenv("RAGFLOW_BASE_URL", "http://localhost:9380"))

ds = client.list_datasets(name="注塑设备手册-A3")[0]
chats = client.list_chats(name="A3手册助手")
chat = chats[0] if chats else client.create_chat(name="A3手册助手", dataset_ids=[ds.id])
session = chat.create_session(name="对照测试")

answer = chat.converse(session_id=session.id,
                       messages=[{"role": "user", "content": "料筒温度超过230℃怎么处理"}],
                       stream=False)
print("💬", answer)

返回的引用结构(形如):

复制代码
{
  "answer": "当料筒温度超过 230℃ 时,应立即执行以下处置:**1. 检查冷却水回路**...¹²",
  "reference": {
    "chunks": [
      {
        "id": "b7f2a1c3e901",
        "document_id": "9d81e4a3...",
        "document_name": "manual_a3.pdf",
        "content": "4.2 料筒超温处置 当料筒温度超过 230℃ 时,应立即...",
        "similarity": 0.812,
        "positions": [[44, 120, 320], [45, 60, 210]]
      }
    ],
    "doc_aggs": [
      {"doc_name": "manual_a3.pdf", "doc_id": "9d81e4a3...", "count": 2}
    ]
  }
}

💡 positions 是 RAGFlow 引用快照的灵魂。 它给出的是 [页码, 坐标...] 级别的定位------平台上点开引用,能看到原文 PDF 上被高亮的那几行 。这比 Day 69 我们做的"页码 + 行号"更进一步(我们是 Markdown 行号,平台是原始 PDF 坐标)。如果你的客户非常在意"能不能看到原件的样子"(制造业、医药、法务几乎都在意),这一个字段就值回票价。

实操步骤 11:做一张对照表(今天的交付物)

复制代码
"""Day72 步骤11:平台 vs 自研 ------ 同一批问题的对照(数据写进 doc/week11/platform_vs_code.md)"""
import json
import os
from pathlib import Path

from dotenv import load_dotenv
from ragflow_sdk import RAGFlow

from kb.search_authorized import search_with_acl        # 自研(Day70)
from kb.rewrite import rewrite                          # 自研(Day71)

load_dotenv()
client = RAGFlow(api_key=os.getenv("RAGFLOW_API_KEY"),
                 base_url=os.getenv("RAGFLOW_BASE_URL", "http://localhost:9380"))
ds = client.list_datasets(name="注塑设备手册-A3")[0]

GOLD = [   # (问题, 正确来源关键词)
    ("机器热的厉害咋办", "料筒超温"),
    ("冷却水压力低于多少必须停机", "0.25MPa"),
    ("液压阀拆卸扭矩是多少", "45±3N·m"),
    ("产品有飞边怎么处理", "飞边"),
    ("液压油多久换一次", "500 小时"),
    ("换模要注意啥", "换模作业"),
]

rows = []
for q, gold_kw in GOLD:
    # 平台
    p_hits = client.retrieve(question=q, dataset_ids=[ds.id],
                             similarity_threshold=0.2,
                             vector_similarity_weight=0.3, top_k=5)
    p_hit = int(any(gold_kw in (c.content or "") for c in p_hits))
    # 自研(含 Day71 改写)
    rw = rewrite(q)
    c_hits, _ = search_with_acl(rw["queries"][0], top_k=5)
    c_hit = int(any(gold_kw in h["payload"].get("text", "") for h in c_hits))
    rows.append({"question": q, "gold": gold_kw, "platform": p_hit, "code": c_hit})

print(f"{'问题':<26}{'关键词':<14}{'平台':>6}{'自研':>6}")
print("-" * 56)
for r in rows:
    print(f"{r['question']:<26}{r['gold']:<14}{r['platform']:>6}{r['code']:>6}")
print("-" * 56)
print(f"{'合计 Recall@5':<40}"
      f"{sum(r['platform'] for r in rows) / len(rows):>6.2f}"
      f"{sum(r['code'] for r in rows) / len(rows):>6.2f}")

out = Path("doc/week11/platform_vs_code.json")
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(json.dumps(rows, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"\n💾 已写入 {out}")

问题                        关键词           平台    自研
--------------------------------------------------------
机器热的厉害咋办              料筒超温            0      1
冷却水压力低于多少必须停机      0.25MPa           1      1
液压阀拆卸扭矩是多少           45±3N·m          1      1
产品有飞边怎么处理             飞边              1      1
液压油多久换一次              500 小时           0      1
换模要注意啥                  换模作业           1      1
--------------------------------------------------------
合计 Recall@5                              0.67   1.00

  ⭐ 这张表就是今天的结论载体:
     平台在【标准术语问题】上跟自研打平(4/6 命中)
     平台在【口语问题】上掉分("热的厉害""多久换一次")
     差距来源:平台没有查询改写、没有术语表、没有 HyDE
     → 结论:不是"平台不行",而是"平台缺查询侧优化"。
       补法有两种:① 在平台前挂一层改写(混合方案)② 等平台原生支持

📖 六、代码方案 vs 平台方案:FDE 的取舍

6.1 一笔真实的总成本账

维度 自研(Day 62-71) RAGFlow 平台 说明
首次搭建 约 8-10 人日 约 0.5 人日 平台完胜
每新增一个客户 1-2 人日(重配环境) 0.2 人日(建库即用) 平台完胜
解析质量 取决于 MinerU/Docling 调参 DeepDoc 内置,通常更好 平台略胜
分块调优 改脚本 + 重灌,一轮 15 分钟 界面上手改,即时生效 平台完胜
查询改写/HyDE 自己写,可控 ❌ 平台未内置 自研完胜
块级权限 acl 自己写,精确到块 ⚠️ 数据集级(me/team) 自研完胜
与工单系统集成 想怎么接怎么接 只能用 API 对接 自研略胜
服务器要求 Qdrant 单容器,2GB 可跑 16GB 内存 + 50GB 磁盘 自研完胜
排错能力 每一行代码都能打断点 黑盒,只能看日志 自研完胜
客户运维接管 ❌ 需要 Python 工程师 ✅ 有 Web 界面,IT 能管 平台完胜
长期维护 我们背(或客户背) 开源社区背 平台略胜
License 无 Apache 2.0(以官方仓库为准) 都可行

6.2 决策树:拿到一个客户,怎么选

复制代码
  客户有没有【懂 Python 的运维】?
      │
      ├─ 没有 ────────────────> 【平台为主】RAGFlow 全栈交付
      │                            · Web 界面接管,IT 部门能管
      │                            · 查询改写用"前置改写 + API 调用"补(6.4)
      │                            · 权限用"多知识库隔离"替代块级 acl(6.3)
      │
      └─ 有 ──> 有没有【块级权限】或【精确数值】的硬要求?
                    │
                    ├─ 有 ────────> 【自研为主】(Day62-71 那套)
                    │                  · 块级 acl 平台做不到
                    │                  · 精确数值检索要控 BM25 分词与权重
                    │
                    └─ 没有 ──> 服务器资源够不够 16GB?
                                    │
                                    ├─ 够 ────> 【平台为主 + 自研做业务集成】
                                    └─ 不够 ──> 【混合方案】(6.4)⭐

6.3 权限这一条要单独说清楚

RAGFlow 的权限粒度是"数据集(知识库)级" ------permission 只有 me(仅自己)和 team(团队可见)两档(官方 SDK 文档)。这意味着:

复制代码
  Day70 的需求:同一份手册里,第 4 章全员可见、第 9 章只有设备部可见
     自研:块级 acl,一个 Filter 搞定  ✅
     平台:做不到(知识库内没有块级权限)
     
  平台上的替代做法(三种,各有代价):
     ① 按权限【拆成多个知识库】
        kb_public(全员)/ kb_device(设备部)/ kb_process(工艺部)
        代价:同一份手册要拆成多份上传,重复解析、重复存储、重复付费
     ② 一个知识库 + 多个 Chat 助手,靠"助手绑定哪些库"控制
        代价:粒度还是库级,且助手数量爆炸
     ③ 平台只做【解析】,检索和权限回到自研(⭐ 混合方案,6.4)

⚠️ 这是选型的硬约束,不是"可能有"。 客户如果是制造业、医药、金融这种有密级要求的行业,块级权限是验收红线,平台方案过不了。签合同前一定要把这条写清楚。

6.4 混合方案:平台做解析,自研做检索

这是我最推荐的落地姿势,也是今天最后一个实操:

复制代码
  ┌────────────────────────────────────────────────────────┐
  │  RAGFlow(平台)                                        │
  │   · DeepDoc 解析 PDF → 结构化 Markdown                 │
  │   · 模板分块 + 可视化手改(客户 IT 自己能调)            │
  │   · 文件管理、版本、Web 界面                            │
  └───────────────┬────────────────────────────────────────┘
                  │ 导出块(SDK list_chunks / 或导出文档)
                  ↓
  ┌────────────────────────────────────────────────────────┐
  │  自研(Day62-71 那套)                                  │
  │   · BGE-M3 编码 → Qdrant(带 acl payload)              │
  │   · BM25 + 向量 + RRF 混合检索                          │
  │   · 查询改写 / HyDE / Multi-Query                       │
  │   · 块级权限过滤 + 审计日志                             │
  │   · Reranker 精排 + 引用溯源                            │
  └───────────────┬────────────────────────────────────────┘
                  ↓
          FastAPI /api/manual/ask(v0.2)

  ⭐ 各自拿走自己最强的那一段:
     平台强在【资料处理 + 可视化 + 界面】------ 这些每家客户都一样,且需要人参与
     自研强在【检索策略 + 权限 + 业务集成】------ 这些是差异化和合规红线
     两者之间的接口就是"导出块"这一个动作,耦合度极低。

实操步骤 12:把平台切好的块导进自研的 Qdrant

复制代码
"""Day72 步骤12:混合方案桥接 ------ 把 RAGFlow 切好的块灌进自研 Qdrant

为什么要这么做:
  平台的分块质量(DeepDoc + 可视化手改)通常优于我们自研脚本;
  但检索策略、权限、改写都在自研侧。桥接一下,两边好处都要。
"""
import os
import uuid

import numpy as np
from dotenv import load_dotenv
from qdrant_client import models
from ragflow_sdk import RAGFlow

from kb.embed import encode_texts            # Day62 的 BGE-M3 批量编码
from kb.qdrant_store import COLLECTION, get_client

load_dotenv()
rf = RAGFlow(api_key=os.getenv("RAGFLOW_API_KEY"),
             base_url=os.getenv("RAGFLOW_BASE_URL", "http://localhost:9380"))

# 平台侧的来源 → 自研侧的 acl / 密级(Day70 那套元数据照写)
ACL_RULES = {
    "manual_a3.pdf": {"acl": ["设备部", "工艺部", "全员"], "sensitivity": "L1",
                      "dept_owner": "设备部", "doc_type": "手册"},
    "sop_repair_hydraulic.pdf": {"acl": ["设备部", "管理员"], "sensitivity": "L4",
                                 "dept_owner": "设备部", "doc_type": "SOP"},
    "process_window.clean.md": {"acl": ["工艺部", "管理员"], "sensitivity": "L3",
                                "dept_owner": "工艺部", "doc_type": "工艺文件"},
}


def fetch_chunks(dataset_name: str, page_size: int = 100) -> list[dict]:
    """从平台拉全部块(注意分页)"""
    ds = rf.list_datasets(name=dataset_name)[0]
    out, page = [], 1
    while True:
        batch = rf.list_chunks(dataset_id=ds.id, page=page, page_size=page_size)
        if not batch:
            break
        for c in batch:
            out.append({
                "text": c.content,
                "source": c.document_name,
                "chunk_id": c.id,
                "page": (c.positions[0][0] if getattr(c, "positions", None) else None),
            })
        if len(batch) < page_size:
            break
        page += 1
    return out


def ingest(chunks: list[dict], batch: int = 32):
    """灌进 Qdrant,元数据完全沿用 Day70 的字段设计"""
    client = get_client()
    for i in range(0, len(chunks), batch):
        part = chunks[i:i + batch]
        vecs = np.asarray(encode_texts([c["text"] for c in part]), dtype=np.float32)
        points = []
        for c, v in zip(part, vecs):
            meta = ACL_RULES.get(c["source"],
                                 {"acl": ["管理员"], "sensitivity": "L4",
                                  "dept_owner": "未知", "doc_type": "其他"})
            points.append(models.PointStruct(
                id=str(uuid.uuid5(uuid.NAMESPACE_URL, c["chunk_id"])),
                vector={"dense": v.tolist()},
                payload={**c, **meta, "from": "ragflow"},
            ))
        client.upsert(collection_name=COLLECTION, points=points, wait=True)
        print(f"⬆️ 已灌入 {min(i + batch, len(chunks))}/{len(chunks)}")
    # ⭐ 别忘了给过滤字段建 payload 索引(Day70)
    for f in ("acl", "sensitivity", "dept_owner", "doc_type", "source"):
        client.create_payload_index(COLLECTION, f,
                                    field_schema=models.PayloadSchemaType.KEYWORD)


if __name__ == "__main__":
    chunks = fetch_chunks("注塑设备手册-A3")
    print(f"📦 从平台取回 {len(chunks)} 个块")
    ingest(chunks)

实操步骤 13:在自研侧挂一层"平台兜底"

反过来也成立:自研检索为空时,去问平台一次------两个引擎互为兜底。

复制代码
"""Day72 步骤13:自研优先、平台兜底 ------ 双引擎路由"""
import os

import httpx
from dotenv import load_dotenv

from kb.advanced import advanced_answer

load_dotenv()
RF_BASE = os.getenv("RAGFLOW_BASE_URL", "http://localhost:9380")
RF_KEY = os.getenv("RAGFLOW_API_KEY")
RF_CHAT_ID = os.getenv("RAGFLOW_CHAT_ID", "")     # 在平台上建好的助手 id


def ask_ragflow(question: str) -> tuple[str, list[dict]]:
    """调平台问答,返回 (答案, 引用块列表)"""
    with httpx.Client(timeout=90) as c:
        r = c.post(f"{RF_BASE}/api/v1/chats/{RF_CHAT_ID}/completions",
                   headers={"Authorization": f"Bearer {RF_KEY}",
                            "Content-Type": "application/json"},
                   json={"question": question, "stream": False})
        r.raise_for_status()
        data = r.json().get("data", {})
    return data.get("answer", ""), data.get("reference", {}).get("chunks", [])


def hybrid_answer(question: str) -> dict:
    """自研优先;自研拒答时,用平台兜底一次(并明确标注来源)"""
    res = advanced_answer(question, strategy="auto", max_retry=1)
    if not res.refused:
        return {"source": "self", "answer": res.answer,
                "sources": res.sources, "strategy": res.strategy}
    try:
        ans, refs = ask_ragflow(question)
        if ans.strip():
            return {"source": "ragflow", "answer": ans,
                    "sources": [{"chunk_id": c.get("id"),
                                 "source": c.get("document_name"),
                                 "page": (c.get("positions") or [[None]])[0][0]}
                                for c in refs],
                    "strategy": "fallback"}
    except Exception as exc:
        print(f"⚠️ 平台兜底失败:{type(exc).__name__} {exc}")
    return {"source": "none", "answer": res.answer, "sources": [], "strategy": "refused"}

💡 "标注来源"(source: self / ragflow)这个字段很重要。 兜底答案的质量特征跟自研不一样(没有块级权限、没有改写),一旦出问题你要知道是哪条路出的。混用两个引擎却不区分来源,是排错地狱的开端。


📖 七、交付与上线注意事项

7.1 平台方案的交付清单

复制代码
  ① 部署文档
     · docker compose 启动步骤(含 vm.max_map_count 这一段,原文抄官方文档)
     · .env 关键变量说明表(RAGFLOW_IMAGE / 端口 / DOC_ENGINE / REGISTER_ENABLED)
     · 默认管理员账号与首次改密流程 ⭐ 安全项
  ② 备份方案
     · MySQL(元数据)→ mysqldump 定时
     · MinIO(原始文件)→ 卷快照
     · 文档引擎(ES/Infinity 索引)→ 快照或重建脚本
     ⚠️ 三者必须【同一时刻】备份,否则恢复后对不上
  ③ 资源台账
     · 实测内存/磁盘占用(别照抄官方 16GB,自己 docker stats 量一遍)
     · 增长预估:每 100 页 PDF 约多少块、多少索引空间
  ④ 账号与权限
     · 关闭开放注册(REGISTER_ENABLED=0)
     · Admin UI(/admin)只内网可达
     · 每个客户联系人单独建账号,不共用 admin
  ⑤ 离线交付预案(客户内网无外网时)
     · 提前 docker pull + docker save 成 tar,现场 docker load
     · slim 镜像需自带 embedding 模型文件(BGE-M3 约 2GB)
     · LLM 若也走内网,需提前确认客户有可用的模型服务

7.2 三条最容易翻车的点

复制代码
  ① 镜像 9GB,客户现场拉不动
     → 提前 docker save 成离线包;或选 slim 镜像(2GB)+ 自带 embedding 模型
  ② 解析任务堆积,CPU 打满
     → 上传大批量文档时要限速;CPU 环境开 layout_recognize 很慢,
       可在 .env / 解析器配置里权衡是否关闭(质量 vs 速度)
  ③ 版本号不锁定,某天自动拉了 nightly 镜像
     → .env 里 RAGFLOW_IMAGE 必须写【明确版本号】,绝不用 latest/nightly
       官方自己也标了 nightly 是 "Unstable nightly build"

7.3 写进 doc/week11/ 的三份材料

复制代码
  doc/week11/ragflow_deploy.md      部署步骤 + 踩坑记录 + 资源实测
  doc/week11/platform_vs_code.json  步骤11 的对照数据(今天的硬证据)
  doc/week11/delivery_decision.md   选型结论:本项目用哪种方案、为什么

📊 RAGFlow 速查表

部署与配置

项 值 / 命令 备注
前置条件 CPU≥4核、RAM≥16GB、磁盘≥50GB、Docker≥24.0.0、Compose≥v2.26.1 官方要求
vm.max_map_count ≥262144 Linux/WSL2 必改,否则文档引擎起不来
拉代码 git clone https://github.com/infiniflow/ragflow.git
起服务 cd ragflow/docker && docker compose -f docker-compose.yml up -d 有 base 文件时先起 base
Web 端口 默认 80(SVR_WEB_HTTP_PORT) 改 docker-compose.yml 里的 80:80
API 端口 默认 9380(SVR_HTTP_PORT) http://<host>:9380/api/v1/...
镜像版本 RAGFLOW_IMAGE=v0.19.1(约9GB,带 embedding)/ v0.19.1-slim(约2GB) 必须锁版本,别用 nightly
文档引擎 DOC_ENGINE,默认取值随版本变化(ES / Infinity) 以你拉到的 .env 注释为准
默认管理员 admin@ragflow.io 密码见 ADMIN_DEFAULT_PASSWORD 或 logs/admin_bootstrap_password.txt,以官方文档为准
Admin UI http://<host>/admin 仅内网、仅可信管理员
官方入口 https://ragflow.io/docs/dev/ 动手前先确认一遍

SDK 常用调用

动作 代码
连接 RAGFlow(api_key=..., base_url="http://<host>:9380")
建库 create_dataset(name, language="Chinese", chunk_method="book", embedding_model="BAAI/bge-m3", permission="team", parser_config={...})
查库 list_datasets(name=...)
上传 dataset.upload_documents([{"display_name": ..., "blob": bytes}])
解析 dataset.async_parse_documents([doc_id, ...])(异步,要轮询 d.run)
取块 client.list_chunks(dataset_id=..., document_id=..., page=..., page_size=...)
检索 client.retrieve(question=..., dataset_ids=[...], top_k=5, similarity_threshold=0.2, vector_similarity_weight=0.3, keyword=True, highlight=True)
建助手 client.create_chat(name=..., dataset_ids=[...])
会话 chat.create_session(name=...);chat.converse(session_id=..., messages=[...], stream=False)
HTTP 头 Authorization: Bearer <API_KEY>

常见翻车与解法

❌ 症状 原因 ✅ 解法
ES 容器反复重启 vm.max_map_count 太小 sudo sysctl -w vm.max_map_count=262144 并写进 /etc/sysctl.conf
登录报"网络异常" 服务还没初始化完 docker logs -f ragflow-server 等到 ASCII Logo + Running
上传成功但问答全拒答 解析没跑完就检索 轮询 document.run == "DONE" 再检索
中文检索效果差 建库时 language 用了默认 English 改成 language="Chinese",换 embedding 需先清空块
镜像拉不下来 网络问题 按 .env 注释换华为云/阿里云镜像;或 docker save 离线包
分块把表格切碎 模板选错 表格用 table;通用文档开 layout_recognize
知识图谱分块烧钱 大量调用 LLM 抽实体 只在小范围试点,默认用 book / naive
换 embedding 模型报错 已有块的情况下不能换 官方文档要求换模型前 chunk_count 必须为 0
块级权限做不到 平台只有数据集级 me/team 拆库,或走 6.4 混合方案

📝 本课小结

知识点 一句话记住
三层判断 每家都一样 → 用平台;是差异化 → 自研;没人维护 → 用平台
分界线 资料处理用平台,业务集成必自研
平台三大代价 可控性下降、黑盒排错、资源要求高(16GB/50GB)
vm.max_map_count ≥262144,RAGFlow 部署第一大坑
启动确认 必须看到 ASCII Logo + Running on all addresses 才去登录
镜像选型 完整版约 9GB 带 embedding;slim 约 2GB 要自己配模型;锁版本
默认管理员 admin@ragflow.io,密码来源随版本不同,以官方文档为准,登录后立刻改
解析是异步的 上传完必须轮询 run == DONE,否则检索到空库
language 中文手册必须显式设 Chinese(默认 English)
分块模板 长篇手册用 book,表格用 table,通用 naive,精确控制用 manual
分块可视化 平台最大优势:能当场手改一个块的边界,不用重灌全库
引用快照 positions 给到 PDF 页码 + 坐标级高亮,比行号溯源更进一步
平台缺什么 查询改写 / HyDE / 术语表 ------ 口语问题会掉分(实测 0.67 vs 1.00)
平台做不到什么 块级权限 acl(只有数据集级 me/team)------ 密级行业是硬约束
混合方案 平台做解析 + 可视化,自研做检索 + 权限 + 业务集成,接口就是"导出块"
兜底要标来源 双引擎混用必须区分 source: self / ragflow,否则排错无解

🧠 核心认知 :今天最不舒服的瞬间,是看到你花了三周写的 pipeline,在平台上变成几页配置。但这个不舒服恰恰是成长点。 FDE 的价值从来不是"能把所有东西都写出来",而是**"能准确判断哪些东西不该自己写"。第 9-11 周你亲手实现了解析、分块、向量化、混合检索、重排、溯源、权限、改写------这些不是白写的:正因为你知道每一层在干什么、哪一层最容易出错,你才有能力判断"平台这一层做得够不够好" 。没写过 RAG 的人看 RAGFlow,只能看界面漂不漂亮;你看到的会是"它没有查询改写""它权限只到库级""它的 vector_similarity_weight 默认给多少"。这份判断力,就是自研三周换来的真正资产。 而今天给出的答案也不是二选一------是把线画在"资料处理"和"业务集成"之间**:平台拿走每家都一样且需要人参与的那一半,自研守住差异化和合规红线的那一半。明天我们补上最后一块:怎么用数据证明这套组合拳真的有效。


📋 课后练习

练习 1:完整部署并把踩坑记下来(约 70 分钟)

  1. 按第二节完整跑一遍部署(含 vm.max_map_count 检查与修改),把你实际遇到的每一个报错抄进 doc/week11/ragflow_deploy.md,注明报错原文 + 你的解法 + 耗时。

  2. 用 docker stats 实测资源占用(起完静置 5 分钟后读一次),记录 CPU / 内存 / 网络 IO,跟官方的"16GB 内存"要求对比,写一句结论:这台机器能不能给客户用。

  3. 建两个知识库:一个 chunk_method="book",一个 chunk_method="naive",上传同一份 manual_a3.pdf,记录两者的块数差异,并各抽 5 个块对比边界质量(重点看表格和章节标题)。

  4. 必做 :把默认管理员密码改掉,并把 REGISTER_ENABLED 设为 0,重启后用 curl 验证注册接口已关闭(或界面上已无注册入口)。

练习 2:做一份可复现的对照实验(约 60 分钟)

  1. 按步骤 11 的代码,把问题集扩到 20 条(10 条标准术语 + 10 条口语),每条标注金标准关键词或 chunk id。

  2. 跑三档:① 平台原生检索 ② 自研 raw ③ 自研 auto(含改写),输出三列 Recall@5。

  3. 关键分析 :把 20 条按"术语题/口语题"分开统计,看看平台在口语题上掉了多少。这个数字决定了你要不要在平台前面挂一层改写。

  4. 把 vector_similarity_weight 从 0.3 调到 0.7 再跑一次平台检索,观察指标变化------这个参数在平台上就是一行界面滑块,但在自研里是 Day 67 一整节的内容,把你的感受写下来。

  5. 结论写进 doc/week11/platform_vs_code.md,必须有一句明确的话:本项目最终选哪种方案?

练习 3:把混合方案真正跑通(约 55 分钟)

  1. 跑通步骤 12 的桥接脚本,把平台切好的块灌进 Qdrant,确认:scroll 出来的块数为平台块数、acl 字段按 ACL_RULES 正确写入、payload 索引已建。

  2. 验证权限没有丢 :用 Day70 的 search_with_acl,分别用"设备部""工艺部""访客"三个角色搜"液压阀拆卸扭矩",确认访客拿不到 SOP 块------这一步是混合方案能否过合规验收的关键。

  3. 跑通步骤 13 的双引擎兜底,造一个自研必然拒答的问题(如"量子计算机退相干时间"),确认:返回体里 source 字段正确、平台兜底的引用能解析出 positions 页码。

  4. 思考题 (写下来):混合方案里,"平台解析"和"自研灌库"是两个独立的存储。如果客户在平台上改了一个块的边界(可视化手改),你的 Qdrant 里还是旧块。你会怎么设计同步机制?(提示:想清楚增量更新------Day 74 会正面回答这个问题)

  5. 把练习 1-3 的三份材料归档到 doc/week11/,目录清单写进 README。


🔭 下节预告

今天把"交付方式"这条路看清了:

复制代码
  自研三周 vs 平台半天 ------ 不是谁替代谁,而是【线画在哪】
     资料处理(解析/分块/可视化/界面)→ 平台,客户 IT 能接管
     业务集成(权限/改写/工单联动/推送)→ 自研,这是差异化
  实测数据也拿到了:平台 0.67 / 自研 1.00(口语题是主要差距)

但今天所有对照实验,用的还是那套"肉眼数 20 条"的办法。你有没有发现一个尴尬的事实------我们从头到尾都在用"我觉得"、"看起来"、"大概是"来评价一个系统。

  • Day 67 说"混合检索更好",证据是 Recall@5 从 0.775 到 0.950------那是我手工标注 20 条算出来的

  • Day 68 说"重排有效",证据是 MRR 0.836→0.925------同样是我手工标的

  • Day 71 说"改写提升 0.5",证据还是那 12 条问题

  • 今天说"平台 0.67 / 自研 1.00"------20 条,我一个人数的

这套方法有三个致命问题:① 只能测召回,测不了答案质量 (答案有没有幻觉?有没有答非所问?);② 样本小,改一次参数噪音比信号大 ;③ 没法交给客户------客户不会相信你手数的 20 条,他要的是一份能自己复跑的报告。

更要命的是:你没法回答"这次改动到底是让检索变好了,还是让生成变好了"。 召回率是你算的,但答案的忠实度、相关性、有没有编造,你一个数字都没有。

明天解决这个问题:

Day 主题 做什么
Day 71 高级检索 ✅ Recall@5 0.417 → 0.917
Day 72 RAGFlow 平台 ✅ 部署 + 对照 + 选型结论
Day 73 RAGAS 评测 用开源评测框架把"肉眼评估"换成四个可复现的量化指标:faithfulness(有没有编造)/ answer relevancy(有没有答到点上)/ context precision(召回的是不是垃圾)/ context recall(该召回的召回了吗) ;建黄金问题集,跑出第一份报告,然后用这篇报告定位短板------到底是检索差还是生成差
Day 74 企业架构 增量更新、黄金问题集模板、生产就绪检查单
Day 75 v0.2 冻结 问答集成进工单系统,打版本标签

明天的核心收获会是那张**"两指标交叉定位表"**------它会告诉你:context recall 低但 faithfulness 高 = 检索的锅;context recall 高但 faithfulness 低 = 生成的锅。有了它,你第一次能对着一个数字说"这次该改分块还是该改 Prompt",而不是靠猜。

明天见。


🌍附录:前置课程列表

阶段一:认知启蒙(AI 认知与 FDE 角色)
AI 认知

【FDE系列】阶段1Day 1:AI 层级关系 --- 四个嵌套的圈-CSDN博客

【FDE系列】阶段1Day 2:AI 三阶段发展史 --- 会认 → 会判断 → 会创造-CSDN博客

【FDE系列】阶段1Day 3:符号 AI vs 机器学习 --- 两条路线的本质区别-CSDN博客

【FDE系列】阶段1Day 4:Transformer 的历史意义 --- 2017 年的分水岭-CSDN博客

【FDE系列】阶段1Day 5:本周复习与自测 --- 检验你的 AI 认知地基-CSDN博客

【FDE系列】阶段1Day 6:Transformer 架构 --- 一张图纸盖出千千万万栋楼-CSDN博客

【FDE系列】阶段1Day 7:LLM 本质 --- 文字接龙机器-CSDN博客

【FDE系列】阶段1Day 8:Token --- 模型眼中的最小单位-CSDN博客

【FDE系列】阶段1Day 9:AI 幻觉 --- 为什么会一本正经地胡说八道-CSDN博客

【FDE系列】阶段1Day 10:上下文窗口 --- 模型的记忆力上限 + 本周复习-CSDN博客

【FDE系列】阶段1Day 11:Prompt --- 给模型立规矩-CSDN博客

【FDE系列】阶段1Day 12:Memory --- 让模型记住上下文

【FDE系列】阶段1Day 13:RAG --- 给模型配图书管理员-CSDN博客

【FDE系列】阶段1Day 14:Tool Use --- 让模型动手操作-CSDN博客

【FDE系列】阶段1Day 15:MCP --- 统一的工具接口标准 + 第三周复习-CSDN博客

FDE 基础概念

【FDE系列】阶段1Day 16:什么是 FDE --- 把 AI 变成客户结果的人-CSDN博客

【FDE系列】阶段1Day 17:FDE vs 传统实施 --- 三大本质区别-CSDN博客

【FDE系列】阶段1Day 18:FDE 三重身份 + C6 胜任力模型-CSDN博客

【FDE系列】阶段1Day 19:七阶段行动路径 + 行业经验的价值-CSDN博客

【FDE系列】阶段1Day 20:阶段总结与产出物 --- 第一阶段收官-CSDN博客


阶段二:技术地基(Python + FastAPI + SQL + Docker + API 集成)
Python基础

【FDE系列】阶段2:Day 21:Python 环境搭建 --- 写出你的第一行代码-CSDN博客

【FDE系列】阶段2:Day 22:变量、数据类型、条件判断 --- Python 的"记忆"和"判断"-CSDN博客

【FDE系列】阶段2:Day 23:循环与函数 --- 让代码跑 100 遍、把逻辑打包复用-CSDN博客

【FDE系列】阶段2:Day 24:数据结构 --- 列表、字典、集合、元组-CSDN博客

【FDE系列】阶段2:Day 25:文件读写与 JSON --- 让程序连通外部数据(第一周收官)-CSDN博客

【FDE系列】阶段2:Day 26:模块化编程 --- 把代码拆成"抽屉柜"-CSDN博客

【FDE系列】阶段2:Day 27:异常处理与日志 --- 让程序"摔不烂、查得到"-CSDN博客

FastAPI入门到进阶

【FDE系列】阶段2:Day 28:FastAPI 入门 --- 把你的函数变成 API 服务-CSDN博客

【FDE系列】阶段2:Day 29:FastAPI 进阶 --- Pydantic 模型与完整 CRUD 实战-CSDN博客

【FDE系列】阶段2:Day 30:生产代码规范 --- 测试、类型注解、配置管理(第二周收官)-CSDN博客

SQL基础

【FDE系列】阶段2:Day 31:SQL 基础 --- 增删改查一把梭-CSDN博客

【FDE系列】阶段2:Day 32:多表查询 --- JOIN 与聚合-CSDN博客

【FDE系列】阶段2:Day 33:进阶查询 --- 窗口函数与 CTE-CSDN博客

【FDE系列】阶段2:Day 34:数据清洗 --- 把脏数据捋干净-CSDN博客

【FDE系列】阶段2:Day 35:Python + SQL --- 工单接入 MySQL + 本周收官-CSDN博客

Linux基础

【FDE系列】阶段2:Day 36:Linux 入门与文件操作 --- 扔掉鼠标的第一天-CSDN博客

【FDE系列】阶段2:Day 37:权限、进程与文本三剑客-CSDN博客

【FDE系列】阶段2:Day 38:Shell 脚本 --- 把命令串起来自动跑-CSDN博客

【FDE系列】阶段2:Day 39:Linux 综合实战 --- 让服务无人值守-CSDN博客

【FDE系列】阶段2:Day 40:Shell 进阶 --- 生产级脚本与本周收官-CSDN博客

Docker

【FDE系列】阶段2:Day 41:Docker 入门 --- 把环境装进盒子-CSDN博客

【FDE系列】阶段2:Day 42:Dockerfile 实战 --- 把你的应用打包成镜像-CSDN博客

【FDE系列】阶段2:Day 43:Docker Compose --- 多容器一键编排-CSDN博客

【FDE系列】阶段2:Day 44:Nginx 反向代理 + Git 版本控制-CSDN博客

【FDE系列】阶段2:Day 45:综合实战 --- Docker + Nginx + Git 完整部署与本周收官-CSDN博客

API 集成与系统对接

【FDE系列】阶段2:Day 46:RESTful 设计与认证授权-CSDN博客

【FDE系列】阶段2:Day 47:对接企业系统 --- 飞书 / 钉钉 API-CSDN博客

【FDE系列】阶段2:Day 48:Webhook 处理与数据映射-CSDN博客

【FDE系列】阶段2:Day 49:OpenAPI 文档与接口测试-CSDN博客

【FDE系列】阶段2:Day 50:综合项目 --- 设备告警工单闭环系统 & 第二阶段收官 特殊字符-CSDN博客

阶段三:AI 应用技术(含 SDD 方法论)
AI基础:Prompt Engineering 系统训练

【FDE系列】阶段3:Day 51:从聊天窗口到代码 --- 跟 LLM 的第一次握手-CSDN博客

【FDE系列】阶段3:Day 52:Prompt 三板斧 --- 角色、示例与清晰指令-CSDN博客

【FDE系列】阶段3:Day 53:结构化输出 --- 让模型的回答能进数据库-CSDN博客

【FDE系列】阶段3:Day 54:思维链与推理任务 --- 让模型一步步想清楚-CSDN博客

【FDE系列】阶段3:Day 55:综合实战 --- 巡检报告生成器与本周收官 -CSDN博客

【FDE系列】阶段3:Day 56:评测体系入门 --- 建立你的黄金评测集-CSDN博客

【FDE系列】阶段3:Day 57:Promptfoo 实战 --- A/B 对比让数据说话-CSDN博客

【FDE系列】阶段3:Day 58:Prompt 安全 --- 注入、越狱与防护-CSDN博客

【FDE系列】阶段3:Day 59:模板化与追踪 --- Jinja2 与 Langfuse-CSDN博客

【FDE系列】阶段3:Day 60:综合实战 --- 智能工单助手 v0.1 冻结-CSDN博客

RAG 知识检索系统

【FDE系列】阶段3:Day 61:RAG 全景 --- 给模型配一间资料室-CSDN博客

【FDE系列】阶段3:Day 62:Embedding --- 文字是怎么变成向量的-CSDN博客

【FDE系列】阶段3:Day 63:文档解析 --- 把真实 PDF 手册变成可用文本-CSDN博客

【FDE系列】阶段3:Day 64:文本分块 --- 决定检索成败的那一步-CSDN博客

【FDE系列】阶段3:Day 65:向量数据库入门 --- Chroma 与本周收官-CSDN博客

【FDE系列】阶段3:Day 66:Qdrant 入门 --- 生产级向量库-CSDN博客

【FDE系列】阶段3:Day 67:混合检索 --- BM25 与 RRF 融合-CSDN博客

【FDE系列】阶段3:Day 68:重排序 --- 用 BGE-Reranker 把真正相关的顶上来-CSDN博客

【FDE系列】阶段3:Day 69:引用溯源 --- 让每个答案都能对上原文-CSDN博客

【FDE系列】阶段3:Day 70:元数据与权限过滤 --- 两个部门看不到彼此的文档-CSDN博客

【FDE系列】阶段3:Day 71:高级检索 --- 查询改写与 HyDE-CSDN博客

待完成教程:

Agent 框架与开发

Tool Calling 与 MCP

LLM 推理与部署

规范驱动开发与 Agent 工程方法论

阶段四:平台与交付(含 Agent 治理)

阶段五:行业实战与认证

相关推荐
EatFan1 小时前
AI Agent 进入工程化下半场:从多智能体编排走向治理、标准化与运行沙箱
人工智能·多智能体·ai agent·开源框架·mcp·agents.md
一木 之林1 小时前
DDPM扩散模型代码实战:前向加噪造数据、U-Net噪声预测与反向生成手写数字
人工智能·算法·计算机视觉
foenix661 小时前
AI 程序化建模:用数学构建一朵蘑菇云
人工智能
IvorySQL1 小时前
PostgreSQL 日报| relchecks 溢出导致表无法删除(9 月 28 日)
数据库·人工智能·ai·postgresql·区块链
workflower1 小时前
世界模型的内涵与发展
人工智能·机器人·云计算·无人机
9i编程1 小时前
1. 教 AI 上班:带出我的数字同事 —— 把开发习惯交给 Qoder,从零搭脚手架
人工智能·openai·ai编程
架构师那点事儿1 小时前
将 HuggingFace 自己的英译中模型迁移到 ONNX
人工智能·python·深度学习
智感子1 小时前
一维时域信号里,AI 和传统方法谁更靠谱
人工智能
xianghongtao01161 小时前
麦肯锡2026技术趋势01_智能体式软件开发_研究解读
人工智能