☰
前端开发者如何用TypeScript从零搭建Agent系统
2026/10/16 18:19:41 网站建设 项目流程

1. 一个前端为什么会跑去搞 Agent

先说结论:Agent 开发跟模型训练,基本上是两码事。我做了六年前端,主栈是 React 加 TypeScript,日常跟组件、状态管理、构建工具打交道。去年开始接触 Agent 相关的东西,最开始我也以为要懂什么反向传播、损失函数、梯度下降,结果真正上手之后发现,前端做 Agent 开发,核心能力反而是你本来就有的那些——状态管理、异步编排、接口设计、错误处理。

这个认知转变挺关键的。市面上很多讲 Agent 的内容,一上来就是 Transformer 架构、注意力机制、微调策略,搞得前端同学觉得门槛很高。但实际上,Agent 开发更像是你在做一个复杂的业务流程编排系统,只不过其中某些节点由大语言模型来执行。你不需要训练模型,你需要的是理解模型的输入输出特性,然后设计一套可靠的调度逻辑。

这篇文章适合谁看?如果你是前端开发者,对 Agent 开发感兴趣但不知道从哪里切入,或者你已经尝试过调用一些大模型接口但不知道怎么把它组织成一个完整的系统,那这篇内容应该能帮你理清思路。我会从认知层面到实操层面,把前端做 Agent 开发需要知道的东西讲清楚。

2. 核心认知:Agent 开发和模型训练的本质区别

2.1 模型训练到底在做什么

模型训练的本质是调整神经网络的参数。你有一堆数据,通过前向传播算出预测结果,再通过反向传播计算梯度,然后用优化器更新参数。这个过程需要大量算力、大量数据、大量时间。训练完之后你得到一个模型文件,这个文件里存的就是那些调整好的参数。

对于前端来说,这件事的参与度极低。你不太可能去写训练脚本,也不太可能去调学习率、批次大小这些超参数。这些工作是算法工程师或者机器学习工程师在做。前端跟模型训练的交叉点,最多是在模型部署之后,你通过 API 去调用它。

2.2 Agent 开发到底在做什么

Agent 开发的核心是编排。你有一个大语言模型作为推理引擎,然后你给它配上工具、记忆、规划能力,让它能自主完成复杂任务。举个具体的例子:用户说“帮我查一下这周的项目进度,然后生成一份周报”,一个 Agent 系统需要做这些事情——理解用户意图、调用项目管理工具获取数据、调用日历工具确认时间范围、把数据整理成周报格式、输出结果。这里面每一步都可能涉及大模型的调用,但大模型本身不存储项目数据,它只是在你设计的流程中做决策和生成。

所以 Agent 开发的工作量分布大概是这样的:百分之七十在流程设计和状态管理,百分之二十在提示词工程和工具定义,百分之十在模型调用和结果解析。你会发现,前两项恰好是前端最擅长的。

2.3 为什么前端做 Agent 有天然优势

前端开发者每天都在处理的事情:用户输入、异步请求、状态流转、错误边界、加载状态、条件渲染。这些东西映射到 Agent 开发里,几乎一一对应。用户输入对应 Agent 的输入解析,异步请求对应工具调用,状态流转对应 Agent 的执行循环,错误边界对应异常处理,加载状态对应 Agent 执行中的反馈,条件渲染对应不同工具结果的差异化展示。

我举个例子你就明白了。前端做表单验证的时候,你会写一堆规则:必填、格式校验、联动校验。Agent 开发里也有类似的东西:工具调用的参数校验、调用结果的格式校验、多轮对话中的上下文校验。思路是一样的,只是校验的对象从用户输入变成了模型输出。

还有一个很实际的优势:前端对 API 的设计和使用非常敏感。Agent 系统里,工具本质上就是一个个 API,你需要定义清晰的输入输出格式,处理超时和重试,管理调用频率。这些对于写过前端接口层的人来说,都是轻车熟路。

3. 前端做 Agent 开发需要补齐的知识短板

3.1 大模型的基本工作原理(够用就行)

你不需要懂反向传播,但你需要理解几件事。第一,大模型是无状态的。每次调用你都要把完整的上下文传进去,它不会记住上一次说了什么。第二,大模型的输出是概率性的。同样的输入,可能得到不同的输出。第三,大模型有上下文窗口限制。你不能无限地往里面塞内容,超出窗口的部分会被截断或者报错。第四,大模型会产生幻觉。它会编造看起来合理但实际上错误的信息。

