更新于

Claude stop_sequences 高级用法:精准控制输出边界


在生产级 LLM 应用里,“模型说太多”往往比”说太少”更难处理。Claude 的 stop_sequences 参数正是解决这个问题的手术刀——它让你在模型触碰特定字符串时立即停止生成,既节省 token,又让下游解析变得可预测。本文从机制、场景到踩坑,全面拆解这个被低估的参数。

一、stop_sequences 是什么

stop_sequences 是 Anthropic Messages API 的一个请求参数,类型为字符串数组,最多可填 8192 字节(注意是字节而非字符数)。当模型生成的文本里出现数组中的任意一个字符串时,生成立即停止,该字符串本身不会出现在响应里

import anthropic

client = anthropic.Anthropic(
    base_url="https://api.yotradeapi.com",  # 中转地址
    api_key="YOUR_API_KEY",
)

response = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    stop_sequences=["</answer>"],
    messages=[{"role": "user", "content": "请用 <answer> 标签包裹你的答案"}],
)

print(response.stop_reason)   # "stop_sequence" 或 "end_turn" 或 "max_tokens"
print(response.content[0].text)

响应的 stop_reason 字段会告诉你是因为什么停下来的:

stop_reason含义
end_turn模型自然结束
stop_sequence命中了你设置的某个停止词
max_tokens触达 max_tokens 上限

如果你需要知道命中了哪个停止词,可以读 response.stop_sequence(仅在 stop_reason == "stop_sequence" 时非 null)。

二、为什么需要停止词

解析可靠性

最常见的场景:你让模型在特定标签内输出结构化内容,然后用字符串截取解析。

<json>
{"name": "Alice", "age": 30}
</json>
后面可能跟着一大段解释文字……

如果没有停止词,你要么正则匹配 </json> 后面的部分,要么写更复杂的解析逻辑。设置 stop_sequences=["</json>"] 之后,模型生成到 </json> 就停,你直接截取 <json> 之后的内容即可,且不用担心模型的”啰嗦尾巴”。

Token 成本控制

模型有时候在给完答案后还会补充”总结”、“注意事项”、“如有需要可以继续”等套话。这些内容对自动化流水线完全无用,却消耗真实 token。通过停止词提前截断,可以节省 10%–30% 的输出 token,对高频调用场景经济效益显著。

多轮对话边界划定

在手工构建的多轮对话模板(非用 SDK 自动管理的场景)中,停止词能防止模型在一次调用里”自问自答”多轮:设置 stop_sequences=["User:"] 后,模型输出中一旦出现 User: 就停止,避免幻觉角色扮演蔓延。

三、常用停止词模式

模式一:XML 结尾标签

stop_sequences=["</result>", "</answer>", "</code>"]

适合需要模型在特定标签内输出内容的场景。注意:开始标签<result>)不适合做停止词,因为你通常希望内容在开始标签后生成;停止词应该设在结尾标签

模式二:分隔符行

stop_sequences=["---", "===", "***"]

适合让模型生成文档”正文部分”,止于分隔线之前。常见于 Markdown 生成场景。

模式三:角色前缀(对话模板)

stop_sequences=["Human:", "User:", "Assistant:"]

在手工拼接的 prompt 里防止模型替你”写下一轮问题”。使用原生 Messages API 不需要这个(SDK 已自动处理),但在直接操作 prompt 字符串的老代码里很实用。

模式四:JSON 结尾

stop_sequences=["\n```", "```\n"]

让模型在 Markdown 代码块结束后停止,适合只需要代码本体的场景。

四、多停止词组合策略

可以同时设置多个停止词,模型命中任意一个就停止。这带来了”或”逻辑:

stop_sequences=["</success>", "</error>", "</timeout>"]

下游读 response.stop_sequence 来判断走哪条分支,相当于让模型通过”生成哪个标签”来表达决策结果——这是一个轻量的”模型路由”技巧,比解析 JSON 分支更省 token。

实际示例:让模型对查询做分类

prompt = """
你是一个意图分类器。用户说:"{user_input}"

如果是技术问题,直接输出答案后跟 </tech>
如果是商务咨询,直接输出答案后跟 </business>
如果无法处理,直接输出原因后跟 </unknown>
""".format(user_input=user_input)

response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=512,
    stop_sequences=["</tech>", "</business>", "</unknown>"],
    messages=[{"role": "user", "content": prompt}],
)

intent = response.stop_sequence  # "</tech>" / "</business>" / "</unknown>"
content = response.content[0].text

