大模型 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、延迟和上下文处理细节。简单抽取任务先关掉,后面如果发现分类确实需要推理,再打开对比。

老墨说一句:我现在选模型,不会先问“哪个最强”,而是先问“哪个能用最低成本跑过验收线”。模型越强,模式越复杂,后面的成本、延迟和排障压力也会跟着上来。

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,而不是只看单条样例顺不顺眼。

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}

这里我不会偷懒。大模型输出进业务系统之前,我会把它当成外部用户输入一样校验。

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

有了这些字段,才知道一次改参数到底是在省钱、提稳,还是只是看起来舒服。

一段可以直接改的 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 先关掉,因为摘要抽取不需要复杂推理。
  • temperature0.2,优先稳定分类。
  • max_tokens512,够用但不放飞。
  • response_format 开 JSON 模式,但仍然做 JSON parse 和字段校验。
  • finish_reason != stop 直接当异常处理,不让半截结果混进业务。
  • usage 打出来,为后续调成本和缓存命中率留数据。

参数速查:我不会背参数表,只按问题查

现象先看哪个参数 / 字段我会怎么处理
同一输入多次输出不一致temperaturetop_p先降低 temperature,不要同时乱调两个
输出写到一半停了finish_reasonmax_tokensfinish_reason=length 时按截断处理
JSON 解析失败response_format、prompt、业务校验json_object,同时在 prompt 里明确只输出 JSON
简单任务成本太高modelthinkingusage先用 flash,简单抽取关闭 thinking
流式输出拿不到总用量stream_options.include_usage开 stream 时额外处理 usage chunk
输出重复或绕圈prompt、任务拆分DeepSeek 当前 frequency_penalty / presence_penalty 已 deprecated,不要指望它们生效
想看模型为什么贵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 模式。

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

参考资料

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

关注公众号:极客老墨

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

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

相关阅读