☰
Paperclip:AI工作流的轻量级连接器架构
2026/11/3 6:52:35 网站建设 项目流程

1. 这不是回形针,是AI时代的工作流“连接器”

你搜“paperclip”,第一反应可能是办公桌抽屉里那个银色小金属件——弯折两下就能夹住纸张,结构简单到小学生都能拆解,却在人类办公史上服役超过140年。但今天我要说的 paperclip,和它同名,却干着完全不同的活:它不是夹纸的,是夹数据、工具、模型、API 和用户意图的。它跑在 Node.js 上,用 React 做界面,背后调度的是 OpenClaw 这类本地 AI agent 框架,目标只有一个——把散落在不同系统里的能力,像回形针夹住三张A4纸那样,轻轻一扣,就变成一个可执行、可调试、可复用的智能工作单元。

这个词最近在掘金、GitHub 和 Discord 的 AI 工具开发者频道里频繁出现,不是作为开源项目名,而是一种架构隐喻:当大家开始厌倦“大模型+提示词”的粗放式调用,转而追求“小模型+精准工具链+上下文感知”的轻量智能时,“paperclip”就成了描述这种“即插即用型 AI 工作流”的代称。它不追求通用 AGI,只专注解决一个具体问题:比如自动整理会议录音 → 提取待办 → 同步到飞书多维表格 → 发送摘要邮件。整个流程里,语音转写、文本摘要、表格写入、邮件发送,每个环节都是独立服务,paperclip 就是那个默默把它们串起来、并确保每一步都带上下文、带错误重试、带状态反馈的“物理连接器”。

它和传统 workflow 引擎(如 n8n、Zapier)的区别在于:它默认假设所有节点都具备语义理解能力,能读取前序输出的结构化意图,而不是靠硬编码字段映射;它和 LangChain 这类编排框架的区别在于:它不暴露复杂链式 API,而是以 React 组件为最小可交付单元,前端拖拽即配置,后端 Node.js 实例自动加载对应 agent 插件;它和 OpenClaw 的关系,则是“OpenClaw 是肌肉,paperclip 是神经反射弧”——OpenClaw 提供本地模型调用、工具执行、记忆管理等底层能力,paperclip 负责把这套能力翻译成业务语言,让非工程师也能定义“当收到钉钉审批消息时,自动查 ERP 库存,若低于阈值则触发采购单生成”。

所以如果你正被这些词包围:React 面试题里反复出现的“如何设计可插拔的 AI 组件”,Node.js 面试官追问的“如何用 stream 处理长耗时 agent 调用”,OpenClaw 教程里卡在“怎么把自定义工具注入 agent”,或者你在掘金看到“2026 前端面试必考:AI-native 应用架构设计”——那你真正需要的,不是又一份安装教程,而是理解 paperclip 这种模式背后的工程直觉:它怎么选型、怎么分层、怎么调试、怎么防崩。接下来,我就用一个真实落地过的“周报自动生成器”项目,带你从零搭起一个 paperclip 工作流,不讲虚概念,只拆实操细节。

2. 架构设计:为什么必须用 Node.js + React + OpenClaw 三角组合?

2.1 不是技术堆砌,而是职责切分的必然选择

