第一章:为什么你要学 AI Agent?—— 从"切图仔"到全栈的进化之路

361 阅读8分钟

为什么你要学 AI Agent?—— 从"切图仔"到全栈的进化之路

先讲个故事归公众号:糖墨夕 最新发布

假设你叫小陈,一个写了三年 Vue3 的前端。你的日常是:接需求、排页面、调接口、改 Bug。偶尔写写 Composable,用 ref 和 reactive 管理状态,用 watch 监听变化,日子过得挺稳。

直到有一天,老板在周会上突然说: "咱们要做个智能客服,能自动回答用户问题的那种。小陈,你研究一下,两周后给我方案。"

你当时脑子里大概是这样的:

1.png

2.png

好了,理清了这个认知,我们来看看,为什么前端工程师在"药剂师"这个角色上,反而比别人更有优势。

为什么前端在 AI 时代反而有优势?

很多人觉得 AI 开发是算法工程师的专利,得会 Python、懂深度学习、能训练模型。这个印象也不能说错——如果你要做模型层的工作,比如训练一个大模型、做微调、搞 RLHF,那确实需要深厚的 ML 背景。但问题是,99% 的 AI 应用开发者根本不需要碰模型层。

打个比方:你开一家餐厅,不需要会种地,只需要会用食材做菜。AI Agent 开发也一样:你不需要自己训练模型,你只需要"用好"现成的模型。种地是农民的事(OpenAI、Anthropic、DeepSeek 这些公司),做菜是厨师的事(你,应用开发者)。

搞 AI Agent,本质上是在做应用层开发,而不是模型层开发。  而说到应用层开发,前端工程师有几个看家本领,在 AI 领域简直是降维打击。

1. 流式渲染:你每天都在写的东西,AI 聊天刚好需要

你有没有用过 ChatGPT?打字的时候,回复是一个字一个字蹦出来的,不是一下子全出来。这种"流式输出"的效果,在 AI 应用里几乎无处不在——因为 LLM 生成文本本身就是一个 token 一个 token 往外蹦的过程,逐字输出是最自然的交互方式。

而实现这种效果,技术上叫什么?叫 Server-Sent Events(SSE)  或者 Streaming HTTP。前端同学应该不陌生——你写大文件分片上传、WebSocket 实时消息、进度条轮询,本质上都是在处理"数据流"。数据不是一次性返回的,而是一段一段来的,来了你就更新 UI。

举个例子,下面这段代码,前端同学看着应该很眼熟:

// 调用 AI 接口,流式读取响应
// 这跟处理大文件上传的进度回调,本质上是同一件事
const response = await fetch('/api/chat', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ message: '你是谁?' })
})

const reader = response.body!.getReader()
const decoder = new TextDecoder()

while (true) {
  const { done, value } = await reader.read()
  if (done) break

  // 每收到一段数据,就拼到消息上,UI 自动更新
  const chunk = decoder.decode(value, { stream: true })
  // Vue3 响应式:改了 messages,DOM 自动更新
  messages.value[messages.value.length - 1].content += chunk
}

这不就是前端最熟悉的"数据来了,更新 DOM"吗?只不过以前是 API 返回 JSON,现在是 AI 返回文字流,本质上没区别。你甚至会发现,AI 对话的流式渲染,比很多前端项目里的复杂交互还要简单。

在 Vue3 里,我们用 ref 或者 reactive 就能轻松接住这些流式数据。Vue 的响应式系统天生就是做这个的——数据变了,UI 自动跟着变,你不用手动操作 DOM。

而且,你还可以把这段流式读取逻辑封装成一个 Composable,以后任何 AI 项目都能复用:

// composables/useStreamChat.ts
// 封装了流式聊天逻辑,以后任何 AI 项目都能直接用
import { ref } from 'vue'

export function useStreamChat() {
  const messages = ref<Message[]>([])
  const isStreaming = ref(false)

  async function sendMessage(content: string) {
    messages.value.push({ role: 'user', content, id: crypto.randomUUID() })
    isStreaming.value = true

    const aiMessage = { role: 'assistant' as const, content: '', id: crypto.randomUUID() }
    messages.value.push(aiMessage)

    try {
      const response = await fetch('/api/chat', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ message: content })
      })

      if (!response.ok) throw new Error(`HTTP ${response.status}`)

      const reader = response.body!.getReader()
      const decoder = new TextDecoder()

      while (true) {
        const { done, value } = await reader.read()
        if (done) break
        aiMessage.content += decoder.decode(value, { stream: true })
      }
    } catch (err) {
      aiMessage.content = '抱歉,AI 响应出错了,请稍后重试。'
      console.error('流式聊天错误:', err)
    } finally {
      isStreaming.value = false
    }
  }

  return { messages, isStreaming, sendMessage }
}

