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。这和你封装一个useApihook,内部处理 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 重新打包。
-
初始化项目
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 -
配置 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 原生支持。这是前端工程师的“舒适区”,别被弃用警告吓退。 -
创建基础目录结构
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 就绪
- 启动开发服务器:
pnpm dev - 打开浏览器,输入:
2026 年最新的 Vue 3 面试题有哪些? - 观察控制台:你会看到 Agent 先调用
search_web,拿到 DuckDuckGo 的结果,然后 LLM 基于结果生成一段总结。 - 再输入:
用 TypeScript 写一个深拷贝函数,并运行测试 - 观察: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

6608

被折叠的 条评论
为什么被折叠?



