👤 关于作者
JavaAgent架构师 --- 十年Java分布式架构老兵,专注AI Agent企业级落地。
主导过数字员工、SOP智能引擎等项目,开发过RPC框架、消息中间件、ORM框架。
正在输出:
专栏:
专栏一:《前端AI工程化》10期适合前端 SSE/流式渲染/.../企业级AI架构/AI前端面试深度解析
专栏二:《Java体系也能玩转AI》24期加急中,适合java深度玩家
专栏三:《从0构建Agent系统》 15期加急中,适合所以玩家
让Java开发者不转Python也能构建企业级AI应用。
点赞+关注+评论 走一波。
-------------------------淘汰自己不是别人,是自己!!!------------------------------------
核心定位:前端侧如何驱动LLM的工具调用与知识检索增强
关键产出:Function Calling执行器 + RAG前端优化方案
1 Function Calling前端全流程解析
开篇:前端从"展示层"变成"执行层"
传统的前后端交互模型是:用户操作 → 前端发请求 → 后端处理 → 前端展示结果。前端只做"展示"。
Function Calling彻底改变了这个模型。当LLM决定调用一个工具时,前端成为了工具的实际执行者------它需要解析LLM的函数调用指令、校验参数、在沙箱中执行、将结果回传给LLM。
前端从"被动的展示层"变成了"主动的执行层",这是AI时代前端角色转变的核心标志。
一、Function Calling协议详解
1.1 完整流程
markdown
用户输入 → 前端组装请求(含tools定义) → LLM决策
↓
返回function_call
↓
前端解析+执行函数
↓
结果回传LLM → 最终回复
1.2 请求格式:tools定义
css
interface ChatCompletionRequest {
model: string;
messages: ChatMessage[];
tools?: ToolDefinition[];
tool_choice?: 'auto' | 'none' | { type: 'function'; function: { name: string } };
}
interface ToolDefinition {
type: 'function';
function: {
name: string;
description: string; // LLM靠这个描述来决定何时调用
parameters: { // JSON Schema格式
type: 'object';
properties: Record<string, PropertyDefinition>;
required?: string[];
};
};
}
interface PropertyDefinition {
type: string;
description: string;
enum?: string[]; // 枚举值,约束LLM输出
}
// 示例:定义一个天气查询工具
const weatherTool: ToolDefinition = {
type: 'function',
function: {
name: 'get_weather',
description: '获取指定城市的当前天气信息',
parameters: {
type: 'object',
properties: {
city: {
type: 'string',
description: '城市名称,如"北京"、"上海"',
},
unit: {
type: 'string',
description: '温度单位',
enum: ['celsius', 'fahrenheit'],
},
},
required: ['city'],
},
},
};
1.3 响应格式:function_call解析
css
// LLM决定调用工具时的响应
interface FunctionCallResponse {
id: 'chatcmpl-xxx';
choices: [{
index: 0;
message: {
role: 'assistant';
content: null; // content为null,表示不是文本回复
tool_calls: [{
id: 'call_abc123'; // 调用ID,回传时需要
type: 'function';
function: {
name: 'get_weather';
arguments: '{"city":"北京","unit":"celsius"}'; // JSON字符串!不是对象
},
}];
};
finish_reason: 'tool_calls'; // 表示因工具调用而停止
}];
}
关键细节 :arguments是一个JSON字符串 ,不是对象!前端必须先JSON.parse才能使用。这是最常见的解析bug来源。
二、Function Calling执行器设计
2.1 架构设计
scss
┌──────────────────────────────────────────────┐
│ FunctionCallingExecutor │
├──────────────────────────────────────────────┤
│ 工具注册层 │
│ ├─ registerTool(name, handler, schema) │
│ ├─ unregisterTool(name) │
│ └─ 工具元数据(描述、参数Schema、权限级别) │
├──────────────────────────────────────────────┤
│ 参数校验层 │
│ ├─ Schema校验(Ajv/Zod) │
│ ├─ 类型转换(字符串→数字等) │
│ └─ 安全过滤(XSS/注入检测) │
├──────────────────────────────────────────────┤
│ 执行层 │
│ ├─ 权限检查 │
│ ├─ 沙箱执行(可选) │
│ ├─ 超时控制 │
│ └─ 并发工具调用的并行执行 │
├──────────────────────────────────────────────┤
│ 结果处理层 │
│ ├─ 结果序列化(对象→JSON字符串) │
│ ├─ 错误捕获与格式化 │
│ └─ 结果回传消息构造 │
└──────────────────────────────────────────────┘
2.2 工具注册与权限分级
typescript
type ToolPermission = 'read' | 'write' | 'dangerous';
interface ToolRegistration {
name: string;
description: string;
parameters: Record<string, any>; // JSON Schema
handler: (args: any) => Promise<any>;
permission: ToolPermission;
timeout?: number; // 超时时间(ms)
maxRetries?: number;
}
class FunctionCallingExecutor {
private tools = new Map<string, ToolRegistration>();
private permissionLevel: ToolPermission = 'read'; // 当前允许的最高权限
/** 注册工具 */
registerTool(registration: ToolRegistration): void {
// 验证handler是异步函数
if (typeof registration.handler !== 'function') {
throw new Error(`工具 "${registration.name}" 的 handler 必须是函数`);
}
this.tools.set(registration.name, registration);
}
/** 批量注册工具 */
registerTools(registrations: ToolRegistration[]): void {
registrations.forEach(r => this.registerTool(r));
}
/** 取消注册 */
unregisterTool(name: string): void {
this.tools.delete(name);
}
/** 获取所有工具定义(用于发送给LLM) */
getToolDefinitions(): ToolDefinition[] {
return Array.from(this.tools.values())
.filter(tool => this.isPermissionAllowed(tool.permission))
.map(tool => ({
type: 'function' as const,
function: {
name: tool.name,
description: tool.description,
parameters: tool.parameters,
},
}));
}
private isPermissionAllowed(required: ToolPermission): boolean {
const levels: Record<ToolPermission, number> = {
read: 0,
write: 1,
dangerous: 2,
};
return levels[required] <= levels[this.permissionLevel];
}
/** 设置权限级别 */
setPermissionLevel(level: ToolPermission): void {
this.permissionLevel = level;
}
}
2.3 参数校验与安全过滤
typescript
import { z } from 'zod';
class FunctionCallingExecutor {
// ... 前面的代码
/** 执行工具调用 */
async execute(toolCall: {
id: string;
function: {
name: string;
arguments: string;
};
}): Promise<ToolCallResult> {
const { name, arguments: argsStr } = toolCall.function;
// 1. 查找工具
const tool = this.tools.get(name);
if (!tool) {
return {
tool_call_id: toolCall.id,
role: 'tool',
content: JSON.stringify({ error: `未知工具: ${name}` }),
};
}
// 2. 权限检查
if (!this.isPermissionAllowed(tool.permission)) {
return {
tool_call_id: toolCall.id,
role: 'tool',
content: JSON.stringify({ error: `权限不足: ${name} 需要 ${tool.permission} 权限` }),
};
}
// 3. 解析参数
let args: any;
try {
args = JSON.parse(argsStr);
} catch {
return {
tool_call_id: toolCall.id,
role: 'tool',
content: JSON.stringify({ error: `参数JSON解析失败: ${argsStr}` }),
};
}
// 4. 参数校验(使用Zod Schema)
if (tool.parameterSchema) {
const result = tool.parameterSchema.safeParse(args);
if (!result.success) {
return {
tool_call_id: toolCall.id,
role: 'tool',
content: JSON.stringify({
error: `参数校验失败: ${result.error.message}`,
}),
};
}
args = result.data; // 使用校验后的数据(可能经过类型转换)
}
// 5. 安全过滤
args = this.sanitizeArgs(args);
// 6. 执行(带超时控制)
try {
const result = await this.executeWithTimeout(
tool.handler(args),
tool.timeout ?? 10000
);
return {
tool_call_id: toolCall.id,
role: 'tool',
content: JSON.stringify(this.sanitizeResult(result)),
};
} catch (error: any) {
return {
tool_call_id: toolCall.id,
role: 'tool',
content: JSON.stringify({
error: error.message ?? '工具执行失败',
}),
};
}
}
/** 超时控制 */
private executeWithTimeout<T>(
promise: Promise<T>,
timeoutMs: number
): Promise<T> {
return new Promise((resolve, reject) => {
const timer = setTimeout(
() => reject(new Error(`工具执行超时 (${timeoutMs}ms)`)),
timeoutMs
);
promise
.then((result) => {
clearTimeout(timer);
resolve(result);
})
.catch((error) => {
clearTimeout(timer);
reject(error);
});
});
}
/** 安全过滤:防止XSS和注入 */
private sanitizeArgs(args: any): any {
if (typeof args === 'string') {
return this.sanitizeString(args);
}
if (typeof args === 'object' && args !== null) {
const sanitized: any = Array.isArray(args) ? [] : {};
for (const [key, value] of Object.entries(args)) {
sanitized[key] = this.sanitizeArgs(value);
}
return sanitized;
}
return args;
}
private sanitizeString(str: string): string {
return str
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '') // 移除script标签
.replace(/on\w+\s*=/gi, '') // 移除事件处理器
.replace(/javascript:/gi, ''); // 移除javascript:协议
}
private sanitizeResult(result: any): any {
// 防止将敏感信息返回给LLM
const resultStr = JSON.stringify(result);
// 检查是否包含可能的敏感信息模式
const sensitivePatterns = [
/password/i,
/secret/i,
/api[_-]?key/i,
/token/i,
/credential/i,
];
for (const pattern of sensitivePatterns) {
if (pattern.test(resultStr)) {
console.warn(`[FunctionCalling] 工具返回结果可能包含敏感信息,已过滤`);
return { data: '[结果已过滤:可能包含敏感信息]' };
}
}
return result;
}
}
interface ToolCallResult {
tool_call_id: string;
role: 'tool';
content: string;
}
三、并行工具调用的并发执行
LLM可能在一个响应中同时请求调用多个工具:
typescript
class FunctionCallingExecutor {
// ... 前面的代码
/** 处理一次LLM响应中可能包含的多个工具调用 */
async executeAll(
toolCalls: Array<{
id: string;
type: 'function';
function: { name: string; arguments: string };
}>
): Promise<ToolCallResult[]> {
// 并行执行所有工具调用
const results = await Promise.allSettled(
toolCalls.map(tc => this.execute(tc))
);
return results.map((result, index) => {
if (result.status === 'fulfilled') {
return result.value;
}
// 执行失败的兜底
return {
tool_call_id: toolCalls[index].id,
role: 'tool' as const,
content: JSON.stringify({
error: result.reason?.message ?? '执行失败',
}),
};
});
}
}
四、完整的多轮Function Calling流程
typescript
class AIFunctionCallingOrchestrator {
private executor: FunctionCallingExecutor;
private sseClient: SSEClient;
private maxToolCallRounds = 5; // 防止无限循环
constructor(executor: FunctionCallingExecutor, sseClient: SSEClient) {
this.executor = executor;
this.sseClient = sseClient;
}
/** 完整的对话流程(含Function Calling循环) */
async chat(messages: ChatMessage[]): Promise<string> {
let currentMessages = [...messages];
let round = 0;
while (round < this.maxToolCallRounds) {
round++;
// 1. 发送请求给LLM(含tools定义)
const response = await this.sendRequest(currentMessages);
// 2. 检查是否有工具调用
if (!response.tool_calls || response.tool_calls.length === 0) {
// 没有工具调用,返回文本回复
return response.content ?? '';
}
// 3. 将assistant的tool_calls消息加入历史
currentMessages.push({
role: 'assistant',
content: response.content,
tool_calls: response.tool_calls,
});
// 4. 执行所有工具调用
const toolResults = await this.executor.executeAll(response.tool_calls);
// 5. 将工具结果加入历史
for (const result of toolResults) {
currentMessages.push({
role: 'tool',
tool_call_id: result.tool_call_id,
content: result.content,
});
}
// 继续循环,让LLM基于工具结果生成最终回复
}
throw new Error(`Function Calling超过最大轮数 (${this.maxToolCallRounds})`);
}
private async sendRequest(messages: ChatMessage[]): Promise<any> {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'gpt-4',
messages,
tools: this.executor.getToolDefinitions(),
tool_choice: 'auto',
}),
});
const data = await response.json();
return data.choices[0].message;
}
}
五、前端可注册的典型工具
php
// 天气查询
executor.registerTool({
name: 'get_weather',
description: '获取指定城市的当前天气',
parameters: {
type: 'object',
properties: {
city: { type: 'string', description: '城市名称' },
},
required: ['city'],
},
parameterSchema: z.object({ city: z.string().min(1) }),
handler: async ({ city }) => {
const res = await fetch(`/api/weather?city=${encodeURIComponent(city)}`);
return res.json();
},
permission: 'read',
});
// 网页搜索
executor.registerTool({
name: 'web_search',
description: '在互联网上搜索信息',
parameters: {
type: 'object',
properties: {
query: { type: 'string', description: '搜索关键词' },
count: { type: 'number', description: '返回结果数量' },
},
required: ['query'],
},
parameterSchema: z.object({
query: z.string().min(1),
count: z.number().min(1).max(10).optional(),
}),
handler: async ({ query, count = 5 }) => {
const res = await fetch('/api/search', {
method: 'POST',
body: JSON.stringify({ query, count }),
});
return res.json();
},
permission: 'read',
});
// 代码执行(危险操作,需要高权限)
executor.registerTool({
name: 'execute_code',
description: '在沙箱中执行JavaScript代码',
parameters: {
type: 'object',
properties: {
code: { type: 'string', description: '要执行的JavaScript代码' },
},
required: ['code'],
},
parameterSchema: z.object({
code: z.string().max(10000), // 限制代码长度
}),
handler: async ({ code }) => {
// 在WebWorker沙箱中执行
return executeInSandbox(code);
},
permission: 'dangerous',
timeout: 5000,
});
实践任务
任务:实现一个FunctionCallingExecutor,支持工具注册、权限分级、并发执行、结果回传。
验收标准:
- 工具可动态注册/卸载
- 参数校验通过Zod Schema
- 权限分级:read/write/dangerous
- 多工具并行执行(Promise.allSettled)
- 超时控制与错误兜底
- 敏感信息过滤
面试题解析
Q:前端如何处理LLM的Function Calling流程?
答题要点:
- 协议理解:LLM返回的tool_calls中arguments是JSON字符串,需要解析
- 执行流程:查找工具 → 权限检查 → 参数校验 → 安全过滤 → 执行 → 结果回传
- 并发处理:多个tool_calls并行执行,用Promise.allSettled容忍部分失败
- 多轮循环:工具结果回传后LLM可能再次调用工具,需要循环直到LLM给出文本回复
- 安全防线:参数校验、XSS过滤、敏感信息检测、超时控制、最大轮数限制
6.2 RAG前端优化实战
开篇:RAG不只是后端的事
RAG(Retrieval-Augmented Generation)的核心流程是:用户查询 → 检索相关文档 → 将文档作为上下文给LLM → 生成回答。
大多数人认为检索、向量计算都在后端完成,前端只是展示结果。但实际上,前端在RAG链路中有4个独特的优化点------这些优化点后端做不了,因为它们依赖前端的用户交互数据。
一、RAG链路中的前端优化点
css
用户输入查询 → [1.查询改写] → 向量检索 → [2.结果缓存] → 排序
↓ ↓
用户反馈 ← [4.纠错信号采集] ← 最终回答 ← [3.结果可视化]
- [1] 查询改写:前端基于用户交互上下文改写查询
- [2] 结果缓存:前端缓存高频查询的向量检索结果
- [3] 结果可视化:前端展示检索命中的chunk,帮助用户理解
- [4] 纠错信号采集:前端收集用户对检索结果的反馈
二、查询改写:前端侧的用户意图扩展
用户输入的查询通常是简短、模糊的,直接用于向量检索效果不佳。前端可以基于对话上下文进行改写:
typescript
class QueryRewriter {
private conversationContext: ChatMessage[];
constructor(context: ChatMessage[]) {
this.conversationContext = context;
}
/** 改写查询 */
rewrite(query: string): string {
let rewritten = query;
// 策略1:指代消解------将代词替换为具体实体
rewritten = this.resolveReferences(rewritten);
// 策略2:上下文扩展------基于对话历史补充隐含信息
rewritten = this.expandWithContext(rewritten);
// 策略3:关键词增强------提取核心概念并添加同义词
rewritten = this.enhanceKeywords(rewritten);
return rewritten;
}
private resolveReferences(query: string): string {
// 简单的指代消解
const recentMessages = this.conversationContext.slice(-5);
// 查找最近的实体(简化实现)
const entityPatterns = [
/React/gi, /Vue/gi, /TypeScript/gi, /JavaScript/gi,
/CSS/gi, /Node\.js/gi, /Python/gi,
];
const entities: string[] = [];
for (const msg of recentMessages) {
for (const pattern of entityPatterns) {
const matches = msg.content.match(pattern);
if (matches) entities.push(...matches);
}
}
// 替换代词
const lastEntity = entities[entities.length - 1];
if (lastEntity) {
query = query
.replace(/它/g, lastEntity)
.replace(/这个/g, lastEntity)
.replace(/那个/g, lastEntity);
}
return query;
}
private expandWithContext(query: string): string {
// 如果查询很短,基于上下文扩展
if (query.length < 10) {
const lastUserMsg = this.conversationContext
.filter(m => m.role === 'user')
.pop()?.content;
if (lastUserMsg) {
return `${query} (上下文: ${lastUserMsg.slice(0, 100)})`;
}
}
return query;
}
private enhanceKeywords(query: string): string {
// 添加技术同义词
const synonymMap: Record<string, string[]> = {
'前端': ['frontend', 'web', '浏览器'],
'组件': ['component', '模块', 'widget'],
'状态管理': ['state management', 'store', 'flux'],
'渲染': ['render', 'paint', '绘制'],
};
let enhanced = query;
for (const [key, synonyms] of Object.entries(synonymMap)) {
if (query.includes(key)) {
enhanced += ` (${synonyms.join(' ')})`;
}
}
return enhanced;
}
}
进阶方案:对于复杂的查询改写,可以调用一个小型LLM(如GPT-3.5)在前端侧做改写,成本远低于主模型的调用。
三、向量检索缓存:IndexedDB存储高频查询结果
ini
interface CachedRetrievalResult {
query: string;
queryEmbedding?: number[]; // 缓存embedding避免重复计算
documents: RetrievedDocument[];
timestamp: number;
hitCount: number; // 缓存命中次数
}
interface RetrievedDocument {
id: string;
content: string;
score: number; // 相似度分数
metadata: {
source: string;
chunkIndex: number;
};
}
class RAGFrontendCache {
private dbName = 'rag-cache';
private storeName = 'retrievals';
private db: IDBDatabase | null = null;
async init(): Promise<void> {
this.db = await this.openDB();
}
/** 查找缓存 */
async get(query: string): Promise<CachedRetrievalResult | null> {
const tx = this.db!.transaction(this.storeName, 'readonly');
const store = tx.objectStore(this.storeName);
const result = await store.get(query);
if (!result) return null;
// 检查缓存是否过期(默认1小时)
if (Date.now() - result.timestamp > 3600000) {
await this.delete(query);
return null;
}
// 更新命中次数
result.hitCount++;
const writeTx = this.db!.transaction(this.storeName, 'readwrite');
writeTx.objectStore(this.storeName).put(result);
return result;
}
/** 存储缓存 */
async set(result: CachedRetrievalResult): Promise<void> {
const tx = this.db!.transaction(this.storeName, 'readwrite');
tx.objectStore(this.storeName).put({
...result,
timestamp: Date.now(),
hitCount: 0,
});
}
/** 删除缓存 */
async delete(query: string): Promise<void> {
const tx = this.db!.transaction(this.storeName, 'readwrite');
tx.objectStore(this.storeName).delete(query);
}
/** 清理过期缓存 */
async cleanup(maxAge: number = 3600000): Promise<number> {
const tx = this.db!.transaction(this.storeName, 'readwrite');
const store = tx.objectStore(this.storeName);
const request = store.openCursor();
let deletedCount = 0;
return new Promise((resolve) => {
request.onsuccess = (event) => {
const cursor = (event.target as IDBRequest).result;
if (cursor) {
if (Date.now() - cursor.value.timestamp > maxAge) {
cursor.delete();
deletedCount++;
}
cursor.continue();
} else {
resolve(deletedCount);
}
};
});
}
private openDB(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1);
request.onupgradeneeded = () => {
const db = request.result;
if (!db.objectStoreNames.contains(this.storeName)) {
const store = db.createObjectStore(this.storeName, { keyPath: 'query' });
store.createIndex('timestamp', 'timestamp');
}
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
}
使用示例:
typescript
class RAGFrontendOptimizer {
private cache: RAGFrontendCache;
async retrieve(query: string): Promise<RetrievedDocument[]> {
// 1. 查询改写
const rewrittenQuery = this.rewriter.rewrite(query);
// 2. 检查缓存
const cached = await this.cache.get(rewrittenQuery);
if (cached) {
console.log(`[RAG Cache] 命中缓存: ${query} → ${cached.documents.length} 条结果`);
return cached.documents;
}
// 3. 调用后端检索API
const documents = await this.fetchFromBackend(rewrittenQuery);
// 4. 存入缓存
await this.cache.set({
query: rewrittenQuery,
documents,
timestamp: Date.now(),
hitCount: 0,
});
return documents;
}
}
四、文档切片可视化
当RAG检索返回命中的文档chunk时,前端应该清晰地展示这些信息,帮助用户理解AI回答的依据:
ini
interface ChunkHighlight {
documentId: string;
chunkIndex: number;
content: string;
score: number; // 相似度分数
isHighlighted: boolean; // 是否在回答中被引用
}
class RAGVisualizationPanel {
private container: HTMLElement;
constructor(container: HTMLElement) {
this.container = container;
}
/** 渲染检索结果面板 */
render(chunks: ChunkHighlight[]): void {
this.container.innerHTML = '';
const header = document.createElement('div');
header.className = 'rag-header';
header.textContent = `参考文档 (${chunks.length} 条)`;
this.container.appendChild(header);
for (const chunk of chunks) {
const item = this.createChunkItem(chunk);
this.container.appendChild(item);
}
}
private createChunkItem(chunk: ChunkHighlight): HTMLElement {
const item = document.createElement('div');
item.className = `rag-chunk ${chunk.isHighlighted ? 'highlighted' : ''}`;
item.innerHTML = `
<div class="rag-chunk-header">
<span class="rag-source">${chunk.documentId}</span>
<span class="rag-score">相似度: ${(chunk.score * 100).toFixed(1)}%</span>
</div>
<div class="rag-chunk-content">${this.truncateContent(chunk.content, 200)}</div>
`;
// 点击展开完整内容
item.addEventListener('click', () => {
this.expandChunk(chunk);
});
return item;
}
private truncateContent(content: string, maxLength: number): string {
if (content.length <= maxLength) return content;
return content.slice(0, maxLength) + '...';
}
private expandChunk(chunk: ChunkHighlight): void {
// 展开显示完整chunk内容
// 同时在AI回答中高亮引用该chunk的段落
}
}
五、用户反馈驱动的Rerank信号采集
typescript
interface RAGFeedback {
query: string;
chunkId: string;
documentId: string;
feedbackType: 'relevant' | 'irrelevant' | 'partially_relevant';
userAction: 'clicked' | 'ignored' | 'expanded' | 'copied';
timestamp: number;
}
class RAGFeedbackCollector {
private feedbacks: RAGFeedback[] = [];
/** 记录用户对检索结果的反馈 */
recordFeedback(feedback: RAGFeedback): void {
this.feedbacks.push(feedback);
// 实时上传到后端,用于优化检索模型
this.uploadFeedback(feedback);
}
/** 监听用户交互行为 */
observeChunkInteractions(container: HTMLElement): void {
// 使用事件委托监听chunk的交互
container.addEventListener('click', (e) => {
const target = (e.target as HTMLElement).closest('.rag-chunk');
if (!target) return;
const chunkId = target.getAttribute('data-chunk-id');
const documentId = target.getAttribute('data-document-id');
if (chunkId && documentId) {
this.recordFeedback({
query: this.currentQuery,
chunkId,
documentId,
feedbackType: 'relevant', // 点击视为相关
userAction: 'clicked',
timestamp: Date.now(),
});
}
});
}
private currentQuery = '';
setCurrentQuery(query: string): void {
this.currentQuery = query;
}
private async uploadFeedback(feedback: RAGFeedback): Promise<void> {
try {
await fetch('/api/rag/feedback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(feedback),
});
} catch (error) {
// 上传失败不影响用户体验,静默处理
console.warn('[RAG Feedback] 上传失败:', error);
}
}
}
六、RAG前端优化完整模块
typescript
class RAGFrontendEnhancer {
private rewriter: QueryRewriter;
private cache: RAGFrontendCache;
private visualizer: RAGVisualizationPanel;
private feedbackCollector: RAGFeedbackCollector;
async query(rawQuery: string): Promise<{
answer: string;
sources: ChunkHighlight[];
}> {
// 1. 查询改写
const query = this.rewriter.rewrite(rawQuery);
this.feedbackCollector.setCurrentQuery(rawQuery);
// 2. 检查缓存
const cached = await this.cache.get(query);
if (cached) {
this.visualizer.render(cached.documents.map(d => ({
documentId: d.metadata.source,
chunkIndex: d.metadata.chunkIndex,
content: d.content,
score: d.score,
isHighlighted: false,
})));
// 3. 缓存命中,直接用检索结果调用LLM
return this.generateAnswer(rawQuery, cached.documents);
}
// 4. 缓存未命中,调用后端检索
const documents = await this.fetchRetrieval(query);
// 5. 缓存结果
await this.cache.set({ query, documents, timestamp: Date.now(), hitCount: 0 });
// 6. 可视化展示
this.visualizer.render(documents.map(d => ({
documentId: d.metadata.source,
chunkIndex: d.metadata.chunkIndex,
content: d.content,
score: d.score,
isHighlighted: false,
})));
// 7. 生成回答
return this.generateAnswer(rawQuery, documents);
}
private async fetchRetrieval(query: string): Promise<RetrievedDocument[]> {
const res = await fetch('/api/rag/retrieve', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, topK: 5 }),
});
return res.json();
}
private async generateAnswer(
query: string,
documents: RetrievedDocument[]
): Promise<{ answer: string; sources: ChunkHighlight[] }> {
// 调用LLM基于检索结果生成回答
// ... 实现略
return { answer: '', sources: [] };
}
}
实践任务
任务:实现RAG前端增强模块:查询改写器 + 检索结果可视化面板 + 用户反馈采集器。
验收标准:
- 查询改写:指代消解、上下文扩展、关键词增强
- IndexedDB缓存:高频查询命中缓存,1小时过期
- 可视化面板:展示检索命中的chunk,含来源、相似度、内容预览
- 反馈采集:用户点击/忽略/展开chunk时记录反馈
- 反馈上传到后端API
面试题解析
Q:RAG在前端侧可以做哪些优化?
答题要点:
- 查询改写:前端基于对话上下文做指代消解、意图扩展、关键词增强,让检索更精准
- 向量检索缓存:IndexedDB缓存高频查询的embedding和检索结果,减少网络请求
- 结果可视化:展示检索命中的chunk来源和相似度,帮助用户理解AI回答的依据
- 反馈驱动的Rerank:采集用户对检索结果的点击/忽略行为,作为排序优化的信号
核心洞察:前端是唯一能同时触达"用户查询"和"用户反馈"的层,这种双向信息优势是后端不具备的。
下期预告:前端AI工程化(七):AI应用安全攻防,我们将从功能层进入安全层,拆解Prompt Injection的攻击向量与前端防御体系。