封装成 Composable 之后,你在任何 Vue3 组件里都能用 const { messages, sendMessage } = useStreamChat() 一行代码搞定流式聊天。这就是前端工程化的力量——把复杂的逻辑封装起来,暴露简单的接口。这个能力,在后端 AI 开发场景里同样重要——你封装得越好,后续维护成本越低。

而且,Vue3 的 watch 和 computed 还能帮你做更多事情。比如:

// 用 watch 监听消息变化,自动滚动到底部
watch(
  () => messages.value.length,
  () => { nextTick(() => { chatContainer.value?.scrollTo({ top: 999999 }) }) }
)

// 用 computed 计算当前对话的 Token 用量
const tokenUsage = computed(() => {
  return messages.value.reduce((sum, msg) => sum + msg.content.length, 0)
})

这些操作,你写 Vue3 的时候天天都在用,搬到 AI 应用里完全复用。

2. 状态管理:你的 Pinia 经验可以直接复用

一个 AI Agent 对话界面,背后牵涉的状态其实不少,而且比普通的 CRUD 页面要复杂一些:

4.png

这些状态管理问题,用 Pinia 一个 Store 就搞定了。你每天都在做的事情,换个场景而已。

// 一个典型的 Agent 对话 Store —— 看看跟你平时的 Pinia Store 有啥区别?
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useChatStore = defineStore('chat', () => {
  // 当前会话的消息列表
  const messages = ref<Message[]>([])

  // Agent 当前的思考状态
  const agentStatus = ref<'idle' | 'thinking' | 'calling_tool' | 'responding' | 'error'>('idle')

  // 当前正在执行的工具调用
  const currentToolCalls = ref<ToolCall[]>([])

  // 用户偏好设置
  const userPreferences = ref<Record<string, any>>({})

  // 多会话列表
  const conversations = ref<Conversation[]>([])

  // 计算属性:当前会话的 Token 用量估算
  const estimatedTokens = computed(() => {
    return messages.value.reduce((sum, msg) => sum + Math.ceil(msg.content.length / 4), 0)
  })

  async function sendMessage(content: string) {
    messages.value.push({ role: 'user', content, id: crypto.randomUUID() })
    agentStatus.value = 'thinking'

    try {
      const response = await fetch('/api/agent/chat', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          messages: messages.value,
          preferences: userPreferences.value
        })
      })

      // 流式读取响应,逐字更新到 UI
      const reader = response.body!.getReader()
      const decoder = new TextDecoder()
      const aiMsg = { role: 'assistant' as const, content: '', id: crypto.randomUUID() }
      messages.value.push(aiMsg)

      agentStatus.value = 'responding'
      while (true) {
        const { done, value } = await reader.read()
        if (done) break
        aiMsg.content += decoder.decode(value, { stream: true })
      }
      agentStatus.value = 'idle'
    } catch (err) {
      agentStatus.value = 'error'
      console.error('Agent 调用失败:', err)
    }
  }

  return {
    messages, agentStatus, currentToolCalls,
    userPreferences, conversations, estimatedTokens,
    sendMessage
  }
})

你看,这不就是 Pinia 的常规操作吗?唯一的区别是,以前你调的是 CRUD 的 REST API,现在你调的是 AI Agent API。状态管理的内核没变,只是数据来源变了。  该用 ref 用 ref,该用 computed 用 computed,该用 watch 用 watch——都是你写了三年的东西。

而且,Pinia 的 $subscribe 和 $onAction 在 Agent 场景下特别好用。比如:

// 监听 Agent 状态变化,自动记录日志(方便调试)
const chatStore = useChatStore()

// 每当 agentStatus 变化时,打印日志
chatStore.$subscribe((mutation, state) => {
  if (mutation.storeId === 'chat') {
    console.log(`[Agent] 状态变更: ${state.agentStatus}`, {
      messageCount: state.messages.length,
      toolCalls: state.currentToolCalls
    })
  }
})

// 拦截 sendMessage action,记录每次请求的耗时
chatStore.$onAction(({ name, after }) => {
  if (name === 'sendMessage') {
    const start = Date.now()
    after(() => {
      console.log(`[Agent] sendMessage 耗时: ${Date.now() - start}ms`)
    })
  }
})

这些 Pinia 的高级特性,在 Agent 应用的调试和监控中非常实用。你不需要额外引入什么监控工具,Pinia 自带的 API 就能搞定。这就是"用你熟悉的工具做新事情"的好处——你不用去学新框架,只需要把旧工具用在新场景。

