1. 项目概述:Paperclip 不是回形针,而是一个被低估的 AI Agent 开发范式
“Paperclip”这个词在开发者社区里最近频繁出现,但和办公用品毫无关系——它指的是一套轻量、可组合、面向真实业务场景的 AI Agent 构建框架,核心定位是解决当前 React + Node.js 全栈团队在落地 AI 功能时最痛的三个问题:状态不可控、链路不透明、调试像盲人摸象。我去年在给一家做智能合同审核的 SaaS 公司做技术咨询时,就亲眼见过团队用传统 LangChain 模式写了个“合同条款风险识别 Agent”,结果上线后用户反馈“有时说有风险,有时又说没问题”,日志里只有一堆 LLM 的 token 流,根本没法定位是 prompt 写错了、工具调用失败了,还是中间某个 step 的 state 被意外覆盖。Paperclip 就是为这种场景而生的:它强制把每个 Agent 的执行过程拆成可命名、可快照、可回溯的Step,每个 Step 必须声明输入 Schema 和输出 Schema,所有中间状态默认持久化到内存或 Redis,整个执行流用纯函数方式编排。这不是炫技,而是把 AI 工程从“黑盒实验”拉回到“可维护软件”的轨道上。它天然适配 React 前端的状态管理逻辑(比如用 Zustand 或 Jotai 管理 Agent 的 stepStatus),后端用 Node.js 实现轻量 runtime,整个栈完全开源、无商业闭源模块、不绑定任何大模型厂商。如果你正在面试 React 岗位,看到“手写 React Agent”这类题,Paperclip 的设计思想就是最扎实的答题素材;如果你在部署 Node.js 应用,它的 runtime 可以直接跑在 Express/Koa 中间件里,连进程管理都不用动。它不是另一个“AI 框架”,而是一套让 AI 逻辑能真正融入现有工程体系的接口规范。
2. 核心设计哲学与架构选型逻辑
2.1 为什么不用 LangChain / LlamaIndex 做基座?
这是 Paperclip 最常被问到的问题。LangChain 确实生态庞大,但它的核心抽象是Chain → Runnable → OutputParser,这个链条在简单 demo 里很顺滑,一旦进入真实业务,问题就集中爆发:Chain 是个黑盒容器,你无法在中间插入断点;Runnable 的输入输出类型是 any,TypeScript 类型推导形同虚设;OutputParser 更是灾难——一个 JSON 解析失败,整个 Chain 就崩,而且你根本不知道是哪个 step 的输出格式不对。我拿自己做过的一个“会议纪要生成 Agent”对比过:用 LangChain 实现,37 行代码,但加日志、加重试、加 fallback 之后膨胀到 156 行,且 80% 的代码都在处理错误边界和状态传递。Paperclip 的解法很朴素:把 Chain 拆成原子化的 Step,每个 Step 是一个带明确 I/O 合约的函数。比如extractActionItems这个 Step,它的 TypeScript 接口长这样:
interface ExtractActionItemsInput { meetingTranscript: string; participants: string[]; } interface ExtractActionItemsOutput { actionItems: Array<{ owner: string; task: string; dueDate?: string; }>; confidenceScore: number; } const extractActionItems: Step<ExtractActionItemsInput, ExtractActionItemsOutput> = { id: 'extract-action-items', inputSchema: z.object({ meetingTranscript: z.string(), participants: z.array(z.string()) }), outputSchema: z.object({ actionItems: z.array(z.object({ owner: z.string(), task: z.string(), dueDate: z.string().optional() })), confidenceScore: z.number().min(0).max(1) }), execute: async (input) => { // 实际调用 LLM 或本地模型 const result = await callLLM({ system: '提取会议中的待办事项,按 owner 分组...', user: input.meetingTranscript }); return parseJSONSafe(result, extractActionItems.outputSchema); } };看到没?输入输出用 Zod 做运行时校验,类型定义即文档,IDE 能自动补全,前端调用时也能用同一份 schema 做表单验证。这比 LangChain 的output_parser=JsonOutputParser()强出两个数量级。更重要的是,Paperclip 的 runtime 会在每个 Step 执行前后自动 snapshot 状态,你可以随时agent.getStateAtStep('extract-action-items')拿到那一刻的完整上下文,调试时再也不用靠 console.log 猜。
2.2 为什么选择 Node.js 作为 runtime 核心?
Paperclip 的 runtime 本质是个轻量状态机引擎,Node.js 是目前唯一能同时满足四个硬性条件的环境:
第一,事件循环模型天然匹配 Agent 的异步 Step 编排。每个 Step 的execute函数返回 Promise,runtime 用await串行调度,但允许你用Promise.all并行触发多个 Step(比如同时调用天气 API 和日历 API),这种混合调度在 Python 的 asyncio 里写起来很绕,在 Node.js 里就是几行await Promise.all([step1(), step2()])。
第二,npm 生态对前端开发者零学习成本。React 团队成员装个paperclip-runtime包,import { createAgent } from 'paperclip-runtime'就能开干,不需要额外学 Python 的 pip、虚拟环境、requirements.txt。我们团队做过测试:一个刚入职的 junior React 工程师,用 2 小时就能基于 Paperclip 写出带文件上传、LLM 调用、结果渲染的完整 Agent,而用 LangChain + FastAPI 的方案,他花了 3 天还在配 uvicorn 和 CORS。
第三,V8 引擎的内存快照能力是调试基石。Paperclip 的agent.snapshot()方法底层调用v8.serialize(),能把整个 Agent 的 state、step history、甚至 LLM 的 token 统计数据序列化成二进制 buffer,存到 Redis 或本地文件。你可以在生产环境 dump 一个出错的 Agent 实例,然后在本地agent.loadSnapshot(buffer)复现,连网络请求都 mock 掉——这在 Python 的 pickle 里做不到,因为 pickle 不保证跨版本兼容,而 V8 serialize 是 Node.js 官方保证的。
第四,与前端共享工具链。Zod schema、ESLint 规则、Jest 测试框架,前后端用同一套。我们有个validateStepInput工具函数,既用在 Node.js runtime 的入参校验,也用在 React 组件里做表单实时验证,代码复用率 100%。这点在 Python/Java 后端里永远做不到。
2.3 React 集成不是“适配”,而是“共生”
Paperclip 对 React 的支持不是简单封装个 Hook,而是深度利用 React 的并发渲染和状态同步机制。它的核心 HookuseAgent返回的不是state和dispatch,而是agentInstance和stepStatusMap:
const { agent, stepStatusMap } = useAgent({ agentId: 'meeting-summary-agent', config: { steps: [transcribeAudio, extractActionItems, generateSummary], initialState: { audioBlob: null } } }); // stepStatusMap 是一个 Map<string, StepStatus> // StepStatus = { status: 'idle' | 'running' | 'success' | 'error', // output: any, error?: Error, durationMs?: number }关键在于stepStatusMap的更新时机:它不是等整个 Agent 执行完才刷新,而是每个 Step 完成后立即触发 React 更新。这意味着你可以写出这样的 UI:
{stepStatusMap.get('transcribeAudio')?.status === 'running' && ( <div className="loading">正在语音转文字... {stepStatusMap.get('transcribeAudio')?.durationMs}ms</div> )} {stepStatusMap.get('extractActionItems')?.status === 'success' && ( <ActionItemsList items={stepStatusMap.get('extractActionItems')!.output.actionItems} /> )}这种细粒度的状态驱动,让 UI 能精确反映 Agent 的执行进度,而不是笼统的“加载中”。更绝的是,Paperclip 的agent.run()方法返回一个AbortController,你可以把它和 React 的useEffectcleanup 绑定:
useEffect(() => { const controller = agent.run(); return () => controller.abort(); // 组件卸载时自动取消 Agent 执行 }, [agent]);这解决了前端 Agent 最常见的内存泄漏问题——用户切走页面,Agent 还在后台疯狂调 LLM。而 LangChain 的chain.invoke()返回 Promise,你得自己包装一层 AbortSignal,还容易漏掉。
3. 从零搭建一个可调试的 Paperclip Agent
3.1 环境准备:Node.js 版本与依赖安装
Paperclip 对 Node.js 版本有明确要求:必须 >= 18.17.0。这不是为了用新语法,而是依赖 V8 的v8.serialize()API 在该版本才稳定支持自定义对象序列化。别信网上那些“node.js 18.20.4 lts版本下载”的教程,那个版本有已知的 crypto 模块 bug,会导致 Paperclip 的签名验证失败。我实测下来,18.19.1 是最稳的 LTS 版本,CentOS 7.9 用户尤其要注意:系统自带的 OpenSSL 版本太老,必须先升级 OpenSSL 到 1.1.1w,再用 nvm 安装 Node.js:
# CentOS 7.9 专用步骤 sudo yum install -y epel-release sudo yum install -y openssl11-devel curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash source ~/.bashrc nvm install 18.19.1 nvm use 18.19.1依赖安装分两部分:后端 runtime 和前端集成。后端只需一个包:
npm install paperclip-runtime # 注意:不要装 @langchain/core 或 llama-index,Paperclip 是独立 runtime前端 React 项目需要两个包:
npm install paperclip-react # 如果你用 Zustand 管理全局状态,再加: npm install zustand提示:Paperclip 的包名故意避开
@paperclip/xxx这种作用域命名,就是为了降低引入门槛。很多团队卡在 npm 私有 registry 权限上,而paperclip-runtime是 public package,CI/CD 流水线里npm ci一次通过。
3.2 定义第一个 Step:带 Schema 校验的文本清洗
真实业务里,第一步往往是脏数据清洗。我们以“用户提交的会议录音文本”为例,定义一个cleanTranscriptStep。重点看 Schema 如何约束业务规则:
import { z } from 'zod'; import { Step } from 'paperclip-runtime'; // 输入 Schema:强制要求 transcript 非空,且长度不超过 10000 字符 // 这个限制不是技术限制,而是业务限制——超过 10000 字的 transcript 很可能包含无关广告 const CleanTranscriptInputSchema = z.object({ transcript: z.string().min(1, 'transcript 不能为空').max(10000, 'transcript 不能超过 10000 字符'), language: z.enum(['zh-CN', 'en-US']).default('zh-CN') }); // 输出 Schema:清洗后的文本,以及清洗报告(用于审计) const CleanTranscriptOutputSchema = z.object({ cleanedText: z.string(), report: z.object({ originalLength: z.number(), cleanedLength: z.number(), removedNoiseCount: z.number(), detectedLanguage: z.string() }) }); export const cleanTranscript: Step< z.infer<typeof CleanTranscriptInputSchema>, z.infer<typeof CleanTranscriptOutputSchema> > = { id: 'clean-transcript', inputSchema: CleanTranscriptInputSchema, outputSchema: CleanTranscriptOutputSchema, execute: async (input) => { // 模拟清洗逻辑:移除重复换行、过滤乱码、标准化标点 let cleaned = input.transcript .replace(/\n{3,}/g, '\n\n') // 合并过多空行 .replace(/[\uFE00-\uFE0F\u200B-\u200F\u2028-\u202F]/g, '') // 移除零宽字符 .replace(/[。!?;:,、]+/g, (match) => match[0]); // 标点去重 // 检测语言(实际项目用 langdetect 库) const detectedLang = input.language; return { cleanedText: cleaned.trim(), report: { originalLength: input.transcript.length, cleanedLength: cleaned.length, removedNoiseCount: input.transcript.length - cleaned.length, detectedLanguage: detectedLang } }; } };这个 Step 的价值在于:Schema 不是摆设,而是第一道防线。如果前端传过来的transcript是null,Paperclip runtime 会在execute执行前就抛出ZodError,错误信息精准到字段:“transcript: 不能为空”。你不用在execute函数里写if (!input.transcript) throw new Error(...),Schema 就是契约。
3.3 编排 Agent:用 DAG 图描述业务逻辑
Paperclip 的 Agent 不是线性 Chain,而是有向无环图(DAG)。我们继续上面的例子,加上语音转文字和摘要生成:
import { createAgent } from 'paperclip-runtime'; import { cleanTranscript } from './steps/cleanTranscript'; import { transcribeAudio } from './steps/transcribeAudio'; import { generateSummary } from './steps/generateSummary'; // 定义 DAG:transcribeAudio -> cleanTranscript -> generateSummary // 注意:cleanTranscript 的输入依赖 transcribeAudio 的输出 export const meetingSummaryAgent = createAgent({ id: 'meeting-summary', steps: [ transcribeAudio, // Step 1: 输入 audioBlob,输出 transcript cleanTranscript, // Step 2: 输入 transcript(来自 transcribeAudio),输出 cleanedText generateSummary // Step 3: 输入 cleanedText,输出 summary ], // 显式声明依赖关系:cleanTranscript 的 transcript 字段来自 transcribeAudio 的 output.transcript dependencies: [ { from: 'transcribeAudio', to: 'cleanTranscript', mapping: { transcript: 'output.transcript' } }, { from: 'cleanTranscript', to: 'generateSummary', mapping: { text: 'output.cleanedText' } } ] });这里的关键是dependencies数组。Paperclip 不会自动推断字段依赖(像 LangChain 的input_variables那样容易出错),而是强制你显式声明。mapping字段用点号路径语法,output.transcript表示取transcribeAudioStep 的output对象里的transcript字段。这种写法的好处是 IDE 能跳转到定义,重构时 rename 字段会自动更新所有 mapping。
3.4 后端启动:嵌入 Express 中间件
Paperclip runtime 可以独立运行,但更推荐嵌入现有 Express 应用,复用你的鉴权、日志、监控体系:
import express from 'express'; import { PaperclipRuntime } from 'paperclip-runtime'; import { meetingSummaryAgent } from './agents/meetingSummaryAgent'; const app = express(); const runtime = new PaperclipRuntime(); // 注册 Agent runtime.registerAgent(meetingSummaryAgent); // 关键:Paperclip 的中间件自动处理 Agent 生命周期 app.use('/api/agent', runtime.expressMiddleware()); // 你自己的业务路由 app.get('/health', (req, res) => res.json({ status: 'ok' })); app.listen(3000, () => { console.log('Server running on http://localhost:3000'); });runtime.expressMiddleware()做了三件事:
- 自动解析
/api/agent/:agentId/run的 POST 请求,提取input参数; - 调用
agent.run(input),捕获所有 Step 的执行时间、错误、输出; - 返回结构化响应,包含
stepHistory数组,每个元素有id,status,output,error,durationMs。
响应体长这样:
{ "agentId": "meeting-summary", "status": "success", "stepHistory": [ { "id": "transcribeAudio", "status": "success", "output": { "transcript": "今天讨论了Q3目标..." }, "durationMs": 2340 }, { "id": "cleanTranscript", "status": "success", "output": { "cleanedText": "今天讨论了Q3目标...", "report": {...} }, "durationMs": 120 } ], "finalOutput": { "summary": "会议达成三点共识..." } }注意:Paperclip 默认不记录原始 LLM 请求内容(出于隐私考虑),但你可以在
execute函数里手动console.log({ input, output }),或者配置runtime.setLogger(console)开启详细日志。
3.5 前端集成:用 React Hook 驱动 UI
React 端的集成极其简洁。我们用paperclip-react提供的useAgentHook:
import React, { useState } from 'react'; import { useAgent } from 'paperclip-react'; import { meetingSummaryAgent } from '../agents/meetingSummaryAgent'; const MeetingSummaryPage = () => { const [audioFile, setAudioFile] = useState<File | null>(null); const { agent, stepStatusMap, run, isRunning } = useAgent({ agentId: 'meeting-summary', config: { steps: meetingSummaryAgent.steps, dependencies: meetingSummaryAgent.dependencies, initialState: { audioBlob: null } } }); const handleRun = async () => { if (!audioFile) return; // 构造 input:前端负责把 File 转成 Blob URL 或 base64 const arrayBuffer = await audioFile.arrayBuffer(); const blob = new Blob([arrayBuffer], { type: audioFile.type }); // Paperclip 的 input 必须是 plain object,不能传 File 对象 await run({ audioBlob: URL.createObjectURL(blob) }); }; return ( <div> <input type="file" accept="audio/*" onChange={(e) => setAudioFile(e.target.files?.[0] || null)} /> <button onClick={handleRun} disabled={isRunning}> {isRunning ? '正在处理...' : '生成会议摘要'} </button> {/* Step 状态驱动的 UI */} {stepStatusMap.get('transcribeAudio')?.status === 'running' && ( <div>🎙️ 正在语音转文字... {stepStatusMap.get('transcribeAudio')?.durationMs}ms</div> )} {stepStatusMap.get('cleanTranscript')?.status === 'success' && ( <div> ✅ 文本清洗完成,移除了 {stepStatusMap.get('cleanTranscript')!.output.report.removedNoiseCount} 个噪声字符 </div> )} {stepStatusMap.get('generateSummary')?.status === 'success' && ( <div> <h3>会议摘要</h3> <p>{stepStatusMap.get('generateSummary')!.output.summary}</p> </div> )} </div> ); }; export default MeetingSummaryPage;这个例子展示了 Paperclip 的核心优势:UI 状态和 Agent 执行状态完全同步。你不需要写一堆useState和useEffect去手动同步,stepStatusMap就是单一数据源。而且run()方法返回 Promise,你可以用await run(...)做串行操作,比如“先跑 Agent,成功后再调用saveToDB()”。
4. 调试、监控与生产化避坑指南
4.1 三类典型错误的定位方法
Paperclip 把错误分为三类,每类都有专属调试路径:
| 错误类型 | 触发时机 | 定位方法 | 典型原因 |
|---|---|---|---|
| Schema Validation Error | Step 执行前 | 查看stepHistory中对应 Step 的error.message,含具体字段名 | 前端传参缺失、类型错误(如传了 number 当 string) |
| Step Execution Error | execute函数内抛出 | stepHistory的error.stack,含完整调用栈 | LLM API 调用失败、网络超时、Zod parse 失败 |
| Dependency Mapping Error | Step 依赖注入时 | stepHistory中下游 Step 的error.message,提示Cannot read property 'xxx' of undefined | mapping路径写错,上游 Step 输出结构变更未同步 |
实战案例:某次上线后,generateSummaryStep 总是报Cannot read property 'cleanedText' of undefined。我们查stepHistory,发现cleanTranscriptStep 的status是success,但output是空对象{}。进一步看它的durationMs是0,说明根本没执行。再查日志,发现cleanTranscript.execute函数里有个if (input.transcript.length < 10) return { cleanedText: '', report: {...} };—— 这个 early return 绕过了 Zod outputSchema 校验,导致返回值不符合 schema。修复很简单:把return改成throw new Error('transcript too short'),让错误暴露在 execution 阶段。
4.2 生产环境必备的监控埋点
Paperclip runtime 提供了onStepStart、onStepEnd、onAgentError三个 hook,必须在生产环境启用:
runtime.onStepStart((stepId, input) => { // 上报到 Sentry,带上 stepId 和 input 的关键字段(脱敏后) Sentry.captureMessage(`Agent Step Start: ${stepId}`, { extra: { agentId: 'meeting-summary', inputSize: JSON.stringify(input).length } }); }); runtime.onStepEnd((stepId, output, durationMs, error) => { // 上报到 Prometheus,指标名:paperclip_step_duration_seconds promClient.histogram('paperclip_step_duration_seconds', { labels: { agentId: 'meeting-summary', stepId, status: error ? 'error' : 'success' } }).observe(durationMs / 1000); }); runtime.onAgentError((agentId, error) => { // 发送企业微信告警 sendAlertToWX(`Agent ${agentId} failed: ${error.message}`); });特别注意onStepStart的input参数:Paperclip 默认只传入经过 Schema 校验后的 input,所以这里的数据是干净的。你不用再做二次校验,直接上报即可。
4.3 React 面试高频考点:手写一个简易 Paperclip Hook
面试官问“手写 React Agent”时,考的不是你能不能调 API,而是你对状态同步的理解。下面是一个精简版useAgent的实现(去掉错误处理和依赖注入,聚焦核心逻辑):
import { useState, useEffect, useCallback } from 'react'; // 简化版 StepStatus 类型 type StepStatus = { status: 'idle' | 'running' | 'success' | 'error'; output?: any; error?: Error; durationMs?: number; }; // 简化版 Agent 实例 type AgentInstance = { id: string; steps: Array<{ id: string }>; run: (input: any) => Promise<void>; }; // 简化版 useAgent Hook export function useAgent(agentConfig: { agentId: string; config: { steps: Array<{ id: string }>; initialState: any } }) { const [stepStatusMap, setStepStatusMap] = useState<Map<string, StepStatus>>(new Map()); const [isRunning, setIsRunning] = useState(false); // 初始化所有 Step 状态为 idle useEffect(() => { const initialMap = new Map<string, StepStatus>(); agentConfig.config.steps.forEach(step => { initialMap.set(step.id, { status: 'idle' }); }); setStepStatusMap(initialMap); }, []); const run = useCallback(async (input: any) => { setIsRunning(true); // 模拟 Step 执行 for (const step of agentConfig.config.steps) { setStepStatusMap(prev => { const newMap = new Map(prev); newMap.set(step.id, { status: 'running' }); return newMap; }); // 模拟异步执行 await new Promise(resolve => setTimeout(resolve, 1000)); setStepStatusMap(prev => { const newMap = new Map(prev); newMap.set(step.id, { status: 'success', output: { result: `output from ${step.id}` } }); return newMap; }); } setIsRunning(false); }, []); return { agent: { id: agentConfig.agentId, run } as AgentInstance, stepStatusMap, run, isRunning }; }这个实现抓住了三个面试要点:
- 用 Map 管理多 Step 状态,避免用
useState<{ [key: string]: StepStatus }>导致不必要的 re-render; - useCallback 包裹 run,防止父组件 re-render 时生成新函数;
- useEffect 初始化状态,确保组件挂载时 Step 状态已就绪。
实操心得:面试时别一上来就写代码,先画个图解释 Paperclip 的状态流:“Agent 启动 → 每个 Step 独立状态 → UI 用 stepStatusMap 驱动 → 状态更新是原子的”。这比写一百行代码更能体现架构思维。
4.4 Node.js 部署的五个致命陷阱
陷阱:在 PM2 cluster 模式下共享内存状态
Paperclip 的默认内存存储是Map,在 PM2 cluster 下每个 worker 进程有独立 Map,导致agent.getStateAtStep()查不到其他 worker 的状态。解决方案:改用 Redis 存储,new PaperclipRuntime({ storage: new RedisStorage() })。陷阱:未设置
NODE_OPTIONS=--max-old-space-size=4096
大文本处理(如 50MB 的会议 transcript)会快速耗尽 V8 堆内存。Node.js 默认 1.4GB,必须显式扩大。在ecosystem.config.js里加:module.exports = { apps: [{ name: 'paperclip', script: './server.js', node_args: '--max-old-space-size=4096' }] };陷阱:Express 的
body-parser限制了文件大小transcribeAudioStep 需要接收音频文件,但 Express 默认 limit 是 100KB。必须在body-parser前加:app.use(express.raw({ type: 'audio/*', limit: '50mb' })); app.use(express.json({ limit: '10mb' }));陷阱:未禁用 Node.js 的
--inspect在生产环境--inspect会开启调试端口,暴露 V8 引擎,是严重安全风险。PM2 启动时加--no-autorestart和--no-daemon参数,并确认NODE_OPTIONS不含--inspect。陷阱:前端用
URL.createObjectURL()造成内存泄漏audioBlob: URL.createObjectURL(blob)创建的 URL 不会自动释放。必须在 Agent 完成后手动URL.revokeObjectURL():useEffect(() => { return () => { if (audioUrl) URL.revokeObjectURL(audioUrl); }; }, [audioUrl]);
5. 从 Paperclip 到 AI 工程化的认知跃迁
Paperclip 的价值远不止于一个库。它代表了一种 AI 工程实践的范式转移:从“模型为中心”转向“工作流为中心”。过去我们花 80% 时间调 prompt、换模型、测 temperature,现在这些只是 Step 的一个实现细节;真正的精力应该放在定义清晰的 Step 边界、设计可验证的 I/O Schema、构建可回溯的执行链路上。我在给客户做培训时,总让他们先画一张纸:左边写“用户要什么”,右边写“系统最终交付什么”,中间用箭头画出必须经过的 3-5 个 Step,每个 Step 下面标注“谁负责”、“输入是什么”、“输出怎么验证”。这张纸的价值,远超写一千行代码。
这种思维也重塑了团队协作。以前前端抱怨“后端给的 API 字段名不一致”,现在大家围着CleanTranscriptOutputSchema讨论:“removedNoiseCount这个字段名够不够直白?要不要改成noiseCharactersRemoved?”——Schema 就是契约,是前后端唯一的真相源。我们团队已经把所有 Step 的 Zod Schema 提交到 Git,PR 检查强制要求zod-to-json-schema生成的 JSON Schema 必须和 Swagger 文档一致。
最后分享一个真实案例:某金融客户用 Paperclip 实现“贷款申请风控 Agent”,包含 7 个 Step(身份核验、征信查询、收入验证、反欺诈扫描、额度计算、利率匹配、合同生成)。上线三个月,他们没改过一行 LLM 相关代码,但通过调整Step的执行顺序(比如把反欺诈扫描提前到征信查询前)和dependencies的 mapping(把征信报告里的逾期次数直接映射到反欺诈模型的输入),将审批通过率提升了 12%。这印证了一个朴素真理:AI 的价值不在模型本身,而在如何把模型编织进业务流程的经纬线里。Paperclip 不是银弹,但它给了你一把可靠的尺子,去丈量每一次 AI 尝试离真实业务还有多远。