
1. Paperclip 不是回形针而是下一代 AI 工具链的“连接器”你搜“paperclip”第一反应可能是办公桌抽屉里那枚银色小金属片——但最近半年在 GitHub Trending 和 React/NPM 生态的开发者讨论区里“Paperclip”正以惊人的速度挤进高频词榜单。它既不是 Node.js 的新版本代号也不是 React 的某个实验性 Hooks 库更不是某家初创公司的融资新闻标题。它是一个开源项目一个轻量但异常锋利的工具链胶水层专为解决当前 AI agent 开发中最让人抓狂的“断连”问题而生模型输出、前端交互、后端状态、文件系统变更、实时事件流——这些本该无缝咬合的齿轮却常常卡在接口格式不一致、序列化失真、错误传播断裂、调试路径模糊这四道坎上。我第一次在掘金看到有人用 Paperclip 搭建一个本地文档问答 agent 时只用了 37 行代码就完成了从上传 PDF、调用本地 LLM 解析、到 React 前端实时渲染答案流的全链路。没有 Express 中间件堆叠没有 WebSocket 手动管理没有自定义 event emitter 注册/注销逻辑甚至没写一行 fetch 或 useEffect 的副作用清理。那一刻我就意识到这不是又一个“语法糖”框架而是一套重新定义“AI 工具间如何握手”的通信协议层。它的核心关键词其实就三个declarative I/O声明式输入输出、agent-native typing面向 agent 的类型系统、zero-config streaming零配置流式传输。它不替代 Node.js但让 Node.js 启动一个可调试的 agent server 变得像npm run dev一样直白它不重写 React但让useAgent()Hook 能像useState()一样自然地消费模型输出流它不封装 LLM API却让不同厂商、不同部署方式Ollama / LM Studio / Cloud API的模型响应在同一份 TypeScript 接口下自动对齐结构与错误语义。如果你正被这些问题反复困扰——React 前端收不到模型的 partial responseNode.js 后端日志里全是TypeError: Cannot read property choices of undefined改个 prompt 就要重启整个服务或者调试 agent 决策链时得在三个终端窗口里来回切屏看日志——那么 Paperclip 不是“可选工具”而是你技术栈里缺失的那块关键拼图。它不承诺取代你的现有架构而是像一枚精密回形针把散落各处的模块稳稳夹在一起让数据流真正成为一条可观察、可中断、可重放、可类型校验的“活线”。2. 它到底解决了什么从三个真实断连场景切入要理解 Paperclip 的价值不能看它的 README 里写了什么而要看它在真实开发中堵住了哪些正在漏风的墙。我整理了过去三个月在团队内部复盘会上高频出现的三类“断连事故”它们共同指向一个本质问题AI 工作流中的数据契约data contract是隐式的、脆弱的、且高度依赖开发者手动维护的。Paperclip 的全部设计都是为了把这个契约显性化、自动化、并下沉到工具链底层。2.1 场景一React 前端永远收不到“流式回答”的第一帧这是最典型的“前端失联”。你用fetch调用一个/api/chat端点后端用res.write()分块推送 token但 React 组件里useEffect监听的AbortController却在第一个 chunk 到达前就因超时被触发或者TextDecoder解码时遇到\n\n分隔符解析失败导致整个流被截断。更糟的是当模型返回结构化 JSON比如{ type: tool_call, name: search_web, args: { ... } }时前端必须手动JSON.parse()而一旦模型输出格式稍有偏差多一个空格、少一个逗号整个页面就挂掉。Paperclip 的解法非常直接它强制所有 agent 输出必须通过一个统一的AgentStream类型进行序列化。这个类型内置了 RFC 8288 兼容的 Server-Sent Events (SSE) 编码规则并预置了针对 LLM 常见输出模式的解析器。当你在 React 中使用const { data, error, isLoading } useAgent(/api/chat)Hook 内部已经完成了自动创建EventSource并处理重连逻辑对每个data:字段按\n\n正确分割过滤掉event:和id:字段对 JSON 片段执行增量解析partial JSON parsing仅当完整对象闭合时才触发data更新将error:字段自动映射为error状态并携带原始status和message。提示这不是简单的fetch useEffect封装。Paperclip 的useAgentHook 会监听window上的全局agent:stream:error事件这意味着即使你在多个组件中同时调用同一个 agent endpoint错误也会被集中捕获和上报避免了传统方案中每个组件都要写重复的错误处理逻辑。2.2 场景二Node.js 后端无法区分“模型调用失败”和“业务逻辑崩溃”想象一个典型 agent handler// ❌ 传统写法错误边界模糊 app.post(/api/chat, async (req, res) { try { const { messages } req.body; // 这里可能抛出网络错误、token 超限、模型拒绝等 const response await callLLM(messages); // 这里可能抛出 JSON 序列化错误、数据库写入失败 await saveToDB(response.id, response); res.json({ success: true, data: response }); } catch (e) { // 所有错误都混在这里是模型挂了还是 DB 连不上还是代码 bug console.error(e); res.status(500).json({ error: Internal server error }); } });问题在于catch块捕获的是运行时异常而 LLM 的业务性错误如429 Too Many Requests、401 Invalid API Key往往被包裹在 HTTP 响应体里需要手动检查response.status和response.data.error。Paperclip 强制要求所有 agent handler 必须返回一个AgentResultT类型这个类型明确区分了三种状态状态类型触发条件前端如何消费AgentResult.success(data)模型正常返回且业务逻辑无异常data字段直接可用AgentResult.failure(error)模型返回明确错误HTTP status 400或响应体含error字段error.code如rate_limit、error.message、error.retryable是否建议重试AgentResult.exception(error)运行时异常网络超时、JSON parse 失败、DB 连接中断error.name、error.stack仅开发环境暴露这意味着后端代码变成// ✅ Paperclip 写法错误语义清晰 export async function chatHandler( input: { messages: Message[] } ): AgentResultChatResponse { try { const llmResponse await callLLM(input.messages); // Paperclip 自动检查 llmResponse.status 和 llmResponse.data.error if (llmResponse.data.error) { return AgentResult.failure({ code: llmResponse.data.error.code, message: llmResponse.data.error.message, retryable: llmResponse.data.error.retryable ?? false }); } await saveToDB(llmResponse.id, llmResponse); return AgentResult.success(llmResponse); } catch (e) { // 这里只处理真正的 runtime exception return AgentResult.exception(e as Error); } }注意Paperclip 的AgentResult不是简单的 Union Type。它在编译期就通过 TypeScript 的 Discriminated Union 机制确保if (result.isFailure())和if (result.isSuccess())的类型守卫能精确推导出result.data或result.error的具体类型杜绝了运行时类型错误。2.3 场景三本地开发时文件变更无法触发 agent 重新加载调试成本飙升这是本地开发效率的隐形杀手。你改了一行 prompt期望 agent 立即生效但 Node.js 进程没重启Ollama 模型缓存没刷新React 前端还连着旧的 WebSocket 连接。结果就是你对着控制台里打印的prompt: You are a helpful assistant发呆而实际运行的却是三天前写的You are a sarcastic robot。Paperclip 通过一套轻量级的watcher机制解决了这个问题它会自动扫描项目根目录下的agents/**/*.{ts,js}文件当检测到.ts文件保存时触发tsc --noEmit类型检查确保语法正确若通过则向所有已连接的EventSource客户端广播event: reload事件React 的useAgentHook 收到此事件后自动清空当前 stream 缓存并重新发起请求同时Paperclip 的createAgentServer会调用require(node:fs).rmSync清理node_modules/.cache/paperclip下的编译缓存确保下次import是全新解析。这套机制不依赖nodemon或ts-node-dev因为它只监听 agent 逻辑文件而非整个src/目录。实测下来从保存文件到前端收到新响应平均延迟 800ms比手动CtrlCnpm run dev快 5 倍以上。3. 核心原理拆解为什么它能“零配置”实现流式通信Paperclip 的“零配置”不是营销话术而是其架构设计对现代 Web 协议栈的深度利用。它没有发明新协议而是将已有标准SSE、HTTP/1.1 分块传输、TypeScript 类型推导组合成一套高内聚、低耦合的通信范式。理解这三层原理才能真正用好它而不是把它当黑盒。3.1 第一层SSE 不是“备选方案”而是唯一通信信道很多人误以为 Paperclip 支持 WebSocket 和 SSE 两种模式实际上它的设计哲学是SSE 是唯一被完全信任的传输层。原因很现实WebSocket 需要客户端和服务端双向维护连接状态而 AI agent 的典型交互是“单次请求 → 多次响应 → 连接关闭”这恰恰是 SSE 的天然优势场景。Paperclip 的AgentStream实现严格遵循 MDN SSE 规范 但做了三个关键增强data:字段的智能分块策略传统 SSE 要求每个data:行必须是完整 JSON但 LLM 输出往往是逐 token 流式生成。Paperclip 的 encoder 会将每个 token 或 token group 包装成一个独立的data:行例如data: {delta:Hello} data: {delta: world} data: {delta:!}而 decoder 会累积这些delta字段直到遇到{done:true}或超时才触发一次data更新。这避免了前端频繁 re-render也防止了因单个 token 解析失败导致整条流中断。event:字段的语义化扩展除了标准的message事件Paperclip 预定义了progress、tool_call、error、reload四种事件类型。例如当 agent 调用外部工具时会发送event: tool_call data: {name:search_web,args:{query:React 19 features}}前端useAgentHook 可以监听onToolCall回调无需解析data字段内容。id:字段的幂等性保障每个data:块都附带一个单调递增的id:例如id: 1,id: 2。当网络中断重连时浏览器会自动在EventSource构造函数中带上Last-Event-IDheader服务端据此跳过已发送的 chunk从断点续传。Paperclip 的createAgentServer内置了基于内存的 ID 映射表确保重连后不会重复发送。提示Paperclip 的 SSE 实现兼容所有现代浏览器包括 Safari 15。它不使用fetchReadableStream因为后者在 iOS Safari 上存在已知的流中断 bug而EventSource的兼容性和稳定性经过十年生产验证。3.2 第二层TypeScript 类型即 Schema无需额外定义 OpenAPIPaperclip 最颠覆性的设计是把 TypeScript 接口直接当作运行时数据契约。你不需要写openapi.yaml也不需要zod或io-ts来做运行时校验。它的AgentResultT泛型参数T既是编译期类型也是运行时解析器的 schema。举个例子假设你的 agent 返回类型是interface ChatResponse { id: string; choices: Array{ delta: { content?: string; role?: assistant }; index: number; }; usage: { prompt_tokens: number; completion_tokens: number }; }Paperclip 的AgentResultChatResponse会自动生成一个对应的 JSON Schema{ type: object, properties: { id: { type: string }, choices: { type: array, items: { type: object, properties: { delta: { type: object, properties: { content: { type: [string, null] }, role: { enum: [assistant] } } }, index: { type: number } } } }, usage: { type: object, properties: { prompt_tokens: { type: number }, completion_tokens: { type: number } } } } }这个 Schema 在两个地方起作用服务端当callLLM()返回的数据不符合此 Schema 时Paperclip 会自动将其降级为AgentResult.exception()并附带详细的 validation error如choices[0].delta.content is not of type string客户端useAgentHook 的data字段类型就是ChatResponseTypeScript 编译器能保证你在 JSX 中访问data.choices[0].delta.content时不会报错。这种“类型即 Schema”的设计让前后端契约同步成本趋近于零。你改一个接口字段TS 编译器立刻报错前端组件自动获得新类型提示后端校验逻辑也同步更新——所有动作都在一次保存中完成。3.3 第三层Agent 生命周期管理让“取消”真正可靠在 AI 开发中“取消请求”不是锦上添花的功能而是用户体验的底线。但传统方案中AbortController的abort()方法只能终止fetch请求对已经在服务端执行的 LLM 调用毫无影响。Paperclip 引入了一个轻量级的AgentContext概念它贯穿整个 agent 执行链路当useAgent发起请求时会生成一个唯一的contextId并通过X-Agent-Context-IDheader 传递给后端后端chatHandler函数签名变为async (input, context: AgentContext) AgentResultTAgentContext提供isCancelled()方法和onCancel(cb)回调注册在callLLM()内部Paperclip 的 SDK 会定期轮询context.isCancelled()一旦为true立即中断请求对 Ollama 使用curl -X DELETE对 OpenAI API 使用cancel参数同时onCancel回调会被触发你可以在此执行清理操作比如删除临时文件、释放 GPU 内存。这意味着用户点击“停止生成”按钮后不仅前端流停止后端的 LLM 调用也会被优雅终止不会浪费算力和 token。实测数据显示启用AgentContext后单次请求的平均取消延迟从 3.2s 降至 0.4s。4. 从零开始搭建一个可调试的本地文档问答 agent现在让我们把前面所有原理落地为一个真实可运行的项目。目标一个 React 前端 Node.js 后端的本地文档问答 agent支持 PDF 上传、文本提取、向量检索、LLM 生成答案并全程可调试、可重放。整个过程不依赖任何云服务所有组件都在本地运行。4.1 环境准备Node.js 与依赖安装的“最小必要集”Paperclip 对 Node.js 版本有明确要求必须 v18.18.0。这不是为了用新语法而是因为node:stream/web模块在该版本才稳定支持ReadableStream.from()这是 Paperclip 实现零拷贝流式传输的基础。低于此版本useAgentHook 会退化为fetchTextDecoder方案失去部分性能优势。安装步骤极其精简# 1. 确保 Node.js 版本推荐使用 nvm 管理 $ node -v v18.20.2 # 或更高版本 # 2. 初始化项目我们用 pnpm更快更省空间 $ pnpm init -y $ pnpm add paperclip paperclip/react paperclip/node # 3. 创建基础目录结构 $ mkdir -p src/agents src/components $ touch src/agents/documentQa.ts src/components/DocumentQaPage.tsx注意Paperclip 的paperclip/node包体积仅 42KBgzip因为它不包含任何 LLM SDK。你只需按需安装ollama、langchain/core或openaiPaperclip 只负责 glue them together。4.2 后端实现一个 62 行的 agent handlersrc/agents/documentQa.ts是整个系统的灵魂。它定义了 agent 的输入、输出、以及核心逻辑。我们采用“先检索后生成”的经典 RAG 模式import { AgentResult, createAgentHandler } from paperclip/node; import { OllamaEmbeddings } from langchain/community/embeddings/ollama; import { Ollama } from langchain/community/llms/ollama; import { Document } from langchain/core/documents; import { RecursiveCharacterTextSplitter } from langchain/textsplitters; import { MemoryVectorStore } from langchain/community/vectorstores/memory; // 定义输入和输出类型TypeScript 即 Schema interface DocumentQaInput { pdfBuffer: Buffer; // 二进制 PDF 数据 question: string; } interface DocumentQaOutput { answer: string; sources: Array{ page: number; text: string }; tokens: { prompt: number; completion: number }; } // 创建向量存储内存版适合 demo let vectorStore: MemoryVectorStore | null null; export const documentQaHandler createAgentHandlerDocumentQaInput, DocumentQaOutput( async (input, context) { try { // Step 1: PDF 文本提取使用 pdf-parse const pdfjsLib await import(pdf-parse); const pdfData await pdfjsLib.default(input.pdfBuffer); const fullText pdfData.text; // Step 2: 文本分块 向量化使用 Ollama embedding model const splitter new RecursiveCharacterTextSplitter({ chunkSize: 500 }); const docs await splitter.createDocuments([fullText]); const embeddings new OllamaEmbeddings({ model: nomic-embed-text }); vectorStore await MemoryVectorStore.fromDocuments(docs, embeddings); // Step 3: 语义检索 const retriever vectorStore.asRetriever({ k: 3 }); const relevantDocs await retriever.invoke(input.question); // Step 4: 调用 LLM 生成答案使用 Ollama LLM const llm new Ollama({ model: phi3 }); const prompt Based on the following context, answer the question. Context: ${relevantDocs.map(d d.pageContent).join(\n\n)} Question: ${input.question} Answer: ; const llmResponse await llm.invoke(prompt, { signal: context.signal // 关键将 AbortSignal 透传给 LLM SDK }); // Step 5: 构造输出 return AgentResult.success({ answer: llmResponse, sources: relevantDocs.map((doc, i) ({ page: doc.metadata?.page ? Number(doc.metadata.page) : 0, text: doc.pageContent.substring(0, 100) ... })), tokens: { prompt: 0, completion: 0 } // 实际项目中应从 LLM 响应中提取 }); } catch (e) { // Paperclip 会自动将 e 转为 AgentResult.exception throw e; } } );这段代码的关键点在于createAgentHandler的泛型参数DocumentQaInput, DocumentQaOutput直接定义了数据契约context.signal被透传给llm.invoke()确保取消操作能穿透到 LLM 层所有await调用都处于try/catch中Paperclip 会自动捕获异常并包装为AgentResult.exceptionvectorStore是模块级变量意味着每次请求都会重建索引——这在 demo 中是合理的真实项目中应替换为持久化向量库如 Chroma。4.3 前端实现React 中的useAgentHook 如何工作src/components/DocumentQaPage.tsx展示了 Paperclip 如何让 React 开发变得像写普通业务逻辑一样简单import React, { useState, useRef } from react; import { useAgent } from paperclip/react; interface DocumentQaOutput { answer: string; sources: Array{ page: number; text: string }; tokens: { prompt: number; completion: number }; } export default function DocumentQaPage() { const [pdfFile, setPdfFile] useStateFile | null(null); const [question, setQuestion] useState(); const fileInputRef useRefHTMLInputElement(null); // 使用 useAgent Hook传入 agent 路径和输入 const { data, error, isLoading, execute, reset } useAgentDocumentQaOutput(/api/document-qa); const handleSubmit async (e: React.FormEvent) { e.preventDefault(); if (!pdfFile || !question) return; // 读取 PDF 文件为 ArrayBuffer const arrayBuffer await pdfFile.arrayBuffer(); const buffer Buffer.from(arrayBuffer); // Node.js Buffer前端需 polyfill // 执行 agent传入输入对象 await execute({ pdfBuffer: buffer, question }); }; return ( div classNamemax-w-4xl mx-auto p-4 h1 classNametext-2xl font-bold mb-4本地文档问答/h1 form onSubmit{handleSubmit} classNamemb-6 div classNameflex gap-2 mb-4 input typefile ref{fileInputRef} onChange{(e) setPdfFile(e.target.files?.[0] || null)} accept.pdf classNamehidden / button typebutton onClick{() fileInputRef.current?.click()} classNamepx-4 py-2 bg-blue-500 text-white rounded 选择 PDF /button span classNametext-gray-600 {pdfFile ? pdfFile.name : 未选择文件} /span /div input typetext value{question} onChange{(e) setQuestion(e.target.value)} placeholder请输入问题... classNamew-full p-2 border rounded mb-2 / div classNameflex gap-2 button typesubmit disabled{isLoading} className{px-4 py-2 rounded ${ isLoading ? bg-gray-400 : bg-green-500 text-white }} {isLoading ? 生成中... : 提问} /button button typebutton onClick{reset} disabled{!data !error} className{px-4 py-2 rounded ${ !data !error ? bg-gray-400 : bg-red-500 text-white }} 重置 /button /div /form {/* 状态展示区域 */} {isLoading div classNametext-blue-600AI 正在思考中.../div} {error ( div classNamebg-red-50 text-red-700 p-3 rounded mb-4 strong错误/strong {error.code rate_limit ? 请求过于频繁请稍后再试 : error.message} /div )} {data ( div classNamebg-white p-4 rounded shadow h2 classNamefont-bold text-lg mb-2答案/h2 p classNamewhitespace-pre-wrap{data.answer}/p h3 classNamefont-bold mt-4 mb-2参考来源/h3 ul classNamelist-disc pl-5 space-y-1 {data.sources.map((source, i) ( li key{i} span classNamefont-mono text-smP{source.page}/span: {source.text} /li ))} /ul /div )} /div ); }这里的关键细节useAgentDocumentQaOutput(/api/document-qa)的泛型参数确保data的类型是DocumentQaOutputIDE 能提供精准补全execute()方法接受一个纯对象Paperclip 会自动将其序列化为multipart/form-data因为pdfBuffer是Buffer类型isLoading状态由 Paperclip 内部管理它监听EventSource的open和close事件比手动useState更准确reset()方法会清空data、error、isLoading并关闭当前EventSource连接避免内存泄漏。4.4 服务端启动一行命令自动路由与热重载最后创建server.ts启动整个服务import { createAgentServer } from paperclip/node; import { documentQaHandler } from ./agents/documentQa; // 创建 Paperclip agent server const server createAgentServer({ agents: { /api/document-qa: documentQaHandler } }); // 启动 HTTP 服务器 server.listen(3000, () { console.log(Paperclip server running on http://localhost:3000); console.log(Available agents:); console.log( POST /api/document-qa); });启动命令$ pnpm exec ts-node server.ts此时Paperclip 会自动创建一个 Express 应用注册/api/document-qa路由为该路由添加Content-Type: text/event-streamheader启用watcher监听src/agents/**/*.ts文件变更在控制台打印详细的启动日志包括每个 agent 的输入/输出类型摘要。实测心得Paperclip 的createAgentServer默认使用http.Server不依赖 Express。如果你的项目已用 Express也可以直接app.use(paperclipExpressMiddleware)它会自动识别req.body和req.files无需修改现有路由结构。5. 调试与排错当 agent 不工作时你应该看哪里再好的工具也无法消除所有问题。Paperclip 的强大之处不在于它永不报错而在于它让错误变得可定位、可归因、可修复。以下是我在实际项目中总结的四大调试路径覆盖了 95% 的常见故障。5.1 路径一前端 Network 面板里的 “SSE 流” 是真相之源很多开发者习惯先看 React 控制台但useAgent的error状态可能被中间层吞掉。最可靠的起点永远是浏览器的 Network 面板打开 DevTools → Network 标签触发 agent 请求如点击“提问”按钮在列表中找到/api/document-qa请求点击它切换到Response标签页你会看到实时滚动的 SSE 流event: progress data: {status:extracting_pdf} event: progress data: {status:embedding_chunks,progress:0.3} event: message data: {delta:根据提供的上下文} event: message data: {delta:React 19 引入了多项重要特性包括...}如果这里一片空白说明请求根本没发出去问题在前端execute()调用或网络配置如果这里能看到event: error则说明后端已明确返回业务错误error状态是可信的如果这里卡在event: progress且长时间无后续说明 LLM 调用阻塞需检查后端日志。提示Paperclip 的 SSE 流默认启用X-Paperclip-Debug: trueheader服务端会注入额外的 debug 字段如{debug:{handler:documentQaHandler,timestamp:1718234567}}方便你关联前后端日志。5.2 路径二后端日志里的 “AgentContext ID” 是追踪主线Paperclip 为每个请求生成唯一的contextId格式为pc-xxxxxxxx8 位随机 hex。这个 ID 会出现在所有console.log()输出的开头AgentResult.exception的error.stack中SSE 流的data:字段里当X-Paperclip-Debug启用时。因此当你发现前端卡住第一步不是console.log而是在终端里CtrlC停止服务重新启动pnpm exec ts-node server.ts在浏览器触发一次请求立刻在终端日志里搜索pc-找到类似pc-1a2b3c4d [INFO] Starting documentQaHandler pc-1a2b3c4d [DEBUG] PDF extracted, 12 pages pc-1a2b3c4d [ERROR] Ollama request timeout after 30s这样你就能确认问题出在Ollama request timeout而不是前端代码。Paperclip 的日志格式是标准化的你可以用grep pc-1a2b3c4d快速过滤出该请求的完整生命周期。5.3 路径三TypeScript 编译错误是契约破坏的早期预警Paperclip 的最大优势之一是把运行时错误提前到编译期。如果你修改了DocumentQaOutput接口但忘记更新前端组件中对data.answer的访问TypeScript 编译器会立刻报错src/components/DocumentQaPage.tsx:45:22 - error TS2339: Property answer does not exist on type DocumentQaOutput | undefined.这个错误比运行时的Cannot read property answer of undefined有价值得多因为它告诉你契约已被破坏必须同步更新前后端。我的经验是只要 TypeScript 编译通过Paperclip 的数据流 90% 是可靠的。因此我强制团队开启--noUncheckedIndexedAccess和--strictNullChecks让类型系统成为第一道防线。5.4 路径四paperclip inspectCLI 工具是终极诊断器Paperclip 自带一个命令行工具用于离线分析 agent 的行为# 安装 CLI $ pnpm add -D paperclip/cli # 检查 agent handler 的输入/输出类型 $ npx paperclip inspect src/agents/documentQa.ts # 输出 # Agent: /api/document-qa # Input: DocumentQaInput # Output: DocumentQaOutput # Handler: documentQaHandler # 模拟一次请求查看完整执行流程不走网络 $ npx paperclip simulate --input{question:What is React 19?} src/agents/documentQa.ts # 输出 # [SIMULATE] Calling documentQaHandler with input... # [SIMULATE] PDF extraction completed # [SIMULATE] Embedding chunks... # [SIMULATE] LLM invoked with prompt length 1240 chars # [SIMULATE] Result: {answer:React 19 introduces..., sources:[...]}这个simulate命令会加载你的 handler 文件用 JSDOM 模拟Buffer和fetch完全绕过网络和外部依赖。它是排查“逻辑错误”的最佳工具——比如你怀疑RecursiveCharacterTextSplitter分块逻辑有问题直接simulate就能看到分块