☰
DeepSeek工程实践:模型选型、提示词模板与长文档接入
2026/11/2 19:11:27 网站建设 项目流程

简介:围绕清华大学新闻与传播学院新媒体研究中心团队整理的《DeepSeek从入门到精通》PDF讲义,内容面向大模型初学者、开发者以及内容与产品从业者,帮助读者系统认识DeepSeek-R1的能力边界、适用场景与使用方法,并回应推理模型选型、提示语设计等常见问题。压缩包内共1个PDF文件,大小约5.5MB,便于直接阅读和检索。已有1873人学习下载,具备一定参考热度。讲义从“DeepSeek是什么、能做什么、如何使用”展开,覆盖智能对话、文本生成、语义理解、计算推理、代码生成补全等应用,并进一步比较推理模型与通用模型在优势领域、决策方式、创造力及人机互动上的差异。读者还可了解快思慢想、CoT链式思维、提示语策略差异,以及数学证明、创意写作、代码生成等任务对应的提示语设计原则,并沿入门到精通路径建立较完整的实践框架。

1. 从一份 PDF 到一条能跑的工作流:这份手册到底解决什么问题

某高校那份《DeepSeek 从入门到精通》PDF 传开之后,我身边大多数人的动作是一样的:存进网盘,再也没打开过。这类「从入门到精通」的文档真正的价值不在于讲了多少新概念,而在于它把模型分类、提示词写法和接入参数按一条能落地的顺序串起来了——从入门到精通这条路上,卡住人的往往不是「不会提问」,而是不知道什么时候该上推理模型、哪些参数调了有用、几十页的 PDF 到底怎么喂进去。这篇笔记就按这个骨架往下走,讲清楚三件事:模型怎么选、提示词怎么固化成模板、工程里怎么接。适合已经能把模型跑通、但输出质量忽高忽低、每次都要重调一遍的人。

2. 手册里最容易被跳过的一页:DeepSeek 的模型谱系与选型决策表

2.1 通用模型和推理模型不是二选一,而是两段流水线

接口里最常见的两个模型标识,一个是通用对话模型,一个是推理模型。很多人第一次接触时的直觉是「推理的肯定更强,那就全用推理的」,这是第一个坑。推理模型在给出答案前会先生成一大段思考内容,这段思考同样按 token 计费、同样占用延迟,在信息抽取、格式转换、简单分类这类任务上,它不但不快,还容易把一句本来该直接给结果的话,讲成三百字的分析过程。

反过来,全用通用模型也不行。凡是需要多步约束同时满足的任务——比如「从这段配置里找出三处互相冲突的设置,并按优先级给出修改顺序」——通用模型经常只满足其中两条就收工。这不是模型能力问题,是这类任务本身需要显式的中间推理步骤,而通用模型不会主动展开。

所以合理的结构是级联,不是二选一:前面用通用模型做清洗、抽取、意图分类、任务拆解这些便宜的活,把真正需要深推理的那一两步单独路由给推理模型。我一般把这条规则写在代码里而不是写在文档里,因为人一旦赶进度就会图省事全用推理模型,成本翻几倍还不自知。

提示:推理模型的思考过程在返回结构里通常是单独字段(常见字段名带 reasoning 前缀),正文在常规的 content 字段里。解析时只取 content,否则你会把思考过程当成答案传进下游。

2.2 用一张参数对照表把「选哪个」变成可执行判断

选型判断光靠感觉没用,落到表格上才能复用。下面这张表是我自己在项目里用的版本,左边是任务特征,右边是可执行的结论:

任务特征首选模型关键理由常见错配后果
批量字段抽取、格式转换通用模型单步映射,无需推理用推理模型,延迟翻倍、成本翻倍
意图分类、路由判断通用模型类别有限,短输出同上,且输出啰嗦影响解析
文案改写、润色、扩写通用模型需要风格稳定可控推理模型会自作主张改结构
多约束规划、方案权衡推理模型需要显式比较多个条件用通用模型会漏约束
数学计算、逻辑推演推理模型中间步骤必须展开通用模型容易在第二步就跑偏
代码报错定位与修复推理模型需要沿着调用链回溯通用模型常给出「检查一下配置」这类废话
长文档问答通用模型 + 检索瓶颈在召回不在于推理直接上推理模型,答非所问且更慢

