简介:这是一份由清华沈少阳于2025年2月发布的“DeepSeek+DeepResearch应用报告”PDF,聚焦推理大模型与通用大模型的协同应用,适合AI产品经理、大模型开发者及企业数字化转型人员阅读。报告从DeepSeek深度思考R1的技术原理切入,用瑞士军刀与手术刀的比喻清晰对比通用模型与推理模型在需求明确度、容错成本、资源限制等维度的选型差异,并围绕电商客服场景,演示通用模型处理常规咨询、推理模型解决复杂纠纷的黄金组合打法。它还展示了推理大模型在分析辅助、思维导图生成、GPTs组合运用等实战案例,附有四步推理流程及关键技术说明。资源为单个PDF文件,大小仅9.94MB,电脑或移动端均可直接阅读,目前已有168人学习下载。读者可从中系统掌握DeepSeek R1的思维链机制、模型融合应用方法,并可借鉴提示词设计、业务落地与使用心得,快速提升大模型应用与内化能力。
1. 当“报告 PDF”成为项目标题,真正该拆的是研究链路
过去两年,AI 圈最不缺的就是“研究报告”。但“2025清华:DeepSeek+DeepResearch应用报告.pdf”这个标题能被反复检索,核心不在 PDF 这个载体,而在它把三件事焊在了一起:DeepSeek 的推理性价比、DeepResearch 风格的自动化调研范式,以及“成果物必须可交付”这条硬约束。对一线工程师来说,真正的价值不是报告里的某个结论,而是它背后的 research pipeline:先让模型把宽泛问题拆成子问题,再去做检索和交叉验证,最后把中间过程收敛成一份能导出的文档。本文不引用那份 PDF 的具体内容,只沿着标题暴露的技术方向,从模型选型、工作流编排、PDF 生成三个层面,把一套能在本地跑通的最小方案讲清楚。
2. DeepResearch 的原理拆解与 DeepSeek 的选型理由
DeepResearch 本质上不是某个产品,而是一套把“深度研究”这个动作流水线化的工程范式。市面上能见到的深度研究应用,底层都是同一个循环:拆解问题、检索、推理、再拆解、再收敛。DeepSeek 在这个循环里承担的不只是“聊天”角色,而是同时扮演规划器、分析器和写作器。把这三层混在一次调用里也能跑,但效果和稳定性都很难控,所以要先从原理上把每个环节的职责拆开。
2.1 plan、search、reason、write 四段式循环
常见做法是把一轮 DeepResearch 拆成四个阶段,每个阶段对模型的输入、输出格式和参数要求都不同。plan 阶段负责把宽泛主题转成 3 到 5 个可检索的子问题,输出是结构化列表;search 阶段对每个子问题补齐检索结果,这一步通常不交给模型,而是走搜索接口或数据库查询;reason 阶段把检索到的内容连同子问题一起送回模型,要求产出结论、指出矛盾、标注信息缺口;write 阶段把多轮 reasoning 结果汇总成带章节的报告,这一步才开始要求长文本输出。
这样拆的好处是任何一步出错都能单独重跑,而不必把整段对话推倒重来。和“一次性写一篇报告”的聊天式做法相比,research loop 里的每一步都是可观测、可续跑的,这也是生产级方案宁可用多次短调用、也不用一次超长对话的原因。
2.2 选 DeepSeek 做推理引擎的三个理由
第一是成本。DeepSeek 的 API 定价在同类推理模型里明显偏低,DeepResearch 这种循环最吃 token,因为每次 search 之后,原始检索内容都要作为输入再喂回模型。同样一批材料,在别的模型上重复送三遍的钱,足够在 DeepSeek 上多跑完两个完整研究任务。第二是兼容性。DeepSeek 的接口兼容 OpenAI 协议,团队里现成的工具链不用改适配层,SDK、监控脚本、压测脚本都能直接复用。vscode 接 DeepSeek、codex 接 DeepSeek 这类玩法,本质都是靠这一层兼容性做出来的。第三是推理能力。处理“信息矛盾”和“来源不全”这类任务时,需要模型明确说“这里证据不足”,而不是强行给一个看似完整的答案。DeepSeek 对这类边界判断的响应质量,在同等价位下值得专门试跑。如果数据不能出域,还可以走本地部署 DeepSeek 的路径,模型权重用 ollama 或 vLLM 跑起来,接口保持兼容,只是吞吐量受显存限制。
2.3 动手前先理清三个模型参数
调用 DeepSeek API 前,先把下面三个参数理清楚,否则后面调工作流会反复被同一类问题卡住。
| 参数 | 作用 | DeepResearch 场景建议 |
|---|---|---|
| temperature | 控制随机性,值越大输出越发散 | plan 阶段 0.2,write 阶段 0.6 |
| max_tokens | 限制单次输出长度,超出会被截断 | 至少 2000,写报告时设 4000 |
| top_p | 核采样,一般保持默认 | 若输出文本结构错乱再调回 0.9 以下 |
不少人习惯把 temperature 拉高来追求“有创意”,这在研究场景里是反效果。检索摘要和结论判定需要的是稳定输出,温度越高,同一个来源被解读两次的结果差异越大,研究结论就没法验收。
2.4 DeepSeek API 调用:一条可运行的最小请求
用 Python 调 DeepSeek,最简单的写法是直接用 openai 库,改一下 base_url 和模型名。
import os from openai import OpenAI client = OpenAI( api_key=os.getenv("DEEPSEEK_API_KEY"), base_url="https://api.deepseek.com" ) resp = client.chat.completions.create( model="deepseek-chat", messages=[ {"role": "system", "content": "你是一个研究规划器。请把主题拆成3到5个可检索的子问题,每行一个。"}, {"role": "user", "content": "主题:DeepSeek 在报告生成类任务中的应用现状"}, ], temperature=0.3, max_tokens=800, ) print(resp.choices[0].message.content)逻辑说明:代码先构造 OpenAI 客户端,再用 chat.completions.create 发起一次对话。base_url 指向 DeepSeek 的服务地址,api_key 从环境变量读取,避免把密钥写进代码库。temperature 和 max_tokens 分别控制发散程度和输出长度,拿到响应后,message.content 就是拆好的子问题文本。
参数说明:如果改用model="deepseek-reasoner",对应的是推理模型,响应里会多出 reasoning_content 字段,用来取中间推理链。规划类任务用 deepseek-chat 就够,write 阶段需要强推理时再切 reasoner,能明显减少“结论对但中间说服力不够”的问题。
3. 搭建一条最小可跑通的 DeepSeek + DeepResearch 工作流
原理讲完,下面直接落一条能跑的工作流。工程目标只有一个:把一个主题变成一份 Markdown 报告,中间每一步的产物都能单独保存、单独追问。常见做法是先把整个流程拆成函数,每个函数只做一件事,最后用一层主循环串起来。
3.1 工作流骨架:一次研究对应一个任务对象
不要为每次研究写一个脚本,而是把研究状态落在一个任务对象上。每个任务包含主题、子问题列表、检索结果、阶段结论四块。后续需要续跑、分析成本、断点重试,都有明确载体。任务对象可以就是一个字典,结构如下。
{ "topic": "DeepSeek 在报告生成类任务中的应用现状", "sub_questions": [], "search_cache": {}, "reasoning": [], "draft": "" }search_cache 是“问题到检索结果”的映射,reasoning 保存每一轮追问的结论,draft 是最终合成稿。这样存的好处是,即使某一步调接口失败,重新执行时也能先把 cache 读出来,避免重复检索。
3.2 第一步:用 DeepSeek 拆解子问题
拆解子问题的关键是限定输出格式。自由文本也能用,但后续解析要写正则,平白增加维护成本。实际做的时候会直接要求模型输出 JSON,再用 json.loads 解析。
import json def plan(topic: str) -> list[str]: system_prompt = ( "你是研究规划器。输出一个 JSON 数组,数组元素是子问题字符串。" "子问题必须能在检索环节落地,涉及具体实体和年份。" ) resp = client.chat.completions.create( model="deepseek-chat", messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": topic}, ], temperature=0.2, max_tokens=1000, ) content = resp.choices[0].message.content data = json.loads(content) return data逻辑说明:系统提示词里明确“输出一个 JSON 数组”,模型通常会遵守;偶发返回代码块包裹的 JSON 时,需要先去掉反引号再解析。为了省事,这里直接调 json.loads,线上建议加一层容错,把首尾的 ```json 标记剥掉。
参数说明:temperature 设 0.2,是确保规划结果重复可测。子问题数量控制在 3 到 5 个,超过这个数量,后续检索和追问的调用次数会按乘积增长,单研究成本会翻两三倍。plan 阶段尽量不要用 reasoner,拆问题是结构性任务,不是强推理任务。
3.3 第二步:检索 + 摘要 + 循环追问
检索环节不直接交给模型。先调用搜索接口拿原始材料,再把材料摘要打包给模型。这里以常见搜索服务 Tavily 为例,如果用的是其他检索服务,只需要替换 search() 函数实现。
def search(query: str, top_k: int = 5) -> str: results = tavily_client.search(query, max_results=top_k) chunks = [] for item in results["results"]: chunks.append(f"[来源] {item['url']}\n{item['content'][:800]}") return "\n\n".join(chunks) def reason(sub_question: str, material: str) -> str: resp = client.chat.completions.create( model="deepseek-reasoner", messages=[ {"role": "system", "content": "基于给定的检索材料回答子问题。\ 如果材料不足以支持结论,明确写出‘信息不足’并指出缺少什么。"}, {"role": "user", "content": f"子问题:{sub_question}\n\n材料:{material}"}, ], temperature=0.3, max_tokens=1500, ) return resp.choices[0].message.content逻辑说明:search 阶段对每条结果截断 800 字,避免后续输入过长;reason 阶段用 deepseek-reasoner 模型,让它给出带中间推理的结果。核心思路是“把模型当裁判,而不是当搜索引擎”。所有原始检索材料必须由外部接口提供,模型只负责筛选、对比和归纳,这样能减少编造来源的情况。
参数说明:top_k 设 5 通常够用,遇到专业度高的主题可以降到 3,减少噪声。每轮材料截断 800 字是一个折中值:太短丢信息,太长则当子问题数量多时,合计 token 会显著超预算。追问逻辑没有写进这个片段,实际使用中,可以对 reason 的输出做一次校验,检测到“信息不足”关键字时换一种查询词再检索,最多重试两轮。
3.4 token 预算估算与“达到对话长度上限”的规避
不少人在正式场景中遇到“deepseek 达到对话长度上限,请开启新对话”的报错,就是因为单次请求把整段研究过程都放在一个 messages 数组里。有效规避方式是不让一次请求携带过多历史。
实际操作中用三层预算来控制。第一层是单次输入的原始材料,每篇压缩到 800 字以内;第二层是单次输出限制,write 阶段 4000 max_tokens 基本够用;第三层是整个研究任务的总 token,用一个简单的估算函数累加每次会话的 usage 字段。
| 环节 | 输入预算 | 输出预算 | 备注 |
|---|---|---|---|
| plan | 500 | 1000 | 输出 JSON,裁掉聊天前缀 |
| search | 外部完成 | 不消耗模型 | 材料截断 800 字 |
| reason | 2000 | 1500 | 每子问题一次调用 |
| write | 3000 | 4000 | 汇总所有子问题结论 |
这个表给出的是保守值,实际消耗跟材料数量和模型版本有关。可以把每次响应的 usage 写入日志文件,连续跑几次后,用统计值去校准预算表。
4. 把研究结果变成 PDF:生成、转换与回读
DeepResearch 的最终交付物,落到“2025清华:DeepSeek+DeepResearch应用报告.pdf”这类标题的语境中,就是一份 PDF。PDF 在生产线上有正反两个方向:正向是研究完成后输出成果物;反向是从存量 PDF 里抽取文本,作为新研究的前置语料。两个方向都要能稳定复现。
4.1 一条命令把 Markdown 报告转成 PDF
最省事的路径是先把报告写成 Markdown,再用 pandoc 统一转 PDF。Markdown 是中间态,方便版本管理和再次编辑;Pandoc 负责版式,中文字体单独配置。命令如下:
pandoc report.md \ --pdf-engine=xelatex \ -V CJKmainfont="Noto Serif CJK SC" \ -V geometry:margin=2.5cm \ -o report.pdf逻辑说明:这是一个标准化的 Pandoc 调用,核心参数是 --pdf-engine 和 CJKmainfont。第一行指定输入文件 report.md,第二行指定 PDF 引擎为 xelatex,第三行设置中文字体为思源宋体,第四行调整页边距。运行前需要安装 texlive-xetex 和 Noto CJK 字体,缺少任一依赖时,命令会报找不到引擎的错。
参数说明:CJKmainfont 指定的是正文字体,标题字体可以再加一个 CJKoptions 来控制。如果你在服务器上跑,没有图形界面也不影响,xelatex 不需要桌面环境。输出文件 report.pdf 直接落到当前目录,后续配合文件服务分发即可。
4.2 用 Python 直接生成带目录的 PDF
Pandoc 依赖 texlive,体积大,在容器化部署里往往不划算。更常见的做法是写一段 Python,先把报告渲染成 HTML,再用 WeasyPrint 转 PDF。这个方案对团队技术栈最友好,样式也更容易对齐公司模板。
from weasyprint import HTML html_content = """<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <title>研究报告</title> <style> body { font-family: "Noto Sans CJK SC"; line-height: 1.7; } h1 { font-size: 22pt; } h2 { font-size: 16pt; border-bottom: 1px solid #ccc; } .meta { color: #666; font-size: 10pt; } </style> </head> <body> <h1>DeepSeek 研究报告</h1> <p class="meta">生成日期:2025-01-20</p> <div>此处放入报告正文</div> </body> </html>""" HTML(string=html_content).write_pdf("research_report.pdf")逻辑说明:代码构造一个完整的 HTML 页面,head 中定义中文字体和标题样式,body 中承载报告内容。WeasyPrint 写 PDF 时会按 CSS 规则排版,这与浏览器渲染机制基本一致。生成文件 research_report.pdf 可直接用于交付。
参数说明:如果需要对首页加独立封面,可以把 h1 部分单独放一页,用 page-break-after: always 控制分页。字体必须指定中文字型,否则会用系统默认字体渲染,出现空格或豆腐块,这就是很多人说的 pdf 中文设置问题。生产环境里,建议把常见中文字体打包进镜像,避免现场缺字体。
4.3 逆方向解析:把现有 PDF 变成 DeepSeek 的上下文
报告类 PDF 常常是研究的起点。要把它复用进 DeepResearch 流程,先要完成 PDF 文本抽取。这里用 PyMuPDF,因为它的中文抽取稳定性优于传统 pdftotext 方案。
import fitz doc = fitz.open("reference_report.pdf") full_text = [] for page in doc: full_text.append(page.get_text()) text = "\n".join(full_text) # 按 2000 字一段切片,方便后续灌入模型 chunks = [text[i:i+2000] for i in range(0, len(text), 2000)] print(chunks[0])逻辑说明:循环遍历每一页,用 get_text 提取当前页的文本块,最后拼成完整字符串。随后按 2000 字切片,是考虑到单次输入的 token 预算,避免一次性把整本 PDF 都传给模型。切片后的文本数组可以直接交给上一节写的 reason 函数做段落级摘要。
参数说明:get_text 提取的文本是线性顺序,若 PDF 本身分栏排版,可能读序错乱。遇到这类情况,可以用 layout=True 参数调整抽取模式,或者先调用 page.get_blocks() 获取区块坐标,再按左到右、上到下的规则重排。对于扫描版 PDF,PyMuPDF 无法直接抽取文字,要先接 OCR 流程,常见的做法是渲染成图片后再过 PaddleOCR。
4.4 PDF 生成后的三项硬检查
PDF 转完不等于交付完成,至少要做三项检查。
| 检查项 | 常见现象 | 修复方式 |
|---|---|---|
| 中文乱码 | 字体未嵌入 | 指定 CJKmainfont 并固定字体版本 |
| 页数跳变 | 字体替换导致重排 | 将中文字体包打入构建镜像 |
| 元数据泄露 | 调试信息外露 | 用 pdfinfo 检查后改写元数据 |
每一项都能用脚本自动化,不必人工点开 PDF 检查。把这三条写进发布 CI,比靠自觉可靠得多。
5. 加一层轻量 harness:重试、结构化输出与成本校验
裸调用 API 能跑通最小流程,但多轮 DeepResearch 上线后,最常出问题的反而不是模型能力,而是调用层稳定性:接口超时、返回格式偶尔包了代码块、输出长度被截断。社区里管这种封装叫 DeepSeek harness,本质就是把重试、限流、结构化输出包在一个薄层里,避免业务代码被调用细节污染。
5.1 给 DeepSeek 调用统一加指数退避重试
最直接的模式是在请求函数上叠加一个重试装饰器,遇到网络错误时退避等待,而不是立刻抛异常。
import time, functools def retry_on_network_error(max_retries=3): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): for attempt in range(max_retries): try: return func(*args, **kwargs) except Exception as e: wait = 2 ** attempt # 1s, 2s, 4s print(f"[retry] {attempt+1}/{max_retries}: {e}") time.sleep(wait) raise return wrapper return decorator逻辑说明:第二次请求时等待 1 秒,第三次等待 2 秒,第四次等待 4 秒,避免高并发场景下立刻重试雪崩。把它装饰在 plan、reason、write 的调用函数上,能统一处理瞬时错误。注意不要对 search 接口用同样的重试,检索结果对时效敏感,重试反而可能拿到脏数据。
5.2 结构化输出的二次校验
在解析 JSON 前加一个清理层,处理常见的代码块包裹问题。
def parse_model_json(raw: str): raw = raw.strip() if raw.startswith("```"): raw = raw.strip("`") raw = raw.removeprefix("json").strip() return json.loads(raw)这个方法只处理最常见的一层包裹,遇到嵌套异常时直接报错,然后靠重试机制重跑,比写死各种容错分支更可控。
5.3 用 usage 字段做一轮成本观测
任何新工作流上线前,都值得先用固定问题跑一轮压测,观察成本和输出稳定性。每次响应都会带 usage 统计,把它追加到本地文件,就能统计出平均单次费用和整条管线总费用。
def log_usage(task_id: str, model: str, resp) -> None: usage = resp.usage row = f"{task_id},{model},{usage.prompt_tokens},{usage.completion_tokens}\n" with open("usage.csv", "a") as f: f.write(row)把 task_id 设为主题名加时间戳,后续迭代时按 task_id 分组对比。如果同一主题的成本比上一版涨了 30%,优先检查是否连续两次把相同材料喂给了模型,不用急着换模型版本。这组数据同时还能帮你判断,当 max_tokens 设为 4000 时,write 阶段的输出是否真的用到过上限,从而反向校准预算表里每个环节的参数。
本文还有配套的精品资源,点击获取