桌面AI助手本地历史记录功能实现:Electron+SQLite全栈实战

最近在开发桌面端AI助手应用时,发现一个普遍痛点:用户与AI的对话历史散落在各个会话窗口,一旦关闭应用或清理缓存,那些有价值的灵感、代码片段和解决方案就再也找不回来了。对于开发者而言,这些历史记录不仅是工作日志,更是重要的知识资产。因此,为桌面应用集成一个可靠、可检索的本地历史记录功能,成为了提升用户体验和实用性的关键一步。

本文将围绕如何为类似ChatGPT的桌面应用设计和实现一个“计算机历史记录”功能展开。我们将从核心概念、技术选型讲起,逐步深入到完整的代码实战,涵盖数据存储、检索、界面展示以及隐私安全等全流程。无论你是使用Electron、Tauri还是Flutter进行桌面开发,都能从本文中找到可复用的思路和代码模块。

1. 功能背景与核心价值

“计算机历史记录”功能,本质上是一个本地化的、结构化的对话日志系统。它不同于云端同步的聊天记录,其核心价值在于:

  • 数据主权与隐私 :所有对话历史完全存储在用户本地计算机上,无需担心隐私数据上传到第三方服务器,符合企业对敏感信息管控和个人对隐私保护的需求。
  • 离线可用性 :即使在没有网络连接的情况下,用户依然可以查看、搜索过往的所有对话,保证了核心功能的可用性。
  • 高性能检索 :本地数据库(如SQLite)或索引文件(如SQLite FTS)可以提供毫秒级的全文搜索,帮助用户快速从海量对话中定位到某一行代码、一个错误信息或一个特定概念的解释。
  • 降低依赖与成本 :不依赖OpenAI或其他服务商的对话历史接口,避免了因API变更、服务不稳定或历史记录长度限制带来的功能缺失,也节省了可能的云端存储成本。

对于开发者而言,实现此功能意味着需要处理几个关键问题: 存储什么数据?用什么技术存储?如何高效检索?以及如何设计用户界面进行交互? 接下来,我们将逐一拆解。

2. 技术选型与环境准备

实现本地历史记录,技术栈的选择取决于你的桌面应用框架。

2.1 桌面应用框架与对应方案

  1. Electron (基于Node.js)

    • 优势 :Node.js生态丰富,选择最多。
    • 存储方案
      • SQLite :轻量级关系型数据库,可靠性高,支持复杂查询和全文搜索(FTS)。推荐使用 better-sqlite3 sqlite3 模块。
      • Lowdb/NeDB :基于文件的NoSQL数据库,API简单,类似MongoDB,适合JSON格式的对话记录。
      • PouchDB :一个在浏览器中运行的CouchDB,支持离线同步,但稍显重量级。
    • 序列化 :直接使用Node.js的 fs 模块读写JSON文件是最简单的方式,但缺乏检索能力,适合记录量小的场景。
  2. Tauri (基于Rust + 前端框架)

    • 优势 :应用体积小,性能好,安全性高。
    • 存储方案 :Tauri提供了强大的 tauri-plugin-sql 插件,可以方便地使用SQLite。你也可以通过Tauri的Command与Rust后端交互,使用Rust的 rusqlite sqlx 库来操作数据库,获得最佳性能和类型安全。
  3. Flutter (桌面端)

    • 优势 :一套代码多端运行,Dart语言。
    • 存储方案
      • sqflite :Flutter中流行的SQLite插件,功能完善。
      • Hive :一个轻量级、极速的键值数据库,纯Dart实现,对于非关系型存储非常友好。
      • Isar :Hive作者开发的更强大的、支持索引和查询的本地数据库。

本文将以最经典的 Electron + SQLite ( better-sqlite3 ) 组合作为主要示例进行讲解 ,因为其技术栈通用性强,原理易于迁移到其他框架。Flutter (sqflite) 和 Tauri (tauri-plugin-sql) 的核心SQL逻辑是相通的。

2.2 开发环境与依赖安装

假设你已有一个基本的Electron应用项目。我们需要安装必要的依赖。

# 在你的Electron项目根目录下执行
npm install better-sqlite3
# 或者如果你使用TypeScript
npm install better-sqlite3 @types/better-sqlite3

