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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -94,7 +94,7 @@ func main() {
|
||||
app.RabbitMQ,
|
||||
app.LLM,
|
||||
app.Logger,
|
||||
)
|
||||
).WithConfigProvider(app.ConfigStore)
|
||||
monitoringService := app.MonitoringService
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user