feat(tenant): make monitoring parse & compliance judge models configurable

Replace the hardcoded doubao-lite model used for monitoring answer
parsing and compliance LLM judging with configurable values, resolved
from config with env overrides and a legacy default fallback.

- Add llm.monitoring_answer_parse_model and compliance.llm_judge_model
  to config struct, env overrides, and all config files
- Thread the resolved model through MonitoringCallbackService via a
  ConfigProvider and into parseMonitoringAnswerWithLLM
- Pass compliance.llm_judge_model into the review-job LLM request

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-09 17:04:01 +08:00
parent fb142e4ca7
commit c8dceb43d7
13 changed files with 103 additions and 17 deletions
+2
View File
@@ -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
+2
View File
@@ -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
+1 -1
View File
@@ -94,7 +94,7 @@ func main() {
app.RabbitMQ,
app.LLM,
app.Logger,
)
).WithConfigProvider(app.ConfigStore)
monitoringService := app.MonitoringService
ctx, cancel := context.WithCancel(context.Background())
+2 -1
View File
@@ -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)
+2
View File
@@ -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
+2
View File
@@ -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
+19 -11
View File
@@ -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
}
@@ -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",
@@ -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,
@@ -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
@@ -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
@@ -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,
@@ -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),
}
}