better-sqlite3 是一个同步SQLite驱动,在Electron的主进程(Main Process)中使用非常合适,因为它避免了异步回调的复杂性,且性能出色。渲染进程(Renderer Process)如需访问,需通过IPC(进程间通信)与主进程交互。

3. 数据库设计与核心操作

3.1 数据表设计

我们需要存储每次对话的元信息以及具体的消息内容。设计两张表是清晰的做法:

  1. conversations (会话表) :记录每一次独立的对话会话。
  2. messages (消息表) :记录每个会话中的每一条消息。

以下是SQL建表语句:

-- 文件:src/database/schema.sql
-- 会话表
CREATE TABLE IF NOT EXISTS conversations (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL DEFAULT '新对话', -- 可自动生成或用户修改
    model_used TEXT, -- 使用的模型,如 ‘gpt-3.5-turbo‘
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- 消息表
CREATE TABLE IF NOT EXISTS messages (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    conversation_id INTEGER NOT NULL,
    role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system')), -- 消息角色
    content TEXT NOT NULL, -- 消息内容
    tokens INTEGER, -- 消耗的token数,可选
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
);

-- 为消息内容创建全文搜索虚拟表(FTS5),大幅提升搜索效率
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
    content,
    content=messages -- 指定源表
);

设计说明

  • ON DELETE CASCADE :当删除一个会话时,其下的所有消息自动删除,保持数据一致性。
  • FTS5 :SQLite的全文搜索扩展。我们创建了一个虚拟表 messages_fts 来索引 messages.content 字段。这样,当用户搜索“Python 递归错误”时,可以快速找到所有包含这些关键词的消息。
  • role 字段:标记消息是用户发送的、AI回复的还是系统指令,便于还原对话上下文。

3.2 初始化数据库与基础操作类

我们在Electron主进程中创建一个数据库服务模块。

// 文件:src/main/database.js
const Database = require('better-sqlite3');
const path = require('path');
const { app } = require('electron');

class HistoryDatabase {
  constructor() {
    // 将数据库文件存储在用户数据目录下
    const userDataPath = app.getPath('userData');
    this.dbPath = path.join(userDataPath, 'chatgpt-desktop-history.db');
    this.db = new Database(this.dbPath);

    // 启用外键约束和WAL模式(提升性能)
    this.db.pragma('foreign_keys = ON');
    this.db.pragma('journal_mode = WAL');

    this.initSchema();
  }

  initSchema() {
    // 执行上面定义的建表SQL
    const schemaSQL = `
      CREATE TABLE IF NOT EXISTS conversations (...);
      CREATE TABLE IF NOT EXISTS messages (...);
      CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(...);
    `; // 此处省略完整SQL,实际应用应从文件读取或完整写入
    this.db.exec(schemaSQL);
  }

  // 1. 创建新会话
  createConversation(title = '新对话', modelUsed = null) {
    const stmt = this.db.prepare(
      'INSERT INTO conversations (title, model_used) VALUES (?, ?)'
    );
    const info = stmt.run(title, modelUsed);
    return info.lastInsertRowid; // 返回新会话的ID
  }

  // 2. 向指定会话插入一条消息
  addMessage(conversationId, role, content, tokens = null) {
    const stmt = this.db.prepare(
      'INSERT INTO messages (conversation_id, role, content, tokens) VALUES (?, ?, ?, ?)'
    );
    const info = stmt.run(conversationId, role, content, tokens);
    const messageId = info.lastInsertRowid;

    // 同时向FTS表插入索引数据
    if (content && content.trim().length > 0) {
      const ftsStmt = this.db.prepare('INSERT INTO messages_fts (rowid, content) VALUES (?, ?)');
      ftsStmt.run(messageId, content);
    }

    // 更新会话的更新时间
    this.db.prepare('UPDATE conversations SET updated_at = CURRENT_TIMESTAMP WHERE id = ?')
          .run(conversationId);

    return messageId;
  }

  // 3. 获取所有会话列表(按更新时间倒序)
  getAllConversations(limit = 50, offset = 0) {
    const stmt = this.db.prepare(
      'SELECT id, title, model_used, created_at, updated_at FROM conversations ORDER BY updated_at DESC LIMIT ? OFFSET ?'
    );
    return stmt.all(limit, offset);
  }

