ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

AI文本检测实战:用维基百科和大模型构建辨别游戏

AI文本检测实战:用维基百科和大模型构建辨别游戏 做一个“AI or Not”式的维基百科答题小游戏核心并不是把两段文本并排展示给用户那么简单。真正的工程链路是先从维基百科取到人类编辑撰写的真实词条摘要再调用大模型生成同一主题的“AI 风格”文本最后把两段文本随机打乱让用户判断哪一段来自 AI判完立刻给反馈并记录得分。这类小游戏在 AI 应用开发里很有代表性。它不依赖高成本数据和复杂模型只需要把维基百科 API、大模型接口和基础前后端组织起来就能形成一个可交互、可演示、可扩展的完整产品。背后涉及的问题包括如何区分人写文本和模型生成文本、大模型提示词如何稳定输出、接口异常如何降级、以及这类能力在内容标注和模型评测中怎么用。适合读者想用大模型做一个真实可玩项目的开发者正在学习 FastAPI 或 Python 后端的前端工程师以及对 AI 应用好奇的产品经理。全文会带你把服务完整搭起来从环境准备到接口调试最后给出常见问题排查和生产化建议。1. 为什么用“维基百科文本”来做 AI 检测游戏1.1 这个游戏到底在测什么“AI or Not”类游戏表面上是在测用户实际上是在测模型和文本风格。人类写的维基百科词条开篇通常追求信息密度句子紧凑结构稳定习惯先给定义再展开背景。大模型生成的文本虽然表面上也有百科风格但细节上容易出现几种可感知的特征喜欢用“值得注意的是”“在当今时代”这类过渡句。信息密度偏低一句话能说完的内容会扩展成三句。偶尔出现事实错误也就是 AI 幻觉。语言非常“平均”缺乏个人编辑带来的句式变化。所以游戏判断的不只是内容真假而是文本风格、信息密度和事实准确性。用户答对之后如果能看到模型原文和人类原文的差异就天然理解了一次 AI 文本生成的特点。1.2 维基百科作为题目源的四个优势选择维基百科而不是自己积累语料是因为它提供的题目源是现成、稳定且合法的。优势说明数据现成REST API 直接返回词条摘要不需要自己抓网页洗数据覆盖话题广随机词条可以覆盖技术、历史、地理、人物等多类主题质量有保证词条经过多轮编辑开篇摘要相对规范适合作为“人类文本”基准内容协议清楚维基百科内容遵循开放许可做学习项目和产品原型时更容易说清来源对学习项目来说这意味着你可以把大部分精力放在题目逻辑和 AI 生成上而不是花一整周去清洗语料。1.3 为什么不是直接调用现成 AI 检测服务市面上有商用 AI 检测服务也能看到“某段文本是人类写的还是 AI 写的”的概率分数。但如果项目只用现成检测 API你就无法控制题目生成过程也无法解释检测结果背后的逻辑。更重要的是检测服务通常只给一个概率用户不知道这个概率是怎么来的。自己实现“人类文本 AI 生成文本”的组合等于把整个链路拆开哪一段是人类写的、哪一段是 AI 写的在服务端是已知的。这样你能验证提示词对文本风格的影响也能控制题目难度甚至后续可以把用户答案收集成标注数据集。这个学习价值是单纯调用检测 API 给不了的。2. 系统设计与一回合的数据结构2.1 模块划分整个服务按职责拆成五个模块各自只处理一件事模块职责关键技术点wikipedia_client获取真实词条摘要调用维基百科 REST API清洗 extractai_generator生成 AI 风格同主题文本调用 OpenAI 兼容接口控制提示词quiz_service组装题目、洗牌、判题维护题目 ID、选项 ID、正解api 层暴露 HTTP 接口FastAPI 路由负责请求参数校验前端展示题目、接收选择原生 HTML/CSS/JSfetch 调用接口这样拆分的好处是你可以单独测试维基百科接口是否可用也可以单独测试模型生成的文本质量。如果以后要换成别的百科源或者别的模型只需要替换对应模块。2.2 一回合完整数据流先梳理清楚一次答题从开始到结束经过哪些环节再写代码就不容易乱。用户点击“下一题”前端请求GET /api/quiz。后端调用维基百科随机词条接口拿到词条标题和人类摘要。后端把标题和人类摘要发给大模型要求写一段同主题百科简介。后端把人类文本和 AI 文本打乱生成两个选项并记录哪个选项是 AI。把题目 ID、词条标题、两个选项文本返回前端。用户选择 A 或 B前端请求POST /api/answer。后端根据题目 ID 取出正解比较用户选择返回是否正确和当前得分。注意第 3 步是最耗时的一步。大模型生成通常需要 1 到 5 秒所以接口设计时不要把“生成题目”和“判题”放在一个请求里做完而不考虑超时实际项目中可以拆成“创建题目”和“获取结果”两步或者先做异步生成。2.3 关键取舍题目实时生成还是预生成实时生成的优点是不需要存储大量题目每次请求都是新题缺点是用户每次等待模型返回而且维基百科接口和模型接口任何一个抖动都会影响体验。预生成的优点是把耗时前置管理员提前批量生成 100 题存在数据库里用户打开页面秒出题缺点是题目库会陈旧而且生成成本固定。学习阶段建议先做实时生成逻辑更直观。等到产品需要稳定体验时再加一个本地缓存把生成过的题目存到 SQLite 或 Redis标题相同的题目直接复用。下面代码都按实时生成的最小闭环来写。3. 环境准备与依赖安装3.1 运行环境要求开发环境建议使用 Python 3.10 及以上因为 FastAPI 和 Pydantic 的较新版本依赖新语法。操作系统不限Windows、macOS、Linux 都可以。依赖项如下表所示安装时以你当前环境能拉到的最新稳定版本为准。依赖包作用fastapiWeb 框架提供 API 路由和参数校验uvicornASGI 服务器用来启动 FastAPIhttpx发起维基百科 HTTP 请求openai调用大模型接口的官方 SDKpython-dotenv读取 .env 环境变量文件开发环境需要能访问维基百科的 REST 接口以及你的模型供应商接口。生产环境还要注意网络策略、超时时间和密钥管理这些在排错一节会提到。3.2 项目结构与依赖文件先建立项目目录目录结构保持简单方便后面扩展。ai-or-not-quiz/ ├── app.py # FastAPI 入口 ├── quiz/ │ ├── __init__.py │ ├── wikipedia_client.py # 维基百科摘要获取 │ ├── ai_generator.py # AI 文本生成 │ ├── quiz_service.py # 题目组装与判定 │ └── models.py # Pydantic 数据模型 ├── static/ │ ├── index.html │ ├── style.css │ └── app.js ├── requirements.txt ├── .env.example └── README.mdrequirements.txt 内容如下fastapi uvicorn[standard] httpx openai python-dotenv安装依赖pip install -r requirements.txt如果你的环境同时存在多个 Python 版本建议先创建虚拟环境再安装python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txtWindows 下激活虚拟环境命令是.venv\Scripts\activate注意区分。3.3 环境变量配置模型密钥不能写死在代码里所以通过环境变量注入。新建.env文件内容参考.env.exampleLLM_API_KEY你的模型APIKey LLM_BASE_URLhttps://api.openai.com/v1 LLM_MODELgpt-4o-mini如果你的大模型服务使用 OpenAI 兼容接口LLM_BASE_URL指向对应服务地址即可。没有填写LLM_API_KEY时程序应该能启动但生成题目的接口返回明确错误而不是直接崩溃。4. 实现维基百科摘要获取模块4.1 选择随机词条接口维基百科提供 REST API其中/page/random/summary可以直接拿到一个随机词条的摘要。这个接口的好处是响应体已经包含标题、摘要和页面链接不需要再解析 HTML。实现quiz/wikipedia_client.pyimport httpx WIKIPEDIA_REST_BASE https://en.wikipedia.org/api/rest_v1 def fetch_random_article_summary() - dict: url f{WIKIPEDIA_REST_BASE}/page/random/summary headers {User-Agent: ai-or-not-quiz/0.1 (learning project)} with httpx.Client(timeout10.0, follow_redirectsTrue) as client: resp client.get(url, headersheaders) resp.raise_for_status() data resp.json() extract data.get(extract, ).strip() if not extract: raise ValueError(维基百科摘要为空请重新获取) return { title: data.get(title, ), extract: extract, pageid: data.get(pageid), url: data.get(content_urls, {}).get(desktop, {}).get(page), }关键点timeout10.0防止请求长时间挂起。User-Agent要声明项目名和用途维基百科文档建议这样做能降低被限流的概率。extract字段就是词条开篇的纯文本摘要默认已经很干净。如果想按指定主题出题可以使用按标题获取摘要的接口from urllib.parse import quote def fetch_article_summary_by_title(title: str) - dict: encoded_title quote(title.replace( , _)) url f{WIKIPEDIA_REST_BASE}/page/summary/{encoded_title} # 后续逻辑与 fetch_random_article_summary 一致4.2 读取和清洗 extract 字段extract虽然是纯文本但直接使用时还要做三件事去掉首尾空格。控制长度。有些词条摘要比较长超过 300 个单词后用户阅读负担会增加。判断文本是否过短。如果摘要只有几个单词说明这个词条可能质量较低建议放弃并重新随机。长度控制和异常处理可以合并成一个函数def normalize_extract(extract: str, max_chars: int 600) - str: text extract.strip().replace(\n, ) if len(text) 80: raise ValueError(摘要过短不适合作为题目) if len(text) max_chars: text text[:max_chars].rsplit( , 1)[0] ... return text这里用rsplit( , 1)在空格处截断避免把一个单词切成两半。4.3 中英文词条与语言切换基础版本使用英文维基百科词条覆盖广模型对英文百科风格也更熟悉。想支持中文只需把域名换成https://zh.wikipedia.orgWIKIPEDIA_REST_BASE https://zh.wikipedia.org/api/rest_v1同时 AI 生成模块的提示词也要改成中文否则会出现“英文模型生成英文中文词条生成中文”的语言错位。建议把语言作为配置项不要写死。5. AI 同主题文本生成模块5.1 提示词设计让模型写出“像维基百科”的文本提示词决定题目难度。如果提示词太弱AI 文本一眼就能看出来如果提示词太强模型可能直接复制人类摘要用户读起来像同源文本。推荐提示词要强调三点客观语气、独立写作、接近百科开篇的信息密度。SYSTEM_PROMPT 你是一名百科词条编辑。请用客观、中立的语气写一段关于指定主题的百科简介。 要求 1. 全文 120 到 180 个英文单词。 2. 不要使用“作为一个AI模型”“我不能”等表述。 3. 不要输出标题直接输出正文。 4. 不要复制参考文本而是基于主题独立写作。 5. 信息密度尽量接近维基百科的开篇段落。注意提示词里明确“不要复制参考文本”是为了保证 AI 文本和人类文本不完全重复否则游戏会退化成“两段内容是否相同”而不是“风格是否像 AI”。5.2 调用 OpenAI 兼容接口使用 openai 官方 SDK并支持通过base_url切换到其他兼容服务这是最常见的接入方式import os from openai import OpenAI client OpenAI( api_keyos.getenv(LLM_API_KEY), base_urlos.getenv(LLM_BASE_URL, https://api.openai.com/v1), ) def generate_ai_extract(title: str, human_extract: str) - str: user_prompt ( f主题{title}\n f参考背景不要直接复制\n{human_extract[:500]}\n 请写一段独立的百科简介。 ) resp client.chat.completions.create( modelos.getenv(LLM_MODEL, gpt-4o-mini), messages[ {role: system, content: SYSTEM_PROMPT}, {role: user, content: user_prompt}, ], temperature0.7, max_tokens300, ) text resp.choices[0].message.content.strip() if not text: raise ValueError(模型返回空文本) return text5.3 控制输出长度和稳定性大模型生成不是确定性的同一提示词两次结果可能不同。为了控制题目质量需要理解几个参数参数作用建议值调大影响调小影响temperature控制随机性0.7文本更多样可能跑题更稳定但容易重复max_tokens限制最大输出长度300可输出更长文本可能截断正文top_p采样范围默认即可更发散更保守如果题目需要“让用户更难猜”可以尝试temperature0.9如果作为评测基准建议固定temperature0.3到0.5保证结果相对稳定。实际中不要同时调大temperature和top_p会让输出不可控。6. 题目组装与判定服务6.1 题目数据模型拿到两段文本后把它们封装成一个题目对象。用 Pydantic 定义模型方便接口层做响应序列化。from pydantic import BaseModel class QuizOption(BaseModel): id: str text: str is_ai: bool class QuizQuestion(BaseModel): question_id: str title: str options: list[QuizOption] class QuizAnswer(BaseModel): question_id: str selected_id: stris_ai字段只存在于服务端真正返回给前端的时候要把它剥离否则用户看一眼 JSON 就知道答案了。这里先讲清楚模型接口层返回时再处理。6.2 随机洗牌与答案记录洗牌是保证游戏公平的关键。如果不洗牌AI 文本永远固定在 A 或 B用户连续玩几次就能猜出规律。import random import uuid class QuizRound: def __init__(self, title: str, human_text: str, ai_text: str): self.question_id uuid.uuid4().hex self.title title self.options [ {id: A, text: human_text, is_ai: False}, {id: B, text: ai_text, is_ai: True}, ] random.shuffle(self.options) self.answer_id next( opt[id] for opt in self.options if opt[is_ai] ) def public_view(self) - dict: return { question_id: self.question_id, title: self.title, options: [ {id: opt[id], text: opt[text]} for opt in self.options ], } def check(self, selected_id: str) - bool: return selected_id self.answer_id这里用uuid生成题目 ID避免自增数字被用户猜测。答案只保存在内存对象里判题时按question_id取出。6.3 计分与会话管理最小闭环可以用内存字典保存题目和分数简单但不适合多进程部署class QuizStore: def __init__(self): self.rounds: dict[str, QuizRound] {} self.scores: dict[str, int] {} quiz_store QuizStore()生产环境需要把question_id和分数存到 Redis 或数据库并且设置过期时间防止内存无限增长。学习阶段用内存即可但要清楚重启服务后所有数据会丢失。7. FastAPI 接口实现7.1 接口设计接口设计要遵循一个原则用户只拿到必要信息服务端保留答案和分数。接口方法请求参数返回内容/api/quizGET无题目 ID、标题、两个选项文本/api/answerPOSTquestion_id, selected_id是否正确、正解选项 ID、当前分数/GET无前端页面没必要把“生成题目”和“获取正解”放在同一个接口里那样用户可以直接通过网络请求看到答案字段。7.2 核心接口代码实现app.pyimport os from dotenv import load_dotenv from fastapi import FastAPI, HTTPException from fastapi.staticfiles import StaticFiles from quiz.ai_generator import generate_ai_extract from quiz.models import QuizAnswer, QuizQuestion from quiz.quiz_service import QuizRound, QuizStore from quiz.wikipedia_client import ( fetch_random_article_summary, normalize_extract, ) load_dotenv() app FastAPI(titleAI or Not Quiz) store QuizStore() app.mount(/static, StaticFiles(directorystatic), namestatic) app.get(/api/quiz) def create_quiz(): try: article fetch_random_article_summary() human_text normalize_extract(article[extract]) ai_text generate_ai_extract(article[title], human_text) except Exception as exc: # 这里要记录完整异常到日志给用户的提示保持简洁 raise HTTPException(status_code503, detailf题目生成失败: {exc}) round_obj QuizRound(article[title], human_text, ai_text) store.rounds[round_obj.question_id] round_obj return round_obj.public_view() app.post(/api/answer) def submit_answer(answer: QuizAnswer): round_obj store.rounds.get(answer.question_id) if round_obj is None: raise HTTPException(status_code404, detail题目不存在或已过期) correct round_obj.check(answer.selected_id) if correct: store.scores[default_user] store.scores.get(default_user, 0) 1 return { correct: correct, answer_id: round_obj.answer_id, score: store.scores.get(default_user, 0), }注意generate_ai_extract和fetch_random_article_summary都是同步阻塞调用FastAPI 的异步事件循环会被阻塞。学习项目里问题不大但并发上来后要改成async或放入线程池。这一点在最佳实践里会再强调。7.3 请求与响应示例创建题目curl http://127.0.0.1:8000/api/quiz响应示例{ question_id: 3f9a1c2e8b6d4f0a9e7c1a2b3c4d5e6f, title: Machine learning, options: [ { id: A, text: Machine learning is a field ... }, { id: B, text: Machine learning (ML) is a field of study ... } ] }提交答案curl -X POST http://127.0.0.1:8000/api/answer \ -H Content-Type: application/json \ -d {question_id: 3f9a1c2e8b6d4f0a9e7c1a2b3c4d5e6f, selected_id: B}响应示例{ correct: true, answer_id: B, score: 1 }8. 前端交互页面8.1 页面结构前端保持极简负责三件事拉取题目、展示两个选项、提交答案。static/index.html核心结构如下!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleAI or Not Quiz/title link relstylesheet href/static/style.css /head body main h1哪一段文字是 AI 写的/h1 p idtitle加载中.../p div idoptions/div p idresult/p button idnext styledisplay:none;下一题/button /main script src/static/app.js/script /body /html8.2 用 fetch 调用接口static/app.js实现加载题目和提交答案const titleEl document.getElementById(title); const optionsEl document.getElementById(options); const resultEl document.getElementById(result); const nextBtn document.getElementById(next); let currentQuestionId null; async function loadQuiz() { resultEl.textContent ; nextBtn.style.display none; optionsEl.innerHTML ; const resp await fetch(/api/quiz); const data await resp.json(); currentQuestionId data.question_id; titleEl.textContent data.title; data.options.forEach((opt) { const card document.createElement(button); card.className option; card.textContent opt.text; card.onclick () submitAnswer(opt.id); optionsEl.appendChild(card); }); } async function submitAnswer(selectedId) { const resp await fetch(/api/answer, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ question_id: currentQuestionId, selected_id: selectedId, }), }); const data await resp.json(); if (data.correct) { resultEl.textContent 答对了正解是 ${data.answer_id}当前得分 ${data.score}; } else { resultEl.textContent 答错了正解是 ${data.answer_id}当前得分 ${data.score}; } nextBtn.style.display block; } nextBtn.onclick loadQuiz; loadQuiz();这里把选项渲染成按钮点击后立即提交。更完整的版本应该在高亮选项后再提交避免用户误触。8.3 反馈与下一题逻辑判题之后要同时展示“是否正确”和“正解是哪一个”否则用户只能盲目猜。下一题按钮复用loadQuiz重新请求新题。学习阶段可以不考虑用户体系分数默认记在内存里的固定 key 上。需要区分用户时可以引入 session_id 或 JWT把分数绑定到具体用户。9. 运行验证与结果质量分析9.1 启动与本地验证启动服务uvicorn app:app --reload --port 8000启动成功的标志是控制台输出类似Uvicorn running on http://127.0.0.1:8000。浏览器打开http://127.0.0.1:8000/应该能看到页面。建议按下面顺序做冒烟测试单独请求维基百科接口确认外网可访问。单独调用generate_ai_extract确认模型 Key 有效。请求/api/quiz确认返回两个选项且文本不重复。提交一个错误答案和一个正确答案确认判定正确。刷新页面连续玩 10 题确认没有明显报错。9.2 curl 验证接口返回直接依赖浏览器调试有时看不到完整报错配合 curl 更高效curl -i http://127.0.0.1:8000/api/quiz用-i能同时看到 HTTP 状态码和响应体。如果返回 503说明题目的某个上游服务出了问题查看服务端日志定位是维基百科超时还是模型返回空。9.3 判断题目质量的检查清单“能跑”不等于“题目能玩”。每生成一题建议按以下清单检查[ ] 两段文本长度是否接近避免用户靠长度猜测。[ ] AI 文本是否出现“作为一个 AI”“我不能”这类提示词痕迹。[ ] AI 文本是否复述了人类文本的大段原句。[ ] 词条标题是否过于生僻用户完全无法判断。[ ] 随机词条是否重复出现游戏后期是否疲劳。[ ] 模型返回是否偶发截断截断后选项是否残缺。其中“AI 文本是否复述原句”是最常出现的问题。如果复述严重说明提示词的“不要复制参考文本”约束不够需要加强或者在生成后用文本相似度做过滤。10. 常见问题与排查路径10.1 常见问题速查表问题现象常见原因检查方式处理建议/api/quiz 返回 503维基百科接口超时或模型 Key 无效单独 curl 维基百科接口单独调用模型 SDK 示例检查网络、密钥、模型名加超时和重试两个选项文本几乎一样模型复制了参考文本对比两段文本相似度查看提示词在提示词中加强“独立写作”约束或过滤高相似度结果AI 文本明显带“AI 痕迹”提示词没有禁用口语化表达查看生成文本增加禁用句式提高 temperature模型返回被截断max_tokens 太小查看响应里的 finish_reason调大 max_tokens或检测截断后重新生成刷新页面后分数丢失分数存在内存检查存储位置学习阶段可接受生产环境改用 Redis 或数据库请求 /api/quiz 很慢模型生成是同步阻塞调用观察日志耗时改成异步生成或预生成题目随机词条重复出现维基百科随机接口没有去重记录最近题目标题维护一个已用过标题的队列临时跳过10.2 从现象到根因的排查顺序遇到问题不要先看最后一行堆栈按下面顺序排查确认请求本身没问题路径和参数正确。确认上游依赖可用维基百科接口、模型接口。确认环境变量已加载.env文件路径正确。查看服务端完整日志识别是网络报错、鉴权报错还是模型响应格式问题。用最小脚本单独复现问题隔离模块。如果只是偶发问题考虑超时和重试策略而不是改提示词。例如“题目生成失败”是一个非常粗的报错必须把原始异常记录到日志里。给用户的 detail 保持简洁但服务端日志要能定位到是httpx.ConnectTimeout还是openai.AuthenticationError。11. 生产化最佳实践与扩展方向11.1 工程化建议清单学习项目跑通后如果要把它做成真正可用的服务建议对照下面的清单补强[ ] 模型 Key 统一从环境变量或密钥管理系统读取不要提交到代码仓库。[ ] 维基百科和模型接口都设置超时和重试并区分“不可重试”的错误类型。[ ] 题目和答案不要混在同一个存储里答案字段不返回给前端。[ ] 用异步方式处理耗时操作避免阻塞 FastAPI 事件循环。[ ] 建立基本日志体系记录请求耗时、上游错误、生成文本长度。[ ] 对生成文本做基础过滤检测空文本、敏感词、超长文本。[ ] 为已生成的题目增加缓存减少重复请求模型。[ ] 增加简单限流防止随机词条接口和模型接口被高频调用。其中“答案字段不返回给前端”是这类产品最容易踩的坑。前端调试时可以查看网络响应如果响应里带is_ai: true玩家直接看开发者工具就能作弊。11.2 可以扩展的方向这个项目可以从一个答题游戏扩展成多个方向的工程实践标注平台把“用户判断是否正确”收集起来形成人工标注数据集用于训练或评估文本分类器。模型评测工具固定一批维基百科词条让不同模型生成同主题文本比较哪个模型更容易被用户识别为 AI。AI Agent 能力测试把“生成百科简介”封装成一个 Agent 工具调用观察不同提示词策略下的效果差异。AI 幻觉观察器对生成文本做事实校验标记出与维基百科原文不一致的地方帮助理解大模型的幻觉问题。前后端功能增强加入用户登录、排行榜、题目历史、多语言支持。如果希望把生成和判题做成更通用的能力可以借鉴 Spring AI 这类框架的模块化思路把模型供应商、提示词模板和任务链路解耦。小项目不需要一步到位但模块边界一开始就要清楚。11.3 一点学习建议把“AI or Not”做完真正值得记住的不是代码本身而是几个判断第一AI 应用不能只关心“模型能不能生成”更要关心生成结果如何进入业务链路、如何验证、如何兜底。第二两段文本对比是一个非常直观的评测方式它比“AI 检测率 90%”这类数字更能说明问题。第三一个最小闭环比一个宏伟架构更有学习价值先把 500 行代码跑通再去考虑异步、缓存、Kubernetes 这些生产化能力。如果自己已经能独立完成这个项目下一步可以试着不依赖维基百科换成自己的文档库或博客语料做一套“识别 AI 改写内容”的内部工具。你会发现核心难点从“接 API”变成了“定义什么才算 AI 风格”那才是这个领域真正值得深挖的地方。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表