ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

前端工程师零基础入门Agent开发:不懂模型训练也能做

前端工程师零基础入门Agent开发:不懂模型训练也能做

Agent 开发不是模型训练——这句话,我最近至少对不同的前端同事讲了十遍。很多人一听到“Agent”就以为要回去补机器学习、要懂模型训练、要会调权重,然后在看到各种术语之后直接放弃。但以我做过的几个 Agent 项目经验来看,这个认知完全是错的。Agent 开发本质上是一种业务系统开发,它跟模型训练是两条不相干的路线。

对前端来说尤其如此。你天天打交道的异步流程、接口封装、状态管理、异常处理,在 Agent 开发里全都用得上。甚至可以说,前端工程师本来就具备做 Agent 开发所需的大部分工程能力。这篇文章就是我以一个前端身份入门 Agent 后的完整复盘,既不打算把 Agent 吹成银弹,也不会堆术语,只讲实际干活时会遇到的东西,以及我踩过的坑。

1. 先说结论:Agent 开发到底在开发什么

1.1 一句话纠正认知偏差

模型训练解决的是“让模型变得更聪明”的问题,比如让它更会写作、更会总结、更能理解代码。而 Agent 开发解决的是“在已有的聪明模型基础上,让它按照你的业务逻辑去干活”的问题。前者需要数据、算力和算法,后者需要工程、产品和流程设计。

打个比方,模型训练像造发动机,Agent 开发像造整车。发动机的缸内燃烧、材料工艺你可以不懂,但方向盘、刹车、仪表盘、空调这些系统怎么协同,你必须懂。前端跳去做 Agent,不是去造发动机,而是去把一台已经造好的发动机装进一辆能跑的车里。你手里拿的也不是训练脚本,而是函数调用、消息队列、状态机和 API。

我最初入坑时犯过一个典型错误:一直在问“模型参数到底怎么调”,想着是不是模型不够聪明才导致 Agent 表现不好。后来发现,大部分失败都不是模型的问题,而是我提供给模型的信息结构不对、工具结果没讲清楚、上下文被截断了。这些问题全部是工程问题,跟模型训练毫不相关。

1.2 Agent 应用的技术栈拆解

把 Agent 应用拆开看,它更像一个三层结构。

最上面是交互层。用户输入一段话,不管是在聊天框、命令行还是页面表单里,这一步本质上就是你熟悉的前端交互。只不过输入变成了自然语言,输出也变成了自然语言。

中间是逻辑层。Agent 拿到输入后,要自己决定下一步做什么。这里会有一个循环:把输入和已有历史发给模型,模型返回文字或工具调用请求,你执行工具,再把结果返回给模型,继续下一轮。这层最像你每天都写的异步流程,只是循环条件是模型说了算,而不是你写死。

最底下是数据层。包括模型 API、你封装的业务接口、第三方服务,以及可能需要用到的一些检索或记忆服务。这层对前端来说就是“多接几个 API”的扩展版。

我习惯用一张表格来对照前端能力和 Agent 开发的关系:

层前端已有经验Agent 开发对应
交互层页面组件、表单、状态展示用户输入框、流式输出、执行过程可视化
逻辑层Promise、事件循环、状态机Agent 循环、工具调度、分支判断
数据层封装 REST 接口、处理错误模型调用、工具函数注册、上下文管理
可观测层日志、埋点、错误上报消息记录、Token 统计、工具调用审计

这个对照表是我后来给别人讲 Agent 时必用的图。它能很快让人从“Agent 很神秘”切换到“这不就是业务系统吗”的状态。

1.3 前端优势:已经具备大半拼装能力

很多人觉得前端做 Agent 会吃亏,因为没有算法背景。但实际工作中,Agent 项目最大的难点很少是“模型不会回答”,而是“模型回答得乱七八糟,你没办法让它稳定地在业务流程里跑”。稳定这件事,恰恰是前端的强项。

你处理过接口超时,知道什么时候该重试;你处理过复杂表单联动,知道状态要收敛;你处理过第三方 SDK 的诡异行为,知道怎么给边界条件兜底。这些经验在做 Agent 时全部能用上。

