package workbench import ( "context" "fmt" "strings" "github.com/Tencent/WeKnora/internal/accesspolicy" "github.com/Tencent/WeKnora/internal/agent/token" "github.com/Tencent/WeKnora/internal/models/chat" "github.com/Tencent/WeKnora/internal/types" ) const qualitySystem = `你是内置跨稿质检员。审查已采用正文的逻辑、先修依赖、衔接、重复和论据一致性。严重逻辑断裂或重复为 blocking,措辞建议为 suggestion。不得编造问题或审查范围。严格输出 JSON {"passed":true,"summary":"中文总结","issues":[{"segment_ids":["实际段落编号"],"severity":"blocking 或 suggestion","problem":"具体问题","suggestion":"具体修改建议"}]}。无 blocking 时 passed 必须为 true,否则 false。` func manuscriptPrompt(segments []Segment, advice string) string { return "本次仅审查以下实际采用的正文。用户可以主动跳过内容段,编号不连续或段数少于原计划本身不是缺陷,不得要求恢复已跳过段;若确有未完成承诺,应建议在已采用段内补足或修正预告。\n已采用正文:" + compact(segments) + "\n用户补充意见:" + advice } // Every accepted segment is reviewed in full, and adjacent windows overlap by // one complete segment. A single segment that cannot fit still fails explicitly. func manuscriptWindows(c *completionRequest, segments []Segment, advice string) ([][]Segment, error) { fits := func(rows []Segment) bool { messages := append(c.messages[:0:0], c.messages...) messages[1].Content = manuscriptPrompt(rows, advice) return c.estimator.EstimateMessages(messages) <= c.inputLimit } if fits(segments) { return [][]Segment{segments}, nil } windows := [][]Segment{} current := []Segment{} for _, segment := range segments { if !fits([]Segment{segment}) { return nil, fmt.Errorf("%w: 模型上下文容量不足以容纳单个内容段,请为质检选择更大上下文模型或拆分该段", ErrInvalid) } if len(current) > 0 && !fits(append(append([]Segment(nil), current...), segment)) { windows = append(windows, current) current = []Segment{current[len(current)-1]} if !fits(append(current, segment)) { return nil, fmt.Errorf("%w: 相邻正文的上下文容量不足,无法完整检查衔接,请选择更大上下文模型", ErrInvalid) } } current = append(current, segment) } if len(current) > 0 { windows = append(windows, current) } return windows, nil } func mergeQualityReports(reports []QualityReport, accepted []Segment) (*QualityReport, error) { merged := &QualityReport{Passed: true, Issues: []QualityIssue{}} seen := map[string]bool{} for i, report := range reports { merged.Summary += fmt.Sprintf("检查 %d:%s\n", i+1, report.Summary) for _, issue := range report.Issues { key := compact(issue) if !seen[key] { seen[key] = true merged.Issues = append(merged.Issues, issue) } if issue.Severity == "blocking" { merged.Passed = false } } } if err := validateQuality(merged, accepted); err != nil { return nil, err } return merged, nil } func (s *Service) prepareQualityCompletion(ctx context.Context, agentID string) (*completionRequest, error) { if !accesspolicy.Allowed(ctx, "content.review") { return nil, ErrForbidden } snapshot, frozen := snapshotFromContext(ctx, agentID) if !frozen || snapshot.ModelID == "" { return s.prepareCompletion(ctx, agentID, qualitySystem, "") } // Use the task's recorded model for review, without loading private skills, // persona, memory, or knowledge that the reviewer cannot otherwise access. metadata, err := s.models.GetModelByID(ctx, snapshot.ModelID) if err != nil { return nil, err } if metadata == nil { return nil, fmt.Errorf("%w: 任务的质检模型已不可用,请更新任务配置", ErrInvalid) } estimator, err := token.NewEstimator() if err != nil { return nil, err } contextSize := types.AgentMaxContextTokens(0, metadata.Parameters.ContextWindow) if snapshot.ContextWindow > 0 { contextSize = min(contextSize, snapshot.ContextWindow) } outputLimit := snapshot.OutputLimit if outputLimit <= 0 { outputLimit = types.DefaultMaxCompletionTokens(types.AgentModeQuickAnswer, "") } if metadata.Parameters.MaxOutputTokens > 0 { outputLimit = min(outputLimit, metadata.Parameters.MaxOutputTokens) } agent := &types.CustomAgent{ID: agentID, Config: types.CustomAgentConfig{ModelID: snapshot.ModelID, Temperature: snapshot.Temperature, Thinking: snapshot.Thinking}} c := &completionRequest{agent: agent, estimator: estimator, contextSize: contextSize, inputLimit: contextSize - contextSize*15/100 - outputLimit, outputLimit: outputLimit, messages: []chat.Message{{Role: "system", Content: qualitySystem}, {Role: "user"}}, jsonOutput: true} return c, c.validateBudget() } func (s *Service) reviewManuscript(ctx context.Context, agentID string, accepted []Segment, advice string) (*QualityReport, error) { c, err := s.prepareQualityCompletion(ctx, agentID) if err != nil { return nil, err } windows, err := manuscriptWindows(c, accepted, advice) if err != nil { return nil, err } reports := []QualityReport{} for i, window := range windows { if lease, ok := ctx.Value(leaseContextKey{}).(jobLease); ok { if err := s.progress(ctx, lease.ID, 40+40*i/len(windows), fmt.Sprintf("跨稿质检:第 %d/%d 组,完整检查正文及相邻衔接", i+1, len(windows))); err != nil { return nil, err } } c.messages[1].Content = manuscriptPrompt(window, advice) c.jsonOutput = true text, err := s.runCompletion(ctx, c) if err != nil { return nil, err } report, err := parseJSON[QualityReport](text) if err != nil { return nil, err } if err := validateQuality(&report, window); err != nil { return nil, err } reports = append(reports, report) } report, err := mergeQualityReports(reports, accepted) if err != nil { return nil, err } if len(windows) > 1 { report.Summary = fmt.Sprintf("共 %d 组,全部已采用正文逐组完整质检,相邻组保留完整衔接段;远距离语义一致性仍需结合整稿复核。\n", len(windows)) + report.Summary } return report, nil } func composeAccepted(accepted []Segment) string { parts := make([]string, 0, len(accepted)) for _, segment := range accepted { parts = append(parts, segment.Content) } return strings.Join(parts, "\n\n") } func (s *Service) composeManuscript(ctx context.Context, agentID string, accepted []Segment, advice string) (string, error) { if strings.TrimSpace(advice) != "" { return "", fmt.Errorf("%w: 组合成稿保留原文;请先在对应内容段应用修改建议并重新质检", ErrInvalid) } return composeAccepted(accepted), nil }