这四点直接决定了你的 Agent 系统怎么设计。无状态意味着你需要自己管理对话历史;概率性意味着你需要做输出校验和重试;上下文窗口限制意味着你需要做信息压缩和摘要;幻觉意味着你需要引入外部验证机制。

3.2 提示词工程的核心要点

提示词工程不是玄学,它是有方法论的。最核心的原则是:清晰、具体、有结构。你不要跟模型说“帮我写个东西”,你要说“帮我写一份周报,包含本周完成事项、下周计划、风险点三个部分,每部分不超过三条,用 Markdown 格式输出”。

另一个关键点是 Few-shot 示例。你给模型两三个输入输出的例子,它的表现会好很多。这在 Agent 开发里特别重要,因为你需要模型按照特定格式输出,以便你的代码能解析。比如你需要模型输出 JSON,那你就得在提示词里明确说明 JSON 的字段名和类型,最好再给一个示例。

还有一个容易被忽略的点:系统提示词和用户提示词要分开。系统提示词定义 Agent 的角色、能力边界、输出规范,用户提示词是具体的任务描述。这两者混在一起会让模型困惑。

3.3 工具调用的实现机制

工具调用是 Agent 的核心能力。实现方式通常是这样的:你在调用大模型的时候,把可用的工具列表传进去,每个工具包含名称、描述、参数 schema。模型在推理过程中,如果判断需要调用某个工具,它会返回一个工具调用的请求,包含工具名和参数。你的代码接收到这个请求后,实际执行工具,然后把结果再传回给模型,模型继续推理。

前端在这里需要做的是:定义工具的描述和参数 schema,实现工具的实际执行逻辑,处理工具调用的结果和异常。工具的描述要尽可能清晰,因为模型是根据描述来判断什么时候调用哪个工具的。参数 schema 要严格,这样模型生成的参数才能被你的代码正确解析。

4. 从零搭建一个最小可用 Agent 的实操过程

4.1 环境准备和技术选型

我用的技术栈是 Node.js 加 TypeScript,因为前端本来就熟悉这套。大模型接口我用的是通用的 HTTP 调用方式,没有依赖特定的 SDK,这样更灵活。状态管理我用的是简单的内存对象,因为最小可用版本不需要持久化。

你需要准备的东西:一个可以调用大模型 API 的密钥,一个 Node.js 环境,基本的 TypeScript 配置。如果你用的是 Next.js 之类的框架,可以直接在 API Route 里写 Agent 逻辑。

注意:API 密钥不要硬编码在代码里,用环境变量管理。这是基本的安全习惯,跟做前端项目时不要把密钥写在前端代码里是一个道理。

4.2 定义 Agent 的核心数据结构

我先定义几个核心类型。消息类型包含角色和内容,角色有 system、user、assistant、tool 四种。工具类型包含名称、描述、参数 schema、执行函数。Agent 配置包含系统提示词、可用工具列表、最大执行轮数。

interface Message { role: 'system' | 'user' | 'assistant' | 'tool'; content: string; toolCallId?: string; } interface Tool { name: string; description: string; parameters: Record<string, { type: string; description: string; required: boolean }>; execute: (args: Record<string, unknown>) => Promise<string>; } interface AgentConfig { systemPrompt: string; tools: Tool[]; maxRounds: number; }

这些类型定义看起来简单,但它们决定了整个系统的结构。消息列表是 Agent 的状态,工具列表是 Agent 的能力,最大轮数是安全边界。

4.3 实现 Agent 的执行循环

Agent 的执行循环是整个系统的核心。逻辑是这样的:把系统提示词和用户消息组成消息列表,调用大模型。如果模型返回的是普通文本,直接输出。如果模型返回的是工具调用请求,执行工具,把结果加入消息列表,再次调用模型。重复这个过程,直到模型返回普通文本或者达到最大轮数。

async function runAgent(config: AgentConfig, userInput: string): Promise<string> { const messages: Message[] = [ { role: 'system', content: config.systemPrompt }, { role: 'user', content: userInput }, ]; for (let round = 0; round < config.maxRounds; round++) { const response = await callLLM(messages, config.tools); if (response.type === 'text') { return response.content; } if (response.type === 'tool_call') { const tool = config.tools.find(t => t.name === response.toolName); if (!tool) { messages.push({ role: 'tool', content: '工具不存在', toolCallId: response.toolCallId }); continue; } try { const result = await tool.execute(response.args); messages.push({ role: 'tool', content: result, toolCallId: response.toolCallId }); } catch (error) { messages.push({ role: 'tool', content: `工具执行失败: ${error.message}`, toolCallId: response.toolCallId }); } } } return '达到最大执行轮数,任务未完成'; }

