Streaming 组装
流式输出最难的地方,不是“怎么把 token 拼起来”,而是“什么时候该渲染、什么时候该先忍住”。
Agentdown 把这件事放进 assembler 里处理。
基础命令
ts
cmd.stream.open({
streamId: 'stream:answer',
slot: 'main',
assembler: 'markdown'
})
cmd.stream.delta('stream:answer', '我来为你查询天气')
cmd.stream.close('stream:answer')当前内置 assembler
createMarkdownAssembler()createPlainTextAssembler()
createPlainTextAssembler()
纯文本 assembler 的策略最直接:
open时创建一个draftblockdelta时不断追加contentclose时把它标记成stable
适合:
- 简单 token 流
- 结构稳定的普通文本
- 不需要 markdown 解析的输出
createMarkdownAssembler()
markdown assembler 会先维护一个尾部草稿:
open时插入一个draftmarkdown blockdelta时持续更新草稿内容- 只把“已经结构稳定的前缀”解析成 stable block,并插到当前 draft 之前
- 尚未稳定的尾巴继续保留为单个 draft
close时把已提交块标记为settled,并 finalize 剩余尾部
这样做的原因是:
- table 需要表头完整后再渲染
- fenced code block 需要结束 fence 后再稳定
- Mermaid、公式、复杂 HTML 也更适合在结构闭合后渲染
- 普通段落、列表、blockquote、setext heading 这类结构也会尽量在边界明确后再稳定提交
为什么需要 draft / stable / settled
这是为了让 UI 在“尽快响应”和“不要提前乱码”之间有一个更自然的平衡。
draftblock 适合尾部进行中内容stableblock 适合已经闭合、可安全展示的内容settledblock 适合已经最终完成、后续不应再变化的内容
当前 draft 还会根据尾部内容自动切换展示模式:
textpreviewhidden
这样列表、blockquote、表格表头、未闭合代码块等场景会更自然。
同时,markdown assembler 也会把尾部分析结果写进 draft block 的 data:
streamingDraftMode当前草稿推荐采用的展示模式streamingDraftKind当前尾部更像哪一类 markdown 结构,例如table、fence、htmlstreamingDraftStability当前尾部采用的稳定化策略,例如candidate-stable或close-stablestreamingDraftMultiline当前尾部是否已经跨越多行
这让 RunSurface、devtools、业务样式层都能读到“这段草稿为什么还在 draft”这层信息,而不是只能看到一段原始文本。
Bridge 的批量 flush 做什么
createBridge() 不会强制每来一个 token 就立刻触发一次 runtime 更新。
它可以把多条命令攒在一起,再统一 flush,减少渲染抖动。
自定义 assembler 适合什么场景
如果你的后端不是普通 markdown token,而是更结构化的片段流,例如:
- JSON patch
- 表格行流
- 代码解释器输出
- 富文本 AST 片段
那你可以自己实现 StreamAssembler,把它们变成更适合前端消费的 block 更新。