跳到主要内容

用 DeepSeek V4 百万 token 上下文做长文档问答(不用 RAG)

一句话定义:用户上传 PDF / 一整套代码 / 整张 Excel,后端用 pdf-parse 解析成纯文本后整篇塞进 prompt,调 CloudBase AI app.ai().createModel('cloudbase').streamText({ model: 'deepseek-v4-pro' }) 一次性问答,跳过 RAG 的 embedding + 向量库 + 召回三件套。

预计耗时:30 分钟 | 难度:进阶

适用场景

  • 单篇长文档的一次性问答:财报 PDF、几十页论文、一份法律合同、白皮书,问完即走
  • 让 AI 读整个 repo 几万行代码做架构理解 / 找 bug / 写文档
  • Excel / CSV 大表(几千行)的数据问答,把表格序列化成纯文本喂进去就行
  • 内部知识库雏形阶段,文档总量稳定在几十篇,还没必要上向量库

不适用:

  • 短文档(几千 token 以下)用 deepseek-v4-pro 的百万上下文是浪费钱,直接走 add-ai-nextjsdeepseek-v4-flash 即可
  • 文档持续增长 / 多文档检索 / 需要细粒度引用某个 chunk,走 add-rag-with-pgvector-cloudbase — embedding + 向量库 + 召回是为「海量知识库 + 精确溯源」准备的
  • 文档超过 80 万 token 软上限(估算:1 token ≈ 1.5 个汉字 / 4 个英文字符),硬塞上去会被截断或报错,需要降级策略
  • 单文档要被高频问答(同一个 PDF 一天问几百次),每次都把全文塞 prompt token 费用爆炸,这种走 RAG 更省

选哪条路:决策树

你的场景推荐路线
文档总量 < 80 万 token,问完即走本篇(长上下文一把梭)
文档总量 < 80 万 token,但单文档要反复问答几百次RAG(add-rag-with-pgvector-cloudbase)
文档总量 > 80 万 token,或者文档持续增长RAG
需要精确给出"这句话引用了文档第几页第几段"RAG(召回 chunk 自带索引)
文档极短(几千 token 以下)直接走 add-ai-nextjs,用 flash 模型,百万上下文用不上

简单说:本篇是 cheaper 工程,但每次 token 贵;RAG 是更复杂工程,但每次 token 便宜。 选哪条看你文档复用率。

环境要求

依赖版本
Next.js14+(App Router)
@cloudbase/node-sdk3.16.0 及以上(AI 模块要求)
pdf-parse^1.1.1(解析 PDF buffer 成纯文本)
Node.js18.17+
Route Handler runtime必须是 nodejs,不能用 edge
CloudBase 环境已开通,且控制台 AI+ 已开通
模型deepseek-v4-pro(百万上下文是 pro 独有,flash 不支持)

服务端必须用 @cloudbase/node-sdk,不要用 @cloudbase/js-sdk + signInAnonymously()。Web SDK 的匿名登录默认会被严格限频(详见 Web SDK 安全策略),长文档场景一次请求几十秒,Web SDK 那条路根本走不通。

第一步:控制台确认 deepseek-v4-pro 已上架

  1. CloudBase 控制台 → 选你的环境 → AI+模型管理
  2. 在可用模型列表里确认能看到 deepseek-v4-pro(完整列表见 接入大模型)
  3. deepseek-v4-pro 是 pro 系列,百万上下文是它独有的能力;deepseek-v4-flash 是短上下文 + 性价比款,不能用于本篇
  4. token 计费按「输入 + 输出」分别计算,pro 系列单价比 flash 高,百万级输入一次性消耗可能是 flash 几十次对话的量级,部署前先去控制台看当前单价

第二步:装依赖 + 配环境变量

npm install @cloudbase/node-sdk pdf-parse
# 类型(可选): npm install -D @types/pdf-parse

.env.local:

CLOUDBASE_ENV=your-env-id
TENCENTCLOUD_SECRETID=your-secret-id
TENCENTCLOUD_SECRETKEY=your-secret-key

add-ai-nextjs 第二步完全一致——三个变量都没有 NEXT_PUBLIC_ 前缀,SDK 只在服务端 Route Handler 里跑;密钥从 腾讯云控制台 → 访问密钥 拿,生产用子账号 + CAM 策略锁定到当前环境。

第三步:写 Route Handler — 解析 PDF + 整篇塞 prompt

新建 app/api/longdoc/route.ts:

import tcb from '@cloudbase/node-sdk';
import pdfParse from 'pdf-parse';

export const runtime = 'nodejs'; // 关键:edge 跑不起 SDK 和 pdf-parse
export const maxDuration = 180; // Next.js 14+ Route Handler 默认 60s,长文档不够

