从一个提示词脚本到双模式产品——AI Academic Polisher 的设计路径
项目地址:AI Academic Polisher
License: MIT
Disclaimer: 本工具仅供辅助学术写作与语言润色使用,旨在帮助作者提升论文的表达质量与可读性。使用者应确保最终提交的学术成果符合所在机构的学术诚信规范。
目录
- § 1 起点:一个问题和一个脚本
- § 2 数据实证:润色效果验证
- § 3 第一个设计决策:一套代码还是两个项目
- § 4 基础设施的"替身":用内存模拟分布式组件
- § 5 异步任务的选型路径
- § 6 实时反馈的选型路径
- § 7 长文档处理的并发策略
- § 8 提示词作为"可热更新的配置"
- § 9 踩坑复盘(精选)
- § 10 回顾:设计原则的提炼
§ 1 起点:一个问题和一个脚本
今年寒假帮一个学弟看毕业论文。他的初稿逻辑没什么大问题,但通篇读下来"机器味"很重——冗长的从句堆砌、生硬的连接词、千篇一律的"总而言之"和"不可否认的是"。这种文风在学术写作中很常见:要么是过度依赖基础翻译工具,要么是用 AI 辅助起草后没有做人工润色,导致表达停留在"语法正确但不像人写的"阶段。
我看了一圈市面上的润色工具,大多是套壳收费,且无法保留复杂的 Word 排版。于是我自己写了一套基于学术语料特征的提示词方案,在终端里跑通了——把生硬的句子改写成更符合人类学术写作习惯的表达,保留原意,让文章读起来像是有经验的研究者写的。一个令人惊喜的副产品是:因为表达变得自然流畅,常规的文本检测器也不再将其误判为低质量的 AIGC 生成物。
但一个终端脚本只能自己用。接下来的几个月,这个东西经历了三次形态变化:
- 自己用:一个 Python 脚本,手动粘贴文本,够了
- 朋友用:需要 Web 界面,部署在服务器上,几个人共用
- 陌生人用:需要桌面版,双击就能跑,不用装环境、不用信任别人的服务器
每一步对产品形态的要求完全不同。核心命题变成了:怎么让"提示词调得好"这件事,变成一个可复用、可交付的产品?
§ 2 数据实证:润色效果验证
工具好不好用,数据说话。以下是使用 gemini-2.5-pro-preview 模型润色后,提交到主流写作质量评估平台的测试结果(2025 年 4 月)
PaperPass
维普
朱雀
需要说明的是:这些数字反映的是文本自然度的提升,而不是某种"对抗检测"的结果。当一段文字的表达从生硬模板化变成自然流畅时,检测器不再将其标记为机器生成,这是润色质量的自然体现。
换个角度理解:如果一个母语者用自己的学术写作习惯重新表述同样的观点,检测器同样不会标记。我们的提示词做的事情,本质上就是模拟这个"母语者重新表述"的过程。
§ 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 模式不可能要求用户装这些东西。怎么办?
设计原则确立了:上层业务代码不应该知道自己跑在哪里。 基础设施的差异,在启动时就要解决掉。
双模式架构设计图
§ 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 配置简单
- 浏览器自动重连:
EventSourceAPI 内置断线重连,前端代码极简 - 单向就够了:我的需求就是服务器往客户端推进度,不需要反向通信
后端通过 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 补充更多的润色策略。