欢迎光临
我们一直在努力

我用什么技术做了TLDR Scholar——AI论文速读产品完整技术栈拆解

我用什么技术做了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目前接入了多个大模型供应商,调用层做了统一的抽象:

    // 统一的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

    const prompt = `你是一个学术论文分析专家。请分析以下论文,输出一句严谨的归纳总结,格式如下:

    {
    "discipline": "学科领域",
    "methodology": "研究方法",
    "coreFinding": "核心发现",
    "reproducibleMetrics": "可重现指标"
    }

    论文内容:
    ${paperText}

    要求:
    1. 每个字段必须填写,不可为空
    2. 一句严谨的归纳总结
    3. 可重现指标必须是论文中明确给出的数据指标
    `;

    结果:格式稳定了,但输出质量参差不齐——有时"核心发现"写得像流水账,有时"可重现指标"列一堆无关数字。

    v3.0 – 结构化Prompt + Few-shot示例

    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):

    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封装):

    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 代码实现

    // 论文上传接口
    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的"高精度模式"
  • 关键术语设置后处理映射表
  • // 后处理:修复常见编码问题
    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提供了环境变量管理功能:

    // 安全的API调用示例
    const client = new OpenAI({
    apiKey: process.env.DEEPSEEK_API_KEY, // 从环境变量读取
    baseURL: 'https://api.deepseek.com', // DeepSeek兼容OpenAI格式
    });

    本地开发时,在项目根目录创建 .env.local 文件:

    DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxx
    TEXTIN_API_KEY=xxxxxxxxxxxxx

    重要:.env.local 必须加入 .gitignore,永远不要提交到Git仓库。


    五、踩坑总结:独立开发者必须知道的那些坑

    做了两个月产品,我踩过的坑比代码行数还多。总结几条核心教训:

    5.1 API成本控制:别被Token烧秃了

    大模型按Token计费,如果不做控制,分分钟烧光预算。

    教训1:上线第一周,我忘了加请求限制,结果被一个用户用脚本刷了几千次请求,一晚上烧掉了50块的API额度。

    解决方案:

    // 限流中间件
    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秒无响应自动提示,并提供重试选项
  • 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"。

    教训:早期版本的错误提示是技术报错,用户看到后一脸懵,不知道该怎么办。

    解决方案:

    // 统一错误处理
    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 #大模型 #独立开发 #技术干货

    赞(0)
    未经允许不得转载:171主机测评 » 我用什么技术做了TLDR Scholar——AI论文速读产品完整技术栈拆解
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址