大模型 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-flashdeepseek-v4-prodeepseek-chatdeepseek-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 支持 highmax。普通请求默认是 high,一些复杂 Agent 请求可能会自动走到 max。为了兼容,lowmedium 会映射到 highxhigh 会映射到 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 / Math0.0
Data Cleaning / Data Analysis1.0
General Conversation1.3
Translation1.3
Creative Writing / Poetry1.5

我们的客服摘要更接近数据清洗和结构化提取。如果优先追求分类稳定,可以从 00.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 模式不支持 temperaturetop_ppresence_penaltyfrequency_penalty官方说了,为了兼容,传这些参数不会报错,但也不会生效。也就是说,代码里看起来写了参数,实际接口可能根本没听。

top_p:别和 temperature 一起乱拧

top_p 也是控制随机性的参数,但它不是调“温度”,而是控制候选词范围

简单说,模型每一步会给很多候选 token 排概率。top_p=0.9 的意思是:只从累计概率达到 90% 的那批候选里采样,剩下长尾候选直接不看。

这东西像什么?像点外卖时只看评分前 90% 的店,剩下那些特别冷门、特别离谱的选择先屏蔽掉。它能减少跑偏,但不是万能保险。

DeepSeek 和 OpenAI 的文档都有同一个建议:temperaturetop_p 通常只改一个,另一个保持默认。

客服摘要接口可以按这个顺序处理:

  • 第一轮只调 temperaturetop_p 保持默认 1
  • 如果输出偶尔出现离题词、奇怪分类,再小心尝试 top_p
  • 不同时把 temperaturetop_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:先讲 temperaturetop_ptop_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}

官方文档这里有两个关键提醒:

  1. response_format 设为 json_object 后,模型生成的内容会是合法 JSON。
  2. 仍然要在 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,不一定要用工具调用。但只要业务开始接后端动作,toolstool_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 就有价值。DeepSeekstream_options.include_usage 可以让流式响应在结束前额外返回一次完整 usage,这对成本统计很重要。

一个保守建议:

  • 面向人看的长文本:可以开 stream
  • 面向程序解析的 JSON:先不开 stream,等链路稳定后再考虑。
  • 开了 stream,要处理 data: [DONE],也要处理异常中断和半截内容。

usage:上线前先把账算清楚

参数调优最后一定要回到数据。这个习惯越早养成越好。

DeepSeek 响应里的 usage 不只告诉你用了多少 token,还会拆出缓存命中情况:

  • prompt_tokens
  • completion_tokens
  • total_tokens
  • prompt_cache_hit_tokens
  • prompt_cache_miss_tokens
  • completion_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。所以这里把参数分成两类看:

  • 通用层modelmessages/inputtemperaturetop_pmax_tokens/max_output_tokensresponse_format/text.formattoolstool_choicestreamusage
  • OpenAI 扩展层:OpenAI 有,但 DeepSeek 或其他兼容服务不一定支持;迁移时要逐个核对。

下面这些参数,至少要知道它们解决什么问题:

参数主要在哪类接口里常见解决什么问题使用建议
developer messageOpenAI Chat/Responsessystem 更明确地表达开发者指令,OpenAI 新模型里常见写 OpenAI 原生代码时优先用;兼容网关先确认是否支持
max_output_tokensOpenAI Responses控制响应最多生成多少 token,包含可见输出和推理 token用 Responses API 时看它;Chat Completions 仍常见 max_tokens / max_completion_tokens
reasoning.effort / reasoning_effortOpenAI reasoning models控制推理强度,影响延迟、成本和 reasoning token复杂任务才调,简单抽取先低推理或不开
verbosityOpenAI 新模型控制回答详略,lowmediumhigh比在 prompt 里反复写“简短一点”更直接,但要确认模型支持
nChat Completions一次生成多个候选线上默认 1,否则 token 成本按多个候选一起涨
seedChat Completions尽量复现采样结果只当调试辅助,不把它当严格确定性保证
parallel_tool_calls工具调用允许模型并行发起多个工具调用查询类 Agent 可以开;涉及写操作、扣费、状态流转时要谨慎
storeChat/Responses是否把请求输出存储用于平台侧能力默认先关;只有明确要用 evals、distillation 或平台追踪时再开
metadataChat/Responses给请求挂业务标签,方便后续查询或看板过滤放订单类型、实验组、版本号,不放隐私
prompt_cache_keyChat Completions帮助平台做相似请求缓存分桶比旧的 user 更偏缓存用途;用稳定、脱敏的业务键
safety_identifierChat Completions帮助平台做滥用检测用哈希后的用户标识,不传手机号、邮箱、真实姓名
service_tierChat Completions控制服务处理层级,比如 default、flex、priority延迟和成本敏感时再引入,普通业务先不复杂化
predictionChat Completions已知大部分输出时,用预测输出降低延迟适合文件小改、模板补全,不适合开放式客服摘要
modalities / audioChat Completions要求模型同时输出文本、音频等做语音助手时才需要;普通 JSON 接口不要加
truncationResponses上下文超长时如何截断长对话或 Agent 才重点看;结构化短请求先控制输入长度
previous_response_idResponses让 Responses API 接续上一轮状态做多轮 Agent 有用;无状态业务接口要谨慎,避免隐藏上下文影响复现
includeResponses要求响应额外返回指定信息排障和追踪时有用,线上默认别把响应撑太大

