Agent 开发不是模型训练一个前端的入门认知开个头。最近常有前端同事问我现在大家都在搞Agent我要不要转算法是不是得先去学PyTorch、微调模型每次听到这种问题我都挺着急的。这其实是两个赛道的事。Agent开发的核心是用代码去编排大模型的能力边界你不需要动模型本身它本质上是一个工程问题。而前端工程师恰好是最熟悉跟黑盒系统打交道的一批人就像你之前处理各种浏览器兼容性问题一样。这篇文章只讲一件事把一个Agent从零写出来的完整过程到底涉及哪些环节每个环节对应的是工程能力还是训练能力以及如果你和我一样是前端背景有哪些思路可以直接复用。我见过不少强行去补模型训练知识然后越补越拧巴的人最后写出来的Agent反而不如一个专注工程实现的人做得利索。所以我希望这篇内容能帮你先立住一个框架你不必成为训练专家照样可以做Agent。而且做Agent的过程中你原有的TypeScript功底、接口设计直觉、对异常的敏感度、对用户交互的理解全都能派上大用场。这些能力远比会跑几行训练代码值钱。1. 先说清楚Agent开发到底在做什么1.1 一个被广泛误解的命题很多人一听到Agent就联想到GPT、Claude这种模型本体下意识觉得做Agent 改模型 必须要训练。这个推导错得离谱。打个比方你现在要用浏览器解析HTML你不需要去修改WebKit内核的渲染源码。V8引擎怎么处理垃圾回收跟你部署一个前端页面几乎没有关系。你做的是在这个引擎之上构建应用。同样Agent开发是在大模型之上构建应用核心工作是让一个模型以什么顺序、调用哪些工具、以什么格式输出结果。严格来说Agent开发主要处理三件事任务编排、上下文提示词组装、外部工具接入。这三件事没有一件需要动模型权重。你是要设计一套流程让模型这个大脑能借助你写的触手工具在真实世界里完成任务。举个我一直用的例子你让一个Agent去查天气然后写进日历。整个过程是调用一个包含查天气这个工具描述的提示词让模型输出它想调用哪个工具以及参数你的代码解析模型的输出真正去调用天气API拿到数据后再拼成新的提示词发给模型模型根据数据生成日历邀请内容你的代码再调用日历API把内容写进去。每一步的智能都来自模型自带的世界知识但真正让这个流程跑通、并且能在异常情况下不中断的是全靠你写的代码。1.2 前端做Agent的优势所在说实话前端在Agent开发里有一个不少后端和后端工程师都没有的优势你同时管着界面层和交互层。Agent产品最终面对用户用户看到的对话界面、任务可视化、工具调用过程的直观反馈都跟App开发密不可分。一个纯后端开发可能把Agent写得很好但用户感知不到而前端能把Agent的能力包装成可感知的体验这就是价值。还有一点Agent开发强调较短的迭代周期需要频繁调整提示词和工具流程这跟前端调试UI、适配不同场景的节奏很像。那种改一点就能立刻看到效果的反馈感前端特别熟。别的岗位可能需要适应这种高频修改模式前端几乎是无缝切换。1.3 什么样的工作内容才需要模型训练为了彻底解除焦虑我把需要训练的场景也列一下方便你对号入座需要模型学习一种全新的、没有公开知识的专业知识需要模型输出风格发生根本性改变比如从通用助手变成某位专家口吻并且通用提示词无法做到需要模型对特定任务如特定领域抽取、特定格式遵循的准确率达到远超基准的程度。以上三类才是你该碰训练的时候。而在Agent开发里99%的真实需求其实是通过提示词工程、检索增强生成、工具调用可以解决的。你完全不需要走到训练那一步。每次有人跟我说效果不好是不是得微调我第一反应都是——提示词里有没有把边界条件写清楚工具返回的错误信息有没有给模型看重试机制做了没有这几个问题排查完大多数效果问题当场就解决了。2. 从工程架构角度拆解一个Agent的最小内核2.1 Agent的三大核心模块把一个Agent放大了看它不比一个复杂的中后台页面更神秘。你应该关心的模块就三个。第一编排核心。这是一个状态机负责决定当前该做什么。它接收来自用户的消息决定是直接回复还是调用工具。如果用代码来理解它就是一系列条件判断与循环的组合。模型不像传统程序那样能精确执行流程所以编排核心要把模型的选择变成程序的路径。第二上下文管理。大模型的输入长度有限你不能把所有历史记录一股脑喂给它。你需要做选择——哪段历史重要、哪段可以压缩、哪些外部注入的数据放哪个位置。这个模块会直接影响Agent的记忆能力和回答质量。第三工具注册与调用。这是Agent跟真实世界交互的窗口。每一个工具都需要有清晰的名称、描述、参数定义模型通过大篇幅揣摩工具的用途然后输出一个结构化的调用请求。你的代码负责真正执行这个调用。这三个模块各司其职打包到一起就是一个非常典型的Agent内核。接下来我用一种伪代码的方式描述这个内核对一次请求的处理流程你感受一下type AgentState { messages: ChatMessage[]; currentToolCalls: ToolCall[]; }; async function runAgent(userInput: string, state: AgentState) { // 1. 将用户输入和工具描述合并成上下文 const modelMessages buildContext(state.messages, toolSchemas); modelMessages.push({ role: user, content: userInput }); // 2. 调用模型允许它输出工具调用请求 const response await chatCompletion(modelMessages, { tools: toolSchemas }); // 3. 如果模型想调动工具 if (response.toolCalls) { for (const call of response.toolCalls) { const result await executeTool(call.name, call.arguments); state.messages.push({ role: tool, content: result, }); } // 4. 把工具结果带回给模型让它生成最终回答 return runAgent(请根据工具结果回答, state); } // 4. 如果没有工具调用直接返回文本 return response.content; }这个循环就是Agent最基本的运行内核。框架之间不管叫什么名字像LangChain里的AgentExecutor、OpenAI的Function Calling循环底子全是这个结构。看明白这个伪代码你对Agent内部发生的事情就已经有了直观认识。2.2 认识提示词工程的位置别说提示词不是技术活。在Agent开发里提示词是你跟模型沟通的合同它定义了模型的行为边界。很多前端同事习惯把重点放在API对接上忽略了提示词的质量结果就是Agent输出各种创造性的意外。一个合格的Agent系统提示词通常包含角色定位、任务规则、调用工具的偏好、输出格式严格规定、兜底话术。关键在于确定性三个字。模型擅长模糊但Agent需要稳定。你要用尽量精确的措辞减少自由度把允许的选项列清楚把不应该做的事写明确。举一个反面例子的对比。一个糟糕的提示词可能是你是一个生活秘书可以查询天气也可以设置提醒请帮助用户。这句提示词对模型约束很弱既没说明工具何时可用也没规定输出格式更没告诉它不能做什么。效果就是同一问题问两次能得出两种天津大麻花式回答。同样的场景一个更偏工程的写法是你是用户的生活秘书。当用户询问天气时你必须使用get_weather工具禁止自行编造天气数据。当用户要求设置提醒时你必须使用create_reminder工具。工具调用完成后用友好简洁的口吻告知用户结果。如果你不确定如何回答请主动说明并询问用户补充信息。知道规则和能执行规则是两回事。提示词写得越靠近指令集模型的行为就更容易被预测你的状态机逻辑也会更好写。这个认知请务必建立起来。2.3 前端思维如何映射到Agent开发这个部分我特别想说透。前端思维不是一个行业黑话它有很多具体能力在Agent开发里直接有用。对接口的敏感度。你在前端经常要处理后端给你的各种字段类型、枚举值、错误码。这种接外部系统的经验放到Agent里完全一样——每个工具就是一个小型API你需要设计参数结构、定义返回格式、处理失败场景。你之前处理过多少格式混乱的接口现在就有多大的优势去规范工具协议。异步和流式体验。前端处理过流式的数据渲染知道流式输出时哪些节点可以展示哪些不能。到了Agent里你同理需要给用户展示正在调用工具工具执行完成这类事件。这种体验设计多数前端天生有感觉却往往被忽视掉。防御式编程。写前端的人都知道不能信接口返回要做空值判断、兜底渲染。到了Agent里这个原则被进一步强化你不能信模型的输出。它说调用工具就调用了吗参数合法吗工具返回了异常数据怎么办这些判断逻辑就是Agent的健壮性来源恰好是前端日常工作的主要组成部分。3. 从零搭建一个带工具调用的Agent实操篇3.1 环境准备与技术选型这里我不会推荐必须用某个框架但会给你一个稳妥的选型组合尤其是在家自学的阶段。架构选型讲究做减法你还不到需要编排多个Agent协同工作的复杂度时就老老实实做一个单Agent加工具调用。模型侧直接选择一个支持工具调用的Chat模型即可。架构侧直接用官方提供的API配合TypeScript不要一开始就上重型框架。我见过有人第一周就把LangChain的各种类都搬进来结果光理解概念就花了两周反而阻碍了对Agent底层逻辑的把握。目录结构给你参考agent-tutorial/ ├── src/ │ ├── agent/ │ │ ├── runAgent.ts # 核心编排逻辑 │ │ ├── types.ts # 消息、工具类型定义 │ │ └── prompts.ts # 系统提示词 │ ├── tools/ │ │ ├── index.ts # 工具注册表 │ │ ├── weather.ts # 天气工具 │ │ └── notify.ts # 通知工具 │ ├── context/ │ │ └── history.ts # 对话记忆管理 │ └── index.ts # 入口 └── package.json这个结构看起来就跟一个前端的service分层差不多非常熟悉。实际上它确实不需要跟训练有任何关系你甚至不需要装Python环境不用开Notebook。3.2 一个让人摸得着的Demo日程安排Agent为了说明整个流程我设计一个最简单的Agent来演示。它能做两件事查询天气、在日历中创建日程。用户在对话里说明天下午三点帮我安排一个户外会议顺便看看天气Agent需要自己决定先查天气再建日程并且在天气不好的情况下向用户确认是否改期。这个场景刚好覆盖了Agent的核心行为多步推理、工具选择、跨工具数据依赖、以及根据外部信息改变计划的能力。麻雀虽小五脏俱全。先定义工具类型。为了能进API的tools参数里每一个工具都要有严格的Schema描述// tools/weather.ts export const weatherSchema { name: get_weather, description: 查询指定城市、指定日期的天气情况返回温度和降水概率, parameters: { type: object, properties: { city: { type: string, description: 城市名称如 上海 }, date: { type: string, description: 日期格式为YYYY-MM-DD } }, required: [city, date] } }; // tools/notify.ts export const notifySchema { name: create_calendar_event, description: 在日历中创建一个日程事件, parameters: { type: object, properties: { title: { type: string }, startTime: { type: string, description: 开始时间ISO字符串 }, endTime: { type: string }, location: { type: string } }, required: [title, startTime, endTime] } };注意看description的写法。很多新手在写工具描述时特别不认真简简单单一句查询天气结果就是模型不知道怎么填参数。工具描述是给模型看的说明书你需要说清楚参数边界、格式、和什么变量相关。模型越理解工具你的代码逻辑越不会跑偏。3.3 一次性把工具注册与调用链路打通接下来是工具注册表。不要写一堆if-else去判断哪个工具应该给模型而是维护一个注册表让代码自己判断// tools/index.ts import { weatherSchema } from ./weather; import { notifySchema } from ./notify; const toolRegistry [ { schema: weatherSchema, execute: async (args) { const { city, date } args; // 这里替换为真实天气API return JSON.stringify({ city, date, temperature: 24, rainProbability: 20, status: 晴 }); } }, { schema: notifySchema, execute: async (args) { const { title, startTime, endTime, location } args; // 这里替换为真实日历API return JSON.stringify({ eventId: evt_123, created: true, title }); } } ]; export function getToolSchemas() { return toolRegistry.map(item item.schema); } export async function executeToolByName(name: string, args: any) { const tool toolRegistry.find(t t.schema.name name); if (!tool) { throw new Error(Tool not found: ${name}); } // 统一处理执行结果和错误 try { const result await tool.execute(args); return result; } catch (error) { return JSON.stringify({ error: 工具执行失败, detail: (error as Error).message }); } }这段代码的重要性在于它把工具的定义和执行分离并且统一了错误返回格式。这样模型收到工具结果时无论成功还是失败都有结构化的信息可判断而不是收到一堆无法理解的抛出异常。3.4 核心编排循环的完整实现有了工具表核心编排的就来了。这是一个比之前写伪代码完整得多的实现关键多了一个最大轮次限制防止Agent陷入无限循环。另外还需要把tool调用记录写进对话历史里让模型能看到上下文的全貌。// agent/runAgent.ts import { getToolSchemas, executeToolByName } from ../tools; type ChatMessage { role: system | user | assistant | tool; content: string; toolCalls?: any[]; }; const MAX_ITERATIONS 5; export async function runAgent(userInput: string, systemPrompt: string) { const messages: ChatMessage[] [ { role: system, content: systemPrompt }, { role: user, content: userInput } ]; for (let i 0; i MAX_ITERATIONS; i) { const response await callModel({ messages, tools: getToolSchemas() }); // 如果模型决定调用工具 if (response.toolCalls response.toolCalls.length 0) { // 记录模型发起工具调用的消息 messages.push({ role: assistant, content: response.content, toolCalls: response.toolCalls }); // 依次执行每个工具调用 for (const toolCall of response.toolCalls) { const result await executeToolByName(toolCall.name, toolCall.arguments); messages.push({ role: tool, content: result, toolCallId: toolCall.id }); } } else { // 模型不再调用工具输出最终答案 return response.content; } } return 达到最大迭代次数无法完成任务请尝试拆分成更简单的步骤; }这里你可以看出Agent的循环跟一个前端轮询请求的循环很像都是发请求—检查条件—决定继续还是终止。跑完这个循环一个Agent的最小可用版本就诞生了。3.5 系统提示词的具体设计工具定义了循环写好了剩下的关键就是系统提示词。我直接贴一个我调了几轮之后比较稳定的版本export const SYSTEM_PROMPT 你是日程助手你能查询天气并创建日程事件。 你需要遵守以下规则 1. 当用户提到查询天气时必须使用get_weather工具禁止直接编造天气数据。 2. 当用户要求安排日程时先判断是否需要天气信息若需要则先查询天气再创建日程。 3. 如果查询到的天气显示降水概率大于等于50%创建日程前必须向用户确认是否更换时间或地点不可直接创建。 4. 日程时间格式需要转化为ISO字符串。 5. 所有回复使用中文简洁自然。 6. 当用户的问题超出你的能力范围直接说明你无法处理。 执行逻辑 - 先调用需要查询的天气信息再依据结果判断下一步确保每一步都有工具调用结果支撑。 ;写提示词的过程本身就是程序思维的体现。第3条就是一道条件判断工具的返回数据如果触发50%这个阈值Agent的行为路径就要改变。这完全是编程逻辑只是用自然语言表达而已。你可以自由扩展规则提示词越接近业务逻辑Agent的输出就越像一个由你完全掌控的程序。3.6 上下文管理简单可用的历史记录与裁剪策略在多轮对话里你不能每次都把所有历史消息一股脑送进模型。我这边的简单策略是保留系统提示词、最近的6条对话之外把更早的消息压缩成历史摘要后再放进去。这个方法在日活大之后会遇到瓶颈但入门阶段够用且容易理解。来一段可落地的代码// context/history.ts export function buildMessages( systemPrompt: string, history: ChatMessage[], maxHistory 6 ): ChatMessage[] { const recent history.slice(-maxHistory); const earlier history.slice(0, history.length - maxHistory); let combinedContext ; if (earlier.length 0) { // 将较早的对话摘要成一段简化做法 combinedContext earlier .map(m ${m.role}: ${m.content}) .join(\n); } return [ { role: system, content: systemPrompt }, ...(combinedContext ? [{ role: system as const, content: 以下是对话摘要供你理解更早的谈话\n combinedContext }] : []), // 历史消息里跳过已经作为摘要注入的部分 ...recent ]; }上下文裁剪是Agent开发里非常核心的话题策略有很多比如基于token数的裁剪、基于重要性的筛选。但在入门阶段你记住近详远略这个原则就够了。你不需要做完美的记忆只需要做不丢失关键信息的记忆。3.7 测试一下这个Agent假设用户输入帮我看一下后天杭州的天气如果适合出行的话创建一个下午三点的公园散步日程。第一次调用。模型收到系统提示词、用户输入、工具列表。它会理解后天杭州→get_weather工具然后输出一个工具调用请求参数大概是{city: 杭州, date: 后天对应的实际日期}。你的Node服务解析这个请求把它替换成真实的日期去调用天气API。返回的数据假如是降水概率30%、温度26度、晴天。第二次循环。模型看到工具返回的数据后判断适合出行成立于是调用create_calendar_event工具生成一个下午三点的日程。第二次工具调用请求送达日历服务的API返回创建成功。第三次循环。模型这时判断任务已经完成直接输出后天杭州天气晴气温26度适合出行。我已为你创建了明天下午三点的公园散步日程届时请留意天气变化。整个流程结束。如果天气API返回的降水概率到了70%第二次循环里模型不会创建日程而是直接反问后天杭州降水概率70%不太适合户外散步。是否要改期或者换个室内的活动看你写的规则生效了。4. 前端同学上手Agent最容易踩的坑4.1 把工具函数写得模糊模型看不懂我想先举一个很多前端都会犯的问题。你写工具时的命名和描述不够详细函数名是聚合缩写描述也是笼统的两个字。模型确实能理解但检索和调用的准确率会下降很多。举个例子// 反例描述不清晰 export const getData { name: get_info, description: 获取信息, parameters: { type: object, properties: { id: { type: string } } } }; // 正例描述精准 export const getData { name: get_order_status, description: 根据订单ID查询物流状态返回最新物流轨迹时间线, parameters: { type: object, properties: { orderId: { type: string, description: 订单号形如DD20240101 } }, required: [orderId] } };工具描述是整个Agent开发里影响力极大的隐藏杠杆成本低但收益大。调整一次描述往往比你去调好几轮提示词效果更明显。4.2 只把成功结果塞给模型隐藏错误信息新手最容易犯的错误是工具执行失败时直接往上下文里丢一行查询失败不给模型任何可判断的细节。模型没什么可依据只能瞎编或者反复重试。正确做法是把错误结果也包装成结构化的tool消息明确告诉模型发生了什么错、为什么、还有什么能做的。模型知道了前因后果才能决定下一步。我个人的小技巧是任何工具函数内部都套一层try-catch返回对象包含一个固定的error字段然后再序列化成字符串传给模型。这样模型就能从上下文里读到判断所需的信息。4.3 死循环风险与溢出风险如果Agent的编排循环写得不好或者工具调用失败后仍然允许模型继续调用同一工具Agent会陷入调用—失败—再调用的死循环。这里机制必须有最大迭代次数限制并且当同一工具连续失败两次时应该把该工具标记为不可用。上下文溢出则是另一个坑。多轮对话后历史记录越来越长直接带着所有历史调用模型费用和延迟都会爆炸而且模型还容易被无关信息干扰。前端对接过大数据量的经验这里也能复用上用分页思想处理对话历史滑窗之外的记忆就压缩成摘要。4.4 再训练一下就能解决的思维误区这个坑不是技术坑是认知坑。如果你做了Agent之后发现效果不好第一反应千万不要是要不要微调模型。先把这几个问题查一遍工具描述清不清楚系统提示词有没有说清边界工具返回的信息结构模型能不能看懂上下文里有没有给足依据如果以上都到位效果还是不满意再考虑是不是选型问题当前模型能力不够换一个更聪明的模型往往比训练解决得快。真到特定领域知识缺失需要微调的时候你会明确知道——不是想试试看而是必须做。5. 前端技能迁移图谱盘点你已经具备的Agent开发能力5.1 从状态管理到编排核心前端管理复杂状态时用Redux、Zustand核心是状态机的思维。Agent的编排核心本质上也是一个状态机。用户在哪个节点当前该调哪个工具工具结果如何影响状态切换所以一个直接用Zustand写过复杂UI状态的人上手Agent编排的时候基本不用补脑。任务编排需要的就是这种把Flow画清楚、把状态归类的能力。5.2 从接口联调到工具接入前端的日常是跟后端定义的接口契约打交道工具接入的本质也是契约式对接。因为你需要精确描述工具的输入输出。前端对着后端给出的文档调试接口和你在Agent里根据工具Schema调试调用链路节奏完全一致。对边界条件的敏感度、对文档规范的重视这两个能力前端都很充沛直接用。5.3 从渲染管线到上下文组装前端把数据经过计算输出成视图类似一个上下文组装过程。Agent里你需要把系统提示词、历史摘要、工具结果、用户输入拼装成一个模型可理解的上下文。你熟悉的数据经过一系列transform函数变成UI的流水线用来理解Context组装非常自然。5.4 从报错体验到Agent的确定性工程前端做错误边界、做骨架屏、处理请求失败状态本质上是提升系统的确定性体验。Agent产品比传统前端更需要对不可控输出做兜底模型会说不确定的话、会给出错误工具参数、会产生幻觉。你之前做前端时对不可控状态的处理习惯到了Agent开发里正好能帮助你提前兜底、验收边界。这个迁移很值钱。6. 从零到一的最简实践路径6.1 第一个阶段手写最小Agent循环不要一开始就上LangChain或者其它高层次框架先按这篇文章的示例手写一个最小循环。花一两个小时把下面的DEMO顺序跑出来即可定义两个假的工具函数写一个简单的提示词用一个支持工具调用的模型API把外部循环打通。这一步修炼的目标是手感不是完成度。你会亲手体验到模型输出一个工具调用请求是什么样的解析过程在哪一环做完这些你才会有根。6.2 第二个阶段加入设计约束和错误处理第二个阶段就是盘外功夫了。给工具执行加上错误返回结构对最大轮次做出限制对同一工具连续失败做熔断梳理一次完整的用户任务流程中可能出现的意外分支。这个阶段你会损失一部分模型的神奇感换来的是工程的可控感。这正好是前端在做的事优雅降级而不是追求一切完美。6.3 第三个阶段包装成可交互的应用到这个阶段你是前端就能施展全部优势了。做一个对话界面把工具调用的过程可视化。前端在这一步的体验设计直接决定Agent产品的可用性上限。你可以把工具调用的中间结果显示成卡片把最终回答和历史记录区分开在工具执行期间给出加载态。大家都在用一个模型谁的流程更优雅就更像标准产品。第三个阶段做完你已经拥有一个完整Agent应用的编码能力。如果这时候再去了解LangChain这类框架你会突然发现原来很多高层次抽象你都已经手写过一遍底层实现看框架的视角会比直接往上灌的同学清晰很多。7. 关于热词式思维聊聊Agent开发的大水漫灌与沉淀我观察到一个现象每过一个阶段市场上就会冒出一批新的技术热词然后有一大部分人开始焦虑自己是不是落后了。三年前是低代码两年前是AIGC今年是Agent。如果你把每个热词都当成需要重新学一遍的新学科你会一直疲于奔命。持续做Agent开发的人会沉淀出一些不太显眼但特别关键的隐性知识工具的封装粒度多大合适描述写成什么程度既能引导模型又不过分限制发挥多工具协作时冲突怎么规避模型给出意外输出时哪些可以直接忽略哪些必须兜底处理这些经验无法速成只能通过手写项目一点点积累。而保留这些经验最好的方式不是写一堆天花乱坠的教程而是沉淀成代码。把工具做成可复用的注册表把提示词做成可维护的配置文件把编排做成可测试的函数模块。前端在这方面的优势又是非常明显的——我们有做组件化、模块化、设计系统的经验可以天然把Agent能力也整理成一套组件库来运营。我现在的工作流里大量工具和提示词是逐渐从早期项目里迁移复用的。真正有效率的开发不是每次遇到问题就重新琢磨而是积累自己的Agent组件库。以后新来的同学拿着这套既有的提示词规范和工具封装可以很快落地新业务。这套东西的价值不亚于一个前端团队积累的UI设计系统值得你花心思维护。产出一个Agent的过程就像做一套带复杂交互逻辑的页面先写静态结构然后接数据最后调交互体验。你把模型的不可控封装在可控的工程结构里一段一段地管住它而不是一跃跳到训练领域去把自己变成炼丹师。你可以记住两个不等式Agent开发不等于模型训练前端工程能力也不等于模型微调能力。但是反过来前端工程能力是Agent开发里最稀缺也最核心的加速器之一。把招子放对位置你的前路很快就会被自己点亮。