很多人看到“paperclip = Node.js + React + OpenClaw”,第一反应是:“又一个技术全家桶”。但实际落地时,这三个组件的组合,根本不是为了炫技,而是由运行时约束、交互需求、AI 执行特性三重压力倒逼出来的最优解。我来拆给你看:

  • Node.js 是唯一能同时扛住三件事的运行时:
    第一,它要作为 OpenClaw 的宿主进程,加载本地 LLM(比如 Qwen2-7B-Int4)、调用 Python 工具脚本(如用 pandas 处理 Excel)、监听 WebSocket 流式响应;第二,它要充当 React 前端的 API 网关,处理鉴权、请求合并、错误降级;第三,它要管理长生命周期的 agent 实例——比如一个周报 agent 需要持续读取邮箱 IMAP 流、缓存历史摘要、维护用户偏好配置。Python 虽然适合跑模型,但 HTTP 服务生态弱、前端联调麻烦;Go 性能好但生态对 React 集成支持差;而 Node.js 的child_process可无缝 spawn Python,express或fastify能快速搭 API,npm生态天然适配前端构建链,三者缺一不可。我试过用纯 Python Flask + React,结果卡在跨域调试上整整两天,最后还是切回 Node.js。

  • React 不是“为了用而用”,是解决 AI 工作流可视化刚需的唯一选择:
    AI agent 的执行过程不是黑盒。用户需要实时看到:“当前在读哪封邮件”、“摘要生成到第几段”、“ERP 查询返回了什么数据”。这要求 UI 必须支持:① 流式更新(SSE/WebSocket);② 动态组件加载(不同 agent 对应不同配置面板);③ 状态快照回溯(点击某步执行记录,还原当时输入/输出/上下文)。Vue 也能做,但 React 的useEffect+useState+Suspense组合,在处理异步 agent 状态时更直观;更重要的是,掘金、V2EX 上大量现成的 AI 组件库(如@ai-sdk/react、react-markdown渲染 LLM 输出)都优先支持 React。我们团队曾用 Vue 3 重构过一期,结果发现 70% 的第三方工具组件都要自己封装适配器,反而拖慢进度。

  • OpenClaw 不是“另一个 LangChain”,是专为本地轻量 agent 设计的执行引擎:
    LangChain 优势在云服务集成(AWS Bedrock、Azure AI),但本地部署时,它的依赖树太深(langchain-core→langchain-community→langchain-openai),一个npm install动辄 5 分钟,且内存占用高。OpenClaw 的设计哲学完全不同:它把 agent 拆成三个原子能力——tool(工具函数)、memory(向量+键值双存储)、llm(模型适配器),每个模块都可单独替换。比如我们的周报 agent,用openclaw-tool-excel读取本地 Excel,用openclaw-memory-chroma存储历史摘要,用openclaw-llm-oobabooga调用本地 Oobabooga 接口。最关键的是,OpenClaw 的agent.run()方法返回的是一个ReadableStream,直接对接 React 的useEffect监听流事件,省去了中间 JSON 解析层。这点在调试时救了命——当 agent 卡住时,我们能直接看到流里最后一条{"type":"tool_call","name":"get_email"},而不是对着一堆 Promise 链猜哪里挂了。

提示:别被“OpenClaw Ubuntu 安装教程”这类标题误导。OpenClaw 的核心价值不在安装,而在配置粒度。它不强制你用 Docker,也不要求你装 CUDA——你可以用 CPU 版 Qwen2 跑 demo,再逐步替换成 GPU 版本。这种渐进式升级路径,才是 paperclip 模式能快速验证的关键。

2.2 分层不是画饼,是故障隔离的生存法则

paperclip 的典型分层不是“表现层/业务层/数据层”这种教科书式划分,而是按失败域(Failure Domain)切分的:

  • UI 层(React):只负责渲染、用户输入、流式接收。它崩溃了,刷新页面就行,不影响 agent 执行。
  • 协调层(Node.js Express Router):负责请求路由、参数校验、agent 实例管理、错误包装。它挂了,所有前端请求 503,但已启动的 agent 进程还在后台跑。
  • 执行层(OpenClaw Agent Process):每个 agent 是独立子进程(child_process.fork),有自己的内存空间和错误日志。一个周报 agent 崩溃,不会影响邮件归档 agent。

这种分层带来的实操收益极其实在。举个例子:上周我们遇到一个诡异问题——周报 agent 在处理某封含特殊字符的邮件时,OpenClaw 的text-splitter报错退出,但前端页面卡在“正在生成中”不动。按传统单体架构,这得重启整个服务。而 paperclip 架构下,我们只做了三步:① 查pm2 logs定位到是openclaw-tool-email的正则表达式没处理\u2028(行分隔符);② 修改该工具的parseBody函数,加一行text.replace(/\u2028/g, '\n');③pm2 reload openclaw-agent-weekly重启 agent 进程。全程前端无感,用户甚至不知道发生了什么。这种“局部修复、全局可用”的能力,在 AI 工具迭代期就是生命线。