采样参数这块的差异更容易被忽略。通用模型支持 temperature、top_p、频率惩罚这些采样参数,推理模型在多数实现里会对这些参数不生效或者直接忽略——你把它设成 0.1 期待输出稳定,结果发现该变的还是变,然后开始怀疑是不是「玄学」。这不是玄学,是参数根本没起作用。推理模型的稳定性靠的是推理路径本身,不靠采样温度。

参数常见取值范围抽取/代码场景文案场景说明
temperature0 ~ 20 ~ 0.30.8 ~ 1.2越低越确定,推理模型上通常无效
top_p0.1 ~ 10.90.95与 temperature 二选一调,不要同时大改
max_tokens按任务定512 ~ 10241024 ~ 2048推理模型要把思考 token 也算进预算
streamtrue / false交互式界面用 true同左批处理用 false,省去拼装逻辑
timeout30 ~ 120 秒6060推理模型务必放宽,别用默认值

2.3 把选型结论写进配置:一份可复用的路由表

光有表没用,人的记忆靠不住。我一般把路由规则写成一份配置,用规则先粗分,只有规则判不准的才交给模型判断,这样能省掉一次额外调用。

# router.py # 用一个轻量规则表决定走哪个模型,避免每次都靠人回忆 import os # 模型标识以你接入时文档给出的为准,这里用两个语义化的键 MODEL_CHAT = os.environ.get("DS_MODEL_CHAT", "chat") # 通用对话模型 MODEL_REASON = os.environ.get("DS_MODEL_REASON", "reason") # 推理模型 # 命中关键词即走高推理路径;关键词宁少勿多,多了等于全走推理 REASON_HINTS = ("证明", "为什么", "冲突", "权衡", "报错", "定位", "几步", "推导") def pick_model(task: str, text: str = "") -> str: """ task: 任务类型标签,如 extract / rewrite / plan / debug text: 原始输入,用于关键词兜底判断 返回:模型标识 """ # 1) 任务类型优先,这一类是稳定的、可预期的 if task in ("plan", "debug", "reason"): return MODEL_REASON if task in ("extract", "rewrite", "classify", "summarize"): return MODEL_CHAT # 2) 任务类型未知时,看输入里有没有强推理信号 if any(h in text for h in REASON_HINTS): return MODEL_REASON # 3) 兜底走便宜的那个,错了再升级 return MODEL_CHAT

这段代码的逻辑是三级判断:任务标签最可靠,放在第一优先级;关键词是兜底,只在任务标签缺失时启用;最后一级永远返回通用模型,保证默认成本最低。

参数说明两点。第一,REASON_HINTS不要堆太多词,我见过有人塞了四十个关键词,结果 80% 的请求都路由到推理模型,等于没做路由。控制在十个以内,只保留那些「出现就一定需要多步推理」的词。第二,模型标识用环境变量读,不要硬编码在源码里,换接入方或者换模型版本时只改环境变量,不用全文搜索替换。

这套路由跑一周之后你会看到两个数:推理模型的调用占比和整体延迟。占比超过 30% 基本说明关键词表太宽,或者任务标签设计得太粗,需要回头拆任务类型,而不是继续加机器。

3. 提示词从「聊天」到「可复用模板」:DeepSeek 上的四段式写法

3.1 同一问题问两遍差一个量级,差在哪

新手最典型的提问方式是「帮我看看这段代码有什么问题」。这句话里有三处缺失:没有说明运行环境,没有说明期望行为,没有说清输出要什么形式。模型只能猜,猜的结果就是你问十次得到十种答案,然后归因成「模型不稳定」。