这里最容易误判的是“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
  • 有的参数会改变数据留存和隐私边界,比如 storemetadata、用户标识类字段。

所以兼容接口参数可以分成三档:

档位参数处理方式
第一档:核心必查modelmax_tokens/max_output_tokenstemperaturetop_presponse_formattoolsstreamusage每个模型接入前都要确认
第二档:OpenAI 原生增强reasoning_effortverbosityparallel_tool_callspredictionservice_tierprevious_response_id用 OpenAI 原生接口时重点看
第三档:治理和观测storemetadataprompt_cache_keysafety_identifierinclude上线治理时再加,先考虑隐私和数据留存

回到客服摘要接口,不要为了“兼容 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 后它没有意义。
  • temperature0.2,优先稳定分类。
  • max_tokens512,够用但不放飞。
  • response_format 开 JSON 模式,但仍然做 JSON parse 和字段校验。
  • finish_reason != stop 直接当异常处理,不让半截结果混进业务。
  • usage 打出来,为后续调成本和缓存命中率留数据。
  • user_id 用脱敏内部标识,不放手机号、邮箱这类隐私信息。

参数速查:按问题查,比背参数表有用

现象先看哪个参数 / 字段处理思路
同一输入多次输出不一致temperaturetop_p先降低 temperature,不要同时乱调两个
本地模型或第三方网关暴露更多采样项top_k、平台文档DeepSeek 官方接口不硬塞;其他平台按文档和固定样本验证
输出写到一半停了finish_reasonmax_tokensfinish_reason=length 时按截断处理
JSON 解析失败response_format、prompt、业务校验json_object,同时在 prompt 里明确只输出 JSON
简单任务成本太高modelthinkingusage先用 flash,简单抽取关闭 thinking
thinking 开了以后还是太贵太慢reasoning_effortreasoning_tokens、延迟默认先用 high,复杂 Agent 再评估 max
OpenAI 兼容网关参数很多nseedstoremetadataservice_tier不整包照搬,按模型文档确认是否支持和是否会增加成本
流式输出拿不到总用量stream_options.include_usage开 stream 时额外处理 usage chunk
输出重复或绕圈prompt、任务拆分DeepSeek 当前 frequency_penalty / presence_penalty 已 deprecated,不要指望它们生效
模型需要触发后端动作toolstool_choicefinish_reason让模型生成调用参数,代码负责校验和执行
想判断分类是否摇摆logprobstop_logprobs只做离线分析和排障参考,不直接当业务置信度
多租户或多用户请求需要隔离user_id使用脱敏内部 ID,不放隐私信息
想看模型为什么贵usage记录 prompt、completion、reasoning、cache hit/miss

老墨总结

调大模型 API 参数,不适合按“参数大全”的方式学。真正上线时,先盯一条链路:任务是什么,输出要多稳定,允许花多少钱,失败时怎么发现。

客服摘要这个例子,可以按这个顺序落地:

  1. 今天先做:用 deepseek-v4-flash、关闭 thinking、低 temperature、开启 json_object,把 20 条真实工单跑通。
  2. 本周补齐:记录 finish_reasonusage,加 JSON parse、字段校验、错误重试和人工兜底。
  3. 暂时不做:不一上来同时调 temperaturetop_p、stop、stream,也不为了简单摘要直接上最强模型、thinking 模式、工具调用和 logprobs。

参数不是越多越专业。能把输入、输出、成本、截断、校验这几个点管住,才是真正能上线的 API 调用。

参考资料

文章有帮助?转发给同样在踩坑的朋友。有不同意见?评论区见。

关注公众号:极客老墨

更多 AI 应用开发、工程实践和效率工具分享,欢迎扫码关注。

极客老墨微信公众号二维码

相关阅读