Phi-3 跑短对话够快——长文续写得先看上下文怎么配

文章导读
Phi-3 在短对话上表现流畅,通常是因为历史短、单次输出短,KV cache 压力小;换成整篇长文续写,同一套参数就容易变慢或者把前文挤掉。这不是模型变差,而是短对话和长文续写本来就是两种工况:前者比响应速度,后者比上下文预算怎么分配。建议给两类任务各留一套参数,用同一批素材各跑一遍,再决定长文那档愿意牺牲多少延迟。
📋 目录
  1. 明确短对话与长文续写对上下文的不同要求
  2. 检查当前配置里上下文长度与输出上限的实际取值
  3. 用分段续写或摘要压缩处理超长素材
  4. 对两类任务各跑一遍并记录质量与耗时
  5. 给出短对话轻量档与长文稳妥档两套配置
A A

Phi-3 在短对话上表现流畅,通常是因为历史短、单次输出短,KV cache 压力小;换成整篇长文续写,同一套参数就容易变慢或者把前文挤掉。这不是模型变差,而是短对话和长文续写本来就是两种工况:前者比响应速度,后者比上下文预算怎么分配。建议给两类任务各留一套参数,用同一批素材各跑一遍,再决定长文那档愿意牺牲多少延迟。

短对话和长文续写建议分开配置,不要指望一套参数通吃。先确认运行时真正生效的上下文长度和输出上限,而不是只看配置文件里写的那个值;再按素材长度决定是分段续写还是压缩历史。把两类任务用同一批素材各跑一遍,记录输入长度、输出长度、耗时和一次人工质量判断,再固化成轻量档和稳妥档。风险边界:可用上下文受具体模型变体和可用显存限制,任何参数改动都要以运行时日志打印的实际值为准。

明确短对话与长文续写对上下文的不同要求

先把差异拆成三项,配置才有依据。

  • 历史长度:短对话一般只带最近 1 到 6 轮,几百 token 就够;长文续写要带人物设定、前情、术语和上一段原文,很容易到几千甚至上万 token。
  • 单次输出长度:短对话通常几十到两百 token;长文续写一次要生成几百到上千 token,而且要连续生成、不能中途断。
  • 可接受延迟:短对话对首字延迟敏感,等两秒就不舒服;续写更看整体吞吐,慢一点可以接受,但不能丢设定、不能提前收尾。

三项里最容易被忽略的是单次输出长度。因为上下文窗口要同时装下历史、提示和生成内容,很多人只按输入长度去估窗口大小,结果输出一长就把历史挤出去,表现为提前截断,或者前文设定在后半段莫名消失。配参数时建议把「历史 + 提示 + 预计输出」三者相加,再留一点余量。

检查当前配置里上下文长度与输出上限的实际取值

很多人以为改过了,其实改的是默认层。参数生效顺序通常是:模型内置默认值 → 服务启动参数 → 单次请求参数,后者覆盖前者。请求里写的值,会被启动参数或服务端默认值盖住,反过来也一样。

先确认运行时实际值,再谈调优。常见做法:

  • llama.cpp / llama-server:看启动日志里的 n_ctx 与 n_predict;部分版本可以通过 /props 之类的接口读取当前配置。
  • Ollama:用 ollama show <model> `--modelfile`/api/show 返回的 parameters 字段确认 num_ctx;ollama ps 可以看到运行中的上下文设置。
  • 通用办法:发一个最小请求,在服务端日志里搜索 context、n_ctx、max_tokens 这类字段,与你以为的值对比。

被默认值覆盖时,现象通常是这几种:请求里写了较大的 max_tokens,但输出总卡在同一个长度;日志里 n_ctx 是 2048 或 4096,而你以为已经设成 8192;日志出现截断或 context shift 之类的提示;提示 token 数一超过某个值,前面的历史就看不到了。看到其中任意一条,就说明该层的值没生效,要往上一层找。

用分段续写或摘要压缩处理超长素材

最稳的思路是不把整篇塞进上下文,而是切段推进。

切分规则:优先按段落或小标题切,单段控制在 600 到 1200 字,给输出和衔接文本留出约两成余量。不要按固定字符数硬切,尽量落在段落边界上,避免句子被劈开。

