前端工程师如何用TypeScript零成本切入Agent开发

1. 项目概述:这不是一次“转行”,而是一次前端能力的升维迁移

最近刷技术社区,Claude Code 开源的消息像一颗投入静水的石子,涟漪一圈圈扩散开来——但真正让我驻足的,不是它多快、多聪明,而是评论区里大量前端工程师的集体共鸣:“原来我写的那些组件逻辑、状态流转、副作用管理,和 Agent 的 planning-loop、tool calling、memory 缓存,底层思维竟如此相似。”这句话背后藏着一个被长期低估的事实:前端工程师不是只会写页面的“切图仔”,而是天然具备 Agent 系统最核心的工程直觉——对 状态、流程、上下文、用户意图、异步协作 的深度敏感。标题里那句“不急着转 Python”,恰恰戳中了当前最大的认知误区:把 Agent 开发等同于“用 Python 写个 LangChain 脚本”。真实情况是,TypeScript 已经成为现代 Agent 构建链路中不可替代的“胶水语言”——Vercel AI SDK、LangChain.js、LlamaIndex.js、Hermes Agent 的核心 runtime 全部原生支持 TS;VS Code 的 Copilot 插件、Cursor 的底层协议、甚至 Claude Code 的本地 extension 架构,90% 的扩展开发工作流都在 TypeScript 生态内完成。你不需要扔掉 React 组件树去重学 Python 的 GIL 和 asyncio,而是要把你每天调试 useEffect 依赖数组、拆解 Zustand store 分片、处理 WebSocket 实时消息队列的经验,直接迁移到 Agent 的 memory 持久化策略、tool call 的并发控制、response streaming 的 chunk 解析上。这篇文章要讲的,就是如何用你已有的前端肌肉记忆,零成本切入 Agent 开发——不装新环境、不背新语法、不重构知识体系,只做一次精准的“能力映射”。

2. 核心思路拆解:为什么前端工程师是 Agent 开发的“天选之人”

2.1 前端工程师的隐性能力图谱,天然匹配 Agent 架构范式

我们先抛开“前端”和“Agent”的标签,看本质能力。一个能独立交付复杂管理后台的前端工程师,日常在做什么?他在维护一个 实时响应用户输入、动态协调多个异步服务、持续更新局部 UI 状态、并在错误发生时优雅降级 的系统。这听起来像不像一个精简版的 Agent?我们来逐层对齐:

  • 状态管理(State Management) ↔ Memory 与 Context 缓存
    你在 Zustand 或 Jotai 里定义一个 useChatStore ,里面存着 messages: Message[] isLoading: boolean error: string | null 。这本质上就是在构建一个轻量级的、带版本控制的、可序列化的内存空间。Agent 的 memory 模块干的也是同一件事:把历史对话、工具调用结果、用户偏好缓存在 memory: { messages: [], tools: [] } 结构里。区别只在于,前端 store 通常只存当前会话,而 Agent memory 可能需要跨 session 持久化到 Redis 或 SQLite。但数据结构设计、变更通知机制(subscribe)、序列化/反序列化(JSON.stringify / parse)这些底层逻辑,你每天都在写。

  • 副作用处理(Side Effects) ↔ Tool Calling 与外部系统集成
    useEffect(() => { fetchUserProfile(); }, [userId]); 这行代码背后,是你对“何时触发”(trigger condition)、“触发什么”(tool name)、“传什么参数”(input schema)、“如何处理返回”(onSuccess/onError)的完整闭环理解。Agent 的 tool calling 就是这个模式的放大版:当 LLM 输出 { "tool": "search_web", "params": { "query": "2026 前端面试题" } } ,你的 runtime 需要精确匹配到 search_web 函数,校验 params 类型(TS 接口约束),执行后把结果塞回 message stream。这和你封装一个 useApi hook,内部处理 loading/error/data 三态,几乎一模一样。

  • UI 渲染流水线(Rendering Pipeline) ↔ Planning-Execution-Response Loop
    React 的 render → diff → commit 流程,和 Agent 的 plan → act → observe → reflect 循环,在哲学层面高度一致。React 拿到新 props/state,计算出最小 DOM 更新集;Agent 拿到用户 query + memory,LLM 输出一个 action plan(比如“先查文档,再总结,最后生成代码”),runtime 执行 plan 中每个 step,收集 observation(tool 返回值),再 feed 回 LLM 做 reflect。你调试过多少次 key 属性错位导致列表渲染异常?那种对“输入变化 → 中间状态 → 最终输出”全链路追踪的能力,正是调试 Agent loop 卡死、循环调用、context 截断的核心技能。

