为了降低论文AIGC率,我手搓了一个”双模式润色“神器

0 阅读14分钟

从一个提示词脚本到双模式产品——AI Academic Polisher 的设计路径

项目地址:AI Academic Polisher
License: MIT
Disclaimer: 本工具仅供辅助学术写作与语言润色使用,旨在帮助作者提升论文的表达质量与可读性。使用者应确保最终提交的学术成果符合所在机构的学术诚信规范。


目录


§ 1 起点:一个问题和一个脚本

今年寒假帮一个学弟看毕业论文。他的初稿逻辑没什么大问题,但通篇读下来"机器味"很重——冗长的从句堆砌、生硬的连接词、千篇一律的"总而言之"和"不可否认的是"。这种文风在学术写作中很常见:要么是过度依赖基础翻译工具,要么是用 AI 辅助起草后没有做人工润色,导致表达停留在"语法正确但不像人写的"阶段。

我看了一圈市面上的润色工具,大多是套壳收费,且无法保留复杂的 Word 排版。于是我自己写了一套基于学术语料特征的提示词方案,在终端里跑通了——把生硬的句子改写成更符合人类学术写作习惯的表达,保留原意,让文章读起来像是有经验的研究者写的。一个令人惊喜的副产品是:因为表达变得自然流畅,常规的文本检测器也不再将其误判为低质量的 AIGC 生成物。

但一个终端脚本只能自己用。接下来的几个月,这个东西经历了三次形态变化:

  1. 自己用:一个 Python 脚本,手动粘贴文本,够了
  2. 朋友用:需要 Web 界面,部署在服务器上,几个人共用
  3. 陌生人用:需要桌面版,双击就能跑,不用装环境、不用信任别人的服务器

02_product_evolution.png 每一步对产品形态的要求完全不同。核心命题变成了:怎么让"提示词调得好"这件事,变成一个可复用、可交付的产品?


§ 2 数据实证:润色效果验证

工具好不好用,数据说话。以下是使用 gemini-2.5-pro-preview 模型润色后,提交到主流写作质量评估平台的测试结果(2025 年 4 月)

PaperPass PaperPass数据报告.png

维普 维普数据报告.png

朱雀 朱雀AI检查数据报告2.png

需要说明的是:这些数字反映的是文本自然度的提升,而不是某种"对抗检测"的结果。当一段文字的表达从生硬模板化变成自然流畅时,检测器不再将其标记为机器生成,这是润色质量的自然体现。

换个角度理解:如果一个母语者用自己的学术写作习惯重新表述同样的观点,检测器同样不会标记。我们的提示词做的事情,本质上就是模拟这个"母语者重新表述"的过程。


§ 3 第一个设计决策:一套代码还是两个项目

Web 版做完后,桌面版的需求摆上来了。摆在面前的选择:

方案 A:用 Electron / Tauri 做一个独立桌面端。

好处是桌面端可以有原生体验。问题是:我的核心逻辑全在 Flask 后端——任务队列、文件解析、AI 调用、进度推送——如果桌面端用另一套技术栈,这些逻辑要么重写一遍,要么通过 IPC 桥接调用 Python,两条路都意味着双倍维护成本。

方案 B:Flask 既当 Web 后端,也当桌面后端。

桌面版本质上就是一个本地跑的 Flask 服务 + 一个打包进去的前端。用户双击 EXE,Flask 在本地启动,浏览器打开 localhost。听起来简陋,但对于一个工具类产品来说,用户并不在意后端是什么——他们在意的是"双击能用"和"数据不出本机"。

我选了方案 B。代价是放弃了原生窗口体验,换来的是一套代码、一套逻辑、一个仓库

顺带一提,后端框架选 Flask 而不是 FastAPI,核心原因是 Flask 在 Windows 桌面环境下的依赖链更干净——PyInstaller 打包时不需要处理 uvicorn/uvloop 等在 Windows 上兼容性不佳的异步运行时依赖。对于一个要打成 EXE 分发的项目,这个差异很实际。

但这带来了一个新问题:Server 模式依赖 Redis + MySQL + 独立 Worker 进程,Desktop 模式不可能要求用户装这些东西。怎么办?

设计原则确立了:上层业务代码不应该知道自己跑在哪里。 基础设施的差异,在启动时就要解决掉。

双模式架构设计图.png

双模式架构设计图


§ 4 基础设施的"替身":用内存模拟分布式组件

问题本质很清楚:Desktop 模式没有 Redis,没有独立 Worker 进程,但 Processor 里写的 redis_client.publish()task_queue.enqueue() 不能改。

解题思路不是"去掉依赖",而是**"提供等价替代"**。

模式自动检测

项目通过 DEPLOY_MODE 环境变量控制运行模式。默认值是 auto——Windows 上自动进入 Desktop 模式,Linux 上自动进入 Server 模式。用户不需要手动配置任何东西。

MemoryRedis:在内存里造一个 Redis

