我用什么技术做了TLDR Scholar------AI论文速读产品完整技术栈拆解
TLDR Scholar:AI论文速读工具地址 https://www.tldrscholar.cn,支持上传PDF/Word文档,30秒提取核心发现,输出学科领域、研究方法、核心发现、可重现指标等结构化摘要。
作为一个从移动端WebView兼容开发转型的独立开发者,我用两个月时间从零搭建了一个完整的AI论文速读产品。这篇文章不讲情怀,不聊愿景,只聊技术------我会完整拆解TLDR Scholar的技术选型、核心实现和踩过的坑。
如果你也在做或者想做类似的AI应用,这篇可能是你见过的最接地气的技术复盘。
一、技术选型思考:为什么是Next.js而不是Vue
前端框架选型是第一个要做的决定。我最终选择了 Next.js 14 + TypeScript 而不是当时更熟悉的Vue3生态,这个决策过程值得展开说说。
1.1 选Next.js的核心原因:AI时代的开发效率
放弃Vue转向Next.js,我经历了三个月的纠结期。真正让我下决心的是一次实验:
我用Cursor同时让Claude生成同一个功能模块,用Vue写花了40分钟调试各种类型错误,用React/Next.js只用了15分钟且一次跑通。
这不是Vue本身的问题,而是AI对React生态的理解深度远超过Vue。GitHub上React项目数量是Vue的数倍,AI训练数据中React代码占绝对多数。当你用AI辅助开发时,这个差异会直接转化为开发效率。
对于独立开发者来说,时间就是最大的成本。选择AI友好度更高的技术栈,本质上是在选择更高的杠杆率。
1.2 为什么不是纯Node.js后端
TLDR Scholar的核心能力是调用大模型API处理论文,这个后端逻辑其实不复杂。技术上完全可以:
- 纯前端:Next.js API Routes处理一切
- 或前后端分离:前端 + Express/Koa后端
我最终选择了Next.js全栈方案,原因是:
- 部署简单:Vercel一键部署,不用折腾Nginx配置
- 冷启动快:Serverless模式下,请求来了才启动
- SSR/SSR灵活切换:落地页用SSR保证SEO,核心功能用CSR
- 一套代码库:前端后端同一个仓库,维护成本低
对于日均几百到几千请求的独立开发者产品,Serverless模式的响应延迟完全可以接受,而运维成本几乎为零。
1.3 最终技术栈一览
前端:Next.js 14 (App Router) + TypeScript + Tailwind CSS
UI组件:shadcn/ui + Radix Primitives
状态管理:Zustand(轻量级,比Redux更适合简单场景)
后端:Next.js API Routes (Serverless Functions)
部署:Vercel
域名:Cloudflare + 国内备案
没有用Redis,没有用独立数据库,没有用消息队列------不是不会,是不需要。
二、AI接口集成:从调通API到稳定输出
AI接口集成是这类产品的核心竞争力,也是最容易踩坑的地方。我会从调用方式、Prompt设计和流式输出三个维度展开。
2.1 大模型API调用:多供应商策略
TLDR Scholar目前接入了多个大模型供应商,调用层做了统一的抽象:
typescript
// 统一的AI调用接口
interface AIProvider {
generateSummary(paperText: string, options?: GenerationOptions): Promise<Summary>;
streamGenerateSummary(paperText: string, options?: GenerationOptions): AsyncGenerator<string>;
}
// 不同供应商的实现
class DeepSeekProvider implements AIProvider { /* ... */ }
class OpenAIProvider implements AIProvider { /* ... */ }
class QwenProvider implements AIProvider { /* ... */ }
// 调用层统一调度
class AIService {
private providers: AIProvider[];
private currentIndex = 0;
async generate(paperText: string): Promise<Summary> {
// 轮询策略:优先用第一个,失败则切换
for (let i = 0; i < this.providers.length; i++) {
const provider = this.providers[(this.currentIndex + i) % this.providers.length];
try {
return await provider.generateSummary(paperText);
} catch (error) {
console.error(`Provider ${provider.name} failed:`, error);
if (i === this.providers.length - 1) throw error;
}
}
throw new Error('All providers failed');
}
}
关键经验:不要把所有鸡蛋放在一个API供应商篮子里。DeepSeek曾经有过服务不稳定的时期,多供应商策略让我成功扛过了那段时间。
2.2 Prompt设计:结构化输出比想象的重要
论文速读的核心输出是结构化的摘要,包含学科领域、研究方法、核心发现、可重现指标四个维度。我的Prompt迭代经历了三个版本:
v1.0 - 简单粗暴
请总结这篇论文的核心内容,包括:研究领域、研究方法、主要发现。
结果:输出格式混乱,有时四个维度齐全,有时只有两个,完全不可控。
v2.0 - 明确要求JSON
typescript
const prompt = `你是一个学术论文分析专家。请分析以下论文,输出一句严谨的归纳总结,格式如下:
{
"discipline": "学科领域",
"methodology": "研究方法",
"coreFinding": "核心发现",
"reproducibleMetrics": "可重现指标"
}
论文内容:
${paperText}
要求:
1. 每个字段必须填写,不可为空
2. 一句严谨的归纳总结
3. 可重现指标必须是论文中明确给出的数据指标
`;
结果:格式稳定了,但输出质量参差不齐------有时"核心发现"写得像流水账,有时"可重现指标"列一堆无关数字。
v3.0 - 结构化Prompt + Few-shot示例
typescript
const prompt = `你是一个严谨的学术论文分析专家。请为以下论文生成一句学术性归纳总结。
【输出格式】
学科领域: [用一句话描述论文所属的学科领域]
研究方法: [用一句话描述论文采用的研究方法]
核心发现: [用一句话描述论文最重要的研究发现,100字以内]
可重现指标: [列出论文中明确给出的可重现量化指标,如F1值、准确率、参数量等]
【示例1】
论文内容: 本文提出了一种新的Transformer变体,在ImageNet上达到了92.3%的准确率...
输出:
学科领域: 计算机视觉-图像分类
研究方法: 消融实验 + 与SOTA模型对比
核心发现: 通过改进注意力机制,模型在保持参数量不变的情况下,准确率提升2.1个百分点
可重现指标: ImageNet Top-1准确率92.3%、参数量28M、FLOPs 4.2G
【示例2】
论文内容: [另一篇论文摘要]...
输出:
学科领域: [根据内容推断]
...
【待分析论文】
${paperText}
请严格按照上述格式输出,不要添加任何解释性文字。`;
v3.0的改进点:
- 给出了Few-shot示例,让模型理解"严谨学术归纳"的标准
- 明确了字数限制,避免流水账
- 强调"不要添加任何解释性文字",减少后处理复杂度
2.3 流式输出:用户等待体验的关键
论文分析通常需要10-30秒,在AI领域这已经算"长等待"了。如果等30秒才看到结果,用户会觉得产品卡死了。
解决方案是流式输出(Streaming),让用户实时看到分析进度。
后端实现(Next.js API Routes):
typescript
import OpenAI from 'openai';
const openai = new OpenAI({ apiKey: process.env.DEEPSEEK_API_KEY });
export async function POST(req: Request) {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
const stream = await openai.chat.completions.create({
model: 'deepseek-chat',
messages: [{ role: 'user', content: prompt }],
stream: true,
});
for await (const chunk of stream) {
const text = chunk.choices[0]?.delta?.content || '';
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ text })}\n\n`));
}
controller.enqueue(encoder.encode('data: [DONE]\n\n'));
}
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
}
前端实现(React Hooks封装):
typescript
function useStreamSummary() {
const [content, setContent] = useState('');
const [isLoading, setIsLoading] = useState(false);
const abortControllerRef = useRef<AbortController | null>(null);
const startStream = async (paperText: string) => {
setIsLoading(true);
setContent('');
abortControllerRef.current = new AbortController();
try {
const response = await fetch('/api/analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ paperText }),
signal: abortControllerRef.current.signal,
});
const reader = response.body?.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (reader) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') {
setIsLoading(false);
return;
}
try {
const { text } = JSON.parse(data);
setContent(prev => prev + text);
} catch {}
}
}
}
} catch (error) {
if (error instanceof Error && error.name !== 'AbortError') {
console.error('Stream error:', error);
}
} finally {
setIsLoading(false);
}
};
const cancel = () => {
abortControllerRef.current?.abort();
setIsLoading(false);
};
return { content, isLoading, startStream, cancel };
}
这样用户可以看到文字"一个字一个字"地蹦出来,等待体验大大提升。
三、论文解析:PDF提取的曲折之路
论文解析是另一个技术难点。用户上传的是PDF文件,需要从中提取可读的文本内容,再喂给大模型。
3.1 PDF解析方案对比
我测试过几种主流方案:
| 方案 | 优点 | 缺点 |
|---|---|---|
| pdf.js(浏览器端) | 无需服务器资源,隐私好 | 提取效果差,表格/公式乱码 |
| pdf-parse(Node.js) | 简单易用 | 对扫描版PDF无能为力 |
| TextIn/腾讯云文档 | 提取效果好,支持表格 | 有免费额度限制,需付费 |
| MinerU(开源) | 提取质量高,开源可控 | 部署复杂,依赖PyTorch |
对于学术论文这个场景,表格和公式的提取质量至关重要------一篇没有表格数据的论文摘要是不完整的。
我最终选择了TextIn文档解析API作为主力方案,原因:
- 百页文档约1.5秒完成解析,速度可接受
- 每天1000页免费额度,个人使用完全够用
- 表格、公式、章节结构都能正确识别
3.2 代码实现
typescript
// 论文上传接口
import { NextRequest, NextResponse } from 'next/server';
import FormData from 'form-data';
import fetch from 'node-fetch';
export async function POST(req: NextRequest) {
const formData = await req.formData();
const file = formData.get('file') as File;
if (!file) {
return NextResponse.json({ error: 'No file uploaded' }, { status: 400 });
}
// 检查文件大小限制(30MB)
if (file.size > 30 * 1024 * 1024) {
return NextResponse.json({ error: 'File too large' }, { status: 400 });
}
try {
// 调用TextIn文档解析API
const textInResponse = await fetch('https://api.textin.com/api/v1/parse', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.TEXTIN_API_KEY}`,
'X-APIVersion': '2023-11-01',
},
body: file,
});
if (!textInResponse.ok) {
throw new Error(`TextIn API error: ${textInResponse.status}`);
}
const result = await textInResponse.json();
// 提取纯文本内容(传给大模型)
const paperText = result.data.content.markdown || result.data.content.text;
return NextResponse.json({ text: paperText, pages: result.data.pages });
} catch (error) {
console.error('PDF parsing error:', error);
return NextResponse.json(
{ error: 'Failed to parse PDF' },
{ status: 500 }
);
}
}
3.3 踩坑:PDF编码问题
一个典型的坑是PDF编码问题。学术论文经常使用特殊的字体编码,有些还是作者自定义的字符映射。
症状:提取出来的文本看起来正常,但关键术语全是乱码------"transformer"变成了"t栅rformêr"。
解决方案:
- 先检测PDF是否包含嵌入字体
- 如果有自定义字体,使用TextIn的"高精度模式"
- 关键术语设置后处理映射表
typescript
// 后处理:修复常见编码问题
function postProcessText(text: string): string {
const encodingFixes: Record<string, string> = {
'栅': 'a',
'ê': 'e',
'ÿ': 'y',
// ... 根据实际提取结果添加
};
let fixed = text;
for (const [wrong, correct] of Object.entries(encodingFixes)) {
fixed = fixed.replace(new RegExp(wrong, 'g'), correct);
}
return fixed;
}
四、部署方案:从本地到生产的完整链路
4.1 部署架构
用户浏览器
↓ HTTPS
Cloudflare CDN
↓ 静态资源
Vercel (Next.js应用)
↓ API调用
大模型API (DeepSeek/OpenAI/Qwen)
↓ 文档解析
TextIn API
选择Cloudflare + Vercel组合的原因:
- Cloudflare:国内访问速度快,免费的SSL证书,CDN缓存
- Vercel:Next.js官方支持,Serverless自动扩缩容,全球CDN
4.2 域名和备案
这是国内独立开发者必须面对的问题。TLDR Scholar最终选择了:
- 主域名在Cloudflare注册(方便管理)
- 国内用户通过已备案的域名访问
- 未备案域名走Cloudflare代理,保持可用
备案流程:个人备案大约需要2-3周,需要购买国内云服务器作为接入商。建议提前规划。
4.3 环境变量管理
敏感信息(API密钥、数据库密码)绝对不能硬编码在代码里。Vercel提供了环境变量管理功能:
typescript
// 安全的API调用示例
const client = new OpenAI({
apiKey: process.env.DEEPSEEK_API_KEY, // 从环境变量读取
baseURL: 'https://api.deepseek.com', // DeepSeek兼容OpenAI格式
});
本地开发时,在项目根目录创建 .env.local 文件:
bash
DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxx
TEXTIN_API_KEY=xxxxxxxxxxxxx
重要 :.env.local 必须加入 .gitignore,永远不要提交到Git仓库。
五、踩坑总结:独立开发者必须知道的那些坑
做了两个月产品,我踩过的坑比代码行数还多。总结几条核心教训:
5.1 API成本控制:别被Token烧秃了
大模型按Token计费,如果不做控制,分分钟烧光预算。
教训1:上线第一周,我忘了加请求限制,结果被一个用户用脚本刷了几千次请求,一晚上烧掉了50块的API额度。
解决方案:
typescript
// 限流中间件
export function withRateLimit(handler: NextHandler) {
return async (req: NextRequest) => {
const ip = req.ip || req.headers.get('x-forwarded-for') || 'unknown';
const key = `rate:${ip}`;
// 使用Vercel KV或Redis存储计数
const count = await kv.get(key);
if (count && parseInt(count) >= 10) {
return NextResponse.json(
{ error: 'Too many requests' },
{ status: 429 }
);
}
await kv.incr(key);
await kv.expire(key, 3600); // 1小时过期
return handler(req);
};
}
教训2:大模型输出不可控,有时会输出几千字的废话。
解决方案:在Prompt里明确字数限制,并在应用层做截断处理。
5.2 用户体验:Loading状态的细节决定体验
AI处理需要时间,但如果用户不知道系统在干什么,他们会焦虑、会刷新、会流失。
教训:早期版本只有"正在分析..."的文字提示,很多用户以为页面卡死了。
解决方案:
- 实现真正的流式输出(参考2.3节)
- 分阶段显示状态:上传中 → 解析中 → 分析中 → 完成
- 超时处理:30秒无响应自动提示,并提供重试选项
typescript
const [stage, setStage] = useState<'idle' | 'uploading' | 'parsing' | 'analyzing' | 'done'>('idle');
const statusMessages = {
uploading: '正在上传文件...',
parsing: '正在解析论文内容...',
analyzing: '正在分析核心发现...',
done: '分析完成!',
};
5.3 错误处理:用户看到的应该是人话
很多开发者习惯在错误信息里写技术细节,但用户不需要知道"TypeError: Cannot read property 'data' of undefined"。
教训:早期版本的错误提示是技术报错,用户看到后一脸懵,不知道该怎么办。
解决方案:
typescript
// 统一错误处理
function handleError(error: unknown): string {
if (error instanceof Error) {
// API相关错误
if (error.message.includes('429')) {
return '服务器繁忙,请稍后重试';
}
if (error.message.includes('timeout')) {
return '网络超时,请检查网络连接后重试';
}
}
// PDF解析错误
if (error instanceof PDFParseError) {
return '无法解析此PDF文件,可能文件已加密或格式不兼容';
}
// 默认提示
return '出了点问题,请稍后重试。如果问题持续存在,请联系反馈。';
}
六、写在最后:技术是为产品服务的
回顾TLDR Scholar的技术选型,我没有追求"最先进"或"最完整",而是始终围绕一个核心问题:如何在有限时间内做出可用的产品。
技术栈不需要完美,够用就行。
架构不需要超前,解决问题就行。
代码不需要炫技,稳定可维护就行。
对于独立开发者来说,最大的敌人不是技术难度,而是完美主义带来的拖延。先跑通核心流程,再迭代优化------这是我在TLDR Scholar开发过程中学到的最重要的一课。
如果你对TLDR Scholar感兴趣,可以访问 https://www.tldrscholar.cn 体验完整功能。如果你在开发类似产品过程中遇到问题,欢迎在评论区交流。
相关资源:
- TLDR Scholar:https://www.tldrscholar.cn
- Next.js官方文档:https://nextjs.org
- DeepSeek API:https://platform.deepseek.com
- TextIn文档解析:https://www.textin.com
作者:独立开发者,专注AI应用产品开发
标签:#AI #Next.js #大模型 #独立开发 #技术干货