DeepSeek API实现文本与图像分类:提示词设计与工程避坑指南
简介:面向有Python编程经验的技术开发者,这份指南系统讲解借助DeepSeek API完成图像分类与文本分类任务的完整流程,也适用于正在集成自动化AI分类系统的开发团队。资源覆盖从账号注册与API密钥获取、requests和Pillow等依赖库安装,到POST请求的URL、HTTP头与请求主体构造细节,并配有可直接修改运行的代码示例。针对图像任务,重点说明如何通过files参数上传图片,并演示用Pillow调整尺寸等预处理;针对文本任务,则给出JSON负载的组装方式。对于返回结果,逐一说明如何解析分类标签与置信度字段;遇到错误时,则介绍了校验API密钥、核对请求负载格式等常见排查思路。资源包为单个docx文档,大小仅17KB,便于离线查阅与复制关键代码片段。目前已有2675人学习,适合正在快速构建AI应用原型的开发者参考。
1. 视觉与文本分类,为什么先想到 DeepSeek API
先抛一个反直觉的结论:DeepSeek 官方 API 并不提供“图像分类模型”和“文本分类模型”这样两个独立接口,但它是目前做这两类任务最顺手的入口之一。原因不在于它有多强,而在于它的 chat 接口同时支持文本输入和图像输入,并且返回格式可控。换句话说,你不需要分别接一个文本分类服务和一个图像分类服务,只用一个 API,用提示词工程的方式就能把两类任务都跑起来。
这篇文章面向两类人。一类是刚接触大模型 API,想把分类需求落地,但不确定怎么设计提示词、怎么解析返回结果的开发者;另一类是已经能调通 chat 接口,但被分类不稳定、输出格式不固定、图片传不进去这些问题卡住的工程师。后面这类人最常见的困惑是:模型能力没问题,为什么我的代码里总在报 JSON 解析错误或者 400 上下文超限。这篇文章会把从密钥申请到两类分类任务的全过程拆开讲,每一步给出可复现代码和参数说明,并标注我实际踩过的坑。如果你正准备把文本分类和图像分类接到业务里,正文里的实现步骤可以直接抄。
2. 先搞清 DeepSeek API 的分类能力边界:选型与前置条件
2.1 DeepSeek API 到底是什么形态,能做什么分类
DeepSeek 官方开放的是 OpenAI 兼容的 chat/completions 接口,不是独立的分类 API。这意味着你可以直接用 OpenAI 的 SDK(只需要改 base_url 和 api_key)来调用,也可以拿 requests 自己拼 JSON 体。它支持两类输入:纯文本和图像。文本分类走 deepseek-chat 这类对话模型,图像分类走带视觉能力的模型。两者共用同一个密钥,账单也合并在一起。
关键点在于“分类能力”不在模型侧,而在你的提示词里。同一个 deepseek-chat,你让它“把下面这段评论分成正面负面中性”就是情感分类;你让它“判断工单是否属于网络故障”就是意图分类;你让它“给商品标题打类目标签”就是文本多分类。图像侧同理,给它一张图并附上“这张图属于室内还是室外、有没有行人、画面里有没有文字”,它返回的就是图像分类结果。这种做成方式的优势是零训练成本,劣势是稳定性需要靠工程手段兜底,后面第三四章会专门讲怎么兜。
2.2 申请密钥与调用前置条件:最少必要配置
去 DeepSeek 开放平台注册账号后,在控制台创建 API Key。这个 Key 只在创建时完整显示一次,务必立刻复制保存。调用前请确认三件事:账户有余额、API Key 有权限调用模型、网络能访问 api.deepseek.com。前两点听起来像废话,但我周围至少三个人被“401 认证失败”卡了半小时,最后发现是复制 Key 时多复制了一个换行符。
我建议第一次调用不要直接用 SDK,先用 curl 验证连通性。这样能把网络问题、密钥问题和代码问题分开。下面是连通性测试的命令:
curl https://api.deepseek.com/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk-你的密钥" \ -d '{ "model": "deepseek-chat", "messages": [ {"role": "user", "content": "只回复两个字:正常"} ], "max_tokens": 10, "temperature": 0 }'逻辑不复杂:向 chat/completions 接口发一个最简单的对话请求,模型名叫 deepseek-chat,temperature 设为 0 表示尽量确定输出。如果返回内容里出现"content":"正常",说明密钥和网络都没问题。如果返回 401,去检查密钥;如果返回 404,去检查模型名;如果超时,去检查网络出口。这一步跑通后再进 Python 代码,排查范围会小很多。
3. 文本分类的工程化实现:从提示词到结构化输出
3.1 用提示词约束分类目标:少样本示例比描述规则更管用
文本分类任务本身不复杂,难的是让模型稳定输出你想要的类别字符串。例如我想把客服工单分为“账号问题 / 支付问题 / 技术故障 / 其他”四类,最省事的做法是把分类规则写在 system prompt 里,然后在 user 消息里给几条示例,最后放待分类文本。
我在实践里发现一个规律:规则描述写得再详细,都不如给两个“边界模糊的示例”效果好。比如“账号问题”和“技术故障”在人工标注时都容易混淆,如果你在示例里明确写了“密码重置属于账号问题,登录报错属于技术故障”,模型输出的准确率会明显提升。这就是少样本提示词的价值。下面给一个可直接用的文本分类请求示例:
import requests API_KEY = "sk-你的密钥" URL = "https://api.deepseek.com/chat/completions" def classify_text(text, labels): prompt = ( f"请对下面的文本进行分类,只能从这些类别中选择一个:{labels}。\n" "示例:\n" "文本:我密码忘了想重置 → 账号问题\n" "文本:支付时提示银行扣款成功但订单未生成 → 支付问题\n" "文本:登录一直转圈然后报错500 → 技术故障\n" "文本:我想问一下你们上班时间 → 其他\n" f"文本:{text}\n" "只输出类别名称,不要解释。" ) payload = { "model": "deepseek-chat", "messages": [ {"role": "system", "content": "你是严格的文本分类器,只输出类别名。"}, {"role": "user", "content": prompt} ], "temperature": 0, "max_tokens": 20, "stream": False } resp = requests.post(URL, json=payload, headers={ "Authorization": f"Bearer {API_KEY}" }, timeout=30) return resp.json()["choices"][0]["message"]["content"].strip() result = classify_text("我刚刚付款成功了但是会员没到账", ["账号问题", "支付问题", "技术故障", "其他"]) print(result)这段代码本质上是在组装提示词然后请求接口。参数上有几个细节值得说明。temperature 设成 0 是为了让分类结果尽量稳定,不因为随机采样飘到别的类别。max_tokens 设成 20 就够,因为期望输出只有一个类别名,给太长反而可能在尾部生成解释文字。timeout 设成 30 秒是为了避免接口异常时请求一直挂着。我把 labels 作为参数传进去,这样换任务时不用改动函数体。注意:这里没有做返回结果的合法性校验,如果模型吐出来的不在 labels 里,需要有一个重试或兜底逻辑,这个问题在第五章会专门讲。
3.2 解析返回结果:处理 JSON 模式与普通文本模式
上面代码里我用的是普通文本输出:模型直接返回“支付问题”这四个字。但实际业务中,往往需要同时得到“类别 + 置信度 + 一句话依据”,这时候就要让模型返回结构化内容。最稳的做法是让模型只生成 JSON,然后代码里去解析 JSON。
DeepSeek 的接口支持响应格式约束,常见做法是在请求体里加response_format: {"type": "json_object"},并提示模型“输出 JSON”。我第一次用这个参数时翻了一次车:接口报了 400,提示“json_object 模式下必须包含 json 这个词”。后来才搞明白,JSON 模式要求 prompt 里明确提到 JSON 格式,否则模型不知道要按什么 schema 组织内容。修正后的写法是这样:
import requests, json def classify_with_json(text): payload = { "model": "deepseek-chat", "messages": [ {"role": "system", "content": "你是文本分类器,只输出 JSON,不要输出其他内容。"}, {"role": "user", "content": ( "分类下面文本,输出 JSON 格式:{\"category\": \"类别\", \"reason\": \"依据\"}\n" "类别限定为:账号问题、支付问题、技术故障、其他。\n" f"文本:{text}" )} ], "response_format": {"type": "json_object"}, "temperature": 0, "max_tokens": 100 } resp = requests.post( "https://api.deepseek.com/chat/completions", json=payload, headers={"Authorization": f"Bearer sk-你的密钥"}, timeout=30 ) content = resp.json()["choices"][0]["message"]["content"] parsed = json.loads(content) return parsed print(classify_with_json("会员开通后无法使用部分功能"))这个版本的代码做了两件事:一是把响应格式显式声明为 JSON,二是把“只输出 JSON”写进了 system 提示词。两者缺一不可。response_format 参数如果设了 json_object,但 prompt 里一点也不提 JSON,模型就会随机发挥,返回一段带 markdown 代码块的内容,json.loads 直接炸。还有一个被问得很多的问题:max_tokens 设多大合适。这里我建议设 100 左右,因为 JSON 里包含 reason 字段时,输出长度会明显大于纯类别名,设小了内容会被截断,截断后的 JSON 必然解析失败。
3.3 批量分类与并发控制:串行还是并行
文本分类很少是一条一条手动调的,绝大多数场景都是几百上千条文本要批量跑。批量跑有两个层次的问题:速度和成本。速度上,你可以用 ThreadPoolExecutor 做并发请求;成本上,你要控制并发数避免接口报限流,也要控制输入长度避免无谓烧 token。
我一般会写成这样的批量函数:
from concurrent.futures import ThreadPoolExecutor, as_completed def classify_many(texts, labels, max_workers=5): results = {} with ThreadPoolExecutor(max_workers=max_workers) as pool: future_map = {pool.submit(classify_text, t, labels): t for t in texts} for future in as_completed(future_map): text = future_map[future] try: results[text] = future.result() except Exception as e: results[text] = f"error: {e}" return results注意我这里把 max_workers 默认设成 5,而不是一口气开 20 个线程。为什么?因为 DeepSeek API 的并发限制是按账号维度计算的,并发过高会直接返回 429 限流错误。而且文本分类任务每个请求体量不大,5 个并发已经能让几百条数据在合理时间内跑完。如果遇到限流,降低 max_workers 是最直接的解决办法。批量任务还有一个隐藏问题:部分请求可能因为超时或网络抖动失败。所以代码里我捕获了异常并记录到结果字典中,后续可以针对 error 条目做重试。这个模式看起来不高级,但非常实用,能省掉你反复排查批量丢数据的精力。
4. 图像分类怎么接:Visual 模型调用与图片传入方式
4.1 图像分类的实现路径:多模态输入与零样本分类
DeepSeek 的视觉模型通过同样的 chat/completions 接口暴露,区别在于消息里的 content 不再是纯字符串,而是包含图像 URL 或 base64 编码的图像数组。图像分类任务在这里的典型写法是:把图片和提示词放进 user 消息里,让模型输出判断结果。
我遇到过不少人误以为图像分类必须走专门的视觉 API 或者要先自己跑目标检测,其实完全不用。比如你想判断一张照片是室内还是室外、有没有文字、有没有人,直接把图传给视觉模型即可。和前面说的文本分类一样,分类规则也写在提示词里。下面是标准的多模态请求代码:
import requests, base64 def classify_image(image_path, labels): with open(image_path, "rb") as f: b64_img = base64.b64encode(f.read()).decode("utf-8") payload = { "model": "deepseek-chat", # 这里替换成你有权限使用的视觉模型名 "messages": [ {"role": "system", "content": "你是图像分类器,只输出类别。"}, {"role": "user", "content": [ {"type": "text", "text": f"判断这张图片属于哪个类别,只能是:{labels}。只输出类别名。"}, {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64_img}"}} ]} ], "max_tokens": 50, "temperature": 0 } resp = requests.post( "https://api.deepseek.com/chat/completions", json=payload, headers={"Authorization": "Bearer sk-你的密钥"}, timeout=120 ) resp_json = resp.json() return resp_json["choices"][0]["message"]["content"] print(classify_image("sample.jpg", "室内/室外/其他"))这段代码有几个需要说明的细节。图片不是直接传二进制,而是先 base64 编码,再拼成 data URL。这里的data:image/jpeg;base64,前缀不能少,而且格式要和实际图片格式一致:JPEG 就写 image/jpeg,PNG 就写 image/png。timeout 我故意设成了 120 秒,因为图像请求比文本请求慢很多,尤其是图片体积较大时,模型需要时间处理视觉编码。如果 timeout 沿用文本任务的 30 秒,大概率会直接超时。还有一点:温度参数我仍然保持 0,分类任务不要引入随机性。
4.2 图像大小、格式与 base64 编码的边界
图像分类在实践中最常翻车的地方不在模型,而在“图片能不能被正确处理”。具体来说有两个常见问题:图片太大导致请求体超限,以及格式不对导致模型读不了图。第一个问题很好理解,一张几 MB 的图片 base64 编码之后会膨胀约 33%,如果 API 有请求体大小限制,你就要在传图之前先压缩。
我处理的办法是先对图片做尺寸和体积的预处理,再进入分类逻辑:
from PIL import Image import io def compress_image(image_path, max_size=1024, max_bytes=512*1024): img = Image.open(image_path) img.thumbnail((max_size, max_size), Image.LANCZOS) buffer = io.BytesIO() img.save(buffer, format="JPEG", quality=85) while buffer.tell() > max_bytes: buffer = io.BytesIO() img = img.resize((int(img.width * 0.8), int(img.height * 0.8))) img.save(buffer, format="JPEG", quality=70) return buffer.getvalue()compress_image 函数的思路是先限制最长边不超过 1024 像素,再控制 JPEG 质量。最后还有一层 while 兜底:如果压缩一次的产物还是超过 512KB,就逐步缩小尺寸和降低画质,直到体积达标。这套逻辑能覆盖绝大多数线上图片。至于格式问题,建议统一转成 JPEG 再传,避免出现 WebP 透明底、HEIC 未解码、单通道灰度图这类让接口解析失败的情况。灰度图在 PIL 里是 mode L,直接保存成 JPEG 没问题;但如果你拿到的是 RGBA 的 PNG,需要先转成 RGB 再存 JPEG,否则会报错或者得到一张黑底的图。这些细节都不难,但漏掉任何一个,线上任务就会成批失败。
4.3 图像分类的提示词怎么设计才不产生幻觉
图像分类的提示词设计比文本分类更讲究,因为模型有“脑补”倾向。你问它“图里有没有文字”,如果图里真的没有,它更倾向于回答“没有”,但也会出现说“有”的情况。为了降低这种幻觉,我在设计提示词时会做三件事:限制类别范围、强制输出边界、要求标注不确定项。
这里说的“强制输出边界”是指让模型在不确定时说“无法判断”,而不是硬猜一个类别。这个策略在分类任务里非常有用,因为它能让你在后续流程里把低置信度的图像单独拎出来人工处理。一个我实测有效的写法是:
请你对这张图片进行分类,类别只能是:行人/车辆/其他。 如果图中无法明确区分主体,输出“其他”。 只输出类别名称,不要解释原因。关键在“无法明确区分主体”这个限定句。它给了模型一个合法出口,避免它为了完成任务强行选择类别。另外,在文本分类里我用了少样本示例,但图像侧我一般不示例,因为文字描述图片内容很容易误导模型。比如你说“文本:图片中是红灯→车辆”,模型可能真的会去图里找红灯,反而把简单任务复杂化。图像分类的提示词宁可简洁,不要堆砌。
5. 避坑与排查:DeepSeek API 分类任务常见的 5 个坑
5.1 报错 401:API Key 无效或权限不足
现象:调用接口返回401 Authentication Fails,但密钥看着没问题。
原因:最常见的是复制时多了空格或换行;其次是账号余额不足导致密钥被禁用;第三个是密钥创建后没有等待几秒就立刻使用,服务端还没来得及同步。
解决:先在 DeepSeek 控制台重新复制密钥,确认不带任何多余字符。然后在 curl 里用环境变量引用密钥,避免手动复制出错。我习惯在 ~/.bashrc 里写入export DEEPSEEK_API_KEY="sk-xxx",脚本里用os.getenv("DEEPSEEK_API_KEY")读取,这样密钥只维护一份,不会因为复制操作引入脏字符。
5.2 报错 400 context length:上下文超限与内容总量失控
现象:请求返回类似this model's maximum context length is ...的 400 错误,尤其是在批量处理长文本时高频出现。
原因:你发送的 messages 数组总 token 数超过了模型的上下文窗口上限。这里容易忽略的是,system 提示词里如果你放入了大量示例,这些 token 也占上下文长度。我见过有人把 30 条少样本示例全部塞进 system,加上待分类文本又很长,直接顶爆上下文。
解决:压缩示例数量,每个分类任务最多保留 3 到 5 条示例;长文本先截断再传入,比如限定前 1000 字。如果你做的是多轮分类(比如先把文本摘要再分类),务必把中间轮次的结果存到变量而不是继续堆进 messages 数组。记住一个原则:messages 数组的 tokens 是累积的,不是只有最后一轮的 tokens。
5.3 返回内容解析失败:JSON 被 markdown 代码块包裹
现象:代码里json.loads(content)报Expecting value,打印出来发现内容是json ...包着。
原因:DeepSeek 的模型在部分情况下会把 JSON 输出放到 markdown 代码块里,即使你设了 response_format 也偶有发生。在某些第三方网关里,response_format 参数被透传但未生效,导致模型按默认格式自由发挥。
解决:解析前做一层清洗——去掉 content 首尾的json 和标记。另外在 system 提示词里强调“不要输出 markdown 代码块”。如果仍然不稳定,建议捕获解析异常后重试一次,重试时把提示词中的“只输出 JSON”换成“直接输出纯文本 JSON,不要使用代码块”,成功率会显著上升。
5.4 温度参数被误解:分类任务到底该用多少
现象:temperature 设成 0.7 的话,同一个文本分类任务跑三次返回三个不同类别;设成 0 也不是绝对稳定,偶尔还是会飘。
原因:temperature 控制的是采样随机性,但即便设为 0,模型仍可能因为解码策略中的 top-k 或其它参数产生微小的不确定性。0.7 是对话场景的推荐值,它让模型更“有创造性”,但在分类场景这是灾难。
解决:分类任务用 0 或接近 0 的值。如果设成 0 仍然偶发不稳定,不要去改 temperature,而是考虑加一条后置校验:判断模型输出类别是否在预设集合内,如果不在,就重新发一次相同请求。重试机制比调参数更能治本。
5.5 图片传不进去:data URL 格式与接口不兼容
现象:图像分类接口返回 400,提示内容格式错误,或者一直超时。
原因:部分是图片 base64 后过大,请求体超过单次限制;部分是 content 数组里 image_url 对象的层级写错,或者 model 没有图像理解权限。
解决:先确认传的是 data URL 而非纯 base64 字符串;再确认图片经过压缩处理,控制在 512KB 以内;最后排除网络问题后,把错误信息里返回的“字段缺失”提示和官方文档里的 JSON 结构逐字段对照。图像接口的报错信息有时候不准确,排查时不要只盯着报错文本,先走一遍最小请求:一张小图、一段最短提示词,能跑通则说明是图片或参数问题,不能跑通则要先检查模型名和鉴权。这套“最小化提单”的思路,在排查所有 API 任务时都通用。
6. 分类效果验证与成本控制:上线前必做的两件事
6.1 用小样本盲测替代“感觉准了”
我在实际项目中养成的一个习惯是:任何分类任务上线前,先从业务数据里挑 100 条有标准答案的样本,跑一遍分类,然后人工计算准确率。这一步看似简单,但能拦住大部分翻车——因为模型在几十条样本上的表现有迷惑性,你试了 3 条都对了不能说明问题,100 条才基本能暴露短板。
具体的验证方法是这样:准备一个 CSV 文件,包含 text 列和真实标签列,批量调用分类函数,把结果和真实标签比对,统计准确率和混淆矩阵。这个过程建议写一个小脚本固定下来,因为后续每次修改提示词都可以重跑同一份样本集对比效果,改一句 system prompt 是变好还是变坏,数据说了算。盲测集还有一个作用:它能帮你发现类别边界定义是否清晰。如果“支付问题”和“其他”频繁混淆,说明这个分类体系本身的定义需要修,而不是模型的错。
6.2 成本与延迟的粗估方法
调用 DeepSeek API 做分类,成本主要取决于三个因素:输入 token 数、输出 token 数和请求次数。输出 token 数可以通过限制 max_tokens 来控制,输入 token 数则取决于提示词长度和文本长度。分类任务建议在批量跑之前先抽样估算:随便选 20 条文本,统计平均每条消耗的 token 数,再乘上总量,就能估算出整体花费。
除了成本,延迟也要心里有数。文本分类单次请求通常在几百毫秒到两秒之间,图像分类则可能到几十秒。如果你的业务对响应速度要求很高,批量分类显然不适合在线同步调用,上去就会把接口打满。常见的做法是拆分流程:同步接口只处理小批量高优先级的请求,大批量任务走异步队列,每小时定时跑一批。批处理时设置的并发数不要拍脑袋,先在 5 并发下测一轮,观察平均耗时和失败率,再逐步上调,直到逼近限流阈值。这个逐步加压的习惯能避免你的服务在深夜批量任务中突然被打进 429 限流的坑。
最后说一个我自己的习惯:每次上线分类任务,我都会在代码里留一个开关,允许把模型返回结果和耗时写入日志。上线后前三天每天抽查日志,看有没有异常的返回格式和特别慢的请求。分类模型的提示词调优是没有终点的,但你在盲测集上的准确率每提高一个点,这个方向就值得继续往下做。希望这些实现步骤和踩坑记录能帮到你,让你的分类任务少走几趟弯路。
本文还有配套的精品资源,点击获取