五、流式场景的注意事项

使用 stream=True(或 client.messages.stream())时,停止词的行为有一个细节需要特别注意:模型在流式传输时是以 token 为单位输出的,而停止词判断发生在服务端。这意味着:

  1. 你不会在流里收到停止词本身(和非流式一致)
  2. 流结束时,最后一个 message_delta 事件的 stop_reasonstop_sequence 字段会被填充
  3. 不要在客户端自己检测流内容来模拟停止词——这是错的,会有 race condition
with client.messages.stream(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    stop_sequences=["</done>"],
    messages=[{"role": "user", "content": "..."}],
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)

    final_message = stream.get_final_message()
    print(f"\nStop reason: {final_message.stop_reason}")
    print(f"Stop sequence: {final_message.stop_sequence}")

六、与 max_tokens 的协同

停止词和 max_tokens 是两套独立的截断机制,先触发哪个就以哪个为准。实践中的推荐配置:

  • 设一个稍大的 max_tokens 作为保底:防止模型因为某种原因不生成停止词而无限输出(计费保护)
  • 把停止词设为主截断逻辑:让停止词承担”正常情况下的截断”职责
  • 监控 stop_reason 的分布:如果生产中大量出现 max_tokens,说明停止词没有按预期命中,需要检查 prompt 设计
# 推荐配置模式
response = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=2048,            # 保底上限
    stop_sequences=["</end>"],  # 主截断
    messages=[...],
)

if response.stop_reason == "max_tokens":
    # 告警:停止词未命中
    logging.warning("Stop sequence missed, hit max_tokens instead")

七、常见陷阱与排查

陷阱一:停止词大小写不匹配

stop_sequences大小写敏感的精确字符串匹配。如果 prompt 让模型输出 </Result> 但停止词是 </result>,不会触发。

排查方法:在 prompt 里明确写出你期望的标签格式,保持大小写一致。

陷阱二:停止词内含转义字符

在 JSON 请求体里,停止词如果包含反斜杠、换行符等需要正确转义:

# 错误:Python 字符串里的 \n 会被解释为换行,不是两个字符 \n
stop_sequences=["Human:\n"]

# 正确:这就是字面上的 "Human:" 后跟一个换行符
stop_sequences=["Human:\n"]  # Python 字符串里这是对的
# 如果你想匹配字面的反斜杠+n,需要:
stop_sequences=["Human:\\n"]

陷阱三:停止词太短导致误触发

单个字符或极短字符串(如 ".""\n")容易在无意中触发。任何句尾的句号都会停止生成,造成截断过早。停止词最好有足够辨识度(3 字符以上),且在正常输出里不常见。

陷阱四:停止词出现在 few-shot 示例里

如果你的 few-shot 示例包含了停止词字符串,会导致模型在 prompt 段就”命中”停止词吗?

不会——停止词只检测模型输出的部分,不检测 prompt 本身(包括 few-shot 里的 assistant 轮次)。这是设计行为,不是 bug。

陷阱五:以为停止词会出现在响应里

再次强调:停止词不会出现在 response.content 里。如果你需要在解析结果里有完整标签,需要在代码里手动补上:

raw = response.content[0].text  # 没有 </answer>
full = raw + "</answer>"          # 手动补回

八、实战:结构化 JSON 提取流水线

把上面的知识整合成一个可以直接用于生产的模式:

def extract_json_from_claude(user_query: str) -> dict:
    prompt = f"""请分析以下查询,以 JSON 格式输出结果,格式如下:

<json>
{{
  "intent": "查询意图(一句话)",
  "entities": ["实体1", "实体2"],
  "confidence": 0.0-1.0
}}
</json>

查询:{user_query}"""

    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=512,
        stop_sequences=["</json>"],
        messages=[{"role": "user", "content": prompt}],
    )

    if response.stop_reason != "stop_sequence":
        raise ValueError(f"Unexpected stop: {response.stop_reason}")

    raw = response.content[0].text
    json_start = raw.find("<json>") + len("<json>")
    json_str = raw[json_start:].strip()

    return json.loads(json_str)

这个模式的优势:

  1. 停止词保证 JSON 内容不会被”解释性文字”污染
  2. stop_reason 校验提供了错误检测
  3. 不依赖正则,解析逻辑极简

九、相关阅读

想在中国网络环境下稳定调用 Claude API、按量付费无需信用卡?YoTradeApi 提供全系列 Claude 模型中转,支持 stop_sequences、流式输出等所有原生参数。