把 Phi-3 接进本地写作工具 / 上下文长度跟输出上限要分开配

文章导读
Phi-3 接进本地写作工具后,最容易被混为一谈的是两个值:上下文窗口决定一次请求能装下多少 token(含系统提示、历史消息和本次输出),单次输出上限只管这次要生成多长。长文续写被截断,通常不是模型能力问题,而是这两个值被写在了同一层、同一个参数上,或者历史消息把输入顶满,服务端在你不注意的时候把前半段裁掉了。处理顺序建议是:先看返回的结束原因判断卡在哪一侧,再拆开配置,最后用可核对的 toke
📋 目录
  1. A 在配置里把上下文窗口和单次输出上限拆成两个字段
  2. B 用一段已知长度的长文测出截断发生在输入端还是输出端
  3. C 检查写作工具怎么拼接历史消息
  4. D 改完参数复测并记录输入输出 token 数
  5. E 把稳定的参数组合写进配置文件并加注释
A A

Phi-3 接进本地写作工具后,最容易被混为一谈的是两个值:上下文窗口决定一次请求能装下多少 token(含系统提示、历史消息和本次输出),单次输出上限只管这次要生成多长。长文续写被截断,通常不是模型能力问题,而是这两个值被写在了同一层、同一个参数上,或者历史消息把输入顶满,服务端在你不注意的时候把前半段裁掉了。处理顺序建议是:先看返回的结束原因判断卡在哪一侧,再拆开配置,最后用可核对的 token 数确认改动生效。

把 Phi-3 当写作后端时,上下文窗口和单次输出上限建议在两个不同的层配置:上下文在服务端加载模型时确定,输出上限在每次请求里传。长文被截断先用返回的结束原因区分——输出侧会是 length 一类的原因且长度刚好等于上限,输入侧通常日志里能看到 prompt 被裁剪、回复接不上或开始重复开头。再检查写作工具是否每轮把历史整段追加。具体字段名和生效位置需要结合你用的推理服务和工具版本确认。

在配置里把上下文窗口和单次输出上限拆成两个字段

先确认工具链里这两个值是不是被写在了同一个参数上。很多写作工具只暴露一个“长度”输入框,内部同时映射到上下文和输出,这种写法最容易互相拖累:输出调大了,留给历史和输入的空间就变小;上下文调大了,输出又可能被服务端默认值卡住。

常见拆法是这样两类字段:

  • 上下文窗口:llama.cpp 启动参数里的 `--ctx-size` / -c,Ollama 侧的 num_ctx,部分 OpenAI 兼容服务里的 context_lengthmax_seq_len它在服务端加载模型时生效,改完通常要重启服务。
  • 单次输出上限:请求体里的 max_tokens,llama.cpp 的 n_predict,Ollama 的 num_predict,transformers 侧的 max_new_tokens它在每次请求时生效,不用重启。

改错层的典型现象:只在请求里把 max_tokens 调大,输出还是早早停住,说明服务端侧的上下文或输出默认值在起作用;反过来,请求里带了一个很大的上下文值,但服务端 `--ctx-size` 更小,服务端通常会静默截断提示词,日志里可能出现 truncating input 之类的字样,而返回结果看上去只是“答得不对”。需要先把这两个字段落在哪一层弄清楚,再谈调值。

把 Phi-3 接进本地写作工具 / 上下文长度跟输出上限要分开配

用一段已知长度的长文测出截断发生在输入端还是输出端

测试素材建议自己准备:把一段你能数出长度的文章复制进工具,比如固定取 1200 个汉字左右,用本地 tokenizer 或工具自带的计数功能先记下输入 token 数。同一段素材、同一句提示(例如“在保持人称和语气的前提下续写 600 字”),只改参数不改素材,这样结果才有可比性。

拿到返回后重点核对两个特征:

  • 结束原因:OpenAI 兼容接口看 finish_reason,部分服务用 stop_reasondone_reason。取值为 length 一类,通常是输出被上限截断;取值为 stop,说明是模型自己收尾了。
  • 返回长度:把返回内容再数一次 token,如果刚好贴近你设的输出上限,基本可以判定是输出侧被卡。

两种截断的区别特征:输出侧截断时,正文往往在半句或半段处硬断,前后文语意完整,只是没写完;输入侧截断时,回复常表现为接不上前文、把刚写过的内容又写一遍、或者从某个中间段落开始回应,服务端日志同时能看到提示词被裁剪的记录。可以先做一次对照:把 max_tokens 明显调小再发同一段长文,如果截断位置跟着小上限走,就是输出侧;如果无论输出上限怎么改,回复都从同一处错位,则更可能是输入被裁。

