遇到 Llama 3 处理中文长文档答非所问、前后矛盾或漏段落,先别急着改提示词。更常见的三条原因是:中文分词导致 token 消耗比预期高、对话模板(角色标记与分隔符)没套对、或者内容在被送进模型之前就已经被截断。判断顺序建议是:先复现并存档,再定位截断位置,然后换模板对照,最后缩短输入做单变量验证。这三件事能通过日志、token 计数和同题对照跑出来,不需要靠感觉猜。
中文长文档输出异常时,优先确认三件事:输入实际 token 数是否超出上下文窗口、中文对话模板的角色标记与分隔符是否匹配当前模型、被提问的关键段落是否真的进入了上下文。做法是先固定一份可复现样本并保存完整请求与返回,再用 token 计数脚本定位截断位置,然后用同一问题跑两套模板对照,最后只保留关键段落做单变量验证。边界是:本文不给出“分词一定导致 X% 差异”这类结论,具体阈值需结合所用推理框架、分词器版本和上下文窗口配置确认。
复现问题并保存原始输入与完整输出
先拿到稳定可复现的样本,后面的对照才有意义。建议固定一份中文长文档和一条固定的提问,反复跑同一请求,直到你能观察到相同的异常表现(比如每次都漏掉第 3 段或每次都把两个时间点说反)。
输入长度要同时统计两个值:字符数和 token 数。字符数用 wc -m 就能看,token 数需要走分词器,不同模型和分词器版本结果可能不一致,所以要记录你用的是哪一份。下面是一个最小保存结构,把请求体、响应体和统计写进同一个目录,方便后面横向比:
sample_01/
input.txt # 原始中文长文档
question.txt # 固定提问
request.json # 完整请求体(含模型名、参数、messages)
response.json # 完整返回(不要只存 text 字段)
meta.txt # 字符数、token 数、分词器版本、上下文窗口设置
制作最小样本时,不要一上来就删到一句话。建议按段落裁剪:先保留提问相关的 2–3 段加一段无关内容,跑一次;如果异常复现,再逐段删减,直到异常消失。消失的那一段通常就是触发点。裁剪过程要记录每版的字符数与段落编号,否则后面无法回看。
在输出里定位内容被截断的位置
确认是否是长度超限导致内容根本没进模型。做法是在输入文本里给每个段落加可识别编号,例如 [P01]、[P02],然后在提问里要求模型“引用你读到的段落编号”。如果回答里出现了 [P01] 和 [P02],但 [P05] 之后再没出现,说明编号较大的部分很可能没有被模型看到,或者被放在了上下文之外。
从回答反查丢失段落时,可以问三类问题:让模型列出它读到的所有编号;让模型复述某个编号段的首句;让模型指出某编号段是否包含某个关键词。三类问题交叉看,比只问一次更可靠。注意模型也可能因为指令遵循问题而漏答,所以这一步只能作为线索,不能单独下结论。
下面是一个通用的 token 计数脚本骨架,用来判断输入是否接近上下文上限。替换成你所用分词器的实际接口即可,比如 Hugging Face 的 AutoTokenizer 或推理框架自带的计数接口:
from pathlib import Path
def count_tokens(text: str, tokenizer) -> int:
# tokenizer 替换为你环境里的实际分词器实例
return len(tokenizer.encode(text, add_special_tokens=False))
def load_and_count(path: str, tokenizer) -> dict:
text = Path(path).read_text(encoding="utf-8")
return {
"chars": len(text),
"tokens": count_tokens(text, tokenizer),
}
if __name__ == "__main__":
import json, sys
# 这里按你的环境替换成真实 tokenizer
tokenizer = None
if tokenizer is None:
print("请先接入实际分词器再运行")
sys.exit(1)
result = load_and_count("sample_01/input.txt", tokenizer)
print(json.dumps(result, ensure_ascii=False, indent=2))
验证方式:先拿一段已知长度的中文文本跑一次,确认脚本输出与分词器直接调用结果一致,再对完整文档跑。如果字符数相近的两份中文文本 token 数差异明显,说明中文分词确实在吃 token,这时要按 token 数而不是字符数来判断是否接近窗口上限。
换中文对话模板对比同一问题的回答
排除模板与角色标记不匹配这条原因。不同微调版本的 Llama 3 对系统提示、角色标记和分隔符的写法有不同预期,用错模板时模型可能把系统提示当成用户输入的一部分,或者提前截断到某个特殊标记处。
模板结构通常包含三部分:角色标记(如 <|start_header_id|>user<|end_header_id|> 这类写法)、系统提示所在位置、以及消息之间的分隔符。如果你用的是推理框架,先确认框架有没有内置该模型的模板,不要手动拼字符串再喂给框架,否则容易多一层转义或标记。
对照运行时,同一份输入、同一条提问、同一组采样参数(temperature、top_p 保持一致),只改模板。建议至少跑两组:一组是你当前使用的模板,一组是推理框架内置或模型说明中给出的模板。把两次的完整返回都存下来,再做差异对比。差异对比表可以按下面几列记录:
- 模板名称与模板字节内容(或配置文件路径)
- 输入字符数与 token 数
- 回答是否引用了全部段落编号
- 回答是否出现角色错位、特殊标记泄漏
- 回答与提问的相关度(主观标注即可,不要编造分数)
如果两套模板下回答质量接近,模板基本可以排除;如果内置模板明显更稳,就固定用内置模板。注意模板改动可能影响 token 计数,因为标记本身也占 token,所以要重新统计。
缩短输入只保留关键段落做单变量验证
验证输入变短后回答是否恢复,从而收窄原因范围。如果缩短后回答变正常,说明问题更可能在长度或截断;如果缩短后仍然答非所问,那分词或模板之外可能还有提示词表述问题。
推荐用分段摘要再汇总的两步流程。第一步,把长文档按段落或小节切分,每段单独送进模型做摘要,控制每次输入在你能确认安全的长度内。第二步,把所有摘要拼成一个短输入,再针对固定提问让模型回答。这样既降低单次输入长度,也保留文档的关键信息。
每一步的输入长度都要记录:第一步记录每段的字符数与 token 数,第二步记录汇总文本的字符数与 token 数。两次结果对照表可以这样写:
- 方案 A:原始全量输入,记录 token 数与回答是否漏段
- 方案 B:分段摘要后汇总输入,记录 token 数与回答是否漏段
- 如果方案 B 正常而方案 A 异常,把方案 B 的汇总长度作为后续处理的上界参考
注意分段摘要本身可能丢细节,如果原问题依赖精确数字或原文措辞,摘要后回答正确不代表全量输入时也会正确。这一步只是用来分离原因,不是最终方案。
把可用的输入长度与模板写成处理流程
把结论固化成能重复使用的文档处理步骤,避免每次遇到长文档都从头试。建议按下面的顺序走:
- 用固定提问确认异常可复现,保存请求与返回。
- 用 token 计数脚本算出文档真实 token 数,与上下文窗口设置对比。
- 固定一套验证过的模板,记录模板字符串或配置文件路径。
- 按段落切分文档,切分长度不要贴着你观察到的上界走,留出提问和回答的余量。
- 相邻片段之间保留少量重叠,重叠长度以能覆盖跨段引用为准,具体值需结合你的文档结构确认。
- 如果采用摘要汇总,先分段摘要再合并,合并后再对固定提问做一次校验。
参数记录表建议包含:模型名与版本、分词器版本、上下文窗口配置、实际使用输入 token 数、切分长度、重叠长度、汇总方式、模板标识、采样参数。这张表放在项目里,换模型或换框架时可以快速对照。
回归复测的执行方法:把之前保存的样本目录重新跑一遍,重点看三件事——回答是否引用了全部段落编号、是否出现特殊标记泄漏、与提问的相关度是否与上次一致。如果换了分词器或模型版本,token 数需要重新统计,因为同样的中文文本在不同分词器下 token 数可能不同。复测结果只记录“通过/不通过”和具体现象,不需要编造量化指标。