目标很明确:实现 redis-py 的方法子集,让上层代码零感知切换。核心是一个带锁的内存字典,加上用 queue.Queue 模拟的 Pub/Sub——每个订阅者持有一个 Queue,发布者往所有订阅者的 Queue 里塞消息。接口签名和 redis-py 完全一致。

MemoryQueue:用守护线程替代 RQ Worker

Server 模式下 RQ Worker 是独立进程,通过 Redis 拿任务。Desktop 模式下没有独立进程,但又不能阻塞主线程。方案是用一个守护线程跑任务循环,从内存队列里取任务执行。

这个思路的边界

"替身"模式在单机单用户场景下完美工作。但它有明确的边界:

  • 不支持多进程:MemoryRedis 是进程内的,多个进程看不到彼此的数据
  • 不支持持久化:进程退出,数据就没了
  • 不支持分布式:没有网络通信能力

对于 Desktop 模式来说,这些限制恰好不是问题——本来就是单用户、单进程、用完即走。


§ 5 异步任务的选型路径

润色一篇论文可能要几分钟,不能让用户盯着一个转圈的页面干等。需要异步任务队列。

Celery vs RQ

很多人第一反应是 Celery。我犹豫的点:

  • 配置复杂:broker、backend、worker 参数,对于一个学术工具来说太重了
  • 依赖链长:Celery + RabbitMQ(或 Redis)+ 各种序列化配置
  • 耦合度高@task 装饰器和代码结构绑定较深,不利于双模式切换

RQ 的优势在于:依赖只有 Redis,API 三行代码就能上手,Worker 是纯 Python 进程,出问题看日志就能定位。

选型的核心逻辑:在"学术工具"这个语境下,简洁性比功能全面性更重要。 我不需要定时任务、不需要任务链、不需要多 broker——我只需要"把一个函数扔到后台执行"。

任务派发的演进

一开始 process_task 里是一堆 if task_type == "text": ...。三种任务类型(文本、DOCX、PDF)写在一起,改一个怕影响另一个。

重构的触发点是加 PDF 支持的时候——我发现自己在复制粘贴 DOCX 处理器的代码改几行。这是明确的信号:该抽象了。

最终用工厂方法 + 模板方法:工厂根据类型返回对应 Processor,模板方法定义标准流程(初始化 → 处理 → 推送完成),子类只实现 process()。新增文件类型只需要写一个类。

取消机制

长任务必须能取消。方案是通过 Redis 写入一个 cancel 信号,Worker 每处理完一个段落就检查一次。检测到取消就提前退出,不浪费后续 API 调用。

因为用的是 redis_client.exists() 这个通用接口,Desktop 模式下 MemoryRedis 的同名方法自动生效——取消逻辑也是双模式通用的。


§ 6 实时反馈的选型路径

用户体验目标:润色进度要实时可见。一篇 50 段的论文,处理到第几段、当前段的结果是什么,都要即时推送到前端。

三条路

方案适合场景不适合的原因
轮询状态变化不频繁延迟高,浪费请求,50 段论文要轮询几百次
WebSocket需要双向通信我不需要客户端通过同一连接发命令,协议更复杂
SSE服务器单向推送—— 完美匹配

为什么 SSE 是天然适配

  • HTTP 原生:不需要额外协议升级,Nginx 配置简单
  • 浏览器自动重连EventSource API 内置断线重连,前端代码极简
  • 单向就够了:我的需求就是服务器往客户端推进度,不需要反向通信

后端通过 Redis Pub/Sub 监听任务进度,收到消息就通过 SSE 推给前端。Desktop 模式下 MemoryRedis 的 Pub/Sub 接口一致,推送逻辑零修改。

部署层面的认知盲区

SSE 上线后遇到一个诡异问题:本地开发一切正常,部署到服务器后进度不是实时推送,而是攒一大块才到前端。

原因是 Nginx 默认缓冲后端响应。流式数据被 Nginx 攒够一定量才转发。解决方案是对 SSE 路由显式关闭 proxy_buffering、关闭 gzip、设置足够长的 proxy_read_timeout

另一个容易忽略的点:Gunicorn 的 sync worker 会被 SSE 长连接阻塞,必须用 gthread(线程模型)。一个 SSE 连接占一个 worker 的话,4 个 worker 只能同时服务 4 个用户。


§ 7 长文档处理的并发策略

DOCX 论文动辄上百段,每段调一次 AI。串行处理一篇 50 页的论文要十几分钟,用户体验不可接受。

矛盾点

要快(并发调 API),但要有序(段落顺序不能乱)。

方案:索引化 + 线程池

思路很直接:预先分配一个和段落数等长的结果数组,每个并发任务知道自己的索引位置。哪个先完成就先写入对应位置,最终数组的顺序天然正确。

