1. 写在前面这一节要解决什么问题如果你是从前端转过来学 Agent 开发的看到“Agent”这个词多半已经很熟悉了。不过前面几节我们讲过的都是单 Agent、简单工具调用、思维链提示词这类基础。到了第六节我认为是时候拉开一点距离把“前端思维”真正用起来——不是继续做“会调 API 的工程师”而是开始思考 Agent 系统的整体架构、状态流转、多角色协作以及最重要的怎么像调试前端一样去调试一个 Agent 系统。老实说前端转 Agent 开发这件事过去两年我见过很多同行想做但卡住。卡住的主要原因并不是不会写代码而是习惯了“页面 — 事件 — 接口 — 状态 — 渲染”这条确定性链路突然面对一个“让模型自己决定下一步做什么”的系统会有一种四肢健全但不会走路的别扭感。所以这一节的定位就是带大家完成一次思维模式上的切换从组件化到智能体编排从状态管理到上下文管理从监视网络请求到追踪模型决策。这一节适合谁已经跑通一个简单的 Agent Demo、会调用大模型 API、知道 Tool Calling 是什么但是还没做出一个成体系项目的人。这一节结束后你不仅能把前端技能迁移过来还能用一套相对完整的方法去设计 Agent 产品。2. 前端技能树里哪些是 Agent 开发的地基2.1 组件化思维与 Agent 工具箱设计前端开发者最熟悉的一件事就是把 UI 拆成组件。按钮是组件、表单是组件、整个页面还是组件组件之间通过 props 和 events 通信。这个思维放在 Agent 开发里简直天然契合。Agent 的工具函数Tool / Function本质上就是组件。每一个工具函数需要定义清楚三件事输入协议、输出协议、副作用。拿前端来类比一个 React 组件要声明 props 类型输入协议渲染出什么结构输出协议以及在生命周期里做哪些请求或存储副作用。工具函数也是这样甚至更严格因为调用方不是你自己而是一个可能产生幻觉的大模型。我见过很多前端转过来的朋友包括我自己初期容易犯一个毛病工具函数写得像页面里的工具方法参数随便传返回一个字符串就行。这在传统后端接口设计里还算能忍但在 Agent 里会直接导致模型乱传参数、解析失败、甚至逻辑错乱。正确做法是在设计工具函数时像封装一个高内聚组件一样把入参 schema 写严谨能枚举的字段就枚举能设默认值就设默认值返回结构也尽量用固定 JSON 而不是自由文本。模型是很擅长“照着说明书发挥”的如果你的说明书含糊它的发挥就会变得很可怕。2.2 状态管理的迁移从 Redux/Pinia 到会话上下文前端的状态管理解决的是“多组件之间共享数据”的问题Redux 的 Store、Pinia 的 State 本质上都是想让数据流可预测。Agent 开发里也有类似问题而且更棘手因为 Agent 的状态不是一个全局对象而是大模型上下文窗口里的一段段文本。这个迁移很有意思前端的 state 是强类型、结构化、可序列化的Agent 的“state”是半结构化的对话历史加工具执行结果缓存。我们当然可以用代码声明一个 ConversationState 对象来维护状态但模型能看到的只能是序列化后的文本或结构化消息模型理解的“当前状态”和我们代码里的状态之间存在信息差。我目前总结下来比较稳的模式是“内外双状态”外层代码里维护一个结构化的状态对象比如订单状态、用户偏好、任务进度负责逻辑控制。内层每次构造消息上下文时把关键状态注入 system prompt 或作为最近几条消息的一部分让模型在生成时能感知到这些状态。这很像前端的“单一数据源”思想Store 是唯一真相组件只通过 action 修改它在 Agent 里代码状态是唯一真相模型每轮决策前从真相中抽取“摘要”塞进上下文。别直接让模型去修改代码状态而是让它输出结构化指令由代码来执行变更。2.3 异步事件驱动与流式输出的本能前端开发对异步和事件流的敏感度是极高的。Pormise、RxJS、WebSocket、SSE 这些概念对前端来说都是日常。到了 Agent 开发这些能力直接变成吃饭的本事。现在的 Agent 应用几乎都是流式输出模型 token 一个接一个蹦出来UI 要边收边渲染。更复杂的是工具调用过程中还要实现“暂停生成 — 执行工具 — 继续生成”的循环。这种控制流放在 Node.js 里用 Async Generator 或者 Stream 协议来处理非常顺手。前端同学理解这些通常没有障碍缺的只是把“DOM 更新”换成“事件总线上的消息分发”而已。而且在做 Agent 编排引擎时事件驱动架构几乎是必然选择工具执行完毕是一个事件、模型响应结束是一个事件、子 Agent 回复完成是一个事件。整个系统像一个复杂的前端应用只是把 onClick 换成了 onToolCallCompleted。3. 从零搭一个能自我纠错的 Agent实操拆解3.1 需求定义与系统边界我记得第一次独立做 Agent 项目时最让我不适应的不是写代码而是“需求边界模糊”。前端开发再复杂交互逻辑总是可以穷举的但 Agent 项目的边界天然是模糊的用户问一个问题模型可能走任何路径去回答。所以我强烈建议第一步不是写代码而是把系统边界画出来。我以“做一个能自主完成软件技术调研并输出报告的 Agent”为例这个用例很典型既用到网页检索、信息抽取又需要多步推理。我定义的系统边界如下输入一个调研主题一句话输出一份结构化 Markdown 调研报告包含摘要、分点分析、参考链接、结论Agent 可使用的工具搜索引擎、网页内容抓取、当前时间查询不允许 Action不允许访问本地文件系统不允许执行任意代码不允许内部跳转指令这个边界决定了这个 Agent 是一个“受限工具型 Agent”不涉及代码执行和多 Agent 协作但已经足够展示核心机制。3.2 工具层封装一个工具就是一个小型服务工具封装我建议用 JSON Schema 来描述输入输出让大模型能够准确理解和使用。下面是一个搜索工具的封装示例我从实战中总结出的最佳实践是参数描述尽量带上格式约束和枚举范围让模型第一次就传对。// searchTool.ts import { z } from zod; export const searchTool { name: web_search, description: 在互联网上搜索指定关键词返回前5条结果的标题、链接和摘要。适合用于获取最新信息、查找资料、确认事实。, parameters: z.object({ query: z.string().describe(搜索关键词建议使用精确的关键短语而不是完整句子例如AI Agent 架构模式), max_results: z .number() .int() .min(1) .max(10) .default(5) .describe(返回的结果数量默认5最大10), }), async execute(args: { query: string; max_results?: number }) { const res await fetchSearchApi(args.query, args.max_results ?? 5); return res.map((item) ({ title: item.title, url: item.url, snippet: item.snippet, })); }, };注意几个关键点description 里说明“什么时候用”和“什么时候不要用”会显著提升调用准确率。这就像给组件写文档时说明使用场景一样。参数用 zod 做实时校验模型传错参数时能捞回来不至于直接把错误抛给用户。执行函数返回的是纯数据不包含渲染格式由模型自己决定如何组织语言。我再加一个网页抓取工具// fetchPageTool.ts import { z } from zod; export const fetchPageTool { name: fetch_page_content, description: 抓取指定URL的正文内容提取标题、核心段落、关键列表过滤掉导航、广告和脚本。适合深入了解搜索结果中的具体文章。, parameters: z.object({ url: z.string().url().describe(目标网页的完整URL例如https://example.com/article), }), async execute(args: { url: string }) { const content await fetchAndExtractMainContent(args.url); return { title: content.title, paragraphs: content.paragraphs.slice(0, 20), links: content.links.slice(0, 20), }; }, };给模型返回网页正文时务必截断并抽取关键内容而不是把整个 500KB 的文章全文塞进上下文否则既浪费 token 又稀释注意力。3.3 主循环设计Observe-Think-Act 的工程化实现Agent 的主循环是核心中的核心。前端的 requestAnimationFrame 循环是每一帧重新渲染页面Agent 的循环则是“模型推理 — 决定动作 — 执行动作 — 观察结果 — 再推理”一直循环到任务完成为止。工程实现上我用 TypeScript 写一个最简的循环控制。这里的关键是让循环具备“任务终点判断”能力否则模型会无限调用工具。// agentLoop.ts import OpenAI from openai; interface AgentContext { messages: Array{ role: system | user | assistant | tool; content: string; tool_call_id?: string }; result?: string; } export class ToolAgent { private client: OpenAI; private tools: any[]; private systemPrompt: string; private maxIterations 8; private ctx: AgentContext; constructor(systemPrompt: string, tools: any[]) { this.systemPrompt systemPrompt; this.tools tools; this.client new OpenAI(); this.ctx { messages: [{ role: system, content: systemPrompt }] }; } async run(userInput: string) { this.ctx.messages.push({ role: user, content: userInput }); for (let i 0; i this.maxIterations; i) { const response await this.client.chat.completions.create({ model: gpt-4o, messages: this.ctx.messages, tools: this.tools.map((t) ({ type: function as const, function: { name: t.name, description: t.description, parameters: t.parameters.toJSON?.() ?? t.parameters, }, })), tool_choice: auto, }); const msg response.choices[0].message; if (msg.tool_calls msg.tool_calls.length 0) { this.ctx.messages.push({ role: assistant, content: msg.content ?? , tool_call_id: undefined, }); for (const call of msg.tool_calls) { const tool this.tools.find((t) t.name call.function.name); console.log([Agent] 调用工具: ${call.function.name}(${call.function.arguments})); let resultJson ; try { const parsedArgs JSON.parse(call.function.arguments); const result await tool.execute(parsedArgs); resultJson JSON.stringify(result); } catch (e: any) { resultJson JSON.stringify({ error: e.message }); } this.ctx.messages.push({ role: tool, content: resultJson, tool_call_id: call.id, }); } } else { // 没有工具调用表示模型已经给出最终回答 this.ctx.result msg.content ?? ; return this.ctx.result; } } throw new Error(达到最大迭代次数任务未完成); } }这个实现里我埋了一个关键细节每次工具调用的结果都作为 roletool 的消息追加进上下文。这样模型在下一次推理时可以看到“我调用了工具工具返回了这样的结果”从而继续判断是否还要调用其他工具或者是否足够输出最终答案。这个主循环的本质就是我在 2.2 里说的“内外双状态”的外层实现代码控制循环模型负责决策工具执行是纯函数。3.4 自我纠错从失败中恢复Agent 和传统程序最大的不同之一是“一次执行很可能不成功”但可以自己纠正。前端里我们用 try-catch 捕捉异常并提示用户重试Agent 里应该把异常和提示信息直接塞回上下文让模型看到错误是什么然后自己调整策略。我在 3.3 的代码里已经做到了一点工具执行出错时返回一个包含 error 字段的 JSON。但这还不够更好的做法是给模型一个“错误自检提示”比如搜索失败时提示模型换一组关键词再试或者换一个信息源。我这里放一个简单的策略实现就是在 system prompt 里追加这么一段当某个工具调用返回 error 时 1. 先阅读 error 信息判断是参数错误、网络错误还是内容抓取受限。 2. 如果是参数错误修正参数后重试一次。 3. 如果是网络错误或内容抓取受限换用 web_search 重新搜索或者从搜索结果中选择另一个 URL。 4. 如果两次尝试仍然失败就在最终报告中如实说明“某些信息未能获取到”。这套提示比任何代码都更能提升成功率。因为模型本质上是在做“增强型试错”给它适当的指导它就能做出像人类一样合理的失败降级。4. 从单 Agent 到多 Agent前端人最容易理解的协作模式4.1 模式一主管与职员模式Supervisor / Worker单 Agent 的瓶颈在于上下文有限、模型视野单一、任务一旦被长流程拖累输出质量会断崖下跌。这时候就需要切换思路——不是让一个 Agent 干所有的活而是把任务拆给多个专职 Agent。我之前一直觉得多 Agent 很高深直到有一天我意识到这不就是前端微前端架构里的“主应用 子应用”模式吗主应用Supervisor负责路由分发、生命周期管理子应用Worker各自负责自己的业务域。在实际代码里这种模式通常表现为一个 Supervisor Agent 负责拆解任务、分配下属、汇总结果若干 Worker Agent 各自具备专用工具和专属 Prompt。他们之间的“通信”不直接发生全部通过 Supervisor 中转消息来实现。这样设计的好处是单点可控坏处是 Supervisor 的上下文会成为瓶颈——所有消息都要过它的窗口。所以这种模式适合任务可拆解但各子任务相对独立的场景。4.2 模式二流水线模式Pipeline流水线模式里每个 Agent 只负责一个环节前一个 Agent 的输出是后一个 Agent 的输入。跟前端工程化里的构建流水线非常像编译 → 压缩 → 打包 → 部署每个环节输入输出都是标准的。这个模式适合流程固定、步骤顺序明确的场景。比如需求分析 Agent → 方案设计 Agent → 代码实现 Agent → 代码审查 Agent。每一环的输出是结构化的比如 JSON下一环拿到 JSON 继续干活互不越权。工程实现上流水线模式其实最简单而且我看过很多确实在线上这么跑的产品。比起 Supervisor 模式它的优点是链路易追踪、成本可控缺点是灵活性低中间一旦出现意外分支就不好处理。4.3 多 Agent 的共享上下文机制不管哪种模式都要面对一个现实问题多 Agent 之间如何共享信息前端里我们把公共状态放在 Store各组件从 Store 取多 Agent 系统里则要把共享结论、中间产物、当前进度放在一个“共享黑板”Shared Blackboard里。我最开始搭多 Agent 系统时犯过一个错试图把完整的任务背景塞给每一个 Agent。结果每个 Agent 的上下文都爆炸模型注意力被稀释回答质量反而变差。后来改成“按需注入”——每个 Agent 启动时只接收与它职责相关的摘要和关键数据而不是全量上下文。这个设计确实踩过几次坑之后才明白的多 Agent 不是上下文越多越好而是每个 Agent 只拿到“刚刚好”的信息。你给模型一堆无关信息它能给你写出让人啼笑皆非的答案。// blackboard.ts class Blackboard { private store: Recordstring, unknown {}; set(key: string, value: unknown) { this.store[key] value; } get(key: string) { return this.store[key]; } snapshot(keys: string[]) { const out: Recordstring, unknown {}; for (const k of keys) { if (this.store[k] ! undefined) out[k] this.store[k]; } return out; } } export const blackboard new Blackboard();5. 让 Agent 说人话输出与 UI 层的工程配合5.1 流式输出的前端消费Agent 生成的报告通常是流式的由于模型分步推导、逐步生成前端要做的不是等所有内容都生成完再一次性渲染而是用 SSE 或 ReadableStream 边收边渲染。如果还是以 React 为例可以在 useEffect 里 fetch 一个 SSE 端点把收到的增量 token 逐段 append 到 state 中配合 Markdown 实时编译实现打字机式的报告呈现效果。这个大家应该很熟毕竟很多聊天框都已经这么做了。我提一个容易被忽略的点流式输出时如果 Agent 在中间调用了工具比如搜索流式输出会中断几秒到十几秒。这时 UI 必须给出“正在执行工具”的可视化状态否则用户会以为页面卡死了。我一般会在流里插入结构化事件帧比如{type: tool_call, tool: web_search, status: running}前端监听这个事件帧渲染一个“正在搜索相关资料”的提示用户体验会好非常多。5.2 把工具执行痕迹展示成“可追踪轨迹”前端有 DevTools 的 Network 面板可以看每一个请求的状态和耗时。Agent 应用同样需要这种透明度用户必须能感知 Agent 做了什么、为什么这么做。这算是我觉得最值得推广的实践经验在 UI 增加一个“推理过程”折叠面板把 Agent 的每一次工具调用、调用参数、返回摘要、时间戳都记录并渲染出来。这不仅能建立用户信任还能帮你调试。用户对 Agent 的信任度和你能看到它的工作过程呈强相关。一个黑盒的 Agent 很难让人放心使用。6. 质量保障与评测没有测试的 Agent 是一场灾难6.1 传统测试为什么不够用前端测试有单元测试、集成测试、E2E 测试断言很明确点击按钮期望弹出弹窗。Agent 的测试很难断言因为输出是自然语言任务的正确答案不止一种。我目前比较推崇的做法是“三层评测法”第一层工具层测试。每个工具函数独立测试断言输入输出的 JSON 结构正确异常情况返回错误 JSON。这一层是确定性测试由传统单测覆盖。第二层轨迹层测试。给 Agent 一个输入检查它调用工具的轨迹是否符合预期。比如调研任务必须调用至少一次搜索工具、一次页面抓取工具。只要轨迹对模型输出的措辞差一点没关系。第三层结果层测试。用另一个更强模型或人工回放对最终报告打分维度包括信息准确度、覆盖度、结构合理性。这层成本高适合上线前的抽测。6.3 评测数据的积累我推荐从第一天就开始积累评测用例集。每发现一次线上 bug、一次低质量输出就把这个输入记录下来修复后跑一遍回归。这就像前端引入 Storybook 沉淀组件用例一样时间越长用例集越有价值。我现在的做法是每个评测用例包括输入、期望工具调用轨迹、期望输出关键词/结构、历史失败原因。手工跑 脚本跑结合每周出一份对比报告。坚持两个月你的 Agent 系统的稳定性会有质的提升。7. Agent 面试/工作里前端出身的人最容易被问什么7.1 核心考点从“会调 API”到“会设计系统”最近一两年AI 应用开发者的面试风格已经变了。问的不再是“Prompt 怎么写的”这种入门题而是操作系统级的问题“你如何设计一个支持多轮对话且不会跑偏的 Agent 系统”“工具调用失败时你的系统怎么降级”“多个工具并发调用时你怎么管理共享状态”“如果模型输出 JSON 不合法你的容错策略是什么”“你如何评估一个 Agent 版本比上一个版本好”这些问题的背后是考察你有没有从“Demo 型选手”升级到“工程型选手”。前端经历在这个环节特别加分因为你会状态管理、会组件设计、会模块化、会监控前端异常这些都直接迁移过来。7.2 前端转 Agent 的技术栈建议如果让我列一个前端转 Agent 开发的最短学习路径大概是这样的语言与运行时TypeScript Node.js / Bun前端同学上手成本最低生态也比较成熟。框架层LangChain.js、Vercel AI SDK 两者可以先用后者因为对前端更友好上手快。工具层Playwright网页自动化与抓取、Puppeteer、cheerio内容解析。存储层Redis会话缓存、SQLite/PostgreSQL结构化状态、向量数据库Qdrant/pgvector做记忆检索。可观测性LangSmith 或自己埋点打日志反正一定要能看到每一步的执行轨迹。7.3 面试实操手写一个最小 Agent 循环面试里如果被要求手写代码十有八九是让写一个最小 Agent 循环不超过五十行那种。核心是考察你清不清楚 tool_calls → 执行 → tool 消息回传 → 再调模型这个闭环。不管用什么语言只要把闭环讲清楚并且能解释每一步的信息流转就能过了。再补充一个容易加分的点给代码里加上“错误恢复机制”。比如工具调用失败时返回一段包含 error 信息的消息给模型并提示模型“可以选择重试或告知用户失败原因”。大多数面试者不会想到这一点你能提出来就已经超出平均水平了。8. 实操中的经验与踩坑记录8.1 上下文窗口不是越大越好刚接触 Agent 时很容易有一个错觉既然模型支持 128K 上下文那把所有的历史、资料、新闻全部塞进去模型一定能给出好答案。实测下来并不是这样上下文越长模型对关键信息的注意力就越分散输出质量反而下降。我现在习惯了“少即是多”的上下文策略必要信息能压缩就压缩历史消息能摘要就摘要工具返回内容能截断就截断。给模型喂太多杂乱信息等于让一个满脑子杂念的人做精算题越算越糊涂。8.2 工具的幂等性很重要前端里做接口请求会关注幂等性同样的请求重复执行不会产生副作用差异。Agent 的工具调用也应该这样设计同一个工具、同样的参数执行两次的结果应尽量一致。否则模型在重试时可能产生不可预期的副作用比如重复创建工单、重复发送邮件、重复扣费。实现上可以给写操作类工具加幂等键idempotency key用 Redis 对相同 key 去重。排障时也更容易复现问题。8.3 Prompt 写不好别怪 Agent 笨我接触过不少前端转过来的开发者遇到 Agent 输出不满足期望第一反应是换更大的模型或者疯狂调温度参数这些都是治标不治本。真正的问题往往出在 Prompt 没有把“任务边界、输出结构、评判标准”说清楚。拿前端类比如果你给一个组件传的 props 是空对象那组件只能用默认值和空状态渲染出并不好看的页面Agent 的 Prompt 就是它的 props给得越清晰它越靠谱。我常用的 Prompt 结构是角色定义 → 任务目标 → 输入内容 → 工作流程 → 输出格式 → 注意禁区 → 评判标准。写清楚这七部分再跑的 Agent输出质量至少提升一半。9. 最后分享一点我的个人体会我做了很多年前端后来转向 Agent 开发时最痛苦的不是技术而是心态。前端里一切都有反馈代码有没有报错、页面有没有渲染、接口有没有返回都在掌控之中Agent 世界不是这样它的每一次输出都带有一丝“不确定性”同一个问题可能每次答案都不太一样。这种不确定性起初让我非常焦虑总想着“能不能让它完全可控”。后来做了一些项目才慢慢想明白Agent 的不可控性和“用户输入不可控”一样是产品的常态我们要做的不是消灭不确定性而是通过架构让不确定性只在可控范围内发生。工具层要确定、状态层要确定、流程层要确定只有决策层是模型发挥的空间。前端转 Agent 开发这条路和当初从切图仔转框架工程师很像初期觉得什么都是新的后期发现核心能力其实是通用的——抽象、拆解、分层、容错、优化。希望这一节能让你少走一些弯路把前端的底子真正变成 Agent 开发里的铠甲。