我做过一个很朴素的对比:同一段有边界问题的分页代码,用开放式提问,模型会给五六条泛泛的建议,其中两条跟实际 bug 无关;换成结构化提问,明确说「只找数组越界和空值两类问题,每条给出出错行号、触发条件、修复代码」,输出立刻收敛到两条,而且都能直接改。差别不在模型,在提问里有没有把「验收标准」写出来。

还有一个隐藏变量是上下文长度。对话越长,前面随手说的话越容易污染后面的判断。这也是为什么模板要固化——模板是每次都从干净状态开始的,不受上一轮对话残留影响。

3.2 角色、约束、输入、验收:四段式模板拆解

四段式的顺序是有讲究的,不是随便排的。角色决定语气和知识边界,约束决定输出范围,输入决定事实来源,验收决定输出形态。四个都缺一个,输出就会在某个维度上漂。

段落写什么常见反例改成这样
角色身份 + 服务对象 + 专业边界「你是一个专家」「你是一名做后端接口评审的工程师,服务对象是刚接手项目的初级开发」
约束必须做 / 禁止做 / 信息不足怎么办「回答要准确」「只基于我给出的日志判断,日志里没出现的信息标注为未知,不要推测」
输入数据本体 + 明确分隔符直接把内容粘在后面用固定标签包住,如<log>...</log>,并在提示里指明标签名
验收输出格式 + 字段 + 长度 + 失败处理「给我一个结论」「输出 JSON,字段为 cause、evidence、fix;证据不足时 cause 填 unknown」

约束那一段有个经验:写「要做什么」比写「不要做什么」有效得多。你写十条「不要编造」,模型该编还是编;你写一条「每个结论后面必须跟原文引用,找不到引用就输出 unknown」,编造率立刻下来。因为前半句是禁止,后半句是可执行的检查动作。

输入段的分隔符别省。用 XML 风格的标签把数据包起来,模型对标签边界的识别比空行可靠得多,尤其是输入里本身混着代码和自然语言的时候。标签名用语义化的,<code>、<error_log>、<user_feedback>,别用<data1>。

验收段最容易漏的是失败路径。任何抽取类任务都要显式规定「抽不到怎么办」,否则模型会用编造来填满字段,这在批量跑的时候是灾难——你拿到一千条结果,没法分辨哪条是真的。

3.3 把模板固化成函数与文件,避免每次手敲

模板写在聊天框里,第二次用就变形了。我一般把它落到文件里,代码里只做变量替换。

# prompt_tpl.py # 模板与代码分离:改提示词不用改业务逻辑,也不用重新部署 from string import Template EXTRACT_TPL = Template(""" 你是做接口日志分析的工程师,服务对象是刚接手项目的初级开发。 约束: 1. 只依据 <log> 标签内的内容判断,标签外的一切不作为证据。 2. 每条结论必须引用日志中的原文片段,引用不到的结论不要给。 3. 信息不足时,对应字段填 unknown,不要推测。 输入: <log> $log_text </log> 验收: 输出 JSON,字段为 cause(根因,一句话)、evidence(原文引用,可为空数组)、 fix(修复建议,不超过三条)。不要输出 JSON 以外的任何内容。 """) def build_extract_prompt(log_text: str) -> str: # safe_substitute 避免日志里出现 $ 符号时抛异常 return EXTRACT_TPL.safe_substitute(log_text=log_text)

这里用Template而不是 f-string,是因为日志里经常出现$、{}这类字符,f-string 会直接抛 KeyError 或者把花括号吃掉,这个坑我踩过一次,排查了半小时才发现是模板引擎的问题。safe_substitute遇到未定义的占位符不会报错,代价是拼错了变量名也不会提醒你,所以变量名要统一,我一般全用_text后缀。

模板文件要进版本管理,每次改动留一条注释说明为什么改。原因是提示词改动的影响不像代码那么直观,你以为只是加了一句话,实际可能让某类输入的抽取率掉十个点。有版本记录,出问题能回滚,这就是提示词的后悔药。

