fix(coordinator): force Tavily search for weather/realtime dialogue answers
Anchor weather and other realtime questions on runner /v1/search results (or an explicit refuse constraint) so the model cannot invent dates or temperatures from training memory.
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const piRunnerSearchTimeout = 20 * time.Second
|
||||
|
||||
// PiRunnerSearcher 通过 pi-agent runner 的 /v1/search 做 Tavily 检索。
|
||||
type PiRunnerSearcher struct {
|
||||
URL string
|
||||
Token string
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
// Search 调用 runner /v1/search,返回 markdown 结果。
|
||||
func (s *PiRunnerSearcher) Search(ctx context.Context, query string) (string, error) {
|
||||
if s == nil {
|
||||
return "", fmt.Errorf("pi runner searcher is nil")
|
||||
}
|
||||
base := strings.TrimRight(strings.TrimSpace(s.URL), "/")
|
||||
q := strings.TrimSpace(query)
|
||||
if base == "" {
|
||||
return "", fmt.Errorf("pi runner url empty")
|
||||
}
|
||||
if q == "" {
|
||||
return "", fmt.Errorf("search query empty")
|
||||
}
|
||||
|
||||
body, err := json.Marshal(map[string]any{
|
||||
"query": q,
|
||||
"max_results": 5,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
reqCtx, cancel := context.WithTimeout(ctx, piRunnerSearchTimeout)
|
||||
defer cancel()
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(reqCtx, http.MethodPost, base+"/v1/search", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
ApplyRunnerAuth(httpReq, s.Token)
|
||||
|
||||
client := s.Client
|
||||
if client == nil {
|
||||
client = &http.Client{Timeout: piRunnerSearchTimeout}
|
||||
}
|
||||
resp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
limited, err := io.ReadAll(io.LimitReader(resp.Body, MaxResultBodyBytes+1))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(limited) > MaxResultBodyBytes {
|
||||
return "", fmt.Errorf("search response too large")
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return "", fmt.Errorf("search http %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var parsed struct {
|
||||
OK bool `json:"ok"`
|
||||
Markdown string `json:"markdown"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
if err := json.Unmarshal(limited, &parsed); err != nil {
|
||||
return "", fmt.Errorf("decode search response: %w", err)
|
||||
}
|
||||
if !parsed.OK {
|
||||
msg := strings.TrimSpace(parsed.Error)
|
||||
if msg == "" {
|
||||
msg = "search failed"
|
||||
}
|
||||
return "", fmt.Errorf("%s", msg)
|
||||
}
|
||||
md := strings.TrimSpace(parsed.Markdown)
|
||||
if md == "" {
|
||||
return "", fmt.Errorf("empty search markdown")
|
||||
}
|
||||
return md, nil
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPiRunnerSearcher_Success(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/search" {
|
||||
t.Fatalf("path=%s", r.URL.Path)
|
||||
}
|
||||
if r.Header.Get("Authorization") != "Bearer tok-1" {
|
||||
t.Fatalf("missing auth: %q", r.Header.Get("Authorization"))
|
||||
}
|
||||
var body map[string]any
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if body["query"] != "今天北京天气" {
|
||||
t.Fatalf("query=%v", body["query"])
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"ok": true,
|
||||
"markdown": "## 北京天气\n气温 21~33°C",
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
s := &PiRunnerSearcher{URL: srv.URL, Token: "tok-1", Client: srv.Client()}
|
||||
md, err := s.Search(context.Background(), "今天北京天气")
|
||||
if err != nil {
|
||||
t.Fatalf("Search: %v", err)
|
||||
}
|
||||
if !strings.Contains(md, "21~33") {
|
||||
t.Fatalf("markdown=%q", md)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPiRunnerSearcher_HTTPFailure(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusBadGateway)
|
||||
_, _ = w.Write([]byte(`{"ok":false,"error":"upstream"}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
s := &PiRunnerSearcher{URL: srv.URL, Client: srv.Client()}
|
||||
_, err := s.Search(context.Background(), "今天天气")
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPiRunnerSearcher_EmptyMarkdown(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "markdown": " "})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
s := &PiRunnerSearcher{URL: srv.URL, Client: srv.Client()}
|
||||
_, err := s.Search(context.Background(), "新闻")
|
||||
if err == nil {
|
||||
t.Fatal("expected empty markdown error")
|
||||
}
|
||||
}
|
||||
@@ -116,6 +116,12 @@ func Assemble(cfg *config.Config, svc *bizdev.Service) (*Runtime, error) {
|
||||
}
|
||||
dialogueBackend := agent.NewBackend(backendName, cfg)
|
||||
coordEngine := coordinator.NewCoordinatorEngineWithDialogue(nil, dialogueBackend, cfg.DataDir)
|
||||
if cfg != nil && strings.TrimSpace(cfg.PiAgentRunnerURL) != "" {
|
||||
coordEngine.SetRealtimeSearcher(&agent.PiRunnerSearcher{
|
||||
URL: cfg.PiAgentRunnerURL,
|
||||
Token: cfg.PiAgentRunnerAuthToken,
|
||||
})
|
||||
}
|
||||
parts := bizdev.Snapshot(svc)
|
||||
if ctxmgr.SemanticSummaryShadowEnabled() && parts.Ctx != nil && backendName == "pi-agent" {
|
||||
model := ""
|
||||
|
||||
@@ -222,6 +222,10 @@ type UserProjectContext struct {
|
||||
UpgradeOffered bool `json:"-"`
|
||||
// DocumentSummaryReader 仅用于服务端鉴权后的只读正文访问,不序列化。
|
||||
DocumentSummaryReader DocumentSummaryReader `json:"-"`
|
||||
// RealtimeSnippet 本轮实时检索(天气/新闻等)成功结果,供 Prompt 注入;不序列化。
|
||||
RealtimeSnippet string `json:"-"`
|
||||
// RealtimeUnavailable 本轮命中实时查询但检索失败/空结果,强制拒答约束;不序列化。
|
||||
RealtimeUnavailable bool `json:"-"`
|
||||
}
|
||||
|
||||
// ContextAssembler 上下文组装服务
|
||||
|
||||
@@ -58,6 +58,22 @@ func (e *CoordinatorEngine) generateDialogueReply(
|
||||
return "", ReplySourceTemplate
|
||||
}
|
||||
|
||||
if userCtx != nil {
|
||||
userCtx.RealtimeSnippet = ""
|
||||
userCtx.RealtimeUnavailable = false
|
||||
if e.realtimeSearcher != nil && IsRealtimeInformationQuery(userInput) {
|
||||
if st := stream.FromContext(ctx); st != nil {
|
||||
st.EmitPhase("dialogue_realtime_search", "正在检索实时信息...", "assistant", 0, 0, 40)
|
||||
}
|
||||
md, searchErr := e.realtimeSearcher.Search(ctx, userInput)
|
||||
if searchErr != nil || strings.TrimSpace(md) == "" {
|
||||
userCtx.RealtimeUnavailable = true
|
||||
} else {
|
||||
userCtx.RealtimeSnippet = strings.TrimSpace(md)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prompt := buildDialoguePrompt(userInput, userCtx)
|
||||
instructions := `你是「水木东方」医疗器械「项目助手」——温和、靠谱、会替用户着想的项目助理。
|
||||
定位:以助理身份与用户友好协作。能力覆盖商务研判、工程方案与法规合规,但对外只以「项目助手」自称,不要分饰多角色,也不要自称「小队/参谋/总工/法规专家」。
|
||||
|
||||
@@ -393,3 +393,98 @@ func TestDialogueLLM_AmbiguousSmalltalkDoesNotUseStageSalesTemplate(t *testing.T
|
||||
t.Fatalf("expected casual-chat template fallback, got: %s", res.NaturalReply)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type stubRealtimeSearcher struct {
|
||||
markdown string
|
||||
err error
|
||||
calls int
|
||||
lastQ string
|
||||
}
|
||||
|
||||
func (s *stubRealtimeSearcher) Search(ctx context.Context, query string) (string, error) {
|
||||
s.calls++
|
||||
s.lastQ = query
|
||||
return s.markdown, s.err
|
||||
}
|
||||
|
||||
func TestDialogueLLM_RealtimeSearchInjectsSnippet(t *testing.T) {
|
||||
backend := &scriptedDialogueBackend{reply: "依据检索:北京今日约 25°C。"}
|
||||
tmp := t.TempDir()
|
||||
engine := NewCoordinatorEngineWithDialogue(nil, backend, tmp)
|
||||
searcher := &stubRealtimeSearcher{markdown: "## 北京天气\n气温 21~33°C"}
|
||||
engine.SetRealtimeSearcher(searcher)
|
||||
|
||||
userCtx := &UserProjectContext{
|
||||
Project: ProjectInfo{
|
||||
ID: "p-rt-1",
|
||||
Name: "全自动冲击波治疗仪",
|
||||
CurrentStage: maturity.StageV01_Triage,
|
||||
},
|
||||
BlackboardFacts: map[string]string{},
|
||||
}
|
||||
rec := &intent.IntentRecognitionResult{
|
||||
RawInput: "今天北京天气怎么样,出去见客户要准备什么吗",
|
||||
Branch: intent.BranchNewIntentPreempt,
|
||||
TopIntent: &intent.CandidateIntent{
|
||||
Intent: intent.IntentCasualChat,
|
||||
Confidence: 0.85,
|
||||
Branch: intent.BranchNewIntentPreempt,
|
||||
},
|
||||
}
|
||||
res, err := engine.ProcessIntent(context.Background(), rec, userCtx)
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessIntent: %v", err)
|
||||
}
|
||||
if searcher.calls != 1 {
|
||||
t.Fatalf("expected 1 search call, got %d", searcher.calls)
|
||||
}
|
||||
if !strings.Contains(backend.lastPrompt, "【实时检索结果】") {
|
||||
t.Fatalf("prompt missing realtime section: %s", backend.lastPrompt)
|
||||
}
|
||||
if !strings.Contains(backend.lastPrompt, "21~33") {
|
||||
t.Fatalf("prompt missing snippet: %s", backend.lastPrompt)
|
||||
}
|
||||
if res.ReplySource != ReplySourcePiAgent {
|
||||
t.Fatalf("reply_source=%q", res.ReplySource)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDialogueLLM_RealtimeSearchFailureInjectsRefuseConstraint(t *testing.T) {
|
||||
backend := &scriptedDialogueBackend{reply: "抱歉,暂时无法获取实时天气数据,建议您查看天气 App。"}
|
||||
tmp := t.TempDir()
|
||||
engine := NewCoordinatorEngineWithDialogue(nil, backend, tmp)
|
||||
searcher := &stubRealtimeSearcher{err: context.DeadlineExceeded}
|
||||
engine.SetRealtimeSearcher(searcher)
|
||||
|
||||
userCtx := &UserProjectContext{
|
||||
Project: ProjectInfo{
|
||||
ID: "p-rt-2",
|
||||
Name: "全自动冲击波治疗仪",
|
||||
CurrentStage: maturity.StageV01_Triage,
|
||||
},
|
||||
BlackboardFacts: map[string]string{},
|
||||
}
|
||||
rec := &intent.IntentRecognitionResult{
|
||||
RawInput: "今天北京天气怎么样",
|
||||
Branch: intent.BranchNewIntentPreempt,
|
||||
TopIntent: &intent.CandidateIntent{
|
||||
Intent: intent.IntentCasualChat,
|
||||
Confidence: 0.9,
|
||||
Branch: intent.BranchNewIntentPreempt,
|
||||
},
|
||||
}
|
||||
_, err := engine.ProcessIntent(context.Background(), rec, userCtx)
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessIntent: %v", err)
|
||||
}
|
||||
if searcher.calls != 1 {
|
||||
t.Fatalf("expected 1 search call, got %d", searcher.calls)
|
||||
}
|
||||
if strings.Contains(backend.lastPrompt, "【实时检索结果】") {
|
||||
t.Fatalf("should not inject empty realtime section: %s", backend.lastPrompt)
|
||||
}
|
||||
if !strings.Contains(backend.lastPrompt, "本轮实时检索失败") {
|
||||
t.Fatalf("missing refuse constraint: %s", backend.lastPrompt)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,15 +65,16 @@ type CoordinationResult struct {
|
||||
|
||||
// CoordinatorEngine L2 协调引擎
|
||||
type CoordinatorEngine struct {
|
||||
stageTasks StageTaskRunner
|
||||
instSvc *projectinstance.Service
|
||||
assembler *ContextAssembler
|
||||
dialogueBackend agent.Backend
|
||||
dataDir string
|
||||
ctxMgr *ctxmgr.ContextManager
|
||||
kbClient *kb.Client
|
||||
impact ImpactActions
|
||||
projectAccess ProjectAccess
|
||||
stageTasks StageTaskRunner
|
||||
instSvc *projectinstance.Service
|
||||
assembler *ContextAssembler
|
||||
dialogueBackend agent.Backend
|
||||
realtimeSearcher RealtimeSearcher
|
||||
dataDir string
|
||||
ctxMgr *ctxmgr.ContextManager
|
||||
kbClient *kb.Client
|
||||
impact ImpactActions
|
||||
projectAccess ProjectAccess
|
||||
}
|
||||
|
||||
// StageTaskRunner is the application owner for ExecuteTask / ExecuteStage.
|
||||
@@ -109,6 +110,14 @@ func (e *CoordinatorEngine) SetInstanceService(svc *projectinstance.Service) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// SetRealtimeSearcher 注入对话路径的实时检索(天气/新闻等);nil 时跳过强制检索。
|
||||
func (e *CoordinatorEngine) SetRealtimeSearcher(s RealtimeSearcher) {
|
||||
if e != nil {
|
||||
e.realtimeSearcher = s
|
||||
}
|
||||
}
|
||||
|
||||
// SetImpactActions wires read-only impact analysis and confirmed revision proposals.
|
||||
func (e *CoordinatorEngine) SetImpactActions(a ImpactActions) {
|
||||
if e != nil {
|
||||
|
||||
@@ -230,6 +230,18 @@ func (mc *MemoryComposer) ComposeDialoguePrompt(userInput string, userCtx *UserP
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 6b. 【实时检索结果】 天气/新闻等权威实时数据(强制锚定)
|
||||
// -------------------------------------------------------------
|
||||
if snip := strings.TrimSpace(userCtx.RealtimeSnippet); snip != "" {
|
||||
sb.WriteString("【实时检索结果】(权威实时数据,回答天气/新闻等只准以此为准)\n")
|
||||
sb.WriteString(snip)
|
||||
if !strings.HasSuffix(snip, "\n") {
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 7. 【用户偏好画像】 PersonalizedGuidance (~300 字)
|
||||
// -------------------------------------------------------------
|
||||
@@ -259,7 +271,12 @@ func (mc *MemoryComposer) ComposeDialoguePrompt(userInput string, userCtx *UserP
|
||||
sb.WriteString("1. 优先级裁决:项目黑板核心事实 > 本轮用户输入 > 近期对话 > 早期摘要 > 检索片段。\n")
|
||||
sb.WriteString("2. 事实与引用:涉及法规条款与标准时,优先依据【参考知识】与【项目基准与协同上下文】并沿用〔引用键〕格式;无把握时明确提示依据不足,严禁生造标准号。\n")
|
||||
sb.WriteString("3. 隐私与跨用户边界:其他成员姓名与私人习惯仅限理解语境,禁止外泄复述。\n")
|
||||
sb.WriteString("4. 答复目标:请结合上下文,以温和严谨的「项目助手」口吻作答,并写入 reply.md。")
|
||||
sb.WriteString("4. 答复目标:请结合上下文,以温和严谨的「项目助手」口吻作答,并写入 reply.md。\n")
|
||||
if userCtx.RealtimeUnavailable {
|
||||
sb.WriteString("5. 本轮实时检索失败:必须明确告知无法获取实时数据(建议查看天气 App),严禁编造任何具体天气/新闻数值。\n")
|
||||
} else if strings.TrimSpace(userCtx.RealtimeSnippet) != "" {
|
||||
sb.WriteString("5. 实时数据:涉及天气/新闻等时效问题,必须严格依据【实时检索结果】作答,不得使用训练记忆中的过期数值。\n")
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
@@ -133,3 +133,47 @@ func TestMemoryComposer_HierarchyAndPriority(t *testing.T) {
|
||||
idxWorkMem, idxProject, idxSummary, idxRecent, idxRecalled, idxReference, idxGuidance, idxUser, idxReq)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func TestMemoryComposer_RealtimeSnippetAndUnavailable(t *testing.T) {
|
||||
composer := coordinator.NewMemoryComposer()
|
||||
fixed := time.Date(2026, 9, 23, 10, 41, 0, 0, time.FixedZone("CST", 8*3600))
|
||||
composer.NowFn = func() time.Time { return fixed }
|
||||
|
||||
okCtx := &coordinator.UserProjectContext{
|
||||
Project: coordinator.ProjectInfo{
|
||||
ID: "proj_rt",
|
||||
Name: "冲击波治疗仪",
|
||||
CurrentStage: maturity.StageV01_Triage,
|
||||
},
|
||||
BlackboardFacts: map[string]string{},
|
||||
RealtimeSnippet: "北京今日气温约 21~33°C,晴。",
|
||||
}
|
||||
okPrompt := composer.ComposeDialoguePrompt("今天北京天气怎么样", okCtx)
|
||||
if !strings.Contains(okPrompt, "【实时检索结果】") {
|
||||
t.Fatalf("missing realtime section: %s", okPrompt)
|
||||
}
|
||||
if !strings.Contains(okPrompt, "21~33") {
|
||||
t.Fatalf("missing snippet body: %s", okPrompt)
|
||||
}
|
||||
if !strings.Contains(okPrompt, "必须严格依据【实时检索结果】作答") {
|
||||
t.Fatalf("missing success constraint: %s", okPrompt)
|
||||
}
|
||||
|
||||
failCtx := &coordinator.UserProjectContext{
|
||||
Project: coordinator.ProjectInfo{
|
||||
ID: "proj_rt",
|
||||
Name: "冲击波治疗仪",
|
||||
CurrentStage: maturity.StageV01_Triage,
|
||||
},
|
||||
BlackboardFacts: map[string]string{},
|
||||
RealtimeUnavailable: true,
|
||||
}
|
||||
failPrompt := composer.ComposeDialoguePrompt("今天北京天气怎么样", failCtx)
|
||||
if strings.Contains(failPrompt, "【实时检索结果】") {
|
||||
t.Fatalf("should not render empty realtime section: %s", failPrompt)
|
||||
}
|
||||
if !strings.Contains(failPrompt, "本轮实时检索失败") {
|
||||
t.Fatalf("missing failure constraint: %s", failPrompt)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package coordinator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"chat-agent-system/internal/intent"
|
||||
)
|
||||
|
||||
// RealtimeSearcher 对话路径的实时信息检索(天气/新闻等);由 app 注入 Pi Runner /v1/search。
|
||||
type RealtimeSearcher interface {
|
||||
Search(ctx context.Context, query string) (markdown string, err error)
|
||||
}
|
||||
|
||||
var realtimeInfoMarkers = []string{
|
||||
"天气", "气温", "温度", "下雨", "下雪", "降雨", "降雪", "台风", "雾霾",
|
||||
"天气预报", "气象", "风力", "体感",
|
||||
"新闻", "头条", "股价", "汇率", "实时", "今日新闻", "热点新闻",
|
||||
}
|
||||
|
||||
// IsRealtimeInformationQuery 判定是否为需要实时检索的时效性问句。
|
||||
// 含强业务词典时不触发(避免「型检温度」「项目进度」等误伤)。
|
||||
func IsRealtimeInformationQuery(input string) bool {
|
||||
trimmed := strings.TrimSpace(input)
|
||||
if trimmed == "" || utf8.RuneCountInString(trimmed) < 2 {
|
||||
return false
|
||||
}
|
||||
if intent.HasStrongBusinessLexicon(trimmed) {
|
||||
return false
|
||||
}
|
||||
for _, m := range realtimeInfoMarkers {
|
||||
if strings.Contains(trimmed, m) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package coordinator
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestIsRealtimeInformationQuery(t *testing.T) {
|
||||
positives := []string{
|
||||
"今天北京天气怎么样",
|
||||
"明天会下雨吗",
|
||||
"外面气温多少",
|
||||
"今天有什么新闻",
|
||||
"美元汇率多少",
|
||||
"查一下实时股价",
|
||||
}
|
||||
for _, in := range positives {
|
||||
if !IsRealtimeInformationQuery(in) {
|
||||
t.Fatalf("expected realtime query: %q", in)
|
||||
}
|
||||
}
|
||||
|
||||
negatives := []string{
|
||||
"",
|
||||
"a",
|
||||
"项目进度怎么样",
|
||||
"型检温度指标怎么定",
|
||||
"注册路径需要注意什么",
|
||||
"帮我推进 V0.5 方案",
|
||||
"你好",
|
||||
}
|
||||
for _, in := range negatives {
|
||||
if IsRealtimeInformationQuery(in) {
|
||||
t.Fatalf("did not expect realtime query: %q", in)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -182,6 +182,11 @@ func IsSmallTalkOrGeneralQuestion(input string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// HasStrongBusinessLexicon 是否含强业务词典(项目/法规/文档治理等),供闲聊与实时检索判定复用。
|
||||
func HasStrongBusinessLexicon(input string) bool {
|
||||
return hasStrongBusinessLexicon(input)
|
||||
}
|
||||
|
||||
func hasStrongBusinessLexicon(input string) bool {
|
||||
markers := []string{
|
||||
"项目", "阶段", "推进", "法规", "注册", "文档", "审核", "批准", "驳回",
|
||||
|
||||
Reference in New Issue
Block a user