重构聊天工作台的流式渲染ROCOCO-WORKBENCH
模型 K3.1 Pro ▾
—▢✕
新 的 信 笺
Porte romaneN° 03
Galerie baroqued'après Guardi
le salon du code est ouvert
或 者 , 从 这 里 开 始
今日 · 10月26日 · 夜
流式回复每个 delta 都触发一次重排,长会话会卡。改成按帧批量提交,别让已完成的消息重复插入。
K I M IKimi正在落笔···
先看一眼现在的提交路径。
ReadFilesrc/renderer/stream.ts96 行运行中0.4s
Grep"flushFrame|commit\(" src/renderer3 处匹配运行中0.3s
ReadFiletests/stream.test.ts58 行运行中0.4s
找到了两处:结束时整条 push,没按 id 去重;每个 delta 都直接 commit,再重排一次。先把去重补上:
Permission需 要 你 的 许 可

Kimi想修改一个文件想执行一条命令

edit_filesrc/renderer/engine/adapter.ts+3−1
run_shelltrianon /package.json · build
@@ -61,4 +61,6 @@ finish(msg)
const done = this.store.byId(msg.id)
− this.store.push(msg)
+ if (done) return done.merge(msg)
+ this.store.push(msg)
+ this.seen.add(msg.id)
$ npm run buildcwd · ~/trianon

Raison流式结束时按 message id 去重,已完成的消息不再整条重复插入。改完 adapter 后做一次完整构建,确认没有类型错误。

已允许 · 编辑 adapter.ts+3−1Accordé · 16:42
StrReplaceFilesrc/renderer/engine/adapter.ts+3−1运行中0.3s
去重补上了。动手合批之前,有个取舍想听你的:
Une questionKIMI 想 问 你

流式渲染用哪种批处理?

1 · 2 · 3 选择
已回答 · 按帧批处理Réponse · 16:43
好,按帧批处理。改成先攒着,下一帧一次提交:
StrReplaceFilesrc/renderer/stream.ts+12−4运行中0.6s
Feuilletsrc/renderer/stream.ts+12−4校 样
@@ −12,7 +12,7 @@createStream()
12 const pending: Delta[] = []
13−let scheduled = false
13+let frame = 0
14
15 export function onDelta(d: Delta) {
16− store.commit(d)
17− view.reflow()
16+ pending.push(d)
17+ if (!frame) frame = requestAnimationFrame(flushFrame)
18 }
⋯ 18 行未改动
@@ −37,5 +37,13 @@onEnd()
37 export function onEnd(msg: Message) {
38 store.upsert(msg.id, msg)
39 view.reflow()
40 }
41−// TODO: 按帧合批
41+
42+/** 把这一帧攒下的 delta 一次提交 */
43+export function flushFrame() {
44+ frame = 0
45+ const batch = pending.splice(0)
46+ if (!batch.length) return
47+ store.commitMany(batch)
48+ view.reflow() // 一帧只重排一次
49+}
StrReplaceFiletests/stream.test.ts+5−3运行中0.4s
Feuillettests/stream.test.ts+5−3校 样
@@ −31,6 +31,8 @@describe('flushFrame')
31 it('每帧只提交一次', async () => {
32− stream.onDelta(d('a'))
33− stream.onDelta(d('b'))
32+ for (const t of 'abc') stream.onDelta(d(t))
33 await nextFrame()
35− expect(store.commit).toHaveBeenCalledTimes(2)
34+ expect(store.commitMany).toHaveBeenCalledTimes(1)
35 })
36+it('结束事件并入同一帧', async () => {
37+ stream.onDelta(d('a')); stream.onEnd(msg('m1'))
38+ await nextFrame(); expect(view.reflow).toHaveBeenCalledTimes(1) })
Shellnpm run typecheck运行中2.1s
$ npm run typecheck
> rococo-workbench@0.1.0 typecheck
> vue-tsc --noEmit
✓ 没有类型错误exit 0 · 2.1s
Shellnpm test运行中3.2s失败exit 1
$ npm test
✓ tests/transcript.test.ts (18)
✕ tests/stream.test.ts › 结束事件并入同一帧
expected reflow to be called 1 time, but got 2
Tests 1 failed | 23 passed (24)
有一条没过:结束事件在同一帧里又单独重排了一次。让它也走 flushFrame。
StrReplaceFilesrc/renderer/stream.ts+1−2运行中0.3s
Feuilletsrc/renderer/stream.ts+1−2校 样
@@ −37,4 +37,3 @@onEnd()
37 export function onEnd(msg: Message) {
38− store.upsert(msg.id, msg)
39− view.reflow()
38+ pending.push(final(msg)); flushFrame() // 并进同一帧
39 }
Shellnpm test运行中1.4s
$ npm test
✓ tests/transcript.test.ts (18)
✓ tests/stream.test.ts (6)
Tests 24 passed (24)exit 0 · 1.4s
好了。完成的消息按 id 合并,不再整条重复插入;delta 按帧合批,一帧只重排一次,结束事件也并进同一帧;测试改了 1 条、补了 1 条,24 条全部通过。
src/renderer/stream.ts改了 2 次+13−6
tests/stream.test.ts新增 1 条测试+5−3
src/renderer/engine/adapter.ts按 id 去重+3−1
撤销全部改动retouches · 16:48
10月25日 · 午后
流式回复每个 delta 都触发一次重排,长会话会卡。改成按帧批量提交,别让已完成的消息重复插入。
K I M I
看过 transcript.ts 了。每个 delta 都直接写进响应式数组,所以每个 token 都重排一次;结束时又整条 push,没按 id 去重。
read_filesrc/stores/transcript.ts0.4s
edit_filesrc/stores/transcript.ts0.6s
@@ -42,9 +42,18 @@
- onDelta(e) { this.stream.text += e.text }
+ onDelta(e) { pending.push(e.text); scheduleFlush() }
先缓冲,再在一帧里一次性提交:
function scheduleFlush() {
  if (frame) return   // 一帧只提交一次
  frame = requestAnimationFrame(() => { frame = 0; store.flush() })
}
好,顺便把去重也补上。
补上了:结束时按 message id 合并,已完成的消息不再整条重复插入;流式测试也加了一条。
Écrivez-moi说一句话⋯
+
需确认后执行 ▾
@ 文件
EnvoyerStopperPatience
Entrée 发送 ✦ Maj + Entrée 换行