AI 工具

用 RAGFlow 检索 API 调试知识库召回:拆解相似度与错误

RAGFlow 是开源的 RAG(检索增强生成)引擎,擅长解析 PDF、表格等复杂文档。 回答不好时,先要分清是“没检索到”还是“检索到了但模型没用好”。这篇做 rfretrieve.py:只调用检索接口,列出命中的片段、综合相似度、关键词相似度、向量相似度和来源文件,方便调整阈值和权重。

BINBHO · 图文教程

RAGFlow 是开源的 RAG(检索增强生成)引擎,擅长解析 PDF、表格等复杂文档。 回答不好时,先要分清是“没检索到”还是“检索到了但模型没用好”。这篇做 rf_retrieve.py:只调用检索接口,列出命中的片段、综合相似度、关键词相似度、向量相似度和来源文件,方便调整阈值和权重。

先看清成品

  • 对 demo.ragflow.io 真实调用:无效 Key 返回 HTTP 401,body 的 code 也是 401,message 是一段 Python 异常的字符串。
  • 脚本同时检查 HTTP 状态和 body code,报错时附带“后端是 1.0 Go 还是之前的 Python”,避免对着错误版本的文档排查。
  • 6 项 pytest 通过(含 HTTP 200 但 code=102 的业务错误)。

合规提醒:RAGFlow 是 Apache 2.0 开源项目。本文只用官方 HTTP API 和你自己实例的 API Key(在 RAGFlow 网页的 API 设置中创建)。Key 只放服务端。demo.ragflow.io 是官方的公开演示站,我只用无效 Key 对它发了几个请求来确认错误格式,没有注册、上传或压测;不要把它当成自己的生产服务,也不要上传敏感文档。

运行说明:RAGFlow 官方推荐至少 4 核 CPU、16 GB 内存、50 GB 磁盘,并用 Docker 部署。box 没有 Docker 权限,所以没有部署 RAGFlow。真实跑过的是:用无效 Key 调用 demo.ragflow.io(2026-10-21 21:31–21:33,UTC+8,返回原样贴在文中),以及全部本地代码。正常返回来自我写的 mock,字段照官方 HTTP API 参考。版本提醒:2026-10-16 发布了 v1.0.0-rc2,main 分支的 API 文档已经按 1.0(Go 后端)写,有几个旧接口被移除;demo 站的响应没有 1.0 才有的 X-API-Source: go 头。先确认你的版本,再看对应版本的文档。

做完之后你会得到什么?

成果 说明
rf_retrieve.py 检索调试工具
test_rf_retrieve.py 6 项测试

需要准备哪些环境?

