LLM stop token 设计完全指南:从训练机制到业务场景选词
很多开发者对 stop token 的认知停留在”API 里有个 stop 参数,填几个字符串就行”。这只是冰山一角。stop token 实际上分两个完全不同的层次:训练时烧进模型里的特殊 token(如 EOS/EOT),和推理时你在请求里临时指定的停止词。前者决定了模型”天生”什么时候觉得该收尾,后者是你在应用层加的一道”手动刹车”。不理解这两层的区别,选停止词的时候很容易踩坑——比如设置了一个停止词,结果模型压根不会生成到那个字符串,或者停止词和模型自身的结束逻辑打架。
本文从底层机制讲起,再给出针对不同业务场景的选词方法论。具体某家 API 的参数用法(比如 Claude 的 stop_sequences)可以参考 Claude stop_sequences 高级用法,本文聚焦更通用的设计原则。
一、两层 stop token:训练时 vs 推理时
训练时特殊 token:几乎所有现代 LLM 在预训练和指令微调阶段都会引入一批特殊 token,其中最关键的是”序列结束”标记,不同模型家族命名不同:
| 模型家族 | 结束标记 | 说明 |
|---|---|---|
| GPT 系列 | `< | endoftext |
| Claude 系列 | \n\nHuman:(早期)/ 内部机制 | 现代版本通过训练直接学会何时结束轮次,不完全依赖显式字符串 |
| Llama / 开源模型 | `< | eot_id |
这些 token 是模型在训练阶段”学会”的——模型看到大量”一段对话在这里结束”的样本,学习在合适的位置生成这个 token。推理时,模型生成到这个 token 就会自然停止,这是默认行为,不需要你在请求里额外配置。
推理时自定义停止词:这是你在 API 请求里传入的字符串数组(OpenAI 叫 stop,Anthropic 叫 stop_sequences)。它的工作原理是纯粹的字符串匹配——推理引擎每生成一个 token 就检查一次已生成文本是否包含你指定的字符串,命中就掐断。这一层和模型训练时学到的东西没有任何关系,纯粹是推理阶段的后处理规则。
理解这个区别的价值在于:如果你想让模型在某个语义节点停止(比如”生成完这一段分析就停”),单靠自定义停止词是做不到的,因为它只能匹配字面字符串,不能理解语义。这种情况该用 max_tokens 加输出格式约束(比如要求模型输出 JSON 并读到闭合括号后截断),而不是硬塞一个停止词。
二、为什么”手动刹车”经常不生效
一个常见的困惑:明明设置了停止词,模型却没有在预期的位置停下来。原因通常是下面几种:
- 模型没有生成出这个字符串:停止词是被动匹配的,如果模型的输出风格和你预期的不一样(比如你期望输出
###END###,但模型压根没有被 Prompt 引导去生成这个标记),停止词永远不会命中。 - 大小写或空格差异:停止词匹配通常是精确字符串匹配(部分 API 大小写敏感),模型输出的实际字符可能和你写的停止词有细微差异(多了个空格、换行符不一致)。
- 停止词出现在你不希望截断的位置:比如把
"}"设为停止词想截断 JSON,但 JSON 内容本身可能包含嵌套对象,第一个}出现在还没到顶层结束的位置,导致输出被提前腰斩。
排查思路:先把停止词去掉,观察模型的原始输出,确认它到底会不会自然生成出你期望匹配的字符串,再决定停止词怎么设计,而不是直接假设模型会”听话”生成。
三、结构化输出场景的选词原则
用 stop token 截断结构化输出(JSON、XML、自定义标签)是最常见的应用场景,几个原则:
- 优先用不会在正文内容中自然出现的标记,比如
<<<END>>>这种明显是”控制字符”而非自然语言的组合,比用"}""]"这类结构本身就会重复出现的符号更安全。 - 在 Prompt 里显式要求模型输出这个标记,不要指望模型”猜”到你要什么。比如:“请在 JSON 输出结束后另起一行输出
<<<END>>>”,配合停止词["<<<END>>>"]。 - 如果目标格式本身就是标准 JSON/XML,优先用结构化输出 API(如 OpenAI 的
response_format、Anthropic 的工具调用强制 JSON),比用停止词”手工”截断更可靠,这部分可以参考 结构化输出:JSON Mode 与 Function Calling 对比。停止词更适合用在结构化输出 API 覆盖不到的自定义格式上。
# 用自定义标记截断非标准格式输出的示例
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=2048,
stop_sequences=["<<<END>>>"],
messages=[{
"role": "user",
"content": "分析以下财报要点,输出后追加一行 <<<END>>> 标记结束:\n" + report_text
}],
)
四、多轮对话场景:防止模型”越权”生成用户轮次
早期的补全式(completion-style)模型没有严格的对话角色边界,容易出现模型不仅回答了当前问题,还继续”帮你”生成下一轮的用户提问,也就是常说的”模型自问自答”。这种场景下停止词的经典用法是拦截角色标记本身:
stop_sequences = ["\nHuman:", "\nUser:", "\n用户:"]
现代的 Chat Completions / Messages API 因为有显式的角色分隔(role: user / role: assistant),这类问题已经大幅减少,模型经过指令微调后天然知道”只生成 assistant 轮次”。但如果你在做本地部署的开源模型(尤其是没有做过充分对话微调的基座模型),或者用补全接口手工拼接对话历史,这种角色边界停止词仍然是必需的防线,值得在 Prompt 模板里预先埋好。
五、工具调用场景:不要和函数调用协议冲突
如果你的应用同时用了工具调用(function calling),要特别小心停止词是否会截断工具调用本身返回的结构化数据。工具调用的返回通常由 API 单独的字段承载(而不是纯文本里嵌入 JSON),所以理论上不会和你设置的文本停止词冲突。但如果你用的是”文本里模拟工具调用”的方式(比如自己解析模型输出的 <tool_call>...</tool_call> 标签,而不是用官方的 tool_use 机制),就要确保停止词不会先于工具调用标签闭合前触发。
推荐做法:优先用模型原生的工具调用机制(结构化返回,天然和停止词逻辑解耦),只有在模型不支持原生工具调用时才退回到文本标签方案,并且这种情况下把停止词设计和标签解析逻辑一起测试,覆盖标签内容包含特殊字符的边界情况。
六、流式场景下停止词的额外延迟
在流式(streaming)响应里,停止词的匹配是逐 token 增量检查的——每收到一个新 token,就把它拼接到已生成文本后检查是否命中停止词。这意味着如果你的停止词很长(比如一整句话),即使命中了,之前已经流式吐给客户端的部分内容也无法收回,用户已经看到了。
对于需要”精确截断、用户完全看不到多余内容”的场景(比如需要过滤掉模型输出的免责声明后缀),更稳妥的做法是不依赖流式实时截断,而是在服务端做完整缓冲后再统一处理输出,牺牲一点首字延迟换取输出的确定性,这个权衡在 LLM 流式响应的背压处理 里也有类似的讨论。
七、多语言场景的停止词陷阱
如果你的产品面向多语言用户,停止词的语义等价词需要覆盖多个语言版本,不能只写中文或只写英文:
# 反例:只覆盖中文,英文用户的输出无法正确截断
stop_sequences = ["结束回答"]
# 更稳妥:覆盖主要目标语言 + 用不依赖自然语言的控制符作为兜底
stop_sequences = ["结束回答", "END OF ANSWER", "<<<STOP>>>"]
更推荐的做法是尽量避免依赖自然语言词汇作为停止词,改用不会因语言环境变化的控制符(如 <<<STOP>>>),并在 Prompt 里明确指示模型无论使用什么语言回答,都固定输出这个控制符作为结束标记。
八、小结
Stop token 分训练时和推理时两层,前者是模型自带的默认行为,后者是你能控制的字符串匹配规则,两者不要混为一谈。选词时优先用不会在正文中自然出现的控制符,务必在 Prompt 里显式要求模型生成这个标记而不是被动等待匹配,结构化输出优先用官方的强制格式 API 而不是手工停止词。流式场景要意识到停止词是增量匹配的,无法收回已经吐出的内容;多语言场景要避免用自然语言词汇作为唯一的停止条件。
九、相关阅读
- Claude stop_sequences 高级用法:精准控制输出边界
- LLM finish_reason 各家差异处理指南
- 结构化输出:JSON Mode 与 Function Calling 对比
- LLM 流式响应的背压处理
- AI 功能 A/B 测试设计:Prompt、模型与体验的三层实验框架
如果你需要跨模型对比不同厂商的停止符行为和输出格式差异,用 YoTradeApi 中转可以用统一的接口调用多家模型,方便快速验证。