Skip to content

DeepSeek API 是无状态的,每次请求需要将完整对话历史一起传入。本文介绍多轮对话的正确实现方式、上下文窗口管理策略,以及在 1M 超长上下文下如何控制 token 成本。

DeepSeek API 多轮对话

核心原则:API 无状态,上下文自己维护

DeepSeek API 的 /chat/completions 接口不记录任何对话状态。要实现多轮对话,需要在每次请求时把完整的历史消息传入 messages 数组。

基本实现

typescript
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.deepseek.com",
  apiKey: process.env.DEEPSEEK_API_KEY,
});

// 维护对话历史
const messages: OpenAI.ChatCompletionMessageParam[] = [
  { role: "system", content: "You are a helpful assistant." },
];

async function chat(userInput: string): Promise<string> {
  // 添加用户消息
  messages.push({ role: "user", content: userInput });

  const response = await client.chat.completions.create({
    model: "deepseek-v4-pro",
    messages,
  });

  const assistantMessage = response.choices[0].message;
  // 把模型回复加入历史
  messages.push(assistantMessage);

  return assistantMessage.content ?? "";
}

// 使用
const reply1 = await chat("世界最高峰是哪座山?");
console.log(reply1); // 珠穆朗玛峰

const reply2 = await chat("它的海拔是多少?");
console.log(reply2); // 8848.86 米(模型知道"它"指上一轮的主题)

Python 版本:

python
from openai import OpenAI

client = OpenAI(api_key="<your api key>", base_url="https://api.deepseek.com")

messages = [{"role": "system", "content": "You are a helpful assistant."}]

def chat(user_input: str) -> str:
    messages.append({"role": "user", "content": user_input})
    response = client.chat.completions.create(
        model="deepseek-v4-pro",
        messages=messages,
    )
    assistant_msg = response.choices[0].message
    messages.append(assistant_msg)
    return assistant_msg.content

print(chat("世界最高峰是哪座山?"))
print(chat("它的海拔是多少?"))

上下文长度管理

DeepSeek V4 支持 1M token 上下文,理论上可以保留极长的对话历史。但随着对话轮数增加,每次请求的输入 token 越来越多,成本也线性增长。

推荐策略:

方案一:保留最近 N 轮(简单)

typescript
const MAX_HISTORY = 20; // 保留最近 20 条消息

function trimHistory(
  messages: OpenAI.ChatCompletionMessageParam[],
  systemPrompt: string,
): OpenAI.ChatCompletionMessageParam[] {
  const system = [{ role: "system" as const, content: systemPrompt }];
  const history = messages.filter((m) => m.role !== "system");
  return [...system, ...history.slice(-MAX_HISTORY)];
}

方案二:token 预算控制(精确)

typescript
function trimByTokenBudget(
  messages: OpenAI.ChatCompletionMessageParam[],
  maxTokens = 50000,
): OpenAI.ChatCompletionMessageParam[] {
  // 估算:中文约 0.6 token/字符,英文约 0.3 token/字符
  const estimateTokens = (text: string) => text.length * 0.6;
  
  let total = 0;
  const kept: OpenAI.ChatCompletionMessageParam[] = [];
  
  // 从最新消息往前保留
  for (let i = messages.length - 1; i >= 0; i--) {
    const msg = messages[i];
    const tokens = estimateTokens(String(msg.content));
    if (total + tokens > maxTokens && msg.role !== "system") break;
    kept.unshift(msg);
    total += tokens;
  }
  
  return kept;
}

与 KV Cache 配合使用

DeepSeek 的 KV Cache 在多轮对话中特别有价值:每次请求传入的历史消息前缀相同,就会触发缓存命中,将缓存部分的费用降至 0.025元/M token(pro)。

建议:

  • 保持 system prompt 不变:同样的 system prompt 每次都命中缓存
  • 不要修改历史消息:只在末尾追加新消息,保证前缀稳定

详见:上下文硬盘缓存

常见问题

Q: 可以修改历史消息中的 assistant 回复吗?

A: 技术上可以,但会破坏 KV Cache 的前缀匹配,增加成本。也可能让模型产生混淆,不建议随意修改历史消息。

Q: 怎么实现"记住用户偏好"这类长期记忆?

A: DeepSeek API 本身不支持持久化记忆。通常做法是:把关键信息提炼成摘要放在 system prompt 里,或使用外部数据库(如向量库)检索相关历史注入上下文。

Q: 多轮对话中 token 突然暴增怎么排查?

A: 检查 response.usage.prompt_tokens。如果历史越来越长,说明没有做 history trimming;如果单次暴增,检查是否有大文件/长文档被附加进了 messages。