4. 接进工程:DeepSeek 的调用脚本、流式输出与长文档链路

4.1 最小可跑通的调用脚本与五个必调参数

接入层没什么神秘,主流做法是按 OpenAI 兼容协议调,SDK 换一下 base_url 和 key 就能用。下面是最小可跑通的一段,重点是那几个参数怎么设。

# client_demo.py import os from openai import OpenAI client = OpenAI( api_key=os.environ["DS_API_KEY"], base_url=os.environ["DS_BASE_URL"], # 填接入方给你的地址 timeout=60.0, # 推理模型要给足,默认值经常不够 max_retries=2, # SDK 自带重试,别自己再套一层 ) def ask(prompt: str, model: str, stream: bool = False): resp = client.chat.completions.create( model=model, messages=[{"role": "user", "content": prompt}], temperature=0.2, # 抽取类任务压到 0.2 上下;推理模型上通常无效 max_tokens=1024, # 推理模型要把思考 token 算进预算,别卡太死 stream=stream, ) if stream: # 流式只拼 content,推理字段不参与业务 for chunk in resp: delta = chunk.choices[0].delta if getattr(delta, "content", None): yield delta.content else: return resp.choices[0].message.content

五个必调参数分别是 model、temperature、max_tokens、stream、timeout。timeout和max_retries是在客户端构造时设的,很多人只在单次调用里设超时,结果 SDK 层重试时用的还是默认值,长任务照样断。max_tokens对推理模型尤其要放宽,因为思考内容也算在里面,卡 512 的话经常思考到一半就被截断,返回的正文是空的——这个现象特别容易误判成模型故障。

流式输出在交互界面上是刚需,但批处理场景别开。流式意味着你要自己处理分片拼装、异常中断、半句话截断,这些在批量任务里都是纯负担。我一般的做法是:面向人的界面开流式,面向管道的任务关掉。

4.2 长 PDF 的分块、摘要与回填

那份手册 PDF 本身就是个很好的测试样本——几十页、有表格、有代码片段。直接整篇塞进上下文有三个问题:一是超窗口,二是中间部分容易被忽略,三是每次问答都全量计费。所以要分块。

分块策略上,按固定字数硬切是最省事也最差的做法,它会把表格从中间劈开。我一般按标题层级切,一级标题一 chunk,二级标题再拆,每个 chunk 控制在 800 到 1200 个 token,相邻 chunk 留 100 到 150 的重叠,防止一句话被切断导致语义丢失。表格单独抽出来走一条链路,转成 Markdown 表格再喂,比让它嵌在正文里识别率高得多。

# chunk_and_summarize.py # 先切块,再逐块摘要,最后聚合,避免一次性喂整篇 def split_by_heading(text: str, max_len: int = 1000, overlap: int = 120): """按标题行切分,超长的再按长度兜底切,保留重叠区""" lines, chunks, buf = text.splitlines(), [], [] for line in lines: # 遇到标题且当前缓冲区已经够长,就落一块 if line.startswith("#") and len("\n".join(buf)) > max_len: chunks.append("\n".join(buf)) # 重叠:保留上一块的最后一段,防止跨块语义断裂 buf = ["\n".join(buf)[-overlap:]] buf.append(line) if buf: chunks.append("\n".join(buf)) return chunks def summarize_each(chunks, ask_fn, model): """逐块摘要,提示词里强制要求保留数字和专有名词""" out = [] for c in chunks: p = ("用三句话概括以下内容,保留所有数字、参数名和结论性表述," "不要评价,不要补充原文没有的信息。\n\n" + c) out.append(ask_fn(p, model=model)) return out

参数上有两个地方值得调。max_len不是越大越好,一千 token 左右是实测下来召回和成本的平衡点,超过两千之后单块的摘要质量反而下降,因为模型开始抓不住重点。overlap不能省,我见过有人设成 0,结果每块开头都是半句话,摘要出来的东西前言不搭后语。

