我一直有个挺直观的痛处:无论换哪个AI助手,它都记不住我上周说过的那句话、上个月拍过的那张照片、昨天停过的那个车位。每次对话都要重新交代上下文,感觉不是在用"助手",而是在带一个短暂失忆的实习生。直到我翻到一个叫 Hindsight 的开源项目,它做的事情正好是给AI补上"长期记忆",而且是把现实世界的照片、语音、位置这些原始信息变成结构化记忆。再配合 Dify 这样的Agent平台,就能让智能体能随时"回想"用户过去的真实经历。这篇文章就讲讲 Hindsight 到底解决了什么问题、底层怎么流转、我实际部署踩过的坑,以及怎么把它接入 Dify,给Agent加上一个能检索的记忆库。
1. 为什么我非要把 Hindsight 拽进来:AI的"失忆"问题比想象中严重
1.1 被"没有记忆"的AI反复折磨的日常
我最早做个人助理型机器人时,最大的挫败感不是模型能力不行,而是它没有持续性。你今天让它记住"我住在浦东,公司在新天地,通勤一般地铁2号线",第二天再问它上班路线,它一脸懵。这不是模型笨,而是架构上压根没给它一个"可以翻旧账"的地方。
临时塞对话上下文的做法我也试过,比如把历史聊天记录一股脑拼进Prompt,结果一旦对话拉长,Token开销爆炸,而且模型会被无关信息干扰。更麻烦的是,现实世界的记忆不只是文字——用户拍了张停车场的照片,留了句语音,当时的地方、时间点,都是信息。传统RAG只处理文本,遇到非结构化输入就抓瞎。Hindsight 解决的就是这类问题:把照片、语音、GPS位置统一"翻译"成结构化文本记忆,再转成向量存起来,之后用户只需要用自然语言去搜。
1.2 Hindsight 和普通 RAG 的差别在哪
很多人一提长期记忆就想到 RAG:文档切块、向量化、召回。但 RAG 的假设是"知识已经在文档里了",而 Hindsight 接收的是现实世界未经打磨的原始输入——图像、声音、位置。它用多模态大模型把这些原始信号转成一句语义密度很高的描述,然后再走嵌入式向量检索。
我个人的理解是:Hindsight 不是又一个文档问答系统,它更像是"个人生活日志的AI编译器"。它能记录的不只是"你说过什么",还包括"你见过什么、在哪里、当时在做什么"。这种记忆粒度对个人助理型Agent非常关键,因为它需要的上下文通常不是书本知识,而是用户自己的经历碎片。
1.3 你会在什么时候真正需要它
我说几个真实场景,各位可以对号入座。第一,寻物类:"我昨天把车停哪儿了?"Hindsight 的GPS加照片描述能直接给答案。第二,行程回顾:"上礼拜跟客户吃饭的那家餐厅叫什么?"它靠多模态解析照片里的店面位置。第三,主动提醒:如果你给Agent接上了日历和位置历史,它可以提前说"你上次去这家医院检查是三个月前,该复查了"。这些场景都有一个共性:信息曾经存在,但存在于现实世界里,而不在对话记录里。让AI学会事后覆盖这些信息,就是 Hindsight 的价值。
2. Hindsight 底层到底在做什么:一张照片怎么变成一条可检索的记忆
2.1 采集端:Flutter 移动应用 + 手机传感器
Hindsight 的客户端用 Flutter 写了 iOS 和 Android 两个版本。用户操作很简单:拍照或从相册选图、录一段语音说明、顺手带上当前GPS坐标。这里有意思的是它没有要求用户填表单,所有的信息录入都贴近人的习惯——拍一张照,说一句话就够了。
移动端在采集时会把图片、语音文件和经纬度一起上报到后端。这个设计很务实,因为手机是最容易获取现实世界数据的设备,也是用户最没有"记录负担"的终端。你让用户写日志他坚持不了三天,但让他随手拍张照、憋一句语音,成本低很多。
2.2 生成端:GPT-4o 视觉解析 + Whisper 语音转写 + 向量化
采集到的原始数据会被送到 Supabase Edge Function 里。Edge Function 本质上就是一个跑在云端的 TypeScript/Deno 服务,它串联了三件事:
- 先调 OpenAI Whisper,把语音转成文本;
- 再把图片和语音文本一起丢给 GPT-4o 的多模态接口,生成一段结构化的记忆描述。描述里通常包含地点、动作、人物、物品、氛围这些维度,等于让模型"看图说话";
- 描述生成后,再调 text-embedding-3-small,把它转成 1536 维的向量。
这个流程的关键是:最终落库的可搜索单元既不是原图也不是原始语音,而是一段高密度描述加向量。原图会被存到对象存储里做备份,但检索主线完全依赖语义层。好处很明显——用户以后问问题时,不用精确回忆文件名或时间,只要说出大概意思就能匹配到。
2.3 存储与检索:Postgres + pgvector 和 Meilisearch 的混合结构
Hindsight 的存储用了两层。一层是 Supabase 自带的 Postgres 数据库,开 pgvector 扩展存向量,做Embedding相似度检索;另一层是 Meilisearch,做全文关键词检索。查询时两边同时跑,再做结果合并。
很多教程只讲 pgvector,我实际测下来发现纯向量检索在模糊记忆场景有短板。比如用户忘记当时的说法,用了完全不同的词,"全文搜索+向量搜索"双通道召回明显更稳。这也符合混合检索(Hybrid Search)的一般经验:语义相似度负责"意思接近",关键词匹配负责"字面命中",两条腿走路比单条腿稳。
2.4 为什么它没做成重型RAG系统
我一开始也困惑,Hindsight 为什么不引入一套完整RAG框架,而是用一个 Edge Function 加两张索引就完事了。后来想明白了:Hindsight 的核心场景是"个人记忆",数据量级和"企业文档库"完全不同。个人一天撑死几十条记忆,百万级向量和文档级 RAG 压根不是一回事。用轻量组件就能处理,没必要把系统做重;做重了反而带来部署复杂度和维护成本。这个取舍对个人开发者非常友好。
3. 部署实录:Supabase、Edge Functions、Meilisearch 一套跑通
3.1 准备阶段:账号、密钥和运行环境
如果你想把 Hindsight 完整跑起来,至少要准备这几样东西:一个 Supabase 项目(负责数据库、认证和 Edge Functions 托管)、一个 OpenAI API Key(用 GPT-4o 和 Whisper)、一个 Meilisearch 实例(可以用云服务也可以本地 Docker 拉一个)、Flutter 开发环境。
我在本地的版本大概是:Node 18+、Docker、Flutter 3.x、Supabase CLI。没有什么特殊要求,唯一建议把 Supabase CLI 升级到最新版,老版本对 Functions 部署支持不太友好。
3.2 按顺序跑通:数据库、函数、环境变量
我建议的顺序是先把数据库准备好,再部署函数,最后配密钥。SQL 里最重要的一条是启用向量扩展:
create extension if not exists vector;然后创建一张记忆表。为了说明问题,我给一个简化版定义:
create table memories ( id bigserial primary key, user_id uuid not null, description text, image_url text, lat double precision, lng double precision, created_at timestamptz default now(), embedding vector(1536) );接下来部署函数。项目根目录里一般已经有supabase/functions/insert和supabase/functions/search这样的目录。先链接你的远程项目:
supabase login supabase link --project-ref 你的项目引用ID supabase functions deploy insert supabase functions deploy search然后设置环境变量。这里容易犯迷糊的是:本地跑supabase start时读的是项目根目录的.env,而线上函数读的是云端的 Secrets,两个地方必须都配置。线上这样设:
supabase secrets set OPENAI_API_KEY=sk-你的key supabase secrets set MEILI_HOST=https://你的实例.meilisearch.io supabase secrets set MEILI_MASTER_KEY=你的主密钥缺一个环境变量,对应功能就会静默失败,而且日志不一定报错得很明显,我踩过一次,后面会详细说。
3.3 移动端运行与第一段记忆的产生
数据库和函数都部署完,最后跑移动端:
cd mobile flutter pub get flutter run首次启动要登录 Supabase 的用户体系,之后就能拍照创建记忆。等模拟器或真机上出现"记忆创建成功"的回执后,可以去 Supabase Dashboard 的memories表里看一眼,你会看到 description 已经被 GPT-4o 整理成一段干净的文字了。那一刻挺有成就感的——一张随手拍的照片,变成了一条可以被语义搜索的结构化记忆。
3.4 部署过程中我发现的两个设计巧思
跑通之后回头看,Hindsight 有两个设计细节值得抄作业。一是所有 AI 调用全放在 Edge Function,客户端拿不到 OpenAI Key,这避免把密钥直接塞进 App 里。二是用户身份通过 Supabase Auth 的 JWT 传给函数,查询时按 user_id 过滤,天然支持多用户隔离。这俩设计对一个"会被复制去生产环境"的参考项目来说,安全底线算是非常扎实了。
4. 让 Dify Agent 调用 Hindsight:一份 OpenAPI Schema 搞定长期记忆
4.1 为什么非要把 Hindsight 接进 Dify
Hindsight 原版是自带移动端的单体体验,但我实际更需要的,是在 Dify 里把 Agent 编排成一个个对外的服务。Dify 的优势在于工作流和 Agent 调度,它自带模型管理、Prompt 编排、日志追踪,但缺一块"长期记忆"能力。于是很自然的思路就出现了:把 Hindsight 的记忆检索能力封装成工具,Dify Agent 在回答问题时随时调用。这也是最近"hindsight dify"这个组合慢慢被搜起来的原因——大家需要的不是又一个演示 App,而是一个能给 Dify 用的记忆插件。
4.2 暴露一个安全的检索 HTTP 接口
Hindsight 的 search 本身是个 Edge Function,已经是一个 HTTP 接口。要让 Dify 能调用,需要确认它能接受 POST JSON,返回 JSON。我在网关层做了一层简单转发,把必要的鉴权 Header 固定成服务端专用 Key,避免 Dify 的调用者和 Supabase 的匿名用户混在一起。
这里提醒一句:如果你直接把带 service_role 的 Supabase 接口暴露给 Dify,一旦 Dify 的 API Key 泄露,攻击者就拿到了对整库数据的操作系统级别权限。建议在 Edge Function 外侧再包一层自己的鉴权逻辑,或者用 Dify 自定义工具里的 Header 鉴权功能,把敏感 Key 放在工具配置里,而不是明文拼在 URL 上。
4.3 在 Dify 里创建自定义工具
Dify 的"自定义工具"支持导入 OpenAPI Schema。我把 Hindsight 的 search 接口整理成一份精简的 schema,然后在 Dify 里选择"自定义工具"->"导入 OpenAPI Schema",再填上服务地址和鉴权 Header 就行。一个可用的 schema 大概长这样:
{ "openapi": "3.0.0", "info": { "title": "Hindsight Memory API", "version": "1.0.0" }, "paths": { "/search": { "post": { "operationId": "searchMemories", "summary": "搜索用户的现实世界记忆,适用于询问过去经历、时间、地点、事件时", "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "query": { "type": "string", "description": "用户的自然语言问题" }, "top_k": { "type": "integer", "default": 5 } } } } } }, "responses": { "200": { "description": "匹配的记忆列表", "content": { "application/json": { "schema": { "type": "object", "properties": { "memories": { "type": "array", "items": { "type": "object", "properties": { "description": { "type": "string" }, "created_at": { "type": "string" }, "location_name": { "type": "string" } } } } } } } } } } } } } }这个 schema 的 operationId 和 summary 别乱写。Dify 的 Agent 会拿这两个字段来决定"什么时候调用这个工具",summary 写得越具体,模型越容易在恰当的时候触发它。
4.4 Agent 提示词与调用测试
工具配置完成后,在 Dify 的 Agent 应用里把"Hindsight"这个工具勾选启用,然后在系统提示词里明确给它"定位"。我用的提示词版本是这样的:
你是一个拥有长期记忆的助手。当用户问起过去的经历、去过的地方、看过的照片、做过的事情时,你必须调用 Hindsight 工具搜索记忆,再结合搜索结果回答。不要臆造记忆内容。这么写以后,测试效果比我预期的好。我上传了一条语音记录"星期四晚上在望京吃了家日料",大概几个小时后在 Dify 里问"之前吃日料那家店在哪个区",Agent 会自动触发 Hindsight 工具,返回记录里的地点信息。整个链路里最核心的其实是"接口描述"——模型能不能判断该调用,就看你对工具能力的描述是否清晰。
5. 联调中踩过的坑和最终沉淀的检查清单
5.1 环境变量不生效:最隐蔽的坑
第一次部署后,我上传照片,Edge Function 日志里没有报错,但数据库里就是没有新记录。查了半天,发现是本地跑supabase serve时压根没加载我在云端设置的 Secrets。本地方案要在supabase/functions/.env里单独配一份相同的变量,云端则走 Secrets。两边配置分离,改一处忘另一处,就会出现"本地调试一切正常,线上静默失败"的灵异情况。
5.2 向量维度对不上,搜了也白搜
还有一次我把 embedding 模型换成了text-embedding-3-small的旧配置,生成出来的向量维度不一样,但表结构里写死了vector(1536)。插入向量时直接报错,检索时返回空。排查方式也很简单:用一条 SQL 查vector_dims(embedding)看维度,然后再去 API 返回里看实际维度。记住,换 embedding 模型不是只改个名字,维度、距离算法、索引类型都要重新对一遍。
5.3 中文场景下 Meilisearch 的表现
Hindsight 默认的全文检索集成对英文分词很友好,但中文记录进来后,Meilisearch 的默认分词器切出来的结果偏碎,关键词匹配效果一般。我在中文环境里最终选择让 pgvector 承担主要召回,Meilisearch 只做辅助。如果你的记忆内容是中文为主,建议把全文检索的权重调低,甚至可以先不接 Meilisearch,纯向量检索也能满足大多数需求。
5.4 联调检查清单
每次我重建这套环境,都会按下面这张表过一遍,帮你省掉来回折腾的时间:
| 检查项 | 操作动作 | 失败时的典型现象 |
|---|---|---|
| 数据库扩展 | 确认vectorextension 已创建 | 插入向量时报错 undefined column |
| embedding 维度 | select vector_dims(embedding)与 API 返回比对 | 插入失败或检索空结果 |
| Edge Function 环境变量 | 云端 Secrets 与本地.env分头配置 | 日志正常但数据不落库 |
| 服务端鉴权 | 确认 Dify 调用用的 Key 不能有全库权限 | 数据泄露风险或权限不足 |
| OpenAPI Schema 字段名 | response 字段必须与真实返回一致 | Dify 工具调用返回解析错误 |
| 工具描述 | summary 写明"什么时候用、不用会怎样" | Agent 不主动调用工具 |
5.5 最后一层思考:长期记忆真正难的是"何时记"和"何时忘"
把 Hindsight 和 Dify 接好之后,我还花了不少时间想一个问题:记忆不是越多越好。如果每张照片、每句话都变成永久记忆,用户查询时召回质量反而会下降。所以我在 Hindsight 的写入端加了一步过滤:只有描述里包含明确地点、人物、事件或时间标签的记忆才落库,其他寒暄类内容直接丢弃。在 Dify 侧,我也给 Agent 规定了"先搜索、再判断、最后回答"的顺序,避免模型直接拿 Prompt 里的老旧上下文硬答。记忆系统做到后面,难点已经从"怎么存"变成了"怎么筛选、怎么遗忘"。
如果你准备在个人项目里复刻这套方案,我的建议是先跑通 Hindsight 原版,然后只把 search 接口用 OpenAPI Schema 的方式暴露给 Dify。等这个链路稳定了,再考虑加自动摘要、记忆过期、按周生成回顾这些进阶动作。这套组合目前是我最满意的"低成本长期记忆"方案,至少不用每次对话都重新教一遍你的AI助手"我是谁、我在哪儿、我干过什么"了。