关于AI 停止序列的几点记录
让它停它不停,不让它多说它偏要多说半页。
接 AI API 做结构化输出,最常见翻车不是模型答错,而是答多了:JSON 前后夹客套话,json.loads 挂;代码补全后面跟一段「以上代码仅供参考」;命令协议里多一句「已为您生成命令」。模型有多说一点的习惯,流水线需要精准截断。
停止序列(stop sequences)就是在这个环节动手。下面几个真实场景和踩坑对比。
背景
先说几个真实场景。
场景一:生成结构化数据。我用 API 生成 JSON 对象,模型往往会在 JSON 前后加几句客套话,比如 “好的,这是您需要的 JSON 数据:” 这种。如果你的代码直接 json.loads(response.content),大概率会抛异常。
场景二:代码生成补全。你想让 AI 帮你补全函数,它往往会补完代码后再加几行注释或者说明。在某些编辑器插件里,这些多余内容会影响直接应用补全结果。
场景三:对话式命令生成。你设计了一个固定格式的命令协议,希望模型每次只返回命令本身,但它总要加一句 “我已经为您生成了命令,可以执行了” 之类的说明。
核心问题都一样:模型有"多说一点"的习惯,但你的程序需要"精准一点"。
当然,你可以用 Prompt Engineering 来约束,比如明确写上 “只返回 JSON,不要其他任何内容”。但这种方式不够可靠,尤其当你无法控制 Prompt 或者需要给模型足够自由度时,硬约束反而可能降低生成质量。
停止序列(stop sequences)就是为了解决这类问题设计的。
需求
什么是停止序列?
简单说,停止序列就是告诉模型:生成过程中如果遇到这些字符或字符串,就马上停下来,不要再继续往后生成了。
大多数主流 AI API 都支持这个参数,只是叫法可能不太一样:
- OpenAI API:
stop参数,接受字符串或字符串数组 - Anthropic Claude:
stop_sequences参数 - Azure OpenAI:继承 OpenAI 的
stop参数 - 部分国内 API:有的用
stop_words,有的用stop_sequences
参数本身很简单,难点在于怎么用才有效、不踩坑。
我的需求很简单:在各种场景下,让模型只输出我需要的核心内容,不要那些画蛇添足的解释、说明、前缀后缀。
实现
先从最简单的例子开始:让 AI 只返回 JSON,不要其他任何废话。
基础实现
from openai import OpenAI
client = OpenAI()
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "user", "content": "请生成一个用户对象,包含 name、email、age 三个字段"}
],
stop=["\n\n", "```"],
temperature=0.7
)
print(response.choices[0].message.content)
这里用了两个停止序列:
\n\n:遇到连续两个换行就停。大部分情况下,JSON 生成完后如果模型想继续说点什么,通常会先换行。```:遇到代码块结束符就停。防止模型把 JSON 包裹在代码块里。
但这种方式不太可靠,因为模型可能不会按你预期的方式换行。
更可靠的方式是结合 Prompt 和停止序列:
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "user",
"content": """请生成一个用户对象,包含 name、email、age 三个字段。
直接返回 JSON 对象,不要任何前缀、后缀、解释或说明。
格式如下:
{
"name": "...",
"email": "...",
"age": ...
}
现在开始生成:"""
}
],
stop=["\n", "}"],
temperature=0.7
)
这里 stop=["\n", "}"] 是个技巧:模型生成 JSON 时,最后一个 } 通常是最后一行,如果之后没有生成内容了,就不会触发停止;但如果模型想继续写什么,必然会换行(\n),这时候就会触发停止。
不过这也有问题:如果 JSON 本身包含换行怎么办?下面会说到。
JSON 场景的专门方案
对于 JSON 生成,最稳妥的方案是利用 JSON 的结构特点。一个有效的 JSON 对象必须以 { 开头、} 结尾,而且括号是成对出现的。
可以这样设计:
import json
def generate_json_strict(prompt: str) -> dict:
"""严格只返回 JSON 的生成函数"""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": "你是一个 JSON 生成器,只返回合法的 JSON 对象,不要任何其他内容。"
},
{
"role": "user",
"content": f"{prompt}\n\nJSON:"
}
],
stop=["}\n\n", "}\n#", "}\n//", "}\n *", "}\n-"],
temperature=0.3 # 降低温度提高稳定性
)
content = response.choices[0].message.content
# 确保以 } 结尾(防止停止序列触发不够精准)
if not content.rstrip().endswith("}"):
content = content.rstrip() + "}"
# 解析 JSON
return json.loads(content)
这里的 stop 数组包含了多种可能的"JSON 结束后"的模式:
}\n\n:JSON 后跟两个换行(最常见的情况)}\n#:JSON 后跟注释}\n//:JSON 后跟 C 风格注释}\n *:JSON 后跟 Markdown 列表}\n-:JSON 后跟无序列表
这种方式覆盖了大部分常见的"继续写"模式,但仍不是 100% 可靠。实际项目中,建议再加一层 JSON 解析异常捕获,失败时重试。
对话场景的停止序列
在对话式应用中,停止序列有另一种用法:防止模型过度展开。
比如你设计了一个问答机器人,希望每次回答不要太长,用户可以继续追问。可以设置这样的停止序列:
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "user", "content": user_question}
],
stop=["\n\n\n", "总结:", "另外,", "此外,"],
max_tokens=500
)
这里的逻辑是:
\n\n\n:如果模型连续三个换行,大概率是想开启新话题或者展开太多总结:、另外,、此外,:这些都是模型开始"超纲"的常见信号
结合 max_tokens=500,可以双重控制输出长度。
代码生成场景
代码生成时,停止序列要格外小心,因为你不想提前截断代码。
常见策略是:只在特定格式下使用停止序列。
# 单行代码补全
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "user", "content": "补全这行代码:result = calculate("}
],
stop=["\n", ")", ";"],
temperature=0.2
)
# 多行代码补全,但只补一个函数
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "user", "content": """补全这个函数:
def process_data(data):
\"\"\"处理输入数据\"\"\"
pass"""
}
],
stop=["\n\ndef ", "\nclass ", "\n\n\n"],
temperature=0.3
)
第二个例子里,stop=["\n\ndef ", "\nclass ", "\n\n\n"] 的意思是:如果模型开始定义新函数或新类,或者连续三个换行,就停下来。这样可以补完当前函数,但不会继续生成后面的代码。
踩坑
坑一:停止序列太长
最初我犯了个错误:把整个 JSON 结束符作为停止序列,比如 stop=["}\n\n```\n"]。
问题很明显:模型生成过程中可能不会完全按照你想象的字符顺序出现,导致停止序列永远不会触发。更糟糕的是,如果模型正好在中间某处匹配了部分内容,可能会出现意外的截断。
教训:停止序列要短而精准,最好控制在 1-3 个字符,或者最常见的关键词。
坑二:停止序列太激进
有一次我为了严格限制 JSON 输出,设置了 stop=["\n"],意思是遇到任何换行就停。
结果模型连 JSON 内部的换行都没法生成,返回的 JSON 变成了一行长字符串,虽然格式正确,但可读性极差,而且在某些后续处理场景下(比如展示给用户看)体验很差。
教训:要区分"内容内部的换行"和"内容结束后的换行",前者应该允许,后者才是停止信号。这需要结合具体内容类型设计。
坑三:不同模型行为不一致
同样的停止序列配置,在不同模型上效果可能完全不同。
比如 stop=["\n\n"] 在 gpt-4o-mini 上效果很好,但在某些第三方模型上可能无效,因为那个模型压根就不会在内容结束后连续换行。
解决方式有两个:要么针对不同模型使用不同的停止序列配置,要么使用更通用的停止序列(比如不依赖换行,而是依赖特定关键词)。
坑四:停止序列与 Prompt 冲突
有次我设计了一个 Prompt,让模型生成多段内容,每段用特定分隔符分开。但我同时设置了 stop=["\n\n"],结果模型在生成第一段后就停了,因为每段之间确实是两个换行。
这种冲突很难一眼看出来,只能在调试时逐步验证。
教训:设置停止序列前,先想清楚你期待的输出格式是什么,确保停止序列不会误伤正常内容。
坑五:多字节字符的边界问题
后来做过一个中文内容生成项目,要求在句号处停止。中文句号是 。,看着简单,但实际使用时发现:有时候模型在 。 之后还会生成一个空格才停,有时候在 。 之前就停了。
核心原因是:停止序列的匹配是在 token 级别进行的,而某些多字节字符可能会被切分到不同 token 里。
比如 。 在某些 tokenizer 里可能是一个完整 token,但在某些情况下可能与前面的字合并成一个 token。当你期望它在输出 。 后停止时,模型可能因为 token 边界原因,在 。 还没完整生成时就触发了停止逻辑,或者需要额外生成一个 token 才能让停止序列匹配上。
这个问题在英文里也存在,但中文因为编码和 token 切分的复杂性,会更明显。
这张图想说明:停止序列的检查是在生成每个 token 后进行的,如果多字节字符被分割到不同 token 中,就可能导致匹配时机不准确。
结果
为了验证停止序列的实际效果,我做了一个简单的对比测试。
测试任务:让模型生成 10 个用户对象的 JSON 数组。
方案一:不用停止序列,只在 Prompt 中约束
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "user",
"content": """生成 10 个用户对象的 JSON 数组。
每个对象包含 name、email、age 三个字段。
直接返回 JSON 数组,不要任何前缀、后缀、解释。"""
}
],
temperature=0.7
)
方案二:Prompt + 停止序列
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "user",
"content": """生成 10 个用户对象的 JSON 数组。
每个对象包含 name、email、age 三个字段。
直接返回 JSON 数组,不要任何前缀、后缀、解释。"""
}
],
stop=["\n\n", "```"],
temperature=0.7
)
重复测试 20 次,统计 JSON 解析成功率:
| 方案 | 成功次数 | 失败原因 |
|---|---|---|
| 无停止序列 | 14/20 | 6 次失败(前缀/后缀/注释) |
| 有停止序列 | 19/20 | 1 次失败(多行内容提前截断) |
可以看到,停止序列显著提升了可控性,但也不是 100% 完美——那一次失败是因为 JSON 数组本身包含换行,被 stop=["\n\n"] 误伤了。
这说明一个道理:停止序列是工具,不是万能钥匙。你需要根据具体场景权衡。
复杂场景的组合策略
在实际项目中,我发现单纯依赖停止序列不够可靠,需要组合多种策略。
策略一:Prompt + 停止序列 + 解析验证
def generate_structured_data(prompt: str, schema: dict, max_retries: int = 3) -> dict:
"""生成结构化数据,带重试机制"""
for attempt in range(max_retries):
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": f"""你是一个结构化数据生成器。
必须严格返回符合以下 JSON Schema 的对象,不要任何前缀、后缀、解释。
JSON Schema:
{json.dumps(schema, ensure_ascii=False, indent=2)}"""
},
{"role": "user", "content": prompt}
],
stop=["\n\n", "```", "}", "】"],
temperature=0.3
)
content = response.choices[0].message.content
# 尝试解析
try:
result = json.loads(content)
# 验证 schema(可以用 jsonschema 库)
# validate(instance=result, schema=schema)
return result
except json.JSONDecodeError as e:
if attempt < max_retries - 1:
print(f"第 {attempt + 1} 次失败,重试中... 错误:{e}")
continue
else:
raise RuntimeError(f"生成失败,已重试 {max_retries} 次") from e
这个策略的核心是:不指望一次成功,而是把停止序列当作"提高成功率的工具",失败时重试。
策略二:前后处理结合
如果对格式要求极高,可以采用"生成后修剪"的方式:
def generate_json_with_cleanup(prompt: str) -> dict:
"""生成 JSON 并自动清理前缀后缀"""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "user", "content": prompt}
],
temperature=0.7
)
content = response.choices[0].message.content
# 找到第一个 { 和最后一个 }
start = content.find("{")
end = content.rfind("}")
if start == -1 or end == -1:
raise ValueError("无法找到有效的 JSON 对象")
json_str = content[start:end + 1]
return json.loads(json_str)
这种方式不需要停止序列,而是依赖 JSON 的结构特点。缺点是如果模型生成的内容里包含多个 {},可能会出错。
策略三:分阶段生成
对于非常复杂的输出,可以分阶段生成,每个阶段只要求输出一小部分:
# 第一阶段:生成结构
structure_response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "user",
"content": "我要生成一个包含用户信息、订单信息、商品信息的复杂 JSON,你先告诉我应该有哪些字段和结构?"
}
],
stop=["\n\n"],
temperature=0.3
)
# 第二阶段:按结构生成内容
# ...
这种方式可控性最强,但需要多次 API 调用,成本较高。
生成流程对比
用一张图来总结一下不同策略的生成流程和可控性:
可以看到,每种策略都有自己的优缺点,关键是根据场景选择。
总结和推荐
折腾了一圈,我总结出一些实践经验:
推荐配置
JSON 生成:
stop=["\n\n", "```", "}\n\n"]
单行文本:
stop=["\n", "。", "!", "?", "。", "!", "?"]
多段落文本(限制长度):
stop=["\n\n\n", "总结:", "另外,", "此外,"]
代码补全:
stop=["\n\ndef ", "\nclass ", "\n\n\n"]
最佳实践
- 停止序列要短而精准,太长容易失效,太宽泛容易误伤
- 结合 Prompt 使用,不要全靠停止序列
- 根据内容类型调整,JSON、代码、对话需要不同的策略
- 加一层解析验证和重试机制,提高可靠性
- 先小规模测试,验证稳定后再上生产
不推荐做法
- 用太复杂的正则表达式作为停止序列(大部分 API 不支持)
- 把停止序列当作"缩短输出长度"的主要手段(应该用 max_tokens)
- 在不同模型上使用完全相同的停止序列配置
- 依赖停止序列处理复杂结构(应该分阶段生成)
结语
写到这里,突然意识到这件事有点哲学意味:我们在教 AI 说话,但同时也在教它闭嘴。
停止序列本质上是一种边界设定——告诉模型"到这里就够了"。这种设定很有必要,因为没有边界的生成会走向不可控,不仅浪费 token,还可能在后续处理中引发各种问题。
但另一方面,如果边界设得太死,又可能限制模型的表现力。就像对话中,你既要让话题不跑偏,又要留一些自由空间让思想流动起来。
技术问题往往也是这种平衡的艺术。停止序列不是万能的,但它给了我们一种工具,可以在"自由生成"和"可控输出"之间找到一个平衡点。
至于这个平衡点在哪里,还得看具体场景和你的容忍度。毕竟,有时候多一点废话也无妨,而有些时候,多一个字符都是灾难。
这种判断,可能还是需要人来定。
版权声明: 本文首发于 指尖魔法屋-关于AI 停止序列的几点记录(https://blog.thinkmoon.cn/post/355-ai-stop-sequence-infinite-controllable-practice/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。