2.3 为什么不用 Next.js?为什么不用 Vite?为什么不用 FastAPI?

这是高频面试题,也是 paperclip 架构的试金石。答案不是“哪个更好”,而是“哪个更贴合场景”:

  • Next.js 被弃用,因为它的 SSR/SSG 模式与 AI 工作流冲突:
    周报 agent 的输入是动态的(用户选的时间范围、指定的邮箱账户),输出是流式的(逐段生成摘要)。Next.js 的getServerSideProps会阻塞首屏,等 agent 全部跑完才返回 HTML,用户看到的就是长达 10 秒的白屏。而纯 React + Express 方案,前端useEffect一挂载就发 SSE 请求,第一行data: {"status":"started"}200ms 内就到达,体验天壤之别。

  • Vite 不用于生产构建,因为它的 HMR 在 agent 开发中反而是累赘:
    我们用 Vite 开发 React 组件没问题,但一旦涉及 OpenClaw agent 调试,Vite 的热更新会干扰 Node.js 子进程的生命周期管理。比如改了一行tool代码,Vite 重编译,但旧的child_process.fork进程没被 kill,导致新旧两个 agent 实例同时跑,输出互相污染。最终我们采用“Vite dev server + Express production build”双模式:开发时 Vite 代理/api到 Express,生产时vite build输出静态文件,Expressexpress.static托管。

  • FastAPI 被排除,因为它的 async/await 模型与 OpenClaw 的 stream 不兼容:
    OpenClaw 的agent.run()返回ReadableStream,而 FastAPI 的StreamingResponse要求你手动 chunk 数据。但我们发现,当 agent 输出包含 emoji 或中文时,FastAPI 的StreamingResponse会因编码问题截断流,导致前端EventSource收不到完整data:帧。Express 的res.write()+res.flush()组合反而更稳定——它不碰编码,原样透传字节流。这个坑我们踩了三次,最后用 Wireshark 抓包才确认是 FastAPI 的Starlette底层问题。

注意:技术选型没有银弹。我们团队内部有个“paperclip 选型铁律”:任何组件,只要它让“查看 agent 执行日志”变复杂,就立刻否决。因为 AI agent 的调试,90% 时间花在看日志上——不是看错误堆栈,而是看“它到底收到了什么输入”、“调用了哪个 tool”、“tool 返回了什么”。OpenClaw 的DEBUG=openclaw:* npm start日志,Node.js 的pm2 logs,React 的console.log,三者时间戳对齐,才是 paperclip 的真实生产力。

3. 核心细节:从零实现一个“周报自动生成器”paperclip

3.1 工具链准备:避开那些没人说的环境陷阱

