飞签记忆模型分析:三层记忆、功能画像与规则追问

这篇分析的对象是 Gitee 上的 PerFectLi7/feiqian,master 分支单次提交 311e789(2026-09-24)。作者发布的推广文章只有产品介绍,没有技术细节;记忆模型和技术栈的判断来自仓库内 4 份设计文档与 4,679 行 TypeScript/TSX/CSS 源码的逐项对照。文中的"文档承诺"和"代码实际"分开标注,不把设计稿当作已实现能力。

一、结论

飞签的记忆系统与其说是向量记忆库,不如说是三层结构的叠加:以 URL 为主键的条目记忆、把页面内容重写成站点能力的功能画像、多轮检索时维护的会话记忆。真正承担排序主力的是功能画像匹配和词面特征重合度,向量相似度只占 10%,用于兜底。把这套系统理解成"带语义召回的关键词检索 + 规则驱动的追问状态机",比理解成 RAG 更接近代码事实。

这个选择在它的场景里是合理的。用户查询通常是"把图片变成像素画的网站"这类功能描述,页面文案则是"Pixelate your photos online"这类产品语言;中文短查询配上以英文语料为主的 MiniLM 模型,向量空间里的区分度并不好。作者改用结构化功能字段加手工词表来判断,牺牲了泛化能力,换来了可解释和可调试。

成熟度需要单独说明:仓库只有一次提交,没有自动化测试,只有一份 12 条的手动验证清单,也没有声明 LICENSE。它的价值在设计和工程取舍,不在可直接复用的生产代码。

二、问题定义与设计动机

