10.2 块流(Block Streaming)

生成模型:Claude Opus 4.6 (anthropic/claude-opus-4-6) Token 消耗:输入 ~500,000 tokens,输出 ~40,000 tokens(本章合计)


模型生成的文本是一个连续的 token 流,但消息通道(如 Telegram、Signal)需要的是离散的消息。将连续文本流切分为合适大小的消息块——这就是 块流(Block Streaming) 要解决的核心问题。

OpenClaw 的 EmbeddedBlockChunker 实现了一套精密的文本分块算法,它不是简单地按固定字符数切分,而是会寻找语义上合理的断点,并且能够安全地处理 Markdown 代码围栏。

10.2.1 EmbeddedBlockChunker 算法(src/agents/pi-embedded-block-chunker.ts

BlockReplyChunking 配置

// src/agents/pi-embedded-block-chunker.ts
export type BlockReplyChunking = {
  minChars: number;       // 最小块大小
  maxChars: number;       // 最大块大小
  breakPreference?: "paragraph" | "newline" | "sentence";
  flushOnParagraph?: boolean; // 遇到段落边界立即刷新
};

minCharsmaxChars 构成了一个水位线区间:文本在 minChars 以下时继续积累,超过 maxChars 时强制断开,在两者之间时寻找合适的断点。

核心类结构

export class EmbeddedBlockChunker {
  #buffer = "";                    // 文本缓冲区
  readonly #chunking: BlockReplyChunking; // 配置
  
  append(text: string) {           // 追加文本
    this.#buffer += text;
  }
  
  reset() {                        // 重置缓冲区
    this.#buffer = "";
  }
  
  hasBuffered(): boolean {         // 是否有未发送的内容
    return this.#buffer.length > 0;
  }
  
  drain(params: {                  // 排出缓冲区
    force: boolean;                // 是否强制(忽略 minChars)
    emit: (chunk: string) => void; // 发射回调
  }) { ... }
}

drain 方法的执行流程

10.2.2 低水位线 / 高水位线分块策略

分块策略基于经典的水位线(Watermark)模式:

  • 低于 minChars:不切分,继续积累文本

  • minChars ~ maxChars 之间:寻找最佳断点——从高到低依次尝试段落、换行、句子、空白

  • 超过 maxChars:强制在 maxChars 处切分,即使在代码块内部也要切(使用围栏重开机制)

这种设计的优势是:短消息不会被过早拆分(避免碎片化),长消息也不会无限积累(避免超时或通道限制)。

衍生解释——水位线(Watermark)模式

水位线模式源自流量控制领域。低水位线表示"开始行动"的阈值,高水位线表示"必须行动"的阈值。在网络编程中,TCP 发送缓冲区的低/高水位线控制着何时通知应用可以写入数据。OpenClaw 借用这一概念来控制文本块的大小。

10.2.3 断点偏好:段落 → 换行 → 句子 → 空白 → 硬断

#pickBreakIndex 方法按优先级依次尝试不同类型的断点:

关键点:每个断点候选都需要通过 isSafeFenceBreak 检查——确保断点不在 Markdown 代码围栏内部。这是因为在代码块中间断开会导致 Markdown 渲染异常。

段落急切刷新模式

flushOnParagraphtrue 时,每遇到一个段落边界(\n\n)就立即发送,不等待缓冲区填满:

10.2.4 代码围栏(Fenced Block)安全拆分:关闭 + 重开

这是块流算法中最精巧的部分。当文本超过 maxChars 但断点恰好在代码围栏内时,不能简单地在中间切断——这会破坏 Markdown 格式。解决方案是关闭当前围栏,然后在下一块重新打开

举例说明。假设原始文本为:

如果 maxChars 恰好在 print("Hello") 后面,分块器会生成:

块 1

块 2

围栏拆分信息来自 parseFenceSpansfindFenceSpanAt,它们解析 Markdown 中所有的围栏代码块并记录其位置、缩进和标记符:


本节小结

  1. EmbeddedBlockChunker 将连续的文本流切分为适合消息通道发送的离散块。

  2. 水位线策略(minChars/maxChars)避免了消息过碎或过大的问题。

  3. 断点优先级(段落 > 换行 > 句子 > 空白 > 硬断)确保文本在语义合理的位置断开。

  4. 围栏安全检查isSafeFenceBreak)防止在代码块内部断开,避免 Markdown 渲染异常。

  5. 围栏关闭 + 重开机制是块流算法的精华——在不得不切分代码块时,通过自动关闭和重开围栏保持 Markdown 的有效性。

Last updated