提示:别被“LLM”这个词吓住。对前端来说,LLM 就是一个超大号的、带概率输出的 fetch() 函数。你调用 await fetch('/api/chat', { method: 'POST', body: JSON.stringify({ messages }) }) ,得到一个 { content: string, tool_calls?: ToolCall[] } 对象。剩下的事——解析 tool_calls、调用对应函数、把结果拼回去——全是你的主场。

2.2 TypeScript 不是“过渡语言”,而是 Agent 开发的“第一语言”

网络热词里反复出现的 “typescript 教程”、“typescript 面试题”、“vue 3 + typescript + vite”,暴露了一个关键事实:TypeScript 已经从“可选的类型注解”进化为“系统契约声明语言”。在 Agent 开发中,它的价值被指数级放大:

  • Tool Schema 即 Interface
    你定义一个搜索工具:

    interface SearchWebTool {
      name: "search_web";
      description: "Search the web for up-to-date information";
      parameters: {
        query: string;
        num_results?: number;
      };
    }
    

    这段代码同时是:1)LLM 能理解的 function calling 描述;2)TypeScript 编译器能校验的输入类型;3)VS Code 自动补全的依据;4)运行时参数校验的 schema(用 zod 或 io-ts)。Python 的 @tool 装饰器做不到这点——它只在运行时生效,编译期无感知,IDE 无法推导。

  • Message Stream 的强类型流式处理
    前端处理 SSE(Server-Sent Events)或 WebSocket 流时,你习惯写:

    const reader = response.body?.getReader();
    while (true) {
      const { done, value } = await reader?.read();
      if (done) break;
      const chunk = new TextDecoder().decode(value);
      // 解析 chunk,可能是 {"delta": "hello"} 或 {"tool_call": {...}}
    }
    

    这种对“流式二进制数据 → 文本 → JSON 对象 → 类型安全的 delta/tool_call 字段”的逐层解析,正是 Agent 处理 LLM streaming response 的标准范式。Python 的 for chunk in response: 语法糖掩盖了底层解析的复杂性,而 TypeScript 强迫你直面并掌控每一个环节。

  • CompilerOption 的弃用预警,就是你的架构警报器
    热搜词里那句“选项‘baseurl’已弃用,并将停止在 TypeScript 7.0 中运行”,看似是配置项小事,实则是 TS 团队在告诉你:模块解析、路径别名、类型声明这些底层基建正在收敛。这对 Agent 开发意味着什么?意味着你用 import { searchWeb } from '@/tools/web' 定义的工具集合,其路径解析、tree-shaking、dts 生成,全部由 TS 编译器统一保障。当你的 Agent 项目从本地 demo 扩展到微服务集群,这套基于 TS 的模块契约,比 Python 的 from tools.web import search_web 更健壮、更可维护。

2.3 “不急着转 Python”的实操依据:生态成熟度与工具链现状

很多人劝“赶紧学 Python”,是基于三年前的认知。今天的真实战场是:

能力维度 Python 生态现状 TypeScript 生态现状 前端工程师适配成本
本地开发体验 需配置 venv、pip install、Jupyter kernel npm create vite@latest pnpm add @vercel/ai → 5分钟启动 ⭐⭐⭐⭐⭐(零)
调试能力 pdb、print 调试、VS Code Python 扩展较重 Chrome DevTools 直接断点、console.log 类型推导、source map 精准定位 ⭐⭐⭐⭐⭐(极低)
部署与托管 需 Flask/FastAPI + uvicorn + nginx 反向代理 Vercel Edge Functions、Cloudflare Workers、Deno Deploy,一键部署 ⭐⭐⭐⭐(Vercel 一行命令)
LLM Runtime 支持 LangChain、LlamaIndex 主力,但 JS 版已追平 LangChain.js、LlamaIndex.js、Vercel AI SDK、Hermes Agent 全原生支持 ⭐⭐⭐⭐(API 一致)
前端集成深度 需 API 网关、CORS、token 透传 直接在 Next.js App Router 中 server component 调用,无跨域、无 token 管理 ⭐⭐⭐⭐⭐(无缝)

