跳至内容
流式输出与多轮对话

流式输出与多轮对话

生成长文本时,等模型全部算完再显示会让用户盯着空白屏幕发呆。流式输出(Streaming) 让 token 一边生成一边显示;多轮对话则是把历史消息追加进请求,让模型「记住」前面说的话。这篇把两者都讲清楚,附可直接运行的代码。

为什么要流式输出

非流式调用的问题:

  • 延迟不可接受:生成 2000 token 的回答,即使 50 token/s,也需要 40 秒才能返回第一个字。
  • 超时风险:HTTP 连接长时间无数据,网关或客户端可能主动断开。

流式的好处:首 token 延迟(Time-to-First-Token)只需 0.5~2 秒,用户立刻看到内容在流淌,体验与打字机相似。

流式调用

Claude SDK 提供流式上下文管理器,用 stream() 替换 create()

from anthropic import Anthropic

client = Anthropic()

with client.messages.stream(
    model="claude-opus-4-8",
    max_tokens=2048,
    system="你是一个简洁专业的技术助手,用中文回答。",
    messages=[{"role": "user", "content": "解释一下什么是事件驱动架构,举一个实际案例。"}],
) as stream:
    # 逐 token 打印,end="" 不换行,flush=True 立即刷新缓冲区
    for text in stream.text_stream:
        print(text, end="", flush=True)

# 流结束后获取完整 Message 对象(含 usage 统计)
message = stream.get_final_message()
print(f"\n\n[usage: {message.usage.input_tokens} in / {message.usage.output_tokens} out]")
.get_final_message() 在流结束后返回完整的 Message 对象,包含 usage(token 计费数据)和 stop_reason。不需要手动拼接 token。

多轮对话

Claude API 本身是无状态的——它不记得上一次调用说了什么。多轮对话的实现方法是:把历史消息追加进 messages 数组一起发送

from anthropic import Anthropic

client = Anthropic()

def chat():
    history = []
    system  = "你是一个有帮助的技术助手,用中文回答。"

    print("开始对话(输入 exit 退出)\n")
    while True:
        user_input = input("你:").strip()
        if user_input.lower() == "exit":
            break

        history.append({"role": "user", "content": user_input})

        with client.messages.stream(
            model="claude-opus-4-8",
            max_tokens=1024,
            system=system,
            messages=history,          # 每次都把完整历史发过去
        ) as stream:
            print("Claude:", end="", flush=True)
            full_reply = ""
            for text in stream.text_stream:
                print(text, end="", flush=True)
                full_reply += text
            print()

        # 把模型的回复追加进历史,供下一轮使用
        history.append({"role": "assistant", "content": full_reply})

chat()

上下文窗口管理

每轮都把完整历史发过去意味着:对话越长,每次请求的 token 越多,成本越高,最终还会超出模型的上下文窗口(Claude Opus 4.8 为 1M token)。

常见管理策略:

策略适用场景实现方式
滑动窗口只需记住近几轮只保留最后 N 条消息,超出时丢弃最早的
摘要压缩长会话且历史重要每隔 K 轮,把旧消息总结成一段文字,替换掉原消息
Token 预算控制精确控制成本client.messages.count_tokens() 估算,超出阈值时触发压缩

滑动窗口示例:

MAX_TURNS = 10  # 保留最近 10 轮(user+assistant 各算一条)

# 在追加之前检查
if len(history) > MAX_TURNS * 2:
    # 如果有 system 消息,保留之(通过 system 参数传的不在 history 里,无需处理)
    history = history[-(MAX_TURNS * 2):]

流式 + 工具调用

流式模式与工具调用可以结合。stream.text_stream 只会生成文本 token,工具调用事件通过其他事件类型触发。更简单的方式是用 .get_final_message() 在流结束后再检查 stop_reason

with client.messages.stream(model="claude-opus-4-8", max_tokens=1024,
                             tools=tools, messages=messages) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)

final = stream.get_final_message()

if final.stop_reason == "tool_use":
    # 处理工具调用(同非流式逻辑)
    ...

提示词缓存

system 提示很长(如带了大量文档)、且每轮对话都重复发送时,开启**提示词缓存(Prompt Caching)**可以大幅节省成本:

import anthropic

client = Anthropic()

# 在需要缓存的内容块末尾加 cache_control
response = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    system=[
        {
            "type": "text",
            "text": "你是技术助手。" + long_knowledge_base,  # 假设有长文档
            "cache_control": {"type": "ephemeral"},          # 标记为可缓存
        }
    ],
    messages=[{"role": "user", "content": user_question}],
    betas=["prompt-caching-2024-07-31"],
)

缓存命中时,被缓存部分的 input token 费用降至 10%(读取缓存比重新处理便宜约 90%)。适合 system prompt 超过 1K token 且请求频繁的场景。

一句话小结

流式 = 用 stream() 替换 create(),逐 token 打印;多轮 = 把历史消息完整追加进 messages 数组。两者组合是大多数对话产品的标准实现,加上窗口管理和提示词缓存,就能把成本和体验都控制好。

最后更新于