如果你是用 Make 或其他低代碼工具打 Qdrant,最容易把人逼瘋的,通常不是向量檢索本身,而是那種看起來完全不合理的 JSON 400。
你會看到這些錯:
Format error in JSON body: key must be a stringexpected value at line 1 column 1expected value at line 2 column 32
最氣的是,你把 body 貼去 JSON validator,居然還過。
然後你就開始懷疑人生,懷疑 Qdrant,懷疑 HTTP module,最後懷疑自己是不是該先離開電腦去買杯咖啡。
這篇想講的核心很簡單:
Qdrant 的 JSON 400,很多時候不是 Qdrant 壞掉,而是 server 實際收到的 body,根本不是你以為你送出去的那個東西。
這件事一旦想通,排查路線就會突然很乾淨。你不再把錯誤想成「Qdrant 有沒有接受我的 request」,而是先回頭問:
我真的送出了一個它期望的 JSON object 嗎?還是我只是送出了一個看起來很像 JSON 的字串?fileciteturn14file1
先講核心主張
如果要把這篇收成一句話,我會這樣講:
JSON 400 不是 Qdrant 的問題類型,而是客戶端真相的問題類型。
這裡所謂的客戶端真相,指的是 server 真正收到的 bytes。
不是你在 Make 裡看到的模板,不是你貼到 validator 的內容,也不是你腦中想像的 request body。
Qdrant 其實很誠實。它收到什麼,就照什麼解析。你覺得自己送的是 object,但它如果實際收到的是 string,它就會把你打回來。這沒什麼玄學。fileciteturn14file1turn851418search2
先把兩種最常用的 endpoint 釘死
很多 JSON 400 看起來像 parse error,實際上是 body shape 根本跟 endpoint 不對齊。
Qdrant points 相關常見的幾條路徑,大概是:
POST /collections/{collection}/points
用來 upsert pointsPOST /collections/{collection}/points/query
單筆 queryPOST /collections/{collection}/points/query/batch
批次 queryciteturn851418search2turn851418search4
這裡有一個很容易踩的坑:
單筆 endpoint 跟 batch endpoint 期待的 body 長得不一樣。
單筆 query
Qdrant 要的是類似這種 object:
{
"query": [0.1, 0.2, 0.3],
"limit": 3,
"with_payload": true
}
batch query
Qdrant 要的是:
{
"searches": [
{
"query": [0.1, 0.2, 0.3],
"limit": 2
},
{
"query": [0.1, 0.2, 0.3],
"limit": 1
}
]
}
如果你把 batch body 打到單筆 endpoint,錯誤訊息未必會很貼心地告訴你「你用錯 endpoint」,它可能只回你一個很討厭的 400。fileciteturn14file1turn851418search2
三種最常見的「看起來像 JSON,但其實不是」
這類問題我現在幾乎都先從三個方向查。
1. 你以為送的是 JSON object,其實送的是 JSON string
這是最常見的。
你在工具裡看到的內容可能長這樣:
{
"query": [0.1, 0.2],
"limit": 3
}
但真正送出去的其實是:
"{"query":[0.1,0.2],"limit":3}"
這時候 validator 還是會說合法,因為它確實是一個合法的 JSON string。
問題是 Qdrant 要的是 object,不是外面又多包一層引號的字串。fileciteturn14file1
這種情況最常見的根源就是 double-stringify。
你先把 object stringify 一次,再讓 HTTP module 又把整個東西當字串送出去。看起來像 JSON,其實只是一顆 JSON 風味糖果。
2. 你以為送的是 array,實際上 array 被展平成裸數字串
你本來想送的是:
{
"query": [-0.02, 0.03, 0.04]
}
但有些低代碼工具在 raw 模板裡插值時,會把 array 展成:
{
"query": -0.02, 0.03, 0.04
}
這就已經不是合法 JSON 了。
server 當然會在某個 column 爆掉。這類 line 2 column 32,很多時候就是在跟你說:**你的 vector 根本沒有被包成 JSON array。**fileciteturn14file1
3. endpoint 跟 body shape 根本不匹配
這個也很常見。
你送的是:
{
"searches": [...]
}
但 URL 寫的是 /points/query。
或者你明明打 /points/query/batch,卻只送了一個單筆的 query object。
這種錯很討厭,因為它看起來像 JSON parse error,實際上是 request shape 與 endpoint contract 不相符。fileciteturn14file1turn851418search2
最穩的排查順序:先把最小可用跑通
我現在很少一開始就上 batch query、filter、payload、rerank 全家餐。
JSON 問題最有效的排查法,其實很樸素:
Step 1:先跑單筆 query,不加 filter
{
"query": [/* embedding */],
"limit": 1,
"with_payload": true
}
Step 2:確認 vector 真的是 array,不是字串
這一步超無聊,但很重要。
如果 HTTP 模組有 request preview 或 raw log,一定要看那個,不要只看你自己組的模板。
Step 3:再加 filter
等單筆 query 穩了,再把 filter.must 加回來。
Step 4:最後才升級到 batch
這時候才把 URL 換成 /points/query/batch,body 也改成 searches: [...]
這個順序的好處是,你永遠知道自己是在哪一層把事情弄壞的。fileciteturn14file1
為什麼 JSON validator 會過,但 server 還是報錯
這件事真的很容易把人搞混。
因為 validator 只能檢查你貼進去的內容是不是一份合法 JSON。它不會幫你檢查:
- 你是不是把 object 包成 string
- 你的 endpoint 跟 body shape 是否對齊
- 你的 array 在實際插值時有沒有被展平
- 你的 HTTP client 最後是不是根本送了別的東西
也就是說,validator 驗的是你手上的文字,不是server 端實際收到的 request body。
這兩者常常不是同一件事。fileciteturn14file1
在 Make 或低代碼工具裡,我比較信的做法
如果你是在 Make 這類環境裡工作,我會比較建議這幾條:
1. 優先用 object builder,不要狂拼 raw text
如果平台有「Create JSON」或類似模組,通常比你在 raw body 裡徒手插值更穩。
2. embedding 要當資料,不要當文字
一旦你把 embedding 先變成字串,再塞回 JSON,後面很容易出現裸數字、CSV 風味、或 escape 一團亂的問題。
3. 避免 double-stringify
這件事值得講三次。
如果整個 body 是 object,就讓 HTTP module 直接送 object。不要自己先 stringify,再交給它幫你包第二次。fileciteturn14file1
這類錯誤跟 Qdrant 其實沒那麼有關
這也是我後來比較確定的一件事。
很多人會把這種問題歸類成「Qdrant 不好用」。但說穿了,這比較像是:
- 低代碼工具的插值真相
- HTTP client 的 body 序列化真相
- endpoint contract 與 payload shape 的真相
Qdrant 在這裡只是負責很誠實地告訴你:「我收到的不是我要的東西。」
什麼時候這篇的判準不成立
這裡也要補個邊界。
不是所有 400 都是 JSON 組裝問題。
有些 400 確實可能是:
- filter schema 不對
- payload index 沒建
- query 結構用的是舊版 SDK / wrapper 期待的格式
- endpoint 路徑本身打錯
所以這篇的核心判準不是「看到 400 就一定是 double-stringify」,而是:
先確認 server 收到的是不是一份 shape 正確的 request,再去懷疑更高層的 retrieval 設計。
我現在比較相信的 debug 心法
如果你要我把這類問題收成一組我真的會拿來用的判準,大概是這樣:
- 先確認 endpoint
- 再確認 body 最外層 shape
- 再確認 vector 是不是 array
- 再確認是不是被 double-stringify
- 單筆跑通後,才升級到 batch
- filter 跟 payload index 問題,留到 JSON 真的穩之後再查
這樣做不華麗,但很有效。
下一篇接什麼
這篇是在講 client 真相。
下一篇會往另一個更大的主題走:RAG 怎麼從能回答,走到真的能上線。
也就是說,retrieval、citation、evaluation、ACL、versioning、observability 這些原本看起來像附加題的東西,為什麼其實才是 production RAG 的主考卷。