还有一个常被忽视的点:前端最擅长把一个复杂过程变成用户能理解的东西。Agent 执行过程中有哪些步骤、当前在做哪一步、为什么需要用户确认,这些都需要产品化。而产品化的核心能力,就是你天天在练的“信息可视化”和“交互设计”。所以前端不要觉得 Agent 是另一个世界,它更像是你熟悉的业务系统换了一个交互范式。

2. 一个前端熟悉的 Agent 最小系统长什么样

2.1 核心组件只有四样

我用一个最小可运行的 Agent 来理解整个事情。一个真正的 Agent 系统,不管宣传里吹得多么花哨,核心组件都逃不过四样东西:模型、上下文、工具、循环。

模型是最底层的大脑,它负责理解自然语言指令,并且决定下一步应该输出普通回复还是请求调用工具。前端不需要训练它,只需要通过 API 调用它。

上下文是模型能看到的全部信息。它不是一个 Redis,也不是数据库,而是当前这一轮会话里所有消息的数组。模型每回答一次,你都把新的消息追加到数组里,再把整个数组发给模型。

工具是你暴露给模型的能力。每个工具都有一个名字、一段描述和一份参数说明。模型看到工具列表后,如果觉得需要查询订单、发送通知、计算表单字段,就会返回一个结构化的“工具调用请求”。

循环是 Agent 的发动机。模型请求调用工具,你执行工具,把结果追加到消息里,再让模型继续处理,如此反复,直到模型认为任务已经完成并且返回最终答案。

这四个东西,没有一个需要你懂模型训练。它们需要的只是你“管理好数据流”的能力。

2.2 看懂 Agent 运行循环

我写过一个最小骨架,核心代码简化后大概是这个样子:

type ChatMessage = | { role: 'user' | 'assistant'; content: string; toolCalls?: ToolCall[] } | { role: 'tool'; toolCallId: string; name: string; content: string }; interface ToolCall { id: string; name: string; arguments: Record<string, unknown>; } async function runAgent(userInput: string) { const messages: ChatMessage[] = [ { role: 'user', content: userInput } ]; for (let step = 0; step < 10; step++) { const response = await callModel(messages); messages.push(response.message); if (!response.toolCalls || response.toolCalls.length === 0) { return response.message.content; } for (const toolCall of response.toolCalls) { const result = await executeTool(toolCall.name, toolCall.arguments); messages.push({ role: 'tool', toolCallId: toolCall.id, name: toolCall.name, content: result }); } } throw new Error('Agent 循环超过了最大步数'); }

这段代码看起来简单,但它包含了 Agent 最核心的循环逻辑。callModel 负责拿着整个消息数组去请求模型服务,模型可能返回一段正常回复,也可能返回一组工具调用请求。如果返回的是正常回复,说明任务结束;如果返回工具调用,就执行工具,把工具结果放回消息数组,再来一轮。

这个循环有一个细节我会特别留意:给循环设置最大步数。否则模型一旦进入反复调用工具的状态,请求数会失控。我见过一个没有最大步数的 Demo,一个搜索工具被模型无意识调了二十多次,账单直接失控。所以这个 10 步上限不是随便写的,是做 Agent 最基本的自我保护。

2.3 关键参数不是模型参数

前端写业务接口时,会关注超时、重试、并发这些参数。到了 Agent 开发里,你同样要关注参数,但关注的是另外几个:温度、最大输出长度、消息窗口大小。

温度控制的是模型回答的随机性。做 Agent 如果希望结果稳定,温度通常设低一点,比如 0.2 到 0.4。我曾经用默认温度跑一个自动工单分类 Agent,同样的输入,两次分类结果竟然不一样。后来把温度调到 0.2,稳定性才上来。关键解释:因为 Agent 经常要输出结构化结果,比如 JSON 或者工具调用参数,随机性太高会直接导致结构不稳定。

最大输出长度也别随便设。这个参数决定模型每轮最多能生成多少 token。如果设得太小,模型可能还没把完整结果写完就被截断,JSON 都残缺不全。我之前设成 512 遇到过好几次截断,后来普通问答放到 1024,需要生成较长计划或文案的场景放到 2048,基本够用。

