第 9 步 · TUI 与发布 —— 流式输出 / 终端颜色 / 全局命令

让 Agent 有"打字机效果",从脚本变成可全局调用的终端工具

配套可运行项目:f:\myagent\task9 | 对比基准:task 8

9

TUI 与发布:流式输出 · 终端颜色 · 全局命令

一句话:前 8 步它是个"能跑的脚本",这一步让它长成"像样的终端产品"。

为什么流式是体验的分水岭? task 8 之前是"等完整答案一次返回"——模型想 10 秒,你盯着空白终端干等 10 秒,然后一整段文字"啪"地出现。真实产品(ChatGPT)是"打字机效果":模型生成一个字、服务器推一个字、终端立即显示一个字。心智转变:对用户,流式把"等待"变成"看得见进展";对开发,首字延迟(TTFT)是衡量体验的关键指标。同时补上两件"产品化"的事:按角色上色(廉价的信息分层)和全局命令(npm link 后任何目录都能启动)。

9.1 · 本步改了什么

文件变化程度
src/ui.ts新增:ANSI 颜色函数(green/yellow/red/cyan)+ isTTY 检测新增文件
src/llm.ts新增 parseSSEChunk(SSE 解析 + 半行累积缓冲)、makeRequestStreamstream:true + 逐块读取 + onChunk 回调);askLLM 加可选参数 onChunk(传了走流式);超时放宽到 60s;非流式 makeRequest 保留给工具调用核心改造
src/agent.ts最终答案那一步改用流式(process.stdout.write 增量打印 + 回填历史);所有输出按角色上色;工具循环仍非流式改造
src/main.ts提示符/命令输出/告别语上色;try/catch 接住 runAgent 抛出的错误(task 8 埋的坑在此补上);/help 补充 /clear改造
package.json新增 "bin": {"my-agent": "src/main.ts"}——npm link 后全局可调用小改
config.ts / types.ts / skills.ts / tools.ts / history.ts沿用 task 8,一字未改无变化

9.2 · 新增:ui.ts(终端颜色)

四个颜色函数 + 一个 isTTY 检测。颜色只在"真终端"里开——管道重定向到文件时,ANSI 码会变成肉眼可见的乱码。

src/ui.ts · task 9(新增)

9.3 · 核心改造:llm.ts 流式(升级前 vs 升级后)

右边是 task 9 新版(232 行,新增 SSE 解析 + 流式请求体),左边是 task 8 原版(155 行)。要点:流式只服务"纯文本最终答案",工具调用仍走非流式——工具参数是分片 JSON,流式解析复杂,教学版不展开,而"最终答案流式"已覆盖 99% 的体验提升。

src/llm.ts · task 9(升级后)
src/llm.ts · task 8(升级前) 默认折叠 · 可点开并排对比

      
SSE 的两个"坑",代码里都处理了半行边界:网络包可能把一个 data: {...} 行切成两半——所以 sseBuffer 把"还没出现换行符的尾巴"攒着,等下一个块补齐再解析;② 流结束标记data: [DONE] 没有 JSON,continue 跳过。这两处不处理,长回答偶尔会丢字或报错。

9.4 · agent.ts:最终答案流式 + 上色(升级前 vs 升级后)

右边是 task 9 新版(113 行),左边是 task 8 原版(107 行)。差异集中在:输出全部包上 yellow/cyan/green/red,最终答案那步改用 askLLM(history, currentTools, onChunk) 流式打印并用 process.stdout.write(而非 console.log,否则每段都换行、拼不出打字机效果)。

src/agent.ts · task 9(升级后)
src/agent.ts · task 8(升级前) 默认折叠 · 可点开并排对比

      

9.5 · main.ts:接住错误(升级前 vs 升级后)

task 8 埋的坑在这里补上:runAgent 现在被 try/catch 包住,请求失败只打印红色 [错误] 行,REPL 继续等下一轮输入。另外提示符、命令输出、告别语都上了色。

src/main.ts · task 9(升级后)
src/main.ts · task 8(升级前) 默认折叠 · 可点开并排对比

      
task 8 埋的坑,在此结清 task 8 让 askLLM 学会"抛得出来",task 9 的 try/catch 让 REPL 学会"接得住"——断网、错 key、超时,现在都只是打印一行红色 [错误],进程稳如泰山。这就是"错误处理的两半":抛得出来 + 接得住,缺一半都算没做完。

9.6 · package.json:全局命令(升级前 vs 升级后)

加一个 bin 字段,npm link 后系统就有了 my-agent 命令,任何目录都能启动。

package.json · task 9(升级后)
package.json · task 8(升级前) 默认折叠 · 可点开并排对比

      

9.7 · 运行验收

终端 · 验收命令
验收标准 ① 最终答案是"逐字蹦出来"的(打字机效果),不是等半天后一整块出现;② 不同角色不同色:轮次/工具=黄、当前工具=青、选择技能/工具结果/最终答案标题=绿、错误=红;③ 断网或错 key 时,[错误] 一行后 REPL 还在,还能继续输入。

9.8 · 与 pi 的对应

pi 的 packages/tui 把这一步的"颜色 + 打字机"做到了极致:它不是一行行读,而是全屏渲染(ALT 屏)、状态栏、多面板——但底层就是同一套 ANSI 转义序列 + 流式回调。你在 task 9 写的 isTTY 检测、onChunk 回调、stream:true,是 pi 的 tui-render.ts / streams.ts 的最小雏形。

字段来源速查表(task 9 新增:流式 / 颜色 / 发布)

名称含义来源
green / yellow / red / cyanANSI 颜色函数(非 TTY 时返回原文)ui.ts 导出
isTTY标准输出是否为真终端ui.ts 内部常量
stream: true请求体里开启流式响应llm.ts makeRequestStream
sseBufferSSE 半行累积缓冲llm.ts 模块级变量
parseSSEChunk(chunk, onDelta)按行解析 SSE,提取 delta.contentllm.ts 内部函数
makeRequestStream(...)流式请求体(reader.read + TextDecoder)llm.ts 内部函数
onChunkaskLLM 可选回调:每段增量调用一次llm.ts askLLM 参数
TIMEOUT_MS = 60_000流式场景超时放宽到 60sllm.ts 常量
process.stdout.write不换行输出(打字机效果的关键)agent.ts 最终答案分支
try/catchrunAgent错误只打印 [错误],REPL 不崩main.ts line 事件
"bin": {"my-agent": "src/main.ts"}全局命令入口(配合 npm linkpackage.json
← Home