  // 4. 获取某个会话的所有消息
  getMessagesByConversationId(conversationId) {
    const stmt = this.db.prepare(
      'SELECT id, role, content, tokens, created_at FROM messages WHERE conversation_id = ? ORDER BY created_at ASC'
    );
    return stmt.all(conversationId);
  }

  // 5. 全文搜索消息内容
  searchMessages(keyword, limit = 20) {
    // FTS5 使用 MATCH 进行搜索
    const stmt = this.db.prepare(`
      SELECT m.id, m.conversation_id, m.role, m.content, m.created_at, c.title as conversation_title
      FROM messages_fts fts
      JOIN messages m ON fts.rowid = m.id
      JOIN conversations c ON m.conversation_id = c.id
      WHERE fts.content MATCH ?
      ORDER BY rank
      LIMIT ?
    `);
    // 注意:MATCH 查询的语法,简单关键词直接使用,复杂查询需构造查询字符串
    const searchQuery = `"${keyword}"*`; // 支持前缀匹配,例如搜索“Pyth”可以匹配“Python”
    return stmt.all(searchQuery, limit);
  }

  // 6. 删除会话(及其关联消息,CASCADE会处理)
  deleteConversation(conversationId) {
    // 首先需要从FTS表中删除相关索引(因为外键CASCADE不作用于虚拟表)
    const msgIdsStmt = this.db.prepare('SELECT id FROM messages WHERE conversation_id = ?');
    const messageIds = msgIdsStmt.all(conversationId).map(row => row.id);
    if (messageIds.length > 0) {
      const placeholders = messageIds.map(() => '?').join(',');
      this.db.prepare(`DELETE FROM messages_fts WHERE rowid IN (${placeholders})`).run(...messageIds);
    }

    // 然后删除会话(消息会自动删除)
    const stmt = this.db.prepare('DELETE FROM conversations WHERE id = ?');
    return stmt.run(conversationId).changes > 0;
  }

  // 7. 更新会话标题
  updateConversationTitle(conversationId, newTitle) {
    const stmt = this.db.prepare('UPDATE conversations SET title = ? WHERE id = ?');
    return stmt.run(newTitle, conversationId).changes > 0;
  }

  close() {
    this.db.close();
  }
}

// 导出单例实例
module.exports = new HistoryDatabase();

4. 前端界面与交互实现

数据库层准备好后,我们需要在渲染进程(前端页面)中创建用户界面来展示和操作历史记录。

4.1 进程间通信 (IPC) 封装

前端不能直接访问主进程的数据库模块,需要通过IPC调用。我们在主进程和渲染进程中分别设置IPC处理器。

// 文件:src/main/ipcHandlers.js (主进程)
const { ipcMain } = require('electron');
const db = require('./database'); // 导入上面的数据库实例

function setupIpcHandlers() {
  // 获取会话列表
  ipcMain.handle('history:get-conversations', async (event, ...args) => {
    const [limit, offset] = args;
    return db.getAllConversations(limit, offset);
  });

  // 获取特定会话消息
  ipcMain.handle('history:get-messages', async (event, conversationId) => {
    return db.getMessagesByConversationId(conversationId);
  });

  // 搜索消息
  ipcMain.handle('history:search-messages', async (event, keyword, limit) => {
    return db.searchMessages(keyword, limit);
  });

  // 删除会话
  ipcMain.handle('history:delete-conversation', async (event, conversationId) => {
    return db.deleteConversation(conversationId);
  });

  // 更新会话标题
  ipcMain.handle('history:update-title', async (event, conversationId, newTitle) => {
    return db.updateConversationTitle(conversationId, newTitle);
  });
}

module.exports = setupIpcHandlers;

在渲染进程(如React/Vue组件)中,我们封装一个服务类来调用这些IPC接口。

// 文件:src/renderer/services/historyService.js
const { ipcRenderer } = window.require('electron');