ThreadPoolExecutor 而不是 asyncio 的原因:OpenAI SDK 的同步版本基于 requests,线程池对 IO 密集型任务已经够用。5 个并发就能把耗时从 15 分钟压到 3 分钟左右。经过多轮提示词热更新的迭代,系统目前能够稳定地将充满"AI 味"的生硬文本段落优化为自然流畅的学术表达。

并发度的上限

并发度不是越高越好。超过 5-8 个并发,API 的速率限制就会触发 429 错误,反而要重试,总耗时可能更长。并发度的上限不是技术决定的,是 API 速率限制决定的。

取消与并发的协作

每个段落处理前检查 cancel 信号。一旦检测到取消,当前正在执行的几个并发任务会自然完成(因为 API 调用已经发出去了),但后续段落不再提交。这是一个"尽力而为"的取消——不会中断正在进行的网络请求,但能避免浪费后续的 API 额度。


§ 8 提示词作为"可热更新的配置"

设计动机

调试阶段最痛苦的事情:改了一个提示词的措辞,要重启 Worker 才能生效。一篇论文调 10 次提示词就要重启 10 次,每次重启还要等任务队列清空。

方案:提示词是文件,不是代码

提示词以 Markdown 文件存放在 prompts/ 目录。启动时不加载到内存,每次任务执行时按需读取。改完文件,下一个任务自动用新版本,不需要重启任何东西。

性能不是问题——Linux 文件系统的页缓存会处理重复 IO,实测开销可以忽略。

策略系统

多套润色策略(标准 / 严格 / 自定义)注册在一个字典里,每个策略对应一组 Markdown 文件。新增策略只需要:写一个 Markdown 文件,在字典里加一行配置。

前端通过配置组件让用户切换策略,选择后立即生效。

举个具体的例子:我可以在 cn_standard.md 中加一条规则——"严禁使用'总而言之'、'不可否认的是'等已被 AI 滥用的陈词滥调,将其替换为更克制的学术过渡词"。保存文件,刷新页面,下一个任务就会立刻应用这条新规则,不需要重启任何服务。非技术用户说"我想让它别把'其次'改成'第二点'",改一行 Markdown 就解决了。

改文件 = 改行为,无需懂代码。 这对于一个需要频繁迭代提示词的项目来说,是生产力的质变。


§ 9 踩坑复盘(精选)

PyInstaller 隐式依赖

"我本地能跑"不等于"打包后能跑"。PyInstaller 静态分析 import 链,但很多库用动态导入(importlib__import__),分析不到就不会打包进去。

典型例子:sqlalchemy.dialects.sqlite 是运行时根据连接字符串动态加载的,不写进 hiddenimports 就会在用户机器上报 ModuleNotFoundError

教训:打包测试必须在干净环境做。 开发机上全局装了一堆包,会掩盖打包遗漏的问题。

SQLite 线程安全

Desktop 模式下 Flask 主线程和 MemoryQueue 的守护线程同时访问 SQLite。SQLite 默认不允许跨线程共享连接。

这个问题在开发时不容易复现——请求量小的时候几乎不会并发访问。但多任务并发时会偶发报错,排查了相当长时间才定位到。

Redis Key 散落

一开始 Redis key 硬编码在十几个文件里。改一次命名规范要全局搜索,容易漏改导致诡异 bug——比如 Publisher 写的 key 和 Subscriber 监听的 key 对不上,任务就永远收不到完成通知。

后来抽成了 RedisKeyManager,所有 key 的生成逻辑集中在一个类里。任何分布式系统中的 key 都应该集中管理——这是我下一个项目从第一天就会做的事。


§ 10 回顾:设计原则的提炼

做完这个项目,有三条设计原则是我以后会反复用的:

1. 把基础设施当依赖注入,不要当前提假设。

代码不应该假设"一定有 Redis"或"一定有 MySQL"。把这些当成可替换的依赖,在启动时注入正确的实现。这样做的好处不只是支持双模式——测试时也可以注入 mock 实现,CI 环境不需要起一堆容器。

2. 选型要看场景匹配度,不要看功能全面性。

Celery 比 RQ 功能强大得多,但在"学术工具"这个场景下,RQ 的简洁性才是真正的优势。WebSocket 比 SSE 能力更强,但在"单向推送"这个需求下,SSE 的轻量才是正确选择。选型不是选"最强的",是选"最匹配的"。

3. "能跑"和"可交付"之间的距离,往往是工程设计填补的。

提示词脚本第一天就"能跑"了。但从"能跑"到"别人能用",中间隔着环境检测、错误处理、进度反馈、任务取消、打包部署……这些不是功能,是工程。好的工程设计不会让用户感知到它的存在,但没有它,产品就只是一个"在我机器上能跑"的脚本。


本项目已在 GitHub 完全开源。如果你也受够了每次修改论文提示词都要重启脚本的痛苦,或者需要一个完全本地化、保护数据隐私的润色工具,欢迎下载体验 Desktop 模式。如果这个架构思路对你有启发,求一个 Star 鼓励。也欢迎提交 PR 补充更多的润色策略。