之前折腾各类 AI 助手时,我发现最麻烦的往往不是模型效果,而是账号体系带来的各种隐性成本:注册要手机号、登录要验证码、频繁提示账户验证中、换台电脑就被风控拦截,甚至还要处理“隐私协议未声明 API scope”之类的合规提示。也正因为这些痛点,一个叫Anjadhe的开源项目引起了我的注意:它把隐私优先作为核心设计原则,不需要注册账户,也没有服务器数据库。本文就从架构、原理到代码实现,完整拆解这种“无账户 + 无服务器数据库”的 AI 助手方案。
这个主题适合三类读者:一是想了解隐私友好型应用设计的开发者,二是正在做本地优先(local-first)工具的工程师,三是对 AI 应用数据流向比较敏感、想自己掌控数据的用户。读完本文,你会理解 Anjadhe 的设计思路,也能用前端本地存储加 AI 接口,搭建一个不依赖后端的 AI 助手原型。
1. 背景:为什么需要“无账户、无服务器 DB”的 AI 助手
1.1 传统 AI 助手的数据隐私痛点
传统 AI 助手通常由三部分组成:客户端、业务后端、模型服务。用户要使用,必须先注册账户,后端数据库保存用户名、邮箱、密码哈希、对话记录、使用日志等数据。整套流程看似正常,但对隐私敏感的用户来说,至少存在几个问题:
- 数据集中存储,一旦数据库泄露,所有用户的历史对话都可能被翻出来。
- 账户体系意味着身份可追溯,AI 对话内容可以和实名信息关联。
- 用户无法彻底掌握自己的数据生命周期,删号后数据是否真的删干净,很难验证。
- 跨境传输和第三方合规问题,也会让部分用户产生顾虑。
日常开发中,我们也会遇到大量与账户和隐私相关的报错,例如:
account verification is pending. please try after some timeyour account is pending approval from your gitlab administratorchooseImage:fail api scope is not declared in the privacy agreementtoo many computers used within the last 24 hours for the same cursor account
这些信息虽然来自不同平台,但本质都指向同一类问题:账户体系把用户和工具之间原本简单的交互,变成了“资质审核 + 权限声明 + 设备管理”的复杂链路。Anjadhe 选择反其道而行:砍掉账户,砍掉服务器数据库,让数据和身份尽可能留在用户自己手里。
1.2 Anjadhe 是什么
Anjadhe 是一个隐私优先的 AI 助手项目,它的核心设计约束是:
- no account:用户不需要注册、登录、绑定手机号,打开即用。
- no server DB:服务端不保存用户数据,没有用户表、没有对话记录表、没有操作日志表。
- privacy first:所有设计决策优先考虑隐私保护和数据最小化。
需要注意,“no server DB”并不等于“完全没有服务器”,更准确的理解是:服务器不承担数据持久化职责。可以存在一个无状态的代理层,用于转发 AI 接口请求、隐藏 API Key、处理跨域或接口鉴权,但这个代理不落库、不追踪、不记录用户身份。
1.3 适用场景
- 个人知识库问答工具,只希望在本机浏览器里保留对话。
- 团队内部使用的轻量 AI 助手,不想引入额外的账号系统和数据库运维。
- 隐私敏感场景,如医疗、法律、财务等领域的文案辅助。
- 想快速做 AI 产品原型验证,不想先搭一套用户系统。
2. 隐私优先架构拆解
2.1 去账户化:砍掉身份体系之后的架构变化
传统应用中,账户系统承担三件事:身份认证、数据隔离、权限控制。去账户化之后,这三件事都要重新设计。
- 身份认证:不再需要 token、session、JWT,转而使用设备本地标识。每次请求可以携带一个随机生成的设备 ID,但后端不把这个 ID 和手机号、邮箱绑定。
- 数据隔离:用户数据保存在浏览器本地,不同用户之间天然隔离,因为数据没出过本机。
- 权限控制:不需要复杂的 RBAC,只有“当前用户自己的数据”和“模型接口的访问权限”两级。
这样一来,后端需要管理的状态就非常少:没有会话状态,没有用户状态,只有无状态的 API 转发。服务端可以随时重启、横向扩容,甚至部署在边缘节点,因为不需要共享 session。
2.2 数据存储位置的变化
有服务器数据库的传统架构,数据流向是:
用户输入 -> 前端 -> 后端 API -> 数据库 -> 模型服务Anjadhe 的架构把“数据库”这一环移到浏览器本地:
用户输入 -> 前端 -> 本地存储(IndexedDB/localStorage) -> 模型服务用户对话记录、偏好设置、密钥信息都留在浏览器中。用户关闭页面后,数据仍然留在本地;用户清除浏览器站点数据后,数据随之删除,服务端完全无感知。
2.3 服务端还剩下什么
虽然不存数据,但服务端仍可以承担以下职责:
- API 转发:浏览器直接调用模型服务会遇到 CORS、密钥泄露问题,通过一个轻量代理转发,把 API Key 保存在服务端环境变量中。
- 限流控制:服务端可以对来源 IP 做简单限流,防止接口被恶意刷量,但只记录计数,不记录内容。
- 静态资源托管:前端页面本身由静态服务器托管。
这个代理层是无状态的,适合部署在 Serverless 平台或轻量容器中,配合 CDN 使用。需要强调的是,代理层是否记录日志、记录哪些字段,需要在部署时通过环境变量明确配置。
3. 环境准备与项目结构
3.1 运行环境
- Node.js 18+,用于本地开发与构建工具运行。
- 现代浏览器(Chrome、Edge、Firefox 等),示例代码使用原生 JavaScript 和浏览器 API。
- 一个可用的 AI 模型接口,本文示例使用 OpenAI 兼容的
/v1/chat/completions接口格式,实际操作时以你所用服务商的文档为准。
版本需要根据你的项目实际情况调整,本文示例以常见环境为例,重点演示“无账户、无服务器数据库”的实现思路。
3.2 项目初始化
创建一个空目录并初始化package.json:
mkdir anjadhe-demo cd anjadhe-demo npm init -y安装 Vite 作为开发服务器和构建工具:
npm install -D vite3.3 目录结构
anjadhe-demo/ ├── index.html ├── package.json └── src/ ├── main.js ├── storage.js └── api.jsindex.html:页面入口,提供聊天界面。src/storage.js:负责本地数据读写。src/api.js:负责调用模型接口。src/main.js:绑定页面事件,组织核心逻辑。
4. 核心功能实现
4.1 本地密钥管理:不落盘到服务端
AI 接口通常需要 API Key。按隐私优先原则,推荐让用户在前端输入密钥,并只保存在浏览器内存或本地存储中,不发送到我们自己维护的服务端。
// src/storage.js const STORAGE_KEYS = { API_KEY: 'anjadhe_api_key', MESSAGES: 'anjadhe_messages' }; export function saveApiKey(apiKey) { // 使用 sessionStorage 时,关闭标签页后自动清除,更安全 sessionStorage.setItem(STORAGE_KEYS.API_KEY, apiKey); } export function getApiKey() { return sessionStorage.getItem(STORAGE_KEYS.API_KEY) || ''; } export function clearApiKey() { sessionStorage.removeItem(STORAGE_KEYS.API_KEY); }这里使用sessionStorage保存 API Key,关闭标签页即失效,尽量避免长期留在浏览器中。如果希望下次打开还能继续使用,也可以改用localStorage,但需要在界面上明确提示用户。
4.2 对话记录本地存储
对话记录用localStorage保存,数据结构是一个消息数组:
// src/storage.js export function loadMessages() { const raw = localStorage.getItem(STORAGE_KEYS.MESSAGES); try { return raw ? JSON.parse(raw) : []; } catch (e) { return []; } } export function saveMessages(messages) { localStorage.setItem(STORAGE_KEYS.MESSAGES, JSON.stringify(messages)); } export function clearMessages() { localStorage.removeItem(STORAGE_KEYS.MESSAGES); }每次用户发送消息、收到模型回复后,都调用saveMessages把完整对话数组写回本地。代码量不大,但能体现“数据本地持久化”的核心思路。
4.3 调用 AI 接口
调用模型接口时,把本地保存的 messages 传给 API:
// src/api.js export async function chatCompletion(apiKey, messages) { const response = await fetch('https://api.example.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ model: 'gpt-4o-mini', // 以实际可用模型为准 messages: messages }) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`API Error ${response.status}: ${errorText}`); } const data = await response.json(); return data.choices[0].message.content; }需要注意,这里的接口地址和模型名只是示例,必须替换为你实际使用的服务商地址。如果浏览器直连存在 CORS 问题,可以改为调用自己的无状态代理。
4.4 主逻辑绑定
在main.js中处理发送消息、渲染消息列表、清空会话等操作:
// src/main.js import { saveApiKey, getApiKey, clearApiKey, loadMessages, saveMessages, clearMessages } from './storage.js'; import { chatCompletion } from './api.js'; const form = document.getElementById('chat-form'); const input = document.getElementById('message-input'); const list = document.getElementById('message-list'); const apiKeyInput = document.getElementById('api-key-input'); const saveKeyBtn = document.getElementById('save-key-btn'); const clearBtn = document.getElementById('clear-btn'); let messages = loadMessages(); function renderMessages() { list.innerHTML = ''; messages.forEach((msg) => { const item = document.createElement('div'); item.className = 'message ' + msg.role; item.textContent = msg.content; list.appendChild(item); }); list.scrollTop = list.scrollHeight; } form.addEventListener('submit', async (e) => { e.preventDefault(); const text = input.value.trim(); if (!text) return; const apiKey = getApiKey(); if (!apiKey) { alert('请先填入 API Key'); return; } messages.push({ role: 'user', content: text }); saveMessages(messages); renderMessages(); input.value = ''; try { const reply = await chatCompletion(apiKey, messages); messages.push({ role: 'assistant', content: reply }); saveMessages(messages); renderMessages(); } catch (err) { messages.push({ role: 'assistant', content: '请求失败:' + err.message }); saveMessages(messages); renderMessages(); } }); saveKeyBtn.addEventListener('click', () => { const key = apiKeyInput.value.trim(); if (key) { saveApiKey(key); apiKeyInput.value = ''; alert('API Key 已保存在当前标签页内存中'); } }); clearBtn.addEventListener('click', () => { if (confirm('确认清空本地对话记录?')) { clearMessages(); messages = []; renderMessages(); } }); renderMessages();这里保留了消息上下文,把历史消息一起传给模型接口,让模型能理解对话语境。数据量较大时,可以考虑只传最近 N 条消息,避免超出模型上下文长度限制。
5. 完整实战案例:从零搭建本地优先的 AI 助手
下面我们完整落地一个最小可运行版本。这里给的是“无账户、无服务器数据库”的本地优先实现,所有对话数据保存在浏览器中。
5.1 创建页面
index.html内容如下:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Anjadhe Demo - 隐私优先 AI 助手</title> <style> body { font-family: system-ui, -apple-system, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; background: #f7f7f8; } .card { background: #fff; border-radius: 12px; padding: 20px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); } .api-row { display: flex; gap: 8px; margin-bottom: 16px; } .api-row input { flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 6px; } button { padding: 8px 16px; border: none; border-radius: 6px; background: #2563eb; color: #fff; cursor: pointer; } button.secondary { background: #64748b; } #message-list { min-height: 300px; max-height: 500px; overflow-y: auto; border: 1px solid #e2e8f0; border-radius: 8px; padding: 12px; margin: 16px 0; background: #fafafa; } .message { margin-bottom: 12px; padding: 10px; border-radius: 8px; white-space: pre-wrap; word-break: break-word; } .message.user { background: #dbeafe; text-align: right; } .message.assistant { background: #f1f5f9; text-align: left; } .message.error { background: #fee2e2; color: #b91c1c; } #chat-form { display: flex; gap: 8px; } #message-input { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 6px; } </style> </head> <body> <div class="card"> <h1>Anjadhe Demo</h1> <p style="color: #475569;">隐私优先 AI 助手:无账户,无服务器数据库,数据只保存在当前浏览器。</p> <div class="api-row"> <input id="api-key-input" type="password" placeholder="输入 API Key,仅保存在当前标签页内存中" /> <button id="save-key-btn">保存 Key</button> <button id="clear-btn" class="secondary">清空对话</button> </div> <div id="message-list"></div> <form id="chat-form"> <input id="message-input" type="text" placeholder="输入消息,按回车发送" autocomplete="off" /> <button type="submit">发送</button> </form> </div> <script type="module" src="/src/main.js"></script> </body> </html>页面结构比较简洁,重点功能都集中在一个界面里:API Key 输入区、消息列表、发送表单。
5.2 启动项目
在package.json中配置 scripts:
{ "name": "anjadhe-demo", "version": "0.1.0", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "devDependencies": { "vite": "^5.0.0" } }运行:
npm install npm run dev浏览器打开终端提示的本地地址(通常是http://localhost:5173)。
5.3 验证隐私效果
- 在页面上输入 API Key,点击“保存 Key”。此时打开 DevTools -> Application -> Session Storage,可以看到 key 只存在于当前标签页的 session 中。
- 发送几条消息,刷新页面。会话记录仍然存在,因为保存在
localStorage。 - 点击“清空对话”,
localStorage中的消息数组被删除。 - 关闭标签页再重新打开,API Key 已经消失,用户需要重新输入。
通过这几个操作可以直观对比:对话记录留在本地,API Key 按需输入,服务端没有保存任何用户数据。
6. 常见问题与排查思路
6.1 为什么本地数据还存在泄露风险
本地存储不等于绝对安全。如果设备被植入恶意软件,或者浏览器存在 XSS 漏洞,攻击者仍然可能读取localStorage和sessionStorage中的内容。因此:
- 不要在本地保存高敏感信息,或在保存前做加密处理。
- 为页面设置严格的 CSP(Content Security Policy),降低 XSS 风险。
- 涉及生产环境时,避免把真实 API Key 以明文形式长期保存在浏览器中。
6.2 浏览器直连模型接口遇到 CORS 错误
浏览器直连外部接口经常会遇到跨域限制,报错形式通常是:
Access to fetch at 'https://api.example.com/v1/chat/completions' from origin 'http://localhost:5173' has been blocked by CORS policy解决方案有三个:
- 服务商支持浏览器直接调用并允许 CORS,就不需要代理。
- 自己部署一个无状态代理,把模型接口转发到同源地址。
- 开发环境使用 Vite 的
server.proxy配置做代理。
Vite 代理配置示例:
// vite.config.js export default { server: { proxy: { '/api': { target: 'https://api.example.com', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } } } };6.3 本地存储被浏览器清理导致数据丢失
用户手动清除浏览器数据、浏览器自动清理、无痕模式关闭等都会导致本地数据丢失。这不是程序 bug,而是本地优先方案的固有特征。
可以在界面上提示用户“数据仅保存在浏览器本地,建议定期导出重要对话”。同时提供导入导出功能,方便用户备份。
6.4 无账户方案下如何防滥用
因为没有账户,后端无法封禁某个用户账号,只能根据请求特征做限制:
- 按 IP 限流。
- 按设备 ID 限流,设备 ID 由前端生成并随请求发送,后端只记录 ID 的哈希值。
- 对模型接口设置每日调用配额。
需要特别注意,后端可以控制限流,但不应把对话内容、设备 ID 和 IP 关联后长期保存,否则就违背了隐私优先的初衷。
6.5 常见报错对照表
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 请求返回 401 | API Key 无效或已过期 | 检查密钥配置,重新生成 |
| 返回 429 | 请求频率超限或账户余额不足 | 降低请求频率,检查服务商配额 |
| 返回 402 | 账户未购买额度 | 到服务商控制台充值或绑定支付方式 |
| 浏览器提示 CORS | 接口不支持跨域 | 使用代理或改为服务端转发 |
| 刷新后对话丢失 | 存储模块未正确读写 localStorage | 检查 localStorage 是否被禁用 |
| 标签页关闭后 Key 消失 | 使用 sessionStorage 存储 | 按需改为 localStorage,但要注意安全 |
7. 最佳实践与工程建议
7.1 数据最小化
无论做个人项目还是企业项目,都建议默认遵循数据最小化原则:只收集完成功能所必需的数据,能不留就不留。具体到 AI 助手场景:
- 不记录用户的浏览器指纹、设备型号、地理位置。
- 不记录每一次对话的完整日志。
- 如果必须记录接口调用指标,只记录时间、状态码、耗时等非内容字段。
7.2 加密本地存储
如果本地需要保存敏感数据,可以使用 Web Crypto API 对数据进行加密,解密密钥保存在sessionStorage中。这样即使localStorage被导出,攻击者也拿不到明文。示例思路:
// 使用 Web Crypto API 加密的伪代码思路 // 1. 生成 AES-GCM 密钥,保存在 sessionStorage // 2. 写入 localStorage 前加密 // 3. 读取 localStorage 后解密 // 注意:Web Crypto API 是异步 API,需要 await这里只是为了说明思路,生产环境需要按实际需求补充完整的密钥管理和错误处理逻辑。加密并不能解决所有问题,因为页面运行时终究需要解密数据,最终安全边界还是在浏览器环境本身。
7.3 服务端无状态与日志策略
如果你部署了一个无状态代理层,请务必检查运行平台的日志配置:
- 关闭请求体日志,防止对话内容被写入日志文件。
- 关闭响应体日志。
- 访问日志中避免记录 Authorization 请求头。
- 设置日志保留周期,定期清理。
在 Serverless 平台上,控制台日志默认可能记录请求信息,需要显式配置脱敏规则。
7.4 生产环境的隐私声明
虽然“无账户、无服务器数据库”,但应用仍然会调用第三方模型服务,用户数据会发送给模型服务商。因此隐私政策中需要如实说明:
- 哪些数据会发送给模型服务商。
- 数据在模型服务商的保留策略。
- 用户如何彻底删除数据。
不做虚假承诺,是隐私优先产品的基本底线。
7.5 数据导入导出
本地存储的最大问题是数据可迁移性差。建议提供 JSON 格式的导出与导入功能:
export function exportMessages() { const messages = loadMessages(); const blob = new Blob([JSON.stringify(messages, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'anjadhe-messages.json'; a.click(); URL.revokeObjectURL(url); } export function importMessages(jsonText) { const messages = JSON.parse(jsonText); saveMessages(messages); return messages; }这个功能帮助用户掌握自己的数据,也是隐私友好设计的一部分。
7.6 风险提示
无账户方案并非适合所有场景:
- 多端同步需要自己设计同步方案。
- 账号找回、数据恢复能力缺失,用户删除本地数据后无法找回。
- 防滥用能力弱于账号体系,大规模商业化时需要额外设计风控。
因此,Anjadhe 这类方案更适合工具型、轻量型、隐私敏感型产品,而不是需要复杂社交关系或强身份认证的业务系统。
8. 总结与下一步学习方向
通过 Anjadhe 这个项目,我们可以梳理出几条清晰的设计主线:
- 去账户化的关键是重新设计身份、数据隔离和权限控制,而不是简单砍掉登录页。
- 无服务器数据库的实质是把数据持久化从服务端迁移到客户端,服务端退化为无状态代理。
- 隐私优先不是“什么都不存”,而是明确告知用户数据去向、提供可验证的删除机制、最小化收集范围。
下一步可以继续深入的方向包括:
- 使用 IndexedDB 替代 localStorage,支持更大量级的对话记录和结构化查询。
- 使用 Web Crypto API 做本地加密存储。
- 使用 BroadcastChannel 实现在多个标签页之间的消息同步。
- 使用 Service Worker 实现完整的离线可用。
如果你正在设计 AI 工具,或者对隐私友好型应用架构感兴趣,可以动手把上面这个 Demo 跑起来,试着去掉会话保存、加上导出功能,再对比一下传统“注册登录 + MySQL + Redis”方案的实现成本。你会发现,去掉账户和数据库之后,整个系统的部署和维护压力会明显下降。后面我也会继续更新本地加密存储和多端同步的实战方案,欢迎保持关注。