3. 用户体验:前端最懂用户想要什么

AI 应用最怕什么?最怕用户不知道 AI 在干嘛。

你见过那种点了按钮之后啥反应都没有的 APP 吗?是不是很想摔手机?AI 对话也一样。如果用户发了一条消息,等了 5 秒钟啥都没看到,他的第一反应不是"AI 在处理",而是"是不是我网断了?是不是系统挂了?"

前端工程师天天跟用户体验打交道,这些东西你闭着眼睛都知道怎么处理:

  • Loading 状态:AI 思考的时候,显示一个"正在思考..."或者跳跃的三个点动画。简单的骨架屏、动态的思考步骤展示,都能大幅降低用户的焦虑感。这些动画效果,你写 CSS 或者用 Vue 的 <Transition> 组件就能搞定。
  • 错误处理:AI 接口超时了怎么办?返回了乱码怎么办?Token 用完了怎么办?调用频率被限制了怎么办?每一种错误,你都得给用户一个友好的提示,而不是一个红色的 500 Internal Server Error。这跟普通表单提交的错误处理逻辑一模一样,只是错误类型多了几种。
  • 思考过程可视化:这是 AI Agent 特有的需求。Agent 在"思考"的过程中,可能会调用工具、搜索知识库、做推理。把这些步骤展示出来——比如"正在搜索知识库..." → "找到 3 条相关文档" → "正在生成回答..."——用户会觉得"哇,这个 AI 好聪明,它真的在做事",而不是"怎么还没好?"
  • 引导式交互:用户第一次用 AI 应用的时候,往往不知道能问什么。一个好的引导面板(比如"试试问这些问题"),或者一些预设的快捷指令,能极大降低使用门槛。
  • 断点续传:用户在对话过程中刷新了页面,回来之后对话还在吗?如果对话进行到一半,Agent 正在思考,刷新之后能恢复吗?这些细节,前端都需要考虑。

这些细节,纯后端工程师可能觉得"能用就行",但前端知道——体验好,用户才愿意用;用户愿意用,产品才有价值。  而 AI 应用的用户体验,目前整个行业都还在摸索阶段。你现在进来,正好是"开荒"的时候,你的每一个体验优化,都可能成为行业标准。

举个例子,现在很多 AI 对话产品的"思考过程可视化"做得很粗糙——要么只显示一个"思考中...",要么干脆什么都不显示。但如果你能做成像下面这样:

用户:帮我分析一下上个月的销售数据 Agent: [1/4] 正在理解你的需求... ✓ [2/4] 正在查询数据库... 找到 847 条记录 ✓ [3/4] 正在分析数据趋势... 发现 3 个异常点 ✓ [4/4] 正在生成分析报告... ▊

用户看到这个,心里就有底了——"Agent 在做事,不是卡住了"。而且每步完成之后打勾,给人一种"进度在推进"的感觉。这种细节,前端天生就比后端敏感。因为前端天天跟用户打交道,知道什么样的反馈能降低用户焦虑。

再比如,Agent 的"思考步骤"其实可以做成可交互的。用户点击某个步骤,可以展开看详细信息——比如"查询数据库"这一步,展开后能看到执行的 SQL 语句、返回的记录数、耗时。这些信息对普通用户可能没用,但对开发者调试非常有用。而且,把"调试信息"自然地融入"用户界面",这正是前端组件化设计的长处。

4. 组件化思维:Agent 前端就是一堆组件的组合

Vue3 的组件化思维,和 AI Agent 应用的前端架构简直是天作之合。你想想,一个 Agent 应用的前端,大概需要哪些组件?拆开来看:

5.png

这跟写一个后台管理系统有什么区别?都是组件拆分、数据流转、状态管理,套路一模一样。  每个组件接收 props,发出 emits,通过 Pinia 共享全局状态。你完全可以把你之前写后台管理系统的经验,平移到 AI Agent 应用的 UI 开发中。

而且,因为 AI 对话的 UI 模式相对固定(消息列表 + 输入框),你甚至可以写一套通用的 Agent UI 组件库,以后做任何 AI 项目都能复用。一套组件,所有 AI 项目通吃,这不比每次都从零写 CRUD 页面爽?

说到组件库,其实 Vue3 生态里已经有一些可以直接用的 AI 对话组件了,比如 Vue Chat UINaive UI 的 Chat 组件等。但我建议你前期还是自己手写一遍——因为只有自己写过,才知道流式渲染的细节、Markdown 渲染的坑、消息滚动定位的边界情况。等你自己写过一遍之后,再用现成的组件库,就能理解它们内部做了什么,出了问题也能自己排查。