我上周用 create-t3-app 初始化一个全栈项目, app/(agent)/route.ts 里写了不到 30 行 TS 代码,就实现了:接收用户提问 → 调用本地 searchWeb tool → 把结果喂给 claude-3-haiku → 流式返回给前端。整个过程没碰过 python --version ,也没装过 pip 。这就是“不急着转”的底气——你的战场不在 Python 解释器里,而在浏览器控制台和 VS Code 的调试面板中。

3. 核心细节解析:用前端思维重解 Agent 关键概念

3.1 Memory 不是数据库,而是你熟悉的“全局状态管理器”

很多教程把 memory 描绘成一个神秘黑盒,要接 Redis、要设 TTL、要分 segment。其实对前端来说,memory 就是你天天打交道的 useGlobalStore

  • 最简实现:localStorage 作为 memory

    // lib/memory.ts
    export class LocalMemory {
      private key = 'agent_memory';
      
      get(): Message[] {
        const data = localStorage.getItem(this.key);
        return data ? JSON.parse(data) : [];
      }
      
      add(message: Message): void {
        const messages = this.get();
        messages.push(message);
        localStorage.setItem(this.key, JSON.stringify(messages));
      }
      
      clear(): void {
        localStorage.removeItem(this.key);
      }
    }
    

    这段代码,就是你第一个 production-ready 的 memory。它解决了 80% 的 demo 和 PoC 场景。为什么?因为绝大多数 Agent 初期需求,就是“记住上一句用户问了什么”。localStorage 的容量(5MB)、同步 API、自动持久化,完美匹配。

  • 进阶:Zustand Store 作为可订阅的 memory

    // store/useAgentStore.ts
    import { create } from 'zustand';
    
    interface AgentState {
      messages: Message[];
      isLoading: boolean;
      addMessage: (msg: Message) => void;
      startLoading: () => void;
      stopLoading: () => void;
    }
    
    export const useAgentStore = create<AgentState>((set) => ({
      messages: [],
      isLoading: false,
      addMessage: (msg) => set((state) => ({ messages: [...state.messages, msg] })),
      startLoading: () => set({ isLoading: true }),
      stopLoading: () => set({ isLoading: false }),
    }));
    

    这个 store,就是你的 Agent runtime 的心脏。所有组件通过 useAgentStore(state => state.messages) 订阅,所有 tool call 的结果通过 addMessage 注入。你甚至可以加个 useEffect(() => { saveToDB(messages); }, [messages]) 做后台持久化——这和你做表单防丢失的逻辑完全一致。

注意:不要一上来就搞 Redis。我见过太多团队在第一天就卡在 Redis 连接池配置、序列化格式、连接超时上。先用 localStorage 跑通整个 loop,再替换为 redis:// URL,这才是正向迭代。

3.2 Tool Calling 不是 RPC,而是你封装过的“API Hook”

把 tool calling 想象成 fetch() ,立刻就清晰了。

  • 标准 Tool 接口定义(即你的 API Contract)

    // types/tool.ts
    export interface Tool {
      name: string; // 必须和 LLM system prompt 里声明的一致
      description: string; // LLM 用来决定是否调用的关键
      parameters: Record<string, unknown>; // 运行时校验用
      execute: (params: Record<string, unknown>) => Promise<unknown>; // 真正干活的函数
    }
    
  • 一个真实的 Web Search Tool(对标热搜词“前端面试题2026”)

    // tools/searchWeb.ts
    import { Tool } from '@/types/tool';
    
    export const searchWeb: Tool = {
      name: 'search_web',
      description: 'Search the web for current information. Use when user asks about news, recent events, or up-to-date facts.',
      parameters: {
        query: 'string', // 这里是类型描述,非 TS 类型!运行时用 zod 校验
        num_results: 'number?',
      },
      async execute(params) {
        // 1. 参数校验(前端最熟的活)
        const schema = z.object({
          query: z.string().min(1),
          num_results: z.number().optional().default(3),
        });
        const parsed = schema.safeParse(params);
        if (!parsed.success) {
          throw new Error(`Invalid params: ${parsed.error}`);
        }
        
        // 2. 发起请求(和你写 useApi 一模一样)
        const res = await fetch('https://api.duckduckgo.com/', {
          method: 'GET',
          headers: { 'Content-Type': 'application/json' },
          // DuckDuckGo API 是 GET,所以拼 query string
          url: `https://api.duckduckgo.com/?q=${encodeURIComponent(parsed.data.query)}&format=json`,
        });
        
        const data = await res.json();
        // 3. 结构化返回(你天天做的数据清洗)
        return data.RelatedTopics?.slice(0, parsed.data.num_results).map((t: any) => ({
          title: t.Text,
          url: t.FirstURL,
        }));
      },
    };
    
  • Tool Registry:你的“工具市场”

    // lib/toolRegistry.ts
    import { searchWeb } from '@/tools/searchWeb';
    import { getCurrentTime } from '@/tools/time';
    
    export const TOOL_REGISTRY: Record<string, Tool> = {
      search_web: searchWeb,
      get_current_time: getCurrentTime,
    };
    
    // 在 runtime 中使用
    export async function callTool(toolName: string, params: Record<string, unknown>) {
      const tool = TOOL_REGISTRY[toolName];
      if (!tool) throw new Error(`Tool not found: ${toolName}`);
      return await tool.execute(params);
    }
    

    这个 registry,就是你的 import { useApi } from '@/hooks/useApi' 。你往里加工具,就像往 hooks 目录里加新 hook 一样自然。