let app: ReturnType<typeof tcb.init> | null = null;

function getAi() {
if (!app) {
// timeout 120000(120s):百万级输入的 streamText 首字延迟可能 30s+,默认 15s 必超
app = tcb.init({ env: process.env.CLOUDBASE_ENV!, timeout: 120000 });
}
return app.ai();
}

// 1 token ≈ 1.5 个汉字 / 4 个英文字符;给百万上下文留 20% 余量,软上限 80 万 token
const MAX_INPUT_TOKENS = 800_000;

function estimateTokens(text: string): number {
// 简化估算:中文按 1.5 字/token,英文按 4 字符/token;中英混合就取中间值
const cjkChars = (text.match(/[\u4e00-\u9fff]/g) || []).length;
const otherChars = text.length - cjkChars;
return Math.ceil(cjkChars / 1.5 + otherChars / 4);
}

export async function POST(req: Request) {
const form = await req.formData();
const file = form.get('file') as File | null;
const question = (form.get('question') as string) || '请总结这份文档';

if (!file) {
return new Response(JSON.stringify({ error: 'file required' }), { status: 400 });
}

// 1. 把 File 转 Buffer,pdf-parse 接受 Buffer
const buf = Buffer.from(await file.arrayBuffer());

// 2. PDF → 纯文本(pdf-parse 自动按页拼接,丢失版式但保留全部文字)
const parsed = await pdfParse(buf);
const fullText = parsed.text;

// 3. token 估算 + 超限降级
const tokens = estimateTokens(fullText);
let docText = fullText;
if (tokens > MAX_INPUT_TOKENS) {
// 简化降级:取头 + 尾(假设关键信息在文档开头和结尾)
// 更聪明的做法是 LLM-based 摘要中间段;再复杂就走 RAG
const cjkRatio = (fullText.match(/[\u4e00-\u9fff]/g) || []).length / fullText.length;
const charsPerToken = cjkRatio > 0.5 ? 1.5 : 4;
const keepChars = Math.floor(MAX_INPUT_TOKENS * charsPerToken * 0.45); // 头尾各拿 45%
docText =
fullText.slice(0, keepChars) +
'\n\n[...中间内容因超出上下文窗口被省略...]\n\n' +
fullText.slice(-keepChars);
}

// 4. 拼 prompt + streamText
const ai = getAi();
const model = ai.createModel('cloudbase');

const result = await model.streamText({
model: 'deepseek-v4-pro', // 百万上下文必须 pro,flash 短上下文不支持
messages: [
{
role: 'system',
content:
'你是一个严谨的长文档分析助手,严格基于「参考文档」回答问题。' +
'文档里没有的信息,直接说"文档中未提及",不要编造。' +
'回答用简体中文,涉及数字/日期/条款时引用原文表述。',
},
{
role: 'user',
content: `参考文档(${file.name},约 ${tokens} tokens):\n\n${docText}\n\n问题:${question}`,
},
],
});

// 5. AsyncIterable → ReadableStream,前端走裸 fetch 读
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
try {
for await (const chunk of result.textStream) {
controller.enqueue(encoder.encode(chunk));
}
controller.close();
} catch (err) {
controller.error(err);
}
},
});

return new Response(stream, {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'X-Doc-Tokens': String(tokens),
'X-Doc-Truncated': tokens > MAX_INPUT_TOKENS ? '1' : '0',
},
});
}

要点说明:

  • tcb.init({ ..., timeout: 120000 }) 必须显式拉到 120s。百万级输入的首字延迟可以到 30s+,默认 60s 在长文档场景边缘,120s 给够余量
  • MAX_INPUT_TOKENS = 800_000 是软上限。deepseek-v4-pro 标称百万,但实际工程要留 20% 给输出 token + system prompt + safety margin
  • 降级策略只做了「头 + 尾」最朴素一刀切。生产里可以叠加:按章节摘要中间段(再调一次 flash 做 summary)、按 user query 用 embedding 粗筛只塞 top-N 段(此时已经是简化版 RAG,建议直接走 add-rag-with-pgvector-cloudbase)
  • 不要用 result.dataStream — 它带 chunk 元数据是给 Vercel AI SDK 协议用的,本篇前端裸 fetch,只要纯文本增量

第四步:Excel / CSV / 代码仓库怎么改

PDF 走 pdf-parse,其他类型只需要把第三步代码里 pdfParse(buf) 那一步换成对应解析器,后面拼 prompt + streamText 完全一样。

Excel / .xlsx — 装 xlsx 解析:

import * as XLSX from 'xlsx';
const wb = XLSX.read(buf, { type: 'buffer' });
const fullText = wb.SheetNames.map((name) => {
const sheet = wb.Sheets[name];
return `## Sheet: ${name}\n${XLSX.utils.sheet_to_csv(sheet)}`;
}).join('\n\n');

