你請本機模型做一張庫存表,文字一段段抵達,表格剛出現,連線卻斷了。按「重新產生」之後,原本選好的篩選條件也消失。這篇 OpenUI 本機串流教學,帶你拆開「畫面看得到」與「結果可以接受」,學會設計斷線、取消和重試的出口。
概念從零講起;動手段落需要已安裝的 Node.js,以及自己的終端機。先用三筆自建資料跑解析器,不必先下載模型;已有 Ollama 本機模型的人,再接生成端點。最後會得到一套可重跑的故障驗收方法,而不是只有一次漂亮的介面展示。
本文固定 OpenUI React Lang 0.4.1,核對日期為 2026 年 10 月 11 日。AlphaLab 跑了下文的四個純解析器測例,沒有啟動 Ollama 模型推論;因此不提供本機生成速度、模型成功率或完整瀏覽器復原的實測成績。
先說結論:OpenUI 本機串流復原要守住兩份畫面
可靠復原=串流預覽+已接受結果+獨立保存的使用者狀態。預覽像正在施工的房間;已接受結果像你仍能住的舊房間。新房間驗收之前,別先拆掉舊房間。
本篇採保守設計:生成中可以預覽,但先不開放預覽中的操作;出錯時顯示原因,仍保留上一份合格結果。使用者的篩選條件另存,重新產生只更換模型來源,不順便清掉人的選擇。這是本文提出的應用程式設計,不是宣稱 OpenUI 會替每個專案自動完成。