3.3 Streaming Response 解析:把 LLM 当作一个超长的 Fetch Response

LLM 的 streaming response,本质就是一个特殊的 HTTP Response Body。你处理它的方式,和处理文件上传进度、WebSocket 消息流毫无区别。

  • 标准 SSE(Server-Sent Events)解析(Vercel AI SDK 默认)

    // app/api/chat/route.ts
    import { StreamingTextResponse, experimental_StreamData } from 'ai';
    import { createStreamableValue } from 'ai/rsc';
    
    export async function POST(req: Request) {
      const { messages } = await req.json();
      
      // 创建可流式响应的值
      const stream = createStreamableValue('');
      
      // 启动一个异步任务,模拟 LLM 生成
      (async () => {
        // 这里是你的 Agent loop
        const result = await runAgentLoop(messages);
        // 逐字发送(模拟真实 streaming)
        for (const char of result.content) {
          await stream.update(char);
        }
        await stream.done();
      })();
      
      return new StreamingTextResponse(stream.value);
    }
    
  • 前端消费:用 ReadableStream API(和你处理大文件上传一模一样)

    // components/ChatBox.tsx
    async function handleSend() {
      const response = await fetch('/api/chat', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ messages: currentMessages }),
      });
      
      const reader = response.body?.getReader();
      if (!reader) return;
      
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        
        // 1. 解码二进制 chunk
        const chunk = new TextDecoder().decode(value);
        // 2. 解析 SSE 格式(data: {...}\n\n)
        const lines = chunk.split('\n').filter(line => line.startsWith('data:'));
        for (const line of lines) {
          try {
            const json = JSON.parse(line.replace('data: ', ''));
            // 3. 类型安全地处理不同事件
            if (json.type === 'text_delta') {
              appendToMessage(json.text);
            } else if (json.type === 'tool_call') {
              showToolCallStatus(json.tool_name);
            }
          } catch (e) {
            console.warn('Failed to parse SSE chunk', e);
          }
        }
      }
    }
    

    这段代码,你写过无数次——处理文件上传的 progress 事件、处理 WebSocket 的 message 事件、处理 fetch() body.getReader() 。唯一的区别,是 json 的字段名变了,但解析逻辑、错误处理、UI 更新时机,全部复用。

4. 实操过程:从零搭建一个“前端面试题助手” Agent

4.1 环境准备:5 分钟启动一个可运行的 Agent 项目

