简介:《DeepSeek从入门到精通》是一份来自清华大学新闻与传播学院新媒体研究中心的PDF学习材料,面向AI初学者、开发者及内容从业者,帮助读者建立对DeepSeek-R1推理模型的整体认知,并掌握从基础使用到提示语设计的进阶方法。资源为1个PDF文件,压缩包约5.5MB,便于在电脑或移动设备上直接阅读。内容围绕“DeepSeek是什么、能做什么、如何使用”展开,涵盖智能对话、文本生成、语义理解、计算推理与代码生成等应用场景,并进一步比较推理模型与通用模型在优势领域、决策方式和创造力上的差异,解释快思慢想与CoT链式思维对模型选择的影响。文中还结合数学证明、创意写作、代码生成等任务,说明结果精准、需求导向、混合模式与启发式提问等提示语策略的适用边界。已有1873人学习,适合希望系统梳理DeepSeek能力图谱、优化提示语设计的读者查漏补缺。
1. 一份被反复转发的 DeepSeek 讲义,真正值钱的是中间那几十页
DeepSeek 这两个字最近被塞进各种标题里,但真正让人反复翻看的,是一份从入门到精通的培训讲义——尤其是中间讲模型边界、参数和失败案例的那几十页。入门部分告诉你它是什么,精通部分才告诉你它什么时候不灵。这份材料的价值不在“多强”,而在“怎么用才不翻车”:同一个模型,提示词差一行、max_tokens 差一个数量级、量化级别差一档,产出质量能差出一个可用和一个不可用。这篇笔记按“选型—接入—调参—避坑—自检”的顺序拆一遍,适合已经拿到 API Key 或一张 24G 显卡、准备把 DeepSeek 塞进自己业务流程的工程师,也适合刚看完简介、想知道下一步该动哪里的新手。
2. 模型选型:DeepSeek 的推理版、通用版和蒸馏版,到底该把哪个放进生产
2.1 推理模型和通用对话模型的分界线:什么时候值得让它“先想再答”
DeepSeek 家族里最容易混淆的是两类模型:一类是通用对话模型,输入输出都走同一个自回归通道,响应快、成本低;另一类是推理模型,在给出最终答案前会先生成一段内部推理内容,再把结论吐出来。前者的典型代表是 deepseek-chat 这一档,后者的代表是 deepseek-reasoner。两者不是“新旧关系”,而是两条并行的产品线,选错了不会报错,只会让你觉得“这模型怎么又慢又贵”或者“这模型怎么答得这么浅”。
原理上的差别在于训练目标和输出分布。推理模型经过强化学习打磨,擅长多步推导、数学证明、代码调试、复杂规则匹配这类“中间步骤不能省”的任务;它愿意花几千个 token 去想,换一个更稳的结论。通用模型则被优化成“给定上下文,快速给出合理续写”,在分类、抽取、改写、摘要、客服问答这类任务上性价比高得多。判断标准很简单:如果一个任务让人来做也需要打草稿,就用推理版;如果一个任务人看一眼就能答,就用通用版。
这里有个反直觉的结论:推理模型并不是“更聪明的通用模型”。把它拿来做简单的情感分类,它会在推理段里反复纠结“这句话到底算不算负面”,最后输出一个和通用模型差不多的标签,但 token 消耗翻了三到五倍,延迟从一秒级拉到十秒级。我一般会先用通用模型跑一遍样本,把准确率低于阈值、且错误集中在多步逻辑上的那部分任务单独切出来,再换推理模型做 A/B,而不是一上来就全量上推理版。
另一个选型维度是蒸馏版。蒸馏版把大模型的能力压到 1.5B、7B、8B、14B、32B、70B 这几档参数规模上,好处是能塞进单卡甚至笔记本,坏处是“压扁”过程中最先丢的往往是长链条推理和冷门知识。常见做法是:对延迟敏感、数据不能出内网的场景,用 14B 或 32B 的蒸馏版做第一道过滤;对准确率要求高、可以接受秒级到十秒级延迟的场景,走 API 的推理版;对成本极敏感、任务又简单的批量离线场景,用 7B 蒸馏版加规则后处理。别指望 7B 蒸馏版在复杂代码修复上接近满血版,那是选型错位,不是模型缺陷。
2.2 从 1.5B 到 671B:一张按显存和任务匹配的选型表
参数规模不等于显存占用,这是本地部署最容易算错的一笔账。像 DeepSeek-V3 这类混合专家结构,总参数 671B,但每个 token 只激活其中约 37B 的专家,算力按激活参数估,显存却要按总参数加 KV Cache 估。很多人看到“激活 37B”就以为一张 80G 卡能跑,实际权重加载阶段就放不下。下面这张表是按常见量化级别和单机场景整理的粗算,不是精确 benchmark,但足够用来排除“根本跑不起来”的方案。
| 模型档位 | 参数结构 | 常见量化 | 权重显存粗算 | 建议硬件 | 典型任务 |
|---|---|---|---|---|---|
| 1.5B 蒸馏版 | 稠密 | Q4_K_M | 1.5~2.5 GB | 笔记本 CPU / 8G 显卡 | 短文本分类、关键词抽取 |
| 7B~8B 蒸馏版 | 稠密 | Q4_K_M | 5~7 GB | 12G 显卡 | 摘要、客服问答、简单改写 |
| 14B 蒸馏版 | 稠密 | Q4_K_M | 9~12 GB | 16G 显卡 | 中等长度文档理解、结构化抽取 |
| 32B 蒸馏版 | 稠密 | Q4_K_M | 20~24 GB | 24G 显卡 / 双卡 | 代码补全、复杂抽取、多轮工具调用 |
| 70B 蒸馏版 | 稠密 | Q4_K_M | 40~48 GB | 48G 及以上 | 高精度离线批处理 |
| 满血通用版 | MoE 671B / 激活 37B | FP8 或 INT8 | 数百 GB 级 | 多卡服务器 | 高并发通用对话、复杂生成 |
| 满血推理版 | MoE 671B / 激活 37B | FP8 或 INT8 | 数百 GB 级 | 多卡服务器 | 数学、代码、多步规划 |
表格里最容易被忽略的是 KV Cache。上下文从 8K 拉到 64K,KV Cache 可能从几 GB 涨到十几 GB,32B 模型在 24G 卡上跑 32K 上下文时,权重占 22G,KV Cache 一挤就 OOM。所以本地选型不能只看“模型能不能加载”,要看“目标上下文长度下还能不能留出并发余量”。我的习惯是先按目标上下文的 1.5 倍估 KV Cache,再倒推量化级别:24G 卡跑 32B,Q4 加 16K 上下文是相对稳的甜点;要跑 64K,要么降到 14B,要么上双卡。
2.3 用一次带探针的请求确认模型身份和上下文边界
选型表是纸面判断,真正接进工程前,先发一条“探针请求”确认三件事:当前 endpoint 实际路由到哪个模型、输出上限是多少、超长上下文会不会静默截断。下面的写法用社区通用的 Chat Completions 兼容 SDK,重点看返回里的 model 字段、usage 和 finish_reason。
from openai import OpenAI client = OpenAI( api_key="你的_key", base_url="https://api.deepseek.com", # 兼容 /v1 前缀的写法也可 timeout=120, max_retries=2, ) resp = client.chat.completions.create( model="deepseek-chat", # 先测通用版,再换 deepseek-reasoner 对比 messages=[ {"role": "system", "content": "你是一个只输出结论的助手。"}, {"role": "user", "content": "用一句话说明 17 乘以 23 的计算过程,然后给结果。"}, ], temperature=1.0, max_tokens=256, stream=False, ) print("model:", resp.model) print("finish_reason:", resp.choices[0].finish_reason) print("usage:", resp.usage) print("content:", resp.choices[0].message.content)这段代码的逻辑是:用一道有中间步骤的算术题同时观察“会不会展开推理”和“输出有没有被截断”。finish_reason是stop说明正常结束,是length说明max_tokens给少了,这在生成长报告时极其常见。usage里的prompt_tokens和completion_tokens用来校准成本,推理模型的completion_tokens往往包含看不见的推理段,账单会比直觉高。temperature=1.0是通用对话的常见起点,换到推理模型时这个参数可能不生效,下一章会细说。
确认上下文边界时,构造一段约 3 万字的已知文本,开头埋一个唯一标记,结尾问“第一行的标记是什么”。如果模型答不出来但接口不报错,说明中间发生了截断,而不是模型能力问题。这个探针花不了几分钱,却能省掉后面排查“为什么长文档问答总是丢开头”的几个小时。
3. 接入路径:API、本地推理、批量离线,三条路的配置与显存账
3.1 用兼容协议接 DeepSeek:最小配置里的三个必改项
大多数团队接 DeepSeek 的第一条路是 API,因为不需要碰显卡。兼容协议的 SDK 基本能直接改base_url和model就跑,但默认配置里有三个值必须改,否则上线后一定出问题:超时、重试、并发。
默认超时往往很长,长到上游已经挂了,你的线程还挂在那里等;对话类接口我一般设 60~120 秒,推理类设 180~300 秒,超过这个时间宁可让任务进重试队列。默认重试次数通常偏少,网络抖动一次就丢任务;但重试也不能无脑拉满,要配合指数退避和幂等键,否则会把限流放大成雪崩。并发默认没有上限,批量任务一开就是几百个协程,结果全撞在 429 上。
import time from openai import OpenAI, RateLimitError, APITimeoutError client = OpenAI( api_key="你的_key", base_url="https://api.deepseek.com", timeout=180.0, # 推理类任务给足时间,对话类可降到 60 max_retries=0, # 关掉 SDK 内置重试,自己控制退避 ) def ask(prompt: str, model: str = "deepseek-chat", max_tokens: int = 1024): for attempt in range(4): try: resp = client.chat.completions.create( model=model, messages=[{"role": "user", "content": prompt}], max_tokens=max_tokens, temperature=1.0, ) return resp.choices[0].message.content except (RateLimitError, APITimeoutError) as e: wait = min(2 ** attempt + 0.5 * attempt, 30) time.sleep(wait) # 指数退避,封顶 30 秒 except Exception as e: raise raise RuntimeError("重试耗尽,转入死信队列")这里把 SDK 自带重试关掉,是为了让退避策略和业务日志对齐:每次重试都能打上同一个任务 ID,后面查“为什么这条数据跑了四遍”才有据可查。max_tokens显式传值,不依赖服务端默认,是因为不同模型的默认输出上限不一样,长报告场景下不传等于给自己埋截断。temperature=1.0只对通用对话有意义,推理模型这一项会被忽略,别在这里反复调。
还有一个容易忽略的配置是流式开关。做前端打字机效果时开流式,做批量离线时关流式,因为流式会让错误处理变复杂:连接在中途断了,你已经收到一半内容,这时候是重试整条还是拼接?我一般只在交互式场景开流式,并且给每次流式请求记一个request_id,断流时按 ID 重放,而不是在客户端拼残片。
3.2 本地起推理服务:显存账怎么算,量化级别怎么定
本地推理的吸引力在于数据不出内网和边际成本低,但第一道坎是显存。前面那张表给的是权重占用,实际启动时还要加 KV Cache、框架开销和碎片余量。一个够用的估算式是:
显存需求 ≈ 权重显存 × 1.1 + KV Cache + 1~2 GB 框架开销
KV Cache 和上下文长度、批大小、层数成正比。以 32B 稠密模型、16K 上下文、批大小 1 为例,Q4 量化下权重约 20~22 GB,KV Cache 约 2~4 GB,框架开销 1~2 GB,24G 卡刚好卡在边缘。把上下文拉到 32K,KV Cache 翻倍,立刻 OOM。所以本地部署的第一个参数不是“温度”,是max_model_len,先把它压到业务真正需要的长度,再谈并发。
用本地模型运行工具拉一个 7B 蒸馏版做冒烟测试,命令通常长这样:
# 拉取并运行 7B 蒸馏版,Q4 量化 ollama run deepseek-r1:7b # 指定上下文长度和并发数启动服务 OLLAMA_NUM_PARALLEL=2 OLLAMA_MAX_LOADED_MODELS=1 ollama serveOLLAMA_NUM_PARALLEL控制同时处理的请求数,设大了 KV Cache 会按倍数涨;OLLAMA_MAX_LOADED_MODELS=1防止多个模型同时驻留把显存吃光。7B 模型在 12G 卡上跑 8K 上下文是比较稳的组合,再往上就要看量化级别。
要跑 32B 或更高吞吐,常见做法是换推理服务框架,用张量并行和 PagedAttention 把显存管起来。启动参数里最影响稳定性的是这三个:--gpu-memory-utilization控制框架愿意占用的显存比例,设 0.9 比设 1.0 更稳,留出碎片空间;--max-model-len直接决定 KV Cache 上限,按业务需要设,不要照抄模型的最大上下文;--tensor-parallel-size是卡数,单卡设 1,双卡设 2,设错会直接起不来。量化级别上,Q4_K_M 是质量和体积的常见甜点,Q2、Q3 能省显存但长上下文下退化明显,后面避坑章会展开。
3.3 批量离线任务的切片、并发与断点续跑
批量离线是最容易“看起来跑完了、其实丢了一半”的场景。常见做法是把输入写成 JSONL,一行一个任务,带唯一 ID;用信号量控制并发;每完成一条就追加写结果文件;失败任务单独落到死信文件。这样进程被 kill 或机器重启后,从结果文件里读出已完成的 ID,跳过它们继续跑,而不是从头再来。
import asyncio, json, os async def worker(task, sem, client, out_fp, dead_fp): async with sem: try: resp = await asyncio.to_thread( client.chat.completions.create, model="deepseek-chat", messages=[{"role": "user", "content": task["prompt"]}], max_tokens=1024, ) record = {"id": task["id"], "ok": True, "content": resp.choices[0].message.content} out_fp.write(json.dumps(record, ensure_ascii=False) + "\n") out_fp.flush() except Exception as e: dead_fp.write(json.dumps({"id": task["id"], "err": str(e)}, ensure_ascii=False) + "\n") dead_fp.flush() async def run(tasks, concurrency=8): sem = asyncio.Semaphore(concurrency) done = set() if os.path.exists("out.jsonl"): with open("out.jsonl", encoding="utf-8") as f: done = {json.loads(line)["id"] for line in f if line.strip()} with open("out.jsonl", "a", encoding="utf-8") as out_fp, \ open("dead.jsonl", "a", encoding="utf-8") as dead_fp: pending = [t for t in tasks if t["id"] not in done] await asyncio.gather(*[worker(t, sem, client, out_fp, dead_fp) for t in pending])concurrency是最关键的一个数,不是越大越好。API 场景下,8~16 是常见起点,撞到 429 就降到 4~8;本地单卡场景下,并发数基本等于OLLAMA_NUM_PARALLEL,设 2 就是 2,多开的请求只会排队。flush()每写一行调一次,牺牲一点吞吐换断点续跑的可靠性,批量任务里这笔账划算。死信文件不要只记错误字符串,把原始 prompt 的哈希也记上,后面才能判断是输入太长、内容触发风控,还是单纯网络超时。
4. 提示词与采样参数:DeepSeek 上四个必调项的取值区间和副作用
4.1 推理模型的提示词要“给目标不给步骤”
通用模型的提示词习惯是“角色 + 任务 + 格式 + 示例”,推理模型不完全吃这一套。它对“一步步想”这类指令不敏感,因为推理段是训练出来的,不是你让它想它才想。更有效的写法是给清楚验收标准:输出必须满足什么条件、边界情况怎么处理、什么算合格。你给的是目标函数,不是解题步骤。
一个常见的翻车写法是:“请你一步一步思考,先分析 A,再分析 B,最后得出结论。”在推理模型上,这句话要么被忽略,要么让它在推理段里机械复述你的步骤,反而挤掉了真正有用的推导。换成“结论必须同时满足以下三条约束,若任一条不满足则输出‘无法判定’并说明缺哪条数据”,输出稳定性会明显不一样。约束越可检验,模型越不容易自由发挥。
系统提示词也要短。推理模型对长系统提示的遵循度并不比通用模型高,塞进去十几条“你必须”“你永远不要”,最后往往只记住前两条。我的习惯是把系统提示压到三行以内,只放身份和硬约束,把任务细节放到用户消息里,这样同一套系统提示可以复用到多个任务,调试时也容易定位是哪一段在起作用。
| 写法 | 示例 | 在 DeepSeek 上的表现 |
|---|---|---|
| 步骤指令 | “先分析 A,再分析 B,最后给结论” | 推理段重复步骤,有效信息被挤占 |
| 目标约束 | “结论需满足三条约束,缺数据则输出无法判定” | 推理段围绕约束展开,结论更可检验 |
| 长系统提示 | 十几条“必须/永远” | 遵循度下降,前几条权重过高 |
| 短系统提示 + 任务消息 | 系统只放身份,任务放用户消息 | 复用性好,定位问题快 |
4.2 temperature、top_p、max_tokens、frequency_penalty 的取值与副作用
这四个参数里,真正需要按任务调的是max_tokens和temperature,另外两个多数时候保持默认。先给一张取值表,再解释副作用。
| 参数 | 常见取值 | 作用 | 调过头的副作用 |
|---|---|---|---|
| temperature | 0.2~0.7(抽取/分类);0.8~1.0(创意/对话) | 控制随机性 | 太低会重复、死板;太高会跑题、编造 |
| top_p | 0.9~0.95 | 核采样,截断低概率词 | 和 temperature 同时大幅调低会让输出过于保守 |
| max_tokens | 按任务设,长报告 2048~8192 | 输出上限 | 设小了静默截断,设大了浪费预算 |
| frequency_penalty | 0~0.3 | 惩罚重复词 | 调高会让专有名词被替换成同义词 |
| presence_penalty | 0~0.3 | 鼓励引入新话题 | 调高会让结构化输出漏字段 |
temperature和top_p不要同时大幅调。常见误区是“为了稳,把 temperature 设 0.1、top_p 设 0.1”,结果模型在几个高概率词之间反复横跳,长文本开始循环。更稳的做法是 temperature 设 0.3 左右、top_p 保持 0.95,让低概率词仍有一点点机会,避免陷入重复循环。
max_tokens是最容易被低估的一项。通用对话默认输出上限往往只有 4K 左右,你让它写一份三千字的技术方案,它写到一半被截断,finish_reason是length,但很多客户端不检查这个字段,于是你拿到一份“结论突然消失”的文档。长输出任务要显式把max_tokens提到 4096 或 8192,同时接受成本上升;如果任务真的需要更长,正确做法是分段生成再拼接,而不是把上限拉到极限赌它一次写完。
frequency_penalty在中文场景下要慎用。中文的专有名词、型号、代码标识符本来就是低频词,惩罚一开,模型可能把“Q4_K_M”改写成“4 位量化中等版本”,看起来更通顺,实际把可解析的字段弄丢了。结构化抽取任务我一般把这两个 penalty 都设 0,把多样性交给 temperature 控制。
4.3 用 JSON 模式把自由文本压成可解析结构
只要下游要进数据库或触发流程,自由文本就是负债。DeepSeek 的兼容接口支持 JSON 输出模式,但有个前提:提示词里必须出现 “json” 字样,并且给出字段说明,否则模型可能返回一段带 Markdown 代码围栏的伪 JSON,解析器一上来就崩。
import json schema_hint = """ 只输出 json,不要输出任何解释。字段如下: { "sentiment": "positive|negative|neutral", "confidence": 0.0 到 1.0 之间的浮点数, "reason": "不超过 30 字的中文理由" } """ resp = client.chat.completions.create( model="deepseek-chat", messages=[ {"role": "system", "content": "你是一个只输出 json 的文本分类器。"}, {"role": "user", "content": schema_hint + "\n评论:物流很快,但包装有点破损。"}, ], response_format={"type": "json_object"}, temperature=0.2, max_tokens=256, ) raw = resp.choices[0].message.content data = json.loads(raw) # 失败时进下面的兜底 assert data["sentiment"] in {"positive", "negative", "neutral"}response_format只是“尽量保证是合法 JSON”,不保证字段齐全、不保证枚举值落在你给的集合里。所以后面必须加一层校验:枚举值用集合判断,数值范围用区间判断,缺字段直接进重试或人工队列。兜底逻辑可以简单地把 ```json 围栏剥掉再解析一次,但不要用正则去“猜”字段,猜错一次污染的是整张表。
temperature=0.2是为了让分类结果稳定,同一批数据跑两遍标签不一致,后面的评估就没法做。max_tokens=256对这种短结构化输出足够,设大了反而给模型留出“多写点解释”的空间。如果任务需要多字段、嵌套结构,先把 schema 压平,DeepSeek 对扁平 JSON 的字段完整率明显高于深层嵌套,这是实操里的取舍,不是理论最优。
5. 避坑与排查:DeepSeek 上线前最常翻车的 5 个现场
5.1 把推理模型当通用模型调,采样参数全部失效
现象:代码里明明设了temperature=0.3、top_p=0.8,换到推理模型后输出风格完全没变,重复率也没下降,日志里也看不到报错。
原因:推理模型的主输出通道和通用模型不同,采样类参数在它身上可能被忽略或只作用于最终答案段,推理段本身不走这套采样。你调的参数没有报错,但也没有生效,属于“静默失效”。
解决:把推理模型和通用模型的调用配置分开维护。推理模型只传max_tokens和必要的停止条件,不再传temperature、top_p、frequency_penalty;需要控制输出长度就压max_tokens,需要控制格式就在提示词里给约束。上线前用同一提示词分别打两个模型,对比usage.completion_tokens和输出长度,确认参数是否真的起作用。
5.2 输出到一半突然截断,日志里没有异常
现象:批量生成技术摘要,前 200 条正常,后面开始出现“最后一句没写完”的记录,接口返回 200,日志无报错。
原因:max_tokens设得刚好卡在平均输出长度附近,长尾样本一超限就被截断,finish_reason变成length,但客户端只取了content,没检查这个字段。
解决:所有调用统一检查finish_reason,等于length的记录打标重跑,并把max_tokens提到目标长度的 1.5 倍;仍然超长的任务改成分段生成。批量任务的结果表里加一列finish_reason,每天统计一次length占比,超过 1% 就说明上限该调了,而不是等用户投诉才发现。
5.3 本地量化到 Q2 之后,模型开始“胡说但很自信”
现象:为了把 32B 塞进 24G 卡,用了 Q2 量化,短问题回答正常,一到多步推理和长文档问答就开始编造不存在的条款,语气还特别肯定。
原因:低比特量化最先损失的是长尾知识和细粒度逻辑,模型不是“不知道”,而是把相邻的、高概率的错误模式补了上来。上下文越长,误差累积越明显。
解决:24G 卡跑 32B 时优先 Q4_K_M,跑不动就降到 14B 的 Q4,而不是把 32B 压到 Q2。必须用低比特时,把上下文压到 8K 以内,并在提示词里要求“不确定时输出无法判定”,给模型一个合法的退出通道。上线前用同一批 50 条样本对比 Q4 和 Q2 的字段完整率,差距超过 5 个百分点就不要省这点显存。
5.4 流式返回的中文出现乱码或半个字
现象:前端打字机效果偶尔蹦出乱码,或者一句话中间卡住半个汉字,刷新后又正常。
原因:流式响应按字节块传输,一个 UTF-8 汉字占 3 个字节,可能被切在两个块之间。客户端如果对每个块单独decode(),切开的字节就被解成替换字符。
解决:用增量解码器缓存不完整字节,而不是逐块强解。Python 里可以用codecs.getincrementaldecoder("utf-8")(),每次decode(chunk, final=False),连接结束时再final=True冲刷。前端同理,用TextDecoder的{stream: true}模式。这个坑和模型无关,但排查时很容易误判成“模型输出脏数据”。
5.5 并发一上来就 429,重试反而更糟
现象:批量任务从 4 并发提到 32 并发,前几分钟吞吐上升,随后大面积 429,重试日志刷屏,最终完成时间比 4 并发还长。
原因:没有令牌桶或信号量做全局节流,重试又没有退避,失败请求立刻重新入队,把限流窗口越撞越满。SDK 自带重试如果开着,还会在底层悄悄放大请求量。
解决:并发数从 8 起步,按 429 比例动态下调;重试走指数退避加随机抖动,封顶 30 秒;关掉 SDK 内置重试,把重试计数落到任务维度,同一个任务超过 3 次进死信队列人工处理。监控上只看两个指标:429 比例和 P95 完成时间,前者升后者也升,说明该降并发而不是加重试。
6. 上线自检:用 50 条样本把 DeepSeek 的输出质量压成一张可对比的表
调参调到最后,最容易陷入“感觉还行”的玄学。我的习惯是上线前固定跑一轮 50 条样本的自检,样本覆盖短文本、长文本、结构化输出、边界输入四类,每条都过同一套指标,产出一张能横向对比的表。脚本不复杂,关键是每次都跑同一批、记同样的字段。
import json, statistics def evaluate(samples, runner): rows = [] for s in samples: out = runner(s["prompt"]) # 内部已带重试和超时 rows.append({ "id": s["id"], "len": len(out), "json_ok": try_json(out) if s["expect_json"] else None, "keyword_hit": sum(k in out for k in s["keywords"]) / len(s["keywords"]), "finish": out.get("finish_reason", "unknown"), }) return rows def summarize(rows): return { "json_parse_rate": statistics.mean(r["json_ok"] for r in rows if r["json_ok"] is not None), "avg_keyword_hit": statistics.mean(r["keyword_hit"] for r in rows), "truncate_rate": statistics.mean(r["finish"] == "length" for r in rows), "avg_len": statistics.mean(r["len"] for r in rows), }json_parse_rate低于 98% 就不要上结构化链路,先把提示词里的 schema 示例补全;truncate_rate高于 1% 就调max_tokens或改分段;avg_keyword_hit用来抓“答得很流畅但关键信息漏了”的情况,比人眼看十篇都准。同一批样本换模型、换量化、换提示词各跑一遍,四列数字摆在一起,选型就不再是拍脑袋。
| 指标 | 及格线 | 低于及格线先查什么 |
|---|---|---|
| JSON 解析成功率 | ≥ 98% | 提示词是否含 json 字样、schema 是否扁平 |
| 截断率 | ≤ 1% | max_tokens 是否够、是否该分段 |
| 关键词命中率 | ≥ 85% | 提示词约束是否可检验、上下文是否被截 |
| 平均输出长度 | 与目标偏差 ≤ 30% | temperature 是否过高、系统提示是否太松 |
这套自检我踩过的最大坑,是样本集一开始只放“正常输入”,跑出来漂亮得不像话,上线后遇到空字符串、超长文本、纯符号输入,整条链路直接崩。后来固定要求样本里至少 10% 是边界输入:空输入、只有标点的输入、超过上下文一半的长文本、带 JSON 特殊字符的文本。自检脚本不追求覆盖率,追求的是每次改动后你能用同一把尺子量出“变好了还是变坏了”。希望帮到你。
本文还有配套的精品资源,点击获取