package main import ( "bytes" "encoding/json" "flag" "fmt" "io" "net/http" "strings" "time" ) const moderationSystemPrompt = `你是中文社区的内容审核助手,负责对"帖子标题、正文、配图"做联合审核。目标是平衡社区安全与正常交流:必须拦截高风险违规内容,但不要误伤正常玩梗、二创、吐槽和轻度调侃。请只输出指定JSON。 审核流程: 1) 先判断是否命中硬性违规; 2) 再判断语境(玩笑/自嘲/朋友间互动/作品讨论); 3) 做文图交叉判断(文本+图片合并理解); 4) 给出 approved 与简短 reason。 硬性违规(命中任一项必须 approved=false): A. 宣传对立与煽动撕裂: - 明确煽动群体对立、地域对立、性别对立、民族宗教对立,鼓动仇恨、排斥、报复。 B. 严重人身攻击与网暴引导: - 持续性侮辱贬损、羞辱人格、号召围攻/骚扰/挂人/线下冲突。 C. 开盒/人肉/隐私暴露: - 故意公开、拼接、索取他人可识别隐私信息(姓名+联系方式、身份证号、住址、学校单位、车牌、定位轨迹等); - 图片/截图中出现可识别隐私信息并伴随曝光意图,也按违规处理。 D. 其他高危违规: - 违法犯罪、暴力威胁、极端仇恨、色情低俗、诈骗引流、恶意广告等。 放行规则(以下通常 approved=true): - 正常玩梗、表情包、谐音梗、二次创作、无恶意的吐槽; - 非定向、轻度口语化吐槽(无明确攻击对象、无网暴号召、无隐私暴露); - 对社会事件/作品的理性讨论、观点争论(即使语气尖锐,但未煽动对立或人身攻击)。 边界判定: - 若只是"梗文化表达"且不指向现实伤害,优先通过; - 若存在明确伤害意图(煽动、围攻、曝光隐私),必须拒绝; - 对模糊内容不因个别粗口直接拒绝,需结合对象、意图、号召性和可执行性综合判断。 reason 要求: - approved=false 时:中文10-30字,说明核心违规点; - approved=true 时:reason 为空字符串。 输出格式(严格): 仅输出一行JSON对象,不要Markdown,不要额外解释: {"approved": true/false, "reason": "..."}` type chatMessage struct { Role string `json:"role"` Content interface{} `json:"content"` } type contentPart struct { Type string `json:"type"` Text string `json:"text,omitempty"` } type chatCompletionsRequest struct { Model string `json:"model"` Messages []chatMessage `json:"messages"` Temperature float64 `json:"temperature,omitempty"` MaxTokens int `json:"max_tokens,omitempty"` EnableThinking *bool `json:"enable_thinking,omitempty"` // qwen3.5思考模式控制 ThinkingBudget *int `json:"thinking_budget,omitempty"` // 思考过程最大token数 ResponseFormat *responseFormatConfig `json:"response_format,omitempty"` // 响应格式 } type responseFormatConfig struct { Type string `json:"type"` // "text" or "json_object" } type chatCompletionsResponse struct { Choices []struct { Message struct { Content string `json:"content"` } `json:"message"` FinishReason string `json:"finish_reason"` } `json:"choices"` Usage struct { PromptTokens int `json:"prompt_tokens"` CompletionTokens int `json:"completion_tokens"` TotalTokens int `json:"total_tokens"` } `json:"usage"` } func main() { baseURL := flag.String("url", "https://api.littlelan.cn/", "API base URL") apiKey := flag.String("key", "", "API key") model := flag.String("model", "qwen3.5-plus", "Model name") maxTokens := flag.Int("max-tokens", 220, "Max tokens for completion") enableThinking := flag.Bool("enable-thinking", false, "Enable thinking mode for qwen3.5") flag.Parse() if *apiKey == "" { fmt.Println("Error: API key is required. Use -key flag") return } // 测试用例 testCases := []struct { name string content string }{ { name: "简单正常内容", content: "帖子标题:今天天气真好\n帖子内容:出门散步,心情愉快!", }, { name: "中等长度内容", content: "帖子标题:分享我的学习经验\n帖子内容:最近在学习Go语言,发现这门语言真的很适合后端开发。并发处理特别方便,goroutine和channel的设计非常优雅。有一起学习的小伙伴吗?", }, { name: "较长内容", content: "帖子标题:关于校园生活的一些思考\n帖子内容:大学四年转眼就过去了,回想起来有很多感慨。刚入学的时候什么都不懂,现在感觉自己成长了很多。在这里想分享一些自己的经验,希望能对学弟学妹们有所帮助。首先是学习方面,一定要认真听课,做好笔记。其次是社交方面,多参加社团活动,结交志同道合的朋友。最后是规划方面,早点想清楚自己想做什么,为之努力。", }, } client := &http.Client{Timeout: 120 * time.Second} fmt.Println("============================================") fmt.Printf("模型: %s\n", *model) fmt.Printf("API URL: %s\n", *baseURL) fmt.Printf("MaxTokens 设置: %d\n", *maxTokens) fmt.Printf("EnableThinking: %v\n", *enableThinking) fmt.Println("============================================") for _, tc := range testCases { fmt.Printf("\n========== 测试: %s ==========\n", tc.name) fmt.Printf("内容长度: %d 字符\n", len(tc.content)) userPrompt := fmt.Sprintf("%s\n图片批次:1/1(本次仅提供当前批次图片)", tc.content) reqBody := chatCompletionsRequest{ Model: *model, Messages: []chatMessage{ {Role: "system", Content: moderationSystemPrompt}, {Role: "user", Content: []contentPart{{Type: "text", Text: userPrompt}}}, }, Temperature: 0.1, MaxTokens: *maxTokens, } // 设置思考模式 if !*enableThinking { reqBody.EnableThinking = enableThinking // 设置思考预算为0,完全禁用思考 zero := 0 reqBody.ThinkingBudget = &zero } // 使用JSON输出格式 reqBody.ResponseFormat = &responseFormatConfig{Type: "json_object"} data, err := json.Marshal(reqBody) if err != nil { fmt.Printf("Error marshaling request: %v\n", err) continue } endpoint := strings.TrimRight(*baseURL, "/") + "/v1/chat/completions" if strings.HasSuffix(strings.TrimRight(*baseURL, "/"), "/v1") { endpoint = strings.TrimRight(*baseURL, "/") + "/chat/completions" } req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(data)) if err != nil { fmt.Printf("Error creating request: %v\n", err) continue } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+*apiKey) start := time.Now() resp, err := client.Do(req) if err != nil { fmt.Printf("Error sending request: %v\n", err) continue } body, err := io.ReadAll(resp.Body) resp.Body.Close() if err != nil { fmt.Printf("Error reading response: %v\n", err) continue } elapsed := time.Since(start) if resp.StatusCode >= 400 { fmt.Printf("API Error: status=%d, body=%s\n", resp.StatusCode, string(body)) continue } var parsed chatCompletionsResponse if err := json.Unmarshal(body, &parsed); err != nil { fmt.Printf("Error parsing response: %v\n", err) fmt.Printf("Raw response: %s\n", string(body)) continue } if len(parsed.Choices) == 0 { fmt.Println("No choices in response") fmt.Printf("Raw response: %s\n", string(body)) continue } fmt.Printf("响应时间: %v\n", elapsed) fmt.Printf("Finish Reason: %s\n", parsed.Choices[0].FinishReason) fmt.Printf("Token使用情况:\n") fmt.Printf(" - PromptTokens: %d\n", parsed.Usage.PromptTokens) fmt.Printf(" - CompletionTokens: %d\n", parsed.Usage.CompletionTokens) fmt.Printf(" - TotalTokens: %d\n", parsed.Usage.TotalTokens) output := parsed.Choices[0].Message.Content fmt.Printf("输出内容长度: %d 字符\n", len(output)) // 检查输出是否符合预期 if parsed.Usage.CompletionTokens > *maxTokens { fmt.Printf("\n⚠️ 警告: CompletionTokens (%d) 超过了 max_tokens 设置 (%d)!\n", parsed.Usage.CompletionTokens, *maxTokens) } if len(output) > 500 { fmt.Printf("\n⚠️ 警告: 输出内容过长! 长度=%d\n", len(output)) fmt.Printf("前500字符:\n%s...\n", output[:min(500, len(output))]) } else { fmt.Printf("输出内容: %s\n", output) } // 尝试解析JSON extractJSONObject := func(raw string) string { text := strings.TrimSpace(raw) start := strings.Index(text, "{") end := strings.LastIndex(text, "}") if start >= 0 && end > start { return text[start : end+1] } return text } jsonStr := extractJSONObject(output) var result struct { Approved bool `json:"approved"` Reason string `json:"reason"` } if err := json.Unmarshal([]byte(jsonStr), &result); err != nil { fmt.Printf("\n⚠️ 警告: 无法解析JSON输出: %v\n", err) fmt.Printf("提取的JSON: %s\n", jsonStr) } else { fmt.Printf("\n✓ 解析成功: approved=%v, reason=\"%s\"\n", result.Approved, result.Reason) } } fmt.Println("\n========== 测试完成 ==========") } func min(a, b int) int { if a < b { return a } return b }