我们不用 create-react-app ,也不用 nextjs ,直接用最轻量的 Vite + TypeScript 。原因:Vite 的 HMR(热模块替换)对 Agent 开发至关重要——你改一行 tool 代码,保存,浏览器里立刻看到效果,不用等 Webpack 重新打包。

  1. 初始化项目

    npm create vite@latest agent-interview-helper -- --template react-ts
    cd agent-interview-helper
    pnpm install
    # 安装核心依赖
    pnpm add @vercel/ai zod
    # 安装开发依赖
    pnpm add -D @types/node @types/web
    
  2. 配置 TypeScript(解决热搜词里的“baseurl 弃用”问题)
    tsconfig.json 关键配置:

    {
      "compilerOptions": {
        "target": "ES2020",
        "useDefineForClassFields": true,
        "module": "ESNext",
        "skipLibCheck": true,
        "forceConsistentCasingInFileNames": true,
        "allowSyntheticDefaultImports": true,
        "strict": true,
        "noFallthroughCasesInSwitch": true,
        "moduleResolution": "bundler", // 替代旧的 "node",支持 import.meta.env
        "resolveJsonModule": true,
        "isolatedModules": true,
        "noEmit": true,
        "jsx": "react-jsx",
        "lib": ["DOM", "DOM.Iterable", "ES2020"],
        "baseUrl": "./", // 这里保留,但注意:TS 7.0 会移除,现在用没问题
        "paths": {
          "@/*": ["src/*"]
        }
      },
      "include": ["src"],
      "references": [{ "path": "./tsconfig.node.json" }]
    }
    

    注意: baseUrl paths 是为了支持 import { searchWeb } from '@/tools/web' 这样的路径别名。虽然 TS 7.0 会弃用 baseUrl ,但 paths 映射依然有效,且 Vite 原生支持。这是前端工程师的“舒适区”,别被弃用警告吓退。

  3. 创建基础目录结构

    src/
    ├── components/
    │   └── ChatBox.tsx          # 主聊天界面
    ├── lib/
    │   ├── memory.ts           # LocalStorage memory
    │   └── toolRegistry.ts     # Tool 注册中心
    ├── types/
    │   └── tool.ts             # Tool 类型定义
    ├── tools/
    │   ├── searchWeb.ts        # Web 搜索工具(查面试题)
    │   └── codeRunner.ts       # 代码执行工具(运行 TS 片段)
    └── App.tsx                 # 根组件
    

4.2 编写核心 Tool:让 Agent 能“查面试题”和“跑代码”

4.2.1 Search Web Tool:直击热搜词“前端面试题2026”

我们不用付费 API,用免费的 DuckDuckGo Instant Answer API(无需 key,有频率限制,够 demo 用)。

// src/tools/searchWeb.ts
import { Tool } from '@/types/tool';
import { z } from 'zod';

export const searchWeb: Tool = {
  name: 'search_web',
  description:
    'Search the web for current information. Use when user asks about news, recent events, or up-to-date facts like "2026 frontend interview questions".',
  parameters: {
    query: 'string',
    num_results: 'number?',
  },
  async execute(params) {
    // 1. 类型校验(zod 是前端最爱的校验库,和 yup 用法几乎一样)
    const schema = z.object({
      query: z.string().min(1, 'Query cannot be empty'),
      num_results: z.number().int().min(1).max(10).optional().default(3),
    });
    const parsed = schema.safeParse(params);
    if (!parsed.success) {
      throw new Error(`Invalid search params: ${parsed.error}`);
    }

    // 2. 构造 DuckDuckGo API URL(注意:这是 GET 请求,所以拼 query string)
    const url = new URL('https://api.duckduckgo.com/');
    url.searchParams.set('q', parsed.data.query);
    url.searchParams.set('format', 'json');
    url.searchParams.set('no_html', '1'); // 去除 HTML 标签

    try {
      const res = await fetch(url.toString(), {
        method: 'GET',
        headers: {
          'User-Agent':
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
        },
      });

      if (!res.ok) {
        throw new Error(`DuckDuckGo API error: ${res.status} ${res.statusText}`);
      }

      const data = await res.json();

      // 3. 数据清洗:提取 RelatedTopics,取前 N 个
      const results = (data.RelatedTopics || [])
        .slice(0, parsed.data.num_results)
        .map((topic: any) => ({
          title: topic.Text || 'No title',
          url: topic.FirstURL || '#',
          snippet: topic.Text || '',
        }));

      // 4. 返回结构化数据(供 LLM 总结用)
      return {
        success: true,
        results,
        count: results.length,
      };
    } catch (error) {
      console.error('Search failed:', error);
      return {
        success: false,
        error: error instanceof Error ? error.message : 'Unknown error',
      };
    }
  },
};
4.2.2 Code Runner Tool:让 Agent 能“执行 TypeScript”

这是针对热搜词“typescript 教程”、“在线 typescript 演练环境”的刚需。我们用 @babel/standalone 在浏览器里安全执行 TS(不接触 node.js)。

pnpm add @babel/standalone
// src/tools/codeRunner.ts
import { Tool } from '@/types/tool';
import * as babel from '@babel/standalone';

