这次我们来看一个被反复讨论但很容易做偏的问题:AI 智能体为什么总是记不住东西,以及怎么给它补一个可用的“第二大脑”。很多人把第二大脑理解为 Obsidian 或 Notion 里的一份精读笔记,但对于 AI 智能体来说,第二大脑必须是一套可读、可写、可检索的外部记忆系统。简单说,智能体应该能把新信息写进去,在需要时把旧信息查出来,再基于查到的内容完成当前任务。围绕这个概念有不少专题讨论,Vanishing Gradients 也专门聊过这个方向,核心结论并不复杂:Agent 的聪明程度,取决于它能在多大范围、多快、多准确地访问自己的过去。
实现层面,这套系统通常由五个模块组成:文档存储、文本切分、向量化、检索服务,以及智能体工具调用链路。别急着上复杂平台,先用最小闭环验证:写入一条记忆、检索一条记忆、让 Agent 在回答中引用这条记忆。能跑通这三步,后面的知识图谱、定时同步、权限审计才有意义。如果只写不查,第二大脑会变成垃圾桶;如果只查不写,智能体仍然是金鱼记忆。
本文会覆盖核心能力、适用边界、系统设计、环境准备、代码实现、接口接入、批量任务、性能观察和常见问题排查,适合正在做 AI 智能体、个人知识库、RAG 应用和研究助手的开发者。文末会给出一个可以直接复制的最小参考实现和检查清单,先把闭环跑通,再谈规模化。
1. 第二大脑系统核心能力速览
所谓给 AI 智能体构建第二大脑,本质是给模型加一层外部记忆。它和普通知识库的区别在于:知识库只提供“读”,第二大脑还要求“写回”。智能体不仅要在需要时把信息查出来,还要能在对话过程中把值得记住的内容写进长期存储。
| 能力项 | 说明 |
|---|---|
| 核心目标 | 为 AI 智能体提供持久、可检索、可写回的外部记忆 |
| 典型记忆类型 | 用户偏好、项目状态、事实笔记、工作日志、决策记录 |
| 存储层 | Markdown、SQLite、PostgreSQL、向量数据库等,按数据规模选择 |
| 检索层 | 关键词、向量相似度、重排序,或直接交给现有知识库工具 |
| 接入方式 | CLI 脚本、HTTP API、智能体 Function Calling、MCP 工具 |
| 批量任务 | 目录导入、增量索引、定时刷新、失败重试 |
| 硬件要求 | 不做强依赖;纯文本和向量检索可在普通 CPU 上运行,Embedding / LLM 按实际服务配置 |
| 显存占用 | 取决于所选 Embedding 模型和 LLM 服务,需要按实际环境实测,没有统一数值 |
| 更适合的场景 | 个人知识库、项目上下文、客服助手、研究助手、长周期任务执行 |
这套方案的典型使用方式是:把当前任务需要用到的知识写进记忆库,Agent 在回答问题前先调用query_memory检索相关片段,再把片段拼进提示词上下文。整个过程对用户透明,但对 Agent 来说,上下文不再是单次会话,而是一个可积累、可更新的外部事实层。
2. 适用场景与使用边界
这个方案最值得做的场景有三类:
- 个人知识管理:笔记、文章、会议记录散落在不同文件里,AI 智能体无法直接使用。第二大脑把这些非结构化文本统一成可检索片段,Agent 能回答“我之前是怎么处理这个问题的”。
- 项目长期上下文:一个项目持续数月,每次新会话都从头讲上下文非常低效。让 Agent 把关键决策、当前进度、待办事项写回记忆,下一次会话直接检索。
- 信息密集型助手:例如客服、研究助理、数据分析助理。它们需要访问大量历史记录和领域知识,并且需要把每次交互中产生的新信息保存下来。
不适合的场景也很明确:如果只是单轮问答,用普通 RAG 就够了,不需要完整写回链路;如果数据本身没有持续积累价值,第二大脑只是在增加维护成本;如果原始数据质量很差、没有来源、没有结构,强行建库只会让智能体更自信地犯错。
使用边界必须以安全和合规为前提。第二大脑里面沉淀的内容不一定是事实,Agent 检索到后必须能追溯来源,不能直接当成结论。涉及隐私、医疗、金融、人脸、声音等敏感信息时,尽量只使用本地模型和本地存储,不要上传到不可控的远端服务。涉及版权材料或他人数据,要先获得授权并做脱敏处理。AI 生成的内容需要人工复核后才能进入长期记忆,否则错误会越攒越多。
3. 系统设计:从“笔记仓库”升级为“外部记忆”
给 AI 智能体用的第二大脑,不能只是一个 Markdown 文件夹。它需要至少包含下面几个层次:
| 模块 | 职责 | 最小可选实现 |
|---|---|---|
| 采集层 | 接收新内容,支持手动写入、文件导入、Agent 写回 | 目录扫描 + 函数调用 |
| 解析层 | 把 Markdown、TXT、PDF 等内容切分成可检索片段 | 文本切分函数 |
| 索引层 | 生成向量,保留标题、来源、时间等元信息 | Embedding API / 本地模型 |
| 存储层 | 保存原始内容、向量和元信息 | SQLite / 向量数据库 |
| 检索层 | 根据用户问题返回最相关的记忆片段 | 向量相似度 + 关键词过滤 |
| 写回层 | 让 Agent 能通过工具写入新记忆 | HTTP API / Function Calling |
| 治理层 | 控制访问范围、记录写入来源、支持删除和审计 | 权限配置 + 操作日志 |
3.1 采集与解析
采集层解决的是“内容怎么进来”。手动写一段笔记、批量导入旧文档、Agent 在对话中自动保存,这三种入口都要支持。
解析层的关键在文本切分。直接整篇向量化的问题是有用信息被稀释,长文档检索质量很差。更稳妥的做法是按语义边界切分,例如 Markdown 标题、段落、代码块、列表,同时保留标题和来源信息。每条记忆应该包含内容体本身和元数据:标题、来源、时间、标签、状态、关联项目等。
一个合理记录格式应该是这样的:
id: kb-20250601-001 type: note source: meeting-notes.md title: 项目例会记录 tags: [project, action] created_at: 2025-06-01T10:00:00+08:00 updated_at: 2025-06-01T10:00:00+08:00 status: active content: | 结论:第二大脑采用检索增强方式实现。 行动:本周完成 API 原型。这种结构化元信息不只是为了好看。它决定了检索层能不能做过滤,比如只查project标签、只查最近一周的action、只查特定来源的笔记。没有元数据的向量库,检索时只能做全量相似度,数据量一大就会变慢且不准确。
3.2 索引与存储
切分完成后,每条文本片段需要转成向量。这一步通常由 Embedding 模型完成,可以是本地模型,也可以是远端服务。向量质量直接决定检索效果,不同模型对中文、代码、专业术语的处理能力有明显差异,需要在真实数据上对比。
存储层有两部分:原始文本和向量。对个人项目,SQLite 已经足够;对几千条甚至几万条笔记,SQLite 配合向量扩展也能跑;数据量更大时,再切换到支持向量索引的数据库,比如 PostgreSQL + pgvector、Chroma、FAISS 等。不要一开始就上分布式系统,复杂度会淹没核心价值。
3.3 检索与写回
检索层不只是“算相似度”。好的第二大脑应该支持:
- 关键词过滤,缩小候选范围
- 向量相似度召回,找到语义相关片段
- 重排序或评分,把最相关的内容排到前面
- 结果附带来源,Agent 回答时能说明依据
写回层是很多人忽略的部分。智能体在对话中会不断收到新信息,例如用户说“我更喜欢 PostgreSQL”“这个项目已经进入测试阶段”。这些事情如果不写回,下次对话又要重新说一遍。写回的关键不是记录所有内容,而是让 Agent 判断哪些信息有长期价值,再调用写入接口保存。
4. 环境准备与前置条件
在开始写代码之前,先把环境检查一遍。这个方案不一定需要 GPU。如果 Embedding 和对话模型都使用远端服务,本地只负责文本处理和 API 转发,一台普通开发机就够。如果使用本地模型,才需要根据模型参数量评估显存和内存。
推荐的最小环境结构:
- 操作系统:Windows / macOS / Linux 均可
- Python:较新的 3.x 稳定版本
- 依赖:FastAPI、Uvicorn、Requests
- 存储:SQLite 或你选择的向量数据库
- 模型服务:可用的 Embedding 接口和 LLM 接口
- 磁盘空间:除了代码,预留文档存储空间
- 端口:预留一个本地 API 端口,比如 8000
建议先创建独立虚拟环境,避免依赖相互污染:
# 新建虚拟环境并激活 python -m venv .venv source .venv/bin/activate # Windows 使用下面的命令激活 # .venv\Scripts\activate # 安装基础依赖 pip install fastapi uvicorn requests python-multipart如果你希望完全本地运行,可以准备一个本地 Embedding 服务,路径通过环境变量控制。这样即使后续更换模型,也不需要改业务代码。建议把服务地址和密钥都放到环境变量里:
# 根据实际服务地址和模型名配置 export MEMORY_DB_PATH="./memory.db" export EMBEDDING_SERVICE_URL="http://127.0.0.1:1234/v1/embeddings" export EMBEDDING_MODEL="your-local-embedding-model" export EMBEDDING_API_KEY="your-token"注意,不同服务商的接口返回格式可能不同。如果接的是本地 Ollama、LM Studio 等工具,就把地址换成对应本机端口;如果接云端服务,就换成服务商提供的地址。密钥不要写死在代码仓库里,用环境变量或本地配置文件管理。
5. 最小实现:让 AI 智能体读写第二大脑
下面给出一套可运行的最小参考实现。它足够简单,适合个人笔记和中小规模 Agent 项目。代码核心思路是:SQLite 存文本和向量,通过本地或远端 Embedding 接口生成向量,用余弦相似度做检索。这个方案在数据量比较小时完全够用,跑通后再考虑换成专业向量库。
5.1 初始化数据库
先用 SQLite 建一张notes表,保存原始文本、向量 JSON、标题、来源和时间。
import json import os import sqlite3 import hashlib import requests from datetime import datetime, timezone from pathlib import Path DB_PATH = Path(os.getenv("MEMORY_DB_PATH", "memory.db")) def init_db(): con = sqlite3.connect(DB_PATH) con.execute(""" CREATE TABLE IF NOT EXISTS notes ( id TEXT PRIMARY KEY, title TEXT NOT NULL, source TEXT DEFAULT '', content TEXT NOT NULL, chunk_index INTEGER NOT NULL, embedding TEXT NOT NULL, created_at TEXT NOT NULL ); """) con.commit() return con字段embedding存的是 JSON 字符串,方便在 SQLite 里直接保存向量。如果你后续改用专业向量库,只需要替换这里的读写逻辑。
5.2 文本切分
文本切分直接决定检索精度。整篇保存会让向量包含太多无关信息,建议把 Markdown 按标题和长度边界切成块:
def split_markdown(content: str, max_chars: int = 800): chunks = [] current = [] length = 0 for line in content.splitlines(): if current and (line.startswith("#") or length + len(line) > max_chars): chunks.append("\n".join(current)) current = [] length = 0 current.append(line) length += len(line) if current: chunks.append("\n".join(current)) return chunksmax_chars可以按实际效果调整。过大会稀释语义,过小会切碎上下文。常见范围在几百到一千字左右,具体以你手机器上的检索结果为准。
5.3 生成向量
这里的embed_text函数调用外部 Embedding 服务。你需要根据实际服务商的返回格式调整取值路径:
def embed_text(text: str) -> list[float]: url = os.environ["EMBEDDING_SERVICE_URL"] headers = { "Authorization": f"Bearer {os.environ.get('EMBEDDING_API_KEY', '')}" } payload = { "model": os.environ.get("EMBEDDING_MODEL", "default"), "input": [text] } resp = requests.post(url, headers=headers, json=payload, timeout=30) resp.raise_for_status() data = resp.json() # 不同服务商返回字段可能不同,这里按常见格式取 data[0].embedding return data["data"][0]["embedding"]如果服务商返回的是embedding字段而不是data,需要改成对应结构。为了避免每次请求都重复这段逻辑,后续可以封装成模块。
5.4 写入记忆
写入时把笔记切块、生成向量、写入 SQLite。这里用source + title生成一个稳定 ID,重复写入时执行INSERT OR REPLACE,相当于更新旧版本。
def add_note(title: str, content: str, source: str = "manual") -> str: con = init_db() raw_id = hashlib.sha1(f"{source}:{title}".encode("utf-8")).hexdigest()[:16] chunks = split_markdown(content) now = datetime.now(timezone.utc).isoformat(timespec="seconds") for idx, chunk in enumerate(chunks): vec = embed_text(chunk) con.execute( "INSERT OR REPLACE INTO notes " "(id, title, source, content, chunk_index, embedding, created_at) " "VALUES (?, ?, ?, ?, ?, ?, ?)", (f"{raw_id}-{idx}", title, source, chunk, idx, json.dumps(vec), now), ) con.commit() con.close() return raw_id5.5 检索记忆
检索时先把用户问题转成向量,再和库里的所有向量计算余弦相似度,返回最相关的几条:
def cosine_similarity(a: list[float], b: list[float]) -> float: dot = sum(x * y for x, y in zip(a, b)) norm_a = sum(x * x for x in a) ** 0.5 norm_b = sum(x * x for x in b) ** 0.5 return dot / (norm_a * norm_b + 1e-12) def query_notes(query_text: str, top_k: int = 5): q_vec = embed_text(query_text) con = init_db() rows = con.execute( "SELECT content, embedding, title, source FROM notes" ).fetchall() scored = [] for content, embedding, title, source in rows: vec = json.loads(embedding) score = cosine_similarity(q_vec, vec) scored.append({ "score": round(score, 4), "title": title, "source": source, "content": content, }) scored.sort(key=lambda x: x["score"], reverse=True) con.close() return scored[:top_k]这套实现适合个人笔记规模。如果数据量继续增长,全表扫描会越来越慢,届时可以把向量检索部分替换成 Chroma、FAISS 或 PostgreSQL pgvector。
5.6 跑通最小闭环
按顺序验证三步:
# 第一步:写入一条记忆 python -c 'from memory import add_note; add_note("用户技术栈", "用户主要使用 Python、FastAPI、PostgreSQL。")' # 第二步:检索这条记忆 python -c 'from memory import query_notes; print(query_notes("用户的数据库选型是什么"))'预期结果是返回包含“PostgreSQL”的记忆片段。如果返回空,先检查写入是否成功、向量是否生成、查询问题是否和原文差异过大。这一个闭环能跑通,后续接入 Agent 才有意义。
6. 接口 API 与批量任务接入
要让 AI 智能体使用第二大脑,最直接的方式是暴露 HTTP API。下面用 FastAPI 写一个最小服务,提供健康检查、写入记忆、检索记忆三个接口:
# server.py from fastapi import FastAPI from pydantic import BaseModel from memory import add_note, query_notes app = FastAPI() class WriteRequest(BaseModel): title: str content: str source: str = "agent" class QueryRequest(BaseModel): query: str top_k: int = 5 @app.get("/health") def health(): return {"status": "ok"} @app.post("/memory/write") def write_memory(request: WriteRequest): note_id = add_note(request.title, request.content, request.source) return {"ok": True, "note_id": note_id} @app.post("/memory/query") def query_memory(request: QueryRequest): docs = query_notes(request.query, request.top_k) return {"docs": docs}启动服务:
uvicorn server:app --host 127.0.0.1 --port 8000启动后可以用 curl 验证:
curl -X POST "http://127.0.0.1:8000/memory/query" \ -H "Content-Type: application/json" \ -d '{"query": "用户的数据库选型是什么", "top_k": 3}'接口能跑通,就可以把写记忆和查记忆包装成智能体的工具。常见做法是通过 Function Calling 暴露两个函数:
[ { "type": "function", "function": { "name": "write_memory", "description": "把当前对话中的重要信息写入外部记忆", "parameters": { "type": "object", "properties": { "title": {"type": "string"}, "content": {"type": "string"} }, "required": ["title", "content"] } } }, { "type": "function", "function": { "name": "query_memory", "description": "根据用户问题检索外部记忆", "parameters": { "type": "object", "properties": { "query": {"type": "string"}, "top_k": {"type": "integer", "default": 5} } } } } ]接入 Agent 后,还要在系统提示词里写清楚触发规则。例如:当用户提供新的长期事实、修改偏好、完成项目关键节点时,调用write_memory;当用户问题涉及历史信息或项目上下文时,先调用query_memory。不要所有对话都写入,写太频繁会导致记忆库噪声变大,检索质量下降。
批量导入是第二大脑落地的重要环节。你通常有一堆旧笔记要一次性建索引,建议做成目录扫描模式:
# 示例命令,实际参数按你的 CLI 实现调整 python ingest.py --dir ./notes --incremental --max-workers 4 --retry-failed增量导入的核心是避免重复计算。可以按文件路径和修改时间判断,也可以先计算文件内容哈希。大批量导入时,给每个文件记录导入状态,失败的任务单独重试。定时同步可以通过系统定时任务触发:
# 每天凌晨 1 点重建增量索引示例,实际路径按环境调整 0 1 * * * cd /opt/second-brain && python ingest.py --dir ./notes --incremental >> logs/ingest.log 2>&17. 资源占用与性能观察
第二大脑系统的资源占用没有固定数字,它主要由三部分组成:Embedding 模型的算力和显存、向量检索的数据规模、LLM 服务的上下文长度。如果 Embedding 和 LLM 都走远端 API,本地进程只需要处理文本和 HTTP 请求,资源占用很低。如果完全本地推理,就要按模型参数量评估显存。
优化的关键点在于:先定位瓶颈在哪个阶段。建议用下面这些简单命令观察状态:
# 查看数据库体积 du -sh memory.db # 查看本地服务进程 ps aux | grep uvicorn # 查看端口和连接状态 curl http://127.0.0.1:8000/health判断资源消耗最容易踩的误区是只盯着显存。实际使用中,更常见的瓶颈是:Embedding 接口响应慢、向量检索全表扫描耗时高、大量智能体并发写库导致 SQLite 锁等待。下表可以按阶段对号入座:
| 阶段 | 观察点 | 优化方向 |
|---|---|---|
| 文本入库 | CPU 占用、写入耗时 | 异步处理、批量 Embedding、控制文件并发 |
| 向量生成 | API 延迟、GPU 占用 | 批量请求、换更快的 Embedding 模型 |
| 检索 | 单次查询耗时、内存占用 | 加向量索引、缩候选范围、用元数据过滤 |
| Agent 调用 | 响应时间、Token 开销 | 控制top_k、限制返回片段长度、加缓存 |
| 批量导入 | 日志、失败率 | 增量更新、断点续传、失败重试 |
观察性能时,每次只改一个变量。比如先固定top_k,测试不同max_chars对检索效果的影响;再固定切分方式,测试不同 Embedding 模型的召回质量。慢慢调出一套适合你数据的最小配置。
8. 常见问题与排查方法
下面的排查表覆盖了从部署到使用的常见问题,遇到异常按表格对号排查。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| Agent 回答完全不使用记忆 | 工具调用没有配置,或模型没有触发query_memory | 查看调用日志,确认 Agent 是否发起工具调用 | 在系统提示词里明确触发规则,简化工具名称 |
| 检索结果明显不相关 | 切分过大、向量模型不匹配、缺少元数据过滤 | 复现一条查询,查看返回片段和来源 | 调整max_chars,换 Embedding 模型,增加关键词过滤 |
| 写入后查询不到 | Embedding 接口失败、向量写入失败、ID 冲突 | 检查写入日志,确认向量数组长度 | 增加异常捕获,确认 Embedding 响应格式正确 |
| 服务启动失败 | 依赖缺失或端口被占用 | 查看 Uvicorn 启动日志,检查 8000 端口 | 安装依赖,更换端口,例如--port 8001 |
| API 返回 401 或超时 | 密钥错误、服务地址不可达、网络不稳定 | 用 curl 单独测试 Embedding 服务 | 检查环境变量,确认服务地址,延长超时时间 |
| 中文内容乱码 | 文件编码没有使用 UTF-8 | 查看原始文件编码 | 统一保存为 UTF-8,读取时显式指定编码 |
| SQLite 写入报 database is locked | 多进程并发写同一个库 | 查看写入日志和进程数 | 使用单进程写入队列,或改用 PostgreSQL |
| 记忆库越积越大,检索变慢 | 所有旧数据都参与全表扫描 | 查看数据库行数和查询耗时 | 引入向量索引,增加时间过滤,定期清理过期记忆 |
如果你给智能体接入了外部记忆,还要注意一个问题:模型可能把记忆当作权威事实,而实际上记忆库里全是旧版本或未经确认的信息。排查时要看“内容是否引用了来源”,而不只是“有没有返回内容”。没有来源的记忆,权威性默认存疑。
9. 最佳实践与合规边界
构建第二大脑不是一次完成后就结束的工程,它是一个需要持续维护的数据系统。下面是几条比较实用的实践建议。
先做最小闭环,再扩展复杂性。很多人一开始就规划知识图谱、权限系统、多人协作,结果一个月后连一条检索都跑不通。第一周只需要完成:手动写入一条笔记、用接口查出来、让 Agent 把这段内容拼进上下文。三个环节全部打通,再逐步增加功能。
给智能体一个明确的记忆策略。不要让 Agent 自由决定写什么。更好的做法是在系统提示词里给出规则,例如“只在用户明确表达偏好、项目状态发生变化、完成重要决策时写入”“检索不到答案时先查记忆,不要硬编”。这样记忆库的噪声会小很多。
每条记忆尽量保留来源和创建时间。Agent 基于记忆回答时,至少要能定位到原始文件或原始对话。没有来源的记忆,时间越久越不可信。结构化元数据还能帮助你后续做清理,例如批量删除某个来源的全部记录。
权限和隐私要前置设计。如果第二大脑只跑在本地,不要无脑开放网络端口。服务默认绑定127.0.0.1,不要绑定0.0.0.0。如果需要对外提供服务,要加上身份认证和请求频率限制。涉及他人数据、版权材料、人脸声音、医疗金融信息时,必须确认授权并做脱敏处理。
最后,AI 生成的内容在进入长期记忆前要做人工复核。第二大脑的价值是“积累可信上下文”,不是“囤积 AI 幻觉”。每条新记忆如果来自模型总结,建议标记为generated,并要求人工确认后才转为verified。这样即便模型出错,错误也不会被下一次任务自动放大。
下一步可以继续扩展的方向包括:把向量检索升级成“向量 + 关键词 + 时间过滤”的多路召回;在记忆里加入知识图谱,把实体关系显式存下来;把短期工作记忆和长期知识记忆分层管理;让 Agent 定期复盘旧记忆,删除过期内容。基础闭环稳定后,这些方向都可以一步步接进去。