首页 / 资讯中心 / 文章详情

前端转Agent开发:从认知转变到最小实现

前端转Agent开发:从认知转变到最小实现 ★ FEATURED ARTICLE
我最近把“前端能不能做 Agent”当成一个严肃问题想了很久最后得出的结论很明确能而且前端之前积累的那套思维方式恰恰是做 Agent 最缺的东西。很多后端和算法背景的人困在“模型能力”里前端反而擅长把不稳定的系统接成一个可用的产品。这篇文章想聊的就是我从“以为要学模型训练”到“原来是在写一种新的前端”的完整认知转变也会给出一个可以直接动手的最小 Agent 实现适合第一次接触 Agent 开发、但已经写过 React/Vue 的前端同学参考。Agent 开发不是模型训练这句话是我踩了很多坑之后才真正理解的。刚接触 Agent 时我下意识觉得这是算法工程师的活需要懂梯度、懂微调、懂 GPU 显存。实际做下来发现大多数项目根本不需要碰训练真正消耗精力的是接口封装、状态管理、异常处理、上下文窗口控制这些全是前端每天都在处理的问题。下面我会把它们一个个对应起来讲尽量让有前端基础的人能少走弯路。1. 认知的十字路口Agent 开发到底在做什么1.1 训练模型和开发 Agent 是两套完全不同的工作先给“训练模型”画一个准确画像训练是拿大量数据通过反向传播不断调整模型内部参数让模型从统计规律中学会某种能力。这个过程涉及数据清洗、算力调度、损失函数设计、评估集划分结果是一个被你“调教”过的权重文件。它像研究材料科学核心工作是改变材料本身的物理特性。Agent 开发完全不同。Agent 不改变模型的任何参数它只是在模型能力之上搭建一套运行逻辑我给模型一段指令它产出文本或调用工具我把结果喂回去再产出一轮新的内容直到完成目标。你把模型换成任何一个支持对话接口的 LLMAgent 的代码结构基本不用动。所以 Agent 开发更像开餐厅厨师模型的厨艺是固定的你要做的不是教厨师做新菜而是设计菜单、安排传菜路线、处理客人退菜、控制每道菜的上菜顺序和时间。菜单是提示词传菜路线是工具调用退菜逻辑是异常处理上菜节奏是编排策略。这个类比解释了为什么前端能快速上手。一个 Agent 系统的核心复杂度不在“模型有多聪明”而在“模型、工具、记忆、用户目标之间怎么协作”。协作问题是工程问题工程问题恰好是前端每天在解决的问题。你担心 DOM 更新顺序、担心接口并发、担心组件卸载后还有异步回调这些对系统不稳定性的敏感在 Agent 开发里就是最大的优势。1.2 前端技能迁移过去并不是重新学而是换个战场把前端技能平移到 Agent 开发可以从这几个维度看组件化思想对应工具拆分。前端把一个复杂的登录页拆成表单、校验、按钮、请求多个组件Agent 也是把一个复杂任务拆成多个工具比如查库存、算价格、生成订单。工具就是 Agent 的“函数组件”输入输出协议就是 props。异步编程对应 Agent 的等待模型调用。Agent 每一步都是在等网络返回超时、重试、竞态处理跟 axios 请求封装没有本质区别。状态管理对应上下文管理。React 里你会思考哪些状态该放全局、哪些放局部Agent 里你要思考哪些信息该留在上下文窗口里、哪些该压缩成摘要、哪些该丢到长期记忆里。错误边界对应护栏设计。前端用 ErrorBoundary 兜住渲染错误Agent 需要兜住模型输出格式错误、工具调用参数错误、循环不终止。这不是硬凑类比是同一个思维模式换了个壳。我在实际做第一个 Agent 时发现自己无意识地在用“接口设计”的思路设计提示词明确职责、控制输入、约束输出。后来才发现这就是前端做组件设计时一直在做的事。1.3 一个足够准确的心智模型Agent 是一个带循环的状态机如果非要用一个词概括 Agent 的核心结构我会选“状态机”。前端写过复杂表单的人都知道状态机能让混乱的交互流变得可预测。Agent 也一样它有一个状态当前的消息历史、已完成的目标、剩余步骤根据当前状态决定下一步动作调用模型、执行工具、结束任务再根据结果更新状态。这个状态机不是数学概念它就是你代码里的一个 while 循环。模型每次只决定“下一步该做什么”工具执行结果改变环境循环再次进入模型。所以 Agent 开发从头到尾都是在写这个循环什么时候该结束、什么时候必须调用工具、模型输出不符合预期时怎么回退。理解了这一点再看后面所有框架和工具本质都是在帮你更好地管理这个循环。2. 一个 Agent 的四块积木用前端概念就能吃透2.1 LLM 调用不过是一次普通的异步请求很多前端把 LLM 调用想得太神秘其实它就是一个标准的 HTTP POST 请求。你往接口发一个消息数组它返回一个回复完事。和请求普通后端接口唯一的区别是接口会读你传进去的整个消息历史来决定怎么回答。一次最简单的调用长这样const response await fetch(https://api.example-llm.com/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${process.env.LLM_API_KEY} }, body: JSON.stringify({ model: base-chat, messages: [ { role: system, content: 你是一个帮助用户安排日程的助手。 }, { role: user, content: 明天下午三点安排一个产品评审会 } ] }) }); const data await response.json(); console.log(data.choices[0].message.content);做过 api 封装的同学看到这个结构应该很亲切。messages 数组就是 request bodychoices[0].message 就是 response.data。区别只在请求是无状态的你必须把历史消息都带上模型才能记得你们之前聊了什么。这很像你把整个表单的所有字段一次性提交给后端而不是只提交最后一个输入框。所以第一步就是把“调用 LLM”封装成一个你熟悉的 apiClient。超时、重试、错误码映射、日志记录全都可以复用之前调后端接口的经验。你唯一要适应的是响应不一定是合法 JSON需要做校验和兜底。2.2 工具调用JSON Schema 版的接口联调Agent 和普通聊天机器人最大的区别就是它能在对话过程中调用工具。怎么实现的不是模型偷偷长了手而是你把工具的描述和参数格式发给模型让模型输出一个“我想调用这个函数”的结构化结果。这一块对前端最友好因为它本质上是在做接口联调。你定义一个工具的时候就是在写一份轻量级的接口协议[ { type: function, function: { name: get_weather, description: 查询一个城市当前的天气情况, parameters: { type: object, properties: { city: { type: string, description: 城市名称比如杭州 } }, required: [city] } } } ]把这份工具描述放进请求里模型在需要时会返回一个 tool_calls 字段{ choices: [ { message: { role: assistant, content: null, tool_calls: [ { id: call_123, type: function, function: { name: get_weather, arguments: {\city\: \杭州\} } } ] } } ] }你的代码要做的事情很明确看到 tool_calls 就解析根据函数名找到本地实现执行然后把执行结果作为一个新的消息塞回对话里。这就是前端的接口调用封装收到响应校验参数调用本地函数回传结果。唯一要注意的是 arguments 是字符串需要 JSON.parse而且模型偶尔会输出不合法 JSON这就要加 try/catch。2.3 记忆与上下文管理状态管理的老朋友前端开发里你绝对不会把整个应用的所有状态都放在一个永不清理的内存对象里该持久化的持久化、该缓存失效的失效、该清理的清理。Agent 的记忆管理也是一回事只是换了个术语。上下文窗口就是 Agent 的“工作内存”所有要发给模型的信息都必须放在里面。窗口有限塞太多内容模型会遗忘前面的指令响应质量也会下降。控制上下文本质就是给工作内存做 GC。常用的手段有三类滑动窗口只保留最近 N 轮对话更早的直接丢弃。适合客服、闲聊这类短期场景。摘要压缩历史太长时让模型先把旧对话压缩成一段摘要再和最近对话一起放回上下文。类似前端把旧的日志做成统计报表而不是保留原始流水。外部记忆把用户资料、业务数据放到向量库或普通数据库里需要时检索出来再拼进上下文。这就更像后端缓存 数据库的组合只是检索时用了向量相似度而不是 SQL。我在项目里经常把这三层叠加用短期对话走滑动窗口中期信息走摘要长期用户偏好走向量库。最开始我也觉得记忆很难后来发现它就是“缓存策略 持久化方案”的 AI 版思路可以完全照搬。2.4 编排与循环Agent 的大脑是代码不是模型很多人以为 Agent 的“智能”全来自模型但真正的决策流程其实是你写的编排代码。模型只负责一个动作给定当前状态预测下一步。如果模型说“我要调用工具”你的代码就调如果模型说“我已经完成了”你的代码就停。这个编排循环通常长这样初始化 messages 和任务目标 循环 把 messages 发给模型 如果模型要求调用工具 解析工具名和参数 执行对应函数 把结果追加到 messages 继续下一轮循环 否则 把模型回复返回给用户 结束循环这就是 ReAct 模式的简化版也是当前大多数 Agent 框架的底层逻辑。你完全可以用一个 while 循环写出来而不用引入任何框架。前端思维在这里又一次派上用场这个循环很像一个事件驱动的 reducer。工具调用结果就是 dispatch 的 actionmessages 就是全局 state每次循环都是根据 action 计算新 state。你写 React 时已经非常习惯这套模式了只是以前 action 是用户点击现在 action 变成 tool_calls。3. 实操一个最小 Agent 的完整实现过程3.1 先跑通一个最朴素的循环在写任何复杂功能之前我强烈建议先跑通一个最小闭环模型能收到消息、能返回回复、代码能把它呈现在控制台。这个闭环能验证 API key 配置、网络连通性、依赖版本避免后面把问题混在一起排查。我习惯用 TypeScript 写但不依赖任何框架只用一个 fetch。下面是一个极简骨架type Message { role: system | user | assistant | tool; content: string | null; tool_calls?: ToolCall[]; name?: string; }; type ToolCall { id: string; type: function; function: { name: string; arguments: string; }; }; type Tool { name: string; description: string; parameters: object; execute: (args: any) Promisestring; }; async function callLLM(messages: Message[], tools: Tool[]) { const response await fetch(https://api.example-llm.com/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${process.env.LLM_API_KEY} }, body: JSON.stringify({ model: base-chat, messages, tools: tools.map(({ name, description, parameters }) ({ type: function, function: { name, description, parameters } })) }) }); const data await response.json(); return data.choices[0].message; } async function agentRun(input: string, tools: Tool[], maxTurns 5) { const messages: Message[] [ { role: system, content: 你是运行在终端里的一个小助手请尽量简洁地回答。 }, { role: user, content: input } ]; for (let turn 0; turn maxTurns; turn) { const message await callLLM(messages, tools); messages.push(message); // 如果没有工具调用说明模型已经给出了最终答案 if (!message.tool_calls || message.tool_calls.length 0) { return message.content; } // 逐个执行工具调用 for (const call of message.tool_calls) { const tool tools.find((t) t.name call.function.name); if (!tool) { messages.push({ role: tool, tool_call_id: call.id, name: call.function.name, content: 未找到工具 ${call.function.name} }); continue; } try { const args JSON.parse(call.function.arguments); const result await tool.execute(args); messages.push({ role: tool, tool_call_id: call.id, name: call.function.name, content: result }); } catch (e) { messages.push({ role: tool, tool_call_id: call.id, name: call.function.name, content: 工具执行失败: ${(e as Error).message} }); } } } return 已达到最大轮次任务未结束。; }这个骨架看起来不起眼但它已经是一个完整的 Agent能对话、能调用工具、能处理工具报错、能防止无限循环。后面所有功能都是在它的基础上加东西。3.2 给 Agent 接入一个真实工具骨架有了我们接一个最简单的工具进去。前端同学最熟悉的就是天气查询这里我用一个模拟实现演示真实场景里把它换成天气服务 API 即可。const weatherTool: Tool { name: get_weather, description: 查询指定城市当前天气。当用户问天气时使用。, parameters: { type: object, properties: { city: { type: string, description: 城市名称例如 杭州 } }, required: [city] }, async execute({ city }) { // 这里换成真实的天气接口调用 const weatherMap: Recordstring, string { 杭州: 阴23 度空气质量良, 上海: 小雨25 度空气质量优, 北京: 晴26 度空气质量轻度污染 }; return weatherMap[city] ?? ${city}暂无数据; } }; async function main() { const reply await agentRun(杭州现在天气怎么样, [weatherTool]); console.log(reply); } main();整个流程会是这样模型判断用户想查杭州天气输出一个 get_weather 的工具调用参数是 city杭州你的 execute 函数从某个记录里拿到天气信息把它作为 tool 消息返回给模型模型看到天气信息后组织成自然语言回复“杭州现在阴23 度空气质量良。”这个流程的关键认知是模型本身不知道杭州天气它靠外部工具拿到了数据。这就是工具调用的意义也是 Agent 比普通对话模型“有用”的地方。前端在这个环节要注意工具返回给模型的文本质量直接影响最终回答。尽量让工具返回结构清晰的数据比如 JSON 字符串而不是一段已经写好的口语化句子给模型留出组织语言的空间。3.3 加上流式输出、超时和错误处理才算能上生产我一开始做 Agent 时用的都是非流式的接口等全部生成完才一次返回。在终端里看着没问题放到网页聊天界面里就出问题了用户发出消息可能要等好几秒才看到第一个字体验非常差。后来才把普通请求改成流式请求。流式请求的思路和普通请求不太一样响应不是一次性返回完整的 JSON而是一块一块推过来像前端通过 WebSocket 接收消息一样你需要一边接收一边渲染。async function streamChat(messages: Message[], onDelta: (text: string) void) { const response await fetch(https://api.example-llm.com/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${process.env.LLM_API_KEY} }, body: JSON.stringify({ model: base-chat, messages, stream: true }) }); const reader response.body?.getReader(); const decoder new TextDecoder(); let buffer ; while (reader) { const { value, done } await reader.read(); if (done) break; buffer decoder.decode(value, { stream: true }); const lines buffer.split(\n); buffer lines.pop() ?? ; for (const line of lines) { const trimmed line.trim(); if (!trimmed.startsWith(data: )) continue; const data trimmed.slice(6); if (data [DONE]) return; try { const parsed JSON.parse(data); const delta parsed.choices?.[0]?.delta?.content; if (delta) onDelta(delta); } catch { // 忽略不完整的 JSON 片段 } } } }这段代码需要配合 AbortController 做超时中断用户取消对话时也需要它const controller new AbortController(); // 10 秒无响应则中断 const timer setTimeout(() controller.abort(), 10000); fetch(url, { method: POST, headers, body, signal: controller.signal }).catch((error) { if (error.name AbortError) { console.log(请求超时已中断); } else { console.error(请求失败, error); } }).finally(() clearTimeout(timer));生产环境里除了流式还有一个必须想清楚的错误处理策略网络错误要重试模型返回格式错误要兜底工具执行失败要明确告诉模型。不要把这些错误直接抛给用户让模型把错误“消化”成一句更自然的解释比如“天气服务暂时不可用请稍后再试”体验会好很多。4. 调试 Agent 的实战心得把大模型当不稳定接口对待4.1 从“代码崩溃”到“输出不对”调试思维的转变前端调试的核心场景是报错和崩溃TypeError、404、白屏。Agent 调试的核心场景是输出不对模型绕圈不结束、工具参数传错、回答太啰嗦、记不住前面说过的话。这些错误不会让程序崩溃但会让产品不可用。所以第一个要建立的观念是把模型当成一个会给你输出“合法但不符合预期”内容的不稳定接口。你不能像信任本地函数一样信任模型的输出。所有从模型返回的结构化数据都要做解析、校验、兜底。我自己调试时最常用的手段是把每次请求的完整消息历史打印出来包括每一轮的用户输入、模型回复、工具调用、工具返回值。这个东西我会单独存成日志格式和 Chrome DevTools 里的 Network 面板类似。问题出现时先从日志定位是哪一轮开始偏离的。是模型没有正确识别用户意图还是工具返回了脏数据还是上下文太长把之前的指令冲掉了每一个都有不同的解法。4.2 高频问题排查速查表下面是我在多个项目里整理出来的高频问题对照表每一行都是真实踩过的坑问题现象前端类比推荐排查方法Agent 循环调用工具不结束死循环或逻辑没有出口设置最大轮次检查工具返回值是否让模型误以为任务未完成打印每轮 tool_calls 观察循环路径上下文越塞越多响应变慢且质量下降内存泄漏增加滑动窗口对旧对话做摘要检查是否不小心把长文档重复塞入系统提示模型没有调用预期工具参数传递错误检查工具描述是否足够明确确认模型版本是否支持工具调用把工具名写得更直白避免“getInfo”这种模糊命名工具参数解析失败JSON.parse 抛错在工具函数里 try/catch用 JSON Schema 做二次校验把模型返回的 arguments 先 console.log 出来检查回答重复绕圈逻辑分支缺失在系统提示里加上“如果你已经给出答案就不要再调用工具”检查工具返回值是否给了太多选项流式输出在界面卡顿渲染性能问题不要每收到一个 delta 就 setState 一次用节流或批量合并确认是否重复解析 Stream 数据模型被用户带偏输出不安全内容XSS/注入防护在系统提示里写死约束范围在展示层对模型输出做过滤不要直接 innerHTML这张表不用背当你把 Agent 当普通业务系统调问题自然归到“输入、处理、输出”三个环节排查思路和你调接口联调一模一样。4.3 我的“护栏”设计清单最后一个相对成熟的心得生产级 Agent 不能只有一条“让模型自由发挥”的通道。我会在代码层面加几层护栏类似前端给用户输入做白名单校验轮次上限。任何循环必须有硬性终点防止模型钻牛角尖。工具白名单。模型只能调用你注册过的工具未注册的调用名直接报错返回给模型而不是尝试执行。输出格式校验。凡是模型要返回结构化内容的地方都加 JSON Schema 校验不合格就重试一次或转成自然语言提示用户。敏感操作二次确认。涉及删除、支付、发送消息这类操作先让模型产出“待确认状态”再向用户确认不能一步执行到底。日志全量记录。每次调用都记录 messages、tool_calls、耗时、token 消耗。前端可能觉得这很笨重但没有日志Agent 出了问题根本没法复盘。这几层护栏让系统变稳。它们不依赖更强的新模型只需要你在开发时多写几层 if。前端对“边界情况”的天然敏感在这个阶段就是别人复制不走的核心竞争力。最后说点真实的开发体会练手时我的建议是把目标定得小一点做一个能查天气、能记账、能帮你查节假日的小助手都行。千万不要一开始就上多工具、多记忆、多层编排的宏大架构。Agent 这个领域最大的特点是失败隐蔽一个问题可能要等系统跑好几轮才暴露所以每次只加一个变量验证稳定了再加下一个比任何优雅设计都管用。我个人实际做项目时最常用的一句话是“限制条件永远写在最前面”。系统提示里一开始就说清“你只有这些工具不能做列表之外的事”后面出问题的概率会显著下降。前端开发里这叫接口约束Agent 开发里这叫预设边界。说到底都是靠工程手段兜住系统的复杂度模型只是这台机器里的一个部件而已。
阅读完成 · 觉得有帮助?
咨询建站