Chrome 原生历史搜索只按标题和 URL 做字面匹配,标题里没有关键词就完全找不到。飞签把这个问题拆成三层模糊:用户描述模糊("AI 聚合平台"没有区分度)、站点内容稀薄(SPA 页面正文只有一行)、类别词泛化(“AI"出现在所有 AI 站点,向量区分度极低)。设计书 V1.2 给出的判断是——不要拿用户原话直接检索,先把描述翻译成"站点能力”,再匹配。

产品层面还有两个约束。一是本地优先:历史记录比书签更敏感,嵌入计算必须在浏览器内完成,敏感域名默认排除索引。二是差异化定位:竞品要么只做历史语义搜索(MemexNG、SmrutiCortex),要么只做云端书签或标签页能力(Raindrop、ChatGPT 扩展),飞签选择"历史 + 书签 + 标签页"三库合一,并在同一次对话里做跨来源推理。

这两个约束决定了后面的技术形态。本地优先排除了服务端向量库和大模型批量打标,只能用 ONNX/WASM 小模型加 IndexedDB;三库合一则要求索引条目必须统一结构,于是有了单一 IndexEntry 承载 history、bookmark、tab 三种来源。

三、记忆模型

条目记忆:IndexEntry

索引条目的主键是 URL 的 31 进制哈希(src/background/indexer.ts:8),天然按页面去重。核心字段包括 source、embedding、summary、lastVisitTime、visitCount、typedCount、tags、folder、archived、contentExtracted 和内嵌的 profile(src/shared/types.ts:3)。

summary 有一个容易被忽略的演化过程:初次索引时它等于标题,contentExtracted=false;后台正文增强抓到内容后,它被替换为正文前 1,500 字,同时重算向量和画像。正文不足 50 字(登录页、JS 渲染页)就直接标记已处理,保留标题索引,避免反复抓取(src/background/content-enricher.ts:25)。

存储是 IndexedDB 三个 object store:entries(主键 id,索引 source/lastVisitTime)、blocklist、settings(src/core/storage/db.ts:12)。没有独立向量库,也没有 ANN 索引。

功能画像:FunctionalProfile

这是整套设计的重心。画像不描述页面写了什么主题,而描述这个站能干什么(src/shared/types.ts:22):

字段 含义 示例
functionalSummary 一句话说清能力 聚合 500+ AI 模型的统一平台,支持对话/图像/视频
actionTags 动作标签 聚合、生成、转换
inputTypes / outputTypes 输入输出类型 图片 → 像素画
siteCategory 站点类别 聚合平台、导航站、格式转换、图像工具
keyDifferentiator 关键区分特征 多模型聚合
modelCount 量化特征 500+
isFree / requiresLogin 属性判定 true / false
operationStyle 操作方式 一键、参数、批量
source 生成方式 heuristic 或 llm

画像生成分两级。索引期的 buildProfile()(src/core/profile.ts:154)完全靠启发式:12 条类别正则、一个约 30 词的 LEXICON、箭头语法(图片→像素画、PDF转Word)和操作方式正则,不调用模型,不阻塞索引。检索期才有条件升级:当前候选的前 6 条、且画像不是 LLM 生成时,调用一次 inferProfiles() 批量补全,并把结果回写 IndexedDB,source 标记为 llm,下次不再重复(src/core/memory-recall.ts:442)。

"先便宜后昂贵、按需升级、结果持久化"是这套代码里最值得借鉴的工程决策。它把 LLM 成本压到了候选集规模,而不是索引规模。

会话记忆:MemoryProfile

多轮检索时,服务端不存会话,状态由弹窗持有并逐轮回传。MemoryProfile 包含四部分(src/shared/types.ts:78):constraints 硬约束(时间范围、内容类型、主题词、站点功能、类别、区分特征、操作方式、专门/通用范围、域名片段)、candidates 当前候选、rejected 用户排除过的 id、asked 已问维度和 pending 挂起的追问。

每一轮 runMemoryTurn()(src/core/memory-recall.ts:478)只有两条路径。如果上一轮有挂起问题,就把用户回答做选项匹配,直接翻译成约束变更和候选集收缩,不重新检索;否则解析意图、向量化、执行召回、可能补全画像,再进入决策。applyChoice()(:246)把"专门做这个的"“上个月”“能批量处理”"都不是"这类回答映射为确定性的过滤动作,没有任何模型参与。

需要指出的是,这个会话对象只存在 React state 里(src/popup/App.tsx:57),关闭弹窗即丢。多轮体验被弹窗生命周期截断,这是实现层的明显短板。

检索与排序

scoreOne()(src/core/memory-recall.ts:105)的打分公式是:

score = 0.5 × 功能摘要匹配
      + 0.3 × 输入输出匹配
      + 0.1 × 站点类型匹配
      + 0.1 × 文本向量相似
      + behavior(时间衰减 ≤ 0.06 + 频次 ≤ 0.04)

其中"匹配"走的是 tokenOverlap()(src/core/profile.ts:111)计算的加权 token 命中率,不是向量运算。泛化词权重 0.2,区分词权重 1.0。泛化词表包含 ai、平台、工具、网站、在线、免费、site、tool 等;区分词表按区分度排序,包含 像素画、聚合、多模型、批量、股东、pdf 等。设计书说的"泛化词降权、区分词升权",落地形态就是一个手工词表加权,而不是 embedding 空间里的操作。

打分前先做硬过滤(:169):时间范围、域名片段、内容类型、操作方式,任一不满足直接出局。之后阈值 0.12、截断 12 条。候选再按 classifyGroup() 分成"专门/通用/其他"三组,feature ≥ 0.45 且命中类别或关键特征才算"专门"(:93)。这个分组让第一轮呈现就能按"专门工具 vs 通用工具"降维,用户只需判断类别,不用逐条比对。

追问策略

设计书描述的是"从不确定性维度里挑信息增益最大的问题问"。代码 decide()(src/core/memory-recall.ts:336)实际是一个固定优先级瀑布:

  1. 已确认目标,输出结果;
  2. 无候选,诚实告知并建议换说法;
  3. 未问过功能范围且分组数 ≥ 2,问"专门做这个的 / 通用工具 / 其他";
  4. 未问过时间且候选 > 3,问"今天 / 最近一周 / 上个月";
  5. 未问过内容形态且形态 ≥ 2、候选 > 2,问"教程 / 博客 / 新闻 / 论坛 / 工具";
  6. 未问过操作方式且 2–4 个候选存在操作差异,问"调参数 / 一键 / 批量";
  7. 候选 ≤ 3,直接呈现并提示"就是它";
  8. 已问过 ≥ 3 个维度,切时间线视图兜底;
  9. 其他情况,提示补充一个功能细节。

维度顺序就是设计书表格的顺序,没有按候选集熵动态计算信息增益。好处是零延迟、可预测、可手测;代价是遇到非典型分布时会多问一轮或过早收敛。

记忆生命周期

写入发生在安装、每次检索前、onVisited 五秒防抖和书签增删改事件;只索引 URL 与标题(src/background/service-worker.ts:25)。增强由 enrichContent(20) 负责,安装后执行一次,之后每 10 分钟处理 20 条,用 fetch 加 Readability 抽正文。遗忘是启发式规则:搜索引擎结果页、登录页、超过 18 个月未访问、仅访问 1 次且非手动输入,四类生成归档建议(src/core/forget.ts:25)。用户确认后可以选择软删除(archived=true)或硬删除。推荐则用时间指数衰减、访问频次、工具/长文分类和时段加权(src/core/recommender.ts:29)。

阶段 实现 触发方式
写入 indexHistory / indexBookmarks,只取 URL 与标题 安装、检索前、onVisited 防抖、书签事件
增强 enrichContent(20),抓正文前 1,500 字并重算向量与画像 安装后一次,之后每 10 分钟
遗忘 四类规则生成归档建议,用户确认 用户主动打开遗忘面板
删除 软删除写 archived=true;硬删除移除条目 用户在遗忘面板确认
推荐 时间衰减 + 频次 + 工具/长文分类 + 时段加权 用户打开推荐面板

这张表也暴露了冷启动问题:正文增强每 10 分钟只处理 20 条,历史记录却按 1,000 条上限一次性写入。新装用户在前几小时里拿到的,多数还是"标题 + 启发式画像"的索引,功能画像的优势要等增强跑完才体现出来。

四、技术栈与运行架构

层 选型 备注
扩展形态 Chrome/Edge Manifest V3 + Service Worker src/manifest.ts
构建 Vite 5 + @crxjs/vite-plugin 2.0-beta + TypeScript 5.5
UI React 18 + 手写 CSS,无 UI 库 弹窗单页,三种模式
本地推理 @huggingface/transformers 4.3 + onnxruntime-web WASM Xenova/all-MiniLM-L6-v2,mean pooling + normalize
推理位置 offscreen document 绕开 MV3 Service Worker 无 DOM、易被回收的限制
WASM 分发 26MB ORT wasm 打包进 public/ort/,构建期改写第三方产物屏蔽 jsdelivr vite.config.ts 的 patchOrtCdn
模型下载 env.remoteHost = 'https://hf-mirror.com',约 20MB 国内可用性取舍
存储 IndexedDB 三个 store 无 SQLite、无向量库
正文抽取 @mozilla/readability;标签页路径用 chrome.scripting.executeScript 注入自包含函数 两种路径
LLM OpenAI 兼容 /chat/completions,默认 DeepSeek 只用于意图解析、画像补全、问答
浏览器权限 history、bookmarks、tabs、scripting、offscreen、alarms host_permissions: <all_urls>

运行链路是:弹窗通过 chrome.runtime 消息访问 Service Worker;Service Worker 负责索引、正文增强和 17 种消息路由,具体逻辑放在 core/ 的纯函数域;嵌入和正文抽取转发给 offscreen document,由 transformers.js 调用本地 ORT;数据落在 IndexedDB。

Popup(React) ──chrome.runtime──> Service Worker
                                   ├─ indexer / content-enricher(定时)
                                   ├─ message-router
                                   └─ core/*(memory-recall / profile / search / ranker)
                    ↕ EMBED_TEXTS / EXTRACT_CONTENT
              Offscreen Document(transformers.js + ORT + Readability)
                    ↕
                 IndexedDB

工程上有三处值得记录的细节。第一,offscreen 文档每次先 closeDocument() 再建,并用 OFFSCREEN_PING 握手等待监听器就绪,因为 createDocument 返回时模块脚本可能还没执行到 addListener(src/core/embedder.ts:5)。第二,transformers.js 默认从 jsdelivr 拉取 ORT 工厂,在 MV3 CSP 和国内网络下不可用,所以构建期用正则改写第三方产物,强制走本地路径,同时配置 cross_origin_embedder_policy: credentialless,让 SharedArrayBuffer 可用又不拦截 hf-mirror 的模型下载。第三,每条降级路径都有兜底:向量失败转功能匹配,LLM 未配置转纯启发式,正文抓不到保留标题索引,原文定位失败明确提示。

五、设计取舍评估

为什么不让向量做主排序。 模型是 all-MiniLM-L6-v2,384 维,英文语料为主;查询是中文短语,文档侧大量是标题。两者在向量空间里的相似度区分度有限,尤其遇到"AI"“平台"这类泛化词。作者的应对没有落在模型规模上,换的是文档表征:把页面重写成"功能画像”,再用加权词面匹配。这个决策把问题从模型能力转到了数据建模,成本低、可控、可解释。代价是对未覆盖的类别和表达泛化不足,需要持续维护 LEXICON 和类别正则。

为什么画像要分两级。 全量 LLM 打标在本地扩展里不可行:成本、延迟、隐私都不允许。启发式画像覆盖大部分标题型站点,LLM 只补当前候选的前 6 条,并且结果落库。这是一个典型的"冷路径便宜、热路径精准"的分层,适合任何需要在端侧做语义增强的系统。

为什么追问是规则而不是 Agent。 本地扩展没有稳定的模型可用,追问又要求低延迟和确定性。固定决策树能保证每次问同一类问题、选项可枚举、回答可映射。代价是扩展性差:新增一个追问维度要改 decide() 的分支顺序,也没法根据候选分布动态决定先问什么。

成本与容量。 设计书引用竞品数据给出"单页索引约 4KB、检索延迟约 50ms、嵌入吞吐 1–3 条/秒",但代码没有做容量基准。实际检索是 getAllEntries() 全量读入后逐条算 cosine,万级条目尚可,十万级会明显退化。这个数字应视为设计目标,不是实测结果。

六、文档与实现的落差

设计文档承诺 代码实际 影响
WASM 向量库(hnswlib-wasm) 无向量库;全量读取后暴力 cosine(src/core/memory-recall.ts:206) 实现简单,规模上限低
多路召回,多组 query 各做向量检索 intent.queries 只参与词面匹配,embedding 只用 intent.query 一条(:534) 多路召回发生在词面层
语义向量为主 向量权重 0.1,功能画像 0.5(:140) 有意为之,中文短查询下更稳
信息增益驱动追问 固定优先级决策树(:336) 可预测,但不自适应
索引期全量 LLM 画像 只补当前候选前 6 条(:450) 召回池大时画像覆盖率不足
全量历史语义索引 getAllHistory(maxResults=1000),historyDays 从未读取(src/background/chrome-api.ts:33) 超过 1000 条静默截断,90 天限制无效
正文抽取建索引 每次 20 条、10 分钟一轮;正文 <50 字放弃;只存前 1,500 字 冷启动阶段索引质量差
cloudEnabled 隐私开关 定义了字段,全仓库无读取(src/shared/types.ts:111) 开关不生效
indexEnabled 暂停索引 只用于状态展示,索引与检索路径不检查 "暂停"不生效
本地优先、不上传 嵌入确实本地;配置 Key 后标题、摘要、查询会发给云端 LLM 宣传口径需要打折
会话记忆可延续 MemoryProfile 只在 React state 关弹窗即丢

还有一处需要单独提示:src/shared/constants.ts:42 预置了一个 LLM API Key 占位符,手动验证清单里也写明"仓库提交/分享前务必移除"。这说明作者清楚风险,但当前版本仍不适合直接分发。仓库没有 LICENSE,代码只能作为设计参考,不能直接复用。

七、借鉴与改进建议

值得借鉴的有三点。第一,换文档表征而不是换模型:当语义检索效果差时,先把文档重写成与查询同一语义层(功能、输入输出、区分特征),比扩大 embedding 模型更便宜。第二,用词表加权重承载领域先验:中文短查询下,一个几十词的区分词表加 0.2 泛化词权重,往往比 384 维英文模型更可靠。第三,懒补全加回写:贵的模型推断只对候选集做,结果持久化,索引随时间自增强。

如果要在此基础上改,我会优先做五件事:

  1. 持久化会话状态:把 MemoryProfile 落到 IndexedDB 或 chrome.storage.session,让多轮对话不依赖弹窗存活。
  2. 把追问维度配置化:用 {dimension, gain, predicate, options} 列表替代 if 瀑布,新增维度不再改主流程,也便于按候选分布计算信息增益。
  3. 改进中文分词:tokensOf() 目前只按非字母数字切分加词表命中,遇到"把图转成像素画风"会漏关键 token;换成 Intl.Segmenter 或轻量分词能明显提升召回。
  4. 补上隐私开关的主链路检查:indexEnabled 在索引和检索入口判断,cloudEnabled 在 chatCompletion 入口判断,避免开关只停留在 UI。
  5. 建立小型评估集:准备 30–50 条"功能描述 → 目标 URL"样本,记录 Top-3 命中率,再调排序权重,避免靠体感迭代。

最小可行骨架可以压缩成一条链路:URL + 标题 → 启发式功能画像 → 落库;后台异步补正文和向量;查询时做意图解析、硬过滤、画像优先打分、分组呈现;会话层用决策树追问并收缩候选。向量层可以后置,先把功能画像和词表跑通,往往比先接向量库更快看到效果。

材料与核验:本文基于 https://gitee.com/PerFectLi7/feiqian 的公开源码与设计文档,核对版本为 master @ 311e789。未运行构建、未安装扩展、未做真实检索评测;文中性能与容量结论均来自代码静态阅读或设计文档引用,未实测。上游未声明许可证,复用前需自行确认授权。


飞签记忆模型分析:三层记忆、功能画像与规则追问
https://blog.fulequn.online/2026/11/feiqian-memory-model-analysis/
作者
Fulequn
发布于
2026年11月13日
许可协议