大模型 API 核心参数:调对了事半功倍,调错了钱打水漂

大家好,我是极客老墨。
我刚开始接大模型 API 时,最容易犯的毛病就是把所有问题都往 prompt 上推。输出不稳定,先改 prompt;JSON 解析失败,继续改 prompt;账单涨了,还想着是不是 prompt 不够精简。改到最后,prompt 越写越长,接口却还是像没装仪表盘的车,能跑,但不知道哪里在烧钱、哪里在抖。
这篇不讲玄学调参。我就按一个常见场景来讲:做一个客服工单摘要接口。
输入是一段用户和客服的对话,输出要稳定变成这样的 JSON:
1{
2 "summary": "用户咨询退款到账时间,客服告知预计 3-5 个工作日",
3 "category": "refund",
4 "risk_level": "low",
5 "next_action": "等待退款到账"
6}
这个接口看着简单,真接到业务里会遇到四个问题:
- 有时输出一段自然语言,JSON 解析直接炸。
- 有时写到一半停了,字段缺一截。
- 有时同一条工单跑两次,分类不一致。
- 有时为了一个简单摘要开了推理模式,成本和延迟都上去了。
这些问题不全是 prompt 的锅。后来我才把 API 参数当成方向盘、油门、刹车和仪表盘来看:prompt 负责告诉模型要去哪,参数负责控制它怎么走、走多远、花多少钱、异常时怎么停下来。
本文以 DeepSeek API 为主线。DeepSeek 当前官方文档里,Chat Completions 的模型 ID 是
deepseek-v4-flash和deepseek-v4-pro;deepseek-chat、deepseek-reasoner仍可兼容,但官方已说明将于 2026/07/24 弃用。下面的参数口径按本文撰写时查阅的官方文档编写,后续以官方最新文档为准。
model:先选一辆够用的车
客服工单摘要这类任务,第一版通常不需要最强模型。它不是奥数题,也不是复杂代码审查,核心是稳定抽取、分类、压缩信息。
DeepSeek 当前官方模型表里有两个主模型:
| model | 适合什么 |
|---|---|
deepseek-v4-flash | 延迟、成本更敏感的通用任务,比如摘要、分类、提取、普通问答 |
deepseek-v4-pro | 更复杂的推理、规划、工具调用、代码分析 |
官方还保留了两个兼容别名:
deepseek-chat:对应deepseek-v4-flash的非 thinking 模式。deepseek-reasoner:对应deepseek-v4-flash的 thinking 模式。
这里有个很实用的判断:能用 deepseek-v4-flash 跑稳,就先不上 deepseek-v4-pro;能关 thinking 跑稳,就先不开 thinking。
客服摘要接口最小版本可以这样定:
1{
2 "model": "deepseek-v4-flash",
3 "thinking": {"type": "disabled"}
4}
为什么先关 thinking?因为我们要的是稳定结构化输出,不是让模型展开复杂推理。thinking 模式会产生 reasoning_content,有助于复杂问题,但也会带来额外 token、延迟和上下文处理细节。简单抽取任务先关掉,后面如果发现分类确实需要推理,再打开对比。
现在选模型,先别急着问“哪个最强”,更该先问“哪个能用最低成本跑过验收线”。模型越强,模式越复杂,后面的成本、延迟和排障压力也会跟着上来。
reasoning_effort:推理模式打开后,还要控制它想多深
thinking 是开关,reasoning_effort 更像档位。
DeepSeek 当前文档里,reasoning_effort 支持 high 和 max。普通请求默认是 high,一些复杂 Agent 请求可能会自动走到 max。为了兼容,low、medium 会映射到 high,xhigh 会映射到 max。
这个参数只在 thinking 模式有意义。客服摘要这种结构化抽取,第一版直接关 thinking 就够了,reasoning_effort 也就不用传:
1{
2 "model": "deepseek-v4-flash",
3 "thinking": {"type": "disabled"}
4}
如果后面发现风险判断确实复杂,比如要结合多轮投诉语气、历史订单、退款规则判断是否升级人工,再单独做一组实验:
1{
2 "model": "deepseek-v4-flash",
3 "thinking": {
4 "type": "enabled",
5 "reasoning_effort": "high"
6 }
7}
这里不建议一上来就设 max。原因很工程化:max 可能提高复杂任务的推理充分度,但也更容易拉高延迟和 reasoning token。要不要开,不能靠感觉,要看这几项指标:
- 高风险工单召回率有没有提升。
- 误报率有没有明显上升。
completion_tokens_details.reasoning_tokens增加了多少。- P95 延迟和单次成本还能不能接受。
如果只是把“退款多久到账”分到 refund,开更深的推理没有意义。模型想得越多,不等于业务结果越稳。
temperature:让输出稳下来,不是让它“更聪明”
我第一次做这类摘要接口时,最难受的不是模型不会总结,而是它不够稳定。同一条工单,第一次分类可能是 refund,第二次又分类为 complaint,第三次则变成了“退款咨询”。这种结果一旦接到数据库、报表、风控规则里,后面排查会很麻烦。
temperature 控制的是采样随机性。可以把它理解成“模型选词时有多愿意冒险”。值越低,越偏向高概率答案,常用在需要准确回答的场景,比如编程;值越高,输出越发散,常用于创意,比如小说创作等。
DeepSeek 官方参数文档给的推荐值是:
| 场景 | 推荐 temperature |
|---|---|
| Coding / Math | 0.0 |
| Data Cleaning / Data Analysis | 1.0 |
| General Conversation | 1.3 |
| Translation | 1.3 |
| Creative Writing / Poetry | 1.5 |
我们的客服摘要更接近数据清洗和结构化提取。如果优先追求分类稳定,可以从 0 或 0.2 开始;如果摘要文本太生硬,再慢慢往上调。
最小请求可以这样写:
1{
2 "model": "deepseek-v4-flash",
3 "thinking": {"type": "disabled"},
4 "temperature": 0.2,
5 "messages": [
6 {
7 "role": "system",
8 "content": "你是客服工单分析助手,只输出 JSON。"
9 },
10 {
11 "role": "user",
12 "content": "请总结这段客服对话,并判断 category、risk_level、next_action。..."
13 }
14 ]
15}
这里容易有个误会:temperature=0 不等于“模型最聪明”。它只是更保守、更确定。写代码、做分类、抽字段,这通常是好事;写营销文案、标题、故事,它就可能变得干巴。
还有一个细节要特别注意:DeepSeek thinking 模式不支持 temperature、top_p、presence_penalty、frequency_penalty。官方说了,为了兼容,传这些参数不会报错,但也不会生效。也就是说,代码里看起来写了参数,实际接口可能根本没听。
top_p:别和 temperature 一起乱拧
top_p 也是控制随机性的参数,但它不是调“温度”,而是控制候选词范围。
简单说,模型每一步会给很多候选 token 排概率。top_p=0.9 的意思是:只从累计概率达到 90% 的那批候选里采样,剩下长尾候选直接不看。
这东西像什么?像点外卖时只看评分前 90% 的店,剩下那些特别冷门、特别离谱的选择先屏蔽掉。它能减少跑偏,但不是万能保险。
DeepSeek 和 OpenAI 的文档都有同一个建议:temperature 和 top_p 通常只改一个,另一个保持默认。
客服摘要接口可以按这个顺序处理:
- 第一轮只调
temperature,top_p保持默认1。 - 如果输出偶尔出现离题词、奇怪分类,再小心尝试
top_p。 - 不同时把
temperature、top_p、各种 penalty 全部改一遍,否则出了问题很难判断是谁导致的。
做参数实验,最不可靠的就是“感觉好多了”。更稳的做法是拿 20 到 50 条真实工单跑一遍,看分类一致性、JSON 解析成功率、平均输出 token,而不是只看单条样例顺不顺眼。
top_k:本地模型常见,DeepSeek 官方接口里先别硬塞
top_k 也是采样参数,很多本地推理框架、开源模型服务和第三方网关都会暴露它。它的意思更直接:每一步只从概率最高的前 K 个 token 里采样。
比如 top_k=40,模型每次生成时就只看最可能的 40 个候选 token。K 越小,候选范围越窄,输出越保守;K 越大,候选范围越宽,输出越容易发散。它和 top_p 都是在控制“候选池”,只是切法不一样:
| 参数 | 控制方式 | 直觉理解 |
|---|---|---|
top_p | 按累计概率截断 | 只看“概率加起来够高”的那批候选 |
top_k | 按候选数量截断 | 只看“排名最靠前”的 K 个候选 |
但这篇以 DeepSeek 官方 Chat Completions 为主线,需要特意提醒一句:DeepSeek 当前官方 Chat Completion 参数里没有列 top_k。
所以在这条链路里,不要往请求体里硬塞:
1{
2 "top_k": 40
3}
有些 OpenAI 兼容网关会悄悄接受额外参数,有些会忽略,有些会直接报错。最麻烦的是第二种:代码看起来传了,实际根本没生效,排障时很容易误判。
如果用的是 Ollama、vLLM、SillyTavern、OpenRouter 或云厂商托管的开源模型,那就要按对应平台文档看 top_k 是否支持、默认值是多少、是否和 temperature / top_p 同时生效。业务接口第一版仍然别同时拧太多采样参数:
- DeepSeek 官方 API:先讲
temperature和top_p,top_k只作为兼容知识点说明。 - 本地模型或第三方网关:可以记录
top_k,但要用固定测试集验证它是否真的影响输出。 - 结构化抽取任务:优先低随机性和输出校验,不要指望
top_k替你保证 JSON 稳定。
frequency_penalty / presence_penalty:看到也先别急着用
很多 OpenAI 兼容接口里都会出现两个 penalty 参数:
frequency_penalty:惩罚已经频繁出现过的 token,常用来减少重复。presence_penalty:只要某个 token 出现过就惩罚,常用来鼓励模型聊新主题。
放到客服摘要里,直觉上它们好像能解决“输出重复”“一直绕圈”。但 DeepSeek 当前文档已经把这两个参数标成 deprecated,并说明传了也不会生效。
所以这里最容易踩的坑不是“调错值”,而是代码里保留了参数,团队以为它在工作。线上出了重复输出,大家还在调 frequency_penalty,实际接口根本没听。
这类参数的处理方式很简单:
- DeepSeek 链路里不要把它们当有效控制项。
- 如果输出重复,先看 prompt 是否让模型解释太多,再看
max_tokens是否放得太大。 - 如果某类长文本任务确实重复严重,用任务拆分、结构约束和后处理兜底,不要把希望压在 deprecated 参数上。
max_tokens:不是越大越安全
客服摘要接口第二个高频问题是截断。
最典型的截断长这样:让模型输出 JSON,它回了半截:
1{
2 "summary": "用户咨询退款到账时间,客服说明
这种结果进代码就是解析失败。我早期会继续改 prompt:“请一定输出完整 JSON”。后来才发现,接口已经撞上 max_tokens 了,再怎么喊它“完整”,它也没地方写。
max_tokens 控制本次最多生成多少 token。DeepSeek 文档也提醒:输入 token 和输出 token 的总长度受模型 context length 限制,具体默认值和范围要看模型文档。
客服摘要这种任务,既不要不设,也不要随手设很大。比较稳的做法是按任务设预算:
- 一段短工单摘要:
max_tokens: 512 - 多轮对话摘要加字段解释:
max_tokens: 1024 - 长文总结或报告:再往上加,但要记录成本
更关键的是响应里要看 finish_reason:
stop:正常结束,或者遇到请求里设置的停止序列。length:输出撞上长度限制,内容可能被截断。content_filter:内容被过滤。tool_calls:模型选择调用工具。insufficient_system_resource:推理资源不足导致中断。
接口返回了 length,就应该把它当成失败处理。要么提高 max_tokens,要么缩短输入,要么把任务拆成两步。
response_format:JSON 模式只保证合法,不保证好用
客服摘要接口最后要进程序解析,只在 prompt 里写“请输出 JSON”,通常不够。模型可能返回:
1```json
2{...}
或者先来一句“以下是整理后的结果”。人看没问题,程序一解析就报错。
DeepSeek 支持 JSON Output:
1{
2 "response_format": {"type": "json_object"}
3}
官方文档这里有两个关键提醒:
response_format设为json_object后,模型生成的内容会是合法 JSON。- 仍然要在 system 或 user message 里明确要求输出 JSON;否则模型可能一直输出空白,直到撞上 token 上限。
所以这个客服摘要接口两边都要写:
1{
2 "response_format": {"type": "json_object"},
3 "messages": [
4 {
5 "role": "system",
6 "content": "你是客服工单分析助手。只输出 JSON,不要输出 Markdown,不要解释。字段必须包含 summary、category、risk_level、next_action。"
7 }
8 ]
9}
但这里不能误会:json_object 只保证“这是合法 JSON”,不保证字段一定符合业务规则。risk_level 可能写成 "中",也可能写成 "medium"。所以 prompt 里要给枚举值,代码里还要做二次校验。
工程里,输出校验应该当成必做项:
1type TicketSummary struct {
2 Summary string `json:"summary"`
3 Category string `json:"category"`
4 RiskLevel string `json:"risk_level"`
5 NextAction string `json:"next_action"`
6}
7
8func validateSummary(s TicketSummary) error {
9 if s.Summary == "" || s.Category == "" || s.RiskLevel == "" {
10 return fmt.Errorf("missing required fields")
11 }
12
13 allowedRisk := map[string]bool{
14 "low": true, "medium": true, "high": true,
15 }
16 if !allowedRisk[s.RiskLevel] {
17 return fmt.Errorf("invalid risk_level: %s", s.RiskLevel)
18 }
19
20 return nil
21}
这一步不能偷懒。大模型输出进业务系统之前,就应该像外部用户输入一样校验。
tools / tool_choice:让模型决定要不要调函数,但执行权还在代码手里
客服摘要第一版只需要模型吐 JSON,不一定要用工具调用。但只要业务开始接后端动作,tools 和 tool_choice 就很关键。
举个例子:模型判断某个工单是高风险投诉后,你可能希望它生成一个“升级人工”的调用参数:
1{
2 "tools": [
3 {
4 "type": "function",
5 "function": {
6 "name": "escalate_ticket",
7 "description": "将高风险客服工单升级给人工主管",
8 "parameters": {
9 "type": "object",
10 "properties": {
11 "ticket_id": {"type": "string"},
12 "risk_level": {"type": "string", "enum": ["medium", "high"]},
13 "reason": {"type": "string"}
14 },
15 "required": ["ticket_id", "risk_level", "reason"]
16 },
17 "strict": true
18 }
19 }
20 ],
21 "tool_choice": "auto"
22}
DeepSeek 当前工具调用只支持 function 类型。tools 是告诉模型“有哪些函数可以用”,tool_choice 是控制“这次要不要用”:
| tool_choice | 含义 | 适合场景 |
|---|---|---|
none | 不调用工具,只生成文本 | 纯摘要、纯分类、纯解释 |
auto | 模型自己决定回文本还是调工具 | 客服助手可以查询订单,也可以直接回答 |
required | 必须调用一个或多个工具 | 这次请求就是为了生成后端动作 |
| 指定函数 | 强制调用某个函数 | 已经由业务流程决定下一步动作,只让模型补参数 |
这里最重要的一句话是:工具调用不是模型替你执行函数,而是模型生成一段函数名和参数。真正执行前,代码必须校验。
官方响应里也提醒,模型生成的 arguments 不一定永远是合法 JSON,也可能出现 schema 之外的参数。所以工程里至少要做三层检查:
finish_reason是否为tool_calls。arguments能不能按目标结构解析。- 参数里的 ID、权限、枚举值、金额、状态流转是否符合业务规则。
如果把 tool call 当成“模型已经判定安全了”,那就危险了。模型只能帮你组织调用意图,权限和执行边界还是后端代码的事。
stop:先不用,除非边界真的需要它
stop 是停止序列。模型一旦生成你指定的字符串,就立刻停下来。DeepSeek Chat Completion API 文档 有对 stop 字段的说明:最多可以传 16 个停止序列,它是一个 string 或者容纳 string 类型的 list。
这个参数适合两类场景:
- 生成某种带分隔符的文本,希望模型到固定标记就停。
- 兼容旧 prompt 的多轮对话,用角色标记控制边界。
但客服 JSON 摘要接口里,不必急着用 stop。原因很简单:JSON 本身就是结构边界,response_format 已经在帮你约束输出。再加一个很短的 stop,比如 "}" 或 "\n",很容易把合法 JSON 截坏。
如果确实要用,停止符要足够独特,比如:
1{
2 "stop": ["<END_OF_TICKET_SUMMARY>"]
3}
然后 prompt 里明确让模型在 JSON 后输出这个标记。但多数结构化输出场景,先用 response_format 和代码校验就够了,不必一上来就加 stop。
stream:用户体验参数,不是质量参数
stream 很容易被误解。它不是让模型更聪明,也不是让总耗时一定变短。它只是把生成过程按 SSE 流式吐出来,用户可以更早看到内容。
客服摘要接口通常不需要流式。我们要的是一个完整 JSON,等它一次性返回再解析,链路更简单。
但如果做的是“客服助手实时回复”或者“长报告生成”,stream 就有价值。DeepSeek 的 stream_options.include_usage 可以让流式响应在结束前额外返回一次完整 usage,这对成本统计很重要。
一个保守建议:
- 面向人看的长文本:可以开
stream。 - 面向程序解析的 JSON:先不开
stream,等链路稳定后再考虑。 - 开了
stream,要处理data: [DONE],也要处理异常中断和半截内容。
usage:上线前先把账算清楚
参数调优最后一定要回到数据。这个习惯越早养成越好。
DeepSeek 响应里的 usage 不只告诉你用了多少 token,还会拆出缓存命中情况:
prompt_tokenscompletion_tokenstotal_tokensprompt_cache_hit_tokensprompt_cache_miss_tokenscompletion_tokens_details.reasoning_tokens
这些字段对工程判断很有用。
比如客服摘要接口里,completion_tokens 很高,可能是 prompt 没压住,模型在解释;reasoning_tokens 很高,可能是 thinking 没关,或者任务确实需要推理;cache miss 很高,可能是 system prompt 和公共上下文没有复用好。
上线前至少要记录这几个指标:
1model
2thinking.type
3temperature
4max_tokens
5finish_reason
6prompt_tokens
7completion_tokens
8reasoning_tokens
9prompt_cache_hit_tokens
10prompt_cache_miss_tokens
11latency_ms
12json_parse_ok
13validation_ok
有了这些字段,才知道一次改参数到底是在省钱、提稳,还是只是看起来舒服。
logprobs / top_logprobs:调试置信度可以看,但别把它当业务概率
logprobs 会返回模型输出 token 的对数概率。top_logprobs 则会返回每个位置上若干个最可能 token 及其概率信息。DeepSeek 文档里要求:如果要传 top_logprobs,必须同时把 logprobs 设为 true,并且 top_logprobs 最大到 20。
这个参数看起来很适合做“置信度”。比如模型输出:
1{
2 "category": "refund",
3 "risk_level": "low"
4}
你可能想看 refund 的概率是不是很高,再决定要不要人工复核。这个思路可以作为调试参考,但不要直接把 token logprob 当成业务置信度,原因有三个:
- JSON 字段值可能被切成多个 token,单个 token 的概率不能直接等于分类概率。
- 低温、JSON 模式、枚举约束都会改变输出分布。
- “模型觉得这个词顺”不等于“业务判断一定正确”。
更合适的用法是:
- 离线分析:看某些分类附近是否有强竞争候选。
- 排障:定位模型是不是在两个标签之间摇摆。
- 评测辅助:和人工标注、历史规则结果一起看,而不是单独决策。
线上默认不建议开 logprobs。它会让响应更大、解析更复杂,客服摘要这类链路先把 JSON 成功率、校验通过率和人工抽检跑稳更重要。
user_id:不是给你塞手机号的地方
user_id 很容易被忽略,但做多用户 AI 应用时它挺有用。DeepSeek 文档里说,它可以用于内容安全审查侧的用户区分、KVCache 隔离和业务侧用户调度隔离。
这里最关键的是官方提醒:不要在 user_id 里放用户隐私信息。
更合适的写法是:
1{
2 "user_id": "tenant_42_user_8f3a9c"
3}
而不是:
1{
2 "user_id": "13800138000"
3}
如果是客服工单系统,可以把它设计成不可逆或内部 ID:
- 多租户系统:带上租户维度,避免不同客户上下文缓存混在一起。
- 企业内部系统:用内部员工 ID 的哈希或映射 ID。
- 公开 C 端产品:避免手机号、邮箱、身份证、真实姓名这类 PII。
user_id 不是鉴权参数,也不是审计日志的替代品。它更像请求侧的隔离和标记线索。权限判断、日志脱敏、数据留存,还是要在自己的系统里做。
OpenAI 兼容接口:这些参数也要认识,但别默认都能用
DeepSeek 这条线讲完后,还要单独看一眼 OpenAI 的参数。原因很现实:很多国产模型、聚合网关和企业内部模型服务都说自己“兼容 OpenAI API”。但兼容通常指请求形状像,不代表每个参数都支持,也不代表语义完全一样。
OpenAI 当前官方文档里也有一个明显趋势:新项目更推荐看 Responses API,老项目和大量兼容网关还在用 Chat Completions API。所以这里把参数分成两类看:
- 通用层:
model、messages/input、temperature、top_p、max_tokens/max_output_tokens、response_format/text.format、tools、tool_choice、stream、usage。 - OpenAI 扩展层:OpenAI 有,但 DeepSeek 或其他兼容服务不一定支持;迁移时要逐个核对。
下面这些参数,至少要知道它们解决什么问题:
| 参数 | 主要在哪类接口里常见 | 解决什么问题 | 使用建议 |
|---|---|---|---|
developer message | OpenAI Chat/Responses | 比 system 更明确地表达开发者指令,OpenAI 新模型里常见 | 写 OpenAI 原生代码时优先用;兼容网关先确认是否支持 |
max_output_tokens | OpenAI Responses | 控制响应最多生成多少 token,包含可见输出和推理 token | 用 Responses API 时看它;Chat Completions 仍常见 max_tokens / max_completion_tokens |
reasoning.effort / reasoning_effort | OpenAI reasoning models | 控制推理强度,影响延迟、成本和 reasoning token | 复杂任务才调,简单抽取先低推理或不开 |
verbosity | OpenAI 新模型 | 控制回答详略,low、medium、high | 比在 prompt 里反复写“简短一点”更直接,但要确认模型支持 |
n | Chat Completions | 一次生成多个候选 | 线上默认 1,否则 token 成本按多个候选一起涨 |
seed | Chat Completions | 尽量复现采样结果 | 只当调试辅助,不把它当严格确定性保证 |
parallel_tool_calls | 工具调用 | 允许模型并行发起多个工具调用 | 查询类 Agent 可以开;涉及写操作、扣费、状态流转时要谨慎 |
store | Chat/Responses | 是否把请求输出存储用于平台侧能力 | 默认先关;只有明确要用 evals、distillation 或平台追踪时再开 |
metadata | Chat/Responses | 给请求挂业务标签,方便后续查询或看板过滤 | 放订单类型、实验组、版本号,不放隐私 |
prompt_cache_key | Chat Completions | 帮助平台做相似请求缓存分桶 | 比旧的 user 更偏缓存用途;用稳定、脱敏的业务键 |
safety_identifier | Chat Completions | 帮助平台做滥用检测 | 用哈希后的用户标识,不传手机号、邮箱、真实姓名 |
service_tier | Chat Completions | 控制服务处理层级,比如 default、flex、priority | 延迟和成本敏感时再引入,普通业务先不复杂化 |
prediction | Chat Completions | 已知大部分输出时,用预测输出降低延迟 | 适合文件小改、模板补全,不适合开放式客服摘要 |
modalities / audio | Chat Completions | 要求模型同时输出文本、音频等 | 做语音助手时才需要;普通 JSON 接口不要加 |
truncation | Responses | 上下文超长时如何截断 | 长对话或 Agent 才重点看;结构化短请求先控制输入长度 |
previous_response_id | Responses | 让 Responses API 接续上一轮状态 | 做多轮 Agent 有用;无状态业务接口要谨慎,避免隐藏上下文影响复现 |
include | Responses | 要求响应额外返回指定信息 | 排障和追踪时有用,线上默认别把响应撑太大 |
这里最容易误判的是“OpenAI 兼容”四个字。不少代码会把一整套 OpenAI 参数原封不动传给不同模型:
1{
2 "temperature": 0.2,
3 "top_p": 1,
4 "seed": 42,
5 "n": 3,
6 "parallel_tool_calls": true,
7 "store": true,
8 "metadata": {"scene": "ticket_summary"}
9}
看起来很专业,实际可能有三个问题:
- 有的参数被网关静默忽略,团队以为它生效了。
- 有的参数会显著增加成本,比如
n > 1。 - 有的参数会改变数据留存和隐私边界,比如
store、metadata、用户标识类字段。
所以兼容接口参数可以分成三档:
| 档位 | 参数 | 处理方式 |
|---|---|---|
| 第一档:核心必查 | model、max_tokens/max_output_tokens、temperature、top_p、response_format、tools、stream、usage | 每个模型接入前都要确认 |
| 第二档:OpenAI 原生增强 | reasoning_effort、verbosity、parallel_tool_calls、prediction、service_tier、previous_response_id | 用 OpenAI 原生接口时重点看 |
| 第三档:治理和观测 | store、metadata、prompt_cache_key、safety_identifier、include | 上线治理时再加,先考虑隐私和数据留存 |
回到客服摘要接口,不要为了“兼容 OpenAI”把所有参数都塞进请求。第一版仍然只保留稳定输出需要的几个:模型、低随机性、输出长度、JSON 约束、usage、必要的用户隔离标识。等业务真的需要多候选、工具并行、状态接续、平台缓存,再把对应参数单独拉出来验证。
一段可以直接改的 Go 封装
下面这段代码只保留客服摘要接口需要的关键路径:请求、JSON 输出、截断检查、usage 记录、业务校验。它不是完整 SDK,更像第一版接口先搭出来的骨架。
1package main
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "fmt"
8 "io"
9 "net/http"
10 "os"
11 "time"
12)
13
14const deepseekAPI = "https://api.deepseek.com/chat/completions"
15
16type Message struct {
17 Role string `json:"role"`
18 Content string `json:"content"`
19}
20
21type Thinking struct {
22 Type string `json:"type"` // "enabled" 或 "disabled"
23 ReasoningEffort string `json:"reasoning_effort,omitempty"` // "high" 或 "max"
24}
25
26type ResponseFormat struct {
27 Type string `json:"type"` // "text" 或 "json_object"
28}
29
30type ChatRequest struct {
31 Model string `json:"model"`
32 Messages []Message `json:"messages"`
33 Thinking *Thinking `json:"thinking,omitempty"`
34 MaxTokens int `json:"max_tokens,omitempty"`
35 Temperature *float64 `json:"temperature,omitempty"`
36 TopP *float64 `json:"top_p,omitempty"`
37 ResponseFormat *ResponseFormat `json:"response_format,omitempty"`
38 Stop []string `json:"stop,omitempty"`
39 Stream bool `json:"stream,omitempty"`
40 Logprobs bool `json:"logprobs,omitempty"`
41 TopLogprobs int `json:"top_logprobs,omitempty"`
42 UserID string `json:"user_id,omitempty"`
43}
44
45type ChatResponse struct {
46 Choices []struct {
47 Message struct {
48 Content string `json:"content"`
49 ReasoningContent string `json:"reasoning_content"`
50 } `json:"message"`
51 FinishReason string `json:"finish_reason"`
52 } `json:"choices"`
53 Usage struct {
54 PromptTokens int `json:"prompt_tokens"`
55 CompletionTokens int `json:"completion_tokens"`
56 TotalTokens int `json:"total_tokens"`
57 PromptCacheHitTokens int `json:"prompt_cache_hit_tokens"`
58 PromptCacheMissTokens int `json:"prompt_cache_miss_tokens"`
59 CompletionDetails struct {
60 ReasoningTokens int `json:"reasoning_tokens"`
61 } `json:"completion_tokens_details"`
62 } `json:"usage"`
63}
64
65type TicketSummary struct {
66 Summary string `json:"summary"`
67 Category string `json:"category"`
68 RiskLevel string `json:"risk_level"`
69 NextAction string `json:"next_action"`
70}
71
72func float64Ptr(v float64) *float64 { return &v }
73
74func callDeepSeek(ctx context.Context, req ChatRequest) (*ChatResponse, error) {
75 apiKey := os.Getenv("DEEPSEEK_API_KEY")
76 if apiKey == "" {
77 return nil, fmt.Errorf("DEEPSEEK_API_KEY is empty")
78 }
79
80 body, err := json.Marshal(req)
81 if err != nil {
82 return nil, fmt.Errorf("marshal request: %w", err)
83 }
84
85 httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, deepseekAPI, bytes.NewReader(body))
86 if err != nil {
87 return nil, err
88 }
89 httpReq.Header.Set("Content-Type", "application/json")
90 httpReq.Header.Set("Authorization", "Bearer "+apiKey)
91
92 client := &http.Client{Timeout: 60 * time.Second}
93 resp, err := client.Do(httpReq)
94 if err != nil {
95 return nil, fmt.Errorf("call deepseek: %w", err)
96 }
97 defer resp.Body.Close()
98
99 if resp.StatusCode != http.StatusOK {
100 b, _ := io.ReadAll(resp.Body)
101 return nil, fmt.Errorf("deepseek api status=%d body=%s", resp.StatusCode, string(b))
102 }
103
104 var result ChatResponse
105 if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
106 return nil, fmt.Errorf("decode response: %w", err)
107 }
108 if len(result.Choices) == 0 {
109 return nil, fmt.Errorf("empty choices")
110 }
111
112 return &result, nil
113}
114
115func validateSummary(s TicketSummary) error {
116 if s.Summary == "" || s.Category == "" || s.RiskLevel == "" || s.NextAction == "" {
117 return fmt.Errorf("missing required field: %+v", s)
118 }
119
120 allowedRisk := map[string]bool{
121 "low": true, "medium": true, "high": true,
122 }
123 if !allowedRisk[s.RiskLevel] {
124 return fmt.Errorf("invalid risk_level: %s", s.RiskLevel)
125 }
126
127 return nil
128}
129
130func summarizeTicket(ctx context.Context, dialogue string) (TicketSummary, error) {
131 req := ChatRequest{
132 Model: "deepseek-v4-flash",
133 Thinking: &Thinking{Type: "disabled"},
134 Temperature: float64Ptr(0.2),
135 MaxTokens: 512,
136 UserID: "tenant_demo_user_demo",
137 ResponseFormat: &ResponseFormat{
138 Type: "json_object",
139 },
140 Messages: []Message{
141 {
142 Role: "system",
143 Content: `你是客服工单分析助手。只输出 JSON,不要输出 Markdown,不要解释。
144字段必须包含:
145- summary: 一句话总结用户问题和客服答复
146- category: refund/payment/account/logistics/other 之一
147- risk_level: low/medium/high 之一
148- next_action: 下一步处理动作`,
149 },
150 {
151 Role: "user",
152 Content: "请分析这段客服对话:\n" + dialogue,
153 },
154 },
155 }
156
157 resp, err := callDeepSeek(ctx, req)
158 if err != nil {
159 return TicketSummary{}, err
160 }
161
162 choice := resp.Choices[0]
163 if choice.FinishReason == "length" {
164 return TicketSummary{}, fmt.Errorf("model output truncated by max_tokens")
165 }
166 if choice.FinishReason != "stop" {
167 return TicketSummary{}, fmt.Errorf("unexpected finish_reason: %s", choice.FinishReason)
168 }
169
170 var summary TicketSummary
171 if err := json.Unmarshal([]byte(choice.Message.Content), &summary); err != nil {
172 return TicketSummary{}, fmt.Errorf("invalid json output: %w, raw=%s", err, choice.Message.Content)
173 }
174 if err := validateSummary(summary); err != nil {
175 return TicketSummary{}, err
176 }
177
178 fmt.Printf(
179 "usage prompt=%d completion=%d total=%d cache_hit=%d cache_miss=%d reasoning=%d\n",
180 resp.Usage.PromptTokens,
181 resp.Usage.CompletionTokens,
182 resp.Usage.TotalTokens,
183 resp.Usage.PromptCacheHitTokens,
184 resp.Usage.PromptCacheMissTokens,
185 resp.Usage.CompletionDetails.ReasoningTokens,
186 )
187
188 return summary, nil
189}
190
191func main() {
192 ctx := context.Background()
193 dialogue := `用户:我上周申请退款了,怎么还没到账?
194客服:您好,退款一般 3-5 个工作日到账。您这笔订单是周五提交的,预计本周三前到账。
195用户:那我再等等。`
196
197 summary, err := summarizeTicket(ctx, dialogue)
198 if err != nil {
199 fmt.Println("error:", err)
200 return
201 }
202 fmt.Printf("%+v\n", summary)
203}
完整的代码见 Github.
这段代码里有几个故意保守的选择:
thinking先关掉,因为摘要抽取不需要复杂推理。reasoning_effort不传,因为关掉 thinking 后它没有意义。temperature设0.2,优先稳定分类。max_tokens设512,够用但不放飞。response_format开 JSON 模式,但仍然做 JSON parse 和字段校验。finish_reason != stop直接当异常处理,不让半截结果混进业务。- usage 打出来,为后续调成本和缓存命中率留数据。
user_id用脱敏内部标识,不放手机号、邮箱这类隐私信息。
参数速查:按问题查,比背参数表有用
| 现象 | 先看哪个参数 / 字段 | 处理思路 |
|---|---|---|
| 同一输入多次输出不一致 | temperature、top_p | 先降低 temperature,不要同时乱调两个 |
| 本地模型或第三方网关暴露更多采样项 | top_k、平台文档 | DeepSeek 官方接口不硬塞;其他平台按文档和固定样本验证 |
| 输出写到一半停了 | finish_reason、max_tokens | finish_reason=length 时按截断处理 |
| JSON 解析失败 | response_format、prompt、业务校验 | 开 json_object,同时在 prompt 里明确只输出 JSON |
| 简单任务成本太高 | model、thinking、usage | 先用 flash,简单抽取关闭 thinking |
| thinking 开了以后还是太贵太慢 | reasoning_effort、reasoning_tokens、延迟 | 默认先用 high,复杂 Agent 再评估 max |
| OpenAI 兼容网关参数很多 | n、seed、store、metadata、service_tier 等 | 不整包照搬,按模型文档确认是否支持和是否会增加成本 |
| 流式输出拿不到总用量 | stream_options.include_usage | 开 stream 时额外处理 usage chunk |
| 输出重复或绕圈 | prompt、任务拆分 | DeepSeek 当前 frequency_penalty / presence_penalty 已 deprecated,不要指望它们生效 |
| 模型需要触发后端动作 | tools、tool_choice、finish_reason | 让模型生成调用参数,代码负责校验和执行 |
| 想判断分类是否摇摆 | logprobs、top_logprobs | 只做离线分析和排障参考,不直接当业务置信度 |
| 多租户或多用户请求需要隔离 | user_id | 使用脱敏内部 ID,不放隐私信息 |
| 想看模型为什么贵 | usage | 记录 prompt、completion、reasoning、cache hit/miss |
老墨总结
调大模型 API 参数,不适合按“参数大全”的方式学。真正上线时,先盯一条链路:任务是什么,输出要多稳定,允许花多少钱,失败时怎么发现。
客服摘要这个例子,可以按这个顺序落地:
- 今天先做:用
deepseek-v4-flash、关闭 thinking、低temperature、开启json_object,把 20 条真实工单跑通。 - 本周补齐:记录
finish_reason和usage,加 JSON parse、字段校验、错误重试和人工兜底。 - 暂时不做:不一上来同时调
temperature、top_p、stop、stream,也不为了简单摘要直接上最强模型、thinking 模式、工具调用和 logprobs。
参数不是越多越专业。能把输入、输出、成本、截断、校验这几个点管住,才是真正能上线的 API 调用。
参考资料
- DeepSeek API Docs:Create Chat Completion
- DeepSeek API Docs:Models & Pricing
- DeepSeek API Docs:The Temperature Parameter
- DeepSeek API Docs:Thinking Mode
- DeepSeek API Docs:Tool Calls
- DeepSeek API Docs:Rate Limit & Isolation
- OpenAI API Reference:Chat Completions
- OpenAI API Reference:Responses
- OpenAI Docs:Reasoning models
文章有帮助?转发给同样在踩坑的朋友。有不同意见?评论区见。
关注公众号:极客老墨
更多 AI 应用开发、工程实践和效率工具分享,欢迎扫码关注。
