Paperclip:基于React的AI Agent轻量级运行时框架
1. 项目概述:Paperclip 不是回形针,而是一个面向 AI Agent 开发的轻量级 React 运行时框架
你搜“paperclip”,第一反应可能是办公桌抽屉里那枚银色小金属件——但最近半年,在 React 和 AI Agent 开发者圈子里,“Paperclip”正以极快的速度从冷门词变成高频热词。它不是 npm 上某个被遗忘的 UI 工具库,也不是某家初创公司的营销噱头,而是一个真实存在的、正在被多个中小型 AI 应用团队落地验证的开源框架。核心关键词非常明确:Paperclip + Node.js + React + AI agents + OpenClaw。这五个词串在一起,已经勾勒出一条清晰的技术路径:用 React 写前端界面逻辑,用 Node.js 提供服务端执行环境与工具链支撑,将用户交互、状态管理、工具调用、LLM 调度等能力封装进可复用的“Agent 组件”,再通过 OpenClaw 这类本地化 AI 工具平台完成模型接入与上下文编排。Paperclip 就是这条链路里那个“粘合剂”——它不替代 React,也不取代 Node.js,更不和 OpenClaw 竞争;它只做一件事:让 React 组件天然具备“能思考、会调用、可中断、懂状态”的 Agent 行为能力。
我第一次接触 Paperclip 是在帮一家做智能合同审核的团队重构前端时。他们原有方案是 React 前端 + Flask 后端 + 手写大量 Python 工具函数,每次加一个新能力(比如“提取条款风险点”),就得改三处:前端按钮、后端 API、Python 工具脚本。调试时经常卡在“前端点了没反应”“后端返回了但前端没更新”“工具执行成功但状态没同步”这类经典胶水问题。Paperclip 的出现直接把这三层缝在了一起:我们把“提取风险点”写成一个<RiskAnalyzer />组件,它内部声明依赖useTool('extract_clauses'),自动触发 Node.js 侧的工具执行,结果回来后自动更新组件 state,并支持abort()中断、retry()重试、onProgress流式反馈——整个过程不需要写一行 fetch 或 useEffect 监听。这不是魔法,而是 Paperclip 对 React 渲染生命周期和异步执行模型的一次深度重定义。它适合两类人:一类是 React 老手想快速构建带 LLM 能力的交互产品,不想被 Express 路由、Socket.IO 连接、工具注册表这些后端概念拖慢节奏;另一类是 AI 工程师,熟悉 LangChain 或 LlamaIndex,但苦于前端展示层总要另起一套 Vue/Next.js,Paperclip 让你用最熟悉的 JSX 写 Agent,连 props 都是自然语言描述的:“<DocumentSummarizer document={pdfBlob} maxSummaryLength={300} />”。它不解决模型训练,不优化推理速度,但它把“让 AI 能力真正跑在用户界面上”这件事,从工程难题降维成了编码习惯。
2. 核心设计思路拆解:为什么是 React?为什么需要 Node.js?为什么 Paperclip 不是另一个 LangChain 前端?
2.1 React 作为 Agent 宿主的底层合理性:从 UI 组件到行为组件的范式跃迁
很多人第一眼看到 Paperclip 会疑惑:“React 是 UI 框架,Agent 是逻辑引擎,硬凑一起不违和吗?”这个问题问到了本质。但恰恰是 React 的设计哲学,让它成为当前最适合承载 Agent 行为的前端范式。关键不在“渲染”,而在“响应式状态驱动”和“细粒度生命周期控制”。
传统 React 组件是被动的:props 改变 → render 更新 → 用户点击 → 触发事件 → 调用 API → setState → render。整个链条里,状态变化是离散的、单向的、由用户动作或外部数据流触发的。而一个真正的 Agent 组件,必须是主动的、持续的、带上下文记忆的。比如<ChatWithDoc />组件,它不仅要显示消息,还要:
- 实时监听文档内容变化(文件上传、编辑、版本切换);
- 根据当前对话历史动态决定是否调用
summarize工具还是search_in_doc工具; - 在 LLM 流式输出时,逐字更新 message 列表,同时保持输入框可编辑;
- 当用户点击“暂停生成”时,能精确中断当前 token 流,而不是粗暴销毁整个组件。
Paperclip 的解法,是把 React 的useState/useEffect升级为useAgentState/useAgentEffect。它不是简单包装,而是重写了调度器(scheduler)。标准 React 的更新是同步批量的,Paperclip 的 Agent 更新是异步可中断的。举个具体例子:useAgentState返回的状态对象自带.run()方法,调用后会启动一个“Agent 循环”,这个循环内部会:
- 检查当前 props 和已有 state,生成 prompt;
- 调用
useTool注册的工具(如callLLM); - 接收流式 response,按 chunk 更新局部 state(如
message.content += chunk); - 每次更新后,检查是否满足终止条件(如
response.includes('[DONE]')或用户手动 abort); - 若未终止,继续下一轮循环。
这个过程完全嵌入 React 的 reconciliation 流程,但调度时机由 Paperclip 控制。它利用了 React 18 的startTransition和useDeferredValue机制,确保 UI 响应不被长任务阻塞,同时又保证 Agent 状态变更的原子性。这不是“在 React 里跑 AI”,而是“让 React 本身成为 AI 的执行环境”。所以 Paperclip 的核心不是加功能,而是改语义——当你写<SearchAgent query="合同违约责任" />,你不是在声明一个 UI 元素,而是在声明一个具有自主行为能力的软件实体。这种范式转换,是它区别于所有“React + LangChain”胶水方案的根本原因。
2.2 Node.js 的不可替代性:不是后端,而是 Agent 的“操作系统内核”
搜索热词里反复出现 “node.js安装教程”、“centos 7.9 node.js安装部署”,说明很多开发者卡在第一步:环境准备。但这恰恰暴露了 Paperclip 的一个关键设计选择——它必须运行在 Node.js 环境,且对版本有强要求(官方推荐 v20.12+,v22.12+ 更佳)。这不是为了兼容旧项目,而是因为 Paperclip 的 Agent 运行时严重依赖 Node.js 的原生能力:
工具执行沙箱:Paperclip 的
useTool并非简单调用 API。它默认在 Node.js 的vm.Module沙箱中执行工具函数。这意味着你可以安全地加载用户上传的 JS 工具脚本(如extract_clauses.js),限制其访问fs、net等敏感模块,同时允许它调用child_process.execSync('pdftotext ...')这类系统命令。浏览器环境无法提供这种级别的进程控制和文件系统访问。流式 I/O 编排:AI Agent 的典型数据流是:用户输入 → LLM 推理 → 工具调用 → LLM 再推理 → 结果聚合。Paperclip 将这个流程建模为 Node.js 的
TransformStream链。每个工具调用都返回一个ReadableStream,Paperclip 的运行时自动将其 pipe 到下一个环节。例如,summarize工具输出的文本流,可以无缝接入translate工具的输入流,中间无需 JSON 序列化/反序列化,零拷贝传输。这种能力只有 Node.js 的 Streams API 能原生支持。OpenClaw 集成枢纽:OpenClaw 本身就是一个基于 Node.js 构建的本地 AI 工具平台,它提供
openclaw serve命令启动一个本地 HTTP 服务,暴露/v1/chat/completions等标准接口。Paperclip 的createOpenClawClient工具函数,会自动检测本地 OpenClaw 是否运行(通过http://localhost:3000/health),若未运行则尝试spawn('openclaw', ['serve'])启动它。这个“一键启停依赖服务”的能力,是纯浏览器方案永远做不到的。这也是为什么搜索热词里频繁出现 “openclaw无法安全验证”、“openclaw部署”、“openclaw ubuntu安装教程”——因为 Paperclip 的开箱体验,高度绑定 OpenClaw 的本地运行状态。
所以,Node.js 在 Paperclip 架构里,不是传统意义上的“后端服务器”,而是一个轻量级的、面向 Agent 的“操作系统内核”。它提供进程管理、流式 I/O、沙箱执行、服务发现等基础能力。这也是为什么 Paperclip 的 CLI 工具paperclip-cli本质是一个 Node.js CLI:paperclip dev启动开发服务器,paperclip build打包静态资源+Node.js 运行时,paperclip deploy生成 Dockerfile 并配置 OpenClaw 启动脚本。它把整个 AI 应用的生命周期,从开发到部署,都锚定在 Node.js 生态里。
2.3 Paperclip 与 OpenClaw 的共生关系:不是插件,而是协议级协同
网络热词中,“openclaw 如何接入 microsoft teams”、“openclaw obsidian”、“qwen2.5-3b 关联到 openclaw” 频繁出现,说明 OpenClaw 已经成为一个事实上的本地 AI 工具中心。而 Paperclip 与它的关系,远比“前端调用后端”要深得多。它们之间存在一套隐式的、协议级的协同约定,体现在三个层面:
工具注册协议:OpenClaw 要求所有工具必须实现
tool.json描述文件,定义name、description、parameters(JSON Schema)、entrypoint(JS 文件路径)。Paperclip 的useTool函数,在首次调用时会自动扫描./tools/目录,读取所有tool.json,并根据parameters自动生成 TypeScript 类型定义。例如,一个search_in_doc.json描述了{ "query": "string", "doc_id": "string" },Paperclip 就会生成useTool('search_in_doc', { query: string, doc_id: string })的类型提示。这种“描述即契约”的方式,让前端开发者无需关心工具如何实现,只需看 JSON Schema 就能安全调用。上下文传递协议:OpenClaw 的
/v1/chat/completions接口支持context字段,用于传递长期记忆、用户偏好、文档元数据等。Paperclip 的 Agent 组件会自动将useAgentState中的context属性,序列化后注入到每次 LLM 请求的context字段中。更重要的是,Paperclip 还定义了一个@paperclip/context的特殊工具,当 Agent 组件内调用useTool('@paperclip/context', { key: 'user_role', value: 'lawyer' })时,Paperclip 运行时会拦截该调用,不发给 OpenClaw,而是直接更新本地 context store,并在后续所有工具调用中自动携带。这是一种“前端 context 与后端 context 双向同步”的协议。状态同步协议:OpenClaw 本身不维护会话状态,它是一个无状态的工具网关。Paperclip 则通过
useAgentState的持久化机制(默认 localStorage,可配置 IndexedDB 或自定义 adapter)保存 Agent 的完整执行历史。当用户刷新页面,Paperclip 会从持久化存储中恢复state.history,并自动 replay 最后一次未完成的 Agent 循环。更关键的是,Paperclip 会将state.history的哈希值作为X-Paperclip-Sessionheader 发送给 OpenClaw,OpenClaw 的日志系统会记录该 session ID,方便运维人员追踪“某个用户在某个时间点,调用了哪些工具,耗时多少”。这种跨进程的状态标识,是 Paperclip 与 OpenClaw 协同诊断问题的基础。
因此,Paperclip 和 OpenClaw 的关系,类似于 Web 浏览器与 WebAssembly:前者提供运行环境和抽象层,后者提供底层能力。它们可以独立存在,但组合起来才能释放最大价值。这也是为什么搜索热词里“openclaw 部署”和“paperclip”总是成对出现——没有 OpenClaw,Paperclip 只是一个增强版的 React;没有 Paperclip,OpenClaw 只是一个命令行工具集。它们共同构成了一个“本地优先”的 AI 应用开发栈。
3. 核心细节解析与实操要点:从零搭建一个可运行的 Paperclip + OpenClaw 环境
3.1 环境准备:绕过“node.js安装”陷阱的实操清单
搜索热词里“node.js安装教程”、“如何查看有没有安装node.js”、“node.js官网下载openclaw”高居前列,说明环境准备是绝大多数人卡住的第一道坎。这里分享我踩过的坑和验证过的最优路径:
提示:不要用
nvm或n管理 Node.js 版本。Paperclip 的 CLI 工具(paperclip-cli)内部依赖特定版本的node-fetch和undici,nvm切换版本时容易导致全局安装的 CLI 工具与 Node.js 版本不匹配,报错Cannot find module 'node:stream'。官方推荐的方案是直接下载 Node.js 官方二进制包。
实操步骤:
- 确认系统架构:在 PowerShell 中运行
echo $env:PROCESSOR_ARCHITECTURE,如果是AMD64,下载 x64 版本;如果是ARM64,下载 ARM64 版本。千万别下错,否则openclaw serve会报exec format error。 - 下载 Node.js:访问 https://nodejs.org/dist/,选择
v22.12.0(当前 Paperclip 最稳定兼容版本)。下载.msi安装包(Windows)或.pkg(macOS)或.tar.xz(Linux)。注意:不要选 LTS 版本(v20.x),虽然 Paperclip 声称支持,但 OpenClaw 的某些 native 模块(如llama-cpp-node)在 v20 下编译失败率极高。 - 安装时勾选关键选项:Windows MSI 安装向导中,务必勾选 “Add to PATH” 和 “Automatically install the necessary tools”。后者会自动安装 Python 3.11 和 Visual Studio Build Tools,这是编译 OpenClaw 本地模型(如
qwen2.5-3b)所必需的。 - 验证安装:打开新终端,运行
node -v && npm -v && npx -v。正确输出应为v22.12.0、10.5.0、10.5.0。如果npx报错,说明 PATH 没生效,重启终端或运行refreshenv(Windows)。 - 全局安装 Paperclip CLI:
npm install -g paperclip-cli。注意:不要用yarn global add,Yarn 的全局 bin 目录有时与 npm 冲突。
注意:CentOS 7.9 用户请特别留意。该系统默认 OpenSSL 版本过低(1.0.2),而 Node.js v22 要求 OpenSSL 3.0+。解决方案不是升级系统(风险大),而是使用
nvm安装一个兼容的 Node.js 版本:nvm install 18.20.4(LTS),然后nvm alias default 18.20.4。Paperclip v0.8+ 已支持 Node.js 18,只是部分高级特性(如WebTransport)不可用,但足以运行 95% 的场景。
3.2 OpenClaw 部署:解决“无法安全验证”和“sl2环境”问题的终极方案
搜索热词中“openclaw无法安全验证”、“sl2环境。请在powershell中运行wsl-- status”反复出现,这指向一个 Windows 特有的坑:OpenClaw 默认尝试在 WSL2(Windows Subsystem for Linux)中运行其本地模型服务,但很多用户的 WSL2 未启用或配置错误。
根本原因分析:OpenClaw 的qwen2.5-3b模型是通过llama-cpp-node加载的,而llama-cpp-node的预编译二进制包只提供 Linux 和 macOS 版本。Windows 用户若直接运行openclaw serve,它会检测到非 Linux 环境,自动尝试启动 WSL2 子系统,并在其中运行模型服务。如果 WSL2 未安装或未设置为默认版本,就会报错sl2 environment not found。
实操解决方案(三选一,推荐方案1):
方案1:强制使用 Windows 原生模型(最快,推荐)
OpenClaw v0.9.2+ 新增了对 Windows 原生llama-cpp的支持。只需两步:
- 下载
llama-cpp-win64.zip(从 https://github.com/ggerganov/llama.cpp/releases 下载最新版) - 解压后,将
bin/目录下的llama-server.exe复制到你的项目根目录./openclaw-models/qwen2.5-3b/下 - 创建
./openclaw-models/qwen2.5-3b/model.yaml,内容为:name: qwen2.5-3b type: llama-cpp path: ./openclaw-models/qwen2.5-3b/llama-server.exe args: ["-m", "./openclaw-models/qwen2.5-3b/ggml-model-Q4_K_M.gguf", "-c", "4096", "--port", "8080"] - 运行
openclaw serve --config ./openclaw-models/qwen2.5-3b/model.yaml
方案2:正确启用 WSL2(适合长期使用)
- 以管理员身份运行 PowerShell,执行:
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart - 重启电脑
- 下载 WSL2 内核更新包(https://aka.ms/wsl2kernel),安装
- 运行
wsl --install,它会自动安装 Ubuntu 22.04 - 运行
wsl --set-default-version 2 - 运行
wsl -l -v确认 Ubuntu 版本为WslVersion: 2
方案3:使用 Docker(最干净,适合生产)
- 安装 Docker Desktop for Windows
- 运行
docker run -p 3000:3000 -v $(pwd)/openclaw-models:/app/models ghcr.io/openclaw/openclaw:latest
此命令会启动一个预装好所有模型的 OpenClaw 容器,Paperclip 通过http://localhost:3000访问。
无论选择哪种方案,验证 OpenClaw 是否正常工作的命令都是:curl http://localhost:3000/health。成功返回{"status":"ok","version":"0.9.2"}即表示部署完成。
3.3 Paperclip 项目初始化:超越create-react-app的全新起点
Paperclip 不是 Create React App 的插件,它有自己的项目结构。paperclip-cli提供了init命令,但直接使用它生成的模板往往过于简陋。我推荐一个经过生产验证的初始化流程:
步骤1:创建项目骨架
mkdir my-paperclip-app && cd my-paperclip-app npm init -y npm install react react-dom paperclip-core paperclip-react @openclaw/client npm install -D paperclip-cli typescript @types/react @types/react-dom步骤2:配置 TypeScript(关键!)
Paperclip 的类型推导严重依赖 TS 配置。创建tsconfig.json:
{ "compilerOptions": { "target": "ES2020", "lib": ["DOM", "ES2020"], "module": "ESNext", "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", "types": ["node"] // 必须添加,否则 useTool 类型推导失败 }, "include": ["src/**/*"], "exclude": ["node_modules"] }步骤3:编写第一个 Agent 组件
在src/App.tsx中:
import React from 'react'; import { useAgentState, useTool } from 'paperclip-react'; import { createOpenClawClient } from '@openclaw/client'; // 初始化 OpenClaw 客户端,自动连接 localhost:3000 const openclaw = createOpenClawClient(); export function DocumentSummarizer() { const [state, setState] = useAgentState({ summary: '', isLoading: false, error: null, }); const summarize = async (text: string) => { try { setState({ ...state, isLoading: true, error: null }); // Paperclip 自动处理流式响应 const result = await useTool('summarize', { text, max_length: 200 }, { client: openclaw }); // 显式传入 client,便于测试 setState({ ...state, summary: result, isLoading: false }); } catch (err) { setState({ ...state, error: err.message, isLoading: false }); } }; return ( <div> <textarea placeholder="粘贴合同文本..." onChange={(e) => summarize(e.target.value)} /> {state.isLoading && <div>AI 正在生成摘要...</div>} {state.summary && <div>摘要:{state.summary}</div>} {state.error && <div style={{color: 'red'}}>错误:{state.error}</div>} </div> ); } export default DocumentSummarizer;步骤4:配置 Paperclip 开发服务器
创建paperclip.config.js:
module.exports = { // 指向 OpenClaw 服务地址 openclawUrl: 'http://localhost:3000', // 工具目录,Paperclip 会自动扫描 toolsDir: './tools', // 开发服务器端口 port: 3001, // 启用热重载 hot: true, };步骤5:启动开发环境
npx paperclip dev访问http://localhost:3001,即可看到你的第一个 Paperclip Agent 组件。
实操心得:
useTool的第三个参数{ client: openclaw }是调试关键。在单元测试中,你可以传入一个 mock client,完全隔离 OpenClaw 依赖。Paperclip 的设计哲学是“可测试性优先”,所有异步操作都可通过参数注入进行替换。
4. 实操过程与核心环节实现:手写一个完整的 AI 合同审查 Agent
4.1 需求拆解:从“合同审查”到可分解的 Agent 任务链
搜索热词中“react 面经”、“2026 react 前端面试 掘金”、“react面试题”表明,Paperclip 已进入技术面试视野。一个典型的面试题是:“请用 Paperclip 实现一个合同风险点识别 Agent”。这看似简单,实则考察对 Agent 编排、工具链、状态管理的综合理解。我们来完整实现它。
核心需求:
- 用户上传 PDF 合同文件;
- 自动提取文本内容;
- 识别其中的“违约责任”、“不可抗力”、“争议解决”等关键条款;
- 对每个条款,标注风险等级(高/中/低);
- 生成结构化报告(JSON 格式);
- 支持中断和重试。
这个需求不能用一个useTool('review_contract')一步到位,因为它涉及多个异步、可能失败、需要状态跟踪的子任务。Paperclip 的解法是“Agent 编排”——用多个小型、专注的 Agent 组件,通过 props 和 context 串联。
4.2 工具链开发:在./tools/目录下编写可复用的原子工具
Paperclip 的工具必须放在./tools/目录下,每个工具一个文件夹,包含tool.json和index.js。我们为合同审查开发三个工具:
工具1:pdf-to-text(PDF 文本提取)./tools/pdf-to-text/tool.json:
{ "name": "pdf-to-text", "description": "将 PDF 文件转换为纯文本", "parameters": { "type": "object", "properties": { "file_path": { "type": "string", "description": "PDF 文件的绝对路径" } }, "required": ["file_path"] } }./tools/pdf-to-text/index.js:
const pdfParse = require('pdf-parse'); module.exports = async function ({ file_path }) { try { const dataBuffer = await require('fs').promises.readFile(file_path); const data = await pdfParse(dataBuffer); return data.text; } catch (err) { throw new Error(`PDF 解析失败: ${err.message}`); } };工具2:clause-extractor(条款提取)./tools/clause-extractor/tool.json:
{ "name": "clause-extractor", "description": "从文本中提取指定类型的法律条款", "parameters": { "type": "object", "properties": { "text": { "type": "string" }, "clause_types": { "type": "array", "items": { "type": "string" } } }, "required": ["text", "clause_types"] } }./tools/clause-extractor/index.js:
// 简化版正则提取,生产环境应替换为 NLP 模型 module.exports = async function ({ text, clause_types }) { const clauses = {}; clause_types.forEach(type => { const regex = new RegExp(`${type}[::\\s]*([^。]+[。])`, 'gi'); const matches = [...text.matchAll(regex)]; clauses[type] = matches.map(m => m[1]).join(' '); }); return clauses; };工具3:risk-assessor(风险评估)./tools/risk-assessor/tool.json:
{ "name": "risk-assessor", "description": "评估法律条款的风险等级", "parameters": { "type": "object", "properties": { "clause_text": { "type": "string" } }, "required": ["clause_text"] } }./tools/risk-assessor/index.js:
// 调用 OpenClaw 的 LLM 进行评估 const { createOpenClawClient } = require('@openclaw/client'); const openclaw = createOpenClawClient(); module.exports = async function ({ clause_text }) { const response = await openclaw.chat.completions.create({ model: 'qwen2.5-3b', messages: [{ role: 'user', content: `请评估以下法律条款的风险等级(高/中/低),并给出理由:${clause_text}` }], stream: false }); return response.choices[0].message.content; };注意:工具代码中
require的模块,必须在package.json的dependencies中声明。Paperclip 的沙箱会自动从项目node_modules中加载,不会污染全局环境。
4.3 Agent 编排:用useAgentState构建多阶段工作流
现在,我们编写主 Agent 组件ContractReviewer.tsx:
import React, { useState, useRef } from 'react'; import { useAgentState, useTool } from 'paperclip-react'; import { createOpenClawClient } from '@openclaw/client'; const openclaw = createOpenClawClient(); export function ContractReviewer() { // 主状态:存储整个审查流程的进度 const [state, setState] = useAgentState({ step: 'idle' as 'idle' | 'uploading' | 'extracting' | 'extracted' | 'assessing' | 'done', pdfFile: null as File | null, extractedText: '', clauses: {} as Record<string, string>, riskAssessments: {} as Record<string, string>, report: null as any, error: null as string | null, }); const fileInputRef = useRef<HTMLInputElement>(null); // 步骤1:文件上传 const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => { const file = e.target.files?.[0]; if (!file) return; setState({ ...state, step: 'uploading', pdfFile: file, error: null }); // 模拟文件上传到临时目录(实际项目应存到 /tmp 或专用目录) const tempPath = `/tmp/${file.name}`; // 这里应调用 Node.js 的 fs.writeFile,但为简化,我们假设文件已就绪 setTimeout(() => { setState({ ...state, step: 'extracting', pdfFile: file }); extractText(file); // 触发下一步 }, 500); }; // 步骤2:文本提取 const extractText = async (file: File) => { try { // Paperclip 的 useTool 会自动处理文件路径 const text = await useTool('pdf-to-text', { file_path: `/tmp/${file.name}` }, { client: openclaw }); setState({ ...state, step: 'extracted', extractedText: text }); // 自动触发条款提取 extractClauses(text); } catch (err) { setState({ ...state, error: `文本提取失败: ${err.message}` }); } }; // 步骤3:条款提取 const extractClauses = async (text: string) => { try { const clauses = await useTool('clause-extractor', { text, clause_types: ['违约责任', '不可抗力', '争议解决'] }, { client: openclaw }); setState({ ...state, step: 'assessing', clauses }); // 并行评估所有条款 const assessments = await Promise.all( Object.entries(clauses).map(async ([type, text]) => [ type, await useTool('risk-assessor', { clause_text: text }, { client: openclaw }) ]) ); const riskAssessments = Object.fromEntries(assessments); setState({ ...state, step: 'done', riskAssessments, report: { clauses, riskAssessments } }); } catch (err) { setState({ ...state, error: `条款处理失败: ${err.message}` }); } }; // 步骤4:生成报告(可选) const generateReport = () => { if (!state.report) return; const blob = new Blob([JSON.stringify(state.report, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'contract-review-report.json'; a.click(); }; return ( <div style={{ padding: '20px', fontFamily: 'sans-serif' }}> <h2>AI 合同审查 Agent</h2> {state.step === 'idle' && ( <div> <input type="file" accept=".pdf" ref={fileInputRef} onChange={handleFileUpload} style={{ display: 'none' }} /> <button onClick={() => fileInputRef.current?.click()}> 上传 PDF 合同 </button> </div> )} {state.step === 'uploading' && <div>正在上传文件...</div>} {state.step === 'extracting' && <div>正在提取文本...</div>} {state.step === 'extracted' && <div>文本提取完成,正在识别条款...</div>} {state.step === 'assessing' && <div>正在评估风险等级...</div>} {state.step === 'done' && ( <div> <h3>审查完成!</h3> <pre>{JSON.stringify(state.report, null, 2)}</pre> <button onClick={generateReport}>下载报告</button> </div> )} {state.error && ( <div style={{ color: 'red', marginTop: '10px' }}> 错误:{state.error} <button onClick={() => setState({...state, error: null})}>重试</button> </div> )} </div> ); } export default ContractReviewer;这个组件展示了 Paperclip 的核心优势:状态驱动的流程编排。step状态不仅用于 UI 渲染,更是整个 Agent 生命周期的控制器。每个useTool调用都发生在特定的step下,失败时error状态会自动更新,用户点击“重试”按钮,就能从当前step重新开始,无需刷新页面。这比手写一堆useState+useEffect+if/else的胶水代码,清晰了至少一个数量级。
4.4 高级特性实战:流式响应、中断控制与上下文记忆
Paperclip 的真正威力,在于它对 AI 交互模式的原生支持。我们来增强ContractReviewer,加入流式摘要和中断功能。
流式摘要:修改extractClauses函数,让