From 440f83f6a7a32187e8563f75a60944b7beec56cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E6=96=87=E5=B3=B0?= Date: Wed, 10 Jun 2026 12:31:43 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- agents/search/search.go | 19 +++++++++++++- agents/search/search_test.go | 10 ++++++++ agents/time/time.go | 2 +- main.go | 49 +++++++++++++++++++++++++++++++++--- main_test.go | 35 ++++++++++++++++++++++++++ 5 files changed, 110 insertions(+), 5 deletions(-) diff --git a/agents/search/search.go b/agents/search/search.go index d93f86c..51a9bc9 100644 --- a/agents/search/search.go +++ b/agents/search/search.go @@ -19,7 +19,8 @@ import ( const ( defaultActivationPrompt = `判断用户问题是否需要联网搜索。 -仅当问题涉及实时信息、新闻、价格、当前版本、近期事件、政策、网页资料核验,或用户明确要求“查一下/搜索/联网/最新”时调用 search。 +当问题涉及实时信息、新闻、价格、当前版本、近期事件、政策、网页资料核验,或用户明确要求“查一下/搜索/联网/最新”时调用 search。 +当用户询问“历史上的今天”、某日期历史事件、需要按当前日期动态确定查询词的常识资料时,也应调用 search;如果联网无结果,主模型会回退到自身知识库回答并说明来源。 普通知识、闲聊、代码推理、已有上下文可回答的问题不要调用。` defaultBaseURL = "https://api.duckduckgo.com/" defaultTimeout = 10 @@ -287,6 +288,22 @@ func BuildErrorContext(query string, err error) string { return fmt.Sprintf("工具路由尝试联网搜索但失败。用户问题:%s\n错误:%v\n请向用户说明联网搜索失败,不要编造搜索结果。", query, err) } +func BuildFallbackContext(config ProfileConfig, query string, routeReason string, err error) string { + var b strings.Builder + fmt.Fprintf(&b, "工具路由尝试联网搜索,但没有可用的搜索结果。当前搜索源: %s(%s)。\n", config.Name, config.Provider) + fmt.Fprintf(&b, "搜索时间: %s\n", time.Now().Format("2006-01-02 15:04:05")) + fmt.Fprintf(&b, "搜索词: %s\n", query) + if strings.TrimSpace(routeReason) != "" { + fmt.Fprintf(&b, "调用原因: %s\n", strings.TrimSpace(routeReason)) + } + if err != nil { + fmt.Fprintf(&b, "搜索结果状态: %v\n", err) + } + fmt.Fprintln(&b, "请改用模型自身知识库回答用户问题,并在回答开头或结尾明确说明:本次联网搜索未获得可用结果,以下内容来自模型训练数据/内置知识,可能不是最新或完整信息。") + fmt.Fprintln(&b, "不要伪造网页链接或声称已由搜索结果证实;涉及时效性、争议性或不确定细节时要提示用户核验。") + return b.String() +} + func defaultConfig() Config { return Config{ Enabled: true, diff --git a/agents/search/search_test.go b/agents/search/search_test.go index 75847d1..52481d9 100644 --- a/agents/search/search_test.go +++ b/agents/search/search_test.go @@ -2,6 +2,7 @@ package search import ( "context" + "errors" "net/http" "net/http/httptest" "os" @@ -66,6 +67,15 @@ func TestBuildResultContext(t *testing.T) { } } +func TestBuildFallbackContext(t *testing.T) { + text := BuildFallbackContext(ProfileConfig{Name: "duckduckgo", Provider: "duckduckgo"}, "历史上的今天都发生了什么?", "需要查询当天历史事件", errors.New("未搜索到相关网页结果")) + for _, want := range []string{"没有可用的搜索结果", "历史上的今天", "需要查询当天历史事件", "模型训练数据/内置知识", "不要伪造网页链接"} { + if !strings.Contains(text, want) { + t.Fatalf("fallback context missing %q:\n%s", want, text) + } + } +} + func TestDuckDuckGoSearchParsesResults(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("q") != "golang" { diff --git a/agents/time/time.go b/agents/time/time.go index 3bc7c5f..ce4c41e 100644 --- a/agents/time/time.go +++ b/agents/time/time.go @@ -6,7 +6,7 @@ import ( "time" ) -const ActivationPrompt = "提供当前日期、时间和常用时间范围。当用户问题包含今天、明天、昨天、本周、本月、本年、最近、日程安排等相对时间表达时,应先调用此工具;如果后续还需要查数据库,可继续调用 sql。" +const ActivationPrompt = "提供当前日期、时间和常用时间范围。当用户问题包含今天、今日、明天、昨天、本周、本月、本年、最近、历史上的今天、日程安排等相对时间表达时,应先调用此工具;如果后续还需要联网搜索或查数据库,可继续调用 search 或 sql。" type Range struct { Start time.Time diff --git a/main.go b/main.go index 3293219..3f4543a 100644 --- a/main.go +++ b/main.go @@ -42,7 +42,8 @@ const ( JSON 格式:{"tools":[{"name":"工具名称","reason":"..."}],"reason":"..."} 工具名称必须来自“可用工具”列表。 可以选择多个工具,工具会按配置顺序依次执行;后面的工具可以使用前面工具写入的上下文。 -如果用户问题包含今天、明天、昨天、本周、本月、本年、最近等相对时间,并且还需要查询数据库,请同时选择 time 和 sql。 +如果用户问题包含今天、今日、明天、昨天、本周、本月、本年、最近等相对时间,且还需要调用 search 或 sql,必须同时选择 time,并让 time 排在这些工具之前。 +例如“历史上的今天都发生了什么”应选择 time 和 search:先获取今天的绝对日期,再搜索当天历史事件;如果联网无结果,主模型会回退到自身知识库回答并说明来源。 例如“本月有什么日程安排”应选择 time 和 sql:先获取本月绝对日期范围,再查询日程表。 如果无需工具,返回 {"tools":[],"reason":"..."}。 只选择确实必要的工具。` @@ -1104,8 +1105,8 @@ func runSearchTool(ctx context.Context, state *searchagent.State, messages []Cha } if len(results) == 0 { err := errors.New("未搜索到相关网页结果") - emit(chatSSEFrame{Type: "trace", Tool: "search", Stage: "results", Status: "error", Message: err.Error()}) - return prependHiddenContext(messages, searchagent.BuildErrorContext(query, err)), nil + emit(chatSSEFrame{Type: "trace", Tool: "search", Stage: "results", Status: "warning", Message: "未搜索到相关网页结果,将使用模型知识库回答"}) + return prependHiddenContext(messages, searchagent.BuildFallbackContext(profile, query, routeReason, err)), nil } emit(chatSSEFrame{Type: "trace", Tool: "search", Stage: "results", Status: "success", Message: fmt.Sprintf("联网搜索完成,找到 %d 条结果", len(results)), Data: map[string]any{"provider": profile.Provider, "count": len(results)}}) return prependHiddenContext(messages, searchagent.BuildResultContext(profile, query, results, routeReason)), nil @@ -1130,6 +1131,7 @@ func enrichMessagesWithRoutedTools(ctx context.Context, chatProfile *OpenAIProfi return messages, err } selected := filterToolSelections(decision, tools, toolRouterState.cfg.Tools) + selected = ensureTimeSelectionForRelativeQuery(selected, tools, toolRouterState.cfg.Tools, latestUserQuery(messages)) if len(selected) == 0 { emit(chatSSEFrame{Type: "trace", Tool: "tool_router", Stage: "route", Status: "success", Message: "工具路由结果:无需调用工具", Data: map[string]any{"reason": decision.Reason}}) return messages, nil @@ -1239,6 +1241,47 @@ func filterToolSelections(decision ToolRoutingDecision, tools map[string]ChatToo selected[item.Name] = item } } + return orderToolSelections(selected, order) +} + +func ensureTimeSelectionForRelativeQuery(selected []ToolSelection, tools map[string]ChatTool, order []ToolRouteConfig, query string) []ToolSelection { + if !containsRelativeTime(query) || hasToolSelection(selected, "time") || (!hasToolSelection(selected, "search") && !hasToolSelection(selected, "sql")) { + return selected + } + if _, ok := tools["time"]; !ok { + return selected + } + withTime := make(map[string]ToolSelection, len(selected)+1) + for _, item := range selected { + withTime[item.Name] = item + } + withTime["time"] = ToolSelection{Name: "time", Reason: "问题包含相对日期,需要先获取当前日期"} + return orderToolSelections(withTime, order) +} + +func containsRelativeTime(query string) bool { + query = strings.TrimSpace(query) + if query == "" { + return false + } + for _, keyword := range []string{"今天", "今日", "明天", "昨天", "本周", "这周", "本月", "这个月", "本年", "今年", "最近", "历史上的今天"} { + if strings.Contains(query, keyword) { + return true + } + } + return false +} + +func hasToolSelection(selected []ToolSelection, name string) bool { + for _, item := range selected { + if item.Name == name { + return true + } + } + return false +} + +func orderToolSelections(selected map[string]ToolSelection, order []ToolRouteConfig) []ToolSelection { result := make([]ToolSelection, 0, len(selected)) for _, item := range order { if selection, ok := selected[item.Name]; ok { diff --git a/main_test.go b/main_test.go index 6f50605..10c3514 100644 --- a/main_test.go +++ b/main_test.go @@ -129,6 +129,41 @@ func TestFilterToolSelections(t *testing.T) { } } +func TestEnsureTimeSelectionForRelativeSearch(t *testing.T) { + tools := map[string]ChatTool{ + "time": fakeChatTool{name: "time", enabled: true}, + "search": fakeChatTool{name: "search", enabled: true}, + } + selected := ensureTimeSelectionForRelativeQuery( + []ToolSelection{{Name: "search", Reason: "查询历史事件"}}, + tools, + []ToolRouteConfig{{Name: "time"}, {Name: "search"}, {Name: "sql"}}, + "历史上的今天都发生了什么?", + ) + if len(selected) != 2 || selected[0].Name != "time" || selected[1].Name != "search" { + t.Fatalf("unexpected selected tools: %#v", selected) + } + if !strings.Contains(selected[0].Reason, "相对日期") { + t.Fatalf("unexpected time reason: %#v", selected[0]) + } +} + +func TestEnsureTimeSelectionSkipsOrdinarySearch(t *testing.T) { + tools := map[string]ChatTool{ + "time": fakeChatTool{name: "time", enabled: true}, + "search": fakeChatTool{name: "search", enabled: true}, + } + selected := ensureTimeSelectionForRelativeQuery( + []ToolSelection{{Name: "search", Reason: "查询资料"}}, + tools, + []ToolRouteConfig{{Name: "time"}, {Name: "search"}}, + "查一下 Go 语言官网", + ) + if len(selected) != 1 || selected[0].Name != "search" { + t.Fatalf("unexpected selected tools: %#v", selected) + } +} + func TestRunTimeToolAddsHiddenDateRanges(t *testing.T) { messages := []ChatMessage{{Role: "user", Content: "本月有什么日程安排"}} withTime, err := runTimeTool(context.Background(), messages, "需要日期范围", func(chatSSEFrame) {})