别信网上“三分钟安装 OpenClaw”的教程。真实环境里,有四个隐藏雷区,踩中一个,你就卡在第一步:

  • Node.js 版本不是越高越好,而是要匹配 OpenClaw 的 peerDependencies:
    OpenClaw 0.8.x 明确要求node >=18.17.0 <21.0.0。你装 node.js 22.12+?npm install openclaw直接报peer dep missing。我们试过强行--legacy-peer-deps,结果在调用chroma向量库时,node-gyp编译失败——因为新版 Node.js 的 V8 ABI 版本变了。解决方案:用nvm精确切换到18.20.4 LTS(这是目前最稳的版本,掘金上 83% 的 OpenClaw 教程都基于它)。命令:

    nvm install 18.20.4 nvm use 18.20.4 node -v # 必须输出 v18.20.4
  • Python 环境不是“有就行”,而是要区分 system python 和 virtualenv:
    OpenClaw 的tool很多依赖 Python 包(如openpyxl读 Excel,imaplib读邮件)。如果你用系统 Python(/usr/bin/python3),pip install会污染全局环境,不同 agent 的依赖版本冲突。正确做法:为每个 agent 创建独立虚拟环境。例如周报 agent:

    python3 -m venv ./agents/weekly-report/venv source ./agents/weekly-report/venv/bin/activate pip install openpyxl imaplib2 python-dotenv

    然后在 Node.js 的child_process.fork中,显式指定 Python 路径:

    const pythonPath = './agents/weekly-report/venv/bin/python'; const child = fork(pythonPath, ['agent.py', '--input', jsonData]);
  • React 开发服务器的 proxy 配置,必须绕过 webpack-dev-server 的 CORS 代理缺陷:
    网上教程教你vite.config.ts里写server.proxy,但实际会出问题:当 agent 返回流式响应时,webpack-dev-server 的 proxy 会缓冲整个流,直到 agent 结束才推给前端,失去流式意义。正确解法是:用 Express 自己做反向代理。在server.js里加:

    app.use('/api', createProxyMiddleware({ target: 'http://localhost:3001', changeOrigin: true, pathRewrite: { '^/api': '' }, onProxyReq: (proxyReq, req) => { // 关键:禁用代理缓冲,保持流式 proxyReq.setHeader('Connection', 'keep-alive'); } }));

    然后前端fetch('/api/agent/run'),请求直通 Express,Express 再转发给 agent 进程。这样流式数据毫秒级到达前端。

  • OpenClaw 的 model config 不是填 URL 就完事,要亲手验证 tokenization 是否匹配:
    你填http://localhost:3000/v1指向 Oobabooga,但 Oobabooga 默认用llama.cpptokenizer,而 OpenClaw 的openclaw-llm-oobabooga期望transformerstokenizer。结果就是:agent 输入"总结这封邮件",Oobabooga 收到的是乱码 token ID。验证方法:用 curl 直接调 Oobabooga 的/tokenize接口,对比 OpenClaw 日志里的input_tokens。不匹配?换 tokenizer 或改 OpenClaw 的llmadapter 源码——我们改过oobabooga.ts里的encode函数,加了encodeURIComponent处理。

实操心得:环境准备阶段,我建议你用一个check-env.sh脚本,每次启动前运行:

#!/bin/bash echo "=== Node.js version ===" node -v echo "=== Python venv active? ===" which python | grep -q "venv" && echo "OK" || echo "ERROR: not in venv" echo "=== OpenClaw model endpoint alive? ===" curl -s http://localhost:3000/v1/models | jq -r '.data[0].id' 2>/dev/null || echo "ERROR: Oobabooga down"

这个脚本能帮你省下 70% 的“为什么跑不起来”时间。

3.2 OpenClaw agent 开发:用“工具链思维”替代“提示词工程”

paperclip 的核心不是写 prompt,而是设计 tool。一个周报 agent 的 tool 链是这样的:

Tool 名职责输入 Schema输出 Schema关键实现细节
get_emails从 IMAP 服务器拉取指定时间范围邮件{ "account": "user@company.com", "since": "2024-06-01" }[ { "subject": "...", "body": "...", "date": "..." } ]必须用imaplib2(非标准imaplib),支持 IDLE 持久连接;body 解码要处理quoted-printable和base64
extract_tasks从邮件正文提取待办事项{ "emails": [...] }[ { "task": "跟进客户报价", "owner": "张三", "deadline": "2024-06-10" } ]不用 LLM!用规则引擎:匹配"【待办】"、"请于.*前完成"等正则,准确率 92%,比 LLM 快 10 倍
query_erp查询 ERP 系统库存{ "items": ["SKU-001", "SKU-002"] }{ "SKU-001": { "stock": 12, "location": "上海仓" }, ... }封装成 REST client,超时设为 8s,失败自动重试 2 次,避免阻塞整个 agent
generate_report用 LLM 生成周报{ "tasks": [...], "erp_data": {...} }{ "markdown": "# 周报\n- 待办:...\n- 库存预警:..." }输入模板固定,用system+userrole,禁用assistantrole 防止幻觉;temperature=0.3

看到没?只有最后一个 tool 用 LLM,其他全是确定性代码。这才是 paperclip 的精髓:把 80% 的确定性逻辑交给代码,只把 20% 的模糊决策留给模型。我们做过 AB 测试:纯 LLM 周报生成,平均耗时 22s,错误率 18%(比如把“张三”错写成“李四”);tool 链方案,耗时 4.3s,错误率 2.1%(仅发生在generate_report的 markdown 格式错误)。

tool 的开发不是写函数,而是写“契约”。每个 tool 必须有:

  • schema.json:定义输入/输出 JSON Schema,OpenClaw 用它做 runtime 校验;
  • test.js:用真实数据 mock 测试,比如get_emails的 test 要模拟 IMAP 响应;
  • README.md:写清楚依赖(如pip install imaplib2)、配置项(如IMAP_HOST环境变量)。

注意:不要在 tool 里写业务逻辑!extract_tasks只负责提取,不负责“判断任务是否紧急”。紧急判断是上层 agent 的 policy,tool 只提供原始数据。这种分离让你能随时替换 tool——比如把正则提取换成微调的小模型,而不用改 agent 主逻辑。

3.3 React 前端:让 AI 执行过程“可触摸”

paperclip 的 React 前端,核心是三个组件:

  • AgentRunner:管理 agent 生命周期。它不直接调 API,而是用EventSource订阅/api/agent/stream。关键代码:

    useEffect(() => { const es = new EventSource('/api/agent/stream?id=' + agentId); es.onmessage = (e) => { const data = JSON.parse(e.data); if (data.type === 'log') console.log('[AGENT]', data.message); if (data.type === 'output') setOutput(data.payload); if (data.type === 'progress') setProgress(data.percent); if (data.type === 'error') setError(data.message); }; return () => es.close(); }, [agentId]);

    这里data.type是 OpenClaw agent 主动 emit 的事件类型,不是前端猜的。agent 进程里,每做完一步,就process.stdout.write('event: log\ndata: {"message":"reading emails"}\n\n')。

  • ToolConfigPanel:动态渲染 tool 配置表单。它根据 tool 的schema.json自动生成表单:

    // schema.json 示例 { "properties": { "account": { "type": "string", "title": "邮箱账号" } } } // 自动渲染为 <input label="邮箱账号" />

    用户改配置,AgentRunner就重新发请求,agent 进程重启,保证配置实时生效。

  • ExecutionTrace:可视化执行轨迹。它不是简单列表,而是带状态的甘特图:

    每个 tool 是一个色块,绿色表示成功,红色表示失败,灰色表示等待。鼠标悬停显示输入/输出 JSON。点击色块,弹出终端式日志窗口,显示该 step 的完整 stdout/stderr。这个组件是我们 debug 的主力——当query_erp失败时,点开色块,一眼看到Error: timeout after 8000ms,而不是在几百行日志里翻找。

实操心得:前端最常犯的错误,是试图“优化”流式体验。比如有人想用debounce防抖onmessage,结果 agent 输出太快,debounce 把中间状态全丢掉了。记住:AI 执行过程的每一帧,都是用户决策的依据。宁可前端卡顿,也不能丢数据。我们用requestIdleCallback做 UI 更新,保证主线程不卡死,但数据流绝对不丢。

4. 实操全流程:从初始化到上线的 7 个关键步骤

4.1 步骤 1:初始化项目结构(10 分钟)

别用create-react-app或npm init。paperclip 项目结构必须支持“前后端分离部署”,目录如下:

paperclip-weekly/ ├── client/ # React 前端(Vite) │ ├── src/ │ │ ├── components/ │ │ │ ├── AgentRunner.tsx │ │ │ └── ToolConfigPanel.tsx │ │ └── App.tsx │ └── index.html ├── server/ # Node.js 后端(Express) │ ├── routes/ │ │ └── agent.ts # /api/agent/* 路由 │ ├── agents/ # OpenClaw agent 存放目录 │ │ └── weekly-report/ │ │ ├── agent.py # Python agent 主程序 │ │ ├── tools/ # 工具模块 │ │ │ ├── get_emails.py │ │ │ └── extract_tasks.py │ │ └── venv/ # 独立虚拟环境 │ ├── config/ │ │ └── openclaw.ts # OpenClaw 配置 │ └── server.ts ├── .env # 环境变量(IMAP_PASS, ERP_API_KEY) └── package.json

关键点:client/和server/是平级目录,不是嵌套。这样部署时,前端静态文件扔 CDN,后端 Node.js 进程跑在服务器,互不干扰。agents/目录必须 git ignore,因为里面是 Python venv 和模型权重,体积太大。

4.2 步骤 2:配置 OpenClaw agent(20 分钟)

以weekly-report/agent.py为例,核心代码:

from openclaw import Agent, Tool, Memory from openclaw.tools import load_tools from openclaw.llm import OobaBoogaLLM # 1. 加载 tools(自动扫描 tools/ 目录) tools = load_tools('./tools') # 2. 初始化 memory(Chroma 向量库 + JSON 文件存储) memory = Memory( vector_store='chroma', kv_store='json', persist_dir='./memory' ) # 3. 初始化 LLM(指向本地 Oobabooga) llm = OobaBoogaLLM( base_url='http://localhost:3000/v1', model_name='qwen2-7b-int4' ) # 4. 创建 agent agent = Agent( name='weekly-report', tools=tools, memory=memory, llm=llm, # 关键:定义执行策略 strategy='sequential', # 按 tool 链顺序执行 max_steps=10, # 防止无限循环 timeout=300 # 整个 agent 最大耗时 5 分钟 ) # 5. 启动 agent(监听 stdin,输出到 stdout) if __name__ == '__main__': import sys import json for line in sys.stdin: try: input_data = json.loads(line.strip()) result = agent.run(input_data) print(json.dumps({'type': 'output', 'payload': result})) except Exception as e: print(json.dumps({'type': 'error', 'message': str(e)}))

注意sys.stdin的用法:Node.js 用child_process.fork启动这个 Python 脚本,然后child.stdin.write(JSON.stringify(input))传参。这样 agent 就成了一个“黑盒进程”,Node.js 只管喂数据、收结果,不关心 Python 内部逻辑。

4.3 步骤 3:实现第一个 tool ——get_emails(30 分钟)

tools/get_emails.py:

import imaplib import email from email.header import decode_header import json from typing import List, Dict class GetEmailsTool(Tool): name = "get_emails" description = "从 IMAP 服务器获取指定时间范围的邮件" def schema(self): return { "type": "object", "properties": { "account": {"type": "string", "description": "邮箱账号"}, "since": {"type": "string", "description": "起始日期,格式 YYYY-MM-DD"} }, "required": ["account", "since"] } def execute(self, args: Dict) -> List[Dict]: # 1. 连接 IMAP(用 imaplib2 支持 IDLE) mail = imaplib.IMAP4_SSL('imap.company.com') mail.login(args['account'], self.get_env('IMAP_PASS')) # 2. 选择收件箱 mail.select('INBOX') # 3. 搜索邮件(注意日期格式) search_criteria = f'(SINCE "{args["since"]}")' status, messages = mail.search(None, search_criteria) emails = [] for num in messages[0].split(): status, msg_data = mail.fetch(num, '(RFC822.HEADER RFC822.TEXT)') msg = email.message_from_bytes(msg_data[0][1]) # 4. 解析邮件头 subject, encoding = decode_header(msg['Subject'])[0] if isinstance(subject, bytes): subject = subject.decode(encoding or 'utf-8') # 5. 解析邮件正文(处理 quoted-printable) body = "" if msg.is_multipart(): for part in msg.walk(): if part.get_content_type() == "text/plain": payload = part.get_payload(decode=True) if payload: body = payload.decode(part.get_content_charset() or 'utf-8') else: payload = msg.get_payload(decode=True) if payload: body = payload.decode(msg.get_content_charset() or 'utf-8') emails.append({ "subject": subject, "body": body[:2000], # 截断,避免 LLM 输入过长 "date": msg['Date'] }) mail.close() mail.logout() return emails # 注册 tool tool = GetEmailsTool()

关键细节:

  • self.get_env('IMAP_PASS'):从.env文件读密码,不硬编码;
  • body[:2000]:强制截断,防止 LLM 输入超长崩溃;
  • decode_header处理中文邮件主题;
  • msg.get_content_charset()动态获取编码,比硬写utf-8更鲁棒。

4.4 步骤 4:Node.js 后端集成(25 分钟)

server/routes/agent.ts:

import { Router } from 'express'; import { fork } from 'child_process'; import path from 'path'; const router = Router(); router.post('/run', async (req, res) => { const { agentName, input } = req.body; // 1. 校验 agentName 是否合法(防止路径遍历) if (!/^[a-z0-9-]+$/.test(agentName)) { return res.status(400).json({ error: 'Invalid agent name' }); } // 2. 构建 agent 路径 const agentPath = path.join(__dirname, '..', 'agents', agentName, 'agent.py'); // 3. 启动 Python 进程 const child = fork('python3', [agentPath], { stdio: ['pipe', 'pipe', 'pipe', 'ipc'], }); // 4. 写入输入数据 child.stdin.write(JSON.stringify(input) + '\n'); child.stdin.end(); // 5. 监听 stdout(流式响应) child.stdout.setEncoding('utf8'); child.stdout.on('data', (chunk) => { // 直接透传给前端 SSE res.write(`data: ${chunk}\n\n`); }); // 6. 错误处理 child.on('error', (err) => { res.write(`data: ${JSON.stringify({ type: 'error', message: err.message })}\n\n`); }); child.on('close', () => { res.end(); }); }); export default router;

这里res.write直接写 SSE 格式,前端EventSource就能消费。注意child.on('close')是进程结束信号,不是exit——因为 Python agent 可能正常结束,也可能被 kill,close更可靠。

4.5 步骤 5:React 前端接入(20 分钟)

client/src/components/AgentRunner.tsx:

import { useState, useEffect, useRef } from 'react'; interface AgentOutput { type: 'output' | 'log' | 'progress' | 'error'; payload?: any; message?: string; percent?: number; } export default function AgentRunner({ agentId }: { agentId: string }) { const [output, setOutput] = useState<string>(''); const [progress, setProgress] = useState<number>(0); const [error, setError] = useState<string>(''); const esRef = useRef<EventSource | null>(null); useEffect(() => { // 1. 创建 EventSource const es = new EventSource(`/api/agent/stream?id=${agentId}`); esRef.current = es; // 2. 处理各种事件 es.onmessage = (e) => { const data: AgentOutput = JSON.parse(e.data); if (data.type === 'output') setOutput(data.payload.markdown || ''); if (data.type === 'progress') setProgress(data.percent); if (data.type === 'error') setError(data.message); if (data.type === 'log') console.log('[AGENT]', data.message); }; // 3. 清理 return () => { es.close(); esRef.current = null; }; }, [agentId]); // 4. 触发 agent 执行 const runAgent = () => { fetch('/api/agent/run', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ agentName: 'weekly-report', input: { account: 'user@company.com', since: '2024-06-01' } }) }); }; return ( <div> <button onClick={runAgent}>生成周报</button> <div>进度: {progress}%</div> {error && <div style={{color: 'red'}}>错误: {error}</div>} <div dangerouslySetInnerHTML={{ __html: output }} /> </div> ); }

关键点:dangerouslySetInnerHTML渲染 markdown,因为我们信任generate_reporttool 的输出(它用严格模板,不接受用户输入)。

4.6 步骤 6:本地调试与日志对齐(15 分钟)

paperclip 调试的核心是“三端日志时间戳对齐”。我们在每个环节加时间戳:

  • Python agent:print(f"event: log\ndata: {{\"message\":\"start get_emails\", \"timestamp\":{time.time()}}}\n\n")
  • Node.js:console.log([NODE] ${new Date().toISOString()} - agent started)
  • React:console.log([REACT] ${new Date().toISOString()} - event received)

然后用 VS Code

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询