export const historyService = {
  async getConversations(limit = 50, offset = 0) {
    return await ipcRenderer.invoke('history:get-conversations', limit, offset);
  },
  async getMessages(conversationId) {
    return await ipcRenderer.invoke('history:get-messages', conversationId);
  },
  async searchMessages(keyword, limit = 20) {
    return await ipcRenderer.invoke('history:search-messages', keyword, limit);
  },
  async deleteConversation(conversationId) {
    return await ipcRenderer.invoke('history:delete-conversation', conversationId);
  },
  async updateConversationTitle(conversationId, newTitle) {
    return await ipcRenderer.invoke('history:update-title', conversationId, newTitle);
  },
};

4.2 React 组件示例:历史记录侧边栏

下面是一个使用React和Ant Design组件库的简单侧边栏实现。

// 文件:src/renderer/components/HistorySidebar.jsx
import React, { useState, useEffect } from 'react';
import { List, Input, Button, Modal, message, Typography } from 'antd';
import { DeleteOutlined, EditOutlined, SearchOutlined } from '@ant-design/icons';
import { historyService } from '../services/historyService';
import './HistorySidebar.css';

const { Text } = Typography;
const { Search } = Input;

const HistorySidebar = ({ onSelectConversation, currentConversationId }) => {
  const [conversations, setConversations] = useState([]);
  const [searchResults, setSearchResults] = useState([]);
  const [searchMode, setSearchMode] = useState(false);
  const [loading, setLoading] = useState(false);

  // 加载会话列表
  const loadConversations = async () => {
    setLoading(true);
    try {
      const data = await historyService.getConversations();
      setConversations(data);
      setSearchMode(false);
    } catch (error) {
      message.error('加载历史记录失败: ' + error.message);
    } finally {
      setLoading(false);
    }
  };

  // 搜索消息
  const handleSearch = async (value) => {
    if (!value.trim()) {
      setSearchMode(false);
      loadConversations();
      return;
    }
    setLoading(true);
    try {
      const results = await historyService.searchMessages(value);
      setSearchResults(results);
      setSearchMode(true);
    } catch (error) {
      message.error('搜索失败: ' + error.message);
    } finally {
      setLoading(false);
    }
  };

  // 删除会话确认
  const confirmDelete = (conversationId, title, e) => {
    e.stopPropagation(); // 防止触发列表项点击事件
    Modal.confirm({
      title: '确认删除',
      content: `确定要删除对话 "${title}" 吗?此操作不可恢复。`,
      okText: '删除',
      okType: 'danger',
      cancelText: '取消',
      onOk: async () => {
        try {
          const success = await historyService.deleteConversation(conversationId);
          if (success) {
            message.success('删除成功');
            loadConversations(); // 刷新列表
          }
        } catch (error) {
          message.error('删除失败: ' + error.message);
        }
      },
    });
  };

  // 编辑会话标题
  const handleEditTitle = async (conversationId, oldTitle, e) => {
    e.stopPropagation();
    Modal.confirm({
      title: '修改对话标题',
      content: (
        <Input
          defaultValue={oldTitle}
          onPressEnter={(e) => {
            Modal.destroyAll(); // 关闭所有弹窗
            updateTitle(conversationId, e.target.value);
          }}
          autoFocus
        />
      ),
      onOk: (e) => {
        const input = e.input;
        updateTitle(conversationId, input?.value || oldTitle);
      },
    });
  };

  const updateTitle = async (id, newTitle) => {
    if (!newTitle.trim()) return;
    try {
      const success = await historyService.updateConversationTitle(id, newTitle.trim());
      if (success) {
        message.success('标题已更新');
        loadConversations();
      }
    } catch (error) {
      message.error('更新失败: ' + error.message);
    }
  };

  useEffect(() => {
    loadConversations();
  }, []);

  const dataSource = searchMode ? searchResults : conversations;

  return (
    <div className="history-sidebar">
      <div className="sidebar-header">
        <h3>对话历史</h3>
        <Search
          placeholder="搜索对话内容..."
          allowClear
          enterButton={<SearchOutlined />}
          onSearch={handleSearch}
          style={{ marginBottom: 16 }}
        />
        <Button type="link" onClick={loadConversations} disabled={loading}>
          刷新列表
        </Button>
      </div>

      <List
        loading={loading}
        dataSource={dataSource}
        renderItem={(item) => {
          const isCurrent = item.id === currentConversationId;
          const title = searchMode ? `[搜索] ${item.conversation_title}` : item.title;
          return (
            <List.Item
              className={`history-item ${isCurrent ? 'active' : ''}`}
              onClick={() => onSelectConversation(item.id)}
              actions={[
                <EditOutlined
                  key="edit"
                  onClick={(e) => handleEditTitle(item.id, item.title, e)}
                  title="编辑标题"
                />,
                <DeleteOutlined
                  key="delete"
                  onClick={(e) => confirmDelete(item.id, item.title, e)}
                  title="删除对话"
                />,
              ]}
            >
              <List.Item.Meta
                title={<Text ellipsis>{title}</Text>}
                description={
                  <>
                    <div>模型: {item.model_used || 'N/A'}</div>
                    <div>
                      {new Date(item.updated_at).toLocaleDateString()} {new Date(item.updated_at).toLocaleTimeString()}
                    </div>
                    {searchMode && (
                      <div style={{ marginTop: 4, fontSize: '12px', color: '#666' }}>
                        <Text type="secondary" ellipsis>
                          匹配内容: {item.content.substring(0, 80)}...
                        </Text>
                      </div>
                    )}
                  </>
                }
              />
            </List.Item>
          );
        }}
      />
    </div>
  );
};