这个循环看起来简单,但里面有几个关键决策。第一,最大轮数必须设置,否则模型可能陷入死循环。第二,工具执行失败时不要把异常抛出去,而是把错误信息作为工具结果传回给模型,让模型决定下一步怎么做。第三,每一轮都要把完整的消息列表传进去,因为模型是无状态的。

4.4 定义第一个工具:获取当前时间

工具的定义要尽可能简单明确。我拿获取当前时间举例,因为这个工具没有外部依赖,适合演示。

const getCurrentTimeTool: Tool = { name: 'get_current_time', description: '获取当前的日期和时间,当用户询问时间相关问题时使用', parameters: { timezone: { type: 'string', description: '时区,例如 Asia/Shanghai,默认为本地时区', required: false, }, }, execute: async (args) => { const timezone = (args.timezone as string) || Intl.DateTimeFormat().resolvedOptions().timeZone; const now = new Date(); return now.toLocaleString('zh-CN', { timeZone: timezone }); }, };

工具描述里我特意写了“当用户询问时间相关问题时使用”,这是给模型看的,帮助它判断什么时候调用这个工具。参数描述里说明了默认值,这样模型不传这个参数也能正常工作。

4.5 组装并运行

把系统提示词、工具、用户输入组装起来,调用 runAgent 函数。

const config: AgentConfig = { systemPrompt: '你是一个助手,可以调用工具来帮助用户解决问题。回答要简洁准确。', tools: [getCurrentTimeTool], maxRounds: 5, }; const result = await runAgent(config, '现在几点了?'); console.log(result);

运行结果大概是:模型判断需要调用 get_current_time 工具,工具返回当前时间,模型把时间整理成自然语言输出。整个过程你不需要训练任何模型,只需要定义工具和编排流程。

5. 实操中踩过的坑和排查技巧

5.1 模型不调用工具怎么办

这是最常见的问题。模型有时候会直接用自己的知识回答,而不是调用你定义的工具。原因通常是工具描述不够清晰,或者系统提示词没有强调要使用工具。

我的解决办法是在系统提示词里明确写:“当需要实时信息或外部数据时,必须调用相应的工具,不要依赖你自己的知识。”另外,工具描述要具体,不要写“获取信息”这种模糊的描述,要写“获取指定城市的实时天气信息,包括温度和湿度”。

还有一个技巧:在 Few-shot 示例里展示工具调用的过程。你可以在系统提示词里加一个例子,展示用户提问、模型调用工具、工具返回结果、模型整理输出的完整流程。这样模型更容易学会什么时候该调用工具。

5.2 工具参数解析失败怎么办

模型生成的参数有时候不符合你的 schema。比如你要求参数是数字,模型给了一个字符串。或者你要求参数是枚举值,模型给了一个不在枚举范围内的值。

处理方式分两层。第一层是在工具执行函数里做参数校验和转换,尽量宽容地处理。第二层是在提示词里把参数格式说清楚,最好给示例。如果模型经常生成错误的参数,那说明你的参数描述不够明确,需要优化。

我一般会在工具执行函数里加一个参数预处理步骤,把字符串数字转成数字,把布尔值字符串转成布尔值,把不在枚举范围内的值映射到默认值。这样即使模型生成的参数有小问题,工具也能正常执行。

5.3 上下文太长导致性能下降怎么办

Agent 执行多轮之后,消息列表会变得很长,导致每次调用大模型的成本增加,响应变慢,甚至超出上下文窗口。

我的做法是定期做上下文压缩。具体来说,当消息数量超过一定阈值时,把早期的工具调用结果替换成摘要。比如工具返回了一大段 JSON 数据,我把它压缩成“工具 X 返回了数据,包含字段 A、B、C”。这样既保留了关键信息,又大幅减少了 token 数量。

另一个技巧是只保留最近 N 轮的消息,更早的消息直接丢弃。但这有风险,可能丢失重要上下文。所以我一般用摘要的方式,而不是直接丢弃。

5.4 常见问题速查表

问题现象可能原因排查方向解决方式
模型不调用工具工具描述模糊检查工具描述是否具体优化描述,增加使用场景说明
参数解析失败schema 不明确检查参数类型和描述增加参数示例,做宽容处理
响应越来越慢上下文过长检查消息列表长度做上下文压缩或截断
工具调用死循环缺少终止条件检查最大轮数设置设置合理的最大轮数
输出格式不稳定提示词不够结构化检查输出格式要求明确格式,增加示例
工具执行超时外部依赖不稳定检查工具实现增加超时和重试机制

