fix(search): 「上游没给链接」不再被当成「没有结果」(真机 no results 的根因)
真机现象:她自主思考的 4 次 web-search 全部报 `search: no results (provider=zhipu)`,而同一查询直连上游返回 10 条。 根因:`Finalize` 把「link 为空」当成「不是一条结果」整条丢弃。而智谱 `search_std` 的 `search_result[].link` 在真机上**经常整批是空串**(实测同一 payload 连打 6 次:10 条全空 link,title/content 齐全;换查询也见过 6/10 空)。 ⇒ 10 条被整批丢光 ⇒ 工具层如实报「一家都没查到」——**那是谎报**。 为什么面板探针是 ok:探针走 `search.Probe → p.Search`,**不过 Finalize**; 工具走 `Execute → p.Search → Finalize`。同一份上游、同一份解析代码,两条路 给出相反结论 —— 探针对这一类缺陷**结构性失明**,以后不能拿探针通不通推断 工具通不通。 修法:无链接条目**照原样保留**(title/content 对模型仍有用,链接空着就空着, **绝不编一个出来**);判据改为「link/title/content/snippet **全空**的空壳行才 不算结果」;无链接条目不参与按 link 的去重(没有链接就没有判同身份,宁可重复 也不静默吞)。「空结果 ≠ 成功」**未放宽**:真·空 search_result 仍报 ErrNoResults。 验收(主会话复跑 + 亲自变异):go build ./... + vet 绿;io 与 search 两包绿; 我用 -overlay 把判据退回缺陷形态 ⇒ TestWebSearch_ZhipuLinklessResultsAreNotNoResults 变红,报错串与真机逐字相同。子代理另用真 key 走 in-process 工具路径实测: 三条当时失败的查询现在各回 10 条。 ⚠️ 本修复要等下次 build + restart 才真机生效(现在跑的还是老产物)。 ⚠️ 其他四家是否也有空 link 形态未验(无 key);修复是通用的。
This commit is contained in:
1 parent
bc1dee3f56
commit
2c56bc9704
5 files changed
+296
-16
No files matched your search
@@ -3,25 +3,30 @@ package search
|
||||
import "testing"
|
||||
|
||||
// TestFinalize_Pipeline 是「归一化之后**统一做一次**」的判据(design/27 §4):
|
||||
// 空 link 丢弃 → 域名过滤 → 去重 → 按分数稳定排序 → 截断。
|
||||
// 丢弃**全空壳行** → 域名过滤 → 去重 → 按分数稳定排序 → 截断。
|
||||
//
|
||||
// 🔴 「没有链接」**不再**是被丢的理由(2026-10-07 真机缺陷的修复):智谱真机经常整批
|
||||
// 不给 link 而 title/content 齐全,旧口径会把上游给的 10 条整批丢光、工具层谎报
|
||||
// 「一家都没查到」。判据改成「四个可读字段全空才不算结果」。
|
||||
func TestFinalize_Pipeline(t *testing.T) {
|
||||
t.Parallel()
|
||||
in := []Result{
|
||||
{Link: "https://a.example/1#frag", Title: "A1", Score: 0.2},
|
||||
{Link: "https://a.example/1", Title: "A1 副本", Score: 0.9}, // 去重被丢(同一地址)
|
||||
{Link: "https://b.example/2", Title: "B2", Score: 0.9},
|
||||
{Link: " ", Title: "没有链接"},
|
||||
{Link: " ", Title: "没有链接"}, // **保留**:上游给了标题,只是没给链接
|
||||
{Title: ""}, // 全空壳行:丢弃
|
||||
{Link: "https://blocked.example/3", Title: "被排除的域名"},
|
||||
{Link: "https://c.example/4/", Title: "C4", Score: 1.5},
|
||||
}
|
||||
got := Finalize(in, Options{BlockDomains: []string{"blocked.example"}})
|
||||
if len(got) != 3 {
|
||||
t.Fatalf("条数 = %d, want 3: %+v", len(got), got)
|
||||
if len(got) != 4 {
|
||||
t.Fatalf("条数 = %d, want 4: %+v", len(got), got)
|
||||
}
|
||||
// 去重保留**先出现的那一条**(A1,分 0.2);Score 降序;同分保持原序(稳定排序)。
|
||||
if got[0].Title != "C4" || got[1].Title != "B2" || got[2].Title != "A1" {
|
||||
t.Fatalf("排序/去重结果 = %q,%q,%q; want C4,B2,A1(分数降序 + 同分稳定 + 重复地址保留首条)",
|
||||
got[0].Title, got[1].Title, got[2].Title)
|
||||
if got[0].Title != "C4" || got[1].Title != "B2" || got[2].Title != "A1" || got[3].Title != "没有链接" {
|
||||
t.Fatalf("排序/去重结果 = %q,%q,%q,%q; want C4,B2,A1,没有链接(分数降序 + 同分稳定 + 重复地址保留首条)",
|
||||
got[0].Title, got[1].Title, got[2].Title, got[3].Title)
|
||||
}
|
||||
if got[2].Link != "https://a.example/1#frag" {
|
||||
t.Fatalf("去重应保留先出现的那一条: %+v", got[2])
|
||||
@@ -29,6 +34,31 @@ func TestFinalize_Pipeline(t *testing.T) {
|
||||
if got[1].Link != "https://b.example/2" {
|
||||
t.Fatalf("B2 的链接被改动: %q", got[1].Link)
|
||||
}
|
||||
if got[3].Link != "" {
|
||||
t.Fatalf("没有链接的条目不许被编出一个链接: %q", got[3].Link)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFinalize_LinklessResultsSurvive 是**真机缺陷的单元级判据**:一整批「有标题正文、
|
||||
// 没链接」的结果(智谱真机形态)必须原样留下——它们**不是**空结果。
|
||||
//
|
||||
// 同时钉住「不按 link 去重无链接条目」:没有链接就没有判同的身份,宁可重复展示,
|
||||
// 也不静默吞掉上游给的结果(吞掉会让上层把「查到了 10 条」谎报成「一家都没查到」)。
|
||||
func TestFinalize_LinklessResultsSurvive(t *testing.T) {
|
||||
t.Parallel()
|
||||
in := make([]Result, 0, 10)
|
||||
for i := 0; i < 10; i++ {
|
||||
in = append(in, Result{Title: "热搜条目", Content: "正文摘要", Link: ""})
|
||||
}
|
||||
got := Finalize(in, Options{Limit: 5})
|
||||
if len(got) != 5 {
|
||||
t.Fatalf("条数 = %d, want 5(Limit 截断,而不是被空 link 判据丢光): %+v", len(got), got)
|
||||
}
|
||||
for i, r := range got {
|
||||
if r.Link != "" || r.Title == "" || r.Content == "" {
|
||||
t.Fatalf("got[%d] = %+v, want 原样保留(链接空、标题正文在)", i, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestFinalize_LimitAndAllowDomains 覆盖截断与域名白名单(域名过滤只在归一化后做一次)。
|
||||
|
||||
Reference in new issue
Block a user