还有一个细节点,前端可能没意识到:Markdown 渲染在 AI 对话里是刚需。  LLM 返回的内容经常包含代码块、表格、列表、加粗、链接。你写的消息气泡组件,需要支持 Markdown 渲染。而前端对 Markdown 渲染太熟了——marked + highlight.js,或者直接用 markdown-it,三行代码搞定。你在技术博客、文档站里早就用过无数次了。

// AI 消息气泡组件 —— 支持 Markdown 渲染
// MessageBubble.vue
<script setup lang="ts">
import { marked } from 'marked'
import { computed } from 'vue'

const props = defineProps<{ message: ChatMessage }>()

// 用 marked 把 Markdown 转成 HTML
const htmlContent = computed(() => {
  if (props.message.role === 'assistant') {
    return marked(props.message.content, { breaks: true })
  }
  return props.message.content
})
</script>

<template>
  <div :class="['message-bubble', message.role]">
    <div v-if="message.role === 'assistant'" v-html="htmlContent" />
    <div v-else>{{ message.content }}</div>
  </div>
</template>

你看,Markdown 渲染、代码高亮、深色模式适配——这些前端做了无数遍的事情,在 AI 对话里全部需要。而且前端做这些,比后端快得多、好得多。因为前端是离用户最近的人,最知道什么样的交互体验是好的

小结

AI Agent 应用的前端开发,本质上就是"流式数据渲染 + 状态管理 + 用户体验优化 + 组件化拆分"这四件事。而在这四件事上,前端工程师已经积累了多年的经验。所以,不要觉得自己是"写页面的"就搞不了 AI——恰恰相反,AI 应用最缺的就是能把用户体验做好的前端。你的技能树,在 AI 应用开发里是非常稀缺的。

AI Agent 不是魔法,就是一个"能自己做事的程序"

聊到 AI Agent,很多人脑子里会浮现出科幻电影里的画面:一个无所不能的 AI,能跟你聊天、帮你订机票、替你写代码、甚至还能跟你谈恋爱。坦白说,这种想象也不算错,但离我们现在的技术现实还有点距离。

现在能落地的 AI Agent,本质上就是一个 "能自己做事的程序" 。它和普通程序的区别在于:普通程序只会按你写的 if-else 执行,而 Agent 能自己"思考"下一步该做什么。 6.png 看到区别了吗?普通程序是"死"的,你写什么它就做什么;Agent 是"活"的,它能理解用户意图,自己决定该做什么,然后调用工具去执行。

那 Agent 是怎么做到"自己思考"的呢?其实拆开来看,就四个核心组件,缺一不可:

Agent 的四个核心组件

8.png

把这四个组件拼起来,就是一个能用的 Agent 了。听起来是不是没那么玄乎?

而且,实现一个最简版的 Agent,代码量比你想象的要少得多。我们直接写一个:

// 一个最简单的 AI Agent,不到 50 行代码
// 功能:用户问天气,Agent 自动调用天气 API 回答
// 运行前需要:npm install openai

import OpenAI from 'openai'

const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })

// 第一步:定义 Agent 可以用的工具
// 每个工具包含:名称、描述、参数定义(JSON Schema 格式)
const tools = [
  {
    type: 'function' as const,
    function: {
      name: 'get_weather',
      description: '获取指定城市的天气信息',
      parameters: {
        type: 'object',
        properties: {
          city: { type: 'string', description: '城市名称,如"北京"' }
        },
        required: ['city']
      }
    }
  }
]

// 第二步:实现工具的实际逻辑
// 实际项目中这里会调用真实的天气 API
async function getWeather(city: string): Promise<string> {
  // 模拟返回天气数据
  // 生产环境替换为:fetch(`https://api.weather.com?city=${city}`)
  return `${city}今天晴,25°C,湿度 60%,适合出门`
}

// 第三步:Agent 主循环 —— 这是 Agent 的核心逻辑
async function agentRun(userMessage: string) {
  const messages: any[] = [{ role: 'user', content: userMessage }]

  // 把用户消息发给 LLM,让它决定要不要调工具
  const response1 = await client.chat.completions.create({
    model: 'gpt-4',
    messages,
    tools  // 告诉 LLM 有哪些工具可用
  })

  const toolCall = response1.choices[0].message.tool_calls?.[0]

  // 如果 LLM 决定要调工具
  if (toolCall && toolCall.function.name === 'get_weather') {
    // 解析 LLM 提取的参数(比如 city: "北京")
    const args = JSON.parse(toolCall.function.arguments)
    const weatherResult = await getWeather(args.city)

    // 把工具执行结果发回给 LLM,让它生成最终回复
    messages.push(response1.choices[0].message)  // LLM 的"我要调工具"决策
    messages.push({
      role: 'tool',
      tool_call_id: toolCall.id,
      content: weatherResult  // 工具返回的结果
    })

    const response2 = await client.chat.completions.create({
      model: 'gpt-4',
      messages
    })

    return response2.choices[0].message.content
  }

  // LLM 觉得不需要调工具,直接返回它的回复
  return response1.choices[0].message.content
}