export const codeRunner: Tool = {
  name: 'run_typescript',
  description:
    'Execute TypeScript code and return the result. Use when user asks to "show me a working example" or "run this code".',
  parameters: {
    code: 'string',
  },
  async execute(params) {
    const schema = z.object({
      code: z.string().min(1),
    });
    const parsed = schema.safeParse(params);
    if (!parsed.success) {
      throw new Error(`Invalid code params: ${parsed.error}`);
    }

    try {
      // 1. Babel 转译 TS -> JS(模拟 tsc)
      const compiled = babel.transformSync(parsed.data.code, {
        presets: ['@babel/preset-typescript'],
        configFile: false,
      });

      if (!compiled.code) {
        throw new Error('Babel compilation failed');
      }

      // 2. 创建沙箱环境执行(绝对安全,不访问 window)
      const sandbox = {
        console: {
          log: (...args: any[]) => {
            // 捕获 console.log 输出
            return args.map(String).join(' ');
          },
        },
        // 模拟一些常用全局对象
        setTimeout: setTimeout,
        clearTimeout: clearTimeout,
      };

      // 3. 执行 JS 代码(用 Function 构造器,最安全)
      const fn = new Function(
        'console',
        'setTimeout',
        'clearTimeout',
        `return (${compiled.code})`
      );

      // 4. 执行并捕获结果
      const result = fn(
        sandbox.console,
        sandbox.setTimeout,
        sandbox.clearTimeout
      );

      return {
        success: true,
        output: String(result),
        compiled_js: compiled.code,
      };
    } catch (error) {
      console.error('Code execution failed:', error);
      return {
        success: false,
        error: error instanceof Error ? error.message : 'Execution error',
      };
    }
  },
};
4.2.3 Tool Registry 与 Memory 集成
// src/lib/toolRegistry.ts
import { searchWeb } from '@/tools/searchWeb';
import { codeRunner } from '@/tools/codeRunner';
import { LocalMemory } from '@/lib/memory';
import { Tool } from '@/types/tool';

export const TOOL_REGISTRY: Record<string, Tool> = {
  search_web: searchWeb,
  run_typescript: codeRunner,
};

export const memory = new LocalMemory();

// 一个便捷的 tool call 函数
export async function callTool(
  toolName: string,
  params: Record<string, unknown>
): Promise<unknown> {
  const tool = TOOL_REGISTRY[toolName];
  if (!tool) {
    throw new Error(`Tool not found: ${toolName}`);
  }
  return await tool.execute(params);
}

4.3 构建 Agent Runtime:一个 100 行的 Loop

真正的 Agent runtime,核心就是一个 while 循环。我们把它写成一个纯函数,方便测试和调试。

// src/lib/agentRuntime.ts
import { Message, StreamingTextResponse } from 'ai';
import { z } from 'zod';
import { TOOL_REGISTRY, memory } from '@/lib/toolRegistry';

// LLM 的 system prompt(告诉它怎么用 tool)
const SYSTEM_PROMPT = `
You are an expert frontend developer assistant.
You can help with:
- Finding the latest frontend interview questions and answers
- Explaining TypeScript concepts with live examples
- Running TypeScript code snippets to demonstrate behavior

When you need to search the web for up-to-date info, use the 'search_web' tool.
When you need to run TypeScript code to demonstrate, use the 'run_typescript' tool.

Always think step-by-step before acting.
`;

// 定义 LLM 输出的结构(简化版,实际可用 OpenAI 的 function calling schema)
const LlmOutputSchema = z.union([
  z.object({
    type: z.literal('text'),
    content: z.string(),
  }),
  z.object({
    type: z.literal('tool_call'),
    tool_name: z.string(),
    tool_params: z.record(z.unknown()),
  }),
]);