提示:这张表是我在实际项目中反复遇到的问题总结,建议你在开发 Agent 的时候把这张表放在手边,遇到问题先对照排查。

6. 从最小可用到生产可用的关键升级

6.1 增加记忆能力

最小可用版本的 Agent 是无状态的,每次对话都是全新的开始。生产环境需要记忆能力,包括短期记忆和长期记忆。短期记忆就是当前对话的历史,这个用消息列表就能实现。长期记忆需要持久化存储,可以用数据库或者文件系统。

实现方式是在每次对话结束后,把关键信息提取出来存到数据库。下次对话开始时,根据用户输入检索相关的历史信息,加入到系统提示词里。检索可以用关键词匹配,也可以用向量相似度。前端做这个部分,主要是设计存储结构和检索逻辑,跟做前端状态持久化思路类似。

6.2 增加错误恢复机制

生产环境的 Agent 必须能处理各种异常。工具调用超时、模型返回格式错误、网络中断,这些都需要有对应的恢复策略。

我的做法是给每个工具调用加上超时和重试。超时时间根据工具的实际执行时间设定,重试次数一般设为两到三次。如果重试之后还是失败,就把错误信息传回给模型,让模型决定是换一个工具还是直接告诉用户失败了。

另外,模型返回格式错误也需要处理。比如你期望 JSON 格式,但模型返回了一段自然语言。这时候可以尝试从文本中提取 JSON 部分,如果提取失败,就重新调用模型并强调格式要求。

6.3 增加可观测性

生产环境需要知道 Agent 在做什么、每一步花了多少时间、哪些工具调用失败了。这些信息对于排查问题和优化性能至关重要。

我一般会在 Agent 执行循环里加日志记录,每一步都记录:当前轮数、模型返回类型、工具名称、工具执行时间、工具执行结果摘要。这些日志可以输出到控制台,也可以存到文件或者发送到日志服务。

前端做可观测性有天然优势,因为你知道怎么设计清晰的日志格式,怎么在界面上展示执行过程。我甚至做过一个简单的可视化界面,实时展示 Agent 的执行步骤,对于调试非常有帮助。

6.4 增加并发控制

当多个用户同时使用 Agent 时,需要考虑并发控制。每个用户的对话应该是独立的,不能互相干扰。这要求你在设计数据结构时,把每个对话的状态单独存储,用会话 ID 来区分。

另外,工具调用可能需要限流。比如你调用了一个外部 API,它有调用频率限制。你需要在工具执行层加一个队列或者令牌桶,控制调用频率。这个跟前端做接口请求限流是一个思路。

7. 前端做 Agent 开发的几个实用建议

7.1 从简单场景开始

不要一上来就做一个全能助手。先做一个单一功能的 Agent,比如只负责查询天气,或者只负责翻译。把单一功能做稳定了,再逐步增加工具和能力。这样你能快速看到效果,也能快速定位问题。

我自己的路径是这样的:先做一个只能获取时间的 Agent,跑通整个流程。然后加一个计算器工具,处理数学运算。再加一个搜索工具,处理信息查询。每一步都验证稳定之后再往下走。这个过程大概花了两周时间,但基础打得很扎实。

7.2 重视提示词的版本管理

提示词是 Agent 的核心资产,需要像代码一样管理。我建议把提示词单独放在一个文件里,用版本控制工具管理。每次修改提示词都要记录修改原因和效果对比。

我吃过亏。有一次优化提示词之后,Agent 的表现突然变差了,但我忘了之前用的是什么提示词,花了很长时间才恢复。从那以后,我把所有提示词都放在 Git 里管理,每次修改都有 commit 记录。

7.3 建立测试用例集

Agent 的行为是概率性的,同样的输入可能得到不同的输出。但这不意味着不需要测试。你需要建立一组测试用例,覆盖常见的用户输入和边界情况。每次修改提示词或者工具之后,跑一遍测试用例,看看通过率有没有下降。

测试用例不需要很复杂,就是输入和期望输出的配对。比如输入“现在几点”,期望输出包含时间信息。输入“帮我算一下 23 乘以 47”,期望输出包含 1081。这些用例能帮你快速发现回归问题。

7.4 关注成本控制

大模型调用是有成本的,特别是当你的 Agent 需要多轮调用时。你需要关注每次对话的 token 消耗,优化提示词长度,减少不必要的工具调用。

我的做法是给每次对话设置 token 预算,超出预算就终止。另外,对于简单的任务,可以用更小的模型;对于复杂的任务,才用更大的模型。这个策略能显著降低成本。

7.5 保持学习但不要焦虑