段间衔接:每段请求里保留上一段末尾一到两段的原文,再加上本段的场景、时间、视角说明,以及一份简短的人物与术语表。这样模型继续写时不会把人称和时间线接错。

压缩历史时必须留下的内容:人物名与固定设定、专有名词和译名、尚未闭合的线索或伏笔、当前场景与叙述视角、风格约束(人称、时态、要避免的表达)。可以丢掉的:已经确认过的寒暄、重复的景物描写、已经收束的支线细节。

一个可替换的请求骨架:

【前情摘要】两三句,写清人物、地点、当前目标
【待续原文末尾】直接粘上一段结尾,保持语气一致
【本次要求】续写约 XXX 字,保持人称与时态,不要提前收尾

摘要由谁生成、压缩到什么程度,可以先用同一段素材试几次,看续写结果有没有丢掉关键信息。

对两类任务各跑一遍并记录质量与耗时

不要凭感觉比较,用同一批素材各跑一遍。素材可以这样准备:三五组短对话,两到三段三千字左右的长文片段。每组都跑两种配置,把结果记下来。

记录字段建议固定为:任务类型、输入长度(token)、输出长度(token)、耗时(秒)、人工质量判断。输入和输出长度可以在服务端日志或接口返回的 usage 字段里取到;耗时最简单的做法是在调用外面包一层计时,写进 CSV。

# 伪代码示意,按自己的调用方式替换
start=$(date +%s.%N)
# 调用模型,把请求与响应分别存文件
tokens_in=$(grep -o 'prompt_tokens[^,]*' resp.json)
tokens_out=$(grep -o 'completion_tokens[^,]*' resp.json)
end=$(date +%s.%N)
echo "短对话,${tokens_in},${tokens_out},$(echo "$end - $start" | bc)" >> bench.csv

人工质量判断建议只看四项,逐条对:是否违背或漏掉前文设定;人称、时间线是否错乱;是否中途截断或提前收尾;风格是否明显漂移。每项给「通过 / 需修 / 不可用」三档,不要打感觉分。四项里出现两个「需修」,这一档配置就值得再调。

给出短对话轻量档与长文稳妥档两套配置

短对话轻量档

历史保留 3 到 6 轮,上下文 2048 到 4096,输出上限 128 到 256,温度偏高一些保证自然。适配客服问答、闲聊、单轮指令这类场景。

# 短对话轻量档:llama.cpp server 启动参数
# -c 是历史与生成内容合计的窗口;-n 是单次生成上限
llama-server -m ./phi-3-mini-q4_k_m.gguf \
  -c 4096 \
  -n 256 \
  `--host` 127.0.0.1 `--port` 8080
// 对应的单次请求参数
{
  "messages": [
    {"role": "system", "content": "你是简洁的助手,回答控制在三句以内。"},
    {"role": "user", "content": "……只保留最近 3 到 6 轮历史……"}
  ],
  "max_tokens": 256,
  "temperature": 0.7,
  "top_p": 0.9
}

长文稳妥档

历史只保留 1 到 2 轮原文加摘要,上下文尽量开大(能否到 8192 要看模型变体和可用显存),输出上限 512 到 1024,温度压低让风格更稳。适配章节续写、报告扩写这类场景。

# 长文稳妥档:给续写留足预算
# -c 开多大取决于模型支持的最大上下文与实际显存
llama-server -m ./phi-3-mini-q4_k_m.gguf \
  -c 8192 \
  -n 1024 \
  `--host` 127.0.0.1 `--port` 8081
// 长文续写请求:历史用摘要,原文只带末尾
{
  "messages": [
    {"role": "system", "content": "续写下面的内容,保持人称与时态一致,不要提前收尾。"},
    {"role": "user", "content": "【前情摘要】……\n【待续原文末尾两段】……\n【本次要求】续写约 600 字"}
  ],
  "max_tokens": 1024,
  "temperature": 0.4,
  "top_p": 0.9,
  "stop": ["<|end|>"]
}

每个值的取舍理由都在注释里:上下文决定能装多少历史和输出,输出上限决定一次能写多长,温度影响续写的收敛程度。改完先按第 2 节的方法确认运行时实际生效值,再用第 4 节的记录表复测一遍,不要只看配置文件里写的数字。