聚合那一步也有讲究。逐块摘要完成后不要简单拼接,要让模型做一次归并去重,因为相邻 chunk 的重叠区会产生重复结论。归并的提示词里加一句「相同结论合并为一条,冲突的结论并列保留并标注来源块编号」,能明显减少最终输出里的重复段落。

4.3 延迟与成本:一张能拿去做决策的对照表

下面这张表是我自己压测后的相对关系,绝对值会随接入方和时段变化,看比例就够了:

链路相对延迟相对成本适用场景
通用模型 + 短提示1x1x抽取、分类、改写
通用模型 + 长文档全量3x ~ 5x4x ~ 6x不推荐,改用检索
推理模型 + 短提示4x ~ 8x3x ~ 5x规划、调试、复杂判断
分块摘要 + 归并6x ~ 10x2x ~ 3x一次性处理长文档,结果可复用
检索 + 通用模型2x1.5x长文档高频问答,首选

省成本最有效的动作不是换模型,是把稳定的前缀固定下来。系统提示、格式说明、示例这几段放在消息数组最前面,且每次内容完全一致,命中前缀缓存后这部分基本不重复计费。反过来说,如果你每次都把时间戳、随机 ID 拼在提示开头,缓存永远命不中,成本会一直是满额。这个细节在手册里通常不会写,但它对批量任务的花费影响是数量级的。

5. 避坑排查:五个「以为是模型不行」的现场

5.1 推理模型返回一大段思考,正文却是空的

现象:调用推理模型,返回里有一大段分析过程,但content字段是空字符串,下游直接报错。

原因有两个。最常见的是max_tokens设得太小,思考过程把预算吃完了,正文还没开始生成就被截断;另一种是解析代码写错了,把带 reasoning 前缀的字段当成正文取,取到的是思考内容,业务侧一判断格式不符就当成空。

解决:先把max_tokens提到 2048 以上复测一次,能出正文就是预算问题。解析层统一只取content,并且在取不到时显式判断finish_reason,如果是长度截断就自动放大预算重试一次,而不是直接把空串传下去。这个判断很便宜,能挡掉大部分线上「模型答非所问」的假故障。

5.2 JSON 解析十次错三次

现象:要求输出 JSON,实际返回里混着「好的,以下是结果:」这种前缀,或者末尾多一个逗号,json.loads直接抛异常。

原因:一是没开结构化输出模式;二是开了但提示里没出现 json 这个词、也没给字段样例,模型不知道要遵守;三是字段太多,模型在中途开始自由发挥。

解决:先开结构化输出模式,然后提示词里显式写「输出 JSON」并给一个两字段的极简样例,字段控制在六个以内。解析层再包一层容错:去掉代码块围栏、截取第一个左花括号到最后一个右花括号之间的内容、失败后带着错误信息重试一次。三层下来成功率能到 99% 以上,剩下那 1% 走降级返回空结构,不要让整批任务挂掉。

5.3 长文档问答答非所问

现象:PDF 明明写了某个参数,问它的时候答「文中未提及」,或者答的是另一个章节的内容。

原因:多半是分块把相关内容切散了,或者检索只按向量相似度召回,把标题信息丢了。手册类文档尤其严重,因为答案常常在表格里,而表格在切块时经常被拆成几段。

解决:分块时给每块补上标题路径(一级标题 / 二级标题),这样即使内容被切散,块本身还带着上下文;表格单独抽取成 Markdown 后单独建索引;召回改成「向量 + 关键词」双路,参数名这种精确词走关键词路更靠谱。调完之后先别急着上线,拿二十个已知答案的问题跑一遍,看召回块里有没有正确段落,没有就是切块问题,有但答错才是模型问题。

5.4 同一提示两次结果差很多

现象:完全一样的输入,上午跑出来是 A,下午跑出来是 B,字段还少了一个。