先認清五個零件:模型只是其中一站
Thesys 的 OpenUI讓模型輸出 OpenUI Lang,再由程式解析成註冊過的元件。它和名稱相近的 Open WebUI 是不同專案;本篇操作的是 thesysdev/openui。
① 本機模型負責寫介面描述;② 傳輸層把片段送過來;③ 解析器讀懂描述;④ Renderer把它交給你自己註冊的元件;⑤ 應用程式外殼保存狀態、顯示錯誤與控制重試。好比廚師、送餐車、驗單員、擺盤員與店長:送餐車到一半熄火,不能當成整桌菜已上齊。
官方 Renderer 文件說明,response接的是目前累積的完整文字,isStreaming表示仍在生成;onError回報解析、工具與元件錯誤。HTTP 斷線則要由你的 fetch/串流接收程式處理。兩種錯誤要記在不同欄位。
想先理解這種責任分工,可讀 Agent Harness 原理。而 json-render 安全元件教學補充元件白名單與執行門禁;這篇進一步處理本機串流何時結束,以及取消後哪些片段還能進入畫面。
① 固定版本與資料:先知道什麼叫正確
為什麼一出錯就難查?因為模型、套件和資料一起變了。先建立獨立練習資料夾,讓安裝只落在這個專案。下面是 macOS/Linux 的終端機步驟;不會修改你其他專案的依賴。
mkdir openui-recovery-lab
cd openui-recovery-lab
npm init -y
npm install --save-exact @openuidev/react-lang@0.4.1 react@19.2.4 react-dom@19.2.4 zod@4.6.5
保留 package-lock.json。本次 官方版本檔與 npm 發布包都對上 0.4.1;版本固定的用途,是讓你能把行為接回同一組程式,不代表它適合所有未來專案。
建立三筆固定資料:A 是原子筆,單價 8、庫存 12;B 是筆記本,單價 25、庫存 0;C 是橡皮擦,單價 5、庫存 20。全部顯示應有三列;「有庫存」應只剩 A、C。把這份資料留在程式裡,模型只回傳 ID,商品名稱、數字與篩選計算都由固定資料決定。
本例自訂一個 Inventory 元件(庫存面板),允許的 ID 只有 A、B、C。Zod 是描述資料形狀的工具;enum像核准名單。這讓輸出容易檢查,但「每個 ID 都合法」和「三筆全都出現」仍是兩種檢查。
② 先跑 OpenUI 本機串流的格式測例
把以下內容存成 parser-check.mjs,再執行 node parser-check.mjs。component: () => null刻意不畫介面,這一輪只驗解析結果;它不是完整 React 應用程式。官方 Parser 與 Library API提供這些接線方法。
import { defineComponent, createLibrary, createParser } from "@openuidev/react-lang";
import { z } from "zod";
const Inventory = defineComponent({
name: "Inventory",
description: "Read-only inventory IDs",
props: z.object({ ids: z.array(z.enum(["A", "B", "C"])) }),
component: () => null,
});
const library = createLibrary({ components: [Inventory], root: "Inventory" });
const cases = {
full: 'root = Inventory(["A","B","C"])\n',
half: 'root = Inventory(["A","B"',
unknown: 'root = Mystery(["A","B","C"])\n',
missing: 'root = Inventory(["A","B"])\n',
};
for (const [name, source] of Object.entries(cases)) {
const result = createParser(library.toJSONSchema()).parse(source);
console.log(name, JSON.stringify(result));
}
這次 AlphaLab 在上述套件版本,以四個固定字串執行:full有完整 root、meta.incomplete=false且 errors 為空;half已能解析出 A、B,但 partial=true與 meta.incomplete=true,errors 仍為空;unknown回傳空 root 與 unknown-component;missing只含 A、B,語法卻完整且 errors 為空。
最容易漏掉的是:錯誤清單空白,仍可能是半截或缺資料。這些結果只涵蓋四個純解析器字串,不等於模型或瀏覽器已過關。完整驗收還要檢查 root、未完成旗標、未解引用,以及 A、B、C 是否各出現一次。
接到既有 React 專案時,把空元件換成自己寫的庫存表與篩選器;它從固定資料查 ID,將篩選條件保存在外殼。<Renderer library={library} response={累積文字} isStreaming={生成中} />負責顯示描述。這段接線省略了你專案的版面、型別、React 錯誤邊界與狀態儲存;官方現有介面整合方式示範 Renderer 的位置。
③ 接 Ollama:先驗端點,再交給模型生成
已有 Ollama 的人先執行 ollama --version、ollama list,記下版本、模型名稱與 digest。再用 curl http://127.0.0.1:11434/api/tags查看模型列表。請選已下載的本機模型;若選雲端模型,整條推論路線就不是純本機。
官方 OpenUI Lab將 Ollama Integration 標為 Community。它連到 作者的本機整合範例;作者記錄小模型可能輸出壞格式,但沒有提供本篇故障測例的量化成功率,因此不能據此估計你的模型會成功幾次。
在後端固定目的地為 http://127.0.0.1:11434/api/chat,送 model、messages與 stream:true。系統訊息由 library.prompt({toolCalls:false, bindings:false})產生,使用者訊息要求「輸出 root = Inventory,ID 為 A、B、C 各一次」。這兩個選項調整提示內容;執行權限仍由程式控制。本練習不接工具提供者,也不開放寫入操作。
Ollama Chat API的串流內容位於 message.content,正常完成訊息帶有 done:true。其 原生串流格式是 NDJSON,也就是一行一個 JSON;OpenAI 相容的 /v1/chat/completions則是另一種接線。本篇固定原生 API,不要拿 SSE 的 data:拆法來讀 NDJSON。
瀏覽器可以向你自己同來源的後端路由取流,後端再連 Ollama,並把取消訊號傳給上游。這樣可以把目的地與允許模型固定在服務端。這一段是接線規格,省略後端框架實作、登入驗證與並發限制;請先在自己控制的本機專案完成,再對外開放。
④ 復原狀態:斷線、取消、重試要各走一條路
收到一個網路片段,就能立即 JSON.parse嗎?先不要。網路切塊可能把一行 JSON 或中文字拆開。用 TextDecoderStream連續解碼,再把文字放進行緩衝區;只有遇到換行才取出一行解析,結束時處理最後一段非空尾行。若物件有 error 欄位,立刻轉成失敗。
每行解析後,把 message.content接到本輪來源,交給 Renderer。不要把整個 JSON 物件顯示成 OpenUI Lang,也別把只有這一塊的文字當完整 response。官方 Ollama 錯誤文件另說明串流途中會以 JSON 錯誤物件回報,HTTP 狀態可能早已是 200;只檢查開頭的 status 不夠。
外殼保存四件事:accepted是上一份合格來源,draft是本輪累積來源,filter是使用者篩選,generationId是本輪編號。下面是語言無關的狀態流程,不是可直接執行的 JavaScript;省略行緩衝器、畫面元件、逾時計時器與儲存介面。
開始生成:
本輪編號 = 增加 generationId
取消上一輪連線;draft = 空字串;status = 生成中
為本輪建立 AbortController
每次收到完整 JSON 行:
若本輪編號 != generationId:忽略這一行
若有 error:轉到失敗
將 message.content 接到 draft;更新唯讀預覽
若 done == true:記錄正常完成
讀流結束:
若本輪編號 != generationId:退出
若未收到 done,或超出生成預算:轉到失敗
完整解析 draft;檢查 root、incomplete、unresolved、errors
驗 A/B/C 各一次,且無工具呼叫或多餘語句
全部通過才 accepted = draft;filter 維持原值
取消/失敗:
只讓當前輪更新狀態;取消時增加 generationId 並 abort
隱藏不合格 draft;保留 accepted 與 filter;顯示原因
重新產生:
新編號、新 draft;不要接在舊半截文字後面
保留 accepted 與 filter,再走一次完整驗收
為什麼已取消,還要檢查編號?AbortController.abort()可以中止 fetch 與讀流,但應用程式內已排入的回呼也要有歸屬檢查。舊輪的 catch、finally、解析回呼都套用同一條規則,避免它們把新輪設成失敗或完成。逾時和輸出長度也設上限,例如由你先決定等待預算與最多來源長度,超出就走相同失敗出口。
模型重新生成與重新查資料也要分開。OpenUI 現行 API 提供 Renderer.QueryError與 Renderer.Retry處理查詢失敗;那個 retry 屬於 Query 流程。模型來源本身斷掉,則由你保存原請求、建立新輪並重新產生。不要讓一顆按鈕同時偷偷重做兩種工作。
⑤ 故障演練與延遲紀錄:把預期寫在測試前
先讓固定版庫存表通過「全部三列、有庫存兩列」,再接生成版。兩者使用同一份商品資料與同一個篩選函式;差別是生成版多了來源生成、傳輸、解析與驗收。完成條件不是「有表格」,而是資料正確、篩選正確,而且失敗後仍能找到上一份已接受結果。