项目 说明
RAGFlow 按官方 Docker 部署(4 核 / 16 GB / 50 GB 起步),或使用已有实例
API Key 在 RAGFlow 网页的 API 设置中创建
Python 3.9 以上,pip install requests pytest
环境变量 RAGFLOW_BASE_URL(如 http://localhost)、`RAGFLOW_API_KEY`
知识库 ID 知识库页面的 URL 里,或 GET /api/v1/datasets

关键事实

事实 来源
响应同时有 HTTP 状态码和 body 业务 code,要分开检查;body code 0 才是成功 官方 HTTP API 参考:ERROR CODES
POST /api/v1/retrieval:question、dataset_ids 必填;similarity_threshold 默认 0.2;keywords_similarity_weight 默认 0.7,向量权重 = 1 − 它 官方 Retrieve chunks
vector_similarity_weight 是旧字段,只在没传 keywords_similarity_weight 时使用;两个都传必须加起来等于 1 同上
返回的每个 chunk 有 similarity、term_similarity、vector_similarity、document_keyword;doc_aggs 是按文件的统计 同上
1.0 的 Go 后端响应带 X-API-Source: go 官方 Migrate to 1.0

文件结构

84-ragflow-A-src/
├── rf_retrieve.py
└── test_rf_retrieve.py

第 1 步:脚本

"""调用 RAGFlow 的检索接口 POST /api/v1/retrieval,只看“检索到了什么”,不经过大模型。
用来调试知识库:问题命中了哪些片段、相似度多少、来自哪个文件。

RAGFlow 的响应同时有 HTTP 状态码和 body 里的业务 code(0 才是成功),官方要求两者分开检查。"""
import argparse, json, os, sys
import requests

UA = "binbho-tutorial/1.0"
BODY_CODES = {101: "参数错误", 102: "数据无效或缺失", 108: "权限错误", 109: "认证错误", 401: "未授权(API Key 无效)"}


class RagflowError(Exception):
    pass


def backend_of(resp):
    """RAGFlow 1.0 的 Go 后端会带 X-API-Source: go;没有这个头多半是 1.0 之前的 Python 后端。"""
    return "go(1.0)" if (getattr(resp, "headers", {}) or {}).get("X-API-Source") == "go" else "python(1.0 之前)"


def post(base, key, path, payload, session=None):
    s = session or requests
    r = s.post(base.rstrip("/") + path, json=payload, timeout=60,
               headers={"Authorization": f"Bearer {key.strip()}", "User-Agent": UA})
    try:
        d = r.json()
    except ValueError:
        raise RagflowError(f"HTTP {r.status_code},不是 JSON:{r.text[:120]}")
    if r.status_code != 200 or d.get("code") != 0:
        code = d.get("code")
        raise RagflowError(f"HTTP {r.status_code} code={code}({BODY_CODES.get(code, '未知')}):{d.get('message')}"
                           f"  [后端:{backend_of(r)}]")
    return d["data"]


def retrieve(base, key, question, dataset_ids, threshold=0.2, kw_weight=0.7, page_size=5, session=None):
    return post(base, key, "/api/v1/retrieval", {
        "question": question, "dataset_ids": dataset_ids, "page": 1, "page_size": page_size,
        "similarity_threshold": threshold, "keywords_similarity_weight": kw_weight, "highlight": True}, session)


def report(data):
    lines = [f"命中 {data.get('total', 0)} 条,显示 {len(data.get('chunks', []))} 条"]
    for i, c in enumerate(data.get("chunks", []), 1):
        lines.append(f"[{i}] {c.get('document_keyword')}  综合 {c['similarity']:.3f}"
                     f"(关键词 {c.get('term_similarity', 0):.3f} / 向量 {c.get('vector_similarity', 0):.3f})")
        lines.append("    " + c["content"].replace("\n", " ")[:100])
    for d in data.get("doc_aggs", []):
        lines.append(f"  文件 {d['doc_name']}:{d['count']} 条")
    return "\n".join(lines)


def main(argv=None):
    ap = argparse.ArgumentParser()
    ap.add_argument("question"); ap.add_argument("--dataset", action="append", required=True)
    ap.add_argument("--threshold", type=float, default=0.2); ap.add_argument("--kw-weight", type=float, default=0.7)
    ap.add_argument("--json", action="store_true")
    a = ap.parse_args(argv)
    base, key = os.environ.get("RAGFLOW_BASE_URL", "http://localhost"), os.environ.get("RAGFLOW_API_KEY", "")
    if not key.strip():
        print("缺少 RAGFLOW_API_KEY", file=sys.stderr); return 1
    try:
        data = retrieve(base, key, a.question, a.dataset, a.threshold, a.kw_weight)
    except (RagflowError, requests.RequestException) as e:
        print(f"检索失败:{e}", file=sys.stderr); return 1
    print(json.dumps(data, ensure_ascii=False, indent=2) if a.json else report(data))
    return 0


if __name__ == "__main__":
    sys.exit(main())

第 2 步:测试(mock)

"""mock 是我写的,结构照 RAGFlow 官方 HTTP API 参考里 Retrieve chunks 的示例。"""
import pytest
import rf_retrieve as r


class R:
    def __init__(self, code, body):
        self.status_code, self._b, self.text, self.headers = code, body, str(body), {}
    def json(self):
        if isinstance(self._b, str):
            raise ValueError
        return self._b


class S:
    def __init__(self, resp):
        self.resp, self.sent = resp, None
    def post(self, url, json=None, timeout=None, headers=None):
        self.sent = (url, json, headers); return self.resp


OK = {"code": 0, "data": {"chunks": [{"content": "入职满一年后,每年享有 10 天带薪年假。", "document_keyword": "员工手册.md",
      "id": "c1", "similarity": 0.8123, "term_similarity": 0.9, "vector_similarity": 0.61}],
      "doc_aggs": [{"count": 1, "doc_id": "d1", "doc_name": "员工手册.md"}], "total": 1}}


def test_payload_and_report():
    s = S(R(200, OK))
    data = r.retrieve("http://localhost/", " k\n", "年假几天", ["ds1"], session=s)
    url, body, h = s.sent
    assert url == "http://localhost/api/v1/retrieval" and body["dataset_ids"] == ["ds1"]
    assert body["keywords_similarity_weight"] == 0.7 and "vector_similarity_weight" not in body
    assert h["Authorization"] == "Bearer k"
    out = r.report(data)
    assert "员工手册.md  综合 0.812(关键词 0.900 / 向量 0.610)" in out and "文件 员工手册.md:1 条" in out


def test_http_200_but_business_error():
    with pytest.raises(r.RagflowError, match="code=102"):
        r.retrieve("http://h", "k", "q", [], session=S(R(200, {"code": 102, "message": "`dataset_ids` is required."})))


def test_real_401_shape():   # 2026-10-21 对 demo.ragflow.io 实测
    body = {"code": 401, "data": None, "message": "<Unauthorized '401: Unauthorized'>"}
    with pytest.raises(r.RagflowError, match="API Key 无效"):
        r.retrieve("http://h", "bad", "q", ["x"], session=S(R(401, body)))


def test_non_json():
    with pytest.raises(r.RagflowError, match="不是 JSON"):
        r.retrieve("http://h", "k", "q", ["x"], session=S(R(502, "<html>")))


def test_empty_result():
    assert r.report({"chunks": [], "doc_aggs": [], "total": 0}).startswith("命中 0 条")


def test_backend_detection():
    resp = R(401, {}); assert r.backend_of(resp).startswith("python")
    resp.headers = {"X-API-Source": "go"}; assert r.backend_of(resp).startswith("go")

第 3 步:运行

# 真实请求(2026-10-21 21:31 UTC+8)
$ curl -s -D - https://demo.ragflow.io/api/v1/datasets -H 'Authorization: Bearer ragflow-invalid-demo'
server: openresty/1.31.1.1
content-type: application/json
{"code":401,"data":null,"message":"<Unauthorized '401: Unauthorized'>"}   ← HTTP 401(不带 Key 结果相同;响应头里没有 X-API-Source)

$ RAGFLOW_BASE_URL=https://demo.ragflow.io RAGFLOW_API_KEY=ragflow-invalid-demo python rf_retrieve.py "年假有几天" --dataset 00000000000000000000000000000000
检索失败:HTTP 401 code=401(未授权(API Key 无效)):<Unauthorized '401: Unauthorized'>  [后端:python(1.0 之前)]
$ python -m pytest -q
6 passed

# 有自己实例时的输出格式(来自 mock):
命中 1 条,显示 1 条
[1] 员工手册.md  综合 0.812(关键词 0.900 / 向量 0.610)
    入职满一年后,每年享有 10 天带薪年假。
  文件 员工手册.md:1 条

怎么检查结果?

  • 同一个问题换几种问法(口语、书面、带错别字)各跑一次,看命中是否稳定。
  • 关键词相似度高、向量相似度低:多半是字面重合但意思不对;反过来是同义改写。用 --kw-weight 调整两者比例。
  • --threshold 调高会减少噪音,也可能把正确片段过滤掉。用 C 篇或 83C 那样的固定问题集来定值。

调试示例

先确认版本,再看文档。 我一开始照着 GitHub main 分支的 API 参考写代码,后来注意到两件事:

  1. 最新发布是 v1.0.0-rc2(2026-10-16),main 分支文档写的是 1.0:公开 API 改由 Go 实现,POST /api/v1/chats_openai/{chat_id}/chat/completions 和 POST /api/v1/chats/{chat_id}/completions 这类旧路径在 1.0 中被移除;文档还提醒“部分校验的业务码、错误信息和认证的 HTTP 状态和 Python 版不同”。
  2. demo.ragflow.io 的响应头只有 server: openresty/… 和 content-type,没有 X-API-Source: go,说明它跑的不是 1.0 的 Go 后端。

如果拿 1.0 的文档去调 1.0 之前的实例(或反过来),会遇到 404 或字段不认识的问题,很容易误以为是 Key 或网络的问题。所以我加了 backend_of():出错时在信息末尾标出后端类型。这个判断依据是官方说 Go 响应会带这个头;如果你前面的反向代理删掉了它,判断会不准。

另外,demo 站返回的 message 是 <Unauthorized '401: Unauthorized'>,一看就是后端异常对象转成的字符串。官方也建议按 code 分支,不要匹配 message 文本。

常见问题

现象 原因 处理
code 401 / 109 Key 无效 重新创建 Key
code 102 dataset_ids is required. 没传知识库 ID 加 --dataset
命中 0 条 阈值太高,或文档还没解析完 降低 --threshold;看 B 篇的解析状态
404 接口路径和后端版本不匹配 对照版本看文档

模板规则

  • 先单独测检索,再测问答。
  • HTTP 状态和 body code 都要检查。
  • 确认后端版本再看文档。
  • 阈值和权重用问题集来定。

项目资源

用途 地址
RAGFlow HTTP API 参考(官方) https://ragflow.io/docs/http_api_reference
RAGFlow GitHub https://github.com/infiniflow/ragflow
RAGFlow 版本发布 https://github.com/infiniflow/ragflow/releases
RAGFlow 许可证(Apache 2.0) https://github.com/infiniflow/ragflow/blob/main/LICENSE
RAGFlow HTTP API 参考(GitHub 源文件) https://github.com/infiniflow/ragflow/blob/main/docs/references/http_api_reference.md

检索调对了,问答就成功了一大半。