大模型 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、延迟和上下文处理细节。简单抽取任务先关掉,后面如果发现分类确实需要推理,再打开对比。
老墨说一句:我现在选模型,不会先问“哪个最强”,而是先问“哪个能用最低成本跑过验收线”。模型越强,模式越复杂,后面的成本、延迟和排障压力也会跟着上来。
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,而不是只看单条样例顺不顺眼。
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}
这里我不会偷懒。大模型输出进业务系统之前,我会把它当成外部用户输入一样校验。
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
有了这些字段,才知道一次改参数到底是在省钱、提稳,还是只是看起来舒服。
一段可以直接改的 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}
24
25type ResponseFormat struct {
26 Type string `json:"type"` // "text" 或 "json_object"
27}
28
29type ChatRequest struct {
30 Model string `json:"model"`
31 Messages []Message `json:"messages"`
32 Thinking *Thinking `json:"thinking,omitempty"`
33 MaxTokens int `json:"max_tokens,omitempty"`
34 Temperature *float64 `json:"temperature,omitempty"`
35 TopP *float64 `json:"top_p,omitempty"`
36 ResponseFormat *ResponseFormat `json:"response_format,omitempty"`
37 Stop []string `json:"stop,omitempty"`
38 Stream bool `json:"stream,omitempty"`
39}
40
41type ChatResponse struct {
42 Choices []struct {
43 Message struct {
44 Content string `json:"content"`
45 ReasoningContent string `json:"reasoning_content"`
46 } `json:"message"`
47 FinishReason string `json:"finish_reason"`
48 } `json:"choices"`
49 Usage struct {
50 PromptTokens int `json:"prompt_tokens"`
51 CompletionTokens int `json:"completion_tokens"`
52 TotalTokens int `json:"total_tokens"`
53 PromptCacheHitTokens int `json:"prompt_cache_hit_tokens"`
54 PromptCacheMissTokens int `json:"prompt_cache_miss_tokens"`
55 CompletionDetails struct {
56 ReasoningTokens int `json:"reasoning_tokens"`
57 } `json:"completion_tokens_details"`
58 } `json:"usage"`
59}
60
61type TicketSummary struct {
62 Summary string `json:"summary"`
63 Category string `json:"category"`
64 RiskLevel string `json:"risk_level"`
65 NextAction string `json:"next_action"`
66}
67
68func float64Ptr(v float64) *float64 { return &v }
69
70func callDeepSeek(ctx context.Context, req ChatRequest) (*ChatResponse, error) {
71 apiKey := os.Getenv("DEEPSEEK_API_KEY")
72 if apiKey == "" {
73 return nil, fmt.Errorf("DEEPSEEK_API_KEY is empty")
74 }
75
76 body, err := json.Marshal(req)
77 if err != nil {
78 return nil, fmt.Errorf("marshal request: %w", err)
79 }
80
81 httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, deepseekAPI, bytes.NewReader(body))
82 if err != nil {
83 return nil, err
84 }
85 httpReq.Header.Set("Content-Type", "application/json")
86 httpReq.Header.Set("Authorization", "Bearer "+apiKey)
87
88 client := &http.Client{Timeout: 60 * time.Second}
89 resp, err := client.Do(httpReq)
90 if err != nil {
91 return nil, fmt.Errorf("call deepseek: %w", err)
92 }
93 defer resp.Body.Close()
94
95 if resp.StatusCode != http.StatusOK {
96 b, _ := io.ReadAll(resp.Body)
97 return nil, fmt.Errorf("deepseek api status=%d body=%s", resp.StatusCode, string(b))
98 }
99
100 var result ChatResponse
101 if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
102 return nil, fmt.Errorf("decode response: %w", err)
103 }
104 if len(result.Choices) == 0 {
105 return nil, fmt.Errorf("empty choices")
106 }
107
108 return &result, nil
109}
110
111func validateSummary(s TicketSummary) error {
112 if s.Summary == "" || s.Category == "" || s.RiskLevel == "" || s.NextAction == "" {
113 return fmt.Errorf("missing required field: %+v", s)
114 }
115
116 allowedRisk := map[string]bool{
117 "low": true, "medium": true, "high": true,
118 }
119 if !allowedRisk[s.RiskLevel] {
120 return fmt.Errorf("invalid risk_level: %s", s.RiskLevel)
121 }
122
123 return nil
124}
125
126func summarizeTicket(ctx context.Context, dialogue string) (TicketSummary, error) {
127 req := ChatRequest{
128 Model: "deepseek-v4-flash",
129 Thinking: &Thinking{Type: "disabled"},
130 Temperature: float64Ptr(0.2),
131 MaxTokens: 512,
132 ResponseFormat: &ResponseFormat{
133 Type: "json_object",
134 },
135 Messages: []Message{
136 {
137 Role: "system",
138 Content: `你是客服工单分析助手。只输出 JSON,不要输出 Markdown,不要解释。
139字段必须包含:
140- summary: 一句话总结用户问题和客服答复
141- category: refund/payment/account/logistics/other 之一
142- risk_level: low/medium/high 之一
143- next_action: 下一步处理动作`,
144 },
145 {
146 Role: "user",
147 Content: "请分析这段客服对话:\n" + dialogue,
148 },
149 },
150 }
151
152 resp, err := callDeepSeek(ctx, req)
153 if err != nil {
154 return TicketSummary{}, err
155 }
156
157 choice := resp.Choices[0]
158 if choice.FinishReason == "length" {
159 return TicketSummary{}, fmt.Errorf("model output truncated by max_tokens")
160 }
161 if choice.FinishReason != "stop" {
162 return TicketSummary{}, fmt.Errorf("unexpected finish_reason: %s", choice.FinishReason)
163 }
164
165 var summary TicketSummary
166 if err := json.Unmarshal([]byte(choice.Message.Content), &summary); err != nil {
167 return TicketSummary{}, fmt.Errorf("invalid json output: %w, raw=%s", err, choice.Message.Content)
168 }
169 if err := validateSummary(summary); err != nil {
170 return TicketSummary{}, err
171 }
172
173 fmt.Printf(
174 "usage prompt=%d completion=%d total=%d cache_hit=%d cache_miss=%d reasoning=%d\n",
175 resp.Usage.PromptTokens,
176 resp.Usage.CompletionTokens,
177 resp.Usage.TotalTokens,
178 resp.Usage.PromptCacheHitTokens,
179 resp.Usage.PromptCacheMissTokens,
180 resp.Usage.CompletionDetails.ReasoningTokens,
181 )
182
183 return summary, nil
184}
185
186func main() {
187 ctx := context.Background()
188 dialogue := `用户:我上周申请退款了,怎么还没到账?
189客服:您好,退款一般 3-5 个工作日到账。您这笔订单是周五提交的,预计本周三前到账。
190用户:那我再等等。`
191
192 summary, err := summarizeTicket(ctx, dialogue)
193 if err != nil {
194 fmt.Println("error:", err)
195 return
196 }
197 fmt.Printf("%+v\n", summary)
198}
完整的代码见 Github.
这段代码里有几个故意保守的选择:
thinking先关掉,因为摘要抽取不需要复杂推理。temperature设0.2,优先稳定分类。max_tokens设512,够用但不放飞。response_format开 JSON 模式,但仍然做 JSON parse 和字段校验。finish_reason != stop直接当异常处理,不让半截结果混进业务。- usage 打出来,为后续调成本和缓存命中率留数据。
参数速查:我不会背参数表,只按问题查
| 现象 | 先看哪个参数 / 字段 | 我会怎么处理 |
|---|---|---|
| 同一输入多次输出不一致 | temperature、top_p | 先降低 temperature,不要同时乱调两个 |
| 输出写到一半停了 | finish_reason、max_tokens | finish_reason=length 时按截断处理 |
| JSON 解析失败 | response_format、prompt、业务校验 | 开 json_object,同时在 prompt 里明确只输出 JSON |
| 简单任务成本太高 | model、thinking、usage | 先用 flash,简单抽取关闭 thinking |
| 流式输出拿不到总用量 | stream_options.include_usage | 开 stream 时额外处理 usage chunk |
| 输出重复或绕圈 | prompt、任务拆分 | DeepSeek 当前 frequency_penalty / presence_penalty 已 deprecated,不要指望它们生效 |
| 想看模型为什么贵 | 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 模式。
参数不是越多越专业。能把输入、输出、成本、截断、校验这几个点管住,才是真正能上线的 API 调用。
参考资料
- DeepSeek API Docs:Create Chat Completion
- DeepSeek API Docs:Models & Pricing
- DeepSeek API Docs:The Temperature Parameter
- DeepSeek API Docs:Thinking Mode
文章有帮助?转发给同样在踩坑的朋友。有不同意见?评论区见。
关注公众号:极客老墨
更多 AI 应用开发、工程实践和效率工具分享,欢迎扫码关注。