检查写作工具怎么拼接历史消息

不少“截断”不是模型的问题,而是写作工具每轮把整段对话原样追加,几轮之后输入已经接近上下文窗口,服务端只能从旧的一端丢消息。通常的消息数组构造是这样的:第一轮是 systemuser,之后每轮把上一轮的 assistant 回复和新的 user 指令 append 进去。历史裁剪一般发生在“拼好数组、准备发请求”这一步,而不是在保存历史的时候,所以你在界面上看到的对话长度和真正发出去的并不一致。

把 Phi-3 接进本地写作工具 / 上下文长度跟输出上限要分开配

判断方法很直接:在真正发起请求的那个函数里,把最终 payload 打印出来。下面是一段通用骨架,字段名按你的接口替换即可,位置放在 HTTP 调用之前:

import json, requests

URL = "http://127.0.0.1:11434/v1/chat/completions"  # 换成你的本地服务地址

def send(messages, **kw):
    payload = {
        "model": "phi3",
        "messages": messages,
        "max_tokens": kw.get("max_tokens", 768),
        "temperature": 0.7,
    }
    # 真正发出去的内容落盘,用来核对历史有没有重复堆积或被裁剪
    with open("last_payload.json", "w", encoding="utf-8") as f:
        json.dump(payload, f, ensure_ascii=False, indent=2)
    return requests.post(URL, json=payload, timeout=120).json()

打开 last_payload.json,看两件事:一是 messages 里有没有同一段正文出现两次以上,二是轮数增长后前面的消息是什么时候开始消失的。找到那一步,再决定是做轮数上限(只保留最近几轮),还是按 token 预算从旧到新丢弃。两种都可以,但不要在裁剪之后又原样把全文塞回 system 提示里,那等于没裁。

改完参数复测并记录输入输出 token 数

改动是否真的生效,靠感觉判断很容易出错,建议用返回里的用量字段核对。OpenAI 兼容接口一般在 usage 里给出 prompt_tokenscompletion_tokens;llama.cpp 服务端会打印 timings 之类的统计。把每次请求的这几项记下来:

把 Phi-3 接进本地写作工具 / 上下文长度跟输出上限要分开配
  • 输入长度(prompt_tokens):用来确认历史裁剪是不是按预期生效。
  • 输出长度(completion_tokens):确认是否顶到了你设的 max_tokens
  • 总耗时:只作为参考,机器负载不同差异很大,不要拿它当结论。
  • 结束原因:与上面两项一起看,才判断得出截断在哪一侧。

对比时保持素材和提示词不变,只变一个参数,一次改一处。如果输入长度没变但输出变长了,说明动的是输出上限;如果输入长度明显下降,说明历史的裁剪规则起作用了。这一步不需要写出漂亮的结果,只要能核对数字对得上,就说明配置改到了正确的那一层。

把稳定的参数组合写进配置文件并加注释

调顺之后建议把参数固化成文件,并在注释里写清用途和调整方向。这样下次升级工具或换模型时,能一眼看出哪些值是刻意设的、哪些是可以再调的,不至于被新版本的默认值悄悄覆盖。下面是一份通用示例,字段名按你的工具替换:

server:                     # 服务端启动时生效,改动后需要重启
  model: ./phi3-mini-instruct.gguf
  ctx_size: 4096            # 上下文窗口:输入 + 历史 + 本次输出共用的总预算
                            # 显存允许可以调大;调小会让长文更容易被裁剪

request:                    # 每次请求生效,不用重启
  max_tokens: 768           # 单次输出上限,只约束生成部分
                            # 续写长文可以调大,但要从 ctx_size 里预留出这块预算
  temperature: 0.7          # 写作场景偏高一点更自然,改小更保守
  stop: ["\n\n\n"]         # 需要时用停止词截住多余的收尾语

history:
  max_rounds: 6             # 只保留最近几轮对话,防止输入被历史顶满
  reserve_for_output: 800   # 从 ctx_size 中先扣掉的输出预算
                            # 建议略大于 max_tokens,留出提示词收尾的余量

改动这份配置时建议一次只动一个值并复测一遍:先确认服务端侧生效(重启后日志里的上下文值跟着变),再确认请求侧生效(返回的 completion_tokensmax_tokens 变化)。ctx_sizemax_tokens 之间要留出空间给输入和历史,具体留多少需要结合你机器的显存和实际文章长度确认,没有一组对所有环境都合适的值。