Files
Agent-System-Backend/internal/tools/search.go
T
weichen ef060d4ee5
ci / vet-and-test (pull_request) Successful in 40s
Keep only hard facts in the blocking gate and add fact-consistency checks
Why: User rulings 4/5 plus the P2-P4 live evidence. Keyword scoring dimensions
(readability, actionability) kept failing valid documents - normal Chinese
section headings flagged as column titles, loops of 4 corrections - because
they grade writing preferences, not verifiable facts. Unverified numbers
(~10-13 per run) remained the largest quality leak through every phase.
And the verifier runs on the same model as the worker (lightweight falls back
to chat), so "independent" verification was self-review. Separately, svc
seeding after boot never registered the tavily tools until a restart because
keys were snapshotted at startup.

Changes: quality.HardFacts (lint actually ran + zero errors) replaces the
full ScoreBytes projection in CheckArtifacts - keyword dimensions remain in
ScoreBytes for eval visibility only. New fact-consistency check
(runtime.factCheck, default on): extract percentages/dates/large numbers from
deliverable text, trace each against the material corpus (attachment text +
tool evidence); misses get exactly one correction round (fix, drop, or mark
as target/estimate), never a loop. runVerifier skips same-source model
verification unless runtime.verifierSameModel is set (hard facts + fact check
still run). Search tools now register lazily, reading tavily keys per call
(settings/seeding take effect immediately).

Evidence: SELF - TestHardFactsIgnoresKeywordDimensions,
TestHardFactsKeepsLintUnavailableBlocking, TestUnverifiedFactsAndNotice,
TestFactCheckContinuationOnce (fact note fires, corrected run passes);
go vet + go test ./... pass; live before/after on the golden task set
recorded on the phase05 task card per the policy clause.

Impact: Gate stops rejecting valid documents on stylistic grounds; invented
figures get one forced correction round; same-model verification no longer
pretends to be independent; tavily config is hot. No contract or storage
changes.
2026-10-10 00:04:57 +00:00

114 lines
4.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package tools —— 网络搜索工具:桥接 Agent-Native 的 TavilyProvider
// (密钥池轮换由其实现)到统一注册表。配置了 tools.tavily.apiKeys 才注册
// (对齐 agent-native 语义:未配置不静默降级为空结果)。
package tools
import (
"context"
"encoding/json"
"fmt"
"strings"
agenttools "agent-native/runtime/tools"
core "agent-system-core"
)
// TavilySettings 装配期读取的搜索配置。
type TavilySettings struct {
Endpoint string
APIKeys []string
}
// RegisterSearchTools 注册 tavily_search(完整参数面)与 web_search(别名)。
// 工具面与 agent-native runtime/tools/search.go 逐字段对齐(名称/参数/输出/失败降级文案)。
func RegisterSearchTools(r *Registry, provider agenttools.SearchProvider) error {
return registerSearch(r, func(ToolExecCtx) agenttools.SearchProvider { return provider })
}
// RegisterLazySearchTools 注册按调用时取密钥的搜索工具:provider 在每次调用时
// 重建(设置页/svc 播种改了 tavily 密钥即刻生效,不再需要重启进程——
// 实测播种后 tavily 工具要重启才注册)。configure 返回 nil 时不静默降级,
// 返回明确的未配置提示。
func RegisterLazySearchTools(r *Registry, configure func() agenttools.SearchProvider) error {
return registerSearch(r, func(ToolExecCtx) agenttools.SearchProvider { return configure() })
}
func registerSearch(r *Registry, providerFor func(ToolExecCtx) agenttools.SearchProvider) error {
tavily := func(rc ToolExecCtx, args json.RawMessage) (string, error) {
provider := providerFor(rc)
if provider == nil {
return "", fmt.Errorf("搜索未配置:settings.tools.tavily.apiKeys(或对应 secret)为空;配置后立即生效,无需重启")
}
return execSearch(provider, args)
}
if err := r.Register(core.ToolDesc{
Name: "tavily_search",
Description: "Search the live internet for real-time information, competitor intelligence, regulatory news, product specs, or market data via Tavily (with automatic API key pool rotation).",
Source: core.SourceBuiltin,
Parameters: json.RawMessage(`{
"type": "object",
"properties": {
"query": {"type": "string", "description": "The search query keywords in Chinese or English"},
"search_depth": {"type": "string", "enum": ["basic", "advanced"], "description": "Search depth: 'basic' (fast summary) or 'advanced' (deep analysis, default 'basic')"},
"max_results": {"type": "number", "description": "Maximum number of search results to retrieve (1 to 10, default 5)"}
},
"required": ["query"]
}`),
}, tavily); err != nil {
return err
}
return r.Register(core.ToolDesc{
Name: "web_search",
Description: "Alias for tavily_search. Searches the live internet for information.",
Source: core.SourceBuiltin,
Parameters: json.RawMessage(`{
"type": "object",
"properties": {"query": {"type": "string", "description": "The search query keywords"}},
"required": ["query"]
}`),
}, tavily)
}
func execSearch(provider agenttools.SearchProvider, args json.RawMessage) (string, error) {
var params struct {
Query string `json:"query"`
SearchDepth string `json:"search_depth"`
MaxResults float64 `json:"max_results"`
}
if err := json.Unmarshal(args, &params); err != nil {
return "", err
}
query := strings.TrimSpace(params.Query)
if query == "" {
return "", fmt.Errorf("Error: query cannot be empty.")
}
resp, err := provider.Search(context.Background(), agenttools.SearchRequest{
Query: query,
Depth: params.SearchDepth,
MaxResults: int(params.MaxResults),
})
if err != nil {
// 失败对模型可见并指引回退(与 agent-native 扩展降级文案一致);
// 注册表把 err.Error() 作为输出回灌,故降级文案承载在 error 内。
return "", fmt.Errorf("[Tavily Search Failed]: %v\n(Agent should rely on workspace materials or local knowledge base as fallback.)", err)
}
return formatSearchResults(resp, query), nil
}
// formatSearchResults 输出 Markdown(对齐 agent-native 的呈现形态:答案 + 编号结果)。
func formatSearchResults(resp agenttools.SearchResponse, query string) string {
var sb strings.Builder
fmt.Fprintf(&sb, "## 🌐 Tavily 实时网络搜索结果: %q\n\n", query)
if strings.TrimSpace(resp.Answer) != "" {
fmt.Fprintf(&sb, "**概要**:%s\n\n", resp.Answer)
}
if len(resp.Results) == 0 {
sb.WriteString("(无命中结果)\n")
return sb.String()
}
for i, r := range resp.Results {
fmt.Fprintf(&sb, "%d. %s\n %s\n\n", i+1, r.Title, strings.TrimSpace(r.Content))
}
return sb.String()
}