这两年Agent概念火到什么程度呢?我身边做前端的同行,几乎每周都要被“Agent”、“智能体”、“多智能体协作”这些词刷屏。作为一个写了六年业务页面的人,我第一次看到这类项目时心里是一紧的:这玩意儿是不是又要我回去补数学、学梯度下降、搞数据集标注?后来真的上手做了几个能用的Agent,才发现最开始那个想法错得有多离谱。Agent开发压根不是模型训练,它更像是在现有的模型能力之上做“工程组装”。这篇文章就是想把这些认知整理清楚,给和我一样从页面、组件、API联调这些日常里切过来的人,一份可落地的入门路径。
先说结论:Agent开发本质上是工程问题,不是算法问题。它做的是把别人已经训练好的模型,通过提示词编排、工具调用、状态维护、异常兜底,组合成一个能完成具体业务任务的系统。前端在这条路上不仅不弱势,反而有不少天然优势。下面我按自己的理解,把整个认知框架、技术栈、实操过程和踩坑点拆开来讲。
1. 先把话说清楚:Agent开发不是模型训练
1.1 一个前端看到Agent时最容易产生的误解
我以为搞Agent得先会训练模型,这个误区太常见了。一听到“大模型”、“智能体”,脑子里全是数据清洗、训练卡、loss收敛这些词。实际上,大部分Agent项目用的都是现成的模型服务,你通过API调一下,拿它返回的结果继续编排,就算把模型用起来了。训练模型那是模型厂商的事情,和你做Agent基本没有交集。
做个比喻:训练模型像是研发一台发动机,要考虑材料学、燃烧效率、缸内压力。Agent开发呢,是拿一台已经造好的发动机去造车——你要考虑的是传动怎么接、方向盘怎么连、刹车失灵了怎么办。前端日常工作就是“把各种现成组件拼成一个可用的产品”,这个感觉在Agent开发里是一模一样的。
所以第一条认知:Agent开发不要求你懂模型内部原理,但要求你懂系统如何组合、调度、容错。这跟“前端工程师”的经验是高度重合的。
1.2 Agent开发真正的核心能力是什么
上手之后我发现,真正让一个Agent从“玩具”变成“能用的工具”,靠的是下面这些工程能力,而不是模型能力:
- 编排能力:把“用户意图→模型推理→工具执行→结果回传→再推理”这条路串成闭环。
- 提示词设计:让模型理解自己的角色边界、可用工具、输出格式。这就像给组件定义了一套好的Props接口。
- 工具封装:模型只能“说”,不能“做”。真正查询数据库、发送请求、改状态,是Agent调用你写好的一堆函数完成的。
- 上下文管理:模型有上下文窗口限制,聊天记录、工具返回结果怎么存、怎么压缩、怎么丢,直接决定成本和效果。
- 可观测性:每次调用模型,请求是什么、返回是什么、中间哪一步错了,都要能查。前端对Debug的敏感度这时候特别有用。
换句话说,一个Agent项目里的代码,绝大多数都是写“怎么把模型安全稳定地用起来”,而不是写“模型本身”。这跟后端联调、中间层服务、BFF设计站得很近。
1.3 为什么前端转Agent开发反而有优势
很多人觉得前端离AI远,我倒觉得前端转Agent有几个隐藏优势。
第一,前端天生在做交互闭环设计。Agent说白了也是一种交互:用户说一句自然语言,系统经过好几轮内部循环,最后给用户一个结果。怎么把用户意图转化成任务、怎么在等待时呈现状态、怎么在失败时提示用户,这都是前端最擅长的事。
第二,事件驱动的思维方式。Agent的典型运行方式是异步多轮的:模型要工具结果、工具执行完毕要再喂给模型、中间可能还要等待用户确认。这不就是一个复杂事件流吗?做过复杂前端应用的人对这种模式再熟悉不过。
第三,前端对状态管理有本能的敏感。用Redux、Zustand、Pinia管理界面状态的人,上手“Agent上下文管理”时,很多概念几乎是平移的。你会发现Agent的memory,就是一个更复杂的Store。
第四,能落地。模型能力再强,最终要变成用户看得见摸得着的产品。谁能最快做出可交互的界面?前端。Agent项目的前期Demo、内部工具、验流程产品,前端一个人就能包圆。
2. 前端入局Agent需要的五个核心技术栈
2.1 模型调用:你的新“fetch”
做Agent第一件要会的事,是用代码调用大模型API。把这件事理解成一个需要鉴权的、有返回值的fetch就行,不用想得太玄。
最基础的一次对话调用,大概是这个样子的:
const resp = await fetch("https://api.llm.example.com/v1/chat/completions", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${process.env.LLM_API_KEY}`, }, body: JSON.stringify({ model: "base-chat-model-v1", messages: [ { role: "system", content: "你是一个订单查询助手。" }, { role: "user", content: "帮我查一下SO20241117001这个订单到哪了" }, ], temperature: 0.3, }), });这个接口返回的JSON里,有一个choices数组,里面是模型生成的回复内容。前端接触到的SSE流式响应、loading状态、超时重试,在这里几乎都能复用。
有一点要特别提醒:模型接口不是每次都一样快,一个复杂请求等几秒甚至几十秒都很正常。如果你做过慢接口的体验优化,就知道这里必须做loading、做超时提示、做重试机制。这也侧面说明,Agent开发大量工作其实是常规工程。
2.2 Prompt编排:不是写文案,是设计协议
写Prompt是Agent开发里最“软”但也最关键的能力。很多人把Prompt理解成“跟模型说好话”,实际上一个合格的系统Prompt更像一份接口协议,它定义了模型这个“组件”的输入输出边界。
我通常会在系统Prompt里写清楚四件事:
- 角色:你是什么助手,服务于什么场景。
- 能力边界:你能做什么,绝对不能做什么。
- 输出风格:默认返回格式、语气、长度要求。
- 工作流:先做什么后做什么,哪些情况需要向用户澄清。
举个例子,一个订单助手Prompt的开头大致是这样:
你是一个电商订单查询助手。你可以查询订单状态、修改订单备注、导出订单报表。 你没有权限处理退款、修改价格、删除订单。当用户提出这些请求时,礼貌说明你无法处理。 回答保持简洁,优先给出订单状态和时间节点。如果工具返回结果为空,直接告诉用户未查询到信息,不要编造订单内容。这个Prompt写得好不好,直接决定了模型后续所有行为。它像前端里定义好了组件的props契约,组件外面的人不乱传参数,组件内部也不乱做事情。
2.3 Function Calling:让模型学会“伸手拿东西”
大模型本身不联网、不查库、不写数据库。它要完成真实操作,靠的是Function Calling机制,也就是我们在API里给模型声明一批“函数”,模型在回答过程中自己决定“我要调用某个函数,参数是什么”。
这里的前端类比非常直接:相当于你把一组封装好的API暴露给模型,模型拿着用户的自然语言去匹配正确的API和参数。你只需要按格式声明工具列表,并在代码里实现执行逻辑。
const tools = [ { type: "function", function: { name: "query_order", description: "根据订单号查询订单状态,返回当前状态、商品列表、金额等信息。", parameters: { type: "object", properties: { order_id: { type: "string", description: "订单号,例如SO20241117001", }, }, required: ["order_id"], }, }, }, { type: "function", function: { name: "update_order_remark", description: "为指定订单修改备注内容。", parameters: { type: "object", properties: { order_id: { type: "string", description: "要修改备注的订单号" }, remark: { type: "string", description: "新的备注内容" }, }, required: ["order_id", "remark"], }, }, }, ];模型返回的消息里会出现tool_calls字段。你的代码需要解析这个字段,找到函数名和参数,然后调用你自己写好的函数,再把结果以“tool”角色回传给模型,模型才能继续生成最终答案。
这里最前端化的一步:参数校验。模型吐出来的参数偶尔会出现格式不对、缺字段、枚举值不合法之类的问题。你在前端做表单校验那套经验,直接平移过来做工具参数校验就对了。2.4 上下文管理:你的新“状态管理”
大模型每一次调用都是无状态的,你必须把历史对话、工具结果、中间状态统统塞进messages数组里再发给它。这个“塞进去”的过程如果不加管理,很快会踩到上下文窗口的墙。
上下文窗口可以理解成模型的一次性工作内存。窗口越大,能容纳的信息越多,但成本也越高,响应也更慢。什么该放进去、什么该精简、什么该丢,这就是Agent开发里的上下文管理,本质上是前端状态管理的换皮。
我常用的策略有这么几个:
- 裁剪旧消息:只保留最近几轮对话。
- 摘要压缩:当历史太长时,先用模型把前面的对话总结成一段摘要,再作为System消息放进去。
- 工具结果精简:查询订单返回20条记录,不要让模型读20条原文,而是只回传“共20条,第一条为xxx,状态为待发货”这种摘要。
- 状态外置:有些状态并不需要放进上下文,比如“当前登录用户ID”,你直接在工具函数里去获取就行,没必要占用模型的上下文。
这一块做得好不好,直接决定你的Agent能用多久、烧多少钱。我见过太多Demo,聊到第五轮就开始乱答,一看log,上下文里塞满了无用的工具返回。
2.5 Agent循环:规划、执行、观察的工程化封装
有了模型调用、Prompt、Function Calling、上下文管理,把它们串起来的就是Agent循环。这也是Agent区别于“单次聊天机器人”的关键。
单次聊天机器人是一问一答,而Agent是“模型→工具→模型→工具→……”多轮循环。模型先理解任务,决定要调用工具,工具返回结果给模型,模型看了结果决定是继续调用工具,还是终止并给出最终回答。
一个最简单的循环骨架长这样:
async function runAgent(userInput: string, history: Message[]) { const messages = [ { role: "system", content: SYSTEM_PROMPT }, ...history, { role: "user", content: userInput }, ]; const MAX_LOOPS = 5; for (let i = 0; i < MAX_LOOPS; i++) { const resp = await callLLM({ messages, tools }); const message = resp.choices[0].message; if (!message.tool_calls || message.tool_calls.length === 0) { return message.content; } messages.push(message); for (const toolCall of message.tool_calls) { const toolName = toolCall.function.name; let toolResult; try { const args = JSON.parse(toolCall.function.arguments || "{}"); toolResult = await executeTool(toolName, args); } catch (err) { toolResult = { error: `工具执行失败: ${err.message}` }; } messages.push({ role: "tool", tool_call_id: toolCall.id, content: JSON.stringify(toolResult), }); } } throw new Error("Agent 超过最大执行轮数,已终止"); }这段逻辑里,最容易被忽略的是MAX_LOOPS。你永远猜不到模型会在哪一步开始绕圈子,不设上限,一个Agent可能能无限调用工具,烧光你的API预算。
Agent循环前端人看着眼熟,因为它特别像Redux里一个复杂的异步action:dispatch一个任务,中间有副作用,有重试,有超时,最后回到主流程。思维模型是相通的。
3. 实操记录:用TypeScript从零搭一个能查订单状态的Agent
3.1 场景设定与整体架构
空谈概念没意思,我拿自己做过的一个某电商后台订单查询Agent来举例。需求很简单:运营人员在后台聊天框输入自然语言,比如“帮我看看SO20241117001这个订单现在什么状态”、“把SO20241116002的备注改成发票已寄出”,Agent负责调用后端接口完成操作,并返回友好结果。
技术选型我定成了React前端 + Node.js中间层 + 大模型API,数据接口沿用团队已有的订单服务。前端这部分其实很常规:一个聊天窗口、一个消息列表、一个输入框,加上流式输出的打字机效果。真正花时间的是Node层里那个Agent循环。
整体架构就三层:
React页面 <-> Node中间层(Agent循环 + 工具执行) <-> 订单服务API | 大模型APINode层对外暴露一个简单的HTTP接口,接收用户输入和会话历史,内部跑几轮模型+工具循环,最后返回结果。前端不关心Agent内部到底调了几轮模型,它看到的仍然是一次请求、一次响应。
3.2 核心代码:定义工具与执行循环
工具列表我上面已经给出了两个模板:query_order和update_order_remark。真正执行工具时,用的是一个简单的路由表:
async function executeTool(name: string, args: any) { switch (name) { case "query_order": return orderService.queryOrder(args.order_id); case "update_order_remark": return orderService.updateRemark(args.order_id, args.remark); default: return { error: `未知工具: ${name}` }; } }这里有个细节:executeTool返回的数据要尽可能结构化,让模型容易理解。比如query_order返回:
{ "order_id": "SO20241117001", "status": "shipped", "status_text": "已发货", "tracking_no": "SF1234567890", "items": ["商品A x1", "商品B x2"], "total_amount": 299.0, "updated_at": "2024-11-18 10:23:00" }不要返回一大段HTML页面片段,也不要返回对象里的嵌套大数组,模型看起来费劲,token还烧得快。字段名用英文,值保持干净。
3.3 参数是怎么定出来的
我第一次调用模型时是全默认参数,效果惨不忍睹。后来每个参数都是根据自己的场景试出来的。
temperature,我直接设成0。因为订单查询是确定性业务,我不希望模型在解释订单状态时发挥想象力。文件名称润色、生成营销话术这种场景才适合0.7以上的温度。经验是:涉及数字、状态、查询结果时,温度一定要低;涉及文案创作时可以调高。
max_tokens要留余量。如果你生成的回答可能很长,但max_tokens设太小,会被拦腰截断。被截断的内容如果是JSON或者Markdown表格,前端解析时直接报错。这个错误还不好排查,因为模型API看着是成功的。我后来统一设成2000,足够覆盖大多数回答,又不至于浪费。
工具调用的参数也有讲究。description字段必须写清楚,模型靠它来理解工具用途。我一开始偷懒,写的是“查订单”,模型就经常把用户输入的“订单号”错传给“查询商品”的工具。后来把description改成“根据订单号查询订单的实时状态、商品明细、金额信息,适用于用户询问订单进度、物流、明细等场景”,准确率明显上升。
3.4 前端思维在这里是怎么帮上忙的
做这个Agent时,前端经验帮我省了很多弯路的第一个点,就是“错误边界”。React里有Error Boundary,Agent里也要有。上面runAgent里,我把每个工具调用都包在try/catch里,还把错误信息结构化后再回传给模型,这样模型不会因为一次工具失败就崩溃,它会自己调整策略重试或向用户解释。
第二个点是“加载状态”。模型接口可能跑好几秒,中间Agent甚至还会调用工具。如果前端只是傻傻地转圈,用户会以为系统挂了。我在消息列表里把Agent的内部动作以步骤卡片的形式展示出来:“正在理解你的问题……”、“正在查询订单接口……”、“正在生成回答……”。这个展示不需要真的从后端拿状态,只要Node层分步骤返回进度事件就行。用户体验直接上一个台阶。
第三个点是“接口数据的防御性处理”。前端天天处理后端返回的脏数据,天然会怀疑一切外部输入。到了Agent里,这个习惯救了我很多次:模型返回的tool_calls参数我从来不会直接信,一定先JSON.parse再校验,order_id必须匹配订单号正则,remark不能为空字符串。这些校验代码,本质上就是前端表单校验的孪生兄弟。
4. 最常见的坑:从我自己踩过的开始说
4.1 上下文爆炸,钱烧得快
第一个坑是上线第二天发现账单涨得离谱。排查下来,问题出在会话历史无限增长上。用户每次发一句话,我就把之前所有消息全发出去,加上工具返回的大量JSON,没过几轮,单次请求token数就上万。有了上下文管理之后,我只保留最近6条对话,并把工具返回的明细字段做摘要,费用迅速降下来。
另外要养成看usage的习惯。每次调用模型,返回里都带prompt_tokens和completion_tokens,把它们记到日志里。哪怕你不主动优化,有数据之后就知道哪些环节最烧钱。
4.2 工具调用失败而不自知
这是很隐蔽的一个坑。模型返回了tool_calls,你的代码也执行了工具,但工具抛了异常。如果没处理好,模型可能会把异常信息原文当成正常结果来回答,甚至“将错就错”编一个结果给用户。
我踩过一次:订单服务接口超时,返回一个timeout对象,结果模型跟用户说“该订单正在配送中”,完全牛头不对马嘴。解决方式是在工具结果里明确加error字段,并在Prompt里写清楚规则:只要工具结果里出现error,就必须如实告诉用户系统暂时无法处理,严禁自行推断。
4.3 模型“幻觉”与错误传播
幻觉是躲不掉的。订单Agent最容易出现的幻觉是,明明只查到一个订单,模型却编造了“该订单包含5件商品,其中1件已退款”这种细节。尤其当工具返回内容很长、模型没看仔细时,就爱自己脑补。
我的防线有三层:
- 第一层,Prompt里强制要求模型只基于工具返回内容作答,未提及的信息一律说“没有查询到”。
- 第二层,工具返回结果尽量精简,字段越少模型越容易聚焦。
- 第三层,在返回给用户前,用代码做一个“关键字段校验”,比如订单状态只能是枚举值之一,如果模型回答里提到金额但工具返回里根本没有金额字段,就让该回答进入“疑似幻觉待复核”状态。
4.4 把Agent当纯函数写的思维误区
我一开始写Agent喜欢把它设计成“输入一句话,输出一个答案”的纯函数。实际跑起来根本不是那么回事,因为工具执行有副作用,修改备注之后,后续对话必须知道“刚才已经改过了”。如果每次调用都是无状态地重跑,那状态就丢了。
后来我把Agent循环里增加了一个steps数组,保存每一步“模型说了什么、调了什么工具、结果是什么”,并且把它作为下一次循环的上下文传给模型。这就有点像前端把用户操作记录放进Store一样,模型自己也清楚当前进行到哪一步。
还有一个我从前端带过来的习惯:每次Agent运行结束,把整个决策过程写入本地日志。用户后续反馈“结果不对”时,我可以像查Redux action链一样,一条条回放Agent每一步在干嘛,很快定位是模型理解错了、工具参数错了还是数据源本来就错了。
5. 给想入局的前端同行的几条实操建议
5.1 先别急着上框架
现在Agent框架非常多,什么编排框架、多智能体框架,听起来很唬人。我的建议是,刚开始别碰框架,先用原生API把“用户提问→模型→工具→结果→模型→回复”这条链路手写一遍。手写过之后,你才知道框架在帮你解决什么问题,哪些是它解决不了的,否则框架对你就是个黑盒。
5.2 从“一个工具”开始
不要一上来就做多Agent协作、复杂规划。先挑一个和应用强相关的工具,比如订单查询,把这一件事做扎实。当你能稳定地把模型和工具串起来,再慢慢加第二个、第三个工具。大多数看起来华丽的Agent,核心也就是这么一个个工具堆出来的。
5.3 日志比模型重要
这句话我越来越认同。模型是概率系统,它今天能回答好,明天同一句话可能就偏了。没有日志,出了错只能干瞪眼。我自己的项目会记录每一次入参、模型响应、工具结果、耗时、token数,以及最终回答。排查问题时,这些记录就是你在黑盒里唯一的手电筒。
5.4 兜底机制不能少
最大轮次、单次运行总超时、API费用上限、敏感操作二次确认,这些兜底机制一个都不能少。尤其涉及改数据、删数据的工具,模型说改就改很危险。我在修改备注这个工具上加了前端Confirm弹窗,工具真正执行前必须用户点确认,成本不高,安全感提升很多。
5.5 用UI去补模型的短板
模型不是万能的,但你的UI可以帮它兜底。当检测到模型输出不确定时,可以在回答下方加一个“该结果已自动核对订单接口,确认于发布时间”的提示条。当工具执行失败时,可以给用户一个“点此复制订单号重新查询”的按钮。这些都是典型的“前端价值”。
我个人到现在依然认为,Agent开发是过去几年里最适合前端切入的方向之一。它不需要你推翻原有知识体系,反而是你之前积累的交互设计、状态管理、接口联调、错误处理经验,都能在这里重新派上用场。如果说模型能力是那台发动机,那前端清楚怎么造车——把各种零件衔接好、给用户一个安全可靠的驾驶舱。亲手把一个小Agent从零写起来之后,你会明显感觉到,自己不是在追热点,而是在用一套已经被验证过的工程能力,做一件新事情。
最后再分享一个小技巧:把第一版Agent做出来后,别急着加功能,先让它每天处理10条真实请求,把日志翻出来一条条看。不用多久你就会发现,真正的问题往往不是模型笨,而是你没给它一个清晰的工作流。把工作流理顺,Agent的质量提升比你换一个更贵的模型快得多。