feat(search): 网络搜索新增 DeepSeek 官方来源
不单独配置凭据,直接复用当前激活的 LLM 配置(base_url/api_key/model)。 这个来源与 ddgs/brave/tavily 性质不同:DeepSeek 没有可直接调用的搜索接口, 搜索只存在于其 Anthropic 兼容 messages 接口内部,以 web_search_20250305 服务端工具的形式由 DeepSeek 服务端执行。由此带来几个必须让用户知道的差异, 已在设置页写明: - 仅支持 DeepSeek 官方端点 + anthropic 协议。实测其 OpenAI 协议端点会直接以 "unknown variant, expected `function`" 拒绝一切服务端工具,web_search_options 之类的顶层字段则被静默忽略,没有可用的替代写法。 - 每次搜索额外消耗一次模型调用。 - 搜索请求由 DeepSeek 服务端发出,不经过搜索出口代理,也不计入流量留痕。 - 返回结果只有标题与链接,没有摘要(DeepSeek 只给 encrypted_content),需要正文 时仍由 WebFetch 抓取。 上述条件不做程序校验,仅在界面说明,由用户自行确认——配置不满足时搜索会在实际 调用时报错,与其它来源填错 Key 的反馈路径一致,可用「测试搜索」按钮实跑验证。 依赖 norma v0.3.8。
This commit is contained in:
1 parent
abf701e03c
commit
e4c744a318
11 files changed
+133
-47
No files matched your search
+23
-1
@@ -273,6 +273,9 @@ const (
|
||||
settingConcurrencyLimit = "task_concurrency_limit"
|
||||
// defaultWebSearchBackend is used when web search is on but no backend was picked.
|
||||
defaultWebSearchBackend = "ddgs"
|
||||
// deepSeekWebSearchBackend borrows the active LLM profile instead of its own
|
||||
// key, so it only works on an anthropic-format profile pointed at DeepSeek.
|
||||
deepSeekWebSearchBackend = "deepseek"
|
||||
// defaultWorkers is the concurrent work-agent count when the setting is unset.
|
||||
defaultWorkers = 3
|
||||
// defaultConcurrencyLimit is the simultaneous-running-task cap when the feature
|
||||
@@ -539,7 +542,26 @@ func (m *Manager) WebSearchOpts() agent.WebSearchOpts {
|
||||
if on && backend == "tavily" && strings.TrimSpace(tavilyKey) == "" {
|
||||
on = false
|
||||
}
|
||||
return agent.WebSearchOpts{Enabled: on, Backend: backend, BraveKey: braveKey, TavilyKey: tavilyKey, Proxy: proxy}
|
||||
o := agent.WebSearchOpts{Enabled: on, Backend: backend, BraveKey: braveKey, TavilyKey: tavilyKey, Proxy: proxy}
|
||||
if backend == deepSeekWebSearchBackend {
|
||||
o.DeepSeekBaseURL, o.DeepSeekAPIKey, o.DeepSeekModel = m.deepSeekSearchCreds()
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
// deepSeekSearchCreds resolves the credentials the "deepseek" search backend
|
||||
// borrows from the active LLM profile (it has no key of its own). Whether that
|
||||
// profile can actually drive server-side search — DeepSeek exposes it only on
|
||||
// the Anthropic-format endpoint — is deliberately NOT validated here: the UI
|
||||
// states the requirement and the user decides. A profile that can't serve it
|
||||
// simply fails at search time (or at the settings page's 测试 button), which is
|
||||
// the same feedback every other backend gives for a bad key.
|
||||
func (m *Manager) deepSeekSearchCreds() (baseURL, apiKey, model string) {
|
||||
p, err := m.pg.ActiveProfile()
|
||||
if err != nil || p == nil {
|
||||
return "", "", ""
|
||||
}
|
||||
return p.BaseURL, p.APIKey, p.Model
|
||||
}
|
||||
|
||||
// SetWebSearch persists and applies the web-search settings. braveKey, tavilyKey, and
|
||||
|
||||
+14
-2
@@ -3413,10 +3413,22 @@ func (s *Server) testWebSearch(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.TrimSpace(req.TavilyKey) != "" {
|
||||
tavilyKey = req.TavilyKey
|
||||
}
|
||||
cfg := actool.WebSearchConfig{Backend: backend, BraveAPIKey: braveKey, TavilyAPIKey: tavilyKey, Proxy: proxy}
|
||||
// Hard cap so a slow/blocked proxy can't hang the request.
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
||||
wall := 30 * time.Second
|
||||
// deepseek 的凭据不在表单里,来自当前激活的 LLM 配置;同时它每次搜索都跑一次
|
||||
// 模型推理,30s 的通用上限偏紧,单独放宽。这里不预判配置能不能用——测这一下
|
||||
// 本来就是给用户自己确认的手段,真跑不通时下面的报错比预判更有信息量。
|
||||
probeQuery := "test"
|
||||
if strings.TrimSpace(backend) == deepSeekWebSearchBackend {
|
||||
cfg.DeepSeekBaseURL, cfg.DeepSeekAPIKey, cfg.DeepSeekModel = s.m.deepSeekSearchCreds()
|
||||
wall = 120 * time.Second
|
||||
// 搜索词由 DeepSeek 端的模型自行决定,"test" 太空泛会让它跳过搜索直接作答。
|
||||
probeQuery = "DeepSeek company official website"
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(r.Context(), wall)
|
||||
defer cancel()
|
||||
results, err := actool.WebSearchProbe(ctx, actool.WebSearchConfig{Backend: backend, BraveAPIKey: braveKey, TavilyAPIKey: tavilyKey, Proxy: proxy}, "test", 3)
|
||||
results, err := actool.WebSearchProbe(ctx, cfg, probeQuery, 3)
|
||||
if err != nil {
|
||||
writeJSON(w, 200, map[string]any{"ok": false, "error": err.Error(), "backend": backend})
|
||||
return
|
||||
|
||||
Reference in new issue
Block a user