先重播固定 full字串,按你選的切塊規則逐段送入;再分別演練:半截斷線,送 half 後關流但不送 done;未知元件,送 unknown;取消競態,取消後刻意延遲舊片段抵達;重試缺資料,送 missing,再送合格的新輪。未知元件的傳輸可以正常完成,來源仍應驗收失敗。
在舊版已接受、篩選為「有庫存」的起點做四題。每題都檢查:錯誤提示是否對上原因、舊版 A/C 是否仍在、篩選是否保留、遲到的舊輪是否被忽略。這是瀏覽器驗收清單;前面的純解析器結果不能替它們填上「通過」。第一次生成就失敗時,沒有舊版可留,應顯示固定資料的文字清單與重新產生入口。
要量延遲,先寫四個時間點:送出請求、第一段非空模型內容、第一個預覽元件、完整驗收後第一個可操作元件。後兩者用瀏覽器提交畫面後的量測鉤子,另保存截圖或操作證據;不要在解析回呼觸發時就宣告按鈕已能用。用同一個單調時鐘,例如 performance.now(),計算各階段相對送出時間的差。
這份紀錄再加版本、模型 digest、冷載入/暖跑、失敗原因、所有重試與最終結果。取消或失敗的「第一個可操作元件時間」填 NA,不要填零。累積樣本後才整理 P50/P95,並列成功與失敗數;不要把成功輪的速度當所有請求的速度。固定版也量同樣的操作完成點,才知道生成式介面是否真的替工作省時間。AI Evals 入門能幫你把這些條件整理成固定驗收題。
常見坑:提示詞、錯誤清單與狀態都不是全套門禁
第一個坑是把 toolCalls:false當安全隔離。它只是 prompt 選項;本例還要在驗收時拒絕 Query、Mutation 與額外語句,且不提供寫入 handler。商品 ID 的合法性、去重和完整性也由應用程式再查一次。
第二個坑是看到 onError([])就放行。半截與缺列的純解析器例子已說明為何不夠。第三個坑是重掛 Renderer 時只存模型文字:使用者狀態應由外殼保存;若採 OpenUI 表單狀態,可利用 onStateUpdate與 initialState做保存與還原,再按欄位名稱、型別和新版本的允許選項檢查。
第四個坑是重試順便重做外部動作。本例保持唯讀;之後若加入送出、刪除或付款,先把它們放在明確操作之後,並由後端用穩定操作 ID 去重。畫面重畫不應自動授權執行。最小 Harness 實作能幫你把停止條件和工具紀錄接回流程。
FAQ:OpenUI 本機串流的八個直接答案
1. 沒有本機模型,能先開始嗎?
可以。先跑固定字串的解析器測例;它驗格式與旗標,不驗模型生成品質。
2. OpenUI 和 Open WebUI 是同一套嗎?
不是同一專案。本篇是 Thesys 的 OpenUI Lang 與 React Renderer;接線前先核對 repository 名稱。
3. 小模型一定能穩定輸出嗎?
不一定。社群作者記錄格式問題,本篇沒有自己的模型成功率;先固定小任務、版本和驗收答案。
4. errors 為空,就能接受結果嗎?
不能只看它。本次 half 和 missing 的 errors 都為空,還要檢查未完成與資料完整性。
5. 按取消,就是撤銷已執行的工具嗎?
不是。本文取消生成與讀流,並拒收舊輪;若你另接外部操作,撤銷要依那個服務的動作契約處理。
6. 重新產生會接著舊字串寫嗎?
本設計不會。新輪從空來源開始,舊合格結果與篩選條件各自保留。
7. 官方 Query 的 Retry 能修好模型斷線嗎?
要分開處理。Query 的重試對準資料查詢;本篇模型斷流由外殼重建生成請求。
8. 首段很快,就代表整個介面很快嗎?
不代表。分開量首段、預覽與驗收後可操作時間;失敗、取消和重試都留下紀錄。
給新手的三個重點
先有標準答案,再讓模型生成;先分清預覽與已接受結果,再接重試;先把人的狀態留在外殼,再量可操作時間。這三件事做好,斷線就有可驗的出口。
接著閱讀
左右滑動查看更多推薦
下一步:今天先讓一個半截輸出失敗得清楚
先執行 node parser-check.mjs,找到 half 的 incomplete 旗標,再替自己的外殼寫下「保留舊版 A/C、保留篩選、顯示中斷、重試從空來源開始」四項驗收。記住:可靠復原=預覽+已接受結果+獨立使用者狀態。一個故障出口驗清楚,再增加下一個元件;更多學習路線可從 AlphaLab AI 專區或 AlphaLab 課程繼續。