Agent 领域变化很快,新的模型、新的框架、新的方法层出不穷。作为前端,你不需要追每一个新东西。把基础打好——理解模型的无状态性、概率性、上下文限制,掌握工具调用和流程编排,这些是不变的核心。新的东西都是在这些基础之上的优化。

我现在的学习策略是:每个月花几个小时看看有什么新东西,但主要精力还是放在把现有的系统做稳定、做可靠。毕竟,用户要的是能解决问题的 Agent,不是用了最新技术的 Agent。

8. 一个完整的工具调用示例:从定义到执行

8.1 定义一个查询天气的工具

我拿天气查询举例,因为它涉及外部 API 调用、参数校验、结果格式化,比较有代表性。

const getWeatherTool: Tool = { name: 'get_weather', description: '查询指定城市的当前天气,包括温度、湿度和天气状况。当用户询问天气相关问题时使用。', parameters: { city: { type: 'string', description: '城市名称,例如 北京、上海、广州', required: true, }, unit: { type: 'string', description: '温度单位,celsius 或 fahrenheit,默认为 celsius', required: false, }, }, execute: async (args) => { const city = args.city as string; const unit = (args.unit as string) || 'celsius'; if (!city) { throw new Error('城市名称不能为空'); } const response = await fetch( `https://api.example.com/weather?city=${encodeURIComponent(city)}&unit=${unit}` ); if (!response.ok) { throw new Error(`天气服务返回错误: ${response.status}`); } const data = await response.json(); return `${data.city}当前天气:${data.condition},温度 ${data.temperature}°${unit === 'celsius' ? 'C' : 'F'},湿度 ${data.humidity}%`; }, };

这个工具的定义有几个细节值得注意。参数描述里给了示例值,帮助模型理解参数格式。执行函数里做了参数校验,城市名称为空时直接抛错。外部 API 调用检查了响应状态,非成功状态抛出异常。返回结果是格式化好的自然语言,方便模型直接使用。

8.2 在 Agent 中使用这个工具

把天气工具加入到 Agent 配置里,然后测试。

const config: AgentConfig = { systemPrompt: '你是一个天气助手,可以查询各个城市的天气。回答要简洁,直接给出天气信息。', tools: [getWeatherTool, getCurrentTimeTool], maxRounds: 5, }; const result = await runAgent(config, '北京今天天气怎么样?'); console.log(result);

执行过程是这样的:模型接收到用户输入,判断需要调用 get_weather 工具,生成参数{ city: '北京' }。工具执行,调用外部 API,返回格式化结果。模型接收到工具结果,整理成自然语言输出。

8.3 处理工具执行中的异常

工具执行可能失败,比如网络超时、API 限流、返回数据格式错误。这些异常需要被捕获并传回给模型。

try { const result = await tool.execute(response.args); messages.push({ role: 'tool', content: result, toolCallId: response.toolCallId }); } catch (error) { const errorMessage = error instanceof Error ? error.message : '未知错误'; messages.push({ role: 'tool', content: `工具执行失败:${errorMessage}。请尝试其他方式回答用户问题。`, toolCallId: response.toolCallId, }); }

把错误信息传回给模型,而不是直接抛给用户,这样模型有机会尝试其他方案。比如天气查询失败了,模型可以告诉用户“暂时无法获取天气信息,请稍后再试”,而不是让用户看到一个错误堆栈。

9. 关于 Agent 开发的一些个人体会

做了几个 Agent 项目之后,我最大的体会是:Agent 开发的核心难点不在技术,而在设计。你需要设计清晰的工具边界,设计合理的执行流程,设计可靠的错误处理。这些设计能力,前端在日常工作中一直在锻炼。

另一个体会是:不要追求完美。Agent 的行为是概率性的,你不可能让它在所有情况下都表现完美。你需要做的是让它在常见情况下表现良好,在异常情况下优雅降级。这个思路跟做前端容错是一样的——网络请求可能失败,但页面不能白屏。

还有一个很实际的建议:多动手。看再多文章,不如自己写一个最小可用的 Agent 跑起来。你会遇到各种文档里不会写的问题,解决这些问题的过程才是真正学到东西的时候。我最初写的 Agent 只有几十行代码,但它让我理解了整个流程,后面做复杂的系统就有了基础。

最后说一个我踩过的坑。我一开始把系统提示词写得很长很复杂,觉得这样模型能更好地理解任务。结果发现,提示词太长反而让模型困惑,表现下降。后来我把提示词精简到核心要点,用 Few-shot 示例来补充细节,效果反而更好。提示词不是越长越好,清晰和具体才是关键。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询