原因:通用模型默认温度不低,采样本身就带随机性;另外提示里有隐含变量,比如把当前时间、随机排序的候选列表拼进去了;还有一种情况是对话历史被带上了,上一轮的残留影响了这一轮。

解决:抽取类任务把温度压到 0.2 以下;每轮请求都从干净的消息数组开始,不复用历史;排查时把完整请求体打日志,对比两次的差异,通常能立刻看到那个多出来的变量。这里有个血泪经验:日志要打完整请求体,只打提示词没用,参数才是最常见的隐形变量。

5.5 并发一上来就超时

现象:单条测试都正常,压到二十并发之后大量超时和限流错误。

原因:没有客户端侧的限流,SDK 自带的重试在高峰期会把请求放大,越重试越堵;另外推理模型的单次耗时本来就长,连接池开得太小会排队。

解决:加一个令牌桶在调用前做限流,超出配额的请求进队列而不是直接发;重试改成指数退避加随机抖动,别用固定间隔;超时时间按模型分开配,推理模型单独给到 120 秒。做之前先量一下 P95 延迟,把并发数卡在系统能稳住的位置,别追求峰值吞吐,长任务场景下稳定比快重要。

6. 把「精通」变成可验证:20 条回归集怎么建、怎么跑

调提示词最怕的是改完 A 场景好了、B 场景坏了,而你只测了 A。这类问题的解法很土但管用:建一个二十条左右的小回归集,每次改提示词、换模型、调参数之前先跑一遍。

回归集不用大,关键在覆盖面。我的分法是四个维度,每个维度五条:

维度条数覆盖内容通过标准
正常输入5结构完整、信息齐全的典型样本字段齐全且值正确
边界输入5空输入、超长输入、纯符号输入不崩溃,返回明确的空结果标记
信息缺失5关键字段缺失、内容自相矛盾缺失字段填 unknown,不编造
格式约束5要求 JSON、要求字数上限可被json.loads直接解析

跑法上不要人工看,写成脚本自动判定,只把失败项打出来看。

# regress.py # 只做机械判定:能否解析、字段是否齐全、缺失项是否标 unknown import json def check(case: dict, output: str) -> tuple[bool, str]: """case 里有 expect_keys 和 allow_unknown 两个字段""" try: # 去掉可能存在的代码块围栏再解析 raw = output.strip().removeprefix("```json").removeprefix("```").removesuffix("```") data = json.loads(raw) except Exception as e: return False, f"解析失败: {e}" missing = [k for k in case["expect_keys"] if k not in data] if missing: return False, f"缺字段: {missing}" if case.get("allow_unknown") is False: # 明确不允许 unknown 的用例,出现即失败,用来抓编造 bad = [k for k, v in data.items() if str(v).lower() == "unknown"] if bad: return False, f"不该出现 unknown 的字段: {bad}" return True, "ok" def run(cases, ask_fn, model): fails = [] for c in cases: ok, msg = check(c, ask_fn(c["prompt"], model=model)) if not ok: fails.append({"id": c["id"], "msg": msg}) return fails

这段代码的重点不在逻辑复杂度,在于allow_unknown这个开关。大部分回归集只检查字段齐不齐,结果模型学会了用 unknown 填满所有字段来骗过测试。给一部分用例设成不允许 unknown,才能把编造行为抓出来。

跑回归集的时机我定在三个节点:改提示词之后、换模型标识之后、以及每周固定一次全量跑。前两个是必须,第三个是为了抓那些不声不响的行为漂移——模型侧的小版本更新你未必能第一时间知道,但回归集能。

最后说个我的习惯:每次回归集出现新失败,先不要改提示词,先把失败样本补进用例集,再改。这样用例集是只增不减的,半年之后它就是你手里最值钱的东西,比任何一份「从入门到精通」的文档都贴近你自己的业务。真正让你从入门走到精通的,不是把手册读完,是手里有一套能随时告诉你「这次改动到底有没有变差」的尺子。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询