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
@@ -154,26 +154,43 @@ type Options struct {
|
||||
}
|
||||
|
||||
// Finalize 对归一化后的结果做一次统一后处理:
|
||||
// 丢弃空 link → 域名过滤 → 去重(按规范化 link)→ 按 Score 降序**稳定**排序 → 截断到 Limit。
|
||||
// 丢弃**四个可读字段全空**的行 → 域名过滤 → 去重(按规范化 link)→ 按 Score 降序
|
||||
// **稳定**排序 → 截断到 Limit。
|
||||
//
|
||||
// 排序用 SliceStable:分数相同(多数上游不给分)时保持上游给出的原始顺序,不制造随机。
|
||||
//
|
||||
// 🔴 「上游没给链接」**不是**「不是一条结果」(2026-10-07 真机缺陷的修复,回归用例
|
||||
// `plugins/tools/io/web_search_zhipu_linkless_test.go`):
|
||||
//
|
||||
// 智谱 `search_std` 的 `search_result[].link` 在真机上**经常整批是空串**(实测同一
|
||||
// 查询连打六次,10 条的 link 全为空,而 title/content 齐全)。旧口径「空 link ⇒ 丢弃」
|
||||
// 会把上游明明返回的 10 条**整批丢光**,工具层于是报 `ErrNoResults` —— 而面板探针
|
||||
// (走 Probe,不过 Finalize)报的是 `{ok:true, count:1}`:同一份上游、同一份解析代码
|
||||
// 给出两个相反的结论,且工具层那句「一家都没查到」是**谎报**。
|
||||
//
|
||||
// 因此:没有链接的条目**照原样保留**(title/content 对模型仍然有用,链接空着就空着,
|
||||
// 绝不编一个出来);只有 link/title/content/snippet **全空**的空壳行才不算结果。
|
||||
// 无链接的条目**不参与按 link 的去重** —— 没有链接就没有判同的身份,宁可重复展示,
|
||||
// 也不静默吞掉上游给的结果。
|
||||
func Finalize(results []Result, opt Options) []Result {
|
||||
out := make([]Result, 0, len(results))
|
||||
seen := make(map[string]struct{}, len(results))
|
||||
for _, r := range results {
|
||||
r.Link = strings.TrimSpace(r.Link)
|
||||
if r.Link == "" {
|
||||
// 没有链接的结果没法被模型引用,也不算一条检索结果。
|
||||
if emptyResult(r) {
|
||||
// 四个可读字段全空的空壳行:没有任何东西可以给模型看,不算一条结果。
|
||||
continue
|
||||
}
|
||||
if !domainAllowed(r.Link, opt.AllowDomains, opt.BlockDomains) {
|
||||
continue
|
||||
}
|
||||
key := canonicalLink(r.Link)
|
||||
if _, dup := seen[key]; dup {
|
||||
continue
|
||||
if r.Link != "" {
|
||||
key := canonicalLink(r.Link)
|
||||
if _, dup := seen[key]; dup {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
out = append(out, r)
|
||||
}
|
||||
sort.SliceStable(out, func(i, j int) bool { return out[i].Score > out[j].Score })
|
||||
@@ -183,6 +200,18 @@ func Finalize(results []Result, opt Options) []Result {
|
||||
return out
|
||||
}
|
||||
|
||||
// emptyResult 报告一条结果是否**四个可读字段全空**(链接 / 标题 / 正文 / 摘要都没有)。
|
||||
//
|
||||
// 判据只放「全空」这一种:任一项非空都算有内容(含「只有标题、没有链接」—— 智谱真机
|
||||
// 就是这种形态)。刻意**不**把 Published/Score 算进来:只有发布时间的行给不了模型任何
|
||||
// 可读内容,是上游的空壳而不是一条结果。
|
||||
func emptyResult(r Result) bool {
|
||||
return r.Link == "" &&
|
||||
strings.TrimSpace(r.Title) == "" &&
|
||||
strings.TrimSpace(r.Content) == "" &&
|
||||
strings.TrimSpace(r.Snippet) == ""
|
||||
}
|
||||
|
||||
// domainAllowed 按 allow/block 名单判定一条结果的域名是否保留。
|
||||
// 解析不出主机时**保留**(不是「过滤失败」——域名过滤不该把正常结果误杀)。
|
||||
func domainAllowed(link string, allow, block []string) bool {
|
||||
|
||||
Reference in new issue
Block a user