This commit is contained in:
王性驊 2026-06-28 20:25:39 +08:00
parent cdda235597
commit a6c429a66d
3 changed files with 83 additions and 31 deletions

View File

@ -75,6 +75,14 @@ func BuildMissionResearchMapUserPrompt(in CopyResearchMapInput) string {
return b.String() return b.String()
} }
func MissionResearchMapJSONOnlyRetryPrompt() string {
return strings.TrimSpace(`上次回覆沒有包含可解析的 JSON****輸出一個 JSON 物件
- 直接以 { 開頭 } 結尾不要任何思考過程說明文字或 markdown 圍欄
- 需含 audienceSummarycontentGoalquestionspillarsexclusionssuggestedTagsbenchmarkNotes 欄位
- suggestedTags 至少 4 每項含 tag/reason/searchIntent/searchType
- 不要使用 ... 省略也不要加註解或尾端逗號`)
}
func ParseMissionResearchMapOutput(raw string) (MissionResearchMap, error) { func ParseMissionResearchMapOutput(raw string) (MissionResearchMap, error) {
payload, err := extractCopyMapJSON(raw) payload, err := extractCopyMapJSON(raw)
if err != nil { if err != nil {

View File

@ -78,17 +78,8 @@ func runAnalyzeCopyMission(ctx context.Context, step StepContext, deps AnalyzeCo
return err return err
} }
result, err := deps.AI.GenerateText(ctx, domai.GenerateRequest{ systemPrompt := libviral.BuildMissionResearchMapSystemPrompt()
Provider: providerID, userPrompt := libviral.BuildMissionResearchMapUserPrompt(libviral.CopyResearchMapInput{
Model: credential.Model,
Credential: domai.Credential{
APIKey: credential.APIKey,
},
System: libviral.BuildMissionResearchMapSystemPrompt(),
Messages: []domai.Message{
{
Role: "user",
Content: libviral.BuildMissionResearchMapUserPrompt(libviral.CopyResearchMapInput{
Label: mission.Label, Label: mission.Label,
SeedQuery: mission.SeedQuery, SeedQuery: mission.SeedQuery,
Brief: mission.Brief, Brief: mission.Brief,
@ -98,18 +89,49 @@ func runAnalyzeCopyMission(ctx context.Context, step StepContext, deps AnalyzeCo
PersonaContentGoal: persona.CopyResearchMap.ContentGoal, PersonaContentGoal: persona.CopyResearchMap.ContentGoal,
PersonaQuestions: append([]string(nil), persona.CopyResearchMap.Questions...), PersonaQuestions: append([]string(nil), persona.CopyResearchMap.Questions...),
PersonaPillars: append([]string(nil), persona.CopyResearchMap.Pillars...), PersonaPillars: append([]string(nil), persona.CopyResearchMap.Pillars...),
}),
},
},
}) })
// ResearchGenerateRequest raises max_tokens so the full research map JSON is
// not truncated mid-object (truncation is the main cause of parse failures).
result, err := deps.AI.GenerateText(ctx, placement.ResearchGenerateRequest(domai.GenerateRequest{
Provider: providerID,
Model: credential.Model,
Credential: domai.Credential{
APIKey: credential.APIKey,
},
System: systemPrompt,
Messages: []domai.Message{
{Role: "user", Content: userPrompt},
},
}))
if err != nil { if err != nil {
return err return err
} }
parsed, err := libviral.ParseMissionResearchMapOutput(result.Text) parsed, err := libviral.ParseMissionResearchMapOutput(result.Text)
if err != nil { if err != nil {
// Model sometimes replies with reasoning/prose and no JSON object.
// Retry once, explicitly demanding a raw JSON object.
jsonOnlyResult, retryErr := deps.AI.GenerateText(ctx, placement.ResearchGenerateRequest(domai.GenerateRequest{
Provider: providerID,
Model: credential.Model,
Credential: domai.Credential{
APIKey: credential.APIKey,
},
System: systemPrompt,
Messages: []domai.Message{
{Role: "user", Content: userPrompt},
{Role: "assistant", Content: result.Text},
{Role: "user", Content: libviral.MissionResearchMapJSONOnlyRetryPrompt()},
},
}))
if retryErr != nil {
return app.For(code.AI).SvcThirdParty("拷貝任務研究地圖 LLM 回傳無法解析:" + err.Error()) return app.For(code.AI).SvcThirdParty("拷貝任務研究地圖 LLM 回傳無法解析:" + err.Error())
} }
parsed, err = libviral.ParseMissionResearchMapOutput(jsonOnlyResult.Text)
if err != nil {
return app.For(code.AI).SvcThirdParty("拷貝任務研究地圖 LLM 回傳無法解析:" + err.Error())
}
}
entityTags := make([]missionentity.SuggestedTag, 0, len(parsed.SuggestedTags)) entityTags := make([]missionentity.SuggestedTag, 0, len(parsed.SuggestedTags))
for _, tag := range parsed.SuggestedTags { for _, tag := range parsed.SuggestedTags {

View File

@ -195,13 +195,38 @@ func runExpandCopyMissionGraph(ctx context.Context, step StepContext, deps Expan
return err return err
} }
graph, err := libkg.ParseSynthOutput(result.Text, libkg.SynthInput{ synthIn := libkg.SynthInput{
Seed: seed, Seed: seed,
TargetAudience: mission.ResearchMap.AudienceSummary, TargetAudience: mission.ResearchMap.AudienceSummary,
}, braveSources) }
graph, err := libkg.ParseSynthOutput(result.Text, synthIn, braveSources)
if err != nil { if err != nil {
// Model occasionally replies with reasoning/prose and no JSON object.
// Retry once, explicitly demanding a raw JSON object.
jsonOnlyReq := placement.ResearchGenerateRequest(domai.GenerateRequest{
Provider: providerID,
Model: credential.Model,
Credential: domai.Credential{
APIKey: credential.APIKey,
},
System: systemPrompt,
Messages: []domai.Message{
{Role: "user", Content: userPrompt},
{Role: "assistant", Content: result.Text},
{Role: "user", Content: libkg.KnowledgeGraphJSONOnlyRetryPrompt()},
},
})
retryResult, retryErr := deps.AI.GenerateText(ctx, jsonOnlyReq)
if retryErr != nil {
return app.For(code.AI).SvcThirdParty("延伸知識產生失敗,請重試:" + err.Error()) return app.For(code.AI).SvcThirdParty("延伸知識產生失敗,請重試:" + err.Error())
} }
retryGraph, retryParseErr := libkg.ParseSynthOutput(retryResult.Text, synthIn, braveSources)
if retryParseErr != nil {
return app.For(code.AI).SvcThirdParty("延伸知識產生失敗,請重試:" + retryParseErr.Error())
}
graph = retryGraph
result = retryResult
}
if libkg.GraphTooThin(graph) { if libkg.GraphTooThin(graph) {
retryReq := placement.ResearchGenerateRequest(domai.GenerateRequest{ retryReq := placement.ResearchGenerateRequest(domai.GenerateRequest{
Provider: providerID, Provider: providerID,
@ -217,10 +242,7 @@ func runExpandCopyMissionGraph(ctx context.Context, step StepContext, deps Expan
}, },
}) })
if retryResult, retryErr := deps.AI.GenerateText(ctx, retryReq); retryErr == nil { if retryResult, retryErr := deps.AI.GenerateText(ctx, retryReq); retryErr == nil {
if retryGraph, parseErr := libkg.ParseSynthOutput(retryResult.Text, libkg.SynthInput{ if retryGraph, parseErr := libkg.ParseSynthOutput(retryResult.Text, synthIn, braveSources); parseErr == nil && !libkg.GraphTooThin(retryGraph) {
Seed: seed,
TargetAudience: mission.ResearchMap.AudienceSummary,
}, braveSources); parseErr == nil && !libkg.GraphTooThin(retryGraph) {
graph = retryGraph graph = retryGraph
} }
} }