feat(tenant): inject brand description context into every generation path

Include the current brand description in template, custom, imitation,
and KOL article-generation prompts while bounding prompt growth.

- Add a shared brand-context builder that resolves the current brand,
  compacts long descriptions, and caps rendered output at 2000 runes.
- Snapshot a structured brand prompt context on queued tasks so old
  jobs stay reproducible and retries stay stable.
- Route brand-profile facts through the existing typed fact guard so
  founding dates and year counts get the same pre-persistence validation
  as RAG facts, with brand facts outranking conflicting RAG facts.
- Fix the KOL creator to attach the brand scope its worker consumes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-10 20:10:39 +08:00
parent 418b3c4998
commit 0bf4b73ebc
12 changed files with 998 additions and 32 deletions
@@ -137,6 +137,10 @@ func (s *ArticleImitationService) Generate(ctx context.Context, req GenerateImit
if strings.TrimSpace(req.BrandName) == "" {
return nil, response.ErrBadRequest(40001, "brand_name_required", "brand_name is required")
}
brandPromptContext, err := loadBrandPromptContext(ctx, s.pool, actor.TenantID, brandID)
if err != nil {
return nil, err
}
quotaRepo := repository.NewQuotaRepository(s.pool)
balance, err := quotaRepo.GetCurrentBalance(ctx, actor.TenantID, "article_generation")
@@ -146,6 +150,7 @@ func (s *ArticleImitationService) Generate(ctx context.Context, req GenerateImit
inputParams := buildImitationInputParams(sourceURL, req)
inputParams["brand_id"] = brandID
inputParams = attachBrandPromptContext(inputParams, brandPromptContext)
inputJSON, _ := json.Marshal(inputParams)
initialTitle := buildImitationInitialTitle(req.SourceTitle)
wizardStateJSON, _ := json.Marshal(map[string]interface{}{
@@ -318,6 +323,13 @@ func (s *ArticleImitationService) executeGeneration(ctx context.Context, job art
}
RecordGenerationTaskEvent(GenerationTaskEventTransition, articleImitationJobLogContext(job, "task_status_running", "", GenerationTaskStatusRunning, GenerationTaskResultSuccess))
LogGenerationTaskInfo(s.logger, "article generation task marked running", articleImitationJobLogContext(job, "task_status_running", "", GenerationTaskStatusRunning, GenerationTaskResultSuccess))
brandID, brandPromptContext, err := ResolveGenerationBrandPromptContext(ctx, s.pool, job.TenantID, job.ArticleID, job.InputParams)
if err != nil {
s.failGeneration(ctx, job, "brand_context_load", err)
return
}
job.InputParams["brand_id"] = brandID
job.InputParams = attachBrandPromptContext(job.InputParams, brandPromptContext)
sourceURL := strings.TrimSpace(extractString(job.InputParams, "source_url"))
if sourceURL == "" {
@@ -345,6 +357,7 @@ func (s *ArticleImitationService) executeGeneration(ctx context.Context, job art
}
knowledgePrompt := ""
var knowledgeFactConstraints []KnowledgeFactConstraint
if knowledgeGroupIDs := extractKnowledgeGroupIDs(job.InputParams["knowledge_group_ids"]); len(knowledgeGroupIDs) > 0 {
if s.knowledge == nil {
s.failGeneration(ctx, job, "knowledge_resolve", fmt.Errorf("knowledge service is not configured"))
@@ -353,7 +366,7 @@ func (s *ArticleImitationService) executeGeneration(ctx context.Context, job art
knowledgeContext, resolveErr := s.knowledge.ResolveContext(
ctx,
job.TenantID,
int64(extractInt(job.InputParams, "brand_id")),
brandID,
knowledgeGroupIDs,
buildImitationKnowledgeQuery(job.InputParams),
)
@@ -362,6 +375,7 @@ func (s *ArticleImitationService) executeGeneration(ctx context.Context, job art
return
}
knowledgePrompt = s.knowledge.RenderPromptSection(knowledgeContext)
knowledgeFactConstraints = knowledgeContext.FactConstraints
}
prompt := buildImitationGenerationPrompt(job.InputParams, sourceContent, knowledgePrompt)
@@ -375,7 +389,8 @@ func (s *ArticleImitationService) executeGeneration(ctx context.Context, job art
req.WebSearch = &job.WebSearch
}
result, err := s.llm.Generate(ctx, req, func(delta string) {
knowledgeFactConstraints = MergeKnowledgeFactConstraints(brandPromptContext.FactConstraints, knowledgeFactConstraints)
result, err := GenerateArticleWithKnowledgeFactGuard(ctx, s.llm, req, knowledgeFactConstraints, func(delta string) {
if runtimeCfg.StreamEnabled && s.streams != nil && delta != "" {
s.streams.AppendDelta(job.ArticleID, job.TaskID, title, delta)
}
@@ -669,12 +684,19 @@ func buildImitationGenerationPrompt(params map[string]interface{}, sourceContent
if knowledgePrompt != "" {
knowledgeSection = "## 知识库参考\n" + knowledgePrompt
}
referenceSections := make([]string, 0, 2)
if brandPrompt := RenderBrandPromptContext(extractBrandPromptContext(params)); brandPrompt != "" {
referenceSections = append(referenceSections, brandPrompt)
}
if knowledgeSection != "" {
referenceSections = append(referenceSections, knowledgeSection)
}
return prompts.ArticleImitationPrompt(
locale,
sourceMeta.String(),
settings.String(),
knowledgeSection,
strings.Join(referenceSections, "\n\n"),
sourceContent,
)
}