)
AI 应用后端【免费下载链接】botpressThe open-source hub to build deploy GPT/LLM Agents ⚡️项目地址https://gitcode.com/gh_mirrors/bo/botpress点击查看免费下载本指南以 Botpress 开源仓库中 LLMz 框架的官方示例 22_chat_streamingStreaming Chat — guided simulation 为蓝本完整讲解如何把一个 LLM Agent 的一切产物——逐 token 的消息体、LLM 生成的代码、工具调用过程、类型化退出与每轮统计——实时渲染到客户端。读完本文你将掌握Chat.onMessageDelta与onTrace两大核心钩子的正确用法理解 LLMz代码优先模式下链式工具调用的实现思路并能把同样的模式迁移到 WebSocket / SSE 前端或其他任意传输层。示例概览一个太空旅行社引导式模拟22_chat_streaming位于 packages/llmz/examples/22_chat_streaming是一个交互式的太空旅行社Space Travel AgencyAgent。它把执行过程中产生的一切信息实时渲染在终端上——这里的客户端是终端但注释明确说明同样的模式完全适用于 websocket、SSE 响应或任何其他传输层。示例完整演示了以下五类能力流式消息体Streamed messagesChat.onMessageDelta在 LLM 仍在生成时随每个 body 分片触发生成的代码Generated codeonTrace钩子收到携带 LLM 所写代码的llm_call_successtrace代码在运行前被渲染在方框中实时工具调用Live tool callstool_calltrace 在调用完成时以内联⚙ tool(input) → output (ms)形式写入会话记录链式工具Chained tools预订是两步流程——bookTrip只做预订processPayment必须支付该预订才能拿到booked退出所要求的确认 IDAgent 在单个生成代码块内完成两者外加checkAvailability的链式调用类型化退出Typed exitsbooked与cancelled携带 zui schemaresult.is(exit)将载荷收窄为类型化数据内建的ListenExit维持对话循环。运行前准备与启动方式环境要求流式渲染要求Cognitive v2beta客户端。示例源码 index.ts 中明确注释了这一点// Streaming requires the Cognitive v2 (beta) client. A regular Botpress // client also works, but messages are then delivered whole, not streamed. const client new Cognitive({ botId: process.env.BOTPRESS_BOT_ID!, token: process.env.BOTPRESS_TOKEN!, })普通 Botpress 客户端同样可以运行该示例但消息将以完整消息形式一次性交付而不是逐 token 流式到达。运行时在 runtime/execute.ts 中会检测客户端类型当传入的 client 是Cognitive或自定义模型客户端时直接使用否则会自动包装为Cognitive。启动命令示例目录的 package.json 提供了pnpm start即tsx start.ts。在packages/llmz/examples目录下执行pnpm start 22_chat_streaming启动器 start.ts 的逻辑是按名字匹配*_chat*/*_worker*目录22或22_chat_streaming均可命中然后通过tsx运行该示例的index.ts。它要求以下两个环境变量缺失时会直接报错退出BOTPRESS_BOT_IDBot 标识BOTPRESS_TOKENBotpress 访问令牌。可以在packages/llmz/examples/.env中配置start.ts 会通过dotenv加载或直接以环境变量方式注入。示例还用到了chalk终端着色与自定义的交互式按钮输入组件 utils/buttons.ts支持↑/↓选择 Agent 建议的快捷按钮、回车提交、退格编辑。核心机制一onMessageDelta—— 消息体的逐 token 流式渲染delta 的数据结构当使用流式 Cognitive 客户端时消息体■send块会以 token 为单位到达。Chat的onMessageDelta在每个分片被解析出的第一时间收到回调其载荷结构为{ id, component, props, delta, content }各字段含义与 src/chat.ts 中的MessageDelta类型定义一致字段含义id正在流式传输的消息标识在单次execute()调用内唯一component消息的组件名如message、buttonprops消息的 props在 body 开始流式传输时已是最终值delta本次新增的 body 文本分片content到目前为止累计的完整 body 文本含本分片示例中 index.ts 用id作为分组键实现打字机效果第一次遇到某个id时打印 Agent:前缀之后每收到一个分片就追加写入deltaonMessageDelta: (delta) { if (streaming ! delta.id) { streaming delta.id process.stdout.write(chalk.bold( Agent: )) } process.stdout.write(delta.delta) },delta 是临时性的restart 语义需要特别注意的是流式 delta 是**provisional临时**的。MessageDelta有两种变体src/chat.tsrestart: false普通文本分片restart: true仅重置的分片在替换文本到达前发出要求客户端清空该iterationId对应的临时消息。当 provider 重启、响应格式错误或传输失败时会发生且重启后该迭代的旧预览会被撤回。而完整消息与代码只有在响应有效且传输成功后才交付——这正是handler被称为权威交付authoritative delivery的原因。在 runtime/generate.ts 中预览回调preview是逐个await的且重启型 delta 的撤回失败会被视为终止性错误以保证替换文本不会在撤回失败后被错误交付。handler权威交付与无 body 组件的唯一通道handler在每条完整消息被解析完后调用一次是所有消息的最终交付点也是按钮这类无 body 组件唯一会被送达的通道。示例中的处理逻辑index.tshandler: async (component) { if (isComponent(component, DefaultComponents.Button)) { buttons.push(component.props.label) return } const text component.children .filter((child) typeof child string) .join() .trim() if (!text.length) return transcript.push({ role: assistant, content: text }) if (streaming) { // 消息体已被 onMessageDelta 实时打印——只需结束当前行 streaming null process.stdout.write(\n) } else { // 非流式客户端的兜底一次性打印整条消息 console.log(${chalk.bold( Agent:)} ${text}) } },这里体现了流式渲染的标准分工onMessageDelta负责实时感handler负责最终结果。运行时在 execute.ts 中将两者接线onSend回调调用ctx.chat.handler(component, messageMetadata)onSendDelta仅在ctx.chat.onMessageDelta存在时传递 delta。核心机制二onTrace—— 实时呈现生成代码与工具调用execute()接受onTrace钩子每次迭代中的新 trace 会以{ trace, iteration, controller }形式实时推送给调用方见 execute.ts 对iteration.traces.onPush的订阅。示例针对三种 trace 类型做了渲染1.code_generation_started模型开始写代码当模型刚打开一个■run块代码块时触发generate.ts。此时代码仍在生成中示例打印⏳ writing code…提示用户等待同时运行时还会预预热 VMonRunStart: () warmupVM()见 execute.ts。2.llm_call_success代码生成完成代码生成完成后产生该 trace其code字段携带 LLM 实际写出的代码。示例在代码运行之前将其渲染在方框中index.tsif (trace.type llm_call_success trace.code.trim().length) { console.log(chalk.dim( ┌─ generated code)) for (const line of trace.code.trim().split(\n)) { console.log(chalk.dim( │ ) chalk.magenta(line)) } console.log(chalk.dim( └─)) }这正是 LLMz 与其他 JSON tool-calling 方案的核心差异Agent 生成的是可执行代码块因此可以在一个代码块内编排多个工具调用详见下一节。3.tool_call工具调用的输入、输出与耗时每次工具调用完成后产生tool_calltrace。示例读取tool_name、input、output/error、started_at、ended_at并计算耗时index.tsif (trace.type tool_call) { const duration chalk.dim((${(trace.ended_at ?? trace.started_at) - trace.started_at}ms)) const call chalk.cyan(${trace.tool_name}(${trace.input undefined ? : compact(trace.input)})) if (trace.success) { console.log( ${chalk.dim(⚙)} ${call} ${chalk.dim(→)} ${chalk.green(compact(trace.output))} ${duration}) } else { console.log( ${chalk.dim(⚙)} ${call} ${chalk.dim(→)} ${chalk.red(compact(trace.error))} ${duration}) } }其中compact()将超长 JSON 截断到 80 字符保证终端可读性。context.ts中也会消费tool_calltracesrc/context.ts用于向模型反馈执行历史。核心机制三链式工具调用 —— 单个代码块内完成预订 → 支付示例通过两步预订强制要求 Agent 进行链式调用index.tsbookTrip(destination, date, travelerName)仅预订返回{ reservationId, totalUsd }并把预订记录存入内存 MapprocessPayment(reservationId)对预订支付返回{ confirmationId, amountUsd }RSV-前缀替换为SP-booked退出要求confirmationId来自processPayment。因此Agent 必须在一个生成的代码块内先调用bookTrip再把返回值传给processPayment最后带上confirmationId退出。README 中的样本代码块展示了完整链条const dates await checkAvailability({ destination: moon }) const reservation await bookTrip({ destination: moon, date: dates[0], travelerName: Sylvain Perron }) const payment await processPayment({ reservationId: reservation.reservationId }) return { ...payment, date: dates[0] }README 明确指出这种单块链式编排是 JSON tool-calling 需要多轮往返才能完成的事——JSON 方案每步都要发起一次新的 LLM 调用而 LLMz 的代码生成模式让 Agent 一次生成、一次执行整条依赖链。指令instructions中也对 Agent 做了强约束Booking is a two-step process: bookTrip only reserves; the reservation must then be paid with processPayment to get the confirmation id.index.ts。同时示例还演示了工具的定义方式每个Tool都带 zui 类型的input/outputschema 与handler如checkAvailability会校验目的地 id 并抛出可读错误见 index.ts。核心机制四类型化退出Typed Exits与对话循环控制定义带 schema 的退出booked与cancelled通过Exit定义携带 zuiZod 兼容schemaindex.tsconst booked new Exit({ name: booked, description: The trip was reserved AND paid. Requires the confirmation id returned by processPayment., schema: z.object({ confirmationId: z.string().describe(The confirmation id returned by processPayment (starts with SP-)), destination: z.string(), date: z.string(), travelerName: z.string(), amountUsd: z.number(), }), }) const cancelled new Exit({ name: cancelled, description: The user decided not to book a trip, schema: z.object({ reason: z.string() }), })Exit的 schema 会被转为 JSON Schema 存储并在执行结束时对退出载荷做运行时校验见 src/exit.ts 的构造与校验逻辑。result.is(exit)会把result.output收窄为对应 schema 推导出的类型。用result.is(exit)处理三种结束分支示例的对话循环index.ts每次调用execute()后按顺序检查if (result.is(booked)) { // result.output 由 exit schema 完全类型化 console.log(chalk.bold.green( Trip booked!)) console.log(chalk.green( Confirmation: ${result.output.confirmationId})) console.log(chalk.green( Destination: ${result.output.destination})) console.log(chalk.green( Launch date: ${result.output.date})) console.log(chalk.green( Traveler: ${result.output.travelerName})) console.log(chalk.green( Amount: $${result.output.amountUsd.toLocaleString(en-US)})) break } if (result.is(cancelled)) { console.log(chalk.bold.yellow( Conversation ended — no booking)) console.log(chalk.yellow( Reason: ${result.output.reason})) break } if (!result.is(ListenExit)) { console.log(chalk.red(✖ Execution ended unexpectedly: ${result.status})) break }内建的ListenExit是把回合交还用户的信号只要执行以ListenExit结束循环就调用交互式prompt()收集用户输入并追加到 transcript进入下一轮否则booked/cancelled/异常终止循环。result.is(exit)的类型守卫实现在 src/result.ts其本质是检查result.exit exit且状态为success。核心机制五每轮统计页脚Stats Footer每轮结束示例打印一行统计信息index.tsconst printStats (result: ExecutionResult) { const iteration result.iteration const llm iteration?.llm const tokens iteration?.tokens const generationMs llm ? llm.ended_at - llm.started_at : undefined const parts [ model ${llm?.model ?? n/a}, turns ${turns}, iterations ${result.iterations.length}, code generation ${generationMs ! undefined ? ${generationMs}ms : n/a}, tokens in ${tokens?.input.toLocaleString(en-US) ?? n/a}, tokens out ${tokens?.output.toLocaleString(en-US) ?? n/a}, ttft ${llm?.time_to_first_token ! undefined ? ${llm.time_to_first_token}ms : n/a}, ] console.log(chalk.dim( ─ ${parts.join( · )})) }数据来源result.iteration最后一次迭代result.ts 的get iteration()取context.iterations.at(-1)result.iteration.llm本次迭代的 LLM 调用信息含model、started_at、ended_atended_at - started_at即代码生成耗时与time_to_first_token首 token 延迟字段定义见 src/context.tsresult.iteration.tokens本次迭代的input/outputtoken 计数result.iterations.length累计迭代次数turns由示例本地计数。ExecutionResult.tokens还提供了跨迭代聚合的{ input, output, total }result.ts。README 中的样例行即该函数的真实输出─ model openai:gpt-5.2 · turns 2 · iterations 2 · code generation 1764ms · tokens in 3,360 · tokens out 109 · ttft 663ms完整样本轮次以下是 README 提供的示例运行轮次直观展示了流式正文 代码框 工具调用 统计页脚 退出横幅的全部要素 User: Book the Moon on the first available date. My name is Sylvain Perron. Agent: Got it — Moon trip for Sylvain Perron. Let me line up the earliest launch… ┌─ generated code │ const dates await checkAvailability({ destination: moon }) │ const reservation await bookTrip({ destination: moon, date: dates[0], travelerName: Sylvain Perron }) │ const payment await processPayment({ reservationId: reservation.reservationId }) │ return { ...payment, date: dates[0] } └─ ⚙ checkAvailability({destination:moon}) → [2026-09-14,…] (0ms) ⚙ bookTrip({destination:moon,…}) → {reservationId:RSV-MOO-3645,totalUsd:125000} (0ms) ⚙ processPayment({reservationId:RSV-MOO-3645}) → {confirmationId:SP-MOO-3645,…} (0ms) Agent: All set — youre booked for the Moon! Confirmation SP-MOO-3645. ─ model openai:gpt-5.2 · turns 2 · iterations 2 · code generation 1764ms · tokens in 3,360 · tokens out 109 · ttft 663ms Trip booked!注意轮次中的几处细节checkAvailability的输出被compact()截断为[2026-09-14,…]bookTrip的输入同样被截断三个工具调用依次完成后Agent 才输出最终确认消息——因为代码块执行完毕前后续消息的预览会被抑制见 generate.ts 的runCompleted逻辑。传输无关性从终端到 WebSocket / SSE 前端README 强调The same pattern works for any transport——流式渲染模式与具体传输层无关。本示例把客户端实现为终端用process.stdout.write/console.log输出迁移到前端时只需把onMessageDelta中的process.stdout.write(delta.delta)换成WebSocket 发送或SSE write把onTrace中渲染代码框与工具调用的console.log换成对应的前端消息协议把result.is(exit)的终端横幅换成前端 UI 状态如订单确认卡片。消息体的分片delta、组件类型component、props 与累计内容content都在 delta 载荷中显式给出前端无需自行拼装即可实现打字机效果与组件化渲染。handler作为权威交付通道负责最终一致性——即使某条流被撤回restart完整消息仍会以正确形态送达。关键源码索引关注点位置完整示例实现packages/llmz/examples/22_chat_streaming/index.ts示例说明文档packages/llmz/examples/22_chat_streaming/README.mdChat基类、MessageDelta类型packages/llmz/src/chat.tsExit定义与 schema 校验packages/llmz/src/exit.tsExecutionResult与is(exit)类型守卫packages/llmz/src/result.ts执行循环与钩子接线packages/llmz/src/runtime/execute.ts流式解析与 trace 生成packages/llmz/src/runtime/generate.ts示例启动器与环境变量检查packages/llmz/examples/start.ts交互式按钮输入组件packages/llmz/examples/utils/buttons.ts小结22_chat_streaming是一个一屏看尽 LLMz 执行全貌的教学示例onMessageDelta负责消息体的打字机式实时渲染handler保证权威交付与无 body 组件按钮的唯一通道onTrace把生成代码、工具调用输入输出与耗时全部透明化类型化Exit配合result.is()提供类型安全的多分支收尾统计页脚则从result.iteration.llm与result.iteration.tokens读出模型、耗时与 token 消耗。这套模式与传输层解耦可以原样迁移到 WebSocket / SSE 前端是构建所见即所得的流式 LLM Agent 界面的完整参考实现。赞分享AI 应用后端【免费下载链接】botpressThe open-source hub to build deploy GPT/LLM Agents ⚡️项目地址https://gitcode.com/gh_mirrors/bo/botpress点击查看免费下载相关推荐JSONVue开发者指南从源码编译到打包发布全流程JSONVue开发者指南从源码编译到打包发布全流程 JSONVue是一款专为基于Chromium的浏览器设计的JSON格式化与查看工具本指南将带你完成从源码用 Yeti Affix 组件拼接表单控件共享边框输入组单位、符号、按钮的实现与无障碍实践用 Yeti Affix 组件拼接表单控件共享边框输入组单位、符号、按钮的实现与无障碍实践 Affix 是 Yeti 中的一个 CSS first 组件AI 应用后端WinSW扩展开发指南如何编写自定义扩展DLLWinSW扩展开发指南如何编写自定义扩展DLL WinSW 是一款免费开源的工具可以把任意可执行文件包装成 Windows 服务来运行。当 WinSW 自带AI 应用后端上一篇wxauto5分钟构建Windows微信自动化机器人的终极指南下一篇如何用wxauto打造你的Windows微信自动化智能助手终极指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考