把每个 sheet 转成 CSV 字符串后拼起来,LLM 对 CSV 格式理解很好。几千行的表格塞进 deepseek-v4-pro 没问题。

整个代码仓库 — 走 file tree 遍历,黑名单过滤 node_modules / .git / dist:

import { promises as fs } from 'node:fs';
import path from 'node:path';

const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', '.next', 'build']);
const ALLOW_EXT = new Set(['.ts', '.tsx', '.js', '.jsx', '.py', '.go', '.md', '.json', '.yaml']);

async function readRepo(root: string): Promise<string> {
const parts: string[] = [];
async function walk(dir: string) {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const e of entries) {
const full = path.join(dir, e.name);
if (e.isDirectory()) {
if (!SKIP_DIRS.has(e.name)) await walk(full);
} else if (ALLOW_EXT.has(path.extname(e.name))) {
const content = await fs.readFile(full, 'utf-8');
parts.push(`### ${path.relative(root, full)}\n\`\`\`\n${content}\n\`\`\``);
}
}
}
await walk(root);
return parts.join('\n\n');
}

中型 repo(几千文件、几万行代码)约 30-50 万 token,正好在 deepseek-v4-pro 舒适区。

CSV / TXT / Markdown — 直接 buf.toString('utf-8'),什么解析都不用。

第五步:前端 — 上传 + 流式展示答案

新建 app/longdoc/page.tsx:

'use client';

import { useState } from 'react';