export default HistorySidebar;
/* 文件:src/renderer/components/HistorySidebar.css */
.history-sidebar {
  width: 320px;
  height: 100vh;
  border-right: 1px solid #f0f0f0;
  display: flex;
  flex-direction: column;
  background: #fff;
}

.sidebar-header {
  padding: 16px;
  border-bottom: 1px solid #f0f0f0;
}

.history-item {
  cursor: pointer;
  padding: 12px 16px;
  border-bottom: 1px solid #fafafa;
  transition: background-color 0.3s;
}

.history-item:hover {
  background-color: #f5f5f5;
}

.history-item.active {
  background-color: #e6f7ff;
  border-left: 3px solid #1890ff;
}

4.3 集成到主应用

最后,在你的主应用组件中集成这个侧边栏,并在用户发送/接收消息时调用数据库的 addMessage 方法(通过IPC)。

// 文件:src/renderer/App.jsx (简化示例)
import React, { useState } from 'react';
import HistorySidebar from './components/HistorySidebar';
import ChatWindow from './components/ChatWindow'; // 你的主聊天窗口组件
import { historyService } from './services/historyService';
import { ipcRenderer } from 'electron';

function App() {
  const [currentConversationId, setCurrentConversationId] = useState(null);

  // 初始化或选择新会话
  const handleSelectConversation = (conversationId) => {
    setCurrentConversationId(conversationId);
    // 可以在这里触发加载该会话的历史消息到ChatWindow
  };

  // 当用户发送一条新消息时(在ChatWindow组件中)
  const handleSendMessage = async (userInput) => {
    let convId = currentConversationId;
    if (!convId) {
      // 创建新会话
      convId = await ipcRenderer.invoke('history:create-conversation', '新对话');
      setCurrentConversationId(convId);
    }
    // 保存用户消息
    await ipcRenderer.invoke('history:add-message', convId, 'user', userInput);
    // ... 调用AI API获取回复 ...
    const aiResponse = '...'; // 获取到的AI回复
    // 保存AI回复
    await ipcRenderer.invoke('history:add-message', convId, 'assistant', aiResponse);
  };

  return (
    <div style={{ display: 'flex', height: '100vh' }}>
      <HistorySidebar
        onSelectConversation={handleSelectConversation}
        currentConversationId={currentConversationId}
      />
      <ChatWindow
        conversationId={currentConversationId}
        onSendMessage={handleSendMessage}
      />
    </div>
  );
}

export default App;

5. 常见问题与排查思路

在实现和运行过程中,你可能会遇到以下问题:

