Files
backend/scripts/test_moderation.go

264 lines
9.2 KiB
Go
Raw Normal View History

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
}