// 试试效果
const answer = await agentRun('北京今天天气怎么样?')
console.log(answer)
// 输出:"北京今天天气不错!晴,25°C,湿度 60%,很适合出门哦~"

7.png

对了,看到上面那个 Agent 代码里 get_weather 这个工具的定义了吗?注意它的参数格式:

{
  type: 'function',
  function: {
    name: 'get_weather',
    description: '获取指定城市的天气信息',
    parameters: {
      type: 'object',
      properties: {
        city: { type: 'string', description: '城市名称,如"北京"' }
      },
      required: ['city']
    }
  }
}

这个格式叫 JSON Schema,是 OpenAI 定义的 Function Calling 规范。你不需要手写这个 JSON,但你需要理解它的含义——它告诉 LLM:"嘿,你有一个叫 get_weather 的工具可以用,它需要一个 city 参数,是字符串类型。"LLM 看到这个描述,就知道当用户问天气的时候,它应该调用这个工具,并且把城市名作为参数传进去。

这个机制,就是 AI Agent 能够"做事"的底层原理。它不是什么神奇的黑箱,就是一个定义清晰、格式规范的接口约定。你作为开发者,要做的就是定义好工具、实现好工具,然后把工具列表发给 LLM,LLM 自己会决定什么时候该用哪个工具。

现在,让我们开始吧

好了,写了这么多,总结一下这一章的核心观点。记住这五句话就行:

其实这五句话,每一句都可以展开成一章。但你现在不需要记住所有细节——你只需要记住一个感觉: "AI Agent 开发,我好像也能搞。"  带着这个感觉进入下一章,学习效率会高很多。因为最难的从来不是技术本身,而是"我能不能学会"这个心理障碍。一旦你跨过了这个障碍,剩下的就是按部就班地学。

  1. AI Agent 开发是应用层开发,不是模型层开发。 你不需要会训练模型,只需要会用模型。就像你不需要会种地,只需要会用食材做菜。
  2. 前端在 AI Agent 开发中有天然优势。 流式渲染、状态管理、用户体验、组件化思维——这些都是你每天都在做的事情,搬到 AI 应用里完全复用。
  3. AI Agent 不是魔法,就是一个"能自己做事的程序"。 核心就四个组件:大脑(LLM)、工具(Tools)、记忆(Memory)、规划(Planning)。把它们拼起来,就是一个能用的 Agent。
  4. Vue3 + NestJS + TypeScript 是前端学 Agent 的最优路线。 全栈 TypeScript,前后端类型安全,学习成本最低,80% 的精力都能花在 Agent 开发本身。
  5. 15 章,四个阶段,边学边做,每章都有产出。 从第 5 章开始就能写出能跑的 Demo,学完就能写在简历上。

这五个观点,其实也是这本小册的"核心设计理念"。我写这本小册的目的,不是为了让你成为 AI 研究员,而是让你成为一个能用 AI 解决实际问题的全栈工程师。所以这本书里不会有复杂的数学公式,不会有晦涩的学术论文引用,只有"能跑通的代码"和"看得懂的比喻"。如果你觉得哪里看不懂,那一定是我没讲清楚,不是你的问题。

如果你看完这一章,心里的想法从"我搞不了 AI"变成了"好像也没那么难",那这一章的目的就达到了。AI Agent 开发的门槛,比你想象的低得多。你缺的不是能力,而是正确的入门路径和一套好的教程——而这两样东西,这本小册都给你准备好了。

最后,我想用一句话收尾:

"不要等到 AI 成熟了再学,那时候别人已经跑了很远了。
现在就是最好的时机,因为你已经具备了一切必要的基础。
剩下的,只是往前走一步。"

下一章,我们来聊聊"AI Agent 到底是个啥?"——用送外卖、炒菜、订机票这些生活场景,把 Agent 的四个核心概念掰开揉碎了讲。保证你看完就能跟朋友吹牛:"AI Agent?我懂,其实就是……"

归公众号:糖墨夕 最新发布