diff --git a/deploy/config.yaml b/deploy/config.yaml index dd5410a..98d8396 100644 --- a/deploy/config.yaml +++ b/deploy/config.yaml @@ -212,6 +212,7 @@ llm: api_key: "7fb6c66b-129c-4935-9617-709236c4205a" model: doubao-seed-2-0-pro-260215 # doubao-seed-2-0-lite-260215 # knowledge_url_model: doubao-seed-2-0-mini-260215 #doubao-seed-2-0-mini-260215 + monitoring_answer_parse_model: doubao-seed-2-0-lite-260215 timeout: 2m max_output_tokens: 16000 temperature: 0.7 @@ -221,6 +222,7 @@ llm: compliance: enabled: true llm_judge_enabled: false + llm_judge_model: "" # 留空时使用 llm.model snapshot_reload_interval: 30s publish_gate_timeout: 800ms review_worker_concurrency: 1 diff --git a/deploy/k3s/config/config.yaml b/deploy/k3s/config/config.yaml index 531245b..8bff38c 100644 --- a/deploy/k3s/config/config.yaml +++ b/deploy/k3s/config/config.yaml @@ -194,6 +194,7 @@ llm: api_key: "7fb6c66b-129c-4935-9617-709236c4205a" model: doubao-seed-2-0-pro-260215 # doubao-seed-2-0-lite-260215 # knowledge_url_model: doubao-seed-2-0-mini-260215 #doubao-seed-2-0-mini-260215 + monitoring_answer_parse_model: doubao-seed-2-0-lite-260215 timeout: 2m max_output_tokens: 16000 temperature: 0.7 @@ -203,6 +204,7 @@ llm: compliance: enabled: true llm_judge_enabled: false + llm_judge_model: "doubao-seed-2-0-lite-260215" # 留空时使用 llm.model snapshot_reload_interval: 30s publish_gate_timeout: 800ms review_worker_concurrency: 1 diff --git a/server/cmd/scheduler/main.go b/server/cmd/scheduler/main.go index 79dcab6..141965a 100644 --- a/server/cmd/scheduler/main.go +++ b/server/cmd/scheduler/main.go @@ -94,7 +94,7 @@ func main() { app.RabbitMQ, app.LLM, app.Logger, - ) + ).WithConfigProvider(app.ConfigStore) monitoringService := app.MonitoringService ctx, cancel := context.WithCancel(context.Background()) diff --git a/server/cmd/tenant-api/main.go b/server/cmd/tenant-api/main.go index 345e226..ad59dcc 100644 --- a/server/cmd/tenant-api/main.go +++ b/server/cmd/tenant-api/main.go @@ -35,7 +35,8 @@ func main() { app.DesktopDispatch.Run(workerCtx) monitoringService := app.MonitoringService - monitoringCallbackService := tenantapp.NewMonitoringCallbackService(app.DB, app.MonitoringDB, app.RabbitMQ, app.LLM, app.Logger) + monitoringCallbackService := tenantapp.NewMonitoringCallbackService(app.DB, app.MonitoringDB, app.RabbitMQ, app.LLM, app.Logger). + WithConfigProvider(app.ConfigStore) tenantapp.NewMonitoringCollectOutboxWorker(monitoringService, app.Logger).Start(workerCtx) tenantapp.NewMonitoringResultIngestWorker(monitoringCallbackService, app.RabbitMQ, app.Logger, app.Config().MonitoringWorkers).Start(workerCtx) tenantapp.NewMonitoringProjectionRebuildWorker(monitoringCallbackService, app.RabbitMQ, app.Logger, app.Config().MonitoringWorkers).Start(workerCtx) diff --git a/server/configs/config.local.yaml.example b/server/configs/config.local.yaml.example index 6e70caa..0815e07 100644 --- a/server/configs/config.local.yaml.example +++ b/server/configs/config.local.yaml.example @@ -192,6 +192,7 @@ llm: base_url: https://ark.cn-beijing.volces.com/api/v3 model: doubao-seed-2-0-lite-260215 knowledge_url_model: doubao-seed-2-0-mini-260215 + monitoring_answer_parse_model: doubao-seed-2-0-lite-260215 timeout: 2m max_output_tokens: 4000 temperature: 0.7 @@ -204,6 +205,7 @@ llm: compliance: enabled: true llm_judge_enabled: false + llm_judge_model: "" # 留空时使用 llm.model snapshot_reload_interval: 30s publish_gate_timeout: 800ms review_worker_concurrency: 1 diff --git a/server/configs/config.yaml b/server/configs/config.yaml index 16791b8..0abfd4e 100644 --- a/server/configs/config.yaml +++ b/server/configs/config.yaml @@ -198,6 +198,7 @@ llm: api_key: "7fb6c66b-129c-4935-9617-709236c4205a" model: doubao-seed-2-0-lite-260428 # doubao-seed-2-0-pro-260215 # knowledge_url_model: doubao-seed-2-0-mini-260428 #doubao-seed-2-0-mini-260215 + monitoring_answer_parse_model: doubao-seed-2-0-lite-260215 timeout: 2m max_output_tokens: 16000 temperature: 0.7 @@ -207,6 +208,7 @@ llm: compliance: enabled: true llm_judge_enabled: false + llm_judge_model: "" # 留空时使用 llm.model snapshot_reload_interval: 30s publish_gate_timeout: 800ms review_worker_concurrency: 1 diff --git a/server/internal/shared/config/config.go b/server/internal/shared/config/config.go index d2c63ec..2a25cdb 100644 --- a/server/internal/shared/config/config.go +++ b/server/internal/shared/config/config.go @@ -328,22 +328,24 @@ type LogConfig struct { } type LLMConfig struct { - Provider string `mapstructure:"provider"` - APIKey string `mapstructure:"api_key"` - BaseURL string `mapstructure:"base_url"` - Model string `mapstructure:"model"` - KnowledgeURLModel string `mapstructure:"knowledge_url_model"` - Timeout time.Duration `mapstructure:"timeout"` - MaxOutputTokens int64 `mapstructure:"max_output_tokens"` - Temperature float64 `mapstructure:"temperature"` - TopP float64 `mapstructure:"top_p"` - ReasoningEffort string `mapstructure:"reasoning_effort"` - WebSearchLimit int32 `mapstructure:"web_search_limit"` + Provider string `mapstructure:"provider"` + APIKey string `mapstructure:"api_key"` + BaseURL string `mapstructure:"base_url"` + Model string `mapstructure:"model"` + KnowledgeURLModel string `mapstructure:"knowledge_url_model"` + MonitoringAnswerParseModel string `mapstructure:"monitoring_answer_parse_model"` + Timeout time.Duration `mapstructure:"timeout"` + MaxOutputTokens int64 `mapstructure:"max_output_tokens"` + Temperature float64 `mapstructure:"temperature"` + TopP float64 `mapstructure:"top_p"` + ReasoningEffort string `mapstructure:"reasoning_effort"` + WebSearchLimit int32 `mapstructure:"web_search_limit"` } type ComplianceConfig struct { Enabled bool `mapstructure:"enabled"` LLMJudgeEnabled bool `mapstructure:"llm_judge_enabled"` + LLMJudgeModel string `mapstructure:"llm_judge_model"` SnapshotReloadInterval time.Duration `mapstructure:"snapshot_reload_interval"` PublishGateTimeout time.Duration `mapstructure:"publish_gate_timeout"` ReviewWorkerConcurrency int `mapstructure:"review_worker_concurrency"` @@ -987,12 +989,18 @@ func applyEnvOverrides(cfg *Config) { if model, ok := lookupNonEmptyEnv("LLM_KNOWLEDGE_URL_MODEL"); ok { cfg.LLM.KnowledgeURLModel = model } + if model, ok := lookupNonEmptyEnv("LLM_MONITORING_ANSWER_PARSE_MODEL"); ok { + cfg.LLM.MonitoringAnswerParseModel = model + } if enabled, ok := lookupBoolEnv("COMPLIANCE_ENABLED"); ok { cfg.Compliance.Enabled = enabled } if enabled, ok := lookupBoolEnv("COMPLIANCE_LLM_ENABLED"); ok { cfg.Compliance.LLMJudgeEnabled = enabled } + if model, ok := lookupNonEmptyEnv("COMPLIANCE_LLM_JUDGE_MODEL"); ok { + cfg.Compliance.LLMJudgeModel = model + } if d, ok := lookupDurationEnv("COMPLIANCE_SNAPSHOT_RELOAD_INTERVAL"); ok { cfg.Compliance.SnapshotReloadInterval = d } diff --git a/server/internal/shared/config/config_test.go b/server/internal/shared/config/config_test.go index 570dba7..7f63c6c 100644 --- a/server/internal/shared/config/config_test.go +++ b/server/internal/shared/config/config_test.go @@ -27,6 +27,30 @@ llm: } } +func TestLoadAppliesSpecializedLLMModelEnvOverrides(t *testing.T) { + t.Setenv("LLM_MONITORING_ANSWER_PARSE_MODEL", "env-monitoring-parse") + t.Setenv("COMPLIANCE_LLM_JUDGE_MODEL", "env-compliance-judge") + + configPath := writeTestConfig(t, ` +llm: + monitoring_answer_parse_model: "config-monitoring-parse" +compliance: + llm_judge_model: "config-compliance-judge" +`) + + cfg, err := Load(configPath) + if err != nil { + t.Fatalf("Load() error = %v", err) + } + + if cfg.LLM.MonitoringAnswerParseModel != "env-monitoring-parse" { + t.Fatalf("expected env monitoring parse model, got %q", cfg.LLM.MonitoringAnswerParseModel) + } + if cfg.Compliance.LLMJudgeModel != "env-compliance-judge" { + t.Fatalf("expected env compliance judge model, got %q", cfg.Compliance.LLMJudgeModel) + } +} + func TestDatabaseConfigDSNEscapesCredentials(t *testing.T) { cfg := DatabaseConfig{ Host: "db.internal", diff --git a/server/internal/tenant/app/monitoring_answer_llm_parse.go b/server/internal/tenant/app/monitoring_answer_llm_parse.go index af0d853..60162b3 100644 --- a/server/internal/tenant/app/monitoring_answer_llm_parse.go +++ b/server/internal/tenant/app/monitoring_answer_llm_parse.go @@ -11,7 +11,7 @@ import ( ) const ( - monitoringAnswerParseLLMModel = "doubao-seed-2-0-lite-260215" + defaultMonitoringAnswerParseLLMModel = "doubao-seed-2-0-lite-260215" monitoringAnswerParseLLMTimeout = 30 * time.Second monitoringAnswerParseLLMMaxOutputTokens = 600 ) @@ -63,6 +63,7 @@ func parseMonitoringAnswerWithLLM( client sharedllm.Client, answer string, brandName string, + model string, ) (monitoringAnswerParseSummary, error) { answer = strings.TrimSpace(answer) brandName = strings.TrimSpace(brandName) @@ -72,9 +73,13 @@ func parseMonitoringAnswerWithLLM( if brandName == "" { return monitoringAnswerParseSummary{}, fmt.Errorf("brand name is empty") } + model = strings.TrimSpace(model) + if model == "" { + model = defaultMonitoringAnswerParseLLMModel + } result, err := client.Generate(ctx, sharedllm.GenerateRequest{ - Model: monitoringAnswerParseLLMModel, + Model: model, Prompt: buildMonitoringAnswerParsePrompt(answer, brandName), Timeout: monitoringAnswerParseLLMTimeout, MaxOutputTokens: monitoringAnswerParseLLMMaxOutputTokens, diff --git a/server/internal/tenant/app/monitoring_callback_service.go b/server/internal/tenant/app/monitoring_callback_service.go index d6637a6..2684cf1 100644 --- a/server/internal/tenant/app/monitoring_callback_service.go +++ b/server/internal/tenant/app/monitoring_callback_service.go @@ -21,6 +21,7 @@ import ( "golang.org/x/net/publicsuffix" sharedauth "github.com/geo-platform/tenant-api/internal/shared/auth" + "github.com/geo-platform/tenant-api/internal/shared/config" sharedllm "github.com/geo-platform/tenant-api/internal/shared/llm" "github.com/geo-platform/tenant-api/internal/shared/messaging/rabbitmq" "github.com/geo-platform/tenant-api/internal/shared/response" @@ -52,6 +53,7 @@ type MonitoringCallbackService struct { monitoringPool *pgxpool.Pool rabbitMQ *rabbitmq.Client llm sharedllm.Client + cfg config.Provider logger *zap.Logger } @@ -70,6 +72,13 @@ func NewMonitoringCallbackService( } } +func (s *MonitoringCallbackService) WithConfigProvider(provider config.Provider) *MonitoringCallbackService { + if s != nil { + s.cfg = provider + } + return s +} + type MonitoringTaskResultRequest struct { LeaseToken string `json:"lease_token" binding:"required"` Status string `json:"status"` @@ -2732,7 +2741,7 @@ func (s *MonitoringCallbackService) enrichMonitoringParsePayload(ctx context.Con parseVersion := resolveMonitoringParseVersion(req) if shouldUseMonitoringLLMParse(req) && strings.TrimSpace(answer) != "" && s.llm != nil && s.llm.Validate() == nil { - llmSummary, llmErr := parseMonitoringAnswerWithLLM(ctx, s.llm, answer, brandName) + llmSummary, llmErr := parseMonitoringAnswerWithLLM(ctx, s.llm, answer, brandName, s.monitoringAnswerParseModel()) if llmErr != nil { if s.logger != nil { s.logger.Warn("monitoring answer llm parse failed", @@ -2782,6 +2791,17 @@ func shouldUseMonitoringLLMParse(req MonitoringTaskResultRequest) bool { return !hasCompleteMonitoringParsePayload(req) } +func (s *MonitoringCallbackService) monitoringAnswerParseModel() string { + if s != nil && s.cfg != nil { + if cfg := s.cfg.Current(); cfg != nil { + if model := strings.TrimSpace(cfg.LLM.MonitoringAnswerParseModel); model != "" { + return model + } + } + } + return defaultMonitoringAnswerParseLLMModel +} + func (s *MonitoringCallbackService) buildCitationFacts(ctx context.Context, tx pgx.Tx, tenantID int64, platformID string, inputs []MonitoringSourceItem) ([]monitoringCitationFact, error) { if len(inputs) == 0 { return nil, nil diff --git a/server/internal/tenant/app/monitoring_callback_service_test.go b/server/internal/tenant/app/monitoring_callback_service_test.go index c3d5665..c6c8e1f 100644 --- a/server/internal/tenant/app/monitoring_callback_service_test.go +++ b/server/internal/tenant/app/monitoring_callback_service_test.go @@ -12,8 +12,26 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/geo-platform/tenant-api/internal/shared/config" ) +func TestMonitoringAnswerParseModelUsesConfig(t *testing.T) { + svc := (&MonitoringCallbackService{}).WithConfigProvider(config.NewStaticProvider(&config.Config{ + LLM: config.LLMConfig{MonitoringAnswerParseModel: " configured-monitoring-model "}, + })) + + if got := svc.monitoringAnswerParseModel(); got != "configured-monitoring-model" { + t.Fatalf("monitoringAnswerParseModel() = %q, want configured-monitoring-model", got) + } +} + +func TestMonitoringAnswerParseModelDefaultsToLegacyLite(t *testing.T) { + if got := (&MonitoringCallbackService{}).monitoringAnswerParseModel(); got != defaultMonitoringAnswerParseLLMModel { + t.Fatalf("monitoringAnswerParseModel() = %q, want %q", got, defaultMonitoringAnswerParseLLMModel) + } +} + func TestDecideHeartbeatPrimaryLease(t *testing.T) { tests := []struct { name string diff --git a/server/internal/tenant/compliance/service.go b/server/internal/tenant/compliance/service.go index 463a888..10a80b8 100644 --- a/server/internal/tenant/compliance/service.go +++ b/server/internal/tenant/compliance/service.go @@ -1120,6 +1120,7 @@ func (s *Service) ProcessReviewJob(ctx context.Context, msg ReviewJobMessage) er return s.markReviewJobDone(ctx, job.ID) } result, err := s.llm.Generate(ctx, sharedllm.GenerateRequest{ + Model: strings.TrimSpace(cfg.LLMJudgeModel), Prompt: prompt, Timeout: cfg.ReviewWorkerTimeout, MaxOutputTokens: 900, diff --git a/server/internal/tenant/transport/desktop_monitoring_handler.go b/server/internal/tenant/transport/desktop_monitoring_handler.go index 760eb85..cfa4cb7 100644 --- a/server/internal/tenant/transport/desktop_monitoring_handler.go +++ b/server/internal/tenant/transport/desktop_monitoring_handler.go @@ -18,7 +18,8 @@ type DesktopMonitoringHandler struct { func NewDesktopMonitoringHandler(a *bootstrap.App) *DesktopMonitoringHandler { return &DesktopMonitoringHandler{ - svc: app.NewMonitoringCallbackService(a.DB, a.MonitoringDB, a.RabbitMQ, a.LLM, a.Logger), + svc: app.NewMonitoringCallbackService(a.DB, a.MonitoringDB, a.RabbitMQ, a.LLM, a.Logger). + WithConfigProvider(a.ConfigStore), } }