消息窗口大小更是一个工程问题。模型能接收的上下文是有限的,你把 50 轮历史全塞进去,既浪费钱,又可能挤爆窗口。所以需要在代码里做上下文裁剪,把过旧的对话总结成摘要,或者直接丢弃。这个我在后面会详细说。

3. 一个能够落地的 Agent:实操记录

3.1 场景选择:订单状态查询助手

理论知识再多,不如做一个能跑的东西。我选了一个很小的场景来练手:做一个订单状态查询助手。假设用户会问“我的订单到哪了”,Agent 需要根据订单号调用一个查询接口,然后把结果转成自然语言告诉用户。

选这个场景的原因很简单:它有明确的工具调用,有结构化参数,有结果的自然语言转化。而且贴近真实业务,前端完全能理解。我把它做成一个简单的网页,左边是对话框,右边是执行日志面板,方便观察每一步发生了什么。

做之前我先拆了需求。核心流程只有三条:用户输入自然语言问题,模型识别出需要调用查询订单工具并提取订单号,执行工具返回 JSON 结果,模型把 JSON 转成一句人能听懂的话。难点不是调用模型,而是怎么把“模型决定调用工具”这件事和“前端展示执行过程”对上。

3.2 注册工具:让模型知道能干什么

模型本身不知道你能提供什么接口,所以你需要把工具显式告诉它。每个工具都包括名称、描述、参数结构,以及真正执行的函数。

完整代码大概是这个样子:

const tools = [ { name: 'queryOrderStatus', description: '根据订单号查询客户订单的物流和签收状态。订单号通常以 SO 开头。', parameters: { type: 'object', properties: { orderId: { type: 'string', description: '完整订单号,例如 SO20241221001' } }, required: ['orderId'] }, async fn(args: { orderId: string }) { // 这里对接真实的订单服务 return { orderId: args.orderId, status: '已签收', signedAt: '2024-12-25 10:20' }; } } ];

这段代码真正重要的不是 fn 里的查询逻辑,而是 description 和 parameters。模型是靠 description 判断什么时候该用这个工具的,也靠 parameters 知道该给工具传什么参数。description 写得越具体,模型选错工具的概率就越低。

我在写参数结构的时候吃过亏。一开始 properties 只有 orderId,但 description 里没写订单号长什么样,结果模型经常把用户话里面的时间提取成 orderId。后来我在 description 里加了“订单号通常以 SO 开头”,并且在参数描述里也强调了一遍,错误率立刻降下来。描述里多写几个字,比多次调试强得多。

3.3 Agent 循环与结果渲染

工具注册好之后,我把 Agent 循环和前端界面接在一起。请求模型时,把 tools 数组放进请求体里。如果你的模型服务商支持原生函数调用,请求里带着这个数组,模型就能在需要工具时返回结构化结果。

执行工具的调度逻辑非常标准:

async function executeTool(name: string, args: Record<string, unknown>) { const tool = tools.find((t) => t.name === name); if (!tool) { return `找不到工具: ${name}`; } try { return JSON.stringify(await tool.fn(args as any)); } catch (err) { return `工具执行失败: ${(err as Error).message}`; } }

这里我特意把工具执行结果统一转成 JSON 字符串再放回消息数组。很多刚入门的人会忽略这一点。工具返回的结果会被模型再次阅读,所以格式越规整,模型越容易理解。如果返回一堆内存对象或者带无关字段的对象,模型很可能被无关信息带偏。

整个 Agent 的执行过程会是这样的:

  • 用户输入:帮我查一下订单 SO20241221001 到哪了
  • 模型返回工具调用请求:调用 queryOrderStatus,参数 orderId 等于 SO20241221001
  • 前端执行工具,拿到结果:订单已签收,签收时间 2024-12-25 10:20
  • 把结果追加到消息数组,再发给模型
  • 模型返回最终回答:您好,您的订单 SO20241221001 已于 2024-12-25 10:20 签收。

你观察这个流程会发现问题:真正难的并不是模型,而是“模型说想调工具,我能不能在工程上安全地调起来”这件事。安全地调起来包括参数校验、超时处理、结果标准化、异常回传。这些都是前端最熟悉的领域。

4. 前端入 Agent 最常见的五个坑

4.1 坑一:把“模型不听话”当模型问题

我见过太多人一遇到 Agent 行为不对,第一反应是“换一个更强的模型”。但多数情况下,模型的“不听话”来自三个原因:一是你没给它足够清楚的指令,二是它需要的工具或信息没有暴露给它,三是消息历史的组织方式有问题。

我在自己的项目里做过一次对照实验。同一个任务,同一个模型,只改了一个工具描述,把“查询订单状态”改成“查询订单物流进度,返回当前节点和预计送达时间”,结果模型的工具使用准确性提升了一大截。模型没有变强,只是你的系统变清晰了。

遇到 Agent 行为异常,建议先去查消息日志,而不是去调模型参数。看看模型实际收到的输入是什么、工具返回了什么、上下文有没有被截断。大部分问题只要看一遍日志就能定位。

4.2 坑二:忽略上下文管理和 Token 成本

前端写列表页会做分页,写日志会做采样,这些都是为了控制数据量。Agent 开发同样要做数据量控制,只不过对象是消息数组。

如果不限制消息历史,AI 助手用着用着就会把整个会话历史全塞进去,然后 Token 成本飙升,模型响应也会变慢。更严重的是,高质量模型往往有固定的上下文上限,超出后要么报错,要么悄悄截断前面的历史,导致模型“忘记”用户最开始的需求。

我采取的方案并不复杂。设定一个消息条数上限,超过上限时,把最早的一部分消息用一句摘要代替,再继续追加新消息。摘要可以让模型调用一次总结接口生成,也可以简单粗暴地保留最近 20 条消息。对于大多数轻量 Agent 场景,够用就行,不必一开始就上向量数据库。

4.3 坑三:把工具调用写成 if-else 地狱

工具调度看起来可以用一个 switch 实现,工具少的时候也没问题。但如果工具多了,你会发现自己既要维护工具描述,又要维护执行函数,还要处理参数校验,代码结构会越来越难控制。

更好的做法是保持工具定义的声明式风格。把 name、description、parameters、fn 放在同一个对象里,调度逻辑根据 name 去查找。这样每新增一个工具,只需要多注册一个对象,而不需要去修改 Agent 循环那一段核心代码。这也是我上面代码示例里为什么要用一个 tools 数组而不是一堆 if 分支的原因。

另外,工具执行必须做异常兜底。工具抛错了,不应该让整个 Agent 崩溃,而是把错误信息作为字符串返回给模型,让模型决定下一步怎么处理。这一点很像你对接口异常的处理:前端从来不会因为一个接口 500 就白屏,而是给用户一个提示,再给用户重试的机会。Agent 也一样,工具失败应该成为模型可以感知到的信息。

4.4 坑四:不做可观测性

开发 Agent 最容易忽略的是日志。因为 Agent 像是一个黑盒,你看到用户输入和最终输出,中间那段模型思考、工具调用、报错重试,如果不记录下来,你几乎没法排查问题。

我自己做第一个 Agent 时,只加了 console.log 打印最终结果。后来有一次用户反馈回答完全错误,我查了半天,最后才发现是工具参数把订单号和时间搞混了。如果没有中间日志,这个问题几乎不可能定位。

现在我的做法是在每次循环里都记录:请求消息数量、当前步骤数、模型返回类型、工具名称、工具参数、工具执行时长、工具返回结果、Token 使用量。这些信息不一定全部展示给用户,但一定要记录到一个可检索的地方。对于前端来说,这就是一个标准的数据埋点工作,没有任何学习成本。

4.5 坑五:一上来就追求复杂架构

有人一入门 Agent,就想着要上多 Agent 协作、上规划器、上记忆、上知识库。我不是说这些东西没用,而是对一个刚开始接触 Agent 的人来说,过早引入复杂架构只会让你失去对核心循环的掌控。

我的建议是先把 4.2 节里那个四组件版本跑通。等你在真实业务里发现单一循环不够用,比如工具越来越多、上下文管理越来越复杂、业务需要多个角色配合时,再逐步拆分。前端在这一点上有天然优势,因为你们早就习惯“先做一个可用的最小功能,再逐步迭代”。

4.6 常见问题速查表

现象真正原因解法
模型老是不调工具工具描述不清晰,或者没在请求里传工具定义重写 description,用具体示例说明使用条件
模型调用工具参数错误参数 schema 不够详细,缺少格式说明在描述中补充格式和示例,必要时在 prompt 里强调
工具结果被模型忽略工具返回格式不规整或字段过多统一 JSON 字符串,只保留关键字段
回答前后矛盾上下文被截断或历史遗漏检查消息数组,做摘要压缩而不是直接丢弃
执行次数过多循环缺少最大步数保护runAgent 里加 step 上限
同样的输入结果不稳定温度设置过高把温度调到 0.2 或更低
输出经常被截断最大输出 token 设置太小设置为 1024 或 2048

这张表基本都是我从实际项目中总结出来的,每一条都对应一次线上或本地的排查经历。踩过这些坑之后,我对 Agent 的敬畏感少了一大半,因为你会发现里面全是工程问题。

5. 前端专长在 Agent 时代的价值延伸

5.1 把 Agent 过程变成可理解的产品交互

Agent 产品跟普通搜索框最大的区别,是它有一个“思考过程”。如果这个过程不给用户任何反馈,用户会觉得自己面对的是一个不稳定的人工智障。

前端最该干的事,就是把这个过程可视化。比如用户在页面上输入一个问题后,旁边实时显示出“正在识别订单号”“正在调用订单查询接口”“查询成功,正在整理回答”这样的状态。这一步我不是为了好看才做,而是为了建立信任。用户看到系统在做具体的事情,而不是在一个黑盒里转圈,才不会反复怀疑结果。

我看到很多 Agent 产品把执行过程隐藏得很深,只给一个最终答案。这样做不好。尤其是 Agent 会调用工具获取信息时,用户需要知道信息从哪里来、什么时候完成的。这个“可解释性”工作,制造上做不了,后端不一定愿意做,但前端正好能接住。

5.2 用前端思维做 Agent 调试工具

前端经常跟开发者工具打交道,也会实现错误上报和性能监控。到了 Agent 开发,这套思维可以直接平移。

我自己后来给内部同事做了一套简易调试面板,核心功能就是展示消息数组的完整结构。每条消息都可以展开,能看到模型返回的原始 JSON、工具调用参数、工具返回值。同事看完之后说,原来 Agent 出问题不像想象中那么玄学,把每一步拉出来看就清楚了。

这个调试面板本质上就是浏览器开发者工具的 Agent 版本。可以用最基础的前端技术二三十行代码实现,但它对团队排查问题的作用非常大。我甚至觉得,这个方向比单纯给 Agent 写 UI 更像前端在 Agent 时代的杀手锏。因为开发者体验也是一种用户体验。

5.3 真正需要补的新知识没有你想的那么多

总结下来,前端切入 Agent 开发,真正需要从零学的新知识只有几块:模型服务的基本调用方式、函数调用或工具调用的协议格式、消息上下文的管理策略、以及如何设计稳定的工具描述。

不需要学反向传播,不需要准备训练集,也不需要理解注意力机制。如果你在面试或团队汇报里被人用这些问题考,你可以客气地反问一句:你希望我训练一个私有模型,还是希望我用现有模型做业务闭环?这两个问题的工程难度完全不同。

我自己从开始写到第一个能自动化完成订单查询的 Agent,前后只用了一个晚上。当然,要把它做成稳定、可观测、有人用的产品,还需要更多轮迭代。但至少它证明了一件事:Agent 开发是一个工程问题,跟模型训练没什么关系。前端完全有能力在这个领域里站稳,并且贡献出非常独特的价值。

最后分享一个小经验:不要一开始就想着“我要做一个通用助手”,而是挑一个你每天都在做的具体任务,比如查订单、整理晨会纪要、归类工单,把它做小做透。你会发现,当你能透彻地向模型解释清楚一个具体业务时,你已经迈过 Agent 入门那道门槛了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进