feat: Agent 流式输出匀速打字机泵(回答逐字顺滑流出) - #273
Merged
Merged
Conversation
网络按块到达时回答一坨坨蹦出加停顿。新增 AgentStreamingDisplayPump: 真实文本照常累积(latestAgentStreamingText 不动),显示端以 33ms 一拍追赶——落后 ≤4 字每拍吐 1 字(摊平 token 突发),落后多时每拍 吐 1/3(几何加速),完成/中断/新请求立即停泵,显示由完成路径的 finishStreamingMarkdown(全文) 接管,无泵延迟。 - WorkspaceStore 仅做生命周期接线(声明 + 3 处清理 + 发布点替换); 33ms 门与可视化块更新保持事件驱动不变; - agentStreaming.text 为纯显示通道,落盘/完成判定/中断/复制不受影响; 聊天不可见时泵暂停,重新可见后经追赶路径快速补齐; - 首帧语义保持:泵吐出首字前 agentStreaming.text 为空,思考指示器 不提前消失;start() 同步走一拍,首字延迟与旧行为一致。 WorkspaceStore.swift 为冻结文件,净增 11 行接线,PR 打 growth-exempt 豁免(理由见 PR 描述)。
原先「落后≤4 字吐 1 字、落后多吐 1/3」在落后量越过阈值时步长在 1 与 2-4 字之间跳变,节奏感有波动。改为连续速率律 + 小数累积: - 每拍 1 字起步,速率随落后量线性爬升(每落后 10 字 +1 字/拍), 上限 12 字/拍(约 360 字/秒,大积压快速泄放不瞬dump); - 小数速率(如 2.5 字/拍)由 pacingCredit 攒零补整,渲染成均匀 字符流;credit 上限 1,长时间贴近目标不会积攒突发; - 稳态验证:落后 10 字整(≈60 字/秒输入)恰好每拍 2 字完全匀速; 突发积压按 2,1,1,2… 或 2×n→1 平滑衰减回打字速度。
匀速泵解决了节奏,但每个字仍是瞬时出现,离散感仍在。新增 streaming-appearance 装饰插件,给编辑器侧补视觉过渡: - 新插入字符(≤24 字的文本块内插入)带 180ms 淡入,30Hz 的 离散插入被连续动画抹平,观感为连续流动; - 流式期间末尾跟随安静光标(2px,呼吸式闪烁),插入停止 400ms 或流式结束后消失;完成后 buffer 置空即重绘一次装饰, 不派发事务、不影响自检的事务/序列化计数; - 纯装饰:不加事务、不碰文档内容;spec 对象共享,未变化的 淡入区间跨 tick 复用 DOM,动画不重启; - 顶层整块插入(新段落)不套淡入/光标(内联装饰必须留在 文本块内,否则渲染为额外顶层元素,已被 WeiBeiWebEditorCheck 的块完整性断言捕获);prefers-reduced-motion 时全部关闭。 CSS 在 index.html(手维护源);编辑器产物已重建入库。
testConcurrentSnapshotRequestsShareOneFlight 依赖 async let 子任务在 一次 Task.yield() 内启动——新工具链无此保证:两次 CI 失败分别为 「子任务均未启动(命令数 0)」与「第二个子任务晚于快照送达启动、 第二趟飞行无人喂而超时」。main 全量检查能通过说明是概率性抖动 (run 32412530544),但本分支连中两次。 改为:同步路径直测「两请求合并一趟飞行、一次送达双双唤醒」 (零调度依赖);新增异步包装的确定性用例——continuation 在子任务 首次挂起前同步注册,轮询命令出现即代表已挂上飞行,再送达断言。 与打字机泵改动无关的测试稳定性修复,为解除本 PR CI 门禁阻塞 一并提交,已在 PR 描述记录。
用户反馈「帧数感觉有点低」:淡入动画与逐拍插入突发在 WKWebView 同一条主线程上竞争——每拍 O(全文) 的规范化两遍、正文正则装饰 扫描把插入对齐的动画帧挤掉,眼睛看到掉帧。 - 编辑器:updateStreamingMarkdown 增加干净尾部快路径——新尾部 不含 $ < \\ ` \r 等规范化触发字符且与 raw 锚点重合时直接 splice,跳过全文 normalize 与 withFrontmatter 序列化;一旦某拍 改写了文本即清锚回全量,finish 强制全量对账; - 装饰:流式期间跳过五组正文正则扫描(完成渲染全量重建), mermaid/代码块装饰保持; - 淡入 180ms → 260ms:典型中文速率下约 8 字同时溶解, 33ms 到达间隙被连续淡入彻底桥接; - 泵:节拍改为绝对 deadline 锚定,消除 MainActor 拥塞下的累计漂移。
taekchef
marked this pull request as ready for review
August 20, 2026 21:16
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
问题
回答随网络节奏一块一块蹦出:一次来一大坨就整坨出现,网络停一下画面就僵住,不是真正的逐字流式。
方案
真实文本照常累积(
latestAgentStreamingText及落盘/完成/中断/复制路径完全不动);新增AgentStreamingDisplayPump只给显示端(agentStreaming.text)配速:finishStreamingMarkdown(全文)接管,无泵延迟;agentStreaming.text为空,思考指示器不提前消失;start()同步走一拍,首字延迟与旧行为一致。改动
Sources/WeiBei/Stores/AgentStreamingDisplayPump.swift(泵本体,~90 行,节奏参数为命名常量);WorkspaceStore.swift仅生命周期接线:泵声明 + 发布点替换(agentStreaming.text = text→pump.start())+ 3 处清理挂stopAndReset();33ms 门与可视化块更新保持事件驱动不变;Tests/WeiBeiSafetyTests/AgentStreamingDisplayPumpTests.swift(7 个确定性用例)。共享核心文件占用
Sources/WeiBei/Stores/WorkspaceStore.swiftWorkspaceStore.swift为零增长冻结文件,本次净增 12 行接线(逻辑全部在新文件),使用 growth-exempt 豁免:[growth-exempt: 匀速打字机泵对 WorkspaceStore 的 12 行生命周期接线(1 处声明 + 3 处清理 + 1 处发布点替换),全部逻辑位于新文件 AgentStreamingDisplayPump.swift,无可避免地需要触碰请求生命周期点]验证
swift build✅swift run WeiBeiSelfCheck✅ /swift run WeiBeiDev selfcheck-assertions✅ /nslog-scan✅swift run WeiBeiWebEditorCheck✅(编辑器/JS 侧零改动回归,流式前缀推送无感)未做
追加:流式视觉平滑(dbedcde)与 CI 阻塞修复(dd70e51)
streaming-appearance.ts:匀速泵解决节奏后,字符仍瞬时出现(「吐字感」)。新增编辑器装饰插件:新插入字符 180ms 淡入 + 末尾呼吸光标(插入停止 400ms 消失;完成后 buffer 置空即重绘装饰,不派发事务)。纯装饰、不加事务;顶层整块插入不套装饰(已被块完整性断言约束);prefers-reduced-motion关闭。编辑器产物随提交重建。NoteEditingSessionTests.testConcurrentSnapshotRequestsShareOneFlight依赖 async let 子任务在一次Task.yield()内启动,新工具链无此保证——本分支两次 CI 失败(命令数 0 / 第二趟飞行超时),main 全量检查通过(run 32412530544)证实为概率性抖动。已确定性重写(同步直测合并 + continuation 注册先于挂起的轮询等待)。