feat(agent): clarify vague briefs before generating and harden prompts

Let the planner return a clarify/chat intent that asks 2-4 targeted
questions (imageCount 0) when essentials are missing, instead of forging
ahead. Tighten prompt guidance to preserve user wording and stated
reference-image roles, avoid inventing copy/specs, keep durable brand
constraints, and treat vague taste feedback as clarification. Also
fix the generation heartbeat goroutine to stop cleanly via defer, add
research-query fallbacks, and refresh image-only capability copy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-08 10:56:55 +08:00
parent 1a1c0a6544
commit ad67fe1ebe
5 changed files with 318 additions and 44 deletions
+88 -20
View File
@@ -1377,12 +1377,17 @@ func (s *DesignService) planAgentTask(ctx context.Context, project design.Projec
}
func normalizeAgentTaskPlan(plan design.AgentTaskPlan, prompt string, mode string, messages []design.AgentChatMessage, webSearchEnabled bool, forceImage bool) design.AgentTaskPlan {
plan.Intent = normalizeAgentIntent(plan.Intent)
if forceImage || shouldGenerateForAgentPrompt(prompt, mode, messages) {
rawIntent := strings.TrimSpace(plan.Intent)
plan.Intent = normalizeAgentIntent(rawIntent)
if forceImage || shouldForceImageMode(mode) {
plan.Intent = "image"
}
if plan.Intent == "" {
plan.Intent = "chat"
if shouldGenerateForAgentPrompt(prompt, mode, messages) {
plan.Intent = "image"
} else {
plan.Intent = "chat"
}
}
if !webSearchEnabled {
plan.ShouldSearch = false
@@ -1434,9 +1439,9 @@ func normalizeAgentTaskPlan(plan design.AgentTaskPlan, prompt string, mode strin
func fallbackPlanningResponse(prompt string, intent string) string {
if normalizeAgentIntent(intent) == "image" {
if prefersEnglishText(prompt) {
return "I will break this into the right visual deliverables and generate them on the canvas."
return "I will turn this into concrete image deliverables and generate them on the canvas."
}
return "我会先把需求拆成合适的视觉交付项,再在画布中生成。"
return "我会先把需求拆成具体图片交付项,再在画布中生成。"
}
if prefersEnglishText(prompt) {
return "I will answer based on the current canvas context."
@@ -1573,9 +1578,9 @@ func (s *DesignService) updateAgentMessage(ctx context.Context, projectID string
func fallbackAgentChatResponse(prompt string, project design.Project) string {
if isCapabilityPrompt(strings.ToLower(strings.TrimSpace(prompt))) {
if prefersEnglishText(prompt) {
return "I mainly help with image and design-canvas work: generating visuals, editing images, shaping ecommerce pages, reading the current canvas, and suggesting the next concrete move. When you give me a clear visual task, I will enter the image-generation pipeline."
return "I focus on image generation and image editing: brand/site visuals, ecommerce product images, posters, banners, social graphics, reference-based generation, background removal, vectorization, mockup placement, and multi-round canvas iteration. Give me a clear visual task and I will generate or edit images on the canvas."
}
return "我主要负责图片与设计画布:生成视觉稿、编辑图片、做电商页面方向、拆解当前画布并给下一步建议。你给我明确的作图目标时,我进入生图链路。"
return "我专注图片生成和图片编辑:品牌/官网视觉、电商产品图、海报 Banner、社媒图、参考图生图、去背景、矢量化、Mockup 贴图,以及在无限画布里多轮迭代。你给我明确的图片任务后,我进入生图或修图链路。"
}
if strings.TrimSpace(project.Brief) != "" {
if prefersEnglishText(prompt) {
@@ -1594,7 +1599,9 @@ func normalizeAgentIntent(intent string) string {
switch intent {
case "image", "generate_image", "edit_image", "visual", "design":
return "image"
case "research":
case "":
return ""
case "research", "clarify", "brief", "briefing", "question", "questions", "chat", "conversation":
return "chat"
default:
return "chat"
@@ -2571,7 +2578,9 @@ func (s *DesignService) completeCreateProjectGeneration(ctx context.Context, pro
message.Type = "assistant"
}
if incremental {
project.Nodes = mergeCompletedIncrementalGeneratedNodes(project.Nodes, plan.GeneratedNodes, "")
project.Nodes = clearRemainingAgentGenerationPlaceholders(project.Nodes, "")
project.Messages = appendMissingIncrementalArtifactMessages(project.Messages, threadID, prompt, plan.GeneratedNodes, now.Add(-time.Millisecond))
} else {
project.Nodes = mergeGeneratedNodesIntoTarget(nonGeneratingNodes(project.Nodes), plan.GeneratedNodes, "")
project.Messages = append(project.Messages, toolArtifactsMessage(threadID, prompt, plan.GeneratedNodes, now.Add(-time.Millisecond)))
@@ -2940,7 +2949,11 @@ func (s *DesignService) completeFollowupGeneration(ctx context.Context, projectI
message.Type = "assistant"
}
if incremental {
project.Nodes = mergeCompletedIncrementalGeneratedNodes(project.Nodes, plan.GeneratedNodes, targetNodeID)
project.Nodes = clearRemainingAgentGenerationPlaceholders(project.Nodes, targetNodeID)
if !isolated {
project.Messages = appendMissingIncrementalArtifactMessages(project.Messages, threadID, prompt, plan.GeneratedNodes, now.Add(-time.Millisecond))
}
} else {
baseNodes := nonGeneratingNodes(project.Nodes)
if targetNodeID != "" {
@@ -2986,7 +2999,15 @@ func (s *DesignService) planWithIncrementalImages(ctx context.Context, projectID
done := make(chan struct{})
if appendArtifacts {
go s.emitGenerationHeartbeat(ctx, projectID, threadID, req.Prompt, done)
heartbeatStopped := make(chan struct{})
go func() {
defer close(heartbeatStopped)
s.emitGenerationHeartbeat(ctx, projectID, threadID, req.Prompt, done)
}()
defer func() {
close(done)
<-heartbeatStopped
}()
}
var mu sync.Mutex
persistedNodes := make([]design.Node, 0, plannedGenerationResultCapacity(req))
@@ -3004,9 +3025,6 @@ func (s *DesignService) planWithIncrementalImages(ctx context.Context, projectID
persistedNodes = append(persistedNodes, node)
return s.applyIncrementalGeneratedNode(ctx, projectID, threadID, prompt, targetNodeID, node, appendArtifacts)
})
if appendArtifacts {
close(done)
}
if len(persistedNodes) > 0 {
plan.GeneratedNodes = append([]design.Node(nil), persistedNodes...)
}
@@ -3039,6 +3057,32 @@ func completedGeneratedImageNodes(nodes []design.Node) []design.Node {
return completed
}
func mergeCompletedIncrementalGeneratedNodes(nodes []design.Node, generated []design.Node, targetNodeID string) []design.Node {
completed := completedGeneratedImageNodes(generated)
if len(completed) == 0 {
return nodes
}
if strings.TrimSpace(targetNodeID) != "" {
return mergeGeneratedNodesIntoTarget(nodes, completed, targetNodeID)
}
for _, node := range completed {
nodes = upsertGeneratedNode(nodes, node)
}
return nodes
}
func appendMissingIncrementalArtifactMessages(messages []design.Message, threadID string, prompt string, nodes []design.Node, createdAt time.Time) []design.Message {
existing := generatedImageArtifactsByID(messages)
for _, node := range completedGeneratedImageNodes(nodes) {
if _, ok := existing[node.ID]; ok {
continue
}
messages = append(messages, toolArtifactsMessage(threadID, prompt, []design.Node{node}, createdAt))
existing[node.ID] = design.GeneratorTaskArtifact{}
}
return messages
}
func plannedGenerationResultCapacity(req design.AgentRequest) int {
count := req.TaskPlan.ImageCount
if count <= 0 {
@@ -3237,10 +3281,7 @@ func (s *DesignService) maybeAddResearchStep(ctx context.Context, projectID stri
Messages: messages,
Memory: buildAgentMemory(project, prompt, mode, messages),
})
if !decision.ShouldSearch {
return nil
}
query := strings.TrimSpace(decision.Query)
query := enabledResearchQuery(prompt, mode, decision)
if query == "" {
return nil
}
@@ -3254,16 +3295,38 @@ func (s *DesignService) maybeAddPlannedResearchStep(ctx context.Context, project
if strings.TrimSpace(plan.Intent) == "" {
return s.maybeAddResearchStep(ctx, projectID, threadID, project, prompt, mode, messages, enabled)
}
if !plan.ShouldSearch {
return nil
}
query := strings.TrimSpace(plan.SearchQuery)
if query == "" && plan.ShouldSearch {
query = fallbackResearchQuery(prompt, mode)
}
if query == "" {
return nil
}
return s.addResearchStep(ctx, projectID, threadID, query)
}
func enabledResearchQuery(prompt string, mode string, decision design.AgentResearchDecision) string {
if strings.TrimSpace(decision.Query) != "" {
return strings.TrimSpace(decision.Query)
}
if !shouldSearchWhenEnabled(prompt, mode) {
return ""
}
return fallbackResearchQuery(prompt, mode)
}
func shouldSearchWhenEnabled(prompt string, mode string) bool {
text := strings.ToLower(strings.TrimSpace(prompt))
if text == "" || isConversationalPrompt(text) {
return false
}
modeText := strings.ToLower(strings.TrimSpace(mode))
if strings.Contains(modeText, "image-action") {
return shouldRunResearchFallback(prompt, mode)
}
return true
}
func (s *DesignService) decideResearch(ctx context.Context, req design.AgentResearchDecisionRequest) design.AgentResearchDecision {
if decider, ok := s.creativeAgent.(design.ResearchDecider); ok {
if decision, err := decider.DecideResearch(ctx, req); err == nil {
@@ -3366,6 +3429,11 @@ func shouldGenerateForAgentPrompt(prompt string, mode string, messages []design.
return false
}
func shouldForceImageMode(mode string) bool {
modeText := strings.ToLower(strings.TrimSpace(mode))
return strings.Contains(modeText, "image-action") || modeText == "image" || modeText == "pure-image"
}
func hasAgentImageInput(messages []design.AgentChatMessage) bool {
for _, message := range messages {
for _, content := range message.Contents {
@@ -5692,7 +5760,7 @@ func inspirationItems() []design.InspirationItem {
return []design.InspirationItem{
{ID: "luxury-spring", Title: "2026 春季系列设计趋势", Category: "Luxury Showcase", Description: "强对比摄影、低饱和色块与清晰购买动线。", Accent: "#111827"},
{ID: "natural-ui", Title: "极简自然主义 UI", Category: "Mobile App", Description: "玻璃拟态信息卡与植物识别流程。", Accent: "#6b8f71"},
{ID: "blue-geometry", Title: "蓝色 3D 几何广告", Category: "Campaign", Description: "冷感材质、漂浮图形和高亮 CTA。", Accent: "#2563eb"},
{ID: "blue-geometry", Title: "蓝色立体几何广告", Category: "Campaign", Description: "冷感材质、漂浮图形和高亮 CTA。", Accent: "#2563eb"},
{ID: "brand-kit", Title: "天然护肤品牌板", Category: "Brand Kit", Description: "字体、插画、包装和详情页模块统一沉淀。", Accent: "#c8a568"},
}
}