1. 项目概述这不是一场“炼丹”修行而是一次工程化思维的迁移“Agent 开发不是模型训练”——这句话我第一次在某前端技术沙龙上听到时下意识摸了摸自己的键盘心想这不就是我们每天写 React 组件、调 API、处理状态、埋监控脚本的活儿吗怎么突然就和“模型训练”扯上关系了后来连续三个月泡在几个开源 Agent 框架源码里、带两个前端团队落地了三类轻量级业务 Agent客服意图路由、内部知识库问答助手、低代码表单生成引导才真正把这句话嚼碎了咽下去前端做 Agent核心动作不是调参、不是跑 epoch、不是画 loss 曲线而是设计决策流、编排工具链、兜住异常边界、把大模型的“不确定性”翻译成用户可感知的“确定性体验”。这个认知转变直接决定了你第一天是去配 PyTorch 环境还是去拆解一个按钮点击后到底触发了几层函数调用、中间哪些环节该加 loading、哪些地方必须 fallback 到静态文案。关键词“Agent 开发”“前端入门”“模型训练”背后藏着的是两类完全不同的工作范式一边是数据科学家在 Jupyter 里反复调试 prompt 和 temperature一边是前端工程师在 Chrome DevTools 里逐帧分析 LLM 流式响应的 chunk 间隔、重试逻辑是否触发、错误提示是否友好。前者追求“输出更准”后者追求“过程可控、失败可感、体验连贯”。适合谁看如果你是写了三年 Vue/React、能手写 Webpack 插件、但看到“LoRA 微调”“RLHF”就头皮发紧的前端如果你已经用过 LangChain.js 或 LlamaIndex但每次卡在“为什么 response 一直 pending 不出来”“为什么 tool call 返回的参数格式总对不上”“为什么 history 一长页面就卡死”如果你的老板说“咱们也搞个 AI 助手”而你第一反应是打开 HuggingFace 找模型而不是画状态机图——那这篇就是为你写的。它不教你怎么训一个 7B 模型但会告诉你当用户问“帮我查下上个月销售报表”你的代码里究竟该有几条 if 分支、几个 await、几处 try-catch以及为什么第 3 个 await 后面必须加一个 800ms 的 debounce。我见过太多前端同学一头扎进 Agent 开发先花两周配 CUDA、下载 Qwen2-7B-Int4、跑通一个本地 Ollama demo结果上线后发现用户提问带错别字Agent 直接返回空网络抖动一次整个对话流就断在半路历史记录超过 5 轮内存暴涨 300MB。问题根本不在模型大小而在前端没把 Agent 当成一个“需要被精心养护的异步服务”而当成一个“会自己思考的黑盒 API”。这篇文章就是帮你把那个黑盒一层层剥开露出里面全是 Promise、EventEmitter、AbortController 和 carefully crafted state machines。2. 核心思路拆解为什么前端做 Agent 必须放弃“训练思维”转向“系统编排思维”2.1 本质差异训练是优化参数空间Agent 是调度执行空间模型训练的本质是在高维参数空间里通过梯度下降寻找一组能让 loss 最小的权重。它关注的是统计规律、分布拟合、泛化能力。而 Agent 的本质是一个运行时决策系统它接收用户输入文本/语音/图像结合当前上下文history、memory、tool output调用外部能力API、数据库、文件系统生成下一步动作reply / tool_call / ask_for_clarification并持续迭代直到目标达成。这个过程不产生新参数只消耗 CPU 时间、网络带宽和开发者的心智带宽。举个具体例子训练场景你给一个模型喂 10 万条“用户问天气 → 返回城市温度湿度”的样本目标是让模型学会从任意问法中提取城市名。你调 learning_rate、batch_size、warmup_steps看 validation loss 下降曲线。Agent 场景用户问“北京今天热不热”你的 Agent 需要识别出实体“北京”可用轻量 NER 模型或正则判断意图是“查天气”可走规则 or 小分类模型调用天气 API需处理鉴权、限流、超时解析 API 返回 JSON字段可能缺失、格式可能变更把结果组织成自然语言回复“北京今天 28°C体感较热建议穿短袖”同时更新本地 history state为下一轮对话准备上下文。提示这里没有一个步骤需要反向传播。所有“智能”都来自你对流程的拆解精度、对异常的预判粒度、对用户体验的打磨深度。模型只是你调度链路上的一个“工具调用器”不是你伺候的“主子”。2.2 前端的独特优势离用户最近天然承担“确定性守门人”角色后端做 Agent强在算力、存储、长连接管理AI 工程师做 Agent强在模型理解、prompt 工程、评估体系。而前端的优势是对用户行为的毫秒级感知、对 UI 状态的绝对控制、对失败反馈的即时呈现。这恰恰是 Agent 体验的生死线。比如一个典型失败场景用户问“上季度销售额是多少”Agent 调用 BI 系统 API但该 API 因权限配置问题返回 403。如果由后端统一 catch 403 并返回 generic error“服务暂时不可用”用户一脸懵如果由前端捕获检测到 status 403 error code “PERMISSION_DENIED”立刻在 UI 上显示“您暂无查看销售数据的权限已自动为您转接管理员”并弹出快捷申请入口。这个“确定性转化”只有前端能完成。再比如流式响应。大模型输出是字符流但用户需要的是语义块。前端必须做缓冲 chunk按标点/换行符/语义停顿如“。”“”“”做分段每段渲染前校验是否包含敏感词避免未过滤的 raw output 直出若某段超 2 秒无新 chunk主动插入“正在思考…”提示并启动后台健康检查用户中途关闭页面需 abort fetch 并清理 memory cache防止内存泄漏。这些都不是模型能教会你的是你写过 50 个 loading 组件、处理过 200 种 HTTP 错误码后肌肉记忆般的工程直觉。2.3 方案选型逻辑为什么推荐从 LangChain.js Vercel AI SDK 入手而非自己造轮子市面上 Agent 框架很多LangChain.js、LlamaIndex、HuggingFace Transformers.js、甚至直接 fetch OpenAI API。我的实测结论是对前端入门者LangChain.js Vercel AI SDK 是目前最平滑的组合。原因有三第一心智模型匹配度最高。LangChain.js 的核心概念Chain、Tool、Memory、Callback和前端开发者的日常抽象高度一致Chain ≈ React 组件树可嵌套、可复用、有输入输出Tool ≈ 自定义 Hook封装副作用暴露明确的 input/output interfaceMemory ≈ useState useEffect管理跨请求的状态但需自己 handle 序列化Callback ≈ useEffect 的 deps 数组监听特定事件触发 UI 更新。你不需要理解什么是“retriever”或“document loader”先用createOpenAIChatModel和createStructuredOutputTool跑通一个“查天气”链比啃完 LangChain 官方文档快 3 倍。第二Vercel AI SDK 天然解决前端最痛的三个问题流式响应的 chunk 解析useChathook 内置分段逻辑前后端消息同步自动序列化 history 到_vercel_aicookie中断/重试/错误状态管理isLoading,error,stop方法开箱即用。我对比过纯 fetch ReadableStream 手动解析光是处理 Safari 对text/event-stream的兼容性 bug 就花了两天。Vercel SDK 把这些全包了。第三生态成熟踩坑成本低。LangChain.js 的langchain/core包已稳定支持 ESM可直接在 Next.js App Router 中 importVercel AI SDK 的ai包提供 TypeScript 类型定义ChatMessage接口清晰标注role: user | assistant | system | tool连tool_calls字段的 shape 都给你定义好了。你不需要自己写zodschema 去校验 LLM 返回的 JSON框架已经帮你做了。注意不要被“LangChain 太重”这种说法带偏。它的“重”在于概念多但每个模块都是独立的。你可以只用Tool和ChatModel完全不用Retriever或DocumentLoader。就像你用 React不一定非要用 Redux。3. 核心细节解析与实操要点从零搭建一个“会议纪要生成 Agent”的关键环节3.1 明确边界什么该前端做什么必须后端兜底这是新手最容易越界的雷区。我带的第一个前端 Agent 项目同学坚持要把 PDF 解析、语音转文字、会议内容摘要全部放在浏览器里做。结果Chrome 内存飙到 2GBSafari 直接崩溃用户等了 47 秒才看到第一行字。后来我们重新划界功能前端职责后端必须承担职责理由说明用户输入接收录音/上传文件/粘贴文本做基础校验大小、格式—前端可拦截明显非法输入减少无效请求语音转文字ASR调用 Web Speech API仅 Chrome或上传至后端提供 ASR 服务返回结构化文本浏览器 ASR 准确率低、无标点、不支持长音频后端可集成 Whisper 等专业模型PDF/Word 解析仅做文件读取FileReader不解析内容提取文本、保留标题层级、处理表格/图片PDF.js 解析复杂文档易出错Word 解析需依赖后端 lib如 mammoth.js server-sideLLM 调用发起请求、处理流式响应、管理 AbortController提供模型路由、prompt 注入、安全过滤、token 限流前端暴露 API Key 极危险大模型响应不稳定需后端做重试/降级/缓存会议纪要生成渲染最终结果、提供编辑/导出/分享功能执行核心生成逻辑调用 LLM 工具链生成逻辑涉及敏感数据会议内容、需审计日志、需统一风控策略这个边界不是技术限制而是工程责任划分。前端的核心价值是“让用户感觉一切丝滑”而不是“证明自己能跑通一个模型”。把重活、脏活、有安全风险的活交给后端你才能专注在如何让加载动画不突兀、如何在 3 秒无响应时优雅降级、如何把一段 2000 字的纪要折叠成可展开的要点列表。3.2 Tool 设计前端如何定义一个安全、可靠、可测试的“工具”在 Agent 语境里“Tool” 不是模型调用的 API而是你封装好的、具备明确契约的业务函数。一个合格的前端 Tool必须满足输入强校验、输出可预测、失败有兜底、调用可追踪。以“查询公司内部通讯录”为例我们定义了一个searchEmployeeTool// tools/searchEmployee.ts import { z } from zod; import { Tool } from langchain/core/tools; // 1. 输入 Schema强制约束拒绝模糊查询 const SearchEmployeeInputSchema z.object({ name: z.string().min(2, 姓名至少2个字符).max(20, 姓名最多20个字符), department: z.string().optional(), }); // 2. Tool 实现所有副作用封装在此前端只暴露这个函数 export const searchEmployeeTool new Tool({ name: search_employee, description: 根据姓名和部门查询员工信息返回工号、职位、邮箱、电话, // 输入 Schema 用于 LLM 理解也用于运行时校验 schema: SearchEmployeeInputSchema, // 核心逻辑前端发起 fetch但绝不暴露后端地址 async invoke(input: z.infertypeof SearchEmployeeInputSchema) { try { // 使用 AbortController 支持取消 const controller new AbortController(); const timeoutId setTimeout(() controller.abort(), 8000); const res await fetch(/api/tools/employee-search, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(input), signal: controller.signal, }); clearTimeout(timeoutId); if (!res.ok) { throw new Error(HTTP ${res.status}: ${res.statusText}); } const data await res.json(); // 3. 输出强校验确保 LLM 调用后拿到的是干净数据 return z.array(z.object({ id: z.string(), name: z.string(), position: z.string(), email: z.string().email(), phone: z.string().regex(/^1[3-9]\d{9}$/, 手机号格式错误), })).parse(data); } catch (error) { // 4. 失败兜底返回结构化错误让 LLM 可以理解并重试 return { error: error instanceof Error ? error.message : 查询失败请稍后重试, suggestion: 请确认姓名拼写是否正确或尝试输入部门名称缩小范围 }; } } });这个 Tool 的设计要点Schema 即契约Zod Schema 不仅用于 LLM 理解更是运行时校验的铁闸。如果后端返回的邮箱不是标准格式z.parse()会直接 throw不会让脏数据流入后续流程。超时与取消双保险AbortController防止请求挂起setTimeout防止 controller.signal 未触发的极端情况。错误结构化返回的error和suggestion字段是专门给 LLM 看的。当 LLM 收到这个结构它可以自然地说“抱歉没查到张三的信息建议您确认下姓名拼写”。绝不暴露后端细节URL/api/tools/employee-search是前端代理层真实后端地址如https://hr-api.internal/search被 Nginx 或 Next.js middleware 隐藏。实操心得我最初犯的错是把 Tool 写成纯前端 fetch结果被安全团队叫停——因为 API Key 被硬编码在 JS 里。后来全部改成走/api/tools/*代理前端只管调用后端负责鉴权和审计。这个教训值 3 天加班。3.3 Memory 管理前端如何安全、高效地维护对话历史Agent 的“记忆”不是数据库而是一个需要被严格序列化、压缩、校验的 JSON 对象数组。前端管理 Memory核心矛盾是既要保证 history 足够长以支撑复杂推理又要防止内存爆炸和 token 超限。我们采用三级 Memory 策略第一级UI 层 Memory实时、轻量存储在 React state 中仅保存当前会话的最新 5 条消息user assistant 交替每条消息包含idUUID、role、content、timestamp、tool_calls?、tool_results?渲染时直接映射无额外计算开销用户点击“清空对话”只清空这一层。第二级Session Storage Memory持久、中等长度使用sessionStorage存储完整 history最大 20 条写入前进行深度清理移除tool_results中的二进制数据如 base64 图片截断content超过 500 字符的部分末尾加...已截断将时间戳转为相对时间如2分钟前避免存储冗余字符串页面刷新后自动恢复保障体验连续性。第三级后端 Memory长期、结构化当用户开启“长期记忆”开关前端将 session storage 中的 history 加密后POST 到/api/memory/save后端存储在 Redis设置 TTL 30 天并打上用户 ID 和设备指纹标签下次登录时前端拉取最近 3 次会话摘要非完整记录用于初始化 context。这个设计的关键经验永远不要相信JSON.stringify(history)是安全的。我们曾遇到过用户上传了一张 5MB 的 PNG前端用fileToBase64转成字符串塞进 history导致sessionStorage.setItem直接报QuotaExceededError。后来强制规定所有tool_results中的文件类数据只存 URL 或 ID原始文件走 CDN。注意Vercel AI SDK 的useChat默认使用 cookie 存储 history但 cookie 有 4KB 限制。我们改用sessionStorage 自定义onSetMessage回调完全接管存储逻辑。代码就 12 行却避开了 80% 的内存相关 bug。4. 实操过程与核心环节实现手把手搭建“会议纪要生成 Agent”全流程4.1 环境准备与依赖安装Next.js 14 App Router Vercel AI SDK我们选择 Next.js 14App Router作为基础框架因为它原生支持 Server Components、Streaming、Suspense完美契合 Agent 的流式响应需求。以下是精简后的package.json核心依赖{ dependencies: { next: 14.2.4, react: 18.3.1, react-dom: 18.3.1, vercel/ai: 3.1.0, // Vercel AI SDK核心 langchain/core: 0.3.1, // LangChain.js 核心 langchain/openai: 0.3.1, // 如需调用 OpenAI zod: 3.22.4 // Schema 校验 }, devDependencies: { types/node: 20.14.2, typescript: 5.4.5 } }创建项目后第一步是初始化 AI SDK。在app/layout.tsx中添加全局 Provider// app/layout.tsx import { AIProvider } from ai/react; // 注意不是 vercel/ai是 ai/react export default function RootLayout({ children, }: Readonly{ children: React.ReactNode }) { return ( html langzh-CN body {/* AIProvider 必须包裹整个应用它管理全局 chat history */} AIProvider // 配置 endpoint指向你的后端 API initialAIState{{ messages: [] }} {children} /AIProvider /body /html ); }关键点AIProvider的initialAIState是初始空状态实际 history 由后端/api/chat/route.ts管理。前端不维护任何初始数据所有状态都来自服务端确保一致性。4.2 后端 Chat Route 实现Next.js Server Action 的最佳实践Agent 的核心逻辑必须在后端。我们在app/api/chat/route.ts中实现// app/api/chat/route.ts import { type Message, StreamingTextResponse, streamText } from ai; import { createOpenAI } from ai-sdk/openai; import { searchEmployeeTool } from /tools/searchEmployee; import { getMeetingSummaryTool } from /tools/meetingSummary; // 1. 创建模型实例注意API Key 从环境变量读取绝不硬编码 const openai createOpenAI({ apiKey: process.env.OPENAI_API_KEY || , }); // 2. 定义 Tools 数组所有 Tool 必须有唯一 name const tools [searchEmployeeTool, getMeetingSummaryTool]; // 3. 主处理函数接收前端 POST 的 { messages, data } export async function POST(req: Request) { const { messages, data } await req.json(); // 4. 构建 LangChain Chain这里是最关键的编排逻辑 const result await streamText({ model: openai(gpt-4o-mini), // 选用轻量模型平衡速度与效果 messages, tools, // 5. Tool 调用后自动执行对应函数并将结果注入下一轮 toolChoice: auto, // 6. 流式响应前端 useChat 会自动处理 }); // 7. 将 LangChain 的 Stream 封装为 Vercel AI SDK 格式 return new StreamingTextResponse(result.toAIStream()); }这个route.ts的精妙之处在于streamText是 LangChain.js 的流式接口它会自动处理tool_calls→invoke tool→inject result→continue generation的完整循环result.toAIStream()是 Vercel SDK 的适配器把 LangChain 的AsyncIterable转成标准ReadableStream前端useChat无需任何修改即可消费所有敏感操作API Key、tool 调用都在服务端前端只传messages数组完全解耦。4.3 前端 Chat UI 实现useChat 自定义 Tool Handling在app/chat/page.tsx中我们构建 UI// app/chat/page.tsx use client; import { useChat } from ai/react; import { useState, useRef, useEffect } from react; import { searchEmployeeTool } from /tools/searchEmployee; export default function ChatPage() { const { messages, input, handleInputChange, handleSubmit, isLoading, stop } useChat({ api: /api/chat, // 指向上面的 route.ts }); // 1. 状态管理记录当前是否在执行 tool call const [isToolExecuting, setIsToolExecuting] useState(false); const toolCallRef useRef{ name: string; args: any } | null(null); // 2. 自定义 handleSubmit在发送前做预处理 const onSubmit (e: React.FormEvent) { e.preventDefault(); // 检查输入是否为空或纯空格 if (!input.trim()) return; // 添加用户消息 handleSubmit(e); }; // 3. 监听 messages 变化检测 tool_calls useEffect(() { if (messages.length 0) return; const lastMsg messages[messages.length - 1]; // 当 LLM 返回 tool_calls 时前端需要手动触发 tool 执行 if (lastMsg.role assistant lastMsg.tool_calls?.length) { const toolCall lastMsg.tool_calls[0]; toolCallRef.current { name: toolCall.name, args: toolCall.args }; setIsToolExecuting(true); // 4. 执行 Tool这里调用的是前端定义的 searchEmployeeTool.invoke() searchEmployeeTool.invoke(toolCall.args) .then((result) { // 5. 将 tool result 作为新消息提交触发下一轮 LLM 生成 // 注意必须用 useChat 的 append 方法不能直接 setState // 这里简化实际应封装成通用 handler }) .catch(err { console.error(Tool execution failed:, err); }) .finally(() { setIsToolExecuting(false); toolCallRef.current null; }); } }, [messages]); return ( div classNameflex flex-col h-screen max-w-4xl mx-auto p-4 h1 classNametext-2xl font-bold mb-4会议纪要生成助手/h1 {/* 消息列表 */} div classNameflex-1 overflow-y-auto mb-4 space-y-4 {messages.map((m) ( div key{m.id} className{flex ${m.role user ? justify-end : justify-start}} div className{max-w-[80%] rounded-lg px-4 py-2 ${ m.role user ? bg-blue-500 text-white rounded-tr-none : bg-gray-100 text-gray-800 rounded-tl-none }} {m.content} {/* 渲染 tool_calls 结果 */} {m.tool_results ( div classNamemt-2 text-sm text-gray-600 strong调用结果/strong {JSON.stringify(m.tool_results)} /div )} /div /div ))} {/* 加载状态 */} {isLoading ( div classNameflex justify-start div classNamebg-gray-100 text-gray-800 rounded-lg rounded-tl-none px-4 py-2 正在思考中... /div /div )} /div {/* 输入框 */} form onSubmit{onSubmit} classNameflex gap-2 input value{input} onChange{handleInputChange} placeholder例如生成上周三产品部会议的纪要 classNameflex-1 border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 / button typesubmit disabled{isLoading || isToolExecuting} className{px-6 py-2 rounded-lg font-medium ${ isLoading || isToolExecuting ? bg-gray-300 cursor-not-allowed : bg-blue-500 text-white hover:bg-blue-600 }} {isLoading || isToolExecuting ? 处理中... : 发送} /button /form /div ); }这段代码展示了前端 Agent 开发的精髓useChat管理基础流式通信但不接管业务逻辑useEffect监听 messages当检测到tool_calls立即触发前端 Tool 执行Tool 执行结果作为新消息 append形成闭环UI 状态loading、disabled精准反映底层状态用户永远知道系统在做什么。实操心得useChat的messages是 immutable 的你不能messages.push()。必须用append方法。我第一次写的时候直接 mutate state导致 UI 不更新debug 了 3 小时才发现是 React 的 re-render 规则问题。记住Agent 的 state 是神圣的只能通过框架提供的方法修改。4.4 Prompt 工程实战如何用 System Message 引导 LLM 成为“靠谱同事”很多人以为 Prompt 就是写几句话。错。System Message 是 Agent 的“宪法”它定义了角色、边界、行为准则。我们为会议纪要 Agent 设计的 System Message 如下已脱敏你是一名专业的会议纪要助理服务于某科技公司。你的任务是 1. 严格基于用户提供的会议原始记录文本/ASR 结果生成纪要绝不虚构事实、不添加个人推测 2. 纪要必须包含【时间】【地点】【主持人】【参会人】【议题】【决议事项】【待办事项含负责人截止时间】 3. 待办事项必须从原文中明确提取格式为“- [事项]负责人张三截止2024-06-30” 4. 如果原文缺少关键信息如无明确负责人必须标注“【待确认】” 5. 语言简洁正式禁用口语化表达如“咱们”“我觉得”禁用 emoji 6. 如果用户要求生成 PPT 大纲需额外输出“PPT 结构建议”章节列出 3-5 页核心内容。 现在开始等待用户提供会议记录。这个 Prompt 的设计逻辑用数字编号明确行为清单比段落描述更易被 LLM 解析强调“绝不虚构”这是企业级 Agent 的生命线定义待办事项的强制格式确保前端能用正则提取负责人和截止时间用于后续自动创建 Jira ticket预留扩展点PPT 大纲为后续功能迭代留白最后一句“现在开始…”是 trigger避免 LLM 在首轮就自我介绍。我们做过 A/B 测试用这个 Prompt待办事项提取准确率从 62% 提升到 94%。关键不是模型更强而是指令更“像在指挥一个真人同事”。5. 常见问题与排查技巧实录前端做 Agent 必踩的 7 个坑及解决方案5.1 问题速查表高频故障现象与根因定位故障现象可能根因排查命令/方法解决方案页面卡死CPU 占用 100%messages数组过大useChat渲染时深度遍历耗时或 Tool 返回巨量数据未截断console.log(messages.length)performance.memory查看内存占用限制 history 长度如 max 20Tool 返回前用JSON.stringify(data).slice(0, 5000)截断流式响应中断只显示前 2 行后端streamText未正确调用toAIStream()或前端useChat版本不匹配检查route.ts是否返回new StreamingTextResponse(...)npm list vercel/ai确认版本升级vercel/ai到 3.1.0确保route.ts返回类型为StreamingTextResponseTool 调用后无响应控制台无报错tool_calls字段名不匹配如 LLM 返回tool_calls但前端期待function_callconsole.log(lastMsg)查看实际返回结构检查 Tool 的name是否与 LLM 返回一致统一使用 LangChain.js 的tool_calls标准在 Tool 定义中显式指定name字段中文乱码显示为 符号后端StreamingTextResponse未设置正确的Content-Typeheadercurl -v http://localhost:3000/api/chat查看响应头在route.ts中添加headers: { Content-Type: text/plain; charsetutf-8 }多次点击发送生成重复消息handleSubmit未禁用按钮且未做防抖或useChat的append被多次调用检查按钮disabled状态console.log(append called)在 append 前加日志提交后立即setIsLoading(true)useChat的handleSubmit内置防抖无需额外处理移动端键盘弹出后遮挡输入框useChat的inputref 未绑定到实际 input 元素或 CSS 未适配 viewportconsole.log(inputRef.current)检查input元素是否有ref{inputRef}使用useRef绑定 input添加scrollIntoView({ behavior: smooth })到 submit 处History 丢失刷新后回到初始状态AIProvider未配置initialAIState或后端/api/chat未正确返回 history检查layout.tsx中AIProvider的 propscurl -X POST http://localhost:3000/api/chat -d {}确保route.ts的POST处理函数返回包含messages的完整对象AIProvider可省略initialAIState5.2 独家避坑技巧那些文档里不会写的实战经验技巧 1用console.time精准定位性能瓶颈Agent 的慢90% 不在模型而在前端。我在useEffect中加入useEffect(() { console.time(Tool Execution); if (lastMsg.tool_calls?.length) { searchEmployeeTool.invoke(...).finally(() console.timeEnd(Tool Execution)); } }, [lastMsg]);结果发现fetch耗时 120ms但z.parse()耗时 800ms因为后端返回了 500 条员工数据。解决方案后端加limit10参数前端 Tool 的schema也改为z.array(...).max(10)。**技巧 2