前端AI工程化(六):Function Calling与RAG前端实践

👤 关于作者

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,支持工具注册、权限分级、并发执行、结果回传。

验收标准

  1. 工具可动态注册/卸载
  2. 参数校验通过Zod Schema
  3. 权限分级:read/write/dangerous
  4. 多工具并行执行(Promise.allSettled)
  5. 超时控制与错误兜底
  6. 敏感信息过滤

面试题解析

Q:前端如何处理LLM的Function Calling流程?

答题要点

  1. 协议理解:LLM返回的tool_calls中arguments是JSON字符串,需要解析
  2. 执行流程:查找工具 → 权限检查 → 参数校验 → 安全过滤 → 执行 → 结果回传
  3. 并发处理:多个tool_calls并行执行,用Promise.allSettled容忍部分失败
  4. 多轮循环:工具结果回传后LLM可能再次调用工具,需要循环直到LLM给出文本回复
  5. 安全防线:参数校验、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前端增强模块:查询改写器 + 检索结果可视化面板 + 用户反馈采集器。

验收标准

  1. 查询改写:指代消解、上下文扩展、关键词增强
  2. IndexedDB缓存:高频查询命中缓存,1小时过期
  3. 可视化面板:展示检索命中的chunk,含来源、相似度、内容预览
  4. 反馈采集:用户点击/忽略/展开chunk时记录反馈
  5. 反馈上传到后端API

面试题解析

Q:RAG在前端侧可以做哪些优化?

答题要点

  1. 查询改写:前端基于对话上下文做指代消解、意图扩展、关键词增强,让检索更精准
  2. 向量检索缓存:IndexedDB缓存高频查询的embedding和检索结果,减少网络请求
  3. 结果可视化:展示检索命中的chunk来源和相似度,帮助用户理解AI回答的依据
  4. 反馈驱动的Rerank:采集用户对检索结果的点击/忽略行为,作为排序优化的信号

核心洞察:前端是唯一能同时触达"用户查询"和"用户反馈"的层,这种双向信息优势是后端不具备的。

下期预告:前端AI工程化(七):AI应用安全攻防,我们将从功能层进入安全层,拆解Prompt Injection的攻击向量与前端防御体系。

相关推荐
用户11481867894841 小时前
Vue 开发者快速上手 Flutter(一)
前端
ZhengEnCi1 小时前
08-编码器结构 🏗️
人工智能
掘金安东尼1 小时前
Buildsom |老板说要加码 AI 推广?我调研后发现:77% 的品牌,其实都在“盲投”
人工智能
Android出海1 小时前
5月合规风暴眼:Google Play权限大限与欧盟游戏监管新棋局
人工智能·游戏·google play·谷歌开发者·android开发者·google开发者·google play开发者
在繁华处1 小时前
轻棋局(一):项目总览与架构设计
人工智能·windows
鹏多多2 小时前
Trae cn里使用Pencil来制作设计图的手把手教程
前端·ai编程·trae
客场消音器2 小时前
如何使用codex进行UI重构,让AI开发的前端页面不再千篇一律
前端·后端·微信小程序
TechubNews2 小时前
稳定币下一战:不是谁发币,而是谁掌握结算通道
人工智能·web3·区块链
火山引擎开发者社区2 小时前
钛投标基于火山引擎 ArkClaw 构建招投标垂直智能服务生态
人工智能