export default function LongDocQA() {
const [file, setFile] = useState<File | null>(null);
const [question, setQuestion] = useState('总结这份文档的核心结论');
const [answer, setAnswer] = useState('');
const [loading, setLoading] = useState(false);
const [meta, setMeta] = useState<{ tokens?: string; truncated?: string }>({});

async function send() {
if (!file || loading) return;
setAnswer('');
setMeta({});
setLoading(true);

try {
const form = new FormData();
form.append('file', file);
form.append('question', question);

const res = await fetch('/api/longdoc', { method: 'POST', body: form });
if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`);

setMeta({
tokens: res.headers.get('X-Doc-Tokens') || undefined,
truncated: res.headers.get('X-Doc-Truncated') || undefined,
});

const reader = res.body.getReader();
const decoder = new TextDecoder();
let acc = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
// stream: true 必须加,否则跨 chunk 边界的中文会乱码成 �
acc += decoder.decode(value, { stream: true });
setAnswer(acc);
}
} catch (err) {
setAnswer(`[出错] ${err instanceof Error ? err.message : String(err)}`);
} finally {
setLoading(false);
}
}

return (
<div style={{ maxWidth: 800, margin: '40px auto', padding: 16 }}>
<h1>长文档问答(DeepSeek V4 Pro)</h1>
<input
type="file"
accept=".pdf"
onChange={(e) => setFile(e.target.files?.[0] || null)}
style={{ display: 'block', margin: '12px 0' }}
/>
<textarea
value={question}
onChange={(e) => setQuestion(e.target.value)}
rows={3}
style={{ width: '100%', padding: 8 }}
placeholder="问点关于这份文档的什么"
/>
<button onClick={send} disabled={!file || loading} style={{ marginTop: 12 }}>
{loading ? '生成中…(长文档首字可能 30s+)' : '发送'}
</button>
{meta.tokens && (
<div style={{ marginTop: 12, color: '#666', fontSize: 13 }}>
文档约 {meta.tokens} tokens
{meta.truncated === '1' && ' · 已超上限,自动截断头尾保留'}
</div>
)}
<div
style={{
marginTop: 16,
padding: 12,
background: '#f5f5f5',
whiteSpace: 'pre-wrap',
minHeight: 200,
}}
>
{answer || '(等待回答)'}
</div>
</div>
);
}

关键细节:

  • accept=".pdf" 只是 UI 提示,不是安全限制——后端 Route Handler 里也要校验 MIME / magic number,生产场景必加
  • 长文档首字延迟可能 30s+(模型先吃完全文才开始吐字),按钮 loading 文案要明示「首字可能 30s+」,不然用户以为卡死了
  • decoder.decode(value, { stream: true })stream: true 不能漏,理由跟 add-ai-nextjs 第四步一样

运行验证

  1. 启动 npm run dev,浏览器开 http://localhost:3000/longdoc
  2. 上传一份十几页的 PDF(例如某公司财报),问「这份报告的核心营收变化」
  3. 网络面板里 /api/longdoc 的 Response Headers 应该能看到 X-Doc-Tokens 标记 token 数,X-Doc-Truncated: 0 表示没触发降级
  4. 等 10-40 秒应该开始流式吐字,UI 上回答一段段冒出来
  5. CloudBase 控制台 → AI+ → 调用记录,能看到刚才那次调用的输入 + 输出 token 计数,模型字段是 deepseek-v4-pro
  6. 换一份超过 80 万 token 的大文档(例如整套源码 zip 解压后的目录,或长篇小说),应该看到 X-Doc-Truncated: 1,UI 上显示「已超上限,自动截断头尾保留」

常见错误

错误信息 / 现象原因修复
timeout / 请求挂死 60s 左右Node SDK 默认 timeout: 15000,长文档场景必超;或者 Next.js Route Handler 自身 maxDuration 没拉高tcb.init({ env, timeout: 120000 }) + export const maxDuration = 180 两处都要改
model not found / model deepseek-v4-pro is not supported环境里没上架 pro 模型,或拼错成了 deepseek-v4-flash控制台「模型管理」对照名字,见 接入大模型;只有 pro 系列支持百万上下文
context length exceeded / prompt too longtoken 估算偏小,实际超模型上限MAX_INPUT_TOKENS 从 800k 降到 700k 再观察;估算函数对纯英文文档偏宽松,纯英文建议按 3.5 字符/token
pdf-parse 解析后 text 为空字符串上传的 PDF 是扫描件 / 图片型 PDF,没有文字层pdf-parse 只能读文字层;扫描件需要先 OCR(走外部 OCR 服务或 腾讯云 OCR API)
部署到 Vercel/Cloudbase Run 后报 cannot find module '@cloudbase/node-sdk'XMLHttpRequest is not definedRoute Handler 用了 export const runtime = 'edge',Edge Runtime 没完整 Node API改成 runtime = 'nodejs';SDK 和 pdf-parse 都依赖 Node Buffer / fs,Edge 跑不起
流式返回到中途断掉for await 里 streamText 抛异常没进 controller.error(err)try/catch 必须把异常转成 controller.error(err),不能 throw(Response 已发出)
前端流式中文显示 ��� 乱码TextDecoder.decode(value) 没加 { stream: true },跨 chunk 边界的多字节字符被劈两半改成 decoder.decode(value, { stream: true })
答案明显与文档无关触发降级时中间段被砍掉,关键信息恰好在中间用户问的问题与中间段强相关就别用本篇;走 add-rag-with-pgvector-cloudbase 让 retriever 精确召回中间段
Excel 解析后给 LLM 是一堆乱码xlsx 库默认输出二进制,要 sheet_to_csv / sheet_to_json 显式转见第四步代码,用 XLSX.utils.sheet_to_csv(sheet)
token 计费一次比预期高十倍整篇 80 万 token 一次输入,deepseek-v4-pro 单价又是 flash 几倍这是预期行为;不能接受成本就走 RAG,只送召回的几千 token 进 prompt

错误码完整定义参考 https://docs.cloudbase.net/error-code/

计费提示

  • deepseek-v4-pro 的输入 + 输出 token 单价均高于 deepseek-v4-flash,百万级输入一次调用的 token 量级 ≈ flash 几十次普通对话,部署前先看控制台当前披露的单价
  • 新开通环境首月赠送 token 试用额度(具体以 控制台计费页面 实际披露为准),够你测十几次百万级请求
  • 同一份长文档高频问答会让 token 费用迅速堆积:每次问都要重塞全文。如果有这种场景,走 RAG 是正解(每次只送召回的几千 token)
  • Route Handler 自身是后端代理层,凭证不会泄露到浏览器,但 /api/longdoc 端口仍对公网开放;长文档接口是 token 消耗大户,必须加身份验证(参考 add-auth-web-with-cloudbase-sdk)+ 按 UID 限频(例如单用户每小时最多 5 次大文档调用),不然容易被刷
  • .gitignore.env.local,凭证不要进仓库;生产环境密钥放公司密钥管理服务

相关文档

  • add-rag-with-pgvector-cloudbase对照路线:文档持续增长 / 高频问答 / 需要精确溯源时,用 embedding + 向量库 + 召回三件套,每次只塞召回片段进 prompt
  • add-ai-nextjs — Next.js + CloudBase AI 的基础对话能力,本篇的 Route Handler 写法直接继承自这篇;短对话场景走那篇用 deepseek-v4-flash 即可
  • add-ai-wechat-miniprogram — 同一套 CloudBase AI 能力在小程序端的对照实现
  • 接入大模型deepseek-v4-pro / deepseek-v4-flash / 混元 / Kimi / GLM 等模型列表 + 上下文长度
  • SDK 初始化与调用app.ai() 在 Node.js 服务端的初始化指引
  • SDK API 参考createModel / streamText 完整签名
  • pdf-parse npm — Node 端 PDF 文本抽取,本篇用的解析器