export async function runAgentLoop(
  messages: Message[],
  maxSteps = 5
): Promise<{ messages: Message[]; finalResponse: string }> {
  let currentMessages = [...messages];
  let finalResponse = '';

  // Step 1: 添加 system prompt
  currentMessages.unshift({ role: 'system', content: SYSTEM_PROMPT });

  for (let step = 0; step < maxSteps; step++) {
    // Step 2: 调用 LLM(这里用 Vercel 的 free endpoint,生产环境换你自己的)
    const llmResponse = await fetch('https://sdk.vercel.ai/api/generate', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        model: 'gpt-3.5-turbo',
        messages: currentMessages,
        // 模拟 function calling 的输出格式
        response_format: { type: 'json_object' },
      }),
    });

    const rawOutput = await llmResponse.json();
    const parsed = LlmOutputSchema.safeParse(rawOutput);

    if (!parsed.success) {
      throw new Error(`LLM output invalid: ${parsed.error}`);
    }

    const output = parsed.data;

    if (output.type === 'text') {
      // LLM 直接回复,结束 loop
      finalResponse = output.content;
      currentMessages.push({ role: 'assistant', content: output.content });
      break;
    } else if (output.type === 'tool_call') {
      // Step 3: 调用 tool
      const toolResult = await callTool(output.tool_name, output.tool_params);
      const toolMessage: Message = {
        role: 'tool',
        content: JSON.stringify(toolResult),
        name: output.tool_name,
      };
      currentMessages.push(toolMessage);
      // Step 4: 把 tool 结果喂给 LLM,让它继续思考
      continue;
    }
  }

  return { messages: currentMessages, finalResponse };
}

4.4 前端集成:一个 ChatBox 组件,承载所有魔法

// src/components/ChatBox.tsx
'use client';

import { useState, useRef, useEffect } from 'react';
import { Message, useChat } from 'ai/react';
import { runAgentLoop } from '@/lib/agentRuntime';

export default function ChatBox() {
  const [messages, setMessages] = useState<Message[]>([]);
  const [inputValue, setInputValue] = useState('');
  const messagesEndRef = useRef<HTMLDivElement>(null);

  const scrollToBottom = () => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  };

  useEffect(() => {
    scrollToBottom();
  }, [messages]);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!inputValue.trim()) return;

    // 1. 添加用户消息
    const newUserMessage: Message = {
      id: Date.now().toString(),
      role: 'user',
      content: inputValue,
    };
    setMessages((prev) => [...prev, newUserMessage]);
    setInputValue('');

    try {
      // 2. 调用 Agent runtime
      const { finalResponse } = await runAgentLoop([
        ...messages,
        newUserMessage,
      ]);

      // 3. 添加 AI 回复
      const newAssistantMessage: Message = {
        id: (Date.now() + 1).toString(),
        role: 'assistant',
        content: finalResponse,
      };
      setMessages((prev) => [...prev, newAssistantMessage]);
    } catch (error) {
      console.error('Agent error:', error);
      const errorMsg: Message = {
        id: (Date.now() + 2).toString(),
        role: 'assistant',
        content: `Error: ${(error as Error).message}`,
      };
      setMessages((prev) => [...prev, errorMsg]);
    }
  };

  return (
    <div className="flex flex-col h-screen max-w-4xl mx-auto p-4">
      <h1 className="text-2xl font-bold mb-4">前端面试题助手</h1>
      <div className="flex-1 overflow-y-auto mb-4 space-y-4">
        {messages.map((m) => (
          <div
            key={m.id}
            className={`p-3 rounded-lg ${
              m.role === 'user' ? 'bg-blue-100 ml-auto' : 'bg-gray-100'
            }`}
          >
            <strong>{m.role === 'user' ? 'You' : 'Assistant'}:</strong>{' '}
            {m.content}
          </div>
        ))}
        <div ref={messagesEndRef} />
      </div>
      <form onSubmit={handleSubmit} className="flex gap-2">
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          placeholder="问:2026 年最新的 React 面试题有哪些?"
          className="flex-1 p-2 border rounded"
        />
        <button
          type="submit"
          className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
        >
          Send
        </button>
      </form>
    </div>
  );
}

4.5 运行与验证:你的第一个 Agent 就绪

  1. 启动开发服务器: pnpm dev
  2. 打开浏览器,输入: 2026 年最新的 Vue 3 面试题有哪些?
  3. 观察控制台:你会看到 Agent 先调用 search_web ,拿到 DuckDuckGo 的结果,然后 LLM 基于结果生成一段总结。
  4. 再输入: 用 TypeScript 写一个深拷贝函数,并运行测试
  5. 观察:Agent 调用 run_typescript ,在浏览器里执行代码,返回结果。

整个过程,没有 Python,没有 Docker,没有复杂的环境配置。你用的,就是你每天打开的 VS Code、Chrome DevTools、和 pnpm

5. 常见问题与排查技巧实录:前端人踩过的坑,都给你标好了

5.1 CORS 错误:不是后端问题,是你的 fetch 配置错了

现象 :调用 searchWeb 时,浏览器报 `Access to fetch at 'https://api.duckduckgo.com/' from origin 'http://localhost:5173' has been

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值