外观
AI 辅助前端开发:从编码助手到 LLM 应用集成
到 2026 年,AI 已经分成两条相对独立的前端实践:
- 用 AI 辅助开发:编码助手参与需求拆解、写代码、补测试、排查问题。
- 在产品中集成 AI:把 LLM 能力做成对话、总结、搜索、生成等用户功能。
两者的技术栈和风险完全不同。前者关注研发效率与代码质量,后者关注流式体验、成本、数据安全与提示词注入。
一、把 AI 变成可靠的工程协作者
AI 编码助手最容易被误用的方式,是把它当成"许愿机":一句话生成整个模块,然后直接合并。这种方式在复杂项目里往往会引入难以察觉的问题。
更有效的工作方式是:
- 提供充分上下文:相关文件、类型定义、现有约定、错误日志。
- 拆小任务:一次改一个函数、一个组件、一个测试,避免大范围重写。
- 让约束可执行:类型检查、ESLint、测试、构建必须能验证结果。
- 人工审查关键路径:鉴权、支付、数据迁移、性能敏感代码不能只看"能跑"。
一个实用的仓库约定文件(例如 AGENTS.md)能显著提升输出质量:
markdown
# 工程约定
- 使用 pnpm workspace,新增依赖前先确认是否已有同类包。
- 组件使用 `<script setup lang="ts">`,样式使用 CSS Modules。
- 所有对外请求必须经过 `src/api/client.ts`,禁止直接使用 fetch。
- 新增逻辑需要补充 Vitest 测试,关键流程补充 Playwright 用例。
- 不要修改 CI 配置、密钥文件和发布脚本,除非明确要求。
- 提交前运行 pnpm lint && pnpm typecheck && pnpm test。这比在每次对话里重复说明有效得多。AI 不擅长猜团队规范,但很擅长遵循明确写出的规则。
让测试成为护栏
AI 生成代码的速度远高于人工审查速度,所以自动化验证必须跟上:
bash
pnpm typecheck
pnpm lint
pnpm test -- --run
pnpm build在 CI 中把这些检查设为合并前置条件。对于 AI 大量参与的项目,测试覆盖率、类型严格度和构建检查比以往更重要,因为它们是唯一不依赖"逐行读代码"的质量门禁。
需要警惕的地方
- API 幻觉:模型可能编造不存在的配置项或库方法,必须以当前版本文档为准。
- 过度自信的重构:让 AI 改一个 bug,它可能顺带重构半个文件。
- 安全边界:不要把密钥、用户数据、内部地址贴进对话。
- 许可证与来源:引入生成代码前确认没有复制受限代码。
- 依赖膨胀:AI 倾向于引入新库解决问题,需要人工判断是否必要。
二、在产品中集成 LLM
产品级 AI 功能的基本架构:
text
浏览器
→ 自有后端(鉴权、限流、提示词、日志)
→ 模型服务商 / 自建推理
→ 流式响应(SSE / fetch stream)API Key 绝不能放在浏览器。所有请求必须经过自己的后端,后端负责鉴权、限流、内容审核、日志和成本控制。
后端流式代理
ts
// app/api/chat/route.ts
export const runtime = 'nodejs'
export async function POST(request: Request) {
const { message } = await request.json()
if (typeof message !== 'string' || message.length === 0 || message.length > 4000) {
return Response.json({ error: '消息长度不合法' }, { status: 400 })
}
const upstream = await fetch(`${process.env.LLM_BASE_URL}/chat/completions`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LLM_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: process.env.LLM_MODEL,
messages: [
{ role: 'system', content: '你是一名严谨的前端技术助手,不确定时明确说明。' },
{ role: 'user', content: message }
],
stream: true
})
})
if (!upstream.ok || !upstream.body) {
return Response.json({ error: '模型服务暂不可用' }, { status: 502 })
}
return new Response(upstream.body, {
headers: {
'Content-Type': 'text/event-stream; charset=utf-8',
'Cache-Control': 'no-cache, no-transform',
Connection: 'keep-alive'
}
})
}生产环境还需要补充:用户鉴权、速率限制、输入审核、输出审核、请求超时、重试和成本统计。
前端处理流式响应
ts
// hooks/useChatStream.ts
import { useCallback, useRef, useState } from 'react'
interface ChatState {
text: string
loading: boolean
error: string | null
}
export function useChatStream() {
const [state, setState] = useState<ChatState>({
text: '',
loading: false,
error: null
})
const controllerRef = useRef<AbortController | null>(null)
const send = useCallback(async (message: string) => {
controllerRef.current?.abort()
const controller = new AbortController()
controllerRef.current = controller
setState({ text: '', loading: true, error: null })
try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message }),
signal: controller.signal
})
if (!response.ok || !response.body) {
throw new Error(`请求失败:${response.status}`)
}
const reader = response.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const events = buffer.split('\n\n')
buffer = events.pop() ?? ''
for (const event of events) {
const dataLine = event
.split('\n')
.find((line) => line.startsWith('data:'))
if (!dataLine) continue
const data = dataLine.slice(5).trim()
if (!data || data === '[DONE]') continue
const payload = JSON.parse(data) as { delta?: string }
if (!payload.delta) continue
setState((current) => ({
...current,
text: current.text + payload.delta
}))
}
}
} catch (error) {
if (controller.signal.aborted) return
setState((current) => ({
...current,
error: error instanceof Error ? error.message : '未知错误'
}))
} finally {
if (!controller.signal.aborted) {
setState((current) => ({ ...current, loading: false }))
}
}
}, [])
const stop = useCallback(() => {
controllerRef.current?.abort()
setState((current) => ({ ...current, loading: false }))
}, [])
return { ...state, send, stop }
}几个容易被忽略的细节:
- SSE 事件可能被网络分片,必须用缓冲区拼接,不能假设一次
read()就是一个完整事件。 - 支持用户中止生成,
AbortController要同时中断前端请求和服务端上游请求。 - 组件卸载时要取消请求,避免状态更新到已卸载组件。
- 流式过程中出现错误,要保留已生成内容并显示可重试的提示。
Markdown 渲染必须清洗
模型输出本质上不可信。直接渲染 HTML 会引入 XSS:
ts
import MarkdownIt from 'markdown-it'
import DOMPurify from 'dompurify'
const markdown = new MarkdownIt({
html: false,
linkify: true,
breaks: true
})
export function renderModelOutput(text: string) {
return DOMPurify.sanitize(markdown.render(text))
}html: false 让 Markdown 中的原始 HTML 被转义,DOMPurify 再做一层兜底。外链要加 rel="noopener noreferrer",必要时限制链接协议。
三、RAG 与工具调用
当模型需要基于内部知识回答时,用检索增强生成(RAG):
text
用户问题
→ 向量化
→ 检索内部文档(带权限过滤)
→ 拼接上下文与引用
→ 模型生成
→ 前端展示答案与来源关键点:
- 权限过滤在检索阶段完成,不能把无权限文档放进上下文。
- 提示词要明确要求"只基于提供的资料回答,资料不足时说明不知道"。
- 前端展示引用来源,用户可以核对。
- 文档更新后要重建索引,否则答案会过期。
工具调用(Function Calling)让模型能查询订单、创建任务、调用内部 API。安全原则:
- 工具在服务端执行,前端只接收结构化结果。
- 每个工具做独立的权限校验,不信任模型传入的参数。
- 破坏性操作需要用户确认,不能由模型直接执行。
- 记录工具调用日志,便于审计。
MCP(Model Context Protocol)正在成为模型访问外部工具和数据的标准协议。它降低了集成成本,但也扩大了攻击面,接入内部系统时要严格控制权限范围。
四、成本、隐私与体验
成本控制:
- 限制单次输入和输出 token 数。
- 对重复问题做缓存,例如相同检索结果加相同问题。
- 简单任务使用小模型,复杂任务再路由到大模型。
- 按用户或租户做配额,防止单用户刷爆额度。
- 监控 token 用量、首 token 延迟和端到端延迟。
隐私与合规:
- 明确告知用户数据会被发送给模型服务商。
- 敏感字段做脱敏,或使用自建模型。
- 不在日志中记录完整对话和密钥。
- 提供数据删除和导出能力。
体验细节:
- 首 token 时间比总时长更影响感知,优先优化首字节。
- 提供停止、重试和重新生成。
- 空状态和错误状态要可恢复,不要让用户面对一个永远转圈的界面。
- 使用
aria-live通知屏幕阅读器内容更新,但避免逐 token 播报。 - 对 AI 生成内容做标识,避免用户误认为是确定事实。
五、团队落地清单
- 约定仓库上下文:编写
AGENTS.md,明确技术栈、目录规范和验证命令。 - 先补护栏:类型检查、Lint、测试、构建必须在 CI 中稳定运行。
- 从低风险任务开始:测试补全、文案调整、类型修复、文档生成。
- 建立审查规范:AI 参与的代码同样走 Code Review,关键路径提高审查级别。
- 沉淀提示词与工具:把高频任务做成模板或内部工具,而不是每个人各写一套。
- 监控产品 AI 功能:延迟、失败率、token 成本、用户反馈和内容安全。
- 定期复盘:哪些任务 AI 做得好,哪些必须人工,形成团队自己的边界。
小结
AI 对前端工程师的价值,不是替代判断,而是放大执行力。在开发侧,用清晰的约束和自动化验证把 AI 的输出纳入工程体系;在产品侧,把模型当作不可信的外部服务,做好流式体验、权限、安全和成本控制。能把这两件事做好,AI 才是真正的生产力。