问题现象 可能原因 解决思路
数据库文件无法创建或写入 1. 用户数据目录无写入权限。
2. 防病毒软件或系统权限限制。
1. 检查 app.getPath('userData’) 返回的路径是否可写。
2. 尝试以管理员身份运行应用(开发阶段)。
3. 将数据库路径改为当前目录( ./history.db )测试。
better-sqlite3 编译失败 Node.js版本与 better-sqlite3 原生模块不兼容。 1. 确保Node.js版本与 better-sqlite3 版本匹配。
2. 运行 npm rebuild better-sqlite3
3. 使用 electron-rebuild 重新编译。
全文搜索 (FTS) 不返回结果 1. FTS表未正确创建或同步。
2. 搜索语法错误。
1. 检查建表SQL,确认 messages_fts 表已创建。
2. 确保在 addMessage 中同步向FTS表插入了数据。
3. 尝试简单的MATCH查询,如 MATCH ‘“python”’
删除会话后,FTS表仍有残留数据 外键 ON DELETE CASCADE 对虚拟表无效。 必须在删除会话前,手动删除 messages_fts 表中对应的 rowid (如示例代码所示)。
前端IPC调用无响应或报错 1. IPC事件名未在主进程注册。
2. 渲染进程中 ipcRenderer 使用方式错误。
1. 检查 ipcMain.handle ipcRenderer.invoke 的事件名是否完全一致。
2. 在渲染进程,确保通过 window.require(‘electron’) 获取 ipcRenderer (如果启用了 contextIsolation nodeIntegration 需相应配置)。
历史记录列表加载缓慢 1. 会话或消息数据量过大。
2. 未对查询进行分页。
1. 在 getAllConversations searchMessages 中严格使用 LIMIT OFFSET 进行分页。
2. 考虑为 conversations.updated_at 字段添加索引: CREATE INDEX idx_conv_updated ON conversations(updated_at DESC)

6. 最佳实践与进阶优化

实现基础功能后,以下实践能让你的历史记录系统更健壮、更友好:

  1. 数据备份与导出

    • 提供定期自动备份数据库到用户指定位置的功能。
    • 支持将会话导出为JSON、Markdown或TXT格式,方便用户归档或分享。
    // 导出为JSON示例
    const exportConversation = async (conversationId) => {
      const messages = await historyService.getMessages(conversationId);
      const conversation = conversations.find(c => c.id === conversationId);
      const exportData = {
        meta: conversation,
        messages: messages
      };
      const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
      // ... 使用 dialog.showSaveDialog 保存文件
    };
    
  2. 数据清理策略

    • 提供设置选项,允许用户自动清理超过一定天数或大小的历史记录。
    • 实现“软删除”(如 is_deleted 标记)而非直接物理删除,保留恢复可能。
  3. 性能优化

    • 索引 :为常用的查询字段(如 conversations.updated_at , messages.conversation_id , messages.created_at )创建索引。
    • 分页 :所有列表查询必须支持分页,避免一次性加载过多数据导致界面卡顿。
    • 虚拟列表 :如果历史记录条目非常多,在前端使用虚拟滚动(如 react-window )来渲染列表,提升渲染性能。
  4. 隐私与安全

    • 加密存储 :如果对话内容高度敏感,可以考虑使用SQLCipher(SQLite的加密扩展)或应用层加密(如使用Node.js的 crypto 模块加密 content 字段后再存储)。注意密钥管理问题。
    • 明文警告 :在应用设置中明确告知用户历史记录的存储位置和未加密状态。
  5. 用户体验细节

    • 自动生成标题 :当创建新会话时,可以用AI模型或简单的规则(如提取用户第一条消息的前N个字符)自动生成一个更有意义的标题。
    • 搜索高亮 :在搜索结果中,对匹配到的关键词进行高亮显示。
    • 批量操作 :支持批量删除、批量导出历史会话。
  6. 多窗口同步 :如果你的应用支持多窗口,需要确保历史记录的变化(如新增、删除)能在所有窗口实时同步。这可以通过主进程作为中心枢纽,使用 BrowserWindow.webContents.send 向所有渲染进程广播数据变更事件来实现。

为桌面AI助手添加本地历史记录功能,看似是一个附加特性,实则是构建可信赖、可依赖的生产力工具的核心一环。它解决了用户对数据丢失的恐惧,并通过强大的检索能力放大了过往对话的价值。本文从设计思路、技术选型到代码实现,提供了一套完整的解决方案。你可以根据自己使用的技术栈(Electron, Tauri, Flutter)进行适配和扩展。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值