1. 从一个前端视角重新理解 Agent 开发
1.1 为什么前端工程师会对 Agent 开发产生误解
我做了七八年前端,从 jQuery 时代一路写到现在的组件化框架,日常打交道最多的就是状态管理、组件通信、接口联调这些事。第一次听到“Agent 开发”这个词的时候,我的直觉反应是:这不就是搞个大模型然后调 API 吗?再往下想,就觉得肯定得懂模型训练、微调、数据集清洗那一套,跟我一个写页面的有什么关系。
后来真正上手做了两个 Agent 相关的项目,才发现这个认知偏差大得离谱。Agent 开发的核心根本不是训练模型,而是编排逻辑。你不需要知道 Transformer 的注意力机制怎么算,也不需要自己跑梯度下降,你需要的是把大模型当成一个能力很强的“外部服务”,然后围绕它设计一套可靠的调度系统。
这个认知转变对我影响很大。打个比方,模型训练像是造发动机,Agent 开发像是造整车。你不需要会造发动机才能造车,你需要的是知道发动机的输出特性、什么时候给油、什么时候刹车、怎么把发动机的动力传到轮子上。前端工程师在这件事上其实有天然优势,因为我们每天都在做的事情——状态流转、事件驱动、异步编排、错误兜底——本质上就是 Agent 开发的底层逻辑。
1.2 Agent 到底是什么:用前端概念做类比
如果把大模型比作一个函数,那它就是一个纯函数:输入一段文本,输出一段文本,没有副作用,没有记忆,每次调用都是独立的。Agent 要做的就是给这个纯函数加上“手脚”和“记忆”。
用前端的概念来映射:
- 大模型= 一个远程 API,类似你调后端接口,输入参数返回结果
- Prompt= 请求参数,只不过这个参数是自然语言
- 工具调用(Tool Use)= 事件回调,模型说“我要调这个函数”,你的代码去执行然后返回结果
- 记忆(Memory)= 本地状态管理,类似 Redux 或者 Pinia 里存的数据
- 规划(Planning)= 一个状态机或者流程编排,决定下一步做什么
- Agent 循环= 一个 while 循环,不断“思考-行动-观察”直到任务完成
这么一类比,你会发现 Agent 开发对前端来说根本不陌生。你写过一个带轮询的订单状态页面吗?你处理过多个异步请求的依赖关系吗?你做过表单校验的错误提示和重试吗?这些经验直接就能迁移过来。
1.3 模型训练和 Agent 开发的分界线在哪
这里必须把边界划清楚,不然很容易陷入“我是不是得先学机器学习”的焦虑里。
模型训练做的事情是:准备数据集、设计损失函数、跑训练循环、调超参数、评估指标、部署模型。产出物是一个模型文件或者一个推理服务。
Agent 开发做的事情是:设计交互流程、编写系统提示词、定义工具接口、管理对话上下文、处理异常和重试、控制成本和延迟。产出物是一个能完成特定任务的应用程序。
两者的交集只有一个:你都需要调用模型。但训练是“造”模型,Agent 是“用”模型。就像你开发网页不需要自己写浏览器内核一样,你做 Agent 不需要自己训练模型。
我踩过的坑就是一开始花了两周去看深度学习的入门课,看到反向传播就卡住了,后来发现这些东西在我实际做 Agent 的时候一个都用不上。真正用得上的是:怎么把任务拆解清楚、怎么让模型稳定输出结构化数据、怎么处理模型“胡说八道”的情况。这些全是工程问题,不是算法问题。
2. 前端做 Agent 开发的天然优势与核心差异
2.1 前端工程师已经具备的四个核心能力
我仔细想过为什么前端转 Agent 开发比后端或者算法同学在某些方面更顺。总结下来有四个能力是前端日常就在练的:
第一是异步编排能力。前端天天跟 Promise、async/await 打交道,处理并发请求、串行依赖、超时重试这些是家常便饭。Agent 的执行流程本质上就是一连串异步操作的编排,模型调用是异步的,工具执行是异步的,多个步骤之间有依赖关系。这套东西前端闭着眼睛都能写。
第二是状态管理能力。Agent 需要维护对话历史、工具调用结果、中间状态。这跟前端管理复杂表单状态、跨组件通信没有本质区别。你知道什么时候该把状态提升,什么时候该用全局 store,什么时候该做持久化。这些直觉直接适用于 Agent 的上下文管理。
第三是用户体验思维。Agent 执行任务可能需要好几秒甚至几十秒,用户不能干等着。前端知道怎么做 loading 状态、怎么做流式输出、怎么做进度提示、怎么做中断取消。这些体验细节决定了 Agent 产品好不好用,而算法同学往往不关注这些。
第四是错误处理意识。前端面对的是各种不确定的浏览器环境和网络状况,早就养成了防御性编程的习惯。Agent 调用模型会失败、工具会报错、输出格式会不符合预期,这些都需要兜底。前端在这方面的经验比谁都丰富。
2.2 思维方式的三个关键转变
虽然有优势,但从写页面转到写 Agent,有三个思维习惯需要调整。
从确定性到概率性。前端代码是确定的,你写if (a === 1)它就一定走那个分支。但模型输出是不确定的,同样的输入可能给你不同的结果。你不能假设模型一定按你想要的格式返回,必须做校验和容错。我一开始写 Agent 的时候,直接JSON.parse模型的输出,结果十次有三次报错,后来加了格式约束和重试机制才稳定下来。
从同步思维到循环思维。前端的主逻辑是事件驱动的,用户点一下你响应一下。Agent 的主逻辑是一个循环:模型思考、决定行动、执行行动、把结果喂回模型、继续思考。这个循环可能跑几轮也可能跑几十轮,你需要设计好终止条件,不然可能死循环烧钱。
从功能实现到效果调优。前端做完功能基本就完事了,但 Agent 做完功能只是开始。提示词怎么写效果更好、工具怎么描述模型才用得对、上下文怎么裁剪才不丢关键信息,这些都需要反复调试。同一个任务,提示词改几个字,成功率可能从 60% 跳到 90%。
2.3 不需要深入但需要了解的模型知识
虽然不用训练模型,但有几个概念还是得知道,不然调优的时候会一头雾水。
Token 的概念。模型处理文本是按 token 算的,一个中文字大概 1 到 2 个 token,英文一个单词大概 1 到 1.5 个 token。这直接关系到成本和上下文长度限制。你得知道你的提示词加历史对话加工具返回结果总共占了多少 token,超了就得裁剪。
上下文窗口。每个模型有一个最大 token 数限制,比如 8K、32K、128K。超过这个限制模型就处理不了了。Agent 跑多轮之后上下文会越来越长,必须做管理,要么摘要压缩,要么只保留最近几轮。
温度参数。这个参数控制输出的随机性。做 Agent 的时候一般调低一点,让它输出更稳定、更确定。做创意类任务可以调高。我一般设 0.1 到 0.3 之间。
结构化输出。让模型按 JSON 格式返回是 Agent 开发中最常用的技巧。不同模型对 JSON 模式的支持程度不一样,有的可以直接指定 response_format,有的只能靠提示词约束。这个后面会详细讲。
这些知识花半天就能了解清楚,不需要深入数学原理,知道它们影响什么、怎么调就行。
3. Agent 核心架构的拆解与实现思路
3.1 最小可用 Agent 的四个组成部分
一个能跑起来的 Agent,最少需要四个部分。我用一个“自动整理会议纪要”的例子来串讲,这个场景比较直观。
第一部分是系统提示词。这是给模型设定角色和规则的地方。比如:“你是一个会议纪要整理助手,用户会给你一段会议录音的文字记录,你需要提取出决议事项、待办任务和负责人,按指定格式输出。”系统提示词决定了模型的行为边界,写得好不好直接决定效果。
第二部分是工具集。工具就是模型可以调用的函数。在这个例子里,工具可能包括:读取文本文件、写入 Markdown 文件、发送邮件通知。每个工具需要定义名称、描述、参数结构。模型根据描述来判断什么时候该调哪个工具。
第三部分是执行循环。这是 Agent 的心脏。基本流程是:把系统提示词和用户输入发给模型,模型返回要么是最终答案,要么是一个工具调用请求,执行工具把结果追加到对话里,再发给模型,如此循环直到模型给出最终答案或者达到最大轮数。
第四部分是上下文管理。会议记录可能很长,对话几轮之后 token 就爆了。你需要决定保留哪些信息、丢弃哪些信息、怎么压缩历史。常见做法是保留系统提示词和最近 N 轮对话,更早的做摘要。
这四个部分加起来可能就两三百行代码,但已经能完成很多实际任务了。不要一上来就追求复杂的多 Agent 协作、向量数据库、长期记忆,先把最小闭环跑通。
3.2 工具调用的实现细节与常见坑
工具调用是 Agent 区别于普通聊天机器人的关键。实现上有几个细节容易踩坑。
工具描述要写清楚。模型是根据工具的名称和描述来决定调不调的。描述写得太简单,模型可能该调的时候不调,不该调的时候乱调。比如一个“查询天气”的工具,描述写成“查天气”就不如“根据城市名称查询当前天气状况,返回温度和天气描述”来得清楚。
参数结构要严格。用 JSON Schema 定义参数,指定哪些是必填、哪些是可选、类型是什么。模型有时候会漏传参数或者传错类型,你的代码要做校验,校验不过就把错误信息返回给模型让它重试。
工具执行要幂等。模型可能因为超时或者格式问题重复调用同一个工具。如果你的工具是“发送邮件”,重复调用就会发两封。所以要么做幂等设计,要么在执行前检查是否已经执行过。
错误信息要有用。工具执行失败的时候,返回给模型的错误信息要具体。返回“执行失败”模型不知道怎么改,返回“城市名称参数为空,请提供有效的城市名”模型就知道下一步该怎么做了。
我实测下来,工具调用的稳定性很大程度上取决于描述的质量。花十分钟优化工具描述,比花一小时调提示词效果还好。
3.3 上下文管理的策略与取舍
上下文管理是 Agent 开发中最容易被低估的部分。做 demo 的时候对话就几轮,感觉不到问题。一旦上生产,用户连续对话几十轮,上下文直接爆掉。
我常用的策略是分层管理:
- 固定层:系统提示词,永远保留,不参与裁剪
- 摘要层:把较早的对话压缩成一段摘要,保留关键信息
- 近期层:最近 5 到 10 轮完整对话,保证连贯性
- 工具层:工具调用的结果如果很长,只保留关键字段,原始数据存到外部
摘要的生成也是调模型来做,提示词大概是“请把以下对话压缩成 200 字以内的摘要,保留所有决议、数字和专有名词”。这个摘要会随着新对话不断更新。
取舍的核心是:什么信息丢了会导致任务失败,什么信息丢了只是体验差一点。决议和数字丢了任务就错了,寒暄和重复确认丢了无所谓。想清楚这个,裁剪策略就清楚了。
3.4 从单 Agent 到多 Agent 的演进时机
很多人一上来就想搞多 Agent 协作,觉得这样才高级。我的经验是:单 Agent 能解决的问题绝对不要上多 Agent。
多 Agent 的复杂度是成倍增加的。你要处理 Agent 之间的通信、任务分配、结果汇总、冲突解决。调试难度也大得多,一个任务失败了,你都不知道是哪个 Agent 的问题。
什么时候该上多 Agent?当任务可以清晰拆分成独立的子任务,且子任务之间不需要频繁交互的时候。比如一个“竞品分析”任务,可以拆成“收集资料 Agent”“数据分析 Agent”“报告撰写 Agent”,三个 Agent 串行工作,每个的输出是下一个的输入。这种场景多 Agent 是合理的。
但如果任务本身是交织的,需要频繁来回确认,那多 Agent 反而会互相干扰。我做过一个客服场景,一开始拆成“意图识别 Agent”和“回答生成 Agent”,结果意图识别错了后面全错,还不如一个 Agent 端到端处理。
4. 实操:从零搭一个可用的 Agent
4.1 环境准备与技术选型
我用 Node.js 环境来演示,因为前端同学最熟悉。核心依赖就两个:一个 HTTP 客户端用来调模型 API,一个 Web 框架用来暴露接口。不需要什么特殊的 Agent 框架,自己写反而更可控。
mkdir my-agent && cd my-agent npm init -y npm install express axios dotenv模型 API 的调用就是普通的 HTTP 请求,跟你调后端接口一模一样。把 API Key 放到环境变量里,不要硬编码在代码里。
// .env MODEL_API_KEY=your_key_here MODEL_API_URL=https://api.example.com/v1/chat/completions技术选型上我的建议是:初期不要用 LangChain 这类框架。不是说它们不好,而是它们封装太厚,出问题的时候你根本不知道是哪一层的问题。自己用原生 HTTP 请求写一遍,把整个流程摸清楚,后面再用框架也不迟。
4.2 核心循环的代码实现
下面是一个最小 Agent 循环的实现,我加了详细注释。
async function runAgent(userInput, maxTurns = 10) { // 初始化对话历史 const messages = [ { role: 'system', content: SYSTEM_PROMPT }, { role: 'user', content: userInput } ]; for (let turn = 0; turn < maxTurns; turn++) { // 调用模型 const response = await callModel(messages); const message = response.choices[0].message; // 把模型的回复加入历史 messages.push(message); // 如果模型没有调用工具,说明它给出了最终答案 if (!message.tool_calls || message.tool_calls.length === 0) { return message.content; } // 执行所有工具调用 for (const toolCall of message.tool_calls) { const result = await executeTool( toolCall.function.name, JSON.parse(toolCall.function.arguments) ); // 把工具结果加入历史 messages.push({ role: 'tool', tool_call_id: toolCall.id, content: JSON.stringify(result) }); } } throw new Error('达到最大轮数,任务未完成'); }这个循环就是 Agent 的核心。maxTurns是安全阀,防止死循环。实际项目中我一般设 10 到 15 轮,超过基本说明任务设计有问题。
callModel就是发 HTTP 请求,executeTool是根据工具名分发到具体函数。这两个函数都很直白,不展开写了。
4.3 提示词工程的关键技巧
提示词是 Agent 的灵魂,但很多人写提示词太随意。我总结几个实操中验证有效的技巧。
用分隔符划分区块。系统提示词里通常包含角色设定、任务说明、输出格式、约束条件。用###或者---把它们分开,模型理解起来更清晰。
给例子比讲道理管用。与其写“请输出规范的 JSON”,不如直接给一个 JSON 示例。模型模仿能力很强,给例子它就能照着做。
约束要具体可执行。“回答要简洁”是模糊的,“回答不超过 100 字”是具体的。“格式要正确”是模糊的,“必须包含 name 和 age 两个字段,name 是字符串,age 是数字”是具体的。
把最重要的指令放最后。模型对提示词末尾的内容注意力更高。关键的格式要求、禁止事项放在最后强调一遍。
我常用的系统提示词结构是这样的:
### 角色 你是一个 XXX 助手。 ### 任务 你需要完成 XXX。 ### 可用工具 - tool_a: 用于 XXX - tool_b: 用于 XXX ### 输出格式 严格按照以下 JSON 格式输出: {"field1": "值", "field2": 123} ### 约束 - 不要编造不存在的信息 - 如果信息不足,调用工具获取 - 每次只调用一个工具4.4 结构化输出的强制与校验
让模型稳定输出 JSON 是 Agent 开发的基本功。有三种做法,可靠性递增。
第一种是纯提示词约束。在提示词里写清楚格式要求,给例子。简单但不可靠,模型偶尔会加解释文字或者漏字段。
第二种是用模型的 JSON 模式。很多模型 API 支持指定response_format: { type: 'json_object' },模型会保证输出合法 JSON。但字段结构还是可能不对。
第三种是加校验和重试。拿到输出后用 JSON Schema 校验,不通过就把错误信息返回给模型让它重新生成。这是最可靠的,我生产环境都用这种。
async function getStructuredOutput(messages, schema, maxRetry = 3) { for (let i = 0; i < maxRetry; i++) { const response = await callModel(messages, { jsonMode: true }); try { const data = JSON.parse(response); validateSchema(data, schema); // 校验字段和类型 return data; } catch (err) { // 把错误信息追加到对话,让模型修正 messages.push({ role: 'user', content: `输出格式有误:${err.message},请重新输出。` }); } } throw new Error('多次重试后仍无法获得合法输出'); }这个重试机制看起来简单,但能把结构化输出的成功率从 80% 提到 99% 以上。
5. 常见问题与排查技巧实录
5.1 模型不调用工具怎么办
这是新手最常遇到的问题。你定义好了工具,但模型就是不用,直接自己编答案。
排查顺序是这样的:先看工具描述是不是太模糊,模型不知道什么时候该用。再看系统提示词有没有明确要求“信息不足时必须调用工具”。最后看是不是模型能力问题,换个更强的模型试试。
我遇到过一个典型案例:一个查询订单状态的工具,描述写的是“查询订单”。模型经常不调,直接回复“请提供订单号”。后来描述改成“根据订单号查询订单的当前状态,包括已下单、已发货、已签收。当用户询问订单进度时必须调用此工具”,调用率立刻上来了。
5.2 工具调用参数错误的处理
模型传的参数不对,比如该传数字传了字符串,该传数组传了单个值。这种情况不要直接报错给用户,而是把错误信息返回给模型让它自己修正。
async function executeTool(name, args) { try { const tool = tools[name]; if (!tool) { return { error: `工具 ${name} 不存在,可用工具:${Object.keys(tools).join(', ')}` }; } const validated = validateArgs(args, tool.schema); return await tool.handler(validated); } catch (err) { // 返回结构化错误,方便模型理解 return { error: `参数错误:${err.message}。请检查参数格式后重试。` }; } }关键是错误信息要具体,告诉模型哪里错了、应该怎么改。泛泛的“执行失败”模型没法修正。
5.3 上下文超长的应急处理
跑着跑着突然报 token 超限,这是生产环境常见问题。应急处理是立刻裁剪历史,保留系统提示词和最近三轮对话,把更早的丢掉。但这会丢失信息,可能导致任务失败。
更好的做法是提前预防。在每次调用模型前估算 token 数,接近上限就触发摘要。估算可以用简单的字符数除以 2 来粗略计算,中文场景下误差不大。
function estimateTokens(messages) { return messages.reduce((sum, m) => { const content = typeof m.content === 'string' ? m.content : JSON.stringify(m.content); return sum + Math.ceil(content.length / 2); }, 0); } // 调用前检查 if (estimateTokens(messages) > MAX_TOKENS * 0.8) { messages = await compressHistory(messages); }留 20% 的余量是因为模型输出也要占 token,不能把输入塞满。
5.4 成本和延迟的优化手段
Agent 跑一轮可能调好几次模型,成本和延迟都是问题。几个实用的优化手段:
缓存重复调用。同样的输入如果之前调过,直接返回缓存结果。对于工具调用结果这种确定性内容特别有效。
并行执行无依赖的工具。如果模型一次返回多个工具调用,且它们之间没有依赖,用Promise.all并行执行,能省不少时间。
用小模型做简单判断。意图识别、参数提取这类简单任务用便宜的小模型,复杂推理再用大模型。成本能降一半以上。
流式输出改善体验。虽然不减少总延迟,但用户能早点看到内容,感知上快很多。前端处理流式输出是强项,SSE 或者 fetch 的 ReadableStream 都能做。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决手段 |
|---|---|---|---|
| 模型不调工具 | 描述模糊或提示词未要求 | 检查工具描述和系统提示词 | 补充使用场景说明,明确要求 |
| 参数格式错误 | 模型理解偏差 | 查看实际传入参数 | 加校验和错误回传重试 |
| 输出非 JSON | 约束不够强 | 检查提示词格式要求 | 开启 JSON 模式加 Schema 校验 |
| 上下文超限 | 历史累积过多 | 估算 token 数 | 摘要压缩或裁剪历史 |
| 死循环 | 终止条件缺失 | 检查循环退出逻辑 | 设最大轮数,检测重复调用 |
| 响应太慢 | 串行调用过多 | 分析调用链路 | 并行化无依赖步骤,换小模型 |
| 成本过高 | 大模型用太多 | 统计 token 消耗 | 分级用模型,加缓存 |
这张表是我踩坑踩出来的,基本覆盖了 80% 的常见问题。遇到新问题先对照排查,能省不少时间。
6. 从能跑到好用:生产化的几个关键点
6.1 可观测性建设
Demo 能跑不代表生产能用。生产环境最需要的是可观测性,出问题能快速定位。
至少要记录这些信息:每次模型调用的输入输出、token 消耗、耗时;每次工具调用的参数和结果;整个任务的轮数和最终状态。用结构化日志,方便检索和分析。
我一般会做一个简单的 trace 系统,给每个任务分配一个 ID,所有相关日志都带上这个 ID。出问题的时候用 ID 一搜,整个执行链路清清楚楚。
6.2 降级与兜底策略
模型服务会挂,网络会抖,这些都要有预案。
模型调用失败时,重试两到三次,还失败就降级到备用模型或者返回预设的兜底回复。工具调用失败时,把错误信息返回给模型让它决定下一步,而不是直接中断整个任务。
最重要的是设置超时。单个模型调用超过 30 秒就中断,整个任务超过 2 分钟就终止。没有超时控制的 Agent 在生产环境就是定时炸弹。
6.3 效果评估与迭代
Agent 的效果不能靠感觉,要有量化指标。我常用的指标是任务完成率和平均轮数。完成率反映能力,轮数反映效率。
建一个测试集,包含 20 到 50 个典型任务,每次改动提示词或者工具后跑一遍,对比指标变化。这样能避免“改了一个地方,修好了 A 却弄坏了 B”的情况。
评估的时候要注意,模型输出有随机性,同一个测试跑三次结果可能不一样。所以要看整体趋势,不要纠结单次波动。
7. 我在这条路上的几点真实体会
做了几个 Agent 项目之后,最大的体会是:Agent 开发的门槛在工程不在算法。你能不能做出好用的 Agent,取决于你对任务的理解、对流程的设计、对异常的考虑,而不是你懂多少模型原理。
前端转过来做 Agent,最大的优势是工程思维和用户视角。你知道怎么把复杂流程拆成可管理的模块,知道怎么处理异步和错误,知道用户需要什么样的反馈。这些能力比会调模型参数值钱得多。
另一个体会是:不要追求一步到位。我见过太多人一上来就想做通用 Agent,结果什么都做不好。从一个具体场景切入,把单点做到 90% 的成功率,比做一个什么都能干但什么都干不好的通用 Agent 有价值得多。
最后说个实际的:这个领域变化很快,但底层的东西变化很慢。工具调用的模式、上下文管理的策略、错误处理的原则,这些核心概念从我开始做到现在没怎么变过。把精力放在这些不变的东西上,比追新框架新概念划算。
如果你也是前端,想试试 Agent 开发,我的建议是:找一个你自己工作中的重复性任务,用 Agent 把它自动化掉。不用追求完美,先跑通闭环。跑通之后你会发现,后面所有的优化都有了方向。