我用什么技术做了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全栈方案,原因是:

  1. 部署简单:Vercel一键部署,不用折腾Nginx配置
  2. 冷启动快:Serverless模式下,请求来了才启动
  3. SSR/SSR灵活切换:落地页用SSR保证SEO,核心功能用CSR
  4. 一套代码库:前端后端同一个仓库,维护成本低

对于日均几百到几千请求的独立开发者产品,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"。

解决方案:

  1. 先检测PDF是否包含嵌入字体
  2. 如果有自定义字体,使用TextIn的"高精度模式"
  3. 关键术语设置后处理映射表
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处理需要时间,但如果用户不知道系统在干什么,他们会焦虑、会刷新、会流失。

教训:早期版本只有"正在分析..."的文字提示,很多用户以为页面卡死了。

解决方案

  1. 实现真正的流式输出(参考2.3节)
  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 体验完整功能。如果你在开发类似产品过程中遇到问题,欢迎在评论区交流。


相关资源

作者:独立开发者,专注AI应用产品开发

标签:#AI #Next.js #大模型 #独立开发 #技术干货

相关推荐
guslegend4 小时前
第11节:前端 UI 设计与前端基础组件
前端·ui·ai编程
Peter·Pan爱编程4 小时前
Harness Engineering:从 Prompt Engineering 到可自我演化的 AI Agent 工程体系
人工智能·prompt·ai编程
m0_634666734 小时前
MeMo:当记忆本身变成一个模型
人工智能·深度学习·ai·ai编程
HaSaKing_7214 小时前
API 中转站黑话说明:渠道、倍率、风险与选型
人工智能·ai编程·ai写作
DogDaoDao4 小时前
【GitHub】SkyReels-V2 无限时长电影级视频生成模型:技术架构与核心原理深度解析
人工智能·大模型·aigc·音视频·ai agent·生成视频·skyreels-v2
向量引擎4 小时前
给 Agent 加一个可靠的知识检索层:从向量引擎到 RAG 工作流的实践笔记
人工智能·gpt·aigc·api·ai编程·key·agi
lihaozecq4 小时前
Agent开发沙箱设计 - 使工具更安全的被执行
agent·ai编程
guslegend4 小时前
第5节:RAG知识库上传,解析和验证
人工智能·大模型
optimistic_chen6 小时前
【AI Agent 全栈开发